The source code for OrphanBot's image-removal task. Requires libBot.pm and Pearle.pm.

#!/usr/bin/perl


# OrphanBot
#
# A bot to remove images from pages in preparation for deletion

use strict;
use warnings;
use utf8;

use Date::Calc qw(Delta_Days Decode_Month Month_to_Text Today);
use Getopt::Long;

use libBot;

my $homedir = '/path/to/bot/working/directory';

my $test = 0;

my $permit_interruptions = 1;	# Allow talkpage messages to stop the bot?
my $last_image = undef;
my @last_images;
my $task = "";							# One of "source", "copyright", "unsure", "special", "fairuse", "disputed"
my %users_notified;						# List of users notifed.  0, undef = no; 1 = notified once; 2 = notified and second notice
my %notifications;						# List of user,image pairs, used to ensure that no user is ever notified about an image twice.
my %dont_notify = ();						# List of users to never notify

my ($remove_type, $removal_comment, $removal_prefix, @template_match, $uploader_warning, $uploader_warning_summary, $write_remove_log, $limit_by_date); # Params for changing tasks

GetOptions('task=s' => \$task);

# Generate a signature
sub sig
{
	if($task ne 'source' and $task ne 'copyright')
	{
		return " -- ~~~~~";
	}
	else
	{
		return " ~~~~~";
	}
}

%notifications = loadNotificationList("$homedir/orphanbot.note");
%dont_notify = loadNotificationList("$homedir/orphanbot.whitelist");
Pearle::init("<INSERT BOT NAME HERE>", "<INSERT PASSWORD HERE>", "$homedir/orphanbot.log","$homedir/cookies.pearle.txt");
Pearle::config(nullOK => 1, printlevel => 4);
config(username => "<INSERT BOT NAME HERE>");

if(!Pearle::login())
{
	exit;
}


#while(1)
{
	my @images;
	my $image;
	my $edited = 0;
	my $images_removed = 0;
	
	botwarnlog("=== Beginning set at " . time() . " for task '$task' ===\n");

	{
		if($task eq "source")
		{
			my $cat = "Category:All images with unknown source";
			if($test)
			{
				@images = ("Image:Nosuchimage.jpg");
			}
			else
			{
				@images = Pearle::getCategoryImages($cat);
			}
			
			$remove_type = 'normal';
			$removal_comment = "Removing image with no source information.  Such images that are older than seven days may be deleted at any time.";
			$removal_prefix = "Unsourced image removed:";
			@template_match = ("Template:Di-no source", "Template:No copyright holder", "Template:Di-no source no license");
			$uploader_warning = "{{subst:User:OrphanBot/nosource|";
			$uploader_warning_summary = "You've uploaded an unsourced image";
			$write_remove_log = 1;
			$limit_by_date = 1;
		}
		elsif($task eq "copyright")
		{
			my $cat = "Category:All images with unknown copyright status";
			if($test)
			{
				@images = ("");
			}
			else
			{
				@images = Pearle::getCategoryImages($cat);
			}
			
			$remove_type = 'normal';
			$removal_comment = "Removing image with no copyright information.  Such images that are older than seven days may be deleted at any time.";
			$removal_prefix = "Image with unknown copyright status removed:";
			@template_match = ("Template:Di-no license", "Template:No copyright information", "Template:Di-no source no license", "Template:Don't know", "Template:No license needing editor assistance", "Template:Di-no permission");
			$uploader_warning = "{{subst:User:OrphanBot/nocopyright|";
			$uploader_warning_summary = "You've uploaded an image with unknown copyright";
			$write_remove_log = 1;
			$limit_by_date = 1;
		}
		else
		{
			Pearle::myLog(0, "Unknown task: $task\n");
			exit;
		}
	}
	
	if(scalar(@images) == 0)
	{
		Pearle::myLog(2, "Category is empty.\n");
		exit;
	}

IMAGE:	foreach $image (@images)
	{
		my $image_url;
		my $image_regex = $image;
		my $page;
		my @pages = ();
		my $page_remove_log;
		my ($day, $month, $year);
		
		Pearle::myLog(2, "Processing image $image\n");
		
		# Fetch an image page
		my $image_data = Pearle::APIQuery(titles => [$image], prop => ['imageinfo', 'categories', 'templates'],
		                                  iiprop => ['user', 'sha1', 'comment'],
		                                  cllimit => 500,
		                                  tllimit => 500,
		                                  list => 'imageusage',
		                                  iutitle => $image,
		                                  iunamespace => [0, 10, 12, 14, 100],
		                                  meta => 'userinfo',				# Do I have talkpage messages?
		                                  );
		
		next if(!defined($image_data));
		
		my $full_comment = "";

		$page_remove_log = '';
		$last_image = $image;

		if($permit_interruptions and DoIHaveMessages($image_data))
		{
			Pearle::myLog(1, "Talkpage message found; exiting on image $image.\n");
			last;
		}
		
		# Images from Commons
		if($image_data =~ /imagerepository="shared"/)
		{
			Pearle::myLog(2, "*Commons image [[:$image]] found\n");
			botwarnlog("*Commons image [[:$image]] found\n");
			next;
		}
		
		# Check for image existance
		if($image_data =~ /missing=""/)
		{
			Pearle::myLog(2, "Image [[:$image]] has been deleted.\n");
			next;
		}	

		# The odd case of an image description page without an image
		if($image_data =~ /imagerepository=""/)
		{
			Pearle::myLog(2, "*Image [[:$image]] does not appear to exist.\n");
			botwarnlog("*Image [[:$image]] does not appear to exist.\n");
			next;
		}

		# Check for image copyright tag		
		if((scalar(@template_match) > 0) and (not usesTemplate($image_data, @template_match)))
		{
			Pearle::myLog(2, "*Image [[:$image]] in category does not have an appropriate template\n");
			botwarnlog("*Image [[:$image]] in category does not have an appropriate template\n");
			next;
		}
		
		my ($raw_image) = $image =~ /Image:(.*)/;
		$raw_image = MakeWikiRegex($raw_image);
		if($image !~ /(\.jpg|\.jpeg|\.png|\.gif|\.svg)$/i)
		{
			$image_regex = "[ _]*(:?[Ii]mage|[Mm]edia)[ _]*:[ _]*${raw_image}[ _]*";
		}
		else
		{
			$image_regex = "[ _]*[Ii]mage[ _]*:[ _]*${raw_image}[ _]*";
		}
		
		# Sanity check
		if(!defined($raw_image) or $image !~ /$raw_image/)
		{
			Pearle::myLog(1, "Parse error on image [[:$image]] ($raw_image)\n");
			botwarnlog("*Parse error on image [[:$image]] ($raw_image)\n");
			last;
		}
		Pearle::myLog(2, "Image regex: $image_regex\n");


		($day, $month, $year) = getDate($image_data);

		# Notify the user
		my $uploader = GetImageUploader($image_data);
		my $is_notified = 0;
		if(defined($uploader_warning) and defined($uploader))
		{
			$is_notified = IsNotified($uploader, $image_regex, $image, \%notifications, \%dont_notify);
		}

		if(defined($uploader_warning) and !$is_notified)
		{
			if(defined($uploader))
			{
				if(!($users_notified{$uploader}))
				{
					Pearle::myLog(3, "Warning user $uploader\n");
					wikilog("User talk:$uploader", "${uploader_warning}${image}}}" . sig() . "\n", $uploader_warning_summary);
					Pearle::limit();
					$notifications{"$uploader,$image"} = 1;
					$users_notified{$uploader} = 1;
				}
				else
				{
					Pearle::myLog(3, "User $uploader has already been warned repeatedly\n");
					$users_notified{$uploader} += 1;
				}
			}
			else
			{
				Pearle::myLog(1, "Could not determine uploader for [[:$image]]\n");
			}
		}

		if(!Date::Calc::check_date($year, Decode_Month($month), $day))
		{
			Pearle::myLog(1, "Date error for image [[:$image]]\n");
			botwarnlog("*Date error for image [[:$image]]\n");
			next;
		}
		
		if((Delta_Days($year, Decode_Month($month), $day, Today() ) >= 4) or !($limit_by_date))
		{
			@pages = GetPageList($image_data);
			
			if(scalar(@pages) == 0)
			{
				Pearle::myLog(2, "Image $image may already be orphaned\n");
			}

			if(scalar(@pages) > 5)
			{
				botwarnlog("*Found image [[:$image]] on " . scalar(@pages) . " content pages\n");
			}

			foreach $page (@pages)
			{
				print "Page for removal: $page\n";
				my $parsed_removal_comment = $removal_comment;
				$parsed_removal_comment =~ s/image/[[:$image|image]]/;
					
				if(my $hits = RemoveImageFromPage($image, $page, $image_regex, $removal_prefix, $parsed_removal_comment)) 	# Don't limit if we just touched the article
				{
					$page_remove_log .= "#[[$page]]\n";
					Pearle::myLog(2, "Removed image $image from article $page $hits times\n");
					Pearle::limit();
					$edited = 1;
				}
			}
		}
		else
		{
			Pearle::myLog(2, "Recent image: notification only\n");
		}
		
		# Update image description page
		if($write_remove_log)
		{
			my $edited_idp = 0;
			my $text = "";
			# Log all removals on the image description page
			
			if($page_remove_log ne "")
			{
				$text .= "\n\nRemoved from the following pages:\n";
				$text .= FixupLinks($page_remove_log);
				$text .= "--~~~~\n";
				$full_comment .= "Listing pages that the image has been removed from";
				$edited_idp = 1;
				print "Remove log\n";
			}
			if($edited_idp)
			{
				if($test)
				{
					notelog("Edited image description page\n");
				}
				else
				{
					my $wikipage;
					
					$wikipage = Pearle::getPage( $image);
					my $pagetext = $wikipage->getEditableText();
					$pagetext .= $text;
					$wikipage->setEditableText($pagetext);
					Pearle::postPage( $wikipage, $full_comment, 0);
				}
			}
		}

#		exit if($images_removed >= 100);

		if($edited)
		{
			print "Sleeping for 30 seconds\n";
			sleep(30);
		}
		else
		{
			print "Sleeping for two seconds\n";
			sleep(2);
		}
		$edited = 0;
	}
	
	notelog("Saving notification list\n");
	saveNotificationList("/home/mark/orphanbot/orphanbot.note", %notifications);
	Pearle::myLog(2, "Finished with category.\n");
}