@@ -7,6 +7,9 @@ config.json | |||
# Ignore cookies file: | |||
.cookies | |||
# Ignore statistics file: | |||
statistics.txt | |||
# Ignore OS X's crud: | |||
.DS_Store | |||
@@ -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 |
@@ -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): | |||
@@ -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", {}) | |||
@@ -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): | |||
@@ -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 | |||
@@ -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 | |||
@@ -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("(<!-- 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): | |||
"""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): | |||
@@ -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): | |||