@@ -0,0 +1,7 @@ | |||||
repos: | |||||
- repo: https://github.com/astral-sh/ruff-pre-commit | |||||
rev: v0.3.5 | |||||
hooks: | |||||
- id: ruff | |||||
args: [--fix] | |||||
- id: ruff-format |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,8 +20,10 @@ | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class AfCPending(Command): | class AfCPending(Command): | ||||
"""Link the user to the pending AfC submissions page and category.""" | """Link the user to the pending AfC submissions page and category.""" | ||||
name = "pending" | name = "pending" | ||||
commands = ["pending", "pend"] | commands = ["pending", "pend"] | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -23,8 +21,10 @@ | |||||
from earwigbot import wiki | from earwigbot import wiki | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class AfCReport(Command): | class AfCReport(Command): | ||||
"""Get information about an AfC submission by name.""" | """Get information about an AfC submission by name.""" | ||||
name = "report" | name = "report" | ||||
def process(self, data): | def process(self, data): | ||||
@@ -45,19 +45,24 @@ class AfCReport(Command): | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
return | return | ||||
title = " ".join(data.args).replace("http://en.wikipedia.org/wiki/", | |||||
"").replace("http://enwp.org/", "").strip() | |||||
title = ( | |||||
" ".join(data.args) | |||||
.replace("http://en.wikipedia.org/wiki/", "") | |||||
.replace("http://enwp.org/", "") | |||||
.strip() | |||||
) | |||||
titles = [ | titles = [ | ||||
title, "Draft:" + title, | |||||
title, | |||||
"Draft:" + title, | |||||
"Wikipedia:Articles for creation/" + title, | "Wikipedia:Articles for creation/" + title, | ||||
"Wikipedia talk:Articles for creation/" + title | |||||
"Wikipedia talk:Articles for creation/" + title, | |||||
] | ] | ||||
for attempt in titles: | for attempt in titles: | ||||
page = self.site.get_page(attempt, follow_redirects=False) | page = self.site.get_page(attempt, follow_redirects=False) | ||||
if page.exists == page.PAGE_EXISTS: | if page.exists == page.PAGE_EXISTS: | ||||
return self.report(page) | return self.report(page) | ||||
self.reply(data, "Submission \x0302{0}\x0F not found.".format(title)) | |||||
self.reply(data, f"Submission \x0302{title}\x0f not found.") | |||||
def report(self, page): | def report(self, page): | ||||
url = page.url.encode("utf8") | url = page.url.encode("utf8") | ||||
@@ -67,11 +72,11 @@ class AfCReport(Command): | |||||
user_name = user.name | user_name = user.name | ||||
user_url = user.get_talkpage().url.encode("utf8") | user_url = user.get_talkpage().url.encode("utf8") | ||||
msg1 = "AfC submission report for \x0302{0}\x0F ({1}):" | |||||
msg2 = "Status: \x0303{0}\x0F" | |||||
msg3 = "Submitted by \x0302{0}\x0F ({1})" | |||||
msg1 = "AfC submission report for \x0302{0}\x0f ({1}):" | |||||
msg2 = "Status: \x0303{0}\x0f" | |||||
msg3 = "Submitted by \x0302{0}\x0f ({1})" | |||||
if status == "accepted": | if status == "accepted": | ||||
msg3 = "Reviewed by \x0302{0}\x0F ({1})" | |||||
msg3 = "Reviewed by \x0302{0}\x0f ({1})" | |||||
self.reply(self.data, msg1.format(page.title.encode("utf8"), url)) | self.reply(self.data, msg1.format(page.title.encode("utf8"), url)) | ||||
self.say(self.data.chan, msg2.format(status)) | self.say(self.data.chan, msg2.format(status)) | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,9 +22,11 @@ import re | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class AfCStatus(Command): | class AfCStatus(Command): | ||||
"""Get the number of pending AfC submissions, open redirect requests, and | """Get the number of pending AfC submissions, open redirect requests, and | ||||
open file upload requests.""" | open file upload requests.""" | ||||
name = "status" | name = "status" | ||||
commands = ["status", "count", "num", "number"] | commands = ["status", "count", "num", "number"] | ||||
hooks = ["join", "msg"] | hooks = ["join", "msg"] | ||||
@@ -56,7 +56,7 @@ class AfCStatus(Command): | |||||
self.site = self.bot.wiki.get_site() | self.site = self.bot.wiki.get_site() | ||||
if data.line[1] == "JOIN": | if data.line[1] == "JOIN": | ||||
status = " ".join(("\x02Current status:\x0F", self.get_status())) | |||||
status = " ".join(("\x02Current status:\x0f", self.get_status())) | |||||
self.notice(data.nick, status) | self.notice(data.nick, status) | ||||
return | return | ||||
@@ -64,51 +64,56 @@ class AfCStatus(Command): | |||||
action = data.args[0].lower() | action = data.args[0].lower() | ||||
if action.startswith("sub") or action == "s": | if action.startswith("sub") or action == "s": | ||||
subs = self.count_submissions() | subs = self.count_submissions() | ||||
msg = "There are \x0305{0}\x0F pending AfC submissions (\x0302WP:AFC\x0F)." | |||||
msg = "There are \x0305{0}\x0f pending AfC submissions (\x0302WP:AFC\x0f)." | |||||
self.reply(data, msg.format(subs)) | self.reply(data, msg.format(subs)) | ||||
elif action.startswith("redir") or action == "r": | elif action.startswith("redir") or action == "r": | ||||
redirs = self.count_redirects() | redirs = self.count_redirects() | ||||
msg = "There are \x0305{0}\x0F open redirect requests (\x0302WP:AFC/R\x0F)." | |||||
msg = "There are \x0305{0}\x0f open redirect requests (\x0302WP:AFC/R\x0f)." | |||||
self.reply(data, msg.format(redirs)) | self.reply(data, msg.format(redirs)) | ||||
elif action.startswith("file") or action == "f": | elif action.startswith("file") or action == "f": | ||||
files = self.count_redirects() | files = self.count_redirects() | ||||
msg = "There are \x0305{0}\x0F open file upload requests (\x0302WP:FFU\x0F)." | |||||
msg = "There are \x0305{0}\x0f open file upload requests (\x0302WP:FFU\x0f)." | |||||
self.reply(data, msg.format(files)) | self.reply(data, msg.format(files)) | ||||
elif action.startswith("agg") or action == "a": | elif action.startswith("agg") or action == "a": | ||||
try: | try: | ||||
agg_num = int(data.args[1]) | agg_num = int(data.args[1]) | ||||
except IndexError: | except IndexError: | ||||
agg_data = (self.count_submissions(), | |||||
self.count_redirects(), self.count_files()) | |||||
agg_data = ( | |||||
self.count_submissions(), | |||||
self.count_redirects(), | |||||
self.count_files(), | |||||
) | |||||
agg_num = self.get_aggregate_number(agg_data) | agg_num = self.get_aggregate_number(agg_data) | ||||
except ValueError: | except ValueError: | ||||
msg = "\x0303{0}\x0F isn't a number!" | |||||
msg = "\x0303{0}\x0f isn't a number!" | |||||
self.reply(data, msg.format(data.args[1])) | self.reply(data, msg.format(data.args[1])) | ||||
return | return | ||||
aggregate = self.get_aggregate(agg_num) | aggregate = self.get_aggregate(agg_num) | ||||
msg = "Aggregate is \x0305{0}\x0F (AfC {1})." | |||||
msg = "Aggregate is \x0305{0}\x0f (AfC {1})." | |||||
self.reply(data, msg.format(agg_num, aggregate)) | self.reply(data, msg.format(agg_num, aggregate)) | ||||
elif action.startswith("g13_e") or action.startswith("g13e"): | elif action.startswith("g13_e") or action.startswith("g13e"): | ||||
g13_eli = self.count_g13_eligible() | g13_eli = self.count_g13_eligible() | ||||
msg = "There are \x0305{0}\x0F CSD:G13-eligible pages." | |||||
msg = "There are \x0305{0}\x0f CSD:G13-eligible pages." | |||||
self.reply(data, msg.format(g13_eli)) | self.reply(data, msg.format(g13_eli)) | ||||
elif action.startswith("g13_a") or action.startswith("g13a"): | elif action.startswith("g13_a") or action.startswith("g13a"): | ||||
g13_noms = self.count_g13_active() | g13_noms = self.count_g13_active() | ||||
msg = "There are \x0305{0}\x0F active CSD:G13 nominations." | |||||
msg = "There are \x0305{0}\x0f active CSD:G13 nominations." | |||||
self.reply(data, msg.format(g13_noms)) | self.reply(data, msg.format(g13_noms)) | ||||
elif action.startswith("nocolor") or action == "n": | elif action.startswith("nocolor") or action == "n": | ||||
self.reply(data, self.get_status(color=False)) | self.reply(data, self.get_status(color=False)) | ||||
else: | else: | ||||
msg = "Unknown argument: \x0303{0}\x0F. Valid args are " +\ | |||||
"'subs', 'redirs', 'files', 'agg', 'nocolor', " +\ | |||||
"'g13_eligible', 'g13_active'." | |||||
msg = ( | |||||
"Unknown argument: \x0303{0}\x0f. Valid args are " | |||||
+ "'subs', 'redirs', 'files', 'agg', 'nocolor', " | |||||
+ "'g13_eligible', 'g13_active'." | |||||
) | |||||
self.reply(data, msg.format(data.args[0])) | self.reply(data, msg.format(data.args[0])) | ||||
else: | else: | ||||
@@ -122,22 +127,22 @@ class AfCStatus(Command): | |||||
aggregate = self.get_aggregate(agg_num) | aggregate = self.get_aggregate(agg_num) | ||||
if color: | if color: | ||||
msg = "Articles for creation {0} (\x0302AFC\x0F: \x0305{1}\x0F; \x0302AFC/R\x0F: \x0305{2}\x0F; \x0302FFU\x0F: \x0305{3}\x0F)." | |||||
msg = "Articles for creation {0} (\x0302AFC\x0f: \x0305{1}\x0f; \x0302AFC/R\x0f: \x0305{2}\x0f; \x0302FFU\x0f: \x0305{3}\x0f)." | |||||
else: | else: | ||||
msg = "Articles for creation {0} (AFC: {1}; AFC/R: {2}; FFU: {3})." | msg = "Articles for creation {0} (AFC: {1}; AFC/R: {2}; FFU: {3})." | ||||
return msg.format(aggregate, subs, redirs, files) | return msg.format(aggregate, subs, redirs, files) | ||||
def count_g13_eligible(self): | def count_g13_eligible(self): | ||||
""" | """ | ||||
Returns the number of G13 Eligible AfC Submissions (count of | |||||
Category:G13 eligible AfC submissions) | |||||
Returns the number of G13 Eligible AfC Submissions (count of | |||||
Category:G13 eligible AfC submissions) | |||||
""" | """ | ||||
return self.site.get_category("G13 eligible AfC submissions").pages | return self.site.get_category("G13 eligible AfC submissions").pages | ||||
def count_g13_active(self): | def count_g13_active(self): | ||||
""" | """ | ||||
Returns the number of active CSD:G13 nominations ( count of | |||||
Category:Candidates for speedy deletion as abandoned AfC submissions) | |||||
Returns the number of active CSD:G13 nominations ( count of | |||||
Category:Candidates for speedy deletion as abandoned AfC submissions) | |||||
""" | """ | ||||
catname = "Candidates for speedy deletion as abandoned AfC submissions" | catname = "Candidates for speedy deletion as abandoned AfC submissions" | ||||
return self.site.get_category(catname).pages | return self.site.get_category(catname).pages | ||||
@@ -176,23 +181,24 @@ class AfCStatus(Command): | |||||
FFU (for example) indicates that our work is *not* done and the | FFU (for example) indicates that our work is *not* done and the | ||||
project-wide backlog is most certainly *not* clear.""" | project-wide backlog is most certainly *not* clear.""" | ||||
if num == 0: | if num == 0: | ||||
return "is \x02\x0303clear\x0F" | |||||
return "is \x02\x0303clear\x0f" | |||||
elif num <= 200: | elif num <= 200: | ||||
return "is \x0303almost clear\x0F" | |||||
return "is \x0303almost clear\x0f" | |||||
elif num <= 400: | elif num <= 400: | ||||
return "is \x0312normal\x0F" | |||||
return "is \x0312normal\x0f" | |||||
elif num <= 600: | elif num <= 600: | ||||
return "is \x0307lightly backlogged\x0F" | |||||
return "is \x0307lightly backlogged\x0f" | |||||
elif num <= 900: | elif num <= 900: | ||||
return "is \x0304backlogged\x0F" | |||||
return "is \x0304backlogged\x0f" | |||||
elif num <= 1200: | elif num <= 1200: | ||||
return "is \x02\x0304heavily backlogged\x0F" | |||||
return "is \x02\x0304heavily backlogged\x0f" | |||||
else: | else: | ||||
return "is \x02\x1F\x0304severely backlogged\x0F" | |||||
return "is \x02\x1f\x0304severely backlogged\x0f" | |||||
def get_aggregate_number(self, (subs, redirs, files)): | |||||
def get_aggregate_number(self, arg): | |||||
"""Returns an 'aggregate number' based on the real number of pending | """Returns an 'aggregate number' based on the real number of pending | ||||
submissions in CAT:PEND (subs), open redirect submissions in WP:AFC/R | submissions in CAT:PEND (subs), open redirect submissions in WP:AFC/R | ||||
(redirs), and open files-for-upload requests in WP:FFU (files).""" | (redirs), and open files-for-upload requests in WP:FFU (files).""" | ||||
(subs, redirs, files) = arg | |||||
num = subs + (redirs / 2) + (files / 2) | num = subs + (redirs / 2) + (files / 2) | ||||
return num | return num |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -23,8 +21,10 @@ | |||||
from earwigbot import wiki | from earwigbot import wiki | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class AfCSubmissions(Command): | class AfCSubmissions(Command): | ||||
"""Link the user directly to some pending AfC submissions.""" | """Link the user directly to some pending AfC submissions.""" | ||||
name = "submissions" | name = "submissions" | ||||
commands = ["submissions", "subs"] | commands = ["submissions", "subs"] | ||||
@@ -63,4 +63,4 @@ class AfCSubmissions(Command): | |||||
continue | continue | ||||
urls.append(member.url.encode("utf8")) | urls.append(member.url.encode("utf8")) | ||||
pages = ", ".join(urls[:number]) | pages = ", ".join(urls[:number]) | ||||
self.reply(data, "{0} pending AfC subs: {1}".format(number, pages)) | |||||
self.reply(data, f"{number} pending AfC subs: {pages}") |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -27,8 +25,10 @@ from time import time | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
from earwigbot.exceptions import APIError | from earwigbot.exceptions import APIError | ||||
class BlockMonitor(Command): | class BlockMonitor(Command): | ||||
"""Monitors for on-wiki blocked users joining a particular channel.""" | """Monitors for on-wiki blocked users joining a particular channel.""" | ||||
name = "block_monitor" | name = "block_monitor" | ||||
hooks = ["join"] | hooks = ["join"] | ||||
@@ -43,8 +43,9 @@ class BlockMonitor(Command): | |||||
self._last = None | self._last = None | ||||
def check(self, data): | def check(self, data): | ||||
return (self._monitor_chan and self._report_chan and | |||||
data.chan == self._monitor_chan) | |||||
return ( | |||||
self._monitor_chan and self._report_chan and data.chan == self._monitor_chan | |||||
) | |||||
def process(self, data): | def process(self, data): | ||||
ip = self._get_ip(data.host) | ip = self._get_ip(data.host) | ||||
@@ -61,12 +62,16 @@ class BlockMonitor(Command): | |||||
if not block: | if not block: | ||||
return | return | ||||
msg = ("\x02[{note}]\x0F Joined user \x02{nick}\x0F is {type}blocked " | |||||
"on-wiki ([[User:{user}]]) because: {reason}") | |||||
msg = ( | |||||
"\x02[{note}]\x0f Joined user \x02{nick}\x0f is {type}blocked " | |||||
"on-wiki ([[User:{user}]]) because: {reason}" | |||||
) | |||||
self.say(self._report_chan, msg.format(nick=data.nick, **block)) | self.say(self._report_chan, msg.format(nick=data.nick, **block)) | ||||
log = ("Reporting block ({note}): {nick} is [[User:{user}]], " | |||||
"{type}blocked because: {reason}") | |||||
log = ( | |||||
"Reporting block ({note}): {nick} is [[User:{user}]], " | |||||
"{type}blocked because: {reason}" | |||||
) | |||||
self.logger.info(log.format(nick=data.nick, **block)) | self.logger.info(log.format(nick=data.nick, **block)) | ||||
def _get_ip(self, host): | def _get_ip(self, host): | ||||
@@ -84,9 +89,15 @@ class BlockMonitor(Command): | |||||
site = self.bot.wiki.get_site() | site = self.bot.wiki.get_site() | ||||
try: | try: | ||||
result = site.api_query( | result = site.api_query( | ||||
action="query", list="blocks|globalblocks", bkip=ip, bgip=ip, | |||||
bklimit=1, bglimit=1, bkprop="user|reason|range", | |||||
bgprop="address|reason|range") | |||||
action="query", | |||||
list="blocks|globalblocks", | |||||
bkip=ip, | |||||
bgip=ip, | |||||
bklimit=1, | |||||
bglimit=1, | |||||
bkprop="user|reason|range", | |||||
bgprop="address|reason|range", | |||||
) | |||||
except APIError: | except APIError: | ||||
return | return | ||||
lblocks = result["query"]["blocks"] | lblocks = result["query"]["blocks"] | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,14 +19,15 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
from json import loads | from json import loads | ||||
from socket import (AF_INET, AF_INET6, error as socket_error, gethostbyname, | |||||
inet_pton) | |||||
from urllib2 import urlopen | |||||
from socket import AF_INET, AF_INET6, gethostbyname, inet_pton | |||||
from urllib.request import urlopen | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Geolocate(Command): | class Geolocate(Command): | ||||
"""Geolocate an IP address (via http://ipinfodb.com/).""" | """Geolocate an IP address (via http://ipinfodb.com/).""" | ||||
name = "geolocate" | name = "geolocate" | ||||
commands = ["geolocate", "locate", "geo", "ip"] | commands = ["geolocate", "locate", "geo", "ip"] | ||||
@@ -43,7 +42,7 @@ class Geolocate(Command): | |||||
def process(self, data): | def process(self, data): | ||||
if not self.key: | if not self.key: | ||||
msg = 'I need an API key for http://ipinfodb.com/ stored as \x0303config.commands["{0}"]["apiKey"]\x0F.' | |||||
msg = 'I need an API key for http://ipinfodb.com/ stored as \x0303config.commands["{0}"]["apiKey"]\x0f.' | |||||
log = 'Need an API key for http://ipinfodb.com/ stored as config.commands["{0}"]["apiKey"]' | log = 'Need an API key for http://ipinfodb.com/ stored as config.commands["{0}"]["apiKey"]' | ||||
self.reply(data, msg.format(self.name)) | self.reply(data, msg.format(self.name)) | ||||
self.logger.error(log.format(self.name)) | self.logger.error(log.format(self.name)) | ||||
@@ -54,12 +53,12 @@ class Geolocate(Command): | |||||
else: | else: | ||||
try: | try: | ||||
address = gethostbyname(data.host) | address = gethostbyname(data.host) | ||||
except socket_error: | |||||
msg = "Your hostname, \x0302{0}\x0F, is not an IP address!" | |||||
except OSError: | |||||
msg = "Your hostname, \x0302{0}\x0f, is not an IP address!" | |||||
self.reply(data, msg.format(data.host)) | self.reply(data, msg.format(data.host)) | ||||
return | return | ||||
if not self.is_ip(address): | if not self.is_ip(address): | ||||
msg = "\x0302{0}\x0F is not an IP address!" | |||||
msg = "\x0302{0}\x0f is not an IP address!" | |||||
self.reply(data, msg.format(address)) | self.reply(data, msg.format(address)) | ||||
return | return | ||||
@@ -74,10 +73,10 @@ class Geolocate(Command): | |||||
longitude = res["longitude"] | longitude = res["longitude"] | ||||
utcoffset = res["timeZone"] | utcoffset = res["timeZone"] | ||||
if not country and not region and not city: | if not country and not region and not city: | ||||
self.reply(data, "IP \x0302{0}\x0F not found.".format(address)) | |||||
self.reply(data, f"IP \x0302{address}\x0f not found.") | |||||
return | return | ||||
if country == "-" and region == "-" and city == "-": | if country == "-" and region == "-" and city == "-": | ||||
self.reply(data, "IP \x0302{0}\x0F is reserved.".format(address)) | |||||
self.reply(data, f"IP \x0302{address}\x0f is reserved.") | |||||
return | return | ||||
msg = "{0}, {1}, {2} ({3}, {4}), UTC {5}" | msg = "{0}, {1}, {2} ({3}, {4}), UTC {5}" | ||||
@@ -91,9 +90,9 @@ class Geolocate(Command): | |||||
""" | """ | ||||
try: | try: | ||||
inet_pton(AF_INET, address) | inet_pton(AF_INET, address) | ||||
except socket_error: | |||||
except OSError: | |||||
try: | try: | ||||
inet_pton(AF_INET6, address) | inet_pton(AF_INET6, address) | ||||
except socket_error: | |||||
except OSError: | |||||
return False | return False | ||||
return True | return True |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -26,9 +24,11 @@ import git | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Git(Command): | class Git(Command): | ||||
"""Commands to interface with configurable git repositories; use '!git' for | """Commands to interface with configurable git repositories; use '!git' for | ||||
a sub-command list.""" | a sub-command list.""" | ||||
name = "git" | name = "git" | ||||
def setup(self): | def setup(self): | ||||
@@ -78,12 +78,12 @@ class Git(Command): | |||||
elif command == "status": | elif command == "status": | ||||
self.do_status() | self.do_status() | ||||
else: # They asked us to do something we don't know | else: # They asked us to do something we don't know | ||||
msg = "Unknown argument: \x0303{0}\x0F.".format(data.args[0]) | |||||
msg = f"Unknown argument: \x0303{data.args[0]}\x0f." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def get_repos(self): | def get_repos(self): | ||||
data = self.repos.iteritems() | data = self.repos.iteritems() | ||||
repos = ["\x0302{0}\x0F ({1})".format(k, v) for k, v in data] | |||||
repos = [f"\x0302{k}\x0f ({v})" for k, v in data] | |||||
return ", ".join(repos) | return ", ".join(repos) | ||||
def get_remote(self): | def get_remote(self): | ||||
@@ -94,18 +94,18 @@ class Git(Command): | |||||
try: | try: | ||||
return getattr(self.repo.remotes, remote_name) | return getattr(self.repo.remotes, remote_name) | ||||
except AttributeError: | except AttributeError: | ||||
msg = "Unknown remote: \x0302{0}\x0F.".format(remote_name) | |||||
msg = f"Unknown remote: \x0302{remote_name}\x0f." | |||||
self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
def get_time_since(self, date): | def get_time_since(self, date): | ||||
diff = time.mktime(time.gmtime()) - date | diff = time.mktime(time.gmtime()) - date | ||||
if diff < 60: | if diff < 60: | ||||
return "{0} seconds".format(int(diff)) | |||||
return f"{int(diff)} seconds" | |||||
if diff < 60 * 60: | if diff < 60 * 60: | ||||
return "{0} minutes".format(int(diff / 60)) | |||||
return f"{int(diff / 60)} minutes" | |||||
if diff < 60 * 60 * 24: | if diff < 60 * 60 * 24: | ||||
return "{0} hours".format(int(diff / 60 / 60)) | |||||
return "{0} days".format(int(diff / 60 / 60 / 24)) | |||||
return f"{int(diff / 60 / 60)} hours" | |||||
return f"{int(diff / 60 / 60 / 24)} days" | |||||
def do_help(self): | def do_help(self): | ||||
"""Display all commands.""" | """Display all commands.""" | ||||
@@ -119,21 +119,21 @@ class Git(Command): | |||||
} | } | ||||
subcommands = "" | subcommands = "" | ||||
for key in sorted(help.keys()): | for key in sorted(help.keys()): | ||||
subcommands += "\x0303{0}\x0F ({1}), ".format(key, help[key]) | |||||
subcommands += f"\x0303{key}\x0f ({help[key]}), " | |||||
subcommands = subcommands[:-2] # Trim last comma and space | subcommands = subcommands[:-2] # Trim last comma and space | ||||
msg = "Sub-commands are: {0}; repos are: {1}. Syntax: !git \x0303subcommand\x0F \x0302repo\x0F." | |||||
msg = "Sub-commands are: {0}; repos are: {1}. Syntax: !git \x0303subcommand\x0f \x0302repo\x0f." | |||||
self.reply(self.data, msg.format(subcommands, self.get_repos())) | self.reply(self.data, msg.format(subcommands, self.get_repos())) | ||||
def do_branch(self): | def do_branch(self): | ||||
"""Get our current branch.""" | """Get our current branch.""" | ||||
branch = self.repo.active_branch.name | branch = self.repo.active_branch.name | ||||
msg = "Currently on branch \x0302{0}\x0F.".format(branch) | |||||
msg = f"Currently on branch \x0302{branch}\x0f." | |||||
self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
def do_branches(self): | def do_branches(self): | ||||
"""Get a list of branches.""" | """Get a list of branches.""" | ||||
branches = [branch.name for branch in self.repo.branches] | branches = [branch.name for branch in self.repo.branches] | ||||
msg = "Branches: \x0302{0}\x0F.".format(", ".join(branches)) | |||||
msg = "Branches: \x0302{}\x0f.".format(", ".join(branches)) | |||||
self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
def do_checkout(self): | def do_checkout(self): | ||||
@@ -146,18 +146,18 @@ class Git(Command): | |||||
current_branch = self.repo.active_branch.name | current_branch = self.repo.active_branch.name | ||||
if target == current_branch: | if target == current_branch: | ||||
msg = "Already on \x0302{0}\x0F!".format(target) | |||||
msg = f"Already on \x0302{target}\x0f!" | |||||
self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
return | return | ||||
try: | try: | ||||
ref = getattr(self.repo.branches, target) | ref = getattr(self.repo.branches, target) | ||||
except AttributeError: | except AttributeError: | ||||
msg = "Branch \x0302{0}\x0F doesn't exist!".format(target) | |||||
msg = f"Branch \x0302{target}\x0f doesn't exist!" | |||||
self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
else: | else: | ||||
ref.checkout() | ref.checkout() | ||||
ms = "Switched from branch \x0302{0}\x0F to \x0302{1}\x0F." | |||||
ms = "Switched from branch \x0302{0}\x0f to \x0302{1}\x0f." | |||||
msg = ms.format(current_branch, target) | msg = ms.format(current_branch, target) | ||||
self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
log = "{0} checked out branch {1} of {2}" | log = "{0} checked out branch {1} of {2}" | ||||
@@ -181,11 +181,11 @@ class Git(Command): | |||||
try: | try: | ||||
ref = getattr(self.repo.branches, target) | ref = getattr(self.repo.branches, target) | ||||
except AttributeError: | except AttributeError: | ||||
msg = "Branch \x0302{0}\x0F doesn't exist!".format(target) | |||||
msg = f"Branch \x0302{target}\x0f doesn't exist!" | |||||
self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
else: | else: | ||||
self.repo.git.branch("-d", ref) | self.repo.git.branch("-d", ref) | ||||
msg = "Branch \x0302{0}\x0F has been deleted locally." | |||||
msg = "Branch \x0302{0}\x0f has been deleted locally." | |||||
self.reply(self.data, msg.format(target)) | self.reply(self.data, msg.format(target)) | ||||
log = "{0} deleted branch {1} of {2}" | log = "{0} deleted branch {1} of {2}" | ||||
logmsg = log.format(self.data.nick, target, self.repo.working_dir) | logmsg = log.format(self.data.nick, target, self.repo.working_dir) | ||||
@@ -194,7 +194,7 @@ class Git(Command): | |||||
def do_pull(self): | def do_pull(self): | ||||
"""Pull from our remote repository.""" | """Pull from our remote repository.""" | ||||
branch = self.repo.active_branch.name | branch = self.repo.active_branch.name | ||||
msg = "Pulling from remote (currently on \x0302{0}\x0F)..." | |||||
msg = "Pulling from remote (currently on \x0302{0}\x0f)..." | |||||
self.reply(self.data, msg.format(branch)) | self.reply(self.data, msg.format(branch)) | ||||
remote = self.get_remote() | remote = self.get_remote() | ||||
@@ -205,16 +205,18 @@ class Git(Command): | |||||
if updated: | if updated: | ||||
branches = ", ".join([info.ref.remote_head for info in updated]) | branches = ", ".join([info.ref.remote_head for info in updated]) | ||||
msg = "Done; updates to \x0302{0}\x0F (from {1})." | |||||
msg = "Done; updates to \x0302{0}\x0f (from {1})." | |||||
self.reply(self.data, msg.format(branches, remote.url)) | self.reply(self.data, msg.format(branches, remote.url)) | ||||
log = "{0} pulled {1} of {2} (updates to {3})" | log = "{0} pulled {1} of {2} (updates to {3})" | ||||
self.logger.info(log.format(self.data.nick, remote.name, | |||||
self.repo.working_dir, branches)) | |||||
self.logger.info( | |||||
log.format(self.data.nick, remote.name, self.repo.working_dir, branches) | |||||
) | |||||
else: | else: | ||||
self.reply(self.data, "Done; no new changes.") | self.reply(self.data, "Done; no new changes.") | ||||
log = "{0} pulled {1} of {2} (no updates)" | log = "{0} pulled {1} of {2} (no updates)" | ||||
self.logger.info(log.format(self.data.nick, remote.name, | |||||
self.repo.working_dir)) | |||||
self.logger.info( | |||||
log.format(self.data.nick, remote.name, self.repo.working_dir) | |||||
) | |||||
def do_status(self): | def do_status(self): | ||||
"""Check if we have anything to pull.""" | """Check if we have anything to pull.""" | ||||
@@ -227,14 +229,18 @@ class Git(Command): | |||||
if updated: | if updated: | ||||
branches = ", ".join([info.ref.remote_head for info in updated]) | branches = ", ".join([info.ref.remote_head for info in updated]) | ||||
msg = "Last local commit was \x02{0}\x0F ago; updates to \x0302{1}\x0F." | |||||
msg = "Last local commit was \x02{0}\x0f ago; updates to \x0302{1}\x0f." | |||||
self.reply(self.data, msg.format(since, branches)) | self.reply(self.data, msg.format(since, branches)) | ||||
log = "{0} got status of {1} of {2} (updates to {3})" | log = "{0} got status of {1} of {2} (updates to {3})" | ||||
self.logger.info(log.format(self.data.nick, remote.name, | |||||
self.repo.working_dir, branches)) | |||||
self.logger.info( | |||||
log.format(self.data.nick, remote.name, self.repo.working_dir, branches) | |||||
) | |||||
else: | else: | ||||
msg = "Last commit was \x02{0}\x0F ago. Local copy is up-to-date with remote." | |||||
msg = ( | |||||
"Last commit was \x02{0}\x0f ago. Local copy is up-to-date with remote." | |||||
) | |||||
self.reply(self.data, msg.format(since)) | self.reply(self.data, msg.format(since)) | ||||
log = "{0} pulled {1} of {2} (no updates)" | log = "{0} pulled {1} of {2} (no updates)" | ||||
self.logger.info(log.format(self.data.nick, remote.name, | |||||
self.repo.working_dir)) | |||||
self.logger.info( | |||||
log.format(self.data.nick, remote.name, self.repo.working_dir) | |||||
) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2021 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2021 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,14 +19,16 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
import base64 | import base64 | ||||
import cPickle as pickle | |||||
import pickle | |||||
import re | import re | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
from earwigbot.config.permissions import User | from earwigbot.config.permissions import User | ||||
class PartWhen(Command): | class PartWhen(Command): | ||||
"""Ask the bot to part the channel when a condition is met.""" | """Ask the bot to part the channel when a condition is met.""" | ||||
name = "partwhen" | name = "partwhen" | ||||
commands = ["partwhen", "unpartwhen"] | commands = ["partwhen", "unpartwhen"] | ||||
hooks = ["join", "msg"] | hooks = ["join", "msg"] | ||||
@@ -67,54 +67,74 @@ class PartWhen(Command): | |||||
if self._conds.get(channel): | if self._conds.get(channel): | ||||
del self._conds[channel] | del self._conds[channel] | ||||
self._save_conditions() | self._save_conditions() | ||||
self.reply(data, "Cleared part conditions for {0}.".format( | |||||
"this channel" if channel == data.chan else channel)) | |||||
self.reply( | |||||
data, | |||||
"Cleared part conditions for {}.".format( | |||||
"this channel" if channel == data.chan else channel | |||||
), | |||||
) | |||||
else: | else: | ||||
self.reply(data, "No part conditions set.") | self.reply(data, "No part conditions set.") | ||||
return | return | ||||
if not args: | if not args: | ||||
conds = self._conds.get(channel, {}) | conds = self._conds.get(channel, {}) | ||||
existing = "; ".join("{0} {1}".format(cond, ", ".join(str(user) for user in users)) | |||||
for cond, users in conds.iteritems()) | |||||
existing = "; ".join( | |||||
"{} {}".format(cond, ", ".join(str(user) for user in users)) | |||||
for cond, users in conds.iteritems() | |||||
) | |||||
if existing: | if existing: | ||||
status = "Current part conditions: {0}.".format(existing) | |||||
status = f"Current part conditions: {existing}." | |||||
else: | else: | ||||
status = "No part conditions set for {0}.".format( | |||||
"this channel" if channel == data.chan else channel) | |||||
self.reply(data, "{0} Usage: !{1} [<channel>] <event> <args>...".format( | |||||
status, data.command)) | |||||
status = "No part conditions set for {}.".format( | |||||
"this channel" if channel == data.chan else channel | |||||
) | |||||
self.reply( | |||||
data, | |||||
f"{status} Usage: !{data.command} [<channel>] <event> <args>...", | |||||
) | |||||
return | return | ||||
event = args[0] | event = args[0] | ||||
args = args[1:] | args = args[1:] | ||||
if event == "join": | if event == "join": | ||||
if not args: | if not args: | ||||
self.reply(data, "Join event requires an argument for the user joining, " | |||||
"in nick!ident@host syntax.") | |||||
self.reply( | |||||
data, | |||||
"Join event requires an argument for the user joining, " | |||||
"in nick!ident@host syntax.", | |||||
) | |||||
return | return | ||||
cond = args[0] | cond = args[0] | ||||
match = re.match(r"(.*?)!(.*?)@(.*?)$", cond) | match = re.match(r"(.*?)!(.*?)@(.*?)$", cond) | ||||
if not match: | if not match: | ||||
self.reply(data, "User join pattern is invalid; should use " | |||||
"nick!ident@host syntax.") | |||||
self.reply( | |||||
data, | |||||
"User join pattern is invalid; should use " | |||||
"nick!ident@host syntax.", | |||||
) | |||||
return | return | ||||
conds = self._conds.setdefault(channel, {}).setdefault("join", []) | conds = self._conds.setdefault(channel, {}).setdefault("join", []) | ||||
conds.append(User(match.group(1), match.group(2), match.group(3))) | conds.append(User(match.group(1), match.group(2), match.group(3))) | ||||
self._save_conditions() | self._save_conditions() | ||||
self.reply(data, "Okay, I will leave {0} when {1} joins.".format( | |||||
"the channel" if channel == data.chan else channel, cond)) | |||||
self.reply( | |||||
data, | |||||
"Okay, I will leave {} when {} joins.".format( | |||||
"the channel" if channel == data.chan else channel, cond | |||||
), | |||||
) | |||||
else: | else: | ||||
self.reply(data, "Unknown event: {0} (valid events: join).".format(event)) | |||||
self.reply(data, f"Unknown event: {event} (valid events: join).") | |||||
def _handle_join(self, data): | def _handle_join(self, data): | ||||
user = User(data.nick, data.ident, data.host) | user = User(data.nick, data.ident, data.host) | ||||
conds = self._conds.get(data.chan, {}).get("join", {}) | conds = self._conds.get(data.chan, {}).get("join", {}) | ||||
for cond in conds: | for cond in conds: | ||||
if user in cond: | if user in cond: | ||||
self.logger.info("Parting {0} because {1} met join condition {2}".format( | |||||
data.chan, str(user), str(cond))) | |||||
self.part(data.chan, "Requested to leave when {0} joined".format(data.nick)) | |||||
self.logger.info( | |||||
f"Parting {data.chan} because {str(user)} met join condition {str(cond)}" | |||||
) | |||||
self.part(data.chan, f"Requested to leave when {data.nick} joined") | |||||
break | break | ||||
def _load_conditions(self): | def _load_conditions(self): | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,8 +20,10 @@ | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Praise(Command): | class Praise(Command): | ||||
"""Praise people!""" | """Praise people!""" | ||||
name = "praise" | name = "praise" | ||||
def setup(self): | def setup(self): | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,11 +18,11 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
import re | |||||
from collections import namedtuple | from collections import namedtuple | ||||
from datetime import datetime | from datetime import datetime | ||||
from difflib import ndiff | from difflib import ndiff | ||||
from Queue import Queue | |||||
import re | |||||
from queue import Queue | |||||
from threading import Thread | from threading import Thread | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
@@ -34,9 +32,11 @@ from earwigbot.wiki import constants | |||||
_Diff = namedtuple("_Diff", ["added", "removed"]) | _Diff = namedtuple("_Diff", ["added", "removed"]) | ||||
class RCMonitor(Command): | class RCMonitor(Command): | ||||
"""Monitors the recent changes feed for certain edits and reports them to a | """Monitors the recent changes feed for certain edits and reports them to a | ||||
dedicated channel.""" | dedicated channel.""" | ||||
name = "rc_monitor" | name = "rc_monitor" | ||||
commands = ["rc_monitor", "rcm"] | commands = ["rc_monitor", "rcm"] | ||||
hooks = ["msg", "rc"] | hooks = ["msg", "rc"] | ||||
@@ -46,8 +46,10 @@ class RCMonitor(Command): | |||||
self._channel = self.config.commands[self.name]["channel"] | self._channel = self.config.commands[self.name]["channel"] | ||||
except KeyError: | except KeyError: | ||||
self._channel = None | self._channel = None | ||||
log = ('Cannot use without a report channel set as ' | |||||
'config.commands["{0}"]["channel"]') | |||||
log = ( | |||||
"Cannot use without a report channel set as " | |||||
'config.commands["{0}"]["channel"]' | |||||
) | |||||
self.logger.warn(log.format(self.name)) | self.logger.warn(log.format(self.name)) | ||||
return | return | ||||
@@ -55,7 +57,7 @@ class RCMonitor(Command): | |||||
"start": datetime.utcnow(), | "start": datetime.utcnow(), | ||||
"edits": 0, | "edits": 0, | ||||
"hits": 0, | "hits": 0, | ||||
"max_backlog": 0 | |||||
"max_backlog": 0, | |||||
} | } | ||||
self._levels = {} | self._levels = {} | ||||
self._issues = {} | self._issues = {} | ||||
@@ -73,7 +75,8 @@ class RCMonitor(Command): | |||||
if not self._channel: | if not self._channel: | ||||
return | return | ||||
return isinstance(data, RC) or ( | return isinstance(data, RC) or ( | ||||
data.is_command and data.command in self.commands) | |||||
data.is_command and data.command in self.commands | |||||
) | |||||
def process(self, data): | def process(self, data): | ||||
if isinstance(data, RC): | if isinstance(data, RC): | ||||
@@ -90,11 +93,17 @@ class RCMonitor(Command): | |||||
since = self._stats["start"].strftime("%H:%M:%S, %d %B %Y") | since = self._stats["start"].strftime("%H:%M:%S, %d %B %Y") | ||||
seconds = (datetime.utcnow() - self._stats["start"]).total_seconds() | seconds = (datetime.utcnow() - self._stats["start"]).total_seconds() | ||||
rate = self._stats["edits"] / seconds | rate = self._stats["edits"] / seconds | ||||
msg = ("\x02{edits:,}\x0F edits checked since {since} " | |||||
"(\x02{rate:.2f}\x0F edits/sec); \x02{hits:,}\x0F hits; " | |||||
"\x02{qsize:,}\x0F-edit backlog (\x02{max_backlog:,}\x0F max).") | |||||
self.reply(data, msg.format( | |||||
since=since, rate=rate, qsize=self._queue.qsize(), **self._stats)) | |||||
msg = ( | |||||
"\x02{edits:,}\x0f edits checked since {since} " | |||||
"(\x02{rate:.2f}\x0f edits/sec); \x02{hits:,}\x0f hits; " | |||||
"\x02{qsize:,}\x0f-edit backlog (\x02{max_backlog:,}\x0f max)." | |||||
) | |||||
self.reply( | |||||
data, | |||||
msg.format( | |||||
since=since, rate=rate, qsize=self._queue.qsize(), **self._stats | |||||
), | |||||
) | |||||
def unload(self): | def unload(self): | ||||
self._thread.running = False | self._thread.running = False | ||||
@@ -106,17 +115,9 @@ class RCMonitor(Command): | |||||
alert = 2 | alert = 2 | ||||
urgent = 3 | urgent = 3 | ||||
self._levels = { | |||||
routine: "routine", | |||||
alert: "alert", | |||||
urgent: "URGENT" | |||||
} | |||||
self._issues = { | |||||
"g10": alert | |||||
} | |||||
self._descriptions = { | |||||
"g10": "CSD G10 nomination" | |||||
} | |||||
self._levels = {routine: "routine", alert: "alert", urgent: "URGENT"} | |||||
self._issues = {"g10": alert} | |||||
self._descriptions = {"g10": "CSD G10 nomination"} | |||||
def _get_diff(self, oldrev, newrev): | def _get_diff(self, oldrev, newrev): | ||||
"""Return the difference between two revisions. | """Return the difference between two revisions. | ||||
@@ -126,9 +127,12 @@ class RCMonitor(Command): | |||||
site = self.bot.wiki.get_site() | site = self.bot.wiki.get_site() | ||||
try: | try: | ||||
result = site.api_query( | result = site.api_query( | ||||
action="query", prop="revisions", rvprop="ids|content", | |||||
action="query", | |||||
prop="revisions", | |||||
rvprop="ids|content", | |||||
rvslots="main", | rvslots="main", | ||||
revids=(oldrev + "|" + newrev) if oldrev else newrev) | |||||
revids=(oldrev + "|" + newrev) if oldrev else newrev, | |||||
) | |||||
except APIError: | except APIError: | ||||
return None | return None | ||||
@@ -148,10 +152,12 @@ class RCMonitor(Command): | |||||
return _Diff(text.splitlines(), []) | return _Diff(text.splitlines(), []) | ||||
try: | try: | ||||
oldtext = [rv["slots"]["main"]["*"] for rv in revs | |||||
if rv["revid"] == int(oldrev)][0] | |||||
newtext = [rv["slots"]["main"]["*"] for rv in revs | |||||
if rv["revid"] == int(newrev)][0] | |||||
oldtext = [ | |||||
rv["slots"]["main"]["*"] for rv in revs if rv["revid"] == int(oldrev) | |||||
][0] | |||||
newtext = [ | |||||
rv["slots"]["main"]["*"] for rv in revs if rv["revid"] == int(newrev) | |||||
][0] | |||||
except (IndexError, KeyError): | except (IndexError, KeyError): | ||||
return None | return None | ||||
@@ -165,14 +171,20 @@ class RCMonitor(Command): | |||||
site = self.bot.wiki.get_site() | site = self.bot.wiki.get_site() | ||||
try: | try: | ||||
result = site.api_query( | result = site.api_query( | ||||
action="query", list="backlinks", blfilterredir="redirects", | |||||
blnamespace=constants.NS_TEMPLATE, bllimit=50, | |||||
bltitle="Template:" + template) | |||||
action="query", | |||||
list="backlinks", | |||||
blfilterredir="redirects", | |||||
blnamespace=constants.NS_TEMPLATE, | |||||
bllimit=50, | |||||
bltitle="Template:" + template, | |||||
) | |||||
except APIError: | except APIError: | ||||
return [] | return [] | ||||
redirs = {link["title"].split(":", 1)[1].lower() | |||||
for link in result["query"]["backlinks"]} | |||||
redirs = { | |||||
link["title"].split(":", 1)[1].lower() | |||||
for link in result["query"]["backlinks"] | |||||
} | |||||
redirs.add(template) | redirs.add(template) | ||||
return redirs | return redirs | ||||
@@ -184,9 +196,11 @@ class RCMonitor(Command): | |||||
return None | return None | ||||
self._redirects[template] = redirects | self._redirects[template] = redirects | ||||
search = "|".join(r"(template:)?" + re.escape(tmpl).replace(r"\ ", r"[ _]") | |||||
for tmpl in self._redirects[template]) | |||||
return re.compile(r"\{\{\s*(" + search + r")\s*(\||\}\})", re.U|re.I) | |||||
search = "|".join( | |||||
r"(template:)?" + re.escape(tmpl).replace(r"\ ", r"[ _]") | |||||
for tmpl in self._redirects[template] | |||||
) | |||||
return re.compile(r"\{\{\s*(" + search + r")\s*(\||\}\})", re.U | re.I) | |||||
def _evaluate_csd(self, diff): | def _evaluate_csd(self, diff): | ||||
"""Evaluate a diff for CSD tagging.""" | """Evaluate a diff for CSD tagging.""" | ||||
@@ -223,12 +237,20 @@ class RCMonitor(Command): | |||||
notify = " ".join("!rcm-" + issue for issue in report) | notify = " ".join("!rcm-" + issue for issue in report) | ||||
cmnt = rc.comment if len(rc.comment) <= 50 else rc.comment[:47] + "..." | cmnt = rc.comment if len(rc.comment) <= 50 else rc.comment[:47] + "..." | ||||
msg = ("[\x02{level}\x0F] ({descr}) [\x02{notify}\x0F]\x0306 * " | |||||
"\x0314[[\x0307{title}\x0314]]\x0306 * \x0303{user}\x0306 * " | |||||
"\x0302{url}\x0306 * \x0310{comment}") | |||||
msg = ( | |||||
"[\x02{level}\x0f] ({descr}) [\x02{notify}\x0f]\x0306 * " | |||||
"\x0314[[\x0307{title}\x0314]]\x0306 * \x0303{user}\x0306 * " | |||||
"\x0302{url}\x0306 * \x0310{comment}" | |||||
) | |||||
return msg.format( | return msg.format( | ||||
level=level, descr=descr, notify=notify, title=rc.page, | |||||
user=rc.user, url=rc.url, comment=cmnt) | |||||
level=level, | |||||
descr=descr, | |||||
notify=notify, | |||||
title=rc.page, | |||||
user=rc.user, | |||||
url=rc.url, | |||||
comment=cmnt, | |||||
) | |||||
def _handle_event(self, event): | def _handle_event(self, event): | ||||
"""Process a recent change event.""" | """Process a recent change event.""" | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,12 +19,15 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
from json import loads | from json import loads | ||||
from urllib2 import urlopen, HTTPError | |||||
from urllib.error import HTTPError | |||||
from urllib.request import urlopen | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Stars(Command): | class Stars(Command): | ||||
"""Get the number of stargazers for a given GitHub repository.""" | """Get the number of stargazers for a given GitHub repository.""" | ||||
name = "stars" | name = "stars" | ||||
commands = ["stars", "stargazers"] | commands = ["stars", "stargazers"] | ||||
API_REPOS = "https://api.github.com/repos/{repo}" | API_REPOS = "https://api.github.com/repos/{repo}" | ||||
@@ -35,7 +36,7 @@ class Stars(Command): | |||||
def process(self, data): | def process(self, data): | ||||
if not data.args: | if not data.args: | ||||
msg = "Which GitHub repository or user should I look up? Example: \x0306{0}\x0F." | |||||
msg = "Which GitHub repository or user should I look up? Example: \x0306{0}\x0f." | |||||
self.reply(data, msg.format(self.EXAMPLE)) | self.reply(data, msg.format(self.EXAMPLE)) | ||||
return | return | ||||
@@ -55,9 +56,8 @@ class Stars(Command): | |||||
count = int(repo["stargazers_count"]) | count = int(repo["stargazers_count"]) | ||||
plural = "" if count == 1 else "s" | plural = "" if count == 1 else "s" | ||||
msg = "\x0303{0}\x0F has \x02{1}\x0F stargazer{2}: {3}" | |||||
self.reply(data, msg.format( | |||||
repo["full_name"], count, plural, repo["html_url"])) | |||||
msg = "\x0303{0}\x0f has \x02{1}\x0f stargazer{2}: {3}" | |||||
self.reply(data, msg.format(repo["full_name"], count, plural, repo["html_url"])) | |||||
def handle_user(self, data, arg): | def handle_user(self, data, arg): | ||||
"""Handle !stars <user>.""" | """Handle !stars <user>.""" | ||||
@@ -71,18 +71,22 @@ class Stars(Command): | |||||
star_plural = "" if star_count == 1 else "s" | star_plural = "" if star_count == 1 else "s" | ||||
repo_plural = "" if len(repos) == 1 else "s" | repo_plural = "" if len(repos) == 1 else "s" | ||||
if len(repos) == 100: | if len(repos) == 100: | ||||
star_count = "{0}+".format(star_count) | |||||
repo_count = "{0}+".format(repo_count) | |||||
star_count = f"{star_count}+" | |||||
repo_count = f"{repo_count}+" | |||||
if len(repos) > 0: | if len(repos) > 0: | ||||
name = repos[0]["owner"]["login"] | name = repos[0]["owner"]["login"] | ||||
url = repos[0]["owner"]["html_url"] | url = repos[0]["owner"]["html_url"] | ||||
else: | else: | ||||
name = arg | name = arg | ||||
url = "https://github.com/{0}".format(name) | |||||
url = f"https://github.com/{name}" | |||||
msg = "\x0303{0}\x0F has \x02{1}\x0F stargazer{2} across \x02{3}\x0F repo{4}: {5}" | |||||
self.reply(data, msg.format( | |||||
name, star_count, star_plural, repo_count, repo_plural, url)) | |||||
msg = ( | |||||
"\x0303{0}\x0f has \x02{1}\x0f stargazer{2} across \x02{3}\x0f repo{4}: {5}" | |||||
) | |||||
self.reply( | |||||
data, | |||||
msg.format(name, star_count, star_plural, repo_count, repo_plural, url), | |||||
) | |||||
def get_repo(self, repo): | def get_repo(self, repo): | ||||
"""Return the API JSON dump for a given repository. | """Return the API JSON dump for a given repository. | ||||
@@ -1,17 +1,16 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Public domain, 2013 Legoktm; 2013, 2018 Ben Kurtovic | # Public domain, 2013 Legoktm; 2013, 2018 Ben Kurtovic | ||||
# | |||||
from json import loads | |||||
import re | import re | ||||
from urllib import quote | |||||
from urllib2 import urlopen | |||||
from json import loads | |||||
from urllib.parse import quote | |||||
from urllib.request import urlopen | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class UrbanDictionary(Command): | class UrbanDictionary(Command): | ||||
"""Get the definition of a word or phrase using Urban Dictionary.""" | """Get the definition of a word or phrase using Urban Dictionary.""" | ||||
name = "urban" | name = "urban" | ||||
commands = ["urban", "urbandictionary", "dct", "ud"] | commands = ["urban", "urbandictionary", "dct", "ud"] | ||||
@@ -34,7 +33,7 @@ class UrbanDictionary(Command): | |||||
res = loads(query) | res = loads(query) | ||||
results = res.get("list") | results = res.get("list") | ||||
if not results: | if not results: | ||||
self.reply(data, 'Sorry, no results found.') | |||||
self.reply(data, "Sorry, no results found.") | |||||
return | return | ||||
result = results[0] | result = results[0] | ||||
@@ -44,9 +43,10 @@ class UrbanDictionary(Command): | |||||
if definition and definition[-1] not in (".", "!", "?"): | if definition and definition[-1] not in (".", "!", "?"): | ||||
definition += "." | definition += "." | ||||
msg = "{0} \x02Example\x0F: {1} {2}".format( | |||||
definition.encode("utf8"), example.encode("utf8"), url) | |||||
msg = "{} \x02Example\x0f: {} {}".format( | |||||
definition.encode("utf8"), example.encode("utf8"), url | |||||
) | |||||
if self._normalize_term(result["word"]) != self._normalize_term(arg): | if self._normalize_term(result["word"]) != self._normalize_term(arg): | ||||
msg = "\x02{0}\x0F: {1}".format(result["word"].encode("utf8"), msg) | |||||
msg = "\x02{}\x0f: {}".format(result["word"].encode("utf8"), msg) | |||||
self.reply(data, msg) | self.reply(data, msg) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,13 +20,15 @@ | |||||
from datetime import datetime | from datetime import datetime | ||||
from json import loads | from json import loads | ||||
from urllib import quote | |||||
from urllib2 import urlopen | |||||
from urllib.parse import quote | |||||
from urllib.request import urlopen | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Weather(Command): | class Weather(Command): | ||||
"""Get a weather forecast (via http://www.wunderground.com/).""" | """Get a weather forecast (via http://www.wunderground.com/).""" | ||||
name = "weather" | name = "weather" | ||||
commands = ["weather", "weat", "forecast", "temperature", "temp"] | commands = ["weather", "weat", "forecast", "temperature", "temp"] | ||||
@@ -39,15 +39,15 @@ class Weather(Command): | |||||
except KeyError: | except KeyError: | ||||
self.key = None | self.key = None | ||||
addr = "http://wunderground.com/weather/api/" | addr = "http://wunderground.com/weather/api/" | ||||
config = 'config.commands["{0}"]["apiKey"]'.format(self.name) | |||||
config = f'config.commands["{self.name}"]["apiKey"]' | |||||
log = "Cannot use without an API key from {0} stored as {1}" | log = "Cannot use without an API key from {0} stored as {1}" | ||||
self.logger.warn(log.format(addr, config)) | self.logger.warn(log.format(addr, config)) | ||||
def process(self, data): | def process(self, data): | ||||
if not self.key: | if not self.key: | ||||
addr = "http://wunderground.com/weather/api/" | addr = "http://wunderground.com/weather/api/" | ||||
config = 'config.commands["{0}"]["apiKey"]'.format(self.name) | |||||
msg = "I need an API key from {0} stored as \x0303{1}\x0F." | |||||
config = f'config.commands["{self.name}"]["apiKey"]' | |||||
msg = "I need an API key from {0} stored as \x0303{1}\x0f." | |||||
log = "Need an API key from {0} stored as {1}" | log = "Need an API key from {0} stored as {1}" | ||||
self.reply(data, msg.format(addr, config)) | self.reply(data, msg.format(addr, config)) | ||||
self.logger.error(log.format(addr, config)) | self.logger.error(log.format(addr, config)) | ||||
@@ -58,21 +58,25 @@ class Weather(Command): | |||||
if permdb.has_attr(data.host, "weather"): | if permdb.has_attr(data.host, "weather"): | ||||
location = permdb.get_attr(data.host, "weather") | location = permdb.get_attr(data.host, "weather") | ||||
else: | else: | ||||
msg = " ".join(("Where do you want the weather of? You can", | |||||
"set a default with '!{0} default City,", | |||||
"State' (or 'City, Country' if non-US).")) | |||||
msg = " ".join( | |||||
( | |||||
"Where do you want the weather of? You can", | |||||
"set a default with '!{0} default City,", | |||||
"State' (or 'City, Country' if non-US).", | |||||
) | |||||
) | |||||
self.reply(data, msg.format(data.command)) | self.reply(data, msg.format(data.command)) | ||||
return | return | ||||
elif data.args[0] == "default": | elif data.args[0] == "default": | ||||
if data.args[1:]: | if data.args[1:]: | ||||
value = " ".join(data.args[1:]) | value = " ".join(data.args[1:]) | ||||
permdb.set_attr(data.host, "weather", value) | permdb.set_attr(data.host, "weather", value) | ||||
msg = "\x0302{0}\x0F's default set to \x02{1}\x0F." | |||||
msg = "\x0302{0}\x0f's default set to \x02{1}\x0f." | |||||
self.reply(data, msg.format(data.host, value)) | self.reply(data, msg.format(data.host, value)) | ||||
else: | else: | ||||
if permdb.has_attr(data.host, "weather"): | if permdb.has_attr(data.host, "weather"): | ||||
value = permdb.get_attr(data.host, "weather") | value = permdb.get_attr(data.host, "weather") | ||||
msg = "\x0302{0}\x0F's default is \x02{1}\x0F." | |||||
msg = "\x0302{0}\x0f's default is \x02{1}\x0f." | |||||
self.reply(data, msg.format(data.host, value)) | self.reply(data, msg.format(data.host, value)) | ||||
else: | else: | ||||
self.reply(data, "I need a value to set as your default.") | self.reply(data, "I need a value to set as your default.") | ||||
@@ -107,73 +111,82 @@ class Weather(Command): | |||||
"""Format the weather (as dict *data*) to be sent through IRC.""" | """Format the weather (as dict *data*) to be sent through IRC.""" | ||||
data = res["current_observation"] | data = res["current_observation"] | ||||
place = data["display_location"]["full"] | place = data["display_location"]["full"] | ||||
icon = self.get_icon(data["icon"], data["local_time_rfc822"], | |||||
res["sun_phase"]).encode("utf8") | |||||
icon = self.get_icon( | |||||
data["icon"], data["local_time_rfc822"], res["sun_phase"] | |||||
).encode("utf8") | |||||
weather = data["weather"] | weather = data["weather"] | ||||
temp_f, temp_c = data["temp_f"], data["temp_c"] | temp_f, temp_c = data["temp_f"], data["temp_c"] | ||||
humidity = data["relative_humidity"] | humidity = data["relative_humidity"] | ||||
wind_dir = data["wind_dir"] | wind_dir = data["wind_dir"] | ||||
if wind_dir in ("North", "South", "East", "West"): | if wind_dir in ("North", "South", "East", "West"): | ||||
wind_dir = wind_dir.lower() | wind_dir = wind_dir.lower() | ||||
wind = "{0} {1} mph".format(wind_dir, data["wind_mph"]) | |||||
wind = "{} {} mph".format(wind_dir, data["wind_mph"]) | |||||
if float(data["wind_gust_mph"]) > float(data["wind_mph"]): | if float(data["wind_gust_mph"]) > float(data["wind_mph"]): | ||||
wind += " ({0} mph gusts)".format(data["wind_gust_mph"]) | |||||
wind += " ({} mph gusts)".format(data["wind_gust_mph"]) | |||||
msg = "\x02{0}\x0F: {1} {2}; {3}°F ({4}°C); {5} humidity; wind {6}" | |||||
msg = "\x02{0}\x0f: {1} {2}; {3}°F ({4}°C); {5} humidity; wind {6}" | |||||
msg = msg.format(place, icon, weather, temp_f, temp_c, humidity, wind) | msg = msg.format(place, icon, weather, temp_f, temp_c, humidity, wind) | ||||
if data["precip_today_in"] and float(data["precip_today_in"]) > 0: | if data["precip_today_in"] and float(data["precip_today_in"]) > 0: | ||||
msg += "; {0}″ precipitation today".format(data["precip_today_in"]) | |||||
msg += "; {}″ precipitation today".format(data["precip_today_in"]) | |||||
if data["precip_1hr_in"] and float(data["precip_1hr_in"]) > 0: | if data["precip_1hr_in"] and float(data["precip_1hr_in"]) > 0: | ||||
msg += " ({0}″ past hour)".format(data["precip_1hr_in"]) | |||||
msg += " ({}″ past hour)".format(data["precip_1hr_in"]) | |||||
return msg | return msg | ||||
def get_icon(self, condition, local_time, sun_phase): | def get_icon(self, condition, local_time, sun_phase): | ||||
"""Return a unicode icon to describe the given weather condition.""" | """Return a unicode icon to describe the given weather condition.""" | ||||
icons = { | icons = { | ||||
"chanceflurries": u"☃", | |||||
"chancerain": u"☂", | |||||
"chancesleet": u"☃", | |||||
"chancesnow": u"☃", | |||||
"chancetstorms": u"☂", | |||||
"clear": u"☽☀", | |||||
"cloudy": u"☁", | |||||
"flurries": u"☃", | |||||
"fog": u"☁", | |||||
"hazy": u"☁", | |||||
"mostlycloudy": u"☁", | |||||
"mostlysunny": u"☽☀", | |||||
"partlycloudy": u"☁", | |||||
"partlysunny": u"☽☀", | |||||
"rain": u"☂", | |||||
"sleet": u"☃", | |||||
"snow": u"☃", | |||||
"sunny": u"☽☀", | |||||
"tstorms": u"☂", | |||||
"chanceflurries": "☃", | |||||
"chancerain": "☂", | |||||
"chancesleet": "☃", | |||||
"chancesnow": "☃", | |||||
"chancetstorms": "☂", | |||||
"clear": "☽☀", | |||||
"cloudy": "☁", | |||||
"flurries": "☃", | |||||
"fog": "☁", | |||||
"hazy": "☁", | |||||
"mostlycloudy": "☁", | |||||
"mostlysunny": "☽☀", | |||||
"partlycloudy": "☁", | |||||
"partlysunny": "☽☀", | |||||
"rain": "☂", | |||||
"sleet": "☃", | |||||
"snow": "☃", | |||||
"sunny": "☽☀", | |||||
"tstorms": "☂", | |||||
} | } | ||||
try: | try: | ||||
icon = icons[condition] | icon = icons[condition] | ||||
if len(icon) == 2: | if len(icon) == 2: | ||||
lt_no_tz = local_time.rsplit(" ", 1)[0] | lt_no_tz = local_time.rsplit(" ", 1)[0] | ||||
dt = datetime.strptime(lt_no_tz, "%a, %d %b %Y %H:%M:%S") | dt = datetime.strptime(lt_no_tz, "%a, %d %b %Y %H:%M:%S") | ||||
srise = datetime(year=dt.year, month=dt.month, day=dt.day, | |||||
hour=int(sun_phase["sunrise"]["hour"]), | |||||
minute=int(sun_phase["sunrise"]["minute"])) | |||||
sset = datetime(year=dt.year, month=dt.month, day=dt.day, | |||||
hour=int(sun_phase["sunset"]["hour"]), | |||||
minute=int(sun_phase["sunset"]["minute"])) | |||||
srise = datetime( | |||||
year=dt.year, | |||||
month=dt.month, | |||||
day=dt.day, | |||||
hour=int(sun_phase["sunrise"]["hour"]), | |||||
minute=int(sun_phase["sunrise"]["minute"]), | |||||
) | |||||
sset = datetime( | |||||
year=dt.year, | |||||
month=dt.month, | |||||
day=dt.day, | |||||
hour=int(sun_phase["sunset"]["hour"]), | |||||
minute=int(sun_phase["sunset"]["minute"]), | |||||
) | |||||
return icon[int(srise < dt < sset)] | return icon[int(srise < dt < sset)] | ||||
return icon | return icon | ||||
except KeyError: | except KeyError: | ||||
return u"?" | |||||
return "?" | |||||
def format_ambiguous_result(self, res): | def format_ambiguous_result(self, res): | ||||
"""Format a message when there are multiple possible results.""" | """Format a message when there are multiple possible results.""" | ||||
results = [] | results = [] | ||||
for place in res["response"]["results"]: | for place in res["response"]["results"]: | ||||
extra = place["state" if place["state"] else "country"] | extra = place["state" if place["state"] else "country"] | ||||
results.append("{0}, {1}".format(place["city"], extra)) | |||||
results.append("{}, {}".format(place["city"], extra)) | |||||
if len(results) > 21: | if len(results) > 21: | ||||
extra = len(results) - 20 | extra = len(results) - 20 | ||||
res = "; ".join(results[:20]) | res = "; ".join(results[:20]) | ||||
return "Did you mean: {0}... ({1} others)?".format(res, extra) | |||||
return "Did you mean: {0}?".format("; ".join(results)) | |||||
return f"Did you mean: {res}... ({extra} others)?" | |||||
return "Did you mean: {}?".format("; ".join(results)) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -25,8 +23,10 @@ from time import sleep, time | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Welcome(Command): | class Welcome(Command): | ||||
"""Welcome people who enter certain channels.""" | """Welcome people who enter certain channels.""" | ||||
name = "welcome" | name = "welcome" | ||||
commands = ["welcome", "greet"] | commands = ["welcome", "greet"] | ||||
hooks = ["join", "part", "msg"] | hooks = ["join", "part", "msg"] | ||||
@@ -76,7 +76,7 @@ class Welcome(Command): | |||||
if not data.host.startswith("gateway/web/"): | if not data.host.startswith("gateway/web/"): | ||||
return | return | ||||
t_id = "welcome-{0}-{1}".format(data.chan.replace("#", ""), data.nick) | |||||
t_id = "welcome-{}-{}".format(data.chan.replace("#", ""), data.nick) | |||||
thread = Thread(target=self._callback, name=t_id, args=(data,)) | thread = Thread(target=self._callback, name=t_id, args=(data,)) | ||||
thread.daemon = True | thread.daemon = True | ||||
thread.start() | thread.start() | ||||
@@ -107,32 +107,40 @@ class Welcome(Command): | |||||
if len(data.args) < 2: | if len(data.args) < 2: | ||||
self.reply(data, "Which channel should I disable?") | self.reply(data, "Which channel should I disable?") | ||||
elif data.args[1] in self.disabled: | elif data.args[1] in self.disabled: | ||||
msg = "Welcoming in \x02{0}\x0F is already disabled." | |||||
msg = "Welcoming in \x02{0}\x0f is already disabled." | |||||
self.reply(data, msg.format(data.args[1])) | self.reply(data, msg.format(data.args[1])) | ||||
elif data.args[1] not in self.channels: | elif data.args[1] not in self.channels: | ||||
msg = ("I'm not welcoming people in \x02{0}\x0F. " | |||||
"Only the bot owner can add new channels.") | |||||
msg = ( | |||||
"I'm not welcoming people in \x02{0}\x0f. " | |||||
"Only the bot owner can add new channels." | |||||
) | |||||
self.reply(data, msg.format(data.args[1])) | self.reply(data, msg.format(data.args[1])) | ||||
else: | else: | ||||
self.disabled.append(data.args[1]) | self.disabled.append(data.args[1]) | ||||
msg = ("Disabled welcoming in \x02{0}\x0F. Re-enable with " | |||||
"\x0306!welcome enable {0}\x0F.") | |||||
msg = ( | |||||
"Disabled welcoming in \x02{0}\x0f. Re-enable with " | |||||
"\x0306!welcome enable {0}\x0f." | |||||
) | |||||
self.reply(data, msg.format(data.args[1])) | self.reply(data, msg.format(data.args[1])) | ||||
elif data.args[0] == "enable": | elif data.args[0] == "enable": | ||||
if len(data.args) < 2: | if len(data.args) < 2: | ||||
self.reply(data, "Which channel should I enable?") | self.reply(data, "Which channel should I enable?") | ||||
elif data.args[1] not in self.disabled: | elif data.args[1] not in self.disabled: | ||||
msg = ("I don't have welcoming disabled in \x02{0}\x0F. " | |||||
"Only the bot owner can add new channels.") | |||||
msg = ( | |||||
"I don't have welcoming disabled in \x02{0}\x0f. " | |||||
"Only the bot owner can add new channels." | |||||
) | |||||
self.reply(data, msg.format(data.args[1])) | self.reply(data, msg.format(data.args[1])) | ||||
else: | else: | ||||
self.disabled.remove(data.args[1]) | self.disabled.remove(data.args[1]) | ||||
msg = "Enabled welcoming in \x02{0}\x0F." | |||||
msg = "Enabled welcoming in \x02{0}\x0f." | |||||
self.reply(data, msg.format(data.args[1])) | self.reply(data, msg.format(data.args[1])) | ||||
else: | else: | ||||
self.reply(data, "I don't understand that command.") | self.reply(data, "I don't understand that command.") | ||||
else: | else: | ||||
msg = ("This command welcomes people who enter certain channels. " | |||||
"I am welcoming people in: {0}. A bot admin can disable me " | |||||
"with \x0306!welcome disable [channel]\x0F.") | |||||
msg = ( | |||||
"This command welcomes people who enter certain channels. " | |||||
"I am welcoming people in: {0}. A bot admin can disable me " | |||||
"with \x0306!welcome disable [channel]\x0f." | |||||
) | |||||
self.reply(data, msg.format(", ".join(self.channels.keys()))) | self.reply(data, msg.format(", ".join(self.channels.keys()))) |
@@ -0,0 +1,8 @@ | |||||
[tool.ruff] | |||||
target-version = "py311" | |||||
[tool.ruff.lint] | |||||
select = ["E4", "E7", "E9", "F", "I", "UP"] | |||||
[tool.ruff.lint.isort] | |||||
known-first-party = ["earwigbot"] |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,9 +20,11 @@ | |||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
class AfCCatDelink(Task): | class AfCCatDelink(Task): | ||||
"""A task to delink mainspace categories in declined [[WP:AFC]] | """A task to delink mainspace categories in declined [[WP:AFC]] | ||||
submissions.""" | submissions.""" | ||||
name = "afc_catdelink" | name = "afc_catdelink" | ||||
number = 8 | number = 8 | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -30,9 +28,11 @@ import oursql | |||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
class AfCCopyvios(Task): | class AfCCopyvios(Task): | ||||
"""A task to check newly-edited [[WP:AFC]] submissions for copyright | """A task to check newly-edited [[WP:AFC]] submissions for copyright | ||||
violations.""" | violations.""" | ||||
name = "afc_copyvios" | name = "afc_copyvios" | ||||
number = 1 | number = 1 | ||||
@@ -44,10 +44,11 @@ class AfCCopyvios(Task): | |||||
self.max_queries = cfg.get("maxQueries", 10) | self.max_queries = cfg.get("maxQueries", 10) | ||||
self.max_time = cfg.get("maxTime", 150) | self.max_time = cfg.get("maxTime", 150) | ||||
self.cache_results = cfg.get("cacheResults", False) | self.cache_results = cfg.get("cacheResults", False) | ||||
default_summary = "Tagging suspected [[WP:COPYVIO|copyright violation]] of {url}." | |||||
default_summary = ( | |||||
"Tagging suspected [[WP:COPYVIO|copyright violation]] of {url}." | |||||
) | |||||
self.summary = self.make_summary(cfg.get("summary", default_summary)) | self.summary = self.make_summary(cfg.get("summary", default_summary)) | ||||
default_tags = [ | |||||
"Db-g12", "Db-copyvio", "Copyvio", "Copyviocore" "Copypaste"] | |||||
default_tags = ["Db-g12", "Db-copyvio", "Copyvio", "Copyviocore" "Copypaste"] | |||||
self.tags = default_tags + cfg.get("tags", []) | self.tags = default_tags + cfg.get("tags", []) | ||||
# Connection data for our SQL database: | # Connection data for our SQL database: | ||||
@@ -76,38 +77,39 @@ class AfCCopyvios(Task): | |||||
"""Detect copyvios in 'page' and add a note if any are found.""" | """Detect copyvios in 'page' and add a note if any are found.""" | ||||
title = page.title | title = page.title | ||||
if title in self.ignore_list: | if title in self.ignore_list: | ||||
msg = u"Skipping [[{0}]], in ignore list" | |||||
msg = "Skipping [[{0}]], in ignore list" | |||||
self.logger.info(msg.format(title)) | self.logger.info(msg.format(title)) | ||||
return | return | ||||
pageid = page.pageid | pageid = page.pageid | ||||
if self.has_been_processed(pageid): | if self.has_been_processed(pageid): | ||||
msg = u"Skipping [[{0}]], already processed" | |||||
msg = "Skipping [[{0}]], already processed" | |||||
self.logger.info(msg.format(title)) | self.logger.info(msg.format(title)) | ||||
return | return | ||||
code = mwparserfromhell.parse(page.get()) | code = mwparserfromhell.parse(page.get()) | ||||
if not self.is_pending(code): | if not self.is_pending(code): | ||||
msg = u"Skipping [[{0}]], not a pending submission" | |||||
msg = "Skipping [[{0}]], not a pending submission" | |||||
self.logger.info(msg.format(title)) | self.logger.info(msg.format(title)) | ||||
return | return | ||||
tag = self.is_tagged(code) | tag = self.is_tagged(code) | ||||
if tag: | if tag: | ||||
msg = u"Skipping [[{0}]], already tagged with '{1}'" | |||||
msg = "Skipping [[{0}]], already tagged with '{1}'" | |||||
self.logger.info(msg.format(title, tag)) | self.logger.info(msg.format(title, tag)) | ||||
return | return | ||||
self.logger.info(u"Checking [[{0}]]".format(title)) | |||||
result = page.copyvio_check(self.min_confidence, self.max_queries, | |||||
self.max_time) | |||||
self.logger.info(f"Checking [[{title}]]") | |||||
result = page.copyvio_check( | |||||
self.min_confidence, self.max_queries, self.max_time | |||||
) | |||||
url = result.url | url = result.url | ||||
orig_conf = "{0}%".format(round(result.confidence * 100, 2)) | |||||
orig_conf = f"{round(result.confidence * 100, 2)}%" | |||||
if result.violation: | if result.violation: | ||||
if self.handle_violation(title, page, url, orig_conf): | if self.handle_violation(title, page, url, orig_conf): | ||||
self.log_processed(pageid) | self.log_processed(pageid) | ||||
return | return | ||||
else: | else: | ||||
msg = u"No violations detected in [[{0}]] (best: {1} at {2} confidence)" | |||||
msg = "No violations detected in [[{0}]] (best: {1} at {2} confidence)" | |||||
self.logger.info(msg.format(title, url, orig_conf)) | self.logger.info(msg.format(title, url, orig_conf)) | ||||
self.log_processed(pageid) | self.log_processed(pageid) | ||||
@@ -122,22 +124,22 @@ class AfCCopyvios(Task): | |||||
content = page.get() | content = page.get() | ||||
tag = self.is_tagged(mwparserfromhell.parse(content)) | tag = self.is_tagged(mwparserfromhell.parse(content)) | ||||
if tag: | if tag: | ||||
msg = u"A violation was detected in [[{0}]], but it was tagged" | |||||
msg += u" in the mean time with '{1}' (best: {2} at {3} confidence)" | |||||
msg = "A violation was detected in [[{0}]], but it was tagged" | |||||
msg += " in the mean time with '{1}' (best: {2} at {3} confidence)" | |||||
self.logger.info(msg.format(title, tag, url, orig_conf)) | self.logger.info(msg.format(title, tag, url, orig_conf)) | ||||
return True | return True | ||||
confirm = page.copyvio_compare(url, self.min_confidence) | confirm = page.copyvio_compare(url, self.min_confidence) | ||||
new_conf = "{0}%".format(round(confirm.confidence * 100, 2)) | |||||
new_conf = f"{round(confirm.confidence * 100, 2)}%" | |||||
if not confirm.violation: | if not confirm.violation: | ||||
msg = u"A violation was detected in [[{0}]], but couldn't be confirmed." | |||||
msg += u" It may have just been edited (best: {1} at {2} -> {3} confidence)" | |||||
msg = "A violation was detected in [[{0}]], but couldn't be confirmed." | |||||
msg += " It may have just been edited (best: {1} at {2} -> {3} confidence)" | |||||
self.logger.info(msg.format(title, url, orig_conf, new_conf)) | self.logger.info(msg.format(title, url, orig_conf, new_conf)) | ||||
return True | return True | ||||
msg = u"Found violation: [[{0}]] -> {1} ({2} confidence)" | |||||
msg = "Found violation: [[{0}]] -> {1} ({2} confidence)" | |||||
self.logger.info(msg.format(title, url, new_conf)) | self.logger.info(msg.format(title, url, new_conf)) | ||||
safeurl = quote(url.encode("utf8"), safe="/:").decode("utf8") | safeurl = quote(url.encode("utf8"), safe="/:").decode("utf8") | ||||
template = u"\{\{{0}|url={1}|confidence={2}\}\}\n" | |||||
template = "\{\{{0}|url={1}|confidence={2}\}\}\n" | |||||
template = template.format(self.template, safeurl, new_conf) | template = template.format(self.template, safeurl, new_conf) | ||||
newtext = template + content | newtext = template + content | ||||
if "{url}" in self.summary: | if "{url}" in self.summary: | ||||
@@ -206,9 +208,11 @@ class AfCCopyvios(Task): | |||||
query1 = "DELETE FROM cache WHERE cache_id = ?" | query1 = "DELETE FROM cache WHERE cache_id = ?" | ||||
query2 = "INSERT INTO cache VALUES (?, DEFAULT, ?, ?)" | query2 = "INSERT INTO cache VALUES (?, DEFAULT, ?, ?)" | ||||
query3 = "INSERT INTO cache_data VALUES (DEFAULT, ?, ?, ?, ?)" | query3 = "INSERT INTO cache_data VALUES (DEFAULT, ?, ?, ?, ?)" | ||||
cache_id = buffer(sha256("1:1:" + page.get().encode("utf8")).digest()) | |||||
data = [(cache_id, source.url, source.confidence, source.skipped) | |||||
for source in result.sources] | |||||
cache_id = sha256("1:1:" + page.get().encode("utf8")).digest() | |||||
data = [ | |||||
(cache_id, source.url, source.confidence, source.skipped) | |||||
for source in result.sources | |||||
] | |||||
with self.conn.cursor() as cursor: | with self.conn.cursor() as cursor: | ||||
cursor.execute("START TRANSACTION") | cursor.execute("START TRANSACTION") | ||||
cursor.execute(query1, (cache_id,)) | cursor.execute(query1, (cache_id,)) | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,8 +22,10 @@ from datetime import datetime, timedelta | |||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
class AfCDailyCats(Task): | class AfCDailyCats(Task): | ||||
"""A task to create daily categories for [[WP:AFC]].""" | """A task to create daily categories for [[WP:AFC]].""" | ||||
name = "afc_dailycats" | name = "afc_dailycats" | ||||
number = 3 | number = 3 | ||||
@@ -33,7 +33,9 @@ class AfCDailyCats(Task): | |||||
cfg = self.config.tasks.get(self.name, {}) | cfg = self.config.tasks.get(self.name, {}) | ||||
self.prefix = cfg.get("prefix", "Category:AfC submissions by date/") | self.prefix = cfg.get("prefix", "Category:AfC submissions by date/") | ||||
self.content = cfg.get("content", "{{AfC submission category header}}") | self.content = cfg.get("content", "{{AfC submission category header}}") | ||||
default_summary = "Creating {0} category page for [[WP:AFC|Articles for creation]]." | |||||
default_summary = ( | |||||
"Creating {0} category page for [[WP:AFC|Articles for creation]]." | |||||
) | |||||
self.summary = self.make_summary(cfg.get("summary", default_summary)) | self.summary = self.make_summary(cfg.get("summary", default_summary)) | ||||
def run(self, **kwargs): | def run(self, **kwargs): | ||||
@@ -57,6 +59,6 @@ class AfCDailyCats(Task): | |||||
page = self.site.get_page(self.prefix + suffix) | page = self.site.get_page(self.prefix + suffix) | ||||
if page.exists == page.PAGE_MISSING: | if page.exists == page.PAGE_MISSING: | ||||
page.edit(self.content, self.summary.format(word)) | page.edit(self.content, self.summary.format(word)) | ||||
self.logger.info(u"Creating [[{0}]]".format(page.title)) | |||||
self.logger.info(f"Creating [[{page.title}]]") | |||||
else: | else: | ||||
self.logger.debug(u"Skipping [[{0}]], exists".format(page.title)) | |||||
self.logger.debug(f"Skipping [[{page.title}]], exists") |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,8 +20,10 @@ | |||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
class AfCHistory(Task): | class AfCHistory(Task): | ||||
"""A task to generate information about AfC submissions over time.""" | """A task to generate information about AfC submissions over time.""" | ||||
name = "afc_history" | name = "afc_history" | ||||
def setup(self): | def setup(self): | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2019 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2019 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,9 +18,9 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
import re | |||||
from collections import OrderedDict | from collections import OrderedDict | ||||
from datetime import datetime | from datetime import datetime | ||||
import re | |||||
from os.path import expanduser | from os.path import expanduser | ||||
from threading import Lock | from threading import Lock | ||||
from time import sleep | from time import sleep | ||||
@@ -30,8 +28,7 @@ from time import sleep | |||||
import mwparserfromhell | import mwparserfromhell | ||||
import oursql | import oursql | ||||
from earwigbot import exceptions | |||||
from earwigbot import wiki | |||||
from earwigbot import exceptions, wiki | |||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
_DEFAULT_PAGE_TEXT = """<noinclude><!-- You can edit anything on this page \ | _DEFAULT_PAGE_TEXT = """<noinclude><!-- You can edit anything on this page \ | ||||
@@ -47,6 +44,7 @@ templates it uses, documented in [[Template:AfC statistics/doc]]. --> | |||||
_PER_CHART_LIMIT = 1000 | _PER_CHART_LIMIT = 1000 | ||||
class AfCStatistics(Task): | class AfCStatistics(Task): | ||||
"""A task to generate statistics for WikiProject Articles for Creation. | """A task to generate statistics for WikiProject Articles for Creation. | ||||
@@ -55,6 +53,7 @@ class AfCStatistics(Task): | |||||
every four minutes and saved once an hour, on the hour, to subpages of | every four minutes and saved once an hour, on the hour, to subpages of | ||||
self.pageroot. In the live bot, this is "Template:AfC statistics". | self.pageroot. In the live bot, this is "Template:AfC statistics". | ||||
""" | """ | ||||
name = "afc_statistics" | name = "afc_statistics" | ||||
number = 2 | number = 2 | ||||
@@ -75,7 +74,9 @@ class AfCStatistics(Task): | |||||
self.pageroot = cfg.get("page", "Template:AfC statistics") | self.pageroot = cfg.get("page", "Template:AfC statistics") | ||||
self.pending_cat = cfg.get("pending", "Pending AfC submissions") | self.pending_cat = cfg.get("pending", "Pending AfC submissions") | ||||
self.ignore_list = cfg.get("ignoreList", []) | self.ignore_list = cfg.get("ignoreList", []) | ||||
default_summary = "Updating statistics for [[WP:WPAFC|WikiProject Articles for creation]]." | |||||
default_summary = ( | |||||
"Updating statistics for [[WP:WPAFC|WikiProject Articles for creation]]." | |||||
) | |||||
self.summary = self.make_summary(cfg.get("summary", default_summary)) | self.summary = self.make_summary(cfg.get("summary", default_summary)) | ||||
# Templates used in chart generation: | # Templates used in chart generation: | ||||
@@ -143,24 +144,29 @@ class AfCStatistics(Task): | |||||
def _save_page(self, name, chart, summary): | def _save_page(self, name, chart, summary): | ||||
"""Save a statistics chart to a single page.""" | """Save a statistics chart to a single page.""" | ||||
page = self.site.get_page(u"{}/{}".format(self.pageroot, name)) | |||||
page = self.site.get_page(f"{self.pageroot}/{name}") | |||||
try: | try: | ||||
text = page.get() | text = page.get() | ||||
except exceptions.PageNotFoundError: | except exceptions.PageNotFoundError: | ||||
text = _DEFAULT_PAGE_TEXT % {"pageroot": self.pageroot} | text = _DEFAULT_PAGE_TEXT % {"pageroot": self.pageroot} | ||||
newtext = re.sub(u"<!-- stat begin -->(.*?)<!-- stat end -->", | |||||
"<!-- stat begin -->" + chart + "<!-- stat end -->", | |||||
text, flags=re.DOTALL) | |||||
newtext = re.sub( | |||||
"<!-- stat begin -->(.*?)<!-- stat end -->", | |||||
"<!-- stat begin -->" + chart + "<!-- stat end -->", | |||||
text, | |||||
flags=re.DOTALL, | |||||
) | |||||
if newtext == text: | if newtext == text: | ||||
self.logger.info(u"Chart for {} unchanged; not saving".format(name)) | |||||
self.logger.info(f"Chart for {name} unchanged; not saving") | |||||
return | return | ||||
newtext = re.sub("<!-- sig begin -->(.*?)<!-- sig end -->", | |||||
"<!-- sig begin -->~~~ at ~~~~~<!-- sig end -->", | |||||
newtext) | |||||
newtext = re.sub( | |||||
"<!-- sig begin -->(.*?)<!-- sig end -->", | |||||
"<!-- sig begin -->~~~ at ~~~~~<!-- sig end -->", | |||||
newtext, | |||||
) | |||||
page.edit(newtext, summary, minor=True, bot=True) | page.edit(newtext, summary, minor=True, bot=True) | ||||
self.logger.info(u"Chart for {} saved to [[{}]]".format(name, page.title)) | |||||
self.logger.info(f"Chart for {name} saved to [[{page.title}]]") | |||||
def _compile_charts(self): | def _compile_charts(self): | ||||
"""Compile and return all statistics information from our local db.""" | """Compile and return all statistics information from our local db.""" | ||||
@@ -168,20 +174,20 @@ class AfCStatistics(Task): | |||||
with self.conn.cursor(oursql.DictCursor) as cursor: | with self.conn.cursor(oursql.DictCursor) as cursor: | ||||
cursor.execute("SELECT * FROM chart") | cursor.execute("SELECT * FROM chart") | ||||
for chart in cursor: | for chart in cursor: | ||||
name = chart['chart_name'] | |||||
name = chart["chart_name"] | |||||
stats[name] = self._compile_chart(chart) | stats[name] = self._compile_chart(chart) | ||||
return stats | return stats | ||||
def _compile_chart(self, chart_info): | def _compile_chart(self, chart_info): | ||||
"""Compile and return a single statistics chart.""" | """Compile and return a single statistics chart.""" | ||||
chart = self.tl_header + "|" + chart_info['chart_title'] | |||||
if chart_info['chart_special_title']: | |||||
chart += "|" + chart_info['chart_special_title'] | |||||
chart = self.tl_header + "|" + chart_info["chart_title"] | |||||
if chart_info["chart_special_title"]: | |||||
chart += "|" + chart_info["chart_special_title"] | |||||
chart = "{{" + chart + "}}" | chart = "{{" + chart + "}}" | ||||
query = "SELECT * FROM page JOIN row ON page_id = row_id WHERE row_chart = ?" | query = "SELECT * FROM page JOIN row ON page_id = row_id WHERE row_chart = ?" | ||||
with self.conn.cursor(oursql.DictCursor) as cursor: | with self.conn.cursor(oursql.DictCursor) as cursor: | ||||
cursor.execute(query, (chart_info['chart_id'],)) | |||||
cursor.execute(query, (chart_info["chart_id"],)) | |||||
rows = cursor.fetchall() | rows = cursor.fetchall() | ||||
skipped = max(0, len(rows) - _PER_CHART_LIMIT) | skipped = max(0, len(rows) - _PER_CHART_LIMIT) | ||||
rows = rows[:_PER_CHART_LIMIT] | rows = rows[:_PER_CHART_LIMIT] | ||||
@@ -190,7 +196,7 @@ class AfCStatistics(Task): | |||||
footer = "{{" + self.tl_footer | footer = "{{" + self.tl_footer | ||||
if skipped: | if skipped: | ||||
footer += "|skip={}".format(skipped) | |||||
footer += f"|skip={skipped}" | |||||
footer += "}}" | footer += "}}" | ||||
chart += "\n" + footer + "\n" | chart += "\n" + footer + "\n" | ||||
return chart | return chart | ||||
@@ -201,9 +207,11 @@ class AfCStatistics(Task): | |||||
'page' is a dict of page information, taken as a row from the page | 'page' is a dict of page information, taken as a row from the page | ||||
table, where keys are column names and values are their cell contents. | table, where keys are column names and values are their cell contents. | ||||
""" | """ | ||||
row = u"{0}|s={page_status}|t={page_title}|z={page_size}|" | |||||
row = "{0}|s={page_status}|t={page_title}|z={page_size}|" | |||||
if page["page_special_oldid"]: | if page["page_special_oldid"]: | ||||
row += "sr={page_special_user}|sd={page_special_time}|si={page_special_oldid}|" | |||||
row += ( | |||||
"sr={page_special_user}|sd={page_special_time}|si={page_special_oldid}|" | |||||
) | |||||
row += "mr={page_modify_user}|md={page_modify_time}|mi={page_modify_oldid}" | row += "mr={page_modify_user}|md={page_modify_time}|mi={page_modify_oldid}" | ||||
page["page_special_time"] = self._fmt_time(page["page_special_time"]) | page["page_special_time"] = self._fmt_time(page["page_special_time"]) | ||||
@@ -236,7 +244,7 @@ class AfCStatistics(Task): | |||||
self.logger.info("Starting sync") | self.logger.info("Starting sync") | ||||
replag = self.site.get_replag() | replag = self.site.get_replag() | ||||
self.logger.debug("Server replag is {0}".format(replag)) | |||||
self.logger.debug(f"Server replag is {replag}") | |||||
if replag > 600 and not kwargs.get("ignore_replag"): | if replag > 600 and not kwargs.get("ignore_replag"): | ||||
msg = "Sync canceled as replag ({0} secs) is greater than ten minutes" | msg = "Sync canceled as replag ({0} secs) is greater than ten minutes" | ||||
self.logger.warn(msg.format(replag)) | self.logger.warn(msg.format(replag)) | ||||
@@ -277,18 +285,18 @@ class AfCStatistics(Task): | |||||
if oldid == real_oldid: | if oldid == real_oldid: | ||||
continue | continue | ||||
msg = u"Updating page [[{0}]] (id: {1}) @ {2}" | |||||
msg = "Updating page [[{0}]] (id: {1}) @ {2}" | |||||
self.logger.debug(msg.format(title, pageid, oldid)) | self.logger.debug(msg.format(title, pageid, oldid)) | ||||
msg = u" {0}: oldid: {1} -> {2}" | |||||
msg = " {0}: oldid: {1} -> {2}" | |||||
self.logger.debug(msg.format(pageid, oldid, real_oldid)) | self.logger.debug(msg.format(pageid, oldid, real_oldid)) | ||||
real_title = real_title.decode("utf8").replace("_", " ") | real_title = real_title.decode("utf8").replace("_", " ") | ||||
ns = self.site.namespace_id_to_name(real_ns) | ns = self.site.namespace_id_to_name(real_ns) | ||||
if ns: | if ns: | ||||
real_title = u":".join((ns, real_title)) | |||||
real_title = ":".join((ns, real_title)) | |||||
try: | try: | ||||
self._update_page(cursor, pageid, real_title) | self._update_page(cursor, pageid, real_title) | ||||
except Exception: | except Exception: | ||||
e = u"Error updating page [[{0}]] (id: {1})" | |||||
e = "Error updating page [[{0}]] (id: {1})" | |||||
self.logger.exception(e.format(real_title, pageid)) | self.logger.exception(e.format(real_title, pageid)) | ||||
def _add_untracked(self, cursor): | def _add_untracked(self, cursor): | ||||
@@ -317,15 +325,15 @@ class AfCStatistics(Task): | |||||
title = title.decode("utf8").replace("_", " ") | title = title.decode("utf8").replace("_", " ") | ||||
ns_name = self.site.namespace_id_to_name(ns) | ns_name = self.site.namespace_id_to_name(ns) | ||||
if ns_name: | if ns_name: | ||||
title = u":".join((ns_name, title)) | |||||
title = ":".join((ns_name, title)) | |||||
if title in self.ignore_list or ns == wiki.NS_CATEGORY: | if title in self.ignore_list or ns == wiki.NS_CATEGORY: | ||||
continue | continue | ||||
msg = u"Tracking page [[{0}]] (id: {1})".format(title, pageid) | |||||
msg = f"Tracking page [[{title}]] (id: {pageid})" | |||||
self.logger.debug(msg) | self.logger.debug(msg) | ||||
try: | try: | ||||
self._track_page(cursor, pageid, title) | self._track_page(cursor, pageid, title) | ||||
except Exception: | except Exception: | ||||
e = u"Error tracking page [[{0}]] (id: {1})" | |||||
e = "Error tracking page [[{0}]] (id: {1})" | |||||
self.logger.exception(e.format(title, pageid)) | self.logger.exception(e.format(title, pageid)) | ||||
def _update_stale(self, cursor): | def _update_stale(self, cursor): | ||||
@@ -345,12 +353,12 @@ class AfCStatistics(Task): | |||||
cursor.execute(query) | cursor.execute(query) | ||||
for pageid, title, oldid in cursor: | for pageid, title, oldid in cursor: | ||||
msg = u"Updating page [[{0}]] (id: {1}) @ {2}" | |||||
msg = "Updating page [[{0}]] (id: {1}) @ {2}" | |||||
self.logger.debug(msg.format(title, pageid, oldid)) | self.logger.debug(msg.format(title, pageid, oldid)) | ||||
try: | try: | ||||
self._update_page(cursor, pageid, title) | self._update_page(cursor, pageid, title) | ||||
except Exception: | except Exception: | ||||
e = u"Error updating page [[{0}]] (id: {1})" | |||||
e = "Error updating page [[{0}]] (id: {1})" | |||||
self.logger.exception(e.format(title, pageid)) | self.logger.exception(e.format(title, pageid)) | ||||
def _delete_old(self, cursor): | def _delete_old(self, cursor): | ||||
@@ -370,7 +378,7 @@ class AfCStatistics(Task): | |||||
def _untrack_page(self, cursor, pageid): | def _untrack_page(self, cursor, pageid): | ||||
"""Remove a page, given by ID, from our database.""" | """Remove a page, given by ID, from our database.""" | ||||
self.logger.debug("Untracking page (id: {0})".format(pageid)) | |||||
self.logger.debug(f"Untracking page (id: {pageid})") | |||||
query = """DELETE FROM page, row, updatelog USING page JOIN row | query = """DELETE FROM page, row, updatelog USING page JOIN row | ||||
ON page_id = row_id JOIN updatelog ON page_id = update_id | ON page_id = row_id JOIN updatelog ON page_id = update_id | ||||
WHERE page_id = ?""" | WHERE page_id = ?""" | ||||
@@ -384,14 +392,14 @@ class AfCStatistics(Task): | |||||
""" | """ | ||||
content = self._get_content(pageid) | content = self._get_content(pageid) | ||||
if content is None: | if content is None: | ||||
msg = u"Could not get page content for [[{0}]]".format(title) | |||||
msg = f"Could not get page content for [[{title}]]" | |||||
self.logger.error(msg) | self.logger.error(msg) | ||||
return | return | ||||
namespace = self.site.get_page(title).namespace | namespace = self.site.get_page(title).namespace | ||||
status, chart = self._get_status_and_chart(content, namespace) | status, chart = self._get_status_and_chart(content, namespace) | ||||
if chart == self.CHART_NONE: | if chart == self.CHART_NONE: | ||||
msg = u"Could not find a status for [[{0}]]".format(title) | |||||
msg = f"Could not find a status for [[{title}]]" | |||||
self.logger.warn(msg) | self.logger.warn(msg) | ||||
return | return | ||||
@@ -403,8 +411,22 @@ class AfCStatistics(Task): | |||||
query2 = "INSERT INTO page VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" | query2 = "INSERT INTO page VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" | ||||
query3 = "INSERT INTO updatelog VALUES (?, ?)" | query3 = "INSERT INTO updatelog VALUES (?, ?)" | ||||
cursor.execute(query1, (pageid, chart)) | cursor.execute(query1, (pageid, chart)) | ||||
cursor.execute(query2, (pageid, status, title, len(content), notes, | |||||
m_user, m_time, m_id, s_user, s_time, s_id)) | |||||
cursor.execute( | |||||
query2, | |||||
( | |||||
pageid, | |||||
status, | |||||
title, | |||||
len(content), | |||||
notes, | |||||
m_user, | |||||
m_time, | |||||
m_id, | |||||
s_user, | |||||
s_time, | |||||
s_id, | |||||
), | |||||
) | |||||
cursor.execute(query3, (pageid, datetime.utcnow())) | cursor.execute(query3, (pageid, datetime.utcnow())) | ||||
def _update_page(self, cursor, pageid, title): | def _update_page(self, cursor, pageid, title): | ||||
@@ -416,7 +438,7 @@ class AfCStatistics(Task): | |||||
""" | """ | ||||
content = self._get_content(pageid) | content = self._get_content(pageid) | ||||
if content is None: | if content is None: | ||||
msg = u"Could not get page content for [[{0}]]".format(title) | |||||
msg = f"Could not get page content for [[{title}]]" | |||||
self.logger.error(msg) | self.logger.error(msg) | ||||
return | return | ||||
@@ -437,12 +459,14 @@ class AfCStatistics(Task): | |||||
self._update_page_title(cursor, result, pageid, title) | self._update_page_title(cursor, result, pageid, title) | ||||
if m_id != result["page_modify_oldid"]: | if m_id != result["page_modify_oldid"]: | ||||
self._update_page_modify(cursor, result, pageid, len(content), | |||||
m_user, m_time, m_id) | |||||
self._update_page_modify( | |||||
cursor, result, pageid, len(content), m_user, m_time, m_id | |||||
) | |||||
if status != result["page_status"]: | if status != result["page_status"]: | ||||
special = self._update_page_status(cursor, result, pageid, content, | |||||
status, chart) | |||||
special = self._update_page_status( | |||||
cursor, result, pageid, content, status, chart | |||||
) | |||||
s_user = special[0] | s_user = special[0] | ||||
else: | else: | ||||
s_user = result["page_special_user"] | s_user = result["page_special_user"] | ||||
@@ -461,7 +485,7 @@ class AfCStatistics(Task): | |||||
query = "UPDATE page SET page_title = ? WHERE page_id = ?" | query = "UPDATE page SET page_title = ? WHERE page_id = ?" | ||||
cursor.execute(query, (title, pageid)) | cursor.execute(query, (title, pageid)) | ||||
msg = u" {0}: title: {1} -> {2}" | |||||
msg = " {0}: title: {1} -> {2}" | |||||
self.logger.debug(msg.format(pageid, result["page_title"], title)) | self.logger.debug(msg.format(pageid, result["page_title"], title)) | ||||
def _update_page_modify(self, cursor, result, pageid, size, m_user, m_time, m_id): | def _update_page_modify(self, cursor, result, pageid, size, m_user, m_time, m_id): | ||||
@@ -471,10 +495,16 @@ class AfCStatistics(Task): | |||||
WHERE page_id = ?""" | WHERE page_id = ?""" | ||||
cursor.execute(query, (size, m_user, m_time, m_id, pageid)) | cursor.execute(query, (size, m_user, m_time, m_id, pageid)) | ||||
msg = u" {0}: modify: {1} / {2} / {3} -> {4} / {5} / {6}" | |||||
msg = msg.format(pageid, result["page_modify_user"], | |||||
result["page_modify_time"], | |||||
result["page_modify_oldid"], m_user, m_time, m_id) | |||||
msg = " {0}: modify: {1} / {2} / {3} -> {4} / {5} / {6}" | |||||
msg = msg.format( | |||||
pageid, | |||||
result["page_modify_user"], | |||||
result["page_modify_time"], | |||||
result["page_modify_oldid"], | |||||
m_user, | |||||
m_time, | |||||
m_id, | |||||
) | |||||
self.logger.debug(msg) | self.logger.debug(msg) | ||||
def _update_page_status(self, cursor, result, pageid, content, status, chart): | def _update_page_status(self, cursor, result, pageid, content, status, chart): | ||||
@@ -487,16 +517,25 @@ class AfCStatistics(Task): | |||||
cursor.execute(query1, (status, chart, pageid)) | cursor.execute(query1, (status, chart, pageid)) | ||||
msg = " {0}: status: {1} ({2}) -> {3} ({4})" | msg = " {0}: status: {1} ({2}) -> {3} ({4})" | ||||
self.logger.debug(msg.format(pageid, result["page_status"], | |||||
result["row_chart"], status, chart)) | |||||
self.logger.debug( | |||||
msg.format( | |||||
pageid, result["page_status"], result["row_chart"], status, chart | |||||
) | |||||
) | |||||
s_user, s_time, s_id = self._get_special(pageid, content, chart) | s_user, s_time, s_id = self._get_special(pageid, content, chart) | ||||
if s_id != result["page_special_oldid"]: | if s_id != result["page_special_oldid"]: | ||||
cursor.execute(query2, (s_user, s_time, s_id, pageid)) | cursor.execute(query2, (s_user, s_time, s_id, pageid)) | ||||
msg = u" {0}: special: {1} / {2} / {3} -> {4} / {5} / {6}" | |||||
msg = msg.format(pageid, result["page_special_user"], | |||||
result["page_special_time"], | |||||
result["page_special_oldid"], s_user, s_time, s_id) | |||||
msg = " {0}: special: {1} / {2} / {3} -> {4} / {5} / {6}" | |||||
msg = msg.format( | |||||
pageid, | |||||
result["page_special_user"], | |||||
result["page_special_time"], | |||||
result["page_special_oldid"], | |||||
s_user, | |||||
s_time, | |||||
s_id, | |||||
) | |||||
self.logger.debug(msg) | self.logger.debug(msg) | ||||
return s_user, s_time, s_id | return s_user, s_time, s_id | ||||
@@ -529,9 +568,13 @@ class AfCStatistics(Task): | |||||
"""Get the content of a revision by ID from the API.""" | """Get the content of a revision by ID from the API.""" | ||||
if revid in self.revision_cache: | if revid in self.revision_cache: | ||||
return self.revision_cache[revid] | return self.revision_cache[revid] | ||||
res = self.site.api_query(action="query", prop="revisions", | |||||
rvprop="content", rvslots="main", | |||||
revids=revid) | |||||
res = self.site.api_query( | |||||
action="query", | |||||
prop="revisions", | |||||
rvprop="content", | |||||
rvslots="main", | |||||
revids=revid, | |||||
) | |||||
try: | try: | ||||
revision = res["query"]["pages"].values()[0]["revisions"][0] | revision = res["query"]["pages"].values()[0]["revisions"][0] | ||||
content = revision["slots"]["main"]["*"] | content = revision["slots"]["main"]["*"] | ||||
@@ -577,7 +620,7 @@ class AfCStatistics(Task): | |||||
"afc submission/reviewing": "R", | "afc submission/reviewing": "R", | ||||
"afc submission/pending": "P", | "afc submission/pending": "P", | ||||
"afc submission/draft": "T", | "afc submission/draft": "T", | ||||
"afc submission/declined": "D" | |||||
"afc submission/declined": "D", | |||||
} | } | ||||
statuses = [] | statuses = [] | ||||
code = mwparserfromhell.parse(content) | code = mwparserfromhell.parse(content) | ||||
@@ -629,7 +672,7 @@ class AfCStatistics(Task): | |||||
self.CHART_ACCEPT: self.get_accepted, | self.CHART_ACCEPT: self.get_accepted, | ||||
self.CHART_REVIEW: self.get_reviewing, | self.CHART_REVIEW: self.get_reviewing, | ||||
self.CHART_PEND: self.get_pending, | self.CHART_PEND: self.get_pending, | ||||
self.CHART_DECLINE: self.get_decline | |||||
self.CHART_DECLINE: self.get_decline, | |||||
} | } | ||||
return charts[chart](pageid, content) | return charts[chart](pageid, content) | ||||
@@ -675,7 +718,8 @@ class AfCStatistics(Task): | |||||
params = ("decliner", "declinets") | params = ("decliner", "declinets") | ||||
res = self._get_status_helper(pageid, content, ("D"), params) | res = self._get_status_helper(pageid, content, ("D"), params) | ||||
return res or self._search_history( | return res or self._search_history( | ||||
pageid, self.CHART_DECLINE, ["D"], ["R", "P", "T"]) | |||||
pageid, self.CHART_DECLINE, ["D"], ["R", "P", "T"] | |||||
) | |||||
def _get_status_helper(self, pageid, content, statuses, params): | def _get_status_helper(self, pageid, content, statuses, params): | ||||
"""Helper function for get_pending() and get_decline().""" | """Helper function for get_pending() and get_decline().""" | ||||
@@ -686,7 +730,7 @@ class AfCStatistics(Task): | |||||
if tmpl.name.strip().lower() == "afc submission": | if tmpl.name.strip().lower() == "afc submission": | ||||
if all([tmpl.has(par, ignore_empty=True) for par in params]): | if all([tmpl.has(par, ignore_empty=True) for par in params]): | ||||
if status in statuses: | if status in statuses: | ||||
data = [unicode(tmpl.get(par).value) for par in params] | |||||
data = [str(tmpl.get(par).value) for par in params] | |||||
submits.append(data) | submits.append(data) | ||||
if not submits: | if not submits: | ||||
return None | return None | ||||
@@ -774,7 +818,7 @@ class AfCStatistics(Task): | |||||
if re.search(regex, content): | if re.search(regex, content): | ||||
notes += "|nc=1" # Submission is a suspected copyvio | notes += "|nc=1" # Submission is a suspected copyvio | ||||
if not re.search(r"\<ref\s*(.*?)\>(.*?)\</ref\>", content, re.I|re.S): | |||||
if not re.search(r"\<ref\s*(.*?)\>(.*?)\</ref\>", content, re.I | re.S): | |||||
regex = r"(https?:)|\[//(?!{0})([^ \]\t\n\r\f\v]+?)" | regex = r"(https?:)|\[//(?!{0})([^ \]\t\n\r\f\v]+?)" | ||||
sitedomain = re.escape(self.site.domain) | sitedomain = re.escape(self.site.domain) | ||||
if re.search(regex.format(sitedomain), content, re.I | re.S): | if re.search(regex.format(sitedomain), content, re.I | re.S): | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -25,28 +23,49 @@ from datetime import datetime | |||||
import mwparserfromhell | import mwparserfromhell | ||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
from earwigbot.wiki.constants import * | |||||
from earwigbot.wiki.constants import ( | |||||
NS_CATEGORY, | |||||
NS_CATEGORY_TALK, | |||||
NS_FILE, | |||||
NS_FILE_TALK, | |||||
NS_HELP_TALK, | |||||
NS_PROJECT, | |||||
NS_PROJECT_TALK, | |||||
NS_TALK, | |||||
NS_TEMPLATE, | |||||
NS_TEMPLATE_TALK, | |||||
NS_USER, | |||||
) | |||||
NS_DRAFT = 118 | NS_DRAFT = 118 | ||||
class AfCUndated(Task): | class AfCUndated(Task): | ||||
"""A task to clear [[Category:Undated AfC submissions]].""" | """A task to clear [[Category:Undated AfC submissions]].""" | ||||
name = "afc_undated" | name = "afc_undated" | ||||
number = 5 | number = 5 | ||||
def setup(self): | def setup(self): | ||||
cfg = self.config.tasks.get(self.name, {}) | cfg = self.config.tasks.get(self.name, {}) | ||||
self.category = cfg.get("category", "Undated AfC submissions") | self.category = cfg.get("category", "Undated AfC submissions") | ||||
default_summary = "Adding timestamp to undated [[WP:AFC|Articles for creation]] submission." | |||||
default_summary = ( | |||||
"Adding timestamp to undated [[WP:AFC|Articles for creation]] submission." | |||||
) | |||||
self.summary = self.make_summary(cfg.get("summary", default_summary)) | self.summary = self.make_summary(cfg.get("summary", default_summary)) | ||||
self.namespaces = { | self.namespaces = { | ||||
"submission": [NS_USER, NS_PROJECT, NS_PROJECT_TALK, NS_DRAFT], | "submission": [NS_USER, NS_PROJECT, NS_PROJECT_TALK, NS_DRAFT], | ||||
"talk": [NS_TALK, NS_FILE_TALK, NS_TEMPLATE_TALK, NS_HELP_TALK, | |||||
NS_CATEGORY_TALK] | |||||
"talk": [ | |||||
NS_TALK, | |||||
NS_FILE_TALK, | |||||
NS_TEMPLATE_TALK, | |||||
NS_HELP_TALK, | |||||
NS_CATEGORY_TALK, | |||||
], | |||||
} | } | ||||
self.aliases = { | self.aliases = { | ||||
"submission": ["AfC submission"], | "submission": ["AfC submission"], | ||||
"talk": ["WikiProject Articles for creation"] | |||||
"talk": ["WikiProject Articles for creation"], | |||||
} | } | ||||
def run(self, **kwargs): | def run(self, **kwargs): | ||||
@@ -59,7 +78,7 @@ class AfCUndated(Task): | |||||
self.site = self.bot.wiki.get_site() | self.site = self.bot.wiki.get_site() | ||||
category = self.site.get_category(self.category) | category = self.site.get_category(self.category) | ||||
logmsg = u"Undated category [[{0}]] has {1} members" | |||||
logmsg = "Undated category [[{0}]] has {1} members" | |||||
self.logger.info(logmsg.format(category.title, category.size)) | self.logger.info(logmsg.format(category.title, category.size)) | ||||
if category.size: | if category.size: | ||||
self._build_aliases() | self._build_aliases() | ||||
@@ -77,8 +96,12 @@ class AfCUndated(Task): | |||||
base = self.aliases[key][0] | base = self.aliases[key][0] | ||||
aliases = [base, "Template:" + base] | aliases = [base, "Template:" + base] | ||||
result = self.site.api_query( | result = self.site.api_query( | ||||
action="query", list="backlinks", bllimit=50, | |||||
blfilterredir="redirects", bltitle=aliases[1]) | |||||
action="query", | |||||
list="backlinks", | |||||
bllimit=50, | |||||
blfilterredir="redirects", | |||||
bltitle=aliases[1], | |||||
) | |||||
for data in result["query"]["backlinks"]: | for data in result["query"]["backlinks"]: | ||||
redir = self.site.get_page(data["title"]) | redir = self.site.get_page(data["title"]) | ||||
aliases.append(redir.title) | aliases.append(redir.title) | ||||
@@ -89,7 +112,7 @@ class AfCUndated(Task): | |||||
def _process_page(self, page): | def _process_page(self, page): | ||||
"""Date the necessary templates inside a page object.""" | """Date the necessary templates inside a page object.""" | ||||
if not page.check_exclusion(): | if not page.check_exclusion(): | ||||
msg = u"Skipping [[{0}]]; bot excluded from editing" | |||||
msg = "Skipping [[{0}]]; bot excluded from editing" | |||||
self.logger.info(msg.format(page.title)) | self.logger.info(msg.format(page.title)) | ||||
return | return | ||||
@@ -102,7 +125,7 @@ class AfCUndated(Task): | |||||
aliases = self.aliases["talk"] | aliases = self.aliases["talk"] | ||||
timestamp, reviewer = self._get_talkdata(page) | timestamp, reviewer = self._get_talkdata(page) | ||||
else: | else: | ||||
msg = u"[[{0}]] is undated, but in a namespace I don't know how to process" | |||||
msg = "[[{0}]] is undated, but in a namespace I don't know how to process" | |||||
self.logger.warn(msg.format(page.title)) | self.logger.warn(msg.format(page.title)) | ||||
return | return | ||||
if not timestamp: | if not timestamp: | ||||
@@ -120,22 +143,27 @@ class AfCUndated(Task): | |||||
changes += 1 | changes += 1 | ||||
if changes: | if changes: | ||||
msg = u"Dating [[{0}]]: {1}x {2}" | |||||
msg = "Dating [[{0}]]: {1}x {2}" | |||||
self.logger.info(msg.format(page.title, changes, aliases[0])) | self.logger.info(msg.format(page.title, changes, aliases[0])) | ||||
page.edit(unicode(code), self.summary) | |||||
page.edit(str(code), self.summary) | |||||
else: | else: | ||||
msg = u"[[{0}]] is undated, but I can't figure out what to replace" | |||||
msg = "[[{0}]] is undated, but I can't figure out what to replace" | |||||
self.logger.warn(msg.format(page.title)) | self.logger.warn(msg.format(page.title)) | ||||
def _get_timestamp(self, page): | def _get_timestamp(self, page): | ||||
"""Get the timestamp associated with a particular submission.""" | """Get the timestamp associated with a particular submission.""" | ||||
self.logger.debug(u"[[{0}]]: Getting timestamp".format(page.title)) | |||||
self.logger.debug(f"[[{page.title}]]: Getting timestamp") | |||||
result = self.site.api_query( | result = self.site.api_query( | ||||
action="query", prop="revisions", rvprop="timestamp", rvlimit=1, | |||||
rvdir="newer", titles=page.title) | |||||
action="query", | |||||
prop="revisions", | |||||
rvprop="timestamp", | |||||
rvlimit=1, | |||||
rvdir="newer", | |||||
titles=page.title, | |||||
) | |||||
data = result["query"]["pages"].values()[0] | data = result["query"]["pages"].values()[0] | ||||
if "revisions" not in data: | if "revisions" not in data: | ||||
log = u"Couldn't get timestamp for [[{0}]]" | |||||
log = "Couldn't get timestamp for [[{0}]]" | |||||
self.logger.warn(log.format(page.title)) | self.logger.warn(log.format(page.title)) | ||||
return None | return None | ||||
raw = data["revisions"][0]["timestamp"] | raw = data["revisions"][0]["timestamp"] | ||||
@@ -150,32 +178,33 @@ class AfCUndated(Task): | |||||
""" | """ | ||||
subject = page.toggle_talk() | subject = page.toggle_talk() | ||||
if subject.exists == subject.PAGE_MISSING: | if subject.exists == subject.PAGE_MISSING: | ||||
log = u"Couldn't process [[{0}]]: subject page doesn't exist" | |||||
log = "Couldn't process [[{0}]]: subject page doesn't exist" | |||||
self.logger.warn(log.format(page.title)) | self.logger.warn(log.format(page.title)) | ||||
return None, None | return None, None | ||||
if subject.namespace == NS_FILE: | if subject.namespace == NS_FILE: | ||||
self.logger.debug(u"[[{0}]]: Getting filedata".format(page.title)) | |||||
self.logger.debug(f"[[{page.title}]]: Getting filedata") | |||||
return self._get_filedata(subject) | return self._get_filedata(subject) | ||||
self.logger.debug(u"[[{0}]]: Getting talkdata".format(page.title)) | |||||
self.logger.debug(f"[[{page.title}]]: Getting talkdata") | |||||
user, ts, revid = self.statistics.get_accepted(subject.pageid) | user, ts, revid = self.statistics.get_accepted(subject.pageid) | ||||
if not ts: | if not ts: | ||||
if subject.is_redirect or subject.namespace == NS_CATEGORY: | if subject.is_redirect or subject.namespace == NS_CATEGORY: | ||||
log = u"[[{0}]]: Couldn't get talkdata; trying redir/cat data" | |||||
log = "[[{0}]]: Couldn't get talkdata; trying redir/cat data" | |||||
self.logger.debug(log.format(page.title)) | self.logger.debug(log.format(page.title)) | ||||
return self._get_redirdata(subject) | return self._get_redirdata(subject) | ||||
log = u"Couldn't get talkdata for [[{0}]]" | |||||
log = "Couldn't get talkdata for [[{0}]]" | |||||
self.logger.warn(log.format(page.title)) | self.logger.warn(log.format(page.title)) | ||||
return None, None | return None, None | ||||
return ts.strftime("%Y%m%d%H%M%S"), user | return ts.strftime("%Y%m%d%H%M%S"), user | ||||
def _get_filedata(self, page): | def _get_filedata(self, page): | ||||
"""Get the timestamp and reviewer associated with a file talkpage.""" | """Get the timestamp and reviewer associated with a file talkpage.""" | ||||
result = self.site.api_query(action="query", prop="imageinfo", | |||||
titles=page.title) | |||||
result = self.site.api_query( | |||||
action="query", prop="imageinfo", titles=page.title | |||||
) | |||||
data = result["query"]["pages"].values()[0] | data = result["query"]["pages"].values()[0] | ||||
if "imageinfo" not in data: | if "imageinfo" not in data: | ||||
log = u"Couldn't get filedata for [[{0}]]" | |||||
log = "Couldn't get filedata for [[{0}]]" | |||||
self.logger.warn(log.format(page.title)) | self.logger.warn(log.format(page.title)) | ||||
return None, None | return None, None | ||||
info = data["imageinfo"][0] | info = data["imageinfo"][0] | ||||
@@ -185,10 +214,15 @@ class AfCUndated(Task): | |||||
def _get_redirdata(self, page): | def _get_redirdata(self, page): | ||||
"""Get the timestamp and reviewer for a redirect/category talkpage.""" | """Get the timestamp and reviewer for a redirect/category talkpage.""" | ||||
result = self.site.api_query( | result = self.site.api_query( | ||||
action="query", prop="revisions", rvprop="timestamp|user", | |||||
rvlimit=1, rvdir="newer", titles=page.title) | |||||
action="query", | |||||
prop="revisions", | |||||
rvprop="timestamp|user", | |||||
rvlimit=1, | |||||
rvdir="newer", | |||||
titles=page.title, | |||||
) | |||||
if "batchcomplete" not in result: | if "batchcomplete" not in result: | ||||
log = u"Couldn't get redir/cat talkdata for [[{0}]]: has multiple revisions" | |||||
log = "Couldn't get redir/cat talkdata for [[{0}]]: has multiple revisions" | |||||
self.logger.warn(log.format(page.title)) | self.logger.warn(log.format(page.title)) | ||||
return None, None | return None, None | ||||
rev = result["query"]["pages"].values()[0]["revisions"][0] | rev = result["query"]["pages"].values()[0]["revisions"][0] | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2017 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2017 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,8 +22,10 @@ import time | |||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
class BannerUntag(Task): | class BannerUntag(Task): | ||||
"""A task to undo mistaken tagging edits made by wikiproject_tagger.""" | """A task to undo mistaken tagging edits made by wikiproject_tagger.""" | ||||
name = "banner_untag" | name = "banner_untag" | ||||
number = 14 | number = 14 | ||||
@@ -42,8 +42,9 @@ class BannerUntag(Task): | |||||
done = [int(line) for line in donefp.read().splitlines()] | done = [int(line) for line in donefp.read().splitlines()] | ||||
with open(rev_file) as fp: | with open(rev_file) as fp: | ||||
data = [[int(x) for x in line.split("\t")] | |||||
for line in fp.read().splitlines()] | |||||
data = [ | |||||
[int(x) for x in line.split("\t")] for line in fp.read().splitlines() | |||||
] | |||||
data = [item for item in data if item[0] not in done] | data = [item for item in data if item[0] not in done] | ||||
with open(error_file, "a") as errfp: | with open(error_file, "a") as errfp: | ||||
@@ -53,7 +54,7 @@ class BannerUntag(Task): | |||||
def _process_data(self, data, errfile, donefile): | def _process_data(self, data, errfile, donefile): | ||||
chunksize = 50 | chunksize = 50 | ||||
for chunkidx in range((len(data) + chunksize - 1) / chunksize): | for chunkidx in range((len(data) + chunksize - 1) / chunksize): | ||||
chunk = data[chunkidx*chunksize:(chunkidx+1)*chunksize] | |||||
chunk = data[chunkidx * chunksize : (chunkidx + 1) * chunksize] | |||||
if self.shutoff_enabled(): | if self.shutoff_enabled(): | ||||
return | return | ||||
self._process_chunk(chunk, errfile, donefile) | self._process_chunk(chunk, errfile, donefile) | ||||
@@ -61,8 +62,12 @@ class BannerUntag(Task): | |||||
def _process_chunk(self, chunk, errfile, donefile): | def _process_chunk(self, chunk, errfile, donefile): | ||||
pageids_to_revids = dict(chunk) | pageids_to_revids = dict(chunk) | ||||
res = self.site.api_query( | res = self.site.api_query( | ||||
action="query", prop="revisions", rvprop="ids", | |||||
pageids="|".join(str(item[0]) for item in chunk), formatversion=2) | |||||
action="query", | |||||
prop="revisions", | |||||
rvprop="ids", | |||||
pageids="|".join(str(item[0]) for item in chunk), | |||||
formatversion=2, | |||||
) | |||||
stage2 = [] | stage2 = [] | ||||
for pagedata in res["query"]["pages"]: | for pagedata in res["query"]["pages"]: | ||||
@@ -78,7 +83,7 @@ class BannerUntag(Task): | |||||
if pageids_to_revids[pageid] == revid: | if pageids_to_revids[pageid] == revid: | ||||
stage2.append(str(parentid)) | stage2.append(str(parentid)) | ||||
else: | else: | ||||
self.logger.info(u"Skipping [[%s]], not latest edit" % title) | |||||
self.logger.info("Skipping [[%s]], not latest edit" % title) | |||||
donefile.write("%d\n" % pageid) | donefile.write("%d\n" % pageid) | ||||
errfile.write("%s\n" % title.encode("utf8")) | errfile.write("%s\n" % title.encode("utf8")) | ||||
@@ -86,8 +91,13 @@ class BannerUntag(Task): | |||||
return | return | ||||
res2 = self.site.api_query( | res2 = self.site.api_query( | ||||
action="query", prop="revisions", rvprop="content", rvslots="main", | |||||
revids="|".join(stage2), formatversion=2) | |||||
action="query", | |||||
prop="revisions", | |||||
rvprop="content", | |||||
rvslots="main", | |||||
revids="|".join(stage2), | |||||
formatversion=2, | |||||
) | |||||
for pagedata in res2["query"]["pages"]: | for pagedata in res2["query"]["pages"]: | ||||
revision = pagedata["revisions"][0]["slots"]["main"] | revision = pagedata["revisions"][0]["slots"]["main"] | ||||
@@ -97,7 +107,7 @@ class BannerUntag(Task): | |||||
title = pagedata["title"] | title = pagedata["title"] | ||||
content = revision["content"] | content = revision["content"] | ||||
self.logger.debug(u"Reverting one edit on [[%s]]" % title) | |||||
self.logger.debug("Reverting one edit on [[%s]]" % title) | |||||
page = self.site.get_page(title) | page = self.site.get_page(title) | ||||
page.edit(content, self.make_summary(self.summary), minor=True) | page.edit(content, self.make_summary(self.summary), minor=True) | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,9 +20,11 @@ | |||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
class BLPTag(Task): | class BLPTag(Task): | ||||
"""A task to add |blp=yes to ``{{WPB}}`` or ``{{WPBS}}`` when it is used | """A task to add |blp=yes to ``{{WPB}}`` or ``{{WPBS}}`` when it is used | ||||
along with ``{{WP Biography}}``.""" | along with ``{{WP Biography}}``.""" | ||||
name = "blp_tag" | name = "blp_tag" | ||||
number = 12 | number = 12 | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,21 +18,23 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
import re | |||||
from datetime import datetime | from datetime import datetime | ||||
from os.path import expanduser | from os.path import expanduser | ||||
import re | |||||
from threading import RLock | from threading import RLock | ||||
from time import mktime, sleep, time | from time import mktime, sleep, time | ||||
from mwparserfromhell import parse as mw_parse | |||||
import oursql | import oursql | ||||
from mwparserfromhell import parse as mw_parse | |||||
from earwigbot import exceptions | from earwigbot import exceptions | ||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
from earwigbot.wiki import constants | from earwigbot.wiki import constants | ||||
class DRNClerkBot(Task): | class DRNClerkBot(Task): | ||||
"""A task to clerk for [[WP:DRN]].""" | """A task to clerk for [[WP:DRN]].""" | ||||
name = "drn_clerkbot" | name = "drn_clerkbot" | ||||
number = 19 | number = 19 | ||||
@@ -63,19 +63,25 @@ class DRNClerkBot(Task): | |||||
cfg = self.config.tasks.get(self.name, {}) | cfg = self.config.tasks.get(self.name, {}) | ||||
# Set some wiki-related attributes: | # Set some wiki-related attributes: | ||||
self.title = cfg.get("title", | |||||
"Wikipedia:Dispute resolution noticeboard") | |||||
self.title = cfg.get("title", "Wikipedia:Dispute resolution noticeboard") | |||||
self.chart_title = cfg.get("chartTitle", "Template:DRN case status") | self.chart_title = cfg.get("chartTitle", "Template:DRN case status") | ||||
self.volunteer_title = cfg.get("volunteers", | |||||
"Wikipedia:Dispute resolution noticeboard/Volunteering") | |||||
self.volunteer_title = cfg.get( | |||||
"volunteers", "Wikipedia:Dispute resolution noticeboard/Volunteering" | |||||
) | |||||
self.very_old_title = cfg.get("veryOldTitle", "User talk:Szhang (WMF)") | self.very_old_title = cfg.get("veryOldTitle", "User talk:Szhang (WMF)") | ||||
self.notify_stale_cases = cfg.get("notifyStaleCases", False) | self.notify_stale_cases = cfg.get("notifyStaleCases", False) | ||||
clerk_summary = "Updating $3 case$4." | clerk_summary = "Updating $3 case$4." | ||||
notify_summary = "Notifying user regarding [[WP:DRN|dispute resolution noticeboard]] case." | |||||
chart_summary = "Updating statistics for the [[WP:DRN|dispute resolution noticeboard]]." | |||||
notify_summary = ( | |||||
"Notifying user regarding [[WP:DRN|dispute resolution noticeboard]] case." | |||||
) | |||||
chart_summary = ( | |||||
"Updating statistics for the [[WP:DRN|dispute resolution noticeboard]]." | |||||
) | |||||
self.clerk_summary = self.make_summary(cfg.get("clerkSummary", clerk_summary)) | self.clerk_summary = self.make_summary(cfg.get("clerkSummary", clerk_summary)) | ||||
self.notify_summary = self.make_summary(cfg.get("notifySummary", notify_summary)) | |||||
self.notify_summary = self.make_summary( | |||||
cfg.get("notifySummary", notify_summary) | |||||
) | |||||
self.chart_summary = self.make_summary(cfg.get("chartSummary", chart_summary)) | self.chart_summary = self.make_summary(cfg.get("chartSummary", chart_summary)) | ||||
# Templates used: | # Templates used: | ||||
@@ -84,13 +90,10 @@ class DRNClerkBot(Task): | |||||
self.tl_notify_party = templates.get("notifyParty", "DRN-notice") | self.tl_notify_party = templates.get("notifyParty", "DRN-notice") | ||||
self.tl_notify_stale = templates.get("notifyStale", "DRN stale notice") | self.tl_notify_stale = templates.get("notifyStale", "DRN stale notice") | ||||
self.tl_archive_top = templates.get("archiveTop", "DRN archive top") | self.tl_archive_top = templates.get("archiveTop", "DRN archive top") | ||||
self.tl_archive_bottom = templates.get("archiveBottom", | |||||
"DRN archive bottom") | |||||
self.tl_chart_header = templates.get("chartHeader", | |||||
"DRN case status/header") | |||||
self.tl_archive_bottom = templates.get("archiveBottom", "DRN archive bottom") | |||||
self.tl_chart_header = templates.get("chartHeader", "DRN case status/header") | |||||
self.tl_chart_row = templates.get("chartRow", "DRN case status/row") | self.tl_chart_row = templates.get("chartRow", "DRN case status/row") | ||||
self.tl_chart_footer = templates.get("chartFooter", | |||||
"DRN case status/footer") | |||||
self.tl_chart_footer = templates.get("chartFooter", "DRN case status/footer") | |||||
# Connection data for our SQL database: | # Connection data for our SQL database: | ||||
kwargs = cfg.get("sql", {}) | kwargs = cfg.get("sql", {}) | ||||
@@ -114,7 +117,7 @@ class DRNClerkBot(Task): | |||||
if action in ["all", "update_volunteers"]: | if action in ["all", "update_volunteers"]: | ||||
self.update_volunteers(conn, site) | self.update_volunteers(conn, site) | ||||
if action in ["all", "clerk"]: | if action in ["all", "clerk"]: | ||||
log = u"Starting update to [[{0}]]".format(self.title) | |||||
log = f"Starting update to [[{self.title}]]" | |||||
self.logger.info(log) | self.logger.info(log) | ||||
cases = self.read_database(conn) | cases = self.read_database(conn) | ||||
page = site.get_page(self.title) | page = site.get_page(self.title) | ||||
@@ -137,7 +140,7 @@ class DRNClerkBot(Task): | |||||
def update_volunteers(self, conn, site): | def update_volunteers(self, conn, site): | ||||
"""Updates and stores the list of dispute resolution volunteers.""" | """Updates and stores the list of dispute resolution volunteers.""" | ||||
log = u"Updating volunteer list from [[{0}]]" | |||||
log = "Updating volunteer list from [[{0}]]" | |||||
self.logger.info(log.format(self.volunteer_title)) | self.logger.info(log.format(self.volunteer_title)) | ||||
page = site.get_page(self.volunteer_title) | page = site.get_page(self.volunteer_title) | ||||
try: | try: | ||||
@@ -146,7 +149,7 @@ class DRNClerkBot(Task): | |||||
text = "" | text = "" | ||||
marker = "<!-- please don't remove this comment (used by EarwigBot) -->" | marker = "<!-- please don't remove this comment (used by EarwigBot) -->" | ||||
if marker not in text: | if marker not in text: | ||||
log = u"The marker ({0}) wasn't found in the volunteer list at [[{1}]]!" | |||||
log = "The marker ({0}) wasn't found in the volunteer list at [[{1}]]!" | |||||
self.logger.error(log.format(marker, page.title)) | self.logger.error(log.format(marker, page.title)) | ||||
return | return | ||||
text = text.split(marker)[1] | text = text.split(marker)[1] | ||||
@@ -190,8 +193,8 @@ class DRNClerkBot(Task): | |||||
"""Read the noticeboard content and update the list of _Cases.""" | """Read the noticeboard content and update the list of _Cases.""" | ||||
nextid = self.select_next_id(conn) | nextid = self.select_next_id(conn) | ||||
tl_status_esc = re.escape(self.tl_status) | tl_status_esc = re.escape(self.tl_status) | ||||
split = re.split("(^==\s*[^=]+?\s*==$)", text, flags=re.M|re.U) | |||||
for i in xrange(len(split)): | |||||
split = re.split("(^==\s*[^=]+?\s*==$)", text, flags=re.M | re.U) | |||||
for i in range(len(split)): | |||||
if i + 1 == len(split): | if i + 1 == len(split): | ||||
break | break | ||||
if not split[i].startswith("=="): | if not split[i].startswith("=="): | ||||
@@ -209,8 +212,10 @@ class DRNClerkBot(Task): | |||||
id_ = nextid | id_ = nextid | ||||
nextid += 1 | nextid += 1 | ||||
re_id2 = "(\{\{" + tl_status_esc | re_id2 = "(\{\{" + tl_status_esc | ||||
re_id2 += r"(.*?)\}\})(<!-- Bot Case ID \(please don't modify\): .*? -->)?" | |||||
repl = ur"\1 <!-- Bot Case ID (please don't modify): {0} -->" | |||||
re_id2 += ( | |||||
r"(.*?)\}\})(<!-- Bot Case ID \(please don't modify\): .*? -->)?" | |||||
) | |||||
repl = r"\1 <!-- Bot Case ID (please don't modify): {0} -->" | |||||
body = re.sub(re_id2, repl.format(id_), body) | body = re.sub(re_id2, repl.format(id_), body) | ||||
re_f = r"\{\{drn filing editor\|(.*?)\|" | re_f = r"\{\{drn filing editor\|(.*?)\|" | ||||
re_f += r"(\d{2}:\d{2},\s\d{1,2}\s\w+\s\d{4}\s\(UTC\))\}\}" | re_f += r"(\d{2}:\d{2},\s\d{1,2}\s\w+\s\d{4}\s\(UTC\))\}\}" | ||||
@@ -222,16 +227,30 @@ class DRNClerkBot(Task): | |||||
f_time = datetime.strptime(match.group(2), strp) | f_time = datetime.strptime(match.group(2), strp) | ||||
else: | else: | ||||
f_user, f_time = None, datetime.utcnow() | f_user, f_time = None, datetime.utcnow() | ||||
case = _Case(id_, title, status, self.STATUS_UNKNOWN, f_user, | |||||
f_time, f_user, f_time, "", self.min_ts, | |||||
self.min_ts, False, False, False, len(body), | |||||
new=True) | |||||
case = _Case( | |||||
id_, | |||||
title, | |||||
status, | |||||
self.STATUS_UNKNOWN, | |||||
f_user, | |||||
f_time, | |||||
f_user, | |||||
f_time, | |||||
"", | |||||
self.min_ts, | |||||
self.min_ts, | |||||
False, | |||||
False, | |||||
False, | |||||
len(body), | |||||
new=True, | |||||
) | |||||
cases.append(case) | cases.append(case) | ||||
log = u"Added new case {0} ('{1}', status={2}, by {3})" | |||||
log = "Added new case {0} ('{1}', status={2}, by {3})" | |||||
self.logger.debug(log.format(id_, title, status, f_user)) | self.logger.debug(log.format(id_, title, status, f_user)) | ||||
else: | else: | ||||
case.status = status | case.status = status | ||||
log = u"Read active case {0} ('{1}')".format(id_, title) | |||||
log = f"Read active case {id_} ('{title}')" | |||||
self.logger.debug(log) | self.logger.debug(log) | ||||
if case.title != title: | if case.title != title: | ||||
self.update_case_title(conn, id_, title) | self.update_case_title(conn, id_, title) | ||||
@@ -244,7 +263,7 @@ class DRNClerkBot(Task): | |||||
cases.remove(case) # Ignore archived case | cases.remove(case) # Ignore archived case | ||||
else: | else: | ||||
case.status = self.STATUS_UNKNOWN | case.status = self.STATUS_UNKNOWN | ||||
log = u"Dropped case {0} because it is no longer on the page ('{1}')" | |||||
log = "Dropped case {0} because it is no longer on the page ('{1}')" | |||||
self.logger.debug(log.format(case.id, case.title)) | self.logger.debug(log.format(case.id, case.title)) | ||||
self.logger.debug("Done reading cases from the noticeboard page") | self.logger.debug("Done reading cases from the noticeboard page") | ||||
@@ -262,7 +281,7 @@ class DRNClerkBot(Task): | |||||
def read_status(self, body): | def read_status(self, body): | ||||
"""Parse the current status from a case body.""" | """Parse the current status from a case body.""" | ||||
templ = re.escape(self.tl_status) | templ = re.escape(self.tl_status) | ||||
status = re.search("\{\{" + templ + "\|?(.*?)\}\}", body, re.S|re.U) | |||||
status = re.search("\{\{" + templ + "\|?(.*?)\}\}", body, re.S | re.U) | |||||
if not status: | if not status: | ||||
return self.STATUS_NEW | return self.STATUS_NEW | ||||
for option, names in self.ALIASES.iteritems(): | for option, names in self.ALIASES.iteritems(): | ||||
@@ -275,7 +294,7 @@ class DRNClerkBot(Task): | |||||
query = "UPDATE cases SET case_title = ? WHERE case_id = ?" | query = "UPDATE cases SET case_title = ? WHERE case_id = ?" | ||||
with conn.cursor() as cursor: | with conn.cursor() as cursor: | ||||
cursor.execute(query, (title, id_)) | cursor.execute(query, (title, id_)) | ||||
log = u"Updated title of case {0} to '{1}'".format(id_, title) | |||||
log = f"Updated title of case {id_} to '{title}'" | |||||
self.logger.debug(log) | self.logger.debug(log) | ||||
def clerk(self, conn, cases): | def clerk(self, conn, cases): | ||||
@@ -286,7 +305,7 @@ class DRNClerkBot(Task): | |||||
volunteers = [name for (name,) in cursor.fetchall()] | volunteers = [name for (name,) in cursor.fetchall()] | ||||
notices = [] | notices = [] | ||||
for case in cases: | for case in cases: | ||||
log = u"Clerking case {0} ('{1}')".format(case.id, case.title) | |||||
log = f"Clerking case {case.id} ('{case.title}')" | |||||
self.logger.debug(log) | self.logger.debug(log) | ||||
if case.status == self.STATUS_UNKNOWN: | if case.status == self.STATUS_UNKNOWN: | ||||
self.save_existing_case(conn, case) | self.save_existing_case(conn, case) | ||||
@@ -312,8 +331,11 @@ class DRNClerkBot(Task): | |||||
notices = self.clerk_needassist_case(case, volunteers, newsigs) | notices = self.clerk_needassist_case(case, volunteers, newsigs) | ||||
elif case.status == self.STATUS_STALE: | elif case.status == self.STATUS_STALE: | ||||
notices = self.clerk_stale_case(case, newsigs) | notices = self.clerk_stale_case(case, newsigs) | ||||
if case.status in [self.STATUS_RESOLVED, self.STATUS_CLOSED, | |||||
self.STATUS_FAILED]: | |||||
if case.status in [ | |||||
self.STATUS_RESOLVED, | |||||
self.STATUS_CLOSED, | |||||
self.STATUS_FAILED, | |||||
]: | |||||
self.clerk_closed_case(case, signatures) | self.clerk_closed_case(case, signatures) | ||||
else: | else: | ||||
self.add_missing_reflist(case) | self.add_missing_reflist(case) | ||||
@@ -374,10 +396,10 @@ class DRNClerkBot(Task): | |||||
tmpl = self.tl_notify_stale | tmpl = self.tl_notify_stale | ||||
title = case.title.replace("|", "|") | title = case.title.replace("|", "|") | ||||
template = "{{subst:" + tmpl + "|" + title + "}}" | template = "{{subst:" + tmpl + "|" + title + "}}" | ||||
miss = "<!-- Template:DRN stale notice | {0} -->".format(title) | |||||
miss = f"<!-- Template:DRN stale notice | {title} -->" | |||||
notice = _Notice(self.very_old_title, template, miss) | notice = _Notice(self.very_old_title, template, miss) | ||||
case.very_old_notified = True | case.very_old_notified = True | ||||
msg = u" {0}: will notify [[{1}]] with '{2}'" | |||||
msg = " {0}: will notify [[{1}]] with '{2}'" | |||||
log = msg.format(case.id, self.very_old_title, template) | log = msg.format(case.id, self.very_old_title, template) | ||||
self.logger.debug(log) | self.logger.debug(log) | ||||
return [notice] | return [notice] | ||||
@@ -428,7 +450,7 @@ class DRNClerkBot(Task): | |||||
if not re.search(arch_bottom + r"\s*\}\}\s*\Z", case.body): | if not re.search(arch_bottom + r"\s*\}\}\s*\Z", case.body): | ||||
case.body += "\n{{" + arch_bottom + "}}" | case.body += "\n{{" + arch_bottom + "}}" | ||||
case.archived = True | case.archived = True | ||||
self.logger.debug(u" {0}: archived case".format(case.id)) | |||||
self.logger.debug(f" {case.id}: archived case") | |||||
def check_for_needassist(self, case): | def check_for_needassist(self, case): | ||||
"""Check whether a case is old enough to be set to "needassist".""" | """Check whether a case is old enough to be set to "needassist".""" | ||||
@@ -446,10 +468,10 @@ class DRNClerkBot(Task): | |||||
new_n = "NEW" if not new_n else new_n | new_n = "NEW" if not new_n else new_n | ||||
if case.last_action != new: | if case.last_action != new: | ||||
case.status = new | case.status = new | ||||
log = u" {0}: {1} -> {2}" | |||||
log = " {0}: {1} -> {2}" | |||||
self.logger.debug(log.format(case.id, old_n, new_n)) | self.logger.debug(log.format(case.id, old_n, new_n)) | ||||
return | return | ||||
log = u"Avoiding {0} {1} -> {2} because we already did this ('{3}')" | |||||
log = "Avoiding {0} {1} -> {2} because we already did this ('{3}')" | |||||
self.logger.info(log.format(case.id, old_n, new_n, case.title)) | self.logger.info(log.format(case.id, old_n, new_n, case.title)) | ||||
def read_signatures(self, text): | def read_signatures(self, text): | ||||
@@ -461,7 +483,7 @@ class DRNClerkBot(Task): | |||||
regex += r"([^\n\[\]|]{,256}?)(?:\||\]\])" | regex += r"([^\n\[\]|]{,256}?)(?:\||\]\])" | ||||
regex += r"(?!.*?(?:User(?:\stalk)?\:|Special\:Contributions\/).*?)" | regex += r"(?!.*?(?:User(?:\stalk)?\:|Special\:Contributions\/).*?)" | ||||
regex += r".{,256}?(\d{2}:\d{2},\s\d{1,2}\s\w+\s\d{4}\s\(UTC\))" | regex += r".{,256}?(\d{2}:\d{2},\s\d{1,2}\s\w+\s\d{4}\s\(UTC\))" | ||||
matches = re.findall(regex, text, re.U|re.I) | |||||
matches = re.findall(regex, text, re.U | re.I) | |||||
signatures = [] | signatures = [] | ||||
for userlink, stamp in matches: | for userlink, stamp in matches: | ||||
username = userlink.split("/", 1)[0].replace("_", " ").strip() | username = userlink.split("/", 1)[0].replace("_", " ").strip() | ||||
@@ -494,13 +516,13 @@ class DRNClerkBot(Task): | |||||
too_late = "<!--Template:DRN-notice-->" | too_late = "<!--Template:DRN-notice-->" | ||||
re_parties = "<span.*?>'''Users involved'''</span>(.*?)<span.*?>" | re_parties = "<span.*?>'''Users involved'''</span>(.*?)<span.*?>" | ||||
text = re.search(re_parties, case.body, re.S|re.U) | |||||
text = re.search(re_parties, case.body, re.S | re.U) | |||||
for line in text.group(1).splitlines(): | for line in text.group(1).splitlines(): | ||||
user = re.search("[:*#]{,5} \{\{User\|(.*?)\}\}", line) | user = re.search("[:*#]{,5} \{\{User\|(.*?)\}\}", line) | ||||
if user: | if user: | ||||
party = user.group(1).replace("_", " ").strip() | party = user.group(1).replace("_", " ").strip() | ||||
if party.startswith("User:"): | if party.startswith("User:"): | ||||
party = party[len("User:"):] | |||||
party = party[len("User:") :] | |||||
if party: | if party: | ||||
party = party[0].upper() + party[1:] | party = party[0].upper() + party[1:] | ||||
if party == case.file_user: | if party == case.file_user: | ||||
@@ -509,7 +531,7 @@ class DRNClerkBot(Task): | |||||
notices.append(notice) | notices.append(notice) | ||||
case.parties_notified = True | case.parties_notified = True | ||||
log = u" {0}: will try to notify {1} parties with '{2}'" | |||||
log = " {0}: will try to notify {1} parties with '{2}'" | |||||
self.logger.debug(log.format(case.id, len(notices), template)) | self.logger.debug(log.format(case.id, len(notices), template)) | ||||
return notices | return notices | ||||
@@ -562,22 +584,35 @@ class DRNClerkBot(Task): | |||||
for name, stamp in additions: | for name, stamp in additions: | ||||
args.append((case.id, name, stamp)) | args.append((case.id, name, stamp)) | ||||
cursor.executemany(query2, args) | cursor.executemany(query2, args) | ||||
msg = u" {0}: added {1} signatures and removed {2}" | |||||
msg = " {0}: added {1} signatures and removed {2}" | |||||
log = msg.format(case.id, len(additions), len(removals)) | log = msg.format(case.id, len(additions), len(removals)) | ||||
self.logger.debug(log) | self.logger.debug(log) | ||||
def save_new_case(self, conn, case): | def save_new_case(self, conn, case): | ||||
"""Save a brand new case to the database.""" | """Save a brand new case to the database.""" | ||||
args = (case.id, case.title, case.status, case.last_action, | |||||
case.file_user, case.file_time, case.modify_user, | |||||
case.modify_time, case.volunteer_user, case.volunteer_time, | |||||
case.close_time, case.parties_notified, | |||||
case.very_old_notified, case.archived, | |||||
case.last_volunteer_size) | |||||
args = ( | |||||
case.id, | |||||
case.title, | |||||
case.status, | |||||
case.last_action, | |||||
case.file_user, | |||||
case.file_time, | |||||
case.modify_user, | |||||
case.modify_time, | |||||
case.volunteer_user, | |||||
case.volunteer_time, | |||||
case.close_time, | |||||
case.parties_notified, | |||||
case.very_old_notified, | |||||
case.archived, | |||||
case.last_volunteer_size, | |||||
) | |||||
with conn.cursor() as cursor: | with conn.cursor() as cursor: | ||||
query = "INSERT INTO cases VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" | |||||
query = ( | |||||
"INSERT INTO cases VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" | |||||
) | |||||
cursor.execute(query, args) | cursor.execute(query, args) | ||||
log = u" {0}: inserted new case into database".format(case.id) | |||||
log = f" {case.id}: inserted new case into database" | |||||
self.logger.debug(log) | self.logger.debug(log) | ||||
def save_existing_case(self, conn, case): | def save_existing_case(self, conn, case): | ||||
@@ -602,22 +637,22 @@ class DRNClerkBot(Task): | |||||
("case_parties_notified", case.parties_notified), | ("case_parties_notified", case.parties_notified), | ||||
("case_very_old_notified", case.very_old_notified), | ("case_very_old_notified", case.very_old_notified), | ||||
("case_archived", case.archived), | ("case_archived", case.archived), | ||||
("case_last_volunteer_size", case.last_volunteer_size) | |||||
("case_last_volunteer_size", case.last_volunteer_size), | |||||
] | ] | ||||
for column, data in fields_to_check: | for column, data in fields_to_check: | ||||
if data != stored[column]: | if data != stored[column]: | ||||
changes.append(column + " = ?") | changes.append(column + " = ?") | ||||
args.append(data) | args.append(data) | ||||
msg = u" {0}: will alter {1} ('{2}' -> '{3}')" | |||||
msg = " {0}: will alter {1} ('{2}' -> '{3}')" | |||||
log = msg.format(case.id, column, stored[column], data) | log = msg.format(case.id, column, stored[column], data) | ||||
self.logger.debug(log) | self.logger.debug(log) | ||||
if changes: | if changes: | ||||
changes = ", ".join(changes) | changes = ", ".join(changes) | ||||
args.append(case.id) | args.append(case.id) | ||||
query = "UPDATE cases SET {0} WHERE case_id = ?".format(changes) | |||||
query = f"UPDATE cases SET {changes} WHERE case_id = ?" | |||||
cursor.execute(query, args) | cursor.execute(query, args) | ||||
else: | else: | ||||
log = u" {0}: no changes to commit".format(case.id) | |||||
log = f" {case.id}: no changes to commit" | |||||
self.logger.debug(log) | self.logger.debug(log) | ||||
def save(self, page, cases, kwargs, start): | def save(self, page, cases, kwargs, start): | ||||
@@ -629,7 +664,7 @@ class DRNClerkBot(Task): | |||||
newtext = newtext.replace(case.old, case.body) | newtext = newtext.replace(case.old, case.body) | ||||
counter += 1 | counter += 1 | ||||
if newtext == text: | if newtext == text: | ||||
self.logger.info(u"Nothing to edit on [[{0}]]".format(page.title)) | |||||
self.logger.info(f"Nothing to edit on [[{page.title}]]") | |||||
return True | return True | ||||
worktime = time() - start | worktime = time() - start | ||||
@@ -646,7 +681,7 @@ class DRNClerkBot(Task): | |||||
summary = self.clerk_summary.replace("$3", str(counter)) | summary = self.clerk_summary.replace("$3", str(counter)) | ||||
summary = summary.replace("$4", "" if counter == 1 else "s") | summary = summary.replace("$4", "" if counter == 1 else "s") | ||||
page.edit(newtext, summary, minor=True, bot=True) | page.edit(newtext, summary, minor=True, bot=True) | ||||
log = u"Saved page [[{0}]] ({1} updates)" | |||||
log = "Saved page [[{0}]] ({1} updates)" | |||||
self.logger.info(log.format(page.title, counter)) | self.logger.info(log.format(page.title, counter)) | ||||
return True | return True | ||||
@@ -657,13 +692,13 @@ class DRNClerkBot(Task): | |||||
return | return | ||||
for notice in notices: | for notice in notices: | ||||
target, template = notice.target, notice.template | target, template = notice.target, notice.template | ||||
log = u"Trying to notify [[{0}]] with '{1}'" | |||||
log = "Trying to notify [[{0}]] with '{1}'" | |||||
self.logger.debug(log.format(target, template)) | self.logger.debug(log.format(target, template)) | ||||
page = site.get_page(target, follow_redirects=True) | page = site.get_page(target, follow_redirects=True) | ||||
if page.namespace == constants.NS_USER_TALK: | if page.namespace == constants.NS_USER_TALK: | ||||
user = site.get_user(target.split(":", 1)[1]) | user = site.get_user(target.split(":", 1)[1]) | ||||
if not user.exists and not user.is_ip: | if not user.exists and not user.is_ip: | ||||
log = u"Skipping [[{0}]]; user does not exist and is not an IP" | |||||
log = "Skipping [[{0}]]; user does not exist and is not an IP" | |||||
self.logger.info(log.format(target)) | self.logger.info(log.format(target)) | ||||
continue | continue | ||||
try: | try: | ||||
@@ -671,7 +706,7 @@ class DRNClerkBot(Task): | |||||
except exceptions.PageNotFoundError: | except exceptions.PageNotFoundError: | ||||
text = "" | text = "" | ||||
if notice.too_late and notice.too_late in text: | if notice.too_late and notice.too_late in text: | ||||
log = u"Skipping [[{0}]]; was already notified with '{1}'" | |||||
log = "Skipping [[{0}]]; was already notified with '{1}'" | |||||
self.logger.info(log.format(page.title, template)) | self.logger.info(log.format(page.title, template)) | ||||
continue | continue | ||||
text += ("\n" if text else "") + template | text += ("\n" if text else "") + template | ||||
@@ -679,10 +714,10 @@ class DRNClerkBot(Task): | |||||
page.edit(text, self.notify_summary, minor=False, bot=True) | page.edit(text, self.notify_summary, minor=False, bot=True) | ||||
except exceptions.EditError as error: | except exceptions.EditError as error: | ||||
name, msg = type(error).name, error.message | name, msg = type(error).name, error.message | ||||
log = u"Couldn't leave notice on [[{0}]] because of {1}: {2}" | |||||
log = "Couldn't leave notice on [[{0}]] because of {1}: {2}" | |||||
self.logger.error(log.format(page.title, name, msg)) | self.logger.error(log.format(page.title, name, msg)) | ||||
else: | else: | ||||
log = u"Notified [[{0}]] with '{1}'" | |||||
log = "Notified [[{0}]] with '{1}'" | |||||
self.logger.info(log.format(page.title, template)) | self.logger.info(log.format(page.title, template)) | ||||
self.logger.debug("Done sending notices") | self.logger.debug("Done sending notices") | ||||
@@ -690,25 +725,34 @@ class DRNClerkBot(Task): | |||||
def update_chart(self, conn, site): | def update_chart(self, conn, site): | ||||
"""Update the chart of open or recently closed cases.""" | """Update the chart of open or recently closed cases.""" | ||||
page = site.get_page(self.chart_title) | page = site.get_page(self.chart_title) | ||||
self.logger.info(u"Updating case status at [[{0}]]".format(page.title)) | |||||
self.logger.info(f"Updating case status at [[{page.title}]]") | |||||
statuses = self.compile_chart(conn) | statuses = self.compile_chart(conn) | ||||
text = page.get() | text = page.get() | ||||
newtext = re.sub(u"<!-- status begin -->(.*?)<!-- status end -->", | |||||
"<!-- status begin -->\n" + statuses + "\n<!-- status end -->", | |||||
text, flags=re.DOTALL) | |||||
newtext = re.sub( | |||||
"<!-- status begin -->(.*?)<!-- status end -->", | |||||
"<!-- status begin -->\n" + statuses + "\n<!-- status end -->", | |||||
text, | |||||
flags=re.DOTALL, | |||||
) | |||||
if newtext == text: | if newtext == text: | ||||
self.logger.info("Chart unchanged; not saving") | self.logger.info("Chart unchanged; not saving") | ||||
return | return | ||||
newtext = re.sub("<!-- sig begin -->(.*?)<!-- sig end -->", | |||||
"<!-- sig begin -->~~~ at ~~~~~<!-- sig end -->", | |||||
newtext) | |||||
newtext = re.sub( | |||||
"<!-- sig begin -->(.*?)<!-- sig end -->", | |||||
"<!-- sig begin -->~~~ at ~~~~~<!-- sig end -->", | |||||
newtext, | |||||
) | |||||
page.edit(newtext, self.chart_summary, minor=True, bot=True) | page.edit(newtext, self.chart_summary, minor=True, bot=True) | ||||
self.logger.info(u"Chart saved to [[{0}]]".format(page.title)) | |||||
self.logger.info(f"Chart saved to [[{page.title}]]") | |||||
def compile_chart(self, conn): | def compile_chart(self, conn): | ||||
"""Actually generate the chart from the database.""" | """Actually generate the chart from the database.""" | ||||
chart = "{{" + self.tl_chart_header + "|small={{{small|}}}|collapsed={{{collapsed|}}}}}\n" | |||||
chart = ( | |||||
"{{" | |||||
+ self.tl_chart_header | |||||
+ "|small={{{small|}}}|collapsed={{{collapsed|}}}}}\n" | |||||
) | |||||
query = "SELECT * FROM cases WHERE case_status != ?" | query = "SELECT * FROM cases WHERE case_status != ?" | ||||
with conn.cursor(oursql.DictCursor) as cursor: | with conn.cursor(oursql.DictCursor) as cursor: | ||||
cursor.execute(query, (self.STATUS_UNKNOWN,)) | cursor.execute(query, (self.STATUS_UNKNOWN,)) | ||||
@@ -719,12 +763,16 @@ class DRNClerkBot(Task): | |||||
def compile_row(self, case): | def compile_row(self, case): | ||||
"""Generate a single row of the chart from a dict via the database.""" | """Generate a single row of the chart from a dict via the database.""" | ||||
data = u"|t={case_title}|d={title}|s={case_status}" | |||||
data = "|t={case_title}|d={title}|s={case_status}" | |||||
data += "|cu={case_file_user}|cs={file_sortkey}|ct={file_time}" | data += "|cu={case_file_user}|cs={file_sortkey}|ct={file_time}" | ||||
if case["case_volunteer_user"]: | if case["case_volunteer_user"]: | ||||
data += "|vu={case_volunteer_user}|vs={volunteer_sortkey}|vt={volunteer_time}" | |||||
data += ( | |||||
"|vu={case_volunteer_user}|vs={volunteer_sortkey}|vt={volunteer_time}" | |||||
) | |||||
case["volunteer_time"] = self.format_time(case["case_volunteer_time"]) | case["volunteer_time"] = self.format_time(case["case_volunteer_time"]) | ||||
case["volunteer_sortkey"] = int(mktime(case["case_volunteer_time"].timetuple())) | |||||
case["volunteer_sortkey"] = int( | |||||
mktime(case["case_volunteer_time"].timetuple()) | |||||
) | |||||
data += "|mu={case_modify_user}|ms={modify_sortkey}|mt={modify_time}" | data += "|mu={case_modify_user}|ms={modify_sortkey}|mt={modify_time}" | ||||
case["case_title"] = mw_parse(case["case_title"]).strip_code() | case["case_title"] = mw_parse(case["case_title"]).strip_code() | ||||
@@ -748,7 +796,7 @@ class DRNClerkBot(Task): | |||||
num = seconds // size | num = seconds // size | ||||
seconds -= num * size | seconds -= num * size | ||||
if num: | if num: | ||||
chunk = "{0} {1}".format(num, name if num == 1 else name + "s") | |||||
chunk = "{} {}".format(num, name if num == 1 else name + "s") | |||||
msg.append(chunk) | msg.append(chunk) | ||||
return ", ".join(msg) + " ago" if msg else "0 hours ago" | return ", ".join(msg) + " ago" if msg else "0 hours ago" | ||||
@@ -766,12 +814,28 @@ class DRNClerkBot(Task): | |||||
cursor.execute(query, (self.STATUS_UNKNOWN,)) | cursor.execute(query, (self.STATUS_UNKNOWN,)) | ||||
class _Case(object): | |||||
class _Case: | |||||
"""A object representing a dispute resolution case.""" | """A object representing a dispute resolution case.""" | ||||
def __init__(self, id_, title, status, last_action, file_user, file_time, | |||||
modify_user, modify_time, volunteer_user, volunteer_time, | |||||
close_time, parties_notified, archived, very_old_notified, | |||||
last_volunteer_size, new=False): | |||||
def __init__( | |||||
self, | |||||
id_, | |||||
title, | |||||
status, | |||||
last_action, | |||||
file_user, | |||||
file_time, | |||||
modify_user, | |||||
modify_time, | |||||
volunteer_user, | |||||
volunteer_time, | |||||
close_time, | |||||
parties_notified, | |||||
archived, | |||||
very_old_notified, | |||||
last_volunteer_size, | |||||
new=False, | |||||
): | |||||
self.id = id_ | self.id = id_ | ||||
self.title = title | self.title = title | ||||
self.status = status | self.status = status | ||||
@@ -794,8 +858,9 @@ class _Case(object): | |||||
self.old = None | self.old = None | ||||
class _Notice(object): | |||||
class _Notice: | |||||
"""An object representing a notice to be sent to a user or a page.""" | """An object representing a notice to be sent to a user or a page.""" | ||||
def __init__(self, target, template, too_late=None): | def __init__(self, target, template, too_late=None): | ||||
self.target = target | self.target = target | ||||
self.template = template | self.template = template | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,19 +18,20 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from __future__ import unicode_literals | |||||
from time import sleep | from time import sleep | ||||
import mwparserfromhell | |||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
from earwigbot.wiki import constants | from earwigbot.wiki import constants | ||||
import mwparserfromhell | |||||
class InfoboxStation(Task): | class InfoboxStation(Task): | ||||
""" | """ | ||||
A task to replace ``{{Infobox China station}}`` and | A task to replace ``{{Infobox China station}}`` and | ||||
``{{Infobox Japan station}}`` with ``{{Infobox station}}``. | ``{{Infobox Japan station}}`` with ``{{Infobox station}}``. | ||||
""" | """ | ||||
name = "infobox_station" | name = "infobox_station" | ||||
number = 20 | number = 20 | ||||
@@ -43,19 +42,20 @@ class InfoboxStation(Task): | |||||
["Infobox China station", "Infobox china station"], | ["Infobox China station", "Infobox china station"], | ||||
"Infobox China station/sandbox", | "Infobox China station/sandbox", | ||||
"Infobox China station/sandbox/cats", | "Infobox China station/sandbox/cats", | ||||
"Wikipedia:Templates for discussion/Log/2015 February 8#Template:Infobox China station" | |||||
"Wikipedia:Templates for discussion/Log/2015 February 8#Template:Infobox China station", | |||||
), | ), | ||||
"Japan": ( | "Japan": ( | ||||
["Infobox Japan station", "Infobox japan station"], | ["Infobox Japan station", "Infobox japan station"], | ||||
"Infobox Japan station/sandbox", | "Infobox Japan station/sandbox", | ||||
"Infobox Japan station/sandbox/cats", | "Infobox Japan station/sandbox/cats", | ||||
"Wikipedia:Templates for discussion/Log/2015 May 9#Template:Infobox Japan station" | |||||
"Wikipedia:Templates for discussion/Log/2015 May 9#Template:Infobox Japan station", | |||||
), | ), | ||||
} | } | ||||
self._replacement = "{{Infobox station}}" | self._replacement = "{{Infobox station}}" | ||||
self._sleep_time = 2 | self._sleep_time = 2 | ||||
self.summary = self.make_summary( | self.summary = self.make_summary( | ||||
"Replacing {source} with {dest} per [[{discussion}|TfD]].") | |||||
"Replacing {source} with {dest} per [[{discussion}|TfD]]." | |||||
) | |||||
def run(self, **kwargs): | def run(self, **kwargs): | ||||
limit = int(kwargs.get("limit", kwargs.get("edits", 0))) | limit = int(kwargs.get("limit", kwargs.get("edits", 0))) | ||||
@@ -68,7 +68,7 @@ class InfoboxStation(Task): | |||||
""" | """ | ||||
Replace a template in all pages that transclude it. | Replace a template in all pages that transclude it. | ||||
""" | """ | ||||
self.logger.info("Replacing {0} infobox template".format(name)) | |||||
self.logger.info(f"Replacing {name} infobox template") | |||||
count = 0 | count = 0 | ||||
for title in self._get_transclusions(args[0][0]): | for title in self._get_transclusions(args[0][0]): | ||||
@@ -82,15 +82,15 @@ class InfoboxStation(Task): | |||||
page = self.site.get_page(title) | page = self.site.get_page(title) | ||||
self._process_page(page, args) | self._process_page(page, args) | ||||
self.logger.info("All {0} infoboxes updated".format(name)) | |||||
self.logger.info(f"All {name} infoboxes updated") | |||||
def _process_page(self, page, args): | def _process_page(self, page, args): | ||||
""" | """ | ||||
Process a single page to replace a template. | Process a single page to replace a template. | ||||
""" | """ | ||||
self.logger.debug("Processing [[{0}]]".format(page.title)) | |||||
self.logger.debug(f"Processing [[{page.title}]]") | |||||
if not page.check_exclusion(): | if not page.check_exclusion(): | ||||
self.logger.warn("Bot excluded from [[{0}]]".format(page.title)) | |||||
self.logger.warn(f"Bot excluded from [[{page.title}]]") | |||||
return | return | ||||
code = mwparserfromhell.parse(page.get(), skip_style_tags=True) | code = mwparserfromhell.parse(page.get(), skip_style_tags=True) | ||||
@@ -98,7 +98,7 @@ class InfoboxStation(Task): | |||||
for tmpl in code.filter_templates(): | for tmpl in code.filter_templates(): | ||||
if tmpl.name.matches(args[0]): | if tmpl.name.matches(args[0]): | ||||
tmpl.name = "subst:" + args[2] | tmpl.name = "subst:" + args[2] | ||||
cats.extend(self._get_cats(page, unicode(tmpl))) | |||||
cats.extend(self._get_cats(page, str(tmpl))) | |||||
tmpl.name = "subst:" + args[1] | tmpl.name = "subst:" + args[1] | ||||
self._add_cats(code, cats) | self._add_cats(code, cats) | ||||
@@ -108,19 +108,25 @@ class InfoboxStation(Task): | |||||
return | return | ||||
summary = self.summary.format( | summary = self.summary.format( | ||||
source="{{" + args[0][0] + "}}", dest=self._replacement, | |||||
discussion=args[3]) | |||||
page.edit(unicode(code), summary, minor=True) | |||||
source="{{" + args[0][0] + "}}", dest=self._replacement, discussion=args[3] | |||||
) | |||||
page.edit(str(code), summary, minor=True) | |||||
sleep(self._sleep_time) | sleep(self._sleep_time) | ||||
def _add_cats(self, code, cats): | def _add_cats(self, code, cats): | ||||
"""Add category data (*cats*) to wikicode.""" | """Add category data (*cats*) to wikicode.""" | ||||
current_cats = code.filter_wikilinks( | current_cats = code.filter_wikilinks( | ||||
matches=lambda link: link.title.lower().startswith("category:")) | |||||
norm = lambda cat: cat.title.lower()[len("category:"):].strip() | |||||
matches=lambda link: link.title.lower().startswith("category:") | |||||
) | |||||
def norm(cat): | |||||
return cat.title.lower()[len("category:") :].strip() | |||||
catlist = [unicode(cat) for cat in cats if not any( | |||||
norm(cur) == norm(cat) for cur in current_cats)] | |||||
catlist = [ | |||||
str(cat) | |||||
for cat in cats | |||||
if not any(norm(cur) == norm(cat) for cur in current_cats) | |||||
] | |||||
if not catlist: | if not catlist: | ||||
return | return | ||||
text = "\n".join(catlist) | text = "\n".join(catlist) | ||||
@@ -140,8 +146,9 @@ class InfoboxStation(Task): | |||||
""" | """ | ||||
Return the categories that should be added to the page. | Return the categories that should be added to the page. | ||||
""" | """ | ||||
result = self.site.api_query(action="parse", title=page.title, | |||||
prop="text", onlypst=1, text=tmpl) | |||||
result = self.site.api_query( | |||||
action="parse", title=page.title, prop="text", onlypst=1, text=tmpl | |||||
) | |||||
text = result["parse"]["text"]["*"] | text = result["parse"]["text"]["*"] | ||||
return mwparserfromhell.parse(text).filter_wikilinks() | return mwparserfromhell.parse(text).filter_wikilinks() | ||||
@@ -154,6 +161,7 @@ class InfoboxStation(Task): | |||||
LEFT JOIN page ON tl_from = page_id | LEFT JOIN page ON tl_from = page_id | ||||
WHERE tl_namespace = ? AND tl_title = ? AND tl_from_namespace = ?""" | WHERE tl_namespace = ? AND tl_title = ? AND tl_from_namespace = ?""" | ||||
results = self.site.sql_query(query, ( | |||||
constants.NS_TEMPLATE, tmpl.replace(" ", "_"), constants.NS_MAIN)) | |||||
results = self.site.sql_query( | |||||
query, (constants.NS_TEMPLATE, tmpl.replace(" ", "_"), constants.NS_MAIN) | |||||
) | |||||
return [title.decode("utf8").replace("_", " ") for (title,) in results] | return [title.decode("utf8").replace("_", " ") for (title,) in results] |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2021 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2021 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -33,41 +31,43 @@ import unidecode | |||||
from earwigbot.tasks import Task | from earwigbot.tasks import Task | ||||
class SynonymAuthorities(Task): | class SynonymAuthorities(Task): | ||||
""" | """ | ||||
Correct mismatched synonym authorities in taxon articles created by Qbugbot. | Correct mismatched synonym authorities in taxon articles created by Qbugbot. | ||||
""" | """ | ||||
name = 'synonym_authorities' | |||||
name = "synonym_authorities" | |||||
number = 21 | number = 21 | ||||
base_summary = ( | base_summary = ( | ||||
'Fix {changes} mismatched synonym authorities per ITIS ' | |||||
'([[Wikipedia:Bots/Requests for approval/EarwigBot 21|more info]])' | |||||
"Fix {changes} mismatched synonym authorities per ITIS " | |||||
"([[Wikipedia:Bots/Requests for approval/EarwigBot 21|more info]])" | |||||
) | ) | ||||
def setup(self): | def setup(self): | ||||
self.site = self.bot.wiki.get_site() | self.site = self.bot.wiki.get_site() | ||||
self.creator = 'Qbugbot' | |||||
self.pages_path = 'qbugbot_pages.json' | |||||
self.synonyms_path = 'qbugbot_synonyms.json' | |||||
self.edits_path = 'qbugbot_edits.json' | |||||
self.itis_path = 'itis.db' | |||||
self.creator = "Qbugbot" | |||||
self.pages_path = "qbugbot_pages.json" | |||||
self.synonyms_path = "qbugbot_synonyms.json" | |||||
self.edits_path = "qbugbot_edits.json" | |||||
self.itis_path = "itis.db" | |||||
self.summary = self.make_summary(self.base_summary) | self.summary = self.make_summary(self.base_summary) | ||||
def run(self, action=None): | def run(self, action=None): | ||||
if action == 'fetch_pages': | |||||
if action == "fetch_pages": | |||||
self.fetch_pages() | self.fetch_pages() | ||||
elif action == 'fetch_synonyms': | |||||
elif action == "fetch_synonyms": | |||||
self.fetch_synonyms() | self.fetch_synonyms() | ||||
elif action == 'prepare_edits': | |||||
elif action == "prepare_edits": | |||||
self.prepare_edits() | self.prepare_edits() | ||||
elif action == 'view_edits': | |||||
elif action == "view_edits": | |||||
self.view_edits() | self.view_edits() | ||||
elif action == 'save_edits': | |||||
elif action == "save_edits": | |||||
self.save_edits() | self.save_edits() | ||||
elif action is None: | elif action is None: | ||||
raise RuntimeError(f'This task requires an action') | |||||
raise RuntimeError("This task requires an action") | |||||
else: | else: | ||||
raise RuntimeError(f'No such action: {action}') | |||||
raise RuntimeError(f"No such action: {action}") | |||||
def fetch_pages(self): | def fetch_pages(self): | ||||
""" | """ | ||||
@@ -77,49 +77,49 @@ class SynonymAuthorities(Task): | |||||
for chunk in more_itertools.chunked(self._iter_creations(), 500): | for chunk in more_itertools.chunked(self._iter_creations(), 500): | ||||
pages.update(self._fetch_chunk(chunk)) | pages.update(self._fetch_chunk(chunk)) | ||||
self.logger.info(f'Fetched {len(pages)} pages') | |||||
with open(self.pages_path, 'w') as fp: | |||||
self.logger.info(f"Fetched {len(pages)} pages") | |||||
with open(self.pages_path, "w") as fp: | |||||
json.dump(pages, fp) | json.dump(pages, fp) | ||||
def _iter_creations(self): | def _iter_creations(self): | ||||
# TODO: include converted redirects ([[Category:Articles created by Qbugbot]]) | # TODO: include converted redirects ([[Category:Articles created by Qbugbot]]) | ||||
params = { | params = { | ||||
'action': 'query', | |||||
'list': 'usercontribs', | |||||
'ucuser': self.creator, | |||||
'uclimit': 5000, | |||||
'ucnamespace': 0, | |||||
'ucprop': 'ids', | |||||
'ucshow': 'new', | |||||
'formatversion': 2, | |||||
"action": "query", | |||||
"list": "usercontribs", | |||||
"ucuser": self.creator, | |||||
"uclimit": 5000, | |||||
"ucnamespace": 0, | |||||
"ucprop": "ids", | |||||
"ucshow": "new", | |||||
"formatversion": 2, | |||||
} | } | ||||
results = self.site.api_query(**params) | results = self.site.api_query(**params) | ||||
while contribs := results['query']['usercontribs']: | |||||
while contribs := results["query"]["usercontribs"]: | |||||
yield from contribs | yield from contribs | ||||
if 'continue' not in results: | |||||
if "continue" not in results: | |||||
break | break | ||||
params.update(results['continue']) | |||||
params.update(results["continue"]) | |||||
results = self.site.api_query(**params) | results = self.site.api_query(**params) | ||||
def _fetch_chunk(self, chunk): | def _fetch_chunk(self, chunk): | ||||
result = self.site.api_query( | result = self.site.api_query( | ||||
action='query', | |||||
prop='revisions', | |||||
rvprop='ids|content', | |||||
rvslots='main', | |||||
pageids='|'.join(str(page['pageid']) for page in chunk), | |||||
action="query", | |||||
prop="revisions", | |||||
rvprop="ids|content", | |||||
rvslots="main", | |||||
pageids="|".join(str(page["pageid"]) for page in chunk), | |||||
formatversion=2, | formatversion=2, | ||||
) | ) | ||||
pages = result['query']['pages'] | |||||
pages = result["query"]["pages"] | |||||
assert len(pages) == len(chunk) | assert len(pages) == len(chunk) | ||||
return { | return { | ||||
page['pageid']: { | |||||
'title': page['title'], | |||||
'content': page['revisions'][0]['slots']['main']['content'], | |||||
'revid': page['revisions'][0]['revid'], | |||||
page["pageid"]: { | |||||
"title": page["title"], | |||||
"content": page["revisions"][0]["slots"]["main"]["content"], | |||||
"revid": page["revisions"][0]["revid"], | |||||
} | } | ||||
for page in pages | for page in pages | ||||
} | } | ||||
@@ -130,37 +130,38 @@ class SynonymAuthorities(Task): | |||||
""" | """ | ||||
with open(self.pages_path) as fp: | with open(self.pages_path) as fp: | ||||
pages = json.load(fp) | pages = json.load(fp) | ||||
wikidata = self.bot.wiki.get_site('wikidatawiki') | |||||
itis_property = 'P815' | |||||
wikidata = self.bot.wiki.get_site("wikidatawiki") | |||||
itis_property = "P815" | |||||
conn = sqlite3.connect(self.itis_path) | conn = sqlite3.connect(self.itis_path) | ||||
cur = conn.cursor() | cur = conn.cursor() | ||||
synonyms = {} | synonyms = {} | ||||
for chunk in more_itertools.chunked(pages.items(), 50): | for chunk in more_itertools.chunked(pages.items(), 50): | ||||
titles = {page['title']: pageid for pageid, page in chunk} | |||||
titles = {page["title"]: pageid for pageid, page in chunk} | |||||
result = wikidata.api_query( | result = wikidata.api_query( | ||||
action='wbgetentities', | |||||
sites='enwiki', | |||||
titles='|'.join(titles), | |||||
props='claims|sitelinks', | |||||
languages='en', | |||||
sitefilter='enwiki', | |||||
action="wbgetentities", | |||||
sites="enwiki", | |||||
titles="|".join(titles), | |||||
props="claims|sitelinks", | |||||
languages="en", | |||||
sitefilter="enwiki", | |||||
) | ) | ||||
for item in result['entities'].values(): | |||||
if 'sitelinks' not in item: | |||||
self.logger.warning(f'No sitelinks for item: {item}') | |||||
for item in result["entities"].values(): | |||||
if "sitelinks" not in item: | |||||
self.logger.warning(f"No sitelinks for item: {item}") | |||||
continue | continue | ||||
title = item['sitelinks']['enwiki']['title'] | |||||
title = item["sitelinks"]["enwiki"]["title"] | |||||
pageid = titles[title] | pageid = titles[title] | ||||
if itis_property not in item['claims']: | |||||
self.logger.warning(f'No ITIS ID for [[{title}]]') | |||||
if itis_property not in item["claims"]: | |||||
self.logger.warning(f"No ITIS ID for [[{title}]]") | |||||
continue | continue | ||||
claims = item['claims'][itis_property] | |||||
claims = item["claims"][itis_property] | |||||
assert len(claims) == 1, (title, claims) | assert len(claims) == 1, (title, claims) | ||||
itis_id = claims[0]['mainsnak']['datavalue']['value'] | |||||
itis_id = claims[0]["mainsnak"]["datavalue"]["value"] | |||||
cur.execute(""" | |||||
cur.execute( | |||||
""" | |||||
SELECT synonym.complete_name, authors.taxon_author | SELECT synonym.complete_name, authors.taxon_author | ||||
FROM synonym_links sl | FROM synonym_links sl | ||||
INNER JOIN taxonomic_units accepted ON sl.tsn_accepted = accepted.tsn | INNER JOIN taxonomic_units accepted ON sl.tsn_accepted = accepted.tsn | ||||
@@ -172,11 +173,13 @@ class SynonymAuthorities(Task): | |||||
FROM taxonomic_units accepted | FROM taxonomic_units accepted | ||||
LEFT JOIN taxon_authors_lkp authors USING (taxon_author_id) | LEFT JOIN taxon_authors_lkp authors USING (taxon_author_id) | ||||
WHERE accepted.tsn = ?; | WHERE accepted.tsn = ?; | ||||
""", (itis_id, itis_id)) | |||||
""", | |||||
(itis_id, itis_id), | |||||
) | |||||
synonyms[pageid] = cur.fetchall() | synonyms[pageid] = cur.fetchall() | ||||
self.logger.info(f'Fetched {len(synonyms)} synonym lists') | |||||
with open(self.synonyms_path, 'w') as fp: | |||||
self.logger.info(f"Fetched {len(synonyms)} synonym lists") | |||||
with open(self.synonyms_path, "w") as fp: | |||||
json.dump(synonyms, fp) | json.dump(synonyms, fp) | ||||
def prepare_edits(self): | def prepare_edits(self): | ||||
@@ -192,65 +195,73 @@ class SynonymAuthorities(Task): | |||||
for pageid, pageinfo in pages.items(): | for pageid, pageinfo in pages.items(): | ||||
if pageid not in synonyms: | if pageid not in synonyms: | ||||
continue | continue | ||||
wikitext = mwparserfromhell.parse(pageinfo['content']) | |||||
wikitext = mwparserfromhell.parse(pageinfo["content"]) | |||||
try: | try: | ||||
changes = self._update_synonyms(pageinfo['title'], wikitext, synonyms[pageid]) | |||||
changes = self._update_synonyms( | |||||
pageinfo["title"], wikitext, synonyms[pageid] | |||||
) | |||||
if not changes: | if not changes: | ||||
continue | continue | ||||
except Exception: | except Exception: | ||||
self.logger.error(f'Failed to update synonyms for [[{pageinfo["title"]}]]') | |||||
self.logger.error( | |||||
f'Failed to update synonyms for [[{pageinfo["title"]}]]' | |||||
) | |||||
raise | raise | ||||
edits[pageid] = { | edits[pageid] = { | ||||
'title': pageinfo['title'], | |||||
'revid': pageinfo['revid'], | |||||
'original': pageinfo['content'], | |||||
'content': str(wikitext), | |||||
'changes': changes, | |||||
"title": pageinfo["title"], | |||||
"revid": pageinfo["revid"], | |||||
"original": pageinfo["content"], | |||||
"content": str(wikitext), | |||||
"changes": changes, | |||||
} | } | ||||
with open(self.edits_path, 'w') as fp: | |||||
with open(self.edits_path, "w") as fp: | |||||
json.dump(edits, fp) | json.dump(edits, fp) | ||||
def _update_synonyms(self, title, wikitext, synonyms): | def _update_synonyms(self, title, wikitext, synonyms): | ||||
if len(synonyms) <= 1: | if len(synonyms) <= 1: | ||||
return False | return False | ||||
if wikitext.split('\n', 1)[0].upper().startswith('#REDIRECT'): | |||||
self.logger.debug(f'[[{title}]]: Skipping redirect') | |||||
if wikitext.split("\n", 1)[0].upper().startswith("#REDIRECT"): | |||||
self.logger.debug(f"[[{title}]]: Skipping redirect") | |||||
return False | return False | ||||
taxoboxes = wikitext.filter_templates( | taxoboxes = wikitext.filter_templates( | ||||
matches=lambda tmpl: tmpl.name.matches(('Speciesbox', 'Automatic taxobox'))) | |||||
matches=lambda tmpl: tmpl.name.matches(("Speciesbox", "Automatic taxobox")) | |||||
) | |||||
if not taxoboxes: | if not taxoboxes: | ||||
self.logger.warning(f'[[{title}]]: No taxoboxes found') | |||||
self.logger.warning(f"[[{title}]]: No taxoboxes found") | |||||
return False | return False | ||||
if len(taxoboxes) > 1: | if len(taxoboxes) > 1: | ||||
self.logger.warning(f'[[{title}]]: Multiple taxoboxes found') | |||||
self.logger.warning(f"[[{title}]]: Multiple taxoboxes found") | |||||
return False | return False | ||||
try: | try: | ||||
syn_param = taxoboxes[0].get('synonyms') | |||||
syn_param = taxoboxes[0].get("synonyms") | |||||
except ValueError: | except ValueError: | ||||
self.logger.debug(f'[[{title}]]: No synonyms parameter in taxobox') | |||||
self.logger.debug(f"[[{title}]]: No synonyms parameter in taxobox") | |||||
return False | return False | ||||
tmpls = syn_param.value.filter_templates( | tmpls = syn_param.value.filter_templates( | ||||
matches=lambda tmpl: tmpl.name.matches(('Species list', 'Taxon list'))) | |||||
matches=lambda tmpl: tmpl.name.matches(("Species list", "Taxon list")) | |||||
) | |||||
if not tmpls: | if not tmpls: | ||||
# This means the bot's original work is no longer there. In most cases, this is | # This means the bot's original work is no longer there. In most cases, this is | ||||
# an unrelated synonym list added by another editor and there is nothing to check, | # an unrelated synonym list added by another editor and there is nothing to check, | ||||
# but it's possible someone converted the bot's list into a different format without | # but it's possible someone converted the bot's list into a different format without | ||||
# checking the authorities. Those cases need to be manually checked. | # checking the authorities. Those cases need to be manually checked. | ||||
self.logger.warning(f'[[{title}]]: Could not find a taxa list in taxobox') | |||||
self.logger.warning(f"[[{title}]]: Could not find a taxa list in taxobox") | |||||
return False | return False | ||||
if len(tmpls) > 1: | if len(tmpls) > 1: | ||||
self.logger.warning(f'[[{title}]]: Multiple taxa lists found in taxobox') | |||||
self.logger.warning(f"[[{title}]]: Multiple taxa lists found in taxobox") | |||||
return False | return False | ||||
expected = {} | expected = {} | ||||
for taxon, author in synonyms: | for taxon, author in synonyms: | ||||
if taxon in expected and expected[taxon] != author: | if taxon in expected and expected[taxon] != author: | ||||
# These need to be manually reviewed | # These need to be manually reviewed | ||||
self.logger.warning(f'[[{title}]]: Expected synonym list has duplicates') | |||||
self.logger.warning( | |||||
f"[[{title}]]: Expected synonym list has duplicates" | |||||
) | |||||
return False | return False | ||||
expected[self._normalize(taxon)] = self._normalize(author) | expected[self._normalize(taxon)] = self._normalize(author) | ||||
@@ -262,21 +273,27 @@ class SynonymAuthorities(Task): | |||||
taxon = self._normalize(taxon_param.value) | taxon = self._normalize(taxon_param.value) | ||||
author = self._normalize(author_param.value) | author = self._normalize(author_param.value) | ||||
if taxon not in expected: | if taxon not in expected: | ||||
self.logger.warning(f'[[{title}]]: Unknown synonym {taxon!r}') | |||||
self.logger.warning(f"[[{title}]]: Unknown synonym {taxon!r}") | |||||
return False | return False | ||||
actual[taxon] = author | actual[taxon] = author | ||||
formatted_authors.setdefault(author, []).append(author_param.value.strip()) | formatted_authors.setdefault(author, []).append(author_param.value.strip()) | ||||
expected = {taxon: author for taxon, author in expected.items() if taxon in actual} | |||||
expected = { | |||||
taxon: author for taxon, author in expected.items() if taxon in actual | |||||
} | |||||
assert set(expected.keys()) == set(actual.keys()) | assert set(expected.keys()) == set(actual.keys()) | ||||
if expected == actual: | if expected == actual: | ||||
self.logger.debug(f'[[{title}]]: Nothing to update') | |||||
self.logger.debug(f"[[{title}]]: Nothing to update") | |||||
return None | return None | ||||
if list(expected.values()) != list(actual.values()): | if list(expected.values()) != list(actual.values()): | ||||
if set(expected.values()) == set(actual.values()): | if set(expected.values()) == set(actual.values()): | ||||
self.logger.warning(f'[[{title}]]: Actual authors are not in expected order') | |||||
self.logger.warning( | |||||
f"[[{title}]]: Actual authors are not in expected order" | |||||
) | |||||
else: | else: | ||||
self.logger.warning(f'[[{title}]]: Actual authors do not match expected') | |||||
self.logger.warning( | |||||
f"[[{title}]]: Actual authors do not match expected" | |||||
) | |||||
return False | return False | ||||
changes = [] | changes = [] | ||||
@@ -285,15 +302,15 @@ class SynonymAuthorities(Task): | |||||
taxon = self._normalize(taxon_param.value) | taxon = self._normalize(taxon_param.value) | ||||
if expected[taxon] != actual[taxon]: | if expected[taxon] != actual[taxon]: | ||||
author = formatted_authors[expected[taxon]].pop(0) | author = formatted_authors[expected[taxon]].pop(0) | ||||
match = re.match(r'^(\s*).*?(\s*)$', str(author_param.value)) | |||||
match = re.match(r"^(\s*).*?(\s*)$", str(author_param.value)) | |||||
ws_before, ws_after = match.group(1), match.group(2) | ws_before, ws_after = match.group(1), match.group(2) | ||||
author_param.value = f'{ws_before}{author}{ws_after}' | |||||
author_param.value = f"{ws_before}{author}{ws_after}" | |||||
changes.append((taxon, actual[taxon], expected[taxon])) | changes.append((taxon, actual[taxon], expected[taxon])) | ||||
if changes: | if changes: | ||||
self.logger.info(f'Will update {len(changes)} synonyms in [[{title}]]') | |||||
self.logger.info(f"Will update {len(changes)} synonyms in [[{title}]]") | |||||
else: | else: | ||||
self.logger.debug(f'Nothing to update in [[{title}]]') | |||||
self.logger.debug(f"Nothing to update in [[{title}]]") | |||||
return changes | return changes | ||||
@staticmethod | @staticmethod | ||||
@@ -305,7 +322,9 @@ class SynonymAuthorities(Task): | |||||
value = value.strip_code() | value = value.strip_code() | ||||
if not value or not value.strip(): | if not value or not value.strip(): | ||||
return None | return None | ||||
return unidecode.unidecode(value.strip().casefold().replace('&', 'and').replace(',', '')) | |||||
return unidecode.unidecode( | |||||
value.strip().casefold().replace("&", "and").replace(",", "") | |||||
) | |||||
def view_edits(self): | def view_edits(self): | ||||
""" | """ | ||||
@@ -314,15 +333,16 @@ class SynonymAuthorities(Task): | |||||
with open(self.edits_path) as fp: | with open(self.edits_path) as fp: | ||||
edits = json.load(fp) | edits = json.load(fp) | ||||
self.logger.info(f'{len(edits)} pages to edit') | |||||
self.logger.info(f"{len(edits)} pages to edit") | |||||
for pageid, edit in edits.items(): | for pageid, edit in edits.items(): | ||||
print(f'\n{pageid}: {edit["title"]}:') | print(f'\n{pageid}: {edit["title"]}:') | ||||
old, new = edit['original'], edit['content'] | |||||
old, new = edit["original"], edit["content"] | |||||
udiff = difflib.unified_diff(old.splitlines(), new.splitlines(), 'old', 'new') | |||||
udiff = difflib.unified_diff( | |||||
old.splitlines(), new.splitlines(), "old", "new" | |||||
) | |||||
subprocess.run( | subprocess.run( | ||||
['delta', '-s', '--paging', 'never'], | |||||
input='\n'.join(udiff), text=True | |||||
["delta", "-s", "--paging", "never"], input="\n".join(udiff), text=True | |||||
) | ) | ||||
def save_edits(self): | def save_edits(self): | ||||
@@ -332,21 +352,21 @@ class SynonymAuthorities(Task): | |||||
with open(self.edits_path) as fp: | with open(self.edits_path) as fp: | ||||
edits = json.load(fp) | edits = json.load(fp) | ||||
self.logger.info(f'{len(edits)} pages to edit') | |||||
self.logger.info(f"{len(edits)} pages to edit") | |||||
for pageid, edit in edits.items(): | for pageid, edit in edits.items(): | ||||
page = self.site.get_page(edit['title']) | |||||
self.logger.info(f'{pageid}: [[{page.title}]]') | |||||
page = self.site.get_page(edit["title"]) | |||||
self.logger.info(f"{pageid}: [[{page.title}]]") | |||||
if self.shutoff_enabled(): | if self.shutoff_enabled(): | ||||
raise RuntimeError('Shutoff enabled') | |||||
raise RuntimeError("Shutoff enabled") | |||||
if not page.check_exclusion(): | if not page.check_exclusion(): | ||||
self.logger.warning(f'[[{page.title}]]: Bot excluded from editing') | |||||
self.logger.warning(f"[[{page.title}]]: Bot excluded from editing") | |||||
continue | continue | ||||
page.edit( | page.edit( | ||||
edit['content'], | |||||
summary=self.summary.format(changes=len(edit['changes'])), | |||||
baserevid=edit['revid'], | |||||
edit["content"], | |||||
summary=self.summary.format(changes=len(edit["changes"])), | |||||
baserevid=edit["revid"], | |||||
basetimestamp=None, | basetimestamp=None, | ||||
starttimestamp=None, | starttimestamp=None, | ||||
) | ) | ||||