| @@ -7,6 +7,9 @@ config.json | |||||
| # Ignore cookies file: | # Ignore cookies file: | ||||
| .cookies | .cookies | ||||
| # Ignore statistics file: | |||||
| statistics.txt | |||||
| # Ignore OS X's crud: | # Ignore OS X's crud: | ||||
| .DS_Store | .DS_Store | ||||
| @@ -1,8 +1,12 @@ | |||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
| import config | |||||
| import wiki | |||||
| class BaseTask(object): | class BaseTask(object): | ||||
| """A base class for bot tasks that edit Wikipedia.""" | """A base class for bot tasks that edit Wikipedia.""" | ||||
| name = None | name = None | ||||
| number = 0 | |||||
| def __init__(self): | def __init__(self): | ||||
| """Constructor for new tasks. | """Constructor for new tasks. | ||||
| @@ -25,3 +29,57 @@ class BaseTask(object): | |||||
| (e.g. start('mytask', action='save')). | (e.g. start('mytask', action='save')). | ||||
| """ | """ | ||||
| pass | 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 | |||||
| @@ -4,7 +4,7 @@ from classes import BaseCommand | |||||
| import wiki | import wiki | ||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Retrieve a list of rights for a given name.""" | |||||
| """Retrieve a list of rights for a given username.""" | |||||
| name = "rights" | name = "rights" | ||||
| def check(self, data): | def check(self, data): | ||||
| @@ -8,10 +8,11 @@ including encrypting and decrypting passwords and making a new config file from | |||||
| scratch at the inital bot run. | scratch at the inital bot run. | ||||
| Usually you'll just want to do "from core import config" and access config data | 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.components - a list of enabled components | ||||
| * config.wiki - a dict of information about wiki-editing | * 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.irc - a dict of information about IRC | ||||
| * config.metadata - a dict of miscellaneous information | * config.metadata - a dict of miscellaneous information | ||||
| * config.schedule() - returns a list of tasks scheduled to run at a given time | * 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 | _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(): | def _load(): | ||||
| """Load data from our JSON config file (config.json) into _config.""" | """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. | 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. | 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 | If everything goes well, return True if stored passwords are | ||||
| encrypted in the file, or False if they are not. | encrypted in the file, or False if they are not. | ||||
| @@ -88,6 +90,7 @@ def load(): | |||||
| components = _config.get("components", []) | components = _config.get("components", []) | ||||
| wiki = _config.get("wiki", {}) | wiki = _config.get("wiki", {}) | ||||
| tasks = _config.get("tasks", {}) | |||||
| irc = _config.get("irc", {}) | irc = _config.get("irc", {}) | ||||
| metadata = _config.get("metadata", {}) | metadata = _config.get("metadata", {}) | ||||
| @@ -36,8 +36,8 @@ def process(rc): | |||||
| chans.update(("##earwigbot", "#wikipedia-en-afc")) | chans.update(("##earwigbot", "#wikipedia-en-afc")) | ||||
| if r_page.search(page_name): | 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") | chans.add("#wikipedia-en-afc") | ||||
| elif r_ffu.match(page_name): | elif r_ffu.match(page_name): | ||||
| @@ -49,20 +49,20 @@ def process(rc): | |||||
| elif rc.flags == "move" and (r_move1.match(comment) or | elif rc.flags == "move" and (r_move1.match(comment) or | ||||
| r_move2.match(comment)): | r_move2.match(comment)): | ||||
| p = r_moved_pages.findall(rc.comment)[0] | 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") | chans.add("#wikipedia-en-afc") | ||||
| elif rc.flags == "delete" and r_delete.match(comment): | elif rc.flags == "delete" and r_delete.match(comment): | ||||
| p = r_deleted_page.findall(rc.comment)[0] | 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") | chans.add("#wikipedia-en-afc") | ||||
| elif rc.flags == "restore" and r_restore.match(comment): | elif rc.flags == "restore" and r_restore.match(comment): | ||||
| p = r_restored_page.findall(rc.comment)[0] | 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") | chans.add("#wikipedia-en-afc") | ||||
| elif rc.flags == "protect" and r_protect.match(comment): | elif rc.flags == "protect" and r_protect.match(comment): | ||||
| @@ -6,6 +6,7 @@ class Task(BaseTask): | |||||
| """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 | |||||
| def __init__(self): | def __init__(self): | ||||
| pass | pass | ||||
| @@ -5,6 +5,7 @@ from classes import BaseTask | |||||
| class Task(BaseTask): | class Task(BaseTask): | ||||
| """ 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 | |||||
| def __init__(self): | def __init__(self): | ||||
| pass | pass | ||||
| @@ -1,17 +1,83 @@ | |||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
| import time | |||||
| import re | |||||
| from os import path | |||||
| from classes import BaseTask | from classes import BaseTask | ||||
| import config | |||||
| import wiki | |||||
| class Task(BaseTask): | 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" | name = "afc_statistics" | ||||
| number = 2 | |||||
| def __init__(self): | 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): | 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("(<!-- stat begin -->)(.*?)(<!-- stat end -->)", | |||||
| 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("(<!-- sig begin -->)(.*?)(<!-- sig end -->)", | |||||
| "\\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 | |||||
| @@ -18,8 +18,7 @@ class Category(Page): | |||||
| def __repr__(self): | def __repr__(self): | ||||
| """Returns the canonical string representation of the Category.""" | """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) | return res.format(self._title, self._follow_redirects, self._site) | ||||
| def __str__(self): | def __str__(self): | ||||
| @@ -83,8 +83,7 @@ class Page(object): | |||||
| def __repr__(self): | def __repr__(self): | ||||
| """Returns the canonical string representation of the Page.""" | """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) | return res.format(self._title, self._follow_redirects, self._site) | ||||
| def __str__(self): | def __str__(self): | ||||