1
<?php
2
/*
3
 * Part of this code based on http://www.google.com/codesearch/p?hl=en#WwoXSuQk0PM/trunk/expandFns.php
4
 * Author: Smith609
5
 * License: PHP license
6
 */
7
require_once(__DIR__ . '/../Common/common.php');
8
require_once(BPATH . '/Container/CsvContainerList.php');
9
require_once(BPATH . '/UcuchaBot/Snoopy.class.php');
10
class Bot extends Snoopy {
11
	// debug level
12
	private $botdebug = 0;
13
	public $on;
14
	const api = 'https://en.wikipedia.org/w/api.php';
15
	const stddate = 'F j, Y'; // standard date format string for TFA/TFL
16
	const stdmonth = 'F Y';
17
	const mp_notice_limit = 90; // minimum number of edits for user to get notified of TFA
18
	static $Bot_commands = array(
19
		'writewp' => array('name' => 'writewp',
20
			'aka' => array('write'),
21
			'desc' => 'Write to a Wikipedia page',
22
			'arg' => 'Page to write to, and text in --text=',
23
			'execute' => 'callmethod'),
24
		'fetchwp' => array('name' => 'fetchwp',
25
			'aka' => array('fetch'),
26
			'desc' => 'Fetch the wikitext of a Wikipedia page',
27
			'arg' => 'Page to fetch',
28
			'execute' => 'callmethod'),
29
		'do_fa_bolding' => array('name' => 'do_fa_bolding',
30
			'desc' => 'Bold TFA on WP:FA (UcuchaBot task 1)',
31
			'arg' => 'None',
32
			'execute' => 'callmethod'),
33
		'do_fac_maintenance' => array('name' => 'do_fac_maintenance',
34
			'desc' => 'Perform a series of FAC maintenance tasks',
35
			'arg' => 'None',
36
			'execute' => 'callmethod'),
37
		'do_mp_notice' => array('name' => 'do_mp_notice',
38
			'desc' => 'Add notices to the authors of pages that are about to appear on the Main Page',
39
			'arg' => 'None',
40
			'execute' => 'callmethod'),
41
		'do_move_marker' => array('name' => 'do_move_marker',
42
			'desc' => 'Move the FAC marker',
43
			'arg' => 'None',
44
			'execute' => 'callmethod'),
45
		'do_wikicup_notice' => array('name' => 'do_wikicup_notice',
46
			'desc' => 'Add WikiCup notices as necessary',
47
			'arg' => 'None',
48
			'execute' => 'callmethod'),
49
		'setup_facs' => array('name' => 'setup_facs',
50
			'desc' => 'Set up the FACs database',
51
			'arg' => 'None',
52
			'execute' => 'callmethod'),
53
		'do_add_fa_stats' => array('name' => 'do_add_fa_stats',
54
			'desc' => 'Add a line to [[Wikipedia:Featured article statistics]]',
55
			'arg' => 'None',
56
			'execute' => 'callmethod'),
57
		'gettfa' => array('name' => 'gettfa',
58
			'desc' => 'Get information about the TFA for a specific day',
59
			'arg' => 'None',
60
			'execute' => 'callmethod'),
61
		'do_fanmp_update' => array('name' => 'do_fanmp_update',
62
			'desc' => 'Update [[WP:FANMP]]',
63
			'arg' => 'None',
64
			'execute' => 'callmethod'),
65
		'do_create_fa_logs' => array('name' => 'do_create_fa_logs',
66
			'desc' => 'Create monthly FA logs',
67
			'arg' => 'None',
68
			'execute' => 'callmethod'),
69
		'report_debug' => array('name' => 'report_debug',
70
			'desc' => 'Report current debug level',
71
			'arg' => 'None',
72
			'execute' => 'callmethod'),
73
		'set_debug' => array('name' => 'set_debug',
74
			'desc' => 'Set the debug level',
75
			'arg' => 'New debug level',
76
			'execute' => 'callmethod'),
77
	);
78
	/* Basic functionality */
79
	function __construct() {
80
		ini_set("user_agent", "Onkiro; onkiro@gmail.com");
81
		parent::__construct(self::$Bot_commands);
82
		$this->login();
83
	}
84
	public function cli() {
85
		$this->setup_commandline('Bot');
86
	}
87
	public static function singleton() {
88
		static $instance = NULL;
89
		if($instance === NULL) {
90
			$instance = new self();
91
		}
92
		return $instance;
93
	}
94
	/* Bot library */
95
	private function login() {
96
		require(BPATH . '/UcuchaBot/data/onkiro.php');
97
		// Set POST variables to retrieve a token
98
		$submit_vars["format"] = "json";
99
		$submit_vars["action"] = "login";
100
		$submit_vars["lgname"] = $username;
101
		$submit_vars["lgpassword"] = $password;
102
		// Submit POST variables and retrieve a token
103
		$this->submit(self::api, $submit_vars);
104
		$first_response = json_decode($this->results);
105
		$submit_vars["lgtoken"] = $first_response->login->token;
106
		// Store cookies; resubmit with new request (which has token added to post vars)
107
		foreach($this->headers as $header) {
108
			if(substr($header, 0,10) === "Set-Cookie") {
109
				$cookies = explode(";", substr($header, 12));
110
				foreach($cookies as $oCook) {
111
					$cookie = explode("=", $oCook);
112
					// ignore non-cookies
113
					if(count($cookie) !== 2)
114
						continue;
115
					$this->cookies[trim($cookie[0])] = $cookie[1];
116
				}
117
			}
118
		}
119
		$this->submit(self::api, $submit_vars);
120
		$login_result = json_decode($this->results);
121
		if($login_result->login->result === "Success") {
122
			echo("Login succeeded. Using account " . $login_result->login->lgusername . "." . PHP_EOL);
123
			// Add other cookies, which are necessary to remain logged in.
124
			$cookie_prefix = "enwiki";
125
			$this->cookies[$cookie_prefix . "UserName"] = $login_result->login->lgusername;
126
			$this->cookies[$cookie_prefix . "UserID"] = $login_result->login->lguserid;
127
			$this->cookies[$cookie_prefix . "Token"] = $login_result->login->lgtoken;
128
			$this->on = true;
129
			return true;
130
		}
131
		else {
132
			echo "Could not log in to Wikipedia servers. Edits will not be committed." . PHP_EOL;
133
			return false;
134
		}
135
	}
136
	public function writewp(array $paras) {
137
	// write to a page
138
	// @para $page String page to write to
139
		if($this->process_paras($paras, array(
140
			'name' => __FUNCTION__,
141
			'synonyms' => array(0 => 'page'),
142
			'checklist' => array(
143
				'page' => 'Page to write to',
144
				'text' => 'String text to write',
145
				'file' => 'String file containing text to write. Ignored if the text parameter is set',
146
				'summary' => 'String edit summary to be used',
147
				'override' => 'Bool whether to override debug level and edit',
148
				'kind' => 'String appendtext, prependtext, or text (default). Which of those to use.',
149
				'abortifexists' => 'Bool whether to abort the edit if the page already exists',
150
				'donotmarkasbot' => 'Bool whether to pretend we\'re not a bot (useful for talk messages)',
151
			),
152
			'default' => array(
153
				'summary' => 'Bot edit',
154
				'override' => false,
155
				'kind' => 'text',
156
				'abortifexists' => false,
157
				'donotmarkasbot' => false,
158
				'text' => false,
159
				'file' => false,
160
			),
161
			'errorifempty' => array('page'),
162
		)) === PROCESS_PARAS_ERROR_FOUND) return false;
163
		if(!$this->check_login()) return false;
164
		if($paras['text']) {
165
			$data = $paras['text'];
166
		} else if($paras['file']) {
167
			$data = file_get_contents($paras['file']);
168
		} else {
169
			echo 'No text to write to ' . $paras['page'] . PHP_EOL;
170
			return false;
171
		}
172
		$result = $this->fetchapi(array(
173
			'action' => 'query',
174
			'prop' => 'info',
175
			'intoken' => 'edit',
176
			'titles' => $paras['page'],
177
		));
178
		if(!is_array($result)) {
179
			echo "Failed to fetch page to write to: " . $paras['page'] . PHP_EOL;
180
			return false;
181
		}
182
		foreach($result['query']['pages'] as $i_page) {
183
			$my_page = $i_page;
184
		}
185
		$pagetext = $this->fetchwp($paras['page']);
186
		if(preg_match("/(\{\{(no)?bots\|deny=UcuchaBot\}\}|\{\{nobots\}\})/", $pagetext)) {
187
			echo "Edit denied to page " . $paras['page'] . PHP_EOL;
188
			return false;
189
		}
190
		if($pagetext and $paras['abortifexists']) {
191
			echo 'Page ' . $paras['page'] . ' already exists, so aborting edit.' . PHP_EOL;
192
			return false;
193
		}
194
		// prepare query
195
		$submit_vars = array();
196
		$submit_vars["action"] = "edit";
197
		$submit_vars["title"] = $my_page['title'];
198
		switch($paras['kind']) {
199
			case 'text': $submit_vars["text"] = $data; break;
200
			case 'appendtext': $submit_vars['appendtext'] = $data; break;
201
			case 'prependtext': $submit_vars['prependtext'] = $data; break;
202
			default: echo 'Error: invalid kind: ' . $paras['kind'] . PHP_EOL; break;
203
		}
204
		$submit_vars["summary"] = $paras['summary'];
205
		if(!$paras['donotmarkasbot']) {
206
			$submit_vars["minor"] = "1";
207
		}
208
		$submit_vars["bot"] = "1";
209
		$submit_vars["watchlist"] = "nochange";
210
		$submit_vars["format"] = "json";
211
		$submit_vars["token"] = $my_page['edittoken'];
212
		// submit query
213
		if($this->botdebug === 0 or $paras['override']) {
214
			// no debugging; submit edit
215
			$this->submit(self::api, $submit_vars);
216
		} else {
217
			$debuginfo = '';
218
			$debuginfo .= 'Editing to:' . PHP_EOL . self::api . PHP_EOL . PHP_EOL . 'VARIABLES' . PHP_EOL . PHP_EOL;
219
			foreach($submit_vars as $key => $value)
220
				if($key !== 'token') $debuginfo .= $key . ':' . PHP_EOL . '<nowiki>' . $value . '</nowiki>' . PHP_EOL . PHP_EOL;
221
			$debuginfo .= '------------' . PHP_EOL . PHP_EOL;
222
			echo $debuginfo;
223
			if($this->botdebug === 2) {
224
				// write to debug log
225
				$newparas['override'] = true;
226
				$newparas['text'] = $debuginfo;
227
				$newparas['kind'] = 'appendtext';
228
				$newparas['summary'] = 'Test output';
229
				$newparas['page'] = 'User:UcuchaBot/Debug';
230
				$this->writewp($newparas);
231
			}
232
			return true;
233
		}
234
		// handle result
235
		$result = json_decode($this->results);
236
		if(isset($result->edit) and $result->edit->result === "Success") {
237
			return true;
238
		} else if(isset($result->edit) and $result->edit->result) {
239
			echo 'Unsuccessful: ' . $result->edit->result . PHP_EOL;
240
			return false;
241
		} else if(isset($result->error) and $result->error->code) {
242
			// Return error code
243
			echo 'Error code: ' . $result->error->code . ": " .  $result->error->info . PHP_EOL;
244
			return false;
245
		} else {
246
			var_dump($result);
247
			echo "Unhandled error." . PHP_EOL;
248
			return false;
249
		}
250
	}
251
	private function check_login() {
252
		// check whether we're OK to proceed
253
		if(!$this->on) {
254
			return false;
255
		}
256
		// Check that bot is logged in:
257
		$this->fetch(self::api . "?action=query&prop=info&meta=userinfo&format=json");
258
		$result = json_decode($this->results);
259
		if($result->query->userinfo->id == 0) {
260
			echo "LOGGED OUT: The bot has been logged out from Wikipedia servers";
261
			return $this->login();
262
		}
263
		return true;
264
	}
265
	public function fetchwp($paras) {
266
	// fetch a page from WP
267
		if($this->process_paras($paras, array(
268
			'name' => __FUNCTION__,
269
			'toarray' => 'page',
270
			'synonyms' => array(0 => 'page'),
271
			'checklist' => array(
272
				'page' => 'Page to fetch',
273
				'action' => 'Action to fetch',
274
			),
275
			'default' => array('action' => 'raw'),
276
			'errorifempty' => array('page'),
277
		)) === PROCESS_PARAS_ERROR_FOUND) return false;
278
		if($this->botdebug > 0) {
279
			echo 'Fetching page: ' . $paras['page'] . PHP_EOL;
280
		}
281
		if(!$this->check_login()) {
282
			return false;
283
		}
284
		if(!$this->fetch("https://en.wikipedia.org/w/index.php?action=" . $paras['action'] . "&title=" . urlencode($paras['page']))) {
285
			return false;
286
		}
287
		return $this->results ?: false;
288
	}
289
	public function fetchapi(array $apiparas, array $paras = array()) {
290
		if($this->process_paras($paras, array(
291
			'name' => __FUNCTION__,
292
			'checklist' => array(
293
				'pageonly' => 'Whether to return only information about the page itself',
294
			),
295
			'default' => array('pageonly' => false),
296
		)) === PROCESS_PARAS_ERROR_FOUND) return false;
297
		$url = self::api . '?';
298
		if(!isset($apiparas['format']))
299
			$apiparas['format'] = 'json';
300
		foreach($apiparas as $key => $value)
301
			$url .= urlencode($key) . '=' . urlencode($value) . '&';
302
		// remove last &
303
		$url = substr($url, 0, -1);
304
		if($this->botdebug > 0) {
305
			echo 'Fetching page: ' . $url . PHP_EOL;
306
		}
307
		if(!$this->check_login()) return false;
308
		$this->fetch($url);
309
		$out = $this->results ?: false;
310
		if($apiparas['format'] === 'json') {
311
			$decoded = json_decode($out, true);
312
			if($paras['pageonly']) {
313
				// we usually want just the page info, which by default is hidden behind a semi-random key (the page ID)
314
				$tmp = $decoded['query']['pages'];
315
				return array_pop($tmp);
316
			} else {
317
				return $decoded;
318
			}
319
		} else {
320
			return $out;
321
		}
322
	}
323
	/* Bot tasks */
324
	/* TFA */
325
	public function do_fa_bolding() {
326
		$writepage = 'Wikipedia:Featured articles';
327
		$time = new DateTime();
328
		$date = $time->format('F j, Y');
329
		echo $date . ": finding today's TFA..." . PHP_EOL;
330
		$tfa = $this->fetchwp(array('Template:TFA title/' . $date));
331
		if(!$tfa) {
332
			echo "Could not find TFA name." . PHP_EOL;
333
			return false;
334
		}
335
		echo "Done: ". $tfa . PHP_EOL;
336
		echo "Editing $writepage... " . PHP_EOL;
337
		$wpfa = $this->fetchwp(array($writepage));
338
		$pattern = "/(?<!BeenOnMainPage\||BeenOnMainPage\|\"|BeenOnMainPage\|'')((''|\")?\[\[". preg_quote($tfa, '/') . "(\|[^\]]+)?\]\](''|\")?)/";
339
		$wpfa = preg_replace($pattern, "{{FA/BeenOnMainPage|$1}}", $wpfa);
340
		$this->writewp(array(
341
			'page' => $writepage,
342
			'text' => $wpfa,
343
			'summary' => "Bot: bolding today's featured article"
344
		));
345
		echo "done" . PHP_EOL;
346
		$this->do_fanmp_update($wpfa);
347
		return true;
348
	}
349
	public function do_fanmp_update($fetchpage = NULL) {
350
	// update [[WP:FANMP]].
351
		echo 'Updating WP:FANMP... ';
352
		$writetitle = 'Wikipedia:Featured articles that haven\'t been on the Main Page';
353
		$fetchtitle = 'Wikipedia:Featured articles';
354
		// get input pages
355
		if($fetchpage === NULL) {
356
			$fetchpage = $this->fetchwp(array($fetchtitle));
357
		}
358
		$writepage = $this->fetchwp(array($writetitle));
359
		// total articles not on main page
360
		$totalnmp = 0;
361
		// get header and footer for WP:FANMP
362
		// == indicates first section of articles
363
		$writeheader = substr($writepage, 0, strpos($writepage, '=='));
364
		// |} ends list of articles
365
		$writefooter = substr($writepage, strrpos($writepage, '|}'));
366
		$writebody = '';
367
		$falist = explode("\n", $fetchpage);
368
		$inheader = true;
369
		// number of FAs in current sublist
370
		$currlist = 0;
371
		// print the number of articles in a section; update totals
372
		$printnumber = function() use(&$currlist, &$totalnmp, &$writebody) {
373
			$totalnmp += $currlist;
374
			if($currlist === 0) $writebody .= "'''None'''\n";
375
			$writebody .= "<!-- $currlist -->\n\n";
376
			$currlist = 0;
377
		};
378
		foreach($falist as $line) {
379
			$line = trim($line);
380
			// ignore empty lines
381
			if($line === '') continue;
382
			// ignore header
383
			if($inheader and $line[0] !== '=') continue;
384
			// we got to the end
385
			if($line === '|}') {
386
				$printnumber();
387
				break;
388
			}
389
			// ignore once that have been on the MP
390
			if(strpos($line, 'FA/BeenOnMainPage') !== false)
391
				continue;
392
			// if we're in the FA list and get a link, it's an FA
393
			if(strpos($line, '[[') !== false)
394
				$currlist++;
395
			// section headers
396
			if($line[0] === '=') {
397
				if($inheader)
398
					$inheader = false;
399
				else
400
					$printnumber();
401
			}
402
			// add to body
403
			$writebody .= $line . "\n";
404
		}
405
		// clean up
406
		$writebody = preg_replace('/(==\s*\n)ยท /u', '\1', $writebody);
407
		// update total number
408
		$writeheader = preg_replace(
409
			"/(?<=''')[\d,]+(?=''' articles are listed here\.)/u",
410
			number_format($totalnmp),
411
			$writeheader
412
		);
413
		$writetext = $writeheader . $writebody . $writefooter;
414
		// commit edit
415
		$success = $this->writewp(array(
416
			'page' => $writetitle,
417
			'text' => $writetext,
418
			'summary' => "Bot: updating WP:FANMP",
419
		));
420
		if($success === false)
421
			echo "failed";
422
		else
423
			echo "done";
424
		echo PHP_EOL;
425
		return true;
426
	}
427
	public function do_mp_notice() {
428
		// GET PAGES TO HANDLE
429
		$datefile = BPATH . '/UcuchaBot/data/do_tfa_notice_mp.txt';
430
		$currdate = file_get_contents($datefile); // stored date of last TFA
431
		$date = new DateTime($currdate);
432
		$pages = array(); // pages that we are handling
433
		while(true) {
434
			echo 'Trying to locate TFA for ' . $date->format('Y-m-d') . PHP_EOL;
435
			$newpage = $this->gettfa(array('date' => $date));
436
			if(!is_array($newpage)) { // no more TFAs
437
				break;
438
			}
439
			$date->modify('+1 day');
440
			if(!$newpage['name']) {// could not find TFA name
441
				continue;
442
			}
443
			$pages[] = $newpage;
444
		}
445
		file_put_contents($datefile, $date->format(self::stddate));
446
		// HANDLE EACH PAGE
447
		foreach($pages as $page) {
448
			if(!$this->mp_notice_page($page)) {
449
				echo 'Error notifying contributors for page: ' . $page . PHP_EOL;
450
			}
451
		}
452
	}
453
	private function mp_notice_page(array $page) {
454
		echo "Notifying contributors for page " . $page['name'] . PHP_EOL;
455
		$notifiedusers = array(); // users to notify
456
		// edit TFA talk page
457
		$talkpage = $this->fetchwp(array('Talk:' . $page['name']));
458
		if(preg_match('/\|\s*maindate\s*=/u', $talkpage)) {
459
			echo 'Maindate already exists on talkpage of page ' .
460
				$page['name'] . PHP_EOL;
461
		} else {
462
			$text = preg_replace('/(\|\s*currentstatus\s*=\s*FA\s*)/u',
463
				"$1|maindate=" . $page['date'] . "\n",
464
				$talkpage,
465
				-1,
466
				$count);
467
			if($count !== 1) {
468
				echo 'Adding maindate failed for page ' . $page['name'] .
469
					PHP_EOL;
470
				var_dump($text, $count);
471
				// tell me that it failed
472
				$this->writewp(array(
473
					'page' => 'User talk:Ucucha',
474
					'kind' => 'appendtext',
475
					'summary' => 'Failed to write maindate: ' .
476
						$page['name'],
477
					'text' => "\n==Failed to write maindate on [[Talk:" .
478
						$page['name'] . "]]==\n" .
479
						' I was unable to insert a ' .
480
						'<code>|maindate=</code> on the page [[Talk:' .
481
						$page['name'] . ']]. It is TFA on ' .
482
						$page['date'] . '. Thank you! ~~~~',
483
					'donotmarkasbot' => true,
484
				));
485
			}
486
			else {
487
				$this->writewp(array(
488
					'page' => 'Talk:' . $page['name'],
489
					'text' => $text,
490
					'summary' => 'Bot edit: This page will appear as ' .
491
						'today\'s featured article in the near future',
492
				));
493
			}
494
		}
495
		// get noms
496
		$ahparas = $this->parse_ah(array('text' => $talkpage));
497
		$fac = '';
498
		for($i = 1; $i < 50; $i++) {
499
			if(!isset($ahparas['action' . $i])) {
500
				break;
501
			}
502
			if($ahparas['action' . $i] !== 'FAC') {
503
				continue;
504
			}
505
			if($ahparas['action' . $i . 'result'] !== 'promoted') {
506
				continue;
507
			}
508
			$fac = $ahparas['action' . $i . 'link'];
509
		}
510
		// if FAC was not yet archived
511
		if($fac !== '') {
512
			$this->setup_facs();
513
			$list = FacsList::singleton();
514
			$facobj = new FacsEntry(array(
515
				'name' => $fac
516
			), 'n', $list);
517
			$facobj->addnoms();
518
			if(is_array($facobj->nominators)) {
519
				foreach($facobj->nominators as $nom) {
520
					$notifiedusers[$nom] = true;
521
				}
522
			}
523
		}
524
		// get top users
525
		$paras = array(
526
			'action' => 'query',
527
			'prop' => 'revisions',
528
			'titles' => $page['name'],
529
			'rvprop' => 'user',
530
			'rvlimit' => 5000,
531
		);
532
		$api = $this->fetchapi($paras);
533
		// array key here is page ID, so unpredictable
534
		$apipage = array_pop($api['query']['pages']);
535
		$users = array();
536
		foreach($apipage['revisions'] as $rev) {
537
			if(!isset($rev['user'])) {
538
				continue;
539
			}
540
			if(!isset($users[$rev['user']])) {
541
				$users[$rev['user']] = 0;
542
			}
543
			$users[$rev['user']]++;
544
		}
545
		foreach($users as $user => $number) {
546
			if($number > self::mp_notice_limit) {
547
				$notifiedusers[$user] = true;
548
			}
549
		}
550
		$addedUsers = array();
551
		foreach($notifiedusers as $user => $bool) {
552
			$talkText = $this->fetchwp(array('page' => 'User talk:' . $user));
553
			var_dump(substr($talkText, 0, 100));
554
			if(preg_match('/^\s*#redirect\s*\[\[([^\]]+)\]\]/i', $talkText, $matches)) {
555
				$addedUsers[] = $matches[1];
556
				$notifiedusers[$user] = false;
557
			}
558
		}
559
		foreach($addedUsers as $user) {
560
			$notifiedusers[$user] = true;
561
		}
562
		var_dump($notifiedusers);
563
		// notify users
564
		foreach($notifiedusers as $user => $bool) {
565
			if(!$bool) continue;
566
			echo "Notifying user: " . $user . PHP_EOL;
567
			$this->writewp(array(
568
				'page' => 'User talk:' . $user,
569
				'text' => '{{subst:User:UcuchaBot/TFA notice' .
570
					'|page=' . $page['name'] .
571
					'|date=' . $page['date'] .
572
					'|blurb=' . $page['blurb'] .
573
					'}}',
574
				'summary' => 'Bot edit: Notice that [[' . $page['name'] .
575
					']] will appear as today\'s featured article in the ' .
576
					'near future',
577
				'kind' => 'appendtext',
578
				'donotmarkasbot' => true,
579
			));
580
		}
581
		return true;
582
	}
583
	public function do_fl_bolding() {
584
		$writepage = 'Wikipedia:Featured lists';
585
		$time = new DateTime();
586
		$date = $time->format('F j, Y');
587
		echo $date . ": finding today's TFL..." . PHP_EOL;
588
		$tfa = $this->fetchwp(array('Template:TFL title/' . $date));
589
		if(!$tfa) {
590
			echo "Could not find TFL name." . PHP_EOL;
591
			return false;
592
		}
593
		echo "Done: ". $tfa . PHP_EOL;
594
		echo "Editing $writepage..." . PHP_EOL;
595
		$wpfa = $this->fetchwp(array($writepage));
596
		$pattern = "/(?<!BeenOnMainPage\||BeenOnMainPage\|\"|BeenOnMainPage\|'')((''|\")?\[\[". preg_quote($tfa, '/') . "(\|[^\]]+)?\]\](''|\")?)/";
597
		$wpfa = preg_replace($pattern, "{{FA/BeenOnMainPage|$1}}", $wpfa);
598
		$this->writewp(array(
599
			'page' => $writepage,
600
			'text' => $wpfa,
601
			'summary' => "Bot: bolding today's featured list"
602
		));
603
		echo "Done" . PHP_EOL;
604
		return true;
605
	}
606
	/* FAC */
607
	public function do_fac_maintenance() {
608
		$this->setup_facs();
609
		$this->do_wikicup_notice();
610
		$this->do_move_marker();
611
		FacsList::singleton()->saveIfNeeded();
612
	}
613
	public function do_wikicup_notice() {
614
		echo 'Checking for WikiCup participants... ';
615
		// get WP:CUP and list of Cup participants
616
		$date = new DateTime();
617
		$year = $date->format('Y');
618
		// WikiCup does not run in November or December, so do not add notices
619
		$month = $date->format('n');
620
		if($month > 10) {
621
			echo 'We\'re not currently in WikiCup time' . PHP_EOL;
622
			return true;
623
		}
624
		$wpcup = $this->fetchwp(array('Wikipedia:WikiCup/History/' . $year));
625
		preg_match_all(
626
			'/(?<=\{\{Wikipedia:WikiCup\/Participant\d\|)[^\}]*(?=\}\})/u',
627
			$wpcup,
628
			$matches,
629
			PREG_PATTERN_ORDER
630
		);
631
		$cuppers = array();
632
		foreach($matches[0] as $person)
633
			$cuppers[] = $person;
634
		if(count($cuppers) < 1) {
635
			echo 'Unable to retrieve list of WikiCup participants' . PHP_EOL;
636
			return false;
637
		}
638
		// get FACs we need to check
639
		$tocheck = FacsList::singleton()->bfind(array(
640
			'checkedcup' => '/^$/',
641
			'quiet' => true,
642
		));
643
		$cupnoms = array();
644
		if(is_array($tocheck)) foreach($tocheck as $fac) {
645
			$cupnoms[$fac->name] = array();
646
			if(!is_array($fac->nominators)) {
647
				echo 'No nominators for FAC ' . $fac->name . PHP_EOL;
648
				continue;
649
			}
650
			foreach($fac->nominators as $nom) {
651
				if(in_array($nom, $cuppers)) {
652
					$cupnoms[$fac->name][] = $nom;
653
				}
654
			}
655
			if(count($cupnoms[$fac->name]) > 0) {
656
				$nomstring = '[[User:' . implode('|]], [[User:', 	$cupnoms[$fac->name]) . '|]]';
657
				echo 'Found WikiCup nominators for FAC: ' . $fac->name . ': ' . $nomstring . PHP_EOL;
658
				$msg = PHP_EOL . '{{subst:User:Ucucha/Cup|' . $nomstring . '}}';
659
				$this->writewp(array(
660
					'page' => $fac->name,
661
					'text' => $msg,
662
					'summary' => 'Bot adding notice that this is a WikiCup nomination',
663
					'kind' => 'appendtext',
664
				));
665
			}
666
			$fac->checkedcup = true;
667
		}
668
		echo 'done' . PHP_EOL;
669
		return true;
670
	}
671
	public function do_move_marker() {
672
		$dt = new DateTime('-14 days');
673
		$date = $dt->format('Ymd');
674
		// get FAC that older noms needs to be above
675
		$oldfacs = FacsList::singleton()->bfind(array(
676
			'date' => '<' . $date,
677
			'archived' => false,
678
			'quiet' => true,
679
		));
680
		if(!is_array($oldfacs) or count($oldfacs) === 0) return false;
681
		$newlocobj = FacsList::singleton()->largest($oldfacs, 'id', array('return' => 'object'));
682
		$newloc = $newlocobj->name;
683
		// TODO
684
		$fac = $this->fetchwp(array(FacsList::fac));
685
		preg_match('/==\s*Older nominations\s*==\s*{{([^\}]*)/u', $fac, $matches);
686
		var_dump($matches);
687
		// match started failing recently; not clear why
688
		if(!isset($matches[1])) {
689
			echo "Unable to find current marker location." . PHP_EOL;
690
			return;
691
		}
692
		$currloc = $matches[1];
693
		if($currloc === $newloc) {
694
			echo 'Marker does not need to be moved.' . PHP_EOL;
695
			return true;
696
		}
697
		$fac = preg_replace(
698
			array('/_/u',
699
				'/ +(?=\n)/u',
700
				'/(?<=\d)\s+(?=\}\})/u',
701
				'/(?<=\n)\s*==\s*Older nominations\s*==\s*\n/u',
702
			),
703
			array(' ',
704
				'',
705
				'',
706
				'',
707
			),
708
			$fac);
709
		// put in new line
710
		$fac = preg_replace(
711
			'/(?={{' . preg_quote($newloc, '/') . '}})/u',
712
			"\n== Older nominations ==\n",
713
			$fac,
714
			1,
715
			$count);
716
		if($count !== 1) {
717
			echo 'Error: could not find new location' . PHP_EOL;
718
			return false;
719
		}
720
		return $this->writewp(array(
721
			'page' => FacsList::fac,
722
			'text' => $fac,
723
			'summary' => 'Bot edit: Move marker'));
724
	}
725
	/* Miscellaneous FA */
726
	public function do_create_fa_logs() {
727
		$date = new DateTime();
728
		$month = $date->format('F Y');
729
		$writeparas = array(
730
			'text' => "{{TOClimit|3}}\n==$month==",
731
			'abortifexists' => true,
732
			'summary' => 'Bot creating new monthly log page',
733
		);
734
		$writeparas['page'] = 'Wikipedia:Featured article candidates/Featured log/' . $month;
735
		$this->writewp($writeparas);
736
		$writeparas['page'] = 'Wikipedia:Featured article candidates/Archived nominations/' . $month;
737
		$this->writewp($writeparas);
738
		$writeparas['text'] = "{{Featured list log}}\n{{TOClimit|3}}";
739
		$writeparas['page'] = 'Wikipedia:Featured list candidates/Featured log/' . $month;
740
		$this->writewp($writeparas);
741
		$writeparas['page'] = 'Wikipedia:Featured list candidates/Failed log/' . $month;
742
		$this->writewp($writeparas);
743
		$writeparas['text'] = "{{Featured list log}}\n\n==Kept==\n\n==Delisted==";
744
		$writeparas['page'] = 'Wikipedia:Featured list removal candidates/log/' . $month;
745
		$this->writewp($writeparas);
746
		return true;
747
	}
748
	public function do_add_fa_stats() {
749
		$date = new DateTime('-1 month');
750
		// date
751
		$tparas['date'] = $date->format(self::stdmonth);
752
		$tparas['date2'] = str_replace(' ', '&nbsp;', $date->format('M Y'));
753
		// WP:FA oldid
754
		$info = $this->fetchapi(array(
755
			'titles' => 'Wikipedia:Featured articles',
756
			'action' => 'query',
757
			'prop' => 'info',
758
		), array('pageonly' => true));
759
		$tparas['FAoldid'] = $info['lastrevid'];
760
		// FAs promoted
761
		$countfacs = function ($page) {
762
			$text = $this->fetchwp(array($page));
763
			if(!$text) return false;
764
			return preg_match_all('/\{\{Wikipedia:Featured article candidates\//u', $text);
765
		};
766
		$logpage = 'Wikipedia:Featured article candidates/Featured log/' . $tparas['date'];
767
		$text = $this->fetchwp(array($logpage));
768
		if(!$text) {
769
			echo 'Error: could not retrieve text of page ' . $logpage . PHP_EOL;
770
			return false;
771
		}
772
		$tparas['FAs promoted'] = preg_match_all('/\{\{Wikipedia:Featured article candidates\//u', $text, $matches);
773
		// FAs demoted
774
		$logpage = 'Wikipedia:Featured article review/archive/' . $tparas['date'];
775
		$text = $this->fetchwp(array($logpage));
776
		if(!$text) {
777
			echo 'Error: could not retrieve text of page ' . $logpage . PHP_EOL;
778
			return false;
779
		}
780
		$days = cal_days_in_month(CAL_GREGORIAN, $date->format('n'), $date->format('Y'));
781
		$difference = $tparas['FAs promoted'] - $tparas['FAs demoted'];
782
		if($difference < 0)
783
			$tparas['templateused'] = 'no';
784
		else if($difference > 2 * $days)
785
			$tparas['templateused'] = 'yes';
786
		else if($difference > $days)
787
			$tparas['templateused'] = 'yes2';
788
		else
789
			$tparas['templateused'] = 'no2';
790
		// kill stuff we don't want
791
		$text = preg_replace('/==\s*Kept status\s*==.*(?=$|==\s*Removed status\s*==)/su', '', $text);
792
		$tparas['FAs demoted'] = preg_match_all('/\{\{Wikipedia:Featured article review\//u', $text, $matches);
793
		// Current FACs
794
		$tparas['current FACs'] = FacsList::singleton()->bfind(array(
795
			'archived' => '/^$/',
796
			'return' => 'count',
797
			'quiet' => true,
798
		));
799
		$newtext = maketemplate('subst:User:UcuchaBot/FAS line', $tparas);
800
		echo $newtext . PHP_EOL;
801
	}
802
	/* Helper methods */
803
	private function parse_ah(array $paras) {
804
	// parse an article's ArticleHistory template
805
		//@para ['text'] text of article talk page
806
		//@para ['page'] page to be checked. If ['text'] is given, this is disregarded, to avoid an unnecessary API call.
807
		if(!isset($paras['text'])) {
808
			$paras['text'] = $this->fetchwp(array('Talk:' . $paras['page']));
809
			if(!isset($paras['text'])) {
810
				echo 'Could not retrieve talk page.';
811
				return false;
812
			}
813
		}
814
		if(!preg_match('/{{\s*Article\s*(h|H)istory\s*([^}]*?)\s*}}/ui', $paras['text'], $matches)) {
815
			echo 'Could not retrieve AH template' . PHP_EOL;
816
			return false;
817
		}
818
		$ahtext = $matches[1];
819
		$ahparas = array();
820
		$split1 = explode('|', $ahtext);
821
		foreach($split1 as $para) {
822
			$split2 = explode('=', $para);
823
			if(!is_array($split2)) continue;
824
			switch(count($split2)) {
825
				case 1: $ahparas[] = $split2[0]; break; // unnamed parameter
826
				case 2: $ahparas[trim($split2[0])] = trim($split2[1]); break;
827
			}
828
		}
829
		return $ahparas;
830
	}
831
	private function gettfa(array $paras = array()) {
832
	// get information about a TFA for a day
833
	// @return Array with keys
834
	// 'page' String page where the TFA is located
835
	// 'name' String name of the TFA. Set to false if name cannot be located.
836
	// 'date' String date of the TFA
837
	// 'blurb' String TFA blurb
838
		if($this->process_paras($paras, array(
839
			'name' => __FUNCTION__,
840
			'checklist' => array(
841
				'base' => 'Base page name',
842
				'date' => 'DateTime object holding date requested',
843
				'rawdate' => 'String holding date requested',
844
				'print' => 'Whether to print data about the TFA',
845
			),
846
			'default' => array(
847
				'base' => 'Wikipedia:Today\'s featured article',
848
				'print' => false,
849
				'rawdate' => false,
850
			),
851
		)) === PROCESS_PARAS_ERROR_FOUND) return false;
852
		if($paras['rawdate']) {
853
			$paras['date'] = new DateTime($paras['rawdate']);
854
		}
855
		if(!isset($paras['date']))
856
			$paras['date'] = new DateTime();
857
		else if(!$paras['date'] instanceof DateTime) {
858
			echo __METHOD__ . ': date parameter is invalid' . PHP_EOL;
859
			$paras['date'] = new DateTime();
860
		}
861
		$newpage = array();
862
		$newpage['page'] = $paras['base'] . '/' . $paras['date']->format(self::stddate);
863
		$tfatext = $this->fetchwp(array($newpage['page']));
864
		if(!$tfatext or strpos($tfatext, '{{TFAempty}}') !== false) // day hasn't been set yet
865
			return false;
866
		if(!preg_match(
867
			// this regex based on the code for Anomiebot II
868
			"/(?:'''|<b>)\s*\[\[\s*([^|\]]+?)\s*(?:\|[^]]+)?\]\]\s*('''|<\/b>)/u",
869
			$tfatext,
870
			$matches)) {
871
			echo 'Error: could not retrieve TFA name from page ' . $newpage['page'] . PHP_EOL;
872
			$newpage['name'] = false;
873
		}
874
		else {
875
			$newpage['name'] = str_replace('&nbsp;', ' ', $matches[1]);
876
		}
877
		$newpage['date'] = $paras['date']->format(self::stddate);
878
		$newpage['blurb'] = trim(preg_replace(
879
			'/(Recently featured:|\{\{TFAfooter)[^\n]+(\n|$)/u',
880
			'',
881
			$tfatext
882
		));
883
		if($paras['print']) {
884
			foreach($newpage as $key => $value)
885
				echo $key . ': ' . $value . PHP_EOL;
886
		}
887
		return $newpage;
888
	}
889
	protected function setup_facs() {
890
		FacsList::singleton()->update();
891
	}
892
	public function set_debug(array $paras) {
893
		if($this->process_paras($paras, array(
894
			'name' => __FUNCTION__,
895
			'synonyms' => array(0 => 'level'),
896
			'checklist' => array('level' => 'Level to set to'),
897
			'errorifempty' => array('level'),
898
		)) === PROCESS_PARAS_ERROR_FOUND) return false;
899
		$level = (int) $paras['level'];
900
		if($level !== 0 && $level !== 1 && $level !== 2) {
901
			echo 'Invalid debug level: ' . $level . PHP_EOL;
902
			return false;
903
		}
904
		$this->botdebug = $level;
905
		return true;
906
	}
907
	public function report_debug(array $paras = array()) {
908
		echo 'Current debugging level: ' . $this->botdebug . PHP_EOL;
909
		return $this->botdebug;
910
	}
911
}