diff --git a/.gitignore b/.gitignore index ab78225..42c98e0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ config.json # Ignore cookies file: .cookies +# Ignore statistics file: +statistics.txt + # Ignore OS X's crud: .DS_Store diff --git a/bot/classes/base_task.py b/bot/classes/base_task.py index fcffa00..d34758f 100644 --- a/bot/classes/base_task.py +++ b/bot/classes/base_task.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- +import config +import wiki + class BaseTask(object): """A base class for bot tasks that edit Wikipedia.""" name = None + number = 0 def __init__(self): """Constructor for new tasks. @@ -25,3 +29,57 @@ class BaseTask(object): (e.g. start('mytask', action='save')). """ pass + + def make_summary(self, comment): + """Makes an edit summary by filling in variables in a config value. + + config.wiki["summary"] is used, where $2 is replaced by the main + summary body, given as a method arg, and $1 is replaced by the task + number. + + If the config value is not found, we just return the arg as-is. + """ + try: + summary = config.wiki["summary"] + except KeyError: + return comment + return summary.replace("$1", self.number).replace("$2", comment) + + def shutoff_enabled(self, site=None): + """Returns whether on-wiki shutoff is enabled for this task. + + We check a certain page for certain content. This is determined by + our config file: config.wiki["shutoff"]["page"] is used as the title, + with $1 replaced by our username and $2 replaced by the task number, + and config.wiki["shutoff"]["disabled"] is used as the content. + + If the page has that content or the page does not exist, then shutoff + is "disabled", meaning the bot is supposed to run normally, and we + return False. If the page's content is something other than what we + expect, shutoff is enabled, and we return True. + + If a site is not provided, we'll try to use self.site if it's set. + Otherwise, we'll use our default site. + """ + if not site: + try: + site = self.site + except AttributeError: + site = wiki.get_site() + + try: + cfg = config.wiki["shutoff"] + except KeyError: + return False + title = cfg.get("page", "User:$1/Shutoff/Task $2") + username = site.get_user().name() + title = title.replace("$1", username).replace("$2", self.number) + page = site.get_page(title) + + try: + content = page.get() + except wiki.PageNotFoundError: + return False + if content == cfg.get("disabled", "run"): + return False + return True diff --git a/bot/commands/rights.py b/bot/commands/rights.py index db436a7..b7011a4 100644 --- a/bot/commands/rights.py +++ b/bot/commands/rights.py @@ -4,7 +4,7 @@ from classes import BaseCommand import wiki class Command(BaseCommand): - """Retrieve a list of rights for a given name.""" + """Retrieve a list of rights for a given username.""" name = "rights" def check(self, data): diff --git a/bot/config.py b/bot/config.py index 8a99c26..0034b62 100644 --- a/bot/config.py +++ b/bot/config.py @@ -8,10 +8,11 @@ including encrypting and decrypting passwords and making a new config file from scratch at the inital bot run. Usually you'll just want to do "from core import config" and access config data -from within config's four global variables and one function: +from within config's five global variables and one function: * config.components - a list of enabled components * config.wiki - a dict of information about wiki-editing +* config.tasks - a dict of information for bot tasks * config.irc - a dict of information about IRC * config.metadata - a dict of miscellaneous information * config.schedule() - returns a list of tasks scheduled to run at a given time @@ -34,8 +35,8 @@ config_path = path.join(root_dir, "config.json") _config = None # Holds data loaded from our config file -# Set our four easy-config-access global variables to None -components, wiki, irc, metadata = None, None, None, None +# Set our five easy-config-access global variables to None +components, wiki, tasks, irc, metadata = None, None, None, None, None def _load(): """Load data from our JSON config file (config.json) into _config.""" @@ -68,8 +69,9 @@ def load(): First, check if we have a valid config file, and if not, notify the user. If there is no config file at all, offer to make one, otherwise exit. - Store data from our config file in four global variables (components, wiki, - irc, metadata) for easy access (as well as the internal _config variable). + Store data from our config file in five global variables (components, wiki, + tasks, irc, metadata) for easy access (as well as the internal _config + variable). If everything goes well, return True if stored passwords are encrypted in the file, or False if they are not. @@ -88,6 +90,7 @@ def load(): components = _config.get("components", []) wiki = _config.get("wiki", {}) + tasks = _config.get("tasks", {}) irc = _config.get("irc", {}) metadata = _config.get("metadata", {}) diff --git a/bot/rules.py b/bot/rules.py index f4a7a71..6388586 100644 --- a/bot/rules.py +++ b/bot/rules.py @@ -36,8 +36,8 @@ def process(rc): chans.update(("##earwigbot", "#wikipedia-en-afc")) if r_page.search(page_name): - tasks.start("afc_statistics", action="process_edit", page=rc.page) - tasks.start("afc_copyvios", action="process_edit", page=rc.page) + tasks.start("afc_statistics", action="edit", page=rc.page) + tasks.start("afc_copyvios", action="edit", page=rc.page) chans.add("#wikipedia-en-afc") elif r_ffu.match(page_name): @@ -49,20 +49,20 @@ def process(rc): elif rc.flags == "move" and (r_move1.match(comment) or r_move2.match(comment)): p = r_moved_pages.findall(rc.comment)[0] - tasks.start("afc_statistics", action="process_move", pages=p) - tasks.start("afc_copyvios", action="process_move", pages=p) + tasks.start("afc_statistics", action="move", page=p) + tasks.start("afc_copyvios", action="move", page=p) chans.add("#wikipedia-en-afc") elif rc.flags == "delete" and r_delete.match(comment): p = r_deleted_page.findall(rc.comment)[0] - tasks.start("afc_statistics", action="process_delete", page=p) - tasks.start("afc_copyvios", action="process_delete", page=p) + tasks.start("afc_statistics", action="delete", page=p) + tasks.start("afc_copyvios", action="delete", page=p) chans.add("#wikipedia-en-afc") elif rc.flags == "restore" and r_restore.match(comment): p = r_restored_page.findall(rc.comment)[0] - tasks.start("afc_statistics", action="process_restore", page=p) - tasks.start("afc_copyvios", action="process_restore", page=p) + tasks.start("afc_statistics", action="restore", page=p) + tasks.start("afc_copyvios", action="restore", page=p) chans.add("#wikipedia-en-afc") elif rc.flags == "protect" and r_protect.match(comment): diff --git a/bot/tasks/afc_copyvios.py b/bot/tasks/afc_copyvios.py index 4443cf1..173c690 100644 --- a/bot/tasks/afc_copyvios.py +++ b/bot/tasks/afc_copyvios.py @@ -6,6 +6,7 @@ class Task(BaseTask): """A task to check newly-edited [[WP:AFC]] submissions for copyright violations.""" name = "afc_copyvios" + number = 1 def __init__(self): pass diff --git a/bot/tasks/afc_dailycats.py b/bot/tasks/afc_dailycats.py index a00fc4a..9377970 100644 --- a/bot/tasks/afc_dailycats.py +++ b/bot/tasks/afc_dailycats.py @@ -5,6 +5,7 @@ from classes import BaseTask class Task(BaseTask): """ A task to create daily categories for [[WP:AFC]].""" name = "afc_dailycats" + number = 3 def __init__(self): pass diff --git a/bot/tasks/afc_statistics.py b/bot/tasks/afc_statistics.py index 2a6ae44..ce84baf 100644 --- a/bot/tasks/afc_statistics.py +++ b/bot/tasks/afc_statistics.py @@ -1,17 +1,83 @@ # -*- coding: utf-8 -*- -import time +import re +from os import path from classes import BaseTask +import config +import wiki class Task(BaseTask): - """A task to generate statistics for [[WP:AFC]] and save them to - [[Template:AFC_statistics]].""" + """A task to generate statistics for WikiProject Articles for Creation. + + Statistics are stored in the file indicated by self.filename, + "statistics.txt" in the bot's root directory being the default. They are + updated live while watching the recent changes IRC feed. + + The bot saves its statistics once an hour, on the hour, to self.pagename. + In the live bot, this is "Template:AFC statistics". + """ name = "afc_statistics" + number = 2 def __init__(self): - pass + self.filename = path.join(config.root_dir, "statistics.txt") + self.cfg = config.tasks.get(self.name, {}) + self.pagename = cfg.get("page", "Template:AFC statistics") + default = "Updating statistics for [[WP:WPAFC|WikiProject Articles for creation]]." + self.summary = self.make_summary(cfg.get("summary", default)) def run(self, **kwargs): - time.sleep(5) - print kwargs + self.site = wiki.get_site() + + action = kwargs.get("action") + if not action: + return + if action == "save": + self.save() + return + + page = kwargs.get("page") + if page: + methods = { + "edit": self.process_edit, + "move": self.process_move, + "delete": self.process_delete, + "restore": self.process_restore, + } + method = methods.get(action) + if method: + method(page) + + def save(self): + if self.shutoff_enabled(): + return + try: + with open(self.filename) as fp: + statistics = fp.read() + except IOError: + pass + + page = self.site.get_page(self.pagename) + text = page.get() + newtext = re.sub("()(.*?)()", + statistics.join(("\\1", "\\3")), text, + flags=re.DOTALL) + if newtext == text: + return # Don't edit the page if we're not adding anything + + newtext = re.sub("()(.*?)()", + "\\1~~~ at ~~~~~\\3", newtext) + page.edit(newtext, self.summary, minor=True) + + def process_edit(self, page): + pass + + def process_move(self, page): + pass + + def process_delete(self, page): + pass + + def process_restore(self, page): + pass diff --git a/bot/wiki/category.py b/bot/wiki/category.py index 578ae01..6d9b841 100644 --- a/bot/wiki/category.py +++ b/bot/wiki/category.py @@ -18,8 +18,7 @@ class Category(Page): def __repr__(self): """Returns the canonical string representation of the Category.""" - res = ", ".join(("Category(title={0!r}", "follow_redirects={1!r}", - "site={2!r})")) + res = "Category(title={0!r}, follow_redirects={1!r}, site={2!r})" return res.format(self._title, self._follow_redirects, self._site) def __str__(self): diff --git a/bot/wiki/page.py b/bot/wiki/page.py index a917747..808cc18 100644 --- a/bot/wiki/page.py +++ b/bot/wiki/page.py @@ -83,8 +83,7 @@ class Page(object): def __repr__(self): """Returns the canonical string representation of the Page.""" - res = ", ".join(("Page(title={0!r}", "follow_redirects={1!r}", - "site={2!r})")) + res = "Page(title={0!r}, follow_redirects={1!r}, site={2!r})" return res.format(self._title, self._follow_redirects, self._site) def __str__(self):