@@ -0,0 +1,24 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# EarwigBot Configuration File | |||||
# This file tells the bot which of its components should be enabled. | |||||
# The IRC frontend (configured in config/irc.py) sits on a public IRC network, | |||||
# responds to commands given to it, and reports edits (if the IRC watcher | |||||
# component is enabled). | |||||
enable_irc_frontend = True | |||||
# The IRC watcher (connection details configured in config/irc.py as well) sits | |||||
# on an IRC network that gives a recent changes feed, usually irc.wikimedia.net. | |||||
# It looks for edits matching certain (often regex) patterns (rules configured | |||||
# in config/watcher.py), and either reports them to the IRC frontend (if | |||||
# enabled), or activates a task on the WikiBot (if configured to do). | |||||
enable_irc_watcher = True | |||||
# EarwigBot doesn't have to edit a wiki, although this is its main purpose. If | |||||
# the wiki schedule is disabled, it will not be able to handle scheduled tasks | |||||
# that involve editing (such as creating a daily category every day at midnight | |||||
# UTC), but it can still edit through rules given in the watcher, and bot tasks | |||||
# can still be activated by the command line. The schedule is configured in | |||||
# config/schedule.py. | |||||
enable_wiki_schedule = True |
@@ -0,0 +1,28 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# EarwigBot Configuration File | |||||
# This file tells the bot when to run certain wiki-editing tasks. | |||||
def check(minute, hour, month_day, month, week_day): | |||||
tasks = [] # tasks to run this turn, each as a tuple of (task_name, kwargs) or just task_name | |||||
if minute == 0: # run every hour on the hour | |||||
tasks.append(("afc_statistics", {"action": "save"})) # save statistics to [[Template:AFC_statistics]] | |||||
if hour == 0: # run every day at midnight | |||||
tasks.append("afc_dailycats") # create daily categories for WP:AFC | |||||
tasks.append("feed_dailycats") # create daily categories for WP:FEED | |||||
if week_day == 0: # run every Sunday at midnight (that is, the start of Sunday, not the end) | |||||
tasks.append("afc_undated") # clear [[Category:Undated AfC submissions]] | |||||
if week_day == 1: # run every Monday at midnight | |||||
tasks.append("afc_catdelink") # delink mainspace categories in declined AfC submissions | |||||
if week_day == 2: # run every Tuesday at midnight | |||||
tasks.append("wrongmime") # tag files whose extensions do not agree with their MIME type | |||||
if week_day == 3: # run every Wednesday at midnight | |||||
tasks.append("blptag") # add |blp=yes to {{WPB}} or {{WPBS}} when it is used along with {{WP Biography}} | |||||
return tasks |
@@ -5,30 +5,66 @@ | |||||
import re | import re | ||||
from wiki import task_manager | |||||
# Define different report channels on our front-end server. They /must/ be in CHANS in config/irc.py or the bot will not be able to send messages to them (unless they have -n set). | # Define different report channels on our front-end server. They /must/ be in CHANS in config/irc.py or the bot will not be able to send messages to them (unless they have -n set). | ||||
AFC_CHANS = ["#wikipedia-en-afc"] # report recent AfC changes/give AfC status messages upon join | |||||
#AFC_CHANS = ["#wikipedia-en-afc"] # report recent AfC changes/give AfC status messages upon join | |||||
AFC_CHANS = ["##earwigbot"] # report recent AfC changes/give AfC status messages upon join | |||||
BOT_CHANS = ["##earwigbot", "#wikipedia-en-afc"] # report edits containing "!earwigbot" | BOT_CHANS = ["##earwigbot", "#wikipedia-en-afc"] # report edits containing "!earwigbot" | ||||
# Define some commonly used strings. | |||||
afc_prefix = "wikipedia( talk)?:(wikiproject )?articles for creation" | |||||
# Define our compiled regexps used when finding certain edits. | |||||
r_page = re.compile(afc_prefix) | |||||
r_ffu = re.compile("wikipedia( talk)?:files for upload") | |||||
r_move1 = re.compile("moved \[\[{}".format(afc_prefix)) # an AFC page was either moved locally or out | |||||
r_move2 = re.compile("moved \[\[(.*?)\]\] to \[\[{}".format(afc_prefix)) # an outside page was moved into AFC | |||||
r_moved_pages = re.compile("^moved \[\[(.*?)\]\] to \[\[(.*?)\]\]") | |||||
r_delete = re.compile("deleted \"\[\[{}".format(afc_prefix)) | |||||
r_deleted_page = re.compile("^deleted \"\[\[(.*?)\]\]") | |||||
r_restore = re.compile("restored \"\[\[{}".format(afc_prefix)) | |||||
r_restored_page = re.compile("^restored \"\[\[(.*?)\]\]") | |||||
r_protect = re.compile("protected \"\[\[{}".format(afc_prefix)) | |||||
def process(rc): | def process(rc): | ||||
chans = set() # channels to report this message to | chans = set() # channels to report this message to | ||||
page_name = rc.page.lower() | page_name = rc.page.lower() | ||||
comment = rc.comment.lower() | |||||
if "!earwigbot" in rc.msg.lower(): | if "!earwigbot" in rc.msg.lower(): | ||||
chans.update(BOT_CHANS) | chans.update(BOT_CHANS) | ||||
if re.match("wikipedia( talk)?:(wikiproject )?articles for creation", page_name): | |||||
if r_page.search(page_name): | |||||
task_manager.start_task("afc_statistics", action="process_edit", page=rc.page) | |||||
task_manager.start_task("afc_copyvios", action="process_edit", page=rc.page) | |||||
chans.update(AFC_CHANS) | chans.update(AFC_CHANS) | ||||
elif re.match("wikipedia( talk)?:files for upload", page_name): | |||||
elif r_ffu.match(page_name): | |||||
chans.update(AFC_CHANS) | chans.update(AFC_CHANS) | ||||
elif page_name.startswith("template:afc submission"): | elif page_name.startswith("template:afc submission"): | ||||
chans.update(AFC_CHANS) | chans.update(AFC_CHANS) | ||||
elif rc.flags == "delete" and re.match("deleted \"\[\[wikipedia( talk)?:(wikiproject )?articles for creation", rc.comment.lower()): | |||||
elif rc.flags == "move" and (r_move1.match(comment) or r_move2.match(comment)): | |||||
p = r_moved_pages.findall(rc.comment)[0] | |||||
task_manager.start_task("afc_statistics", action="process_move", pages=p) | |||||
task_manager.start_task("afc_copyvios", action="process_move", pages=p) | |||||
chans.update(AFC_CHANS) | chans.update(AFC_CHANS) | ||||
elif rc.flags == "protect" and re.match("protected \"\[\[wikipedia( talk)?:(wikiproject )?articles for creation", rc.comment.lower()): | |||||
elif rc.flags == "delete" and r_delete.match(comment): | |||||
p = r_deleted_page.findall(rc.comment)[0][0] | |||||
task_manager.start_task("afc_statistics", action="process_delete", page=p) | |||||
task_manager.start_task("afc_copyvios", action="process_delete", page=p) | |||||
chans.update(AFC_CHANS) | |||||
elif rc.flags == "delete" and r_restore.match(comment): | |||||
p = r_restored_page.findall(rc.comment)[0][0] | |||||
task_manager.start_task("afc_statistics", action="process_restore", page=p) | |||||
task_manager.start_task("afc_copyvios", action="process_restore", page=p) | |||||
chans.update(AFC_CHANS) | |||||
elif rc.flags == "protect" and r_protect.match(comment): | |||||
chans.update(AFC_CHANS) | chans.update(AFC_CHANS) | ||||
return chans | return chans |
@@ -1,11 +1,23 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
## EarwigBot's Core | ## EarwigBot's Core | ||||
## Basically, this creates threads for our IRC watcher component and Wikipedia component, and then runs the main IRC bot on the main thread. | |||||
## The IRC bot component of EarwigBot has two parts: a front-end and a watcher. | |||||
## The front-end runs on a normal IRC server and expects users to interact with it/give it commands. | |||||
## The watcher runs on a wiki recent-changes server and listens for edits. Users cannot interact with this part of the bot. | |||||
## EarwigBot has three components that can run independently of each other: an | |||||
## IRC front-end, an IRC watcher, and a wiki scheduler. | |||||
## * The IRC front-end runs on a normal IRC server and expects users to | |||||
## interact with it/give it commands. | |||||
## * The IRC watcher runs on a wiki recent-changes server and listens for | |||||
## edits. Users cannot interact with this part of the bot. | |||||
## * The wiki scheduler runs wiki-editing bot tasks in separate threads at | |||||
## user-defined times through a cron-like interface. | |||||
## There is a "priority" system here: | |||||
## 1. If the IRC frontend is enabled, it will run on the main thread, and the | |||||
## IRC watcher and wiki scheduler (if enabled) will run on separate threads. | |||||
## 2. If the wiki scheduler is enabled, it will run on the main thread, and the | |||||
## IRC watcher (if enabled) will run on a separate thread. | |||||
## 3. If the IRC watcher is enabled, it will run on the main (and only) thread. | |||||
## Else, the bot will stop, as no components are enabled. | |||||
import threading | import threading | ||||
import time | import time | ||||
@@ -16,35 +28,95 @@ import os | |||||
parent_dir = os.path.split(sys.path[0])[0] | parent_dir = os.path.split(sys.path[0])[0] | ||||
sys.path.append(parent_dir) # make sure we look in the parent directory for modules | sys.path.append(parent_dir) # make sure we look in the parent directory for modules | ||||
from config.main import * | |||||
from irc import frontend, watcher | from irc import frontend, watcher | ||||
from wiki import task_manager | |||||
f_conn = None | f_conn = None | ||||
w_conn = None | w_conn = None | ||||
def irc_watcher(f_conn): | def irc_watcher(f_conn): | ||||
"""Function to handle the IRC watcher as another thread (if frontend and/or | |||||
scheduler is enabled), otherwise run as the main thread.""" | |||||
global w_conn | global w_conn | ||||
print "\nStarting IRC watcher..." | |||||
while 1: # restart the watcher component if (just) it breaks | while 1: # restart the watcher component if (just) it breaks | ||||
w_conn = watcher.get_connection() | w_conn = watcher.get_connection() | ||||
w_conn.connect() | |||||
print # print a blank line here to signify that the bot has finished starting up | |||||
try: | try: | ||||
watcher.main(w_conn, f_conn) | watcher.main(w_conn, f_conn) | ||||
except: | except: | ||||
traceback.print_exc() | traceback.print_exc() | ||||
time.sleep(5) # sleep a bit before restarting watcher | time.sleep(5) # sleep a bit before restarting watcher | ||||
print "watcher has stopped; restarting component..." | |||||
print "\nWatcher has stopped; restarting component..." | |||||
def run(): | |||||
def wiki_scheduler(): | |||||
"""Function to handle the wiki scheduler as another thread, or as the | |||||
primary thread if the IRC frontend is not enabled.""" | |||||
while 1: | |||||
time_start = time.time() | |||||
now = time.gmtime(time_start) | |||||
task_manager.start_tasks(now) | |||||
time_end = time.time() | |||||
time_diff = time_start - time_end | |||||
if time_diff < 60: # sleep until the next minute | |||||
time.sleep(60 - time_diff) | |||||
def irc_frontend(): | |||||
"""If the IRC frontend is enabled, make it run on our primary thread, and | |||||
enable the wiki scheduler and IRC watcher on new threads if they are | |||||
enabled.""" | |||||
global f_conn | global f_conn | ||||
print "\nStarting IRC frontend..." | |||||
f_conn = frontend.get_connection() | f_conn = frontend.get_connection() | ||||
frontend.startup(f_conn) | frontend.startup(f_conn) | ||||
t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) | |||||
t_watcher.daemon = True | |||||
t_watcher.start() | |||||
if enable_wiki_schedule: | |||||
print "\nStarting wiki scheduler..." | |||||
task_manager.load_tasks() | |||||
t_scheduler = threading.Thread(target=wiki_scheduler) | |||||
t_scheduler.name = "wiki-scheduler" | |||||
t_scheduler.daemon = True | |||||
t_scheduler.start() | |||||
if enable_irc_watcher: | |||||
t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) | |||||
t_watcher.name = "irc-watcher" | |||||
t_watcher.daemon = True | |||||
t_watcher.start() | |||||
frontend.main() | frontend.main() | ||||
w_conn.close() | |||||
if enable_irc_watcher: | |||||
w_conn.close() | |||||
f_conn.close() | f_conn.close() | ||||
def run(): | |||||
if enable_irc_frontend: # make the frontend run on our primary thread if enabled, and enable additional components through that function | |||||
irc_frontend() | |||||
elif enable_wiki_schedule: # the scheduler is enabled - run it on the main thread, but also run the IRC watcher on another thread if it is enabled | |||||
print "\nStarting wiki scheduler..." | |||||
task_manager.load_tasks() | |||||
if enable_irc_watcher: | |||||
t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) | |||||
t_watcher.name = "irc-watcher" | |||||
t_watcher.daemon = True | |||||
t_watcher.start() | |||||
wiki_scheduler() | |||||
elif enable_irc_watcher: # the IRC watcher is our only enabled component, so run its function only and don't worry about anything else | |||||
irc_watcher() | |||||
else: # nothing is enabled! | |||||
exit("\nNo bot parts are enabled; stopping...") | |||||
if __name__ == "__main__": | if __name__ == "__main__": | ||||
run() | |||||
try: | |||||
run() | |||||
except KeyboardInterrupt: | |||||
exit("\nKeyboardInterrupt: stopping main bot loop.") |
@@ -4,12 +4,19 @@ import time | |||||
from subprocess import * | from subprocess import * | ||||
try: | try: | ||||
from config import irc, secure, watcher | |||||
from config import irc, main, schedule, secure, watcher | |||||
except ImportError: | except ImportError: | ||||
print """Missing a config file! Make sure you have configured the bot. All *.py.default files in config/ | print """Missing a config file! Make sure you have configured the bot. All *.py.default files in config/ | ||||
should have their .default extension removed, and the info inside should be corrected.""" | should have their .default extension removed, and the info inside should be corrected.""" | ||||
exit() | exit() | ||||
while 1: | |||||
call(['python', 'core/main.py']) | |||||
time.sleep(5) # sleep for five seconds between bot runs | |||||
def main(): | |||||
while 1: | |||||
call(['python', 'core/main.py']) | |||||
time.sleep(5) # sleep for five seconds between bot runs | |||||
if __name__ == "__main__": | |||||
try: | |||||
main() | |||||
except KeyboardInterrupt: | |||||
exit("\nKeyboardInterrupt: stopping bot wrapper.") |
@@ -7,7 +7,7 @@ import traceback | |||||
commands = [] | commands = [] | ||||
def init_commands(connection): | |||||
def load_commands(connection): | |||||
"""load all valid command classes from irc/commmands/ into the commands variable""" | """load all valid command classes from irc/commmands/ into the commands variable""" | ||||
files = os.listdir(os.path.join("irc", "commands")) # get all files in irc/commands/ | files = os.listdir(os.path.join("irc", "commands")) # get all files in irc/commands/ | ||||
files.sort() # alphabetically sort list of files | files.sort() # alphabetically sort list of files | ||||
@@ -15,18 +15,14 @@ def init_commands(connection): | |||||
for f in files: | for f in files: | ||||
if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" | if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" | ||||
continue | continue | ||||
module = f[:-3] # strip .py from end | module = f[:-3] # strip .py from end | ||||
try: | try: | ||||
exec "from irc.commands import %s" % module | exec "from irc.commands import %s" % module | ||||
except: # importing the file failed for some reason... | except: # importing the file failed for some reason... | ||||
print "Couldn't load file %s:" % f | print "Couldn't load file %s:" % f | ||||
traceback.print_exc() | traceback.print_exc() | ||||
continue | continue | ||||
m = eval(module) # 'module' is a string, so get the actual object for processing | |||||
process_module(connection, m) | |||||
process_module(connection, eval(module)) # 'module' is a string, so get the actual object for processing by eval-ing it | |||||
pretty_cmnds = map(lambda c: c.__class__.__name__, commands) | pretty_cmnds = map(lambda c: c.__class__.__name__, commands) | ||||
print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds)) | print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds)) | ||||
@@ -146,7 +146,7 @@ class Git(BaseCommand): | |||||
try: | try: | ||||
remote = self.exec_shell("git config --get branch.%s.remote" % branch) | remote = self.exec_shell("git config --get branch.%s.remote" % branch) | ||||
url = self.exec_shell("git config --get remote.%s.url" % remote) | url = self.exec_shell("git config --get remote.%s.url" % remote) | ||||
self.connection.reply(self.data, "done; %s. [from %s]" % (changes, url)) | |||||
self.connection.reply(self.data, "done; %s [from %s]." % (changes, url)) | |||||
except subprocess.CalledProcessError: # something in .git/config is not specified correctly, so we cannot get the remote's url | except subprocess.CalledProcessError: # something in .git/config is not specified correctly, so we cannot get the remote's url | ||||
self.connection.reply(self.data, "done; %s." % changes) | self.connection.reply(self.data, "done; %s." % changes) | ||||
@@ -5,6 +5,10 @@ | |||||
import socket | import socket | ||||
import threading | import threading | ||||
class BrokenSocketException(Exception): | |||||
"""A socket has broken, because it is not sending data.""" | |||||
pass | |||||
class Connection(object): | class Connection(object): | ||||
def __init__(self, host=None, port=None, nick=None, ident=None, realname=None): | def __init__(self, host=None, port=None, nick=None, ident=None, realname=None): | ||||
"""a class to interface with IRC""" | """a class to interface with IRC""" | ||||
@@ -33,7 +37,7 @@ class Connection(object): | |||||
"""receive (get) data from the server""" | """receive (get) data from the server""" | ||||
data = self.sock.recv(4096) | data = self.sock.recv(4096) | ||||
if not data: # socket giving us no data, so it is dead/broken | if not data: # socket giving us no data, so it is dead/broken | ||||
raise RuntimeError("socket is dead") | |||||
raise BrokenSocketException() | |||||
return data | return data | ||||
def send(self, msg): | def send(self, msg): | ||||
@@ -7,7 +7,7 @@ from config.irc import * | |||||
from config.secure import * | from config.secure import * | ||||
from irc import command_handler | from irc import command_handler | ||||
from irc.connection import Connection | |||||
from irc.connection import * | |||||
from irc.data import Data | from irc.data import Data | ||||
connection = None | connection = None | ||||
@@ -19,7 +19,7 @@ def get_connection(): | |||||
def startup(conn): | def startup(conn): | ||||
global connection | global connection | ||||
connection = conn | connection = conn | ||||
command_handler.init_commands(connection) | |||||
command_handler.load_commands(connection) | |||||
connection.connect() | connection.connect() | ||||
def main(): | def main(): | ||||
@@ -28,8 +28,8 @@ def main(): | |||||
while 1: | while 1: | ||||
try: | try: | ||||
read_buffer = read_buffer + connection.get() | read_buffer = read_buffer + connection.get() | ||||
except RuntimeError: # socket broke | |||||
print "socket has broken on front-end; restarting bot..." | |||||
except BrokenSocketException: | |||||
print "Socket has broken on front-end; restarting bot..." | |||||
return | return | ||||
lines = read_buffer.split("\n") | lines = read_buffer.split("\n") | ||||
@@ -61,7 +61,7 @@ def main(): | |||||
if data.msg.startswith("!restart"): # hardcode the !restart command (we can't restart from within an ordinary command) | if data.msg.startswith("!restart"): # hardcode the !restart command (we can't restart from within an ordinary command) | ||||
if data.host in OWNERS: | if data.host in OWNERS: | ||||
print "restarting bot per owner request..." | |||||
print "Restarting bot per owner request..." | |||||
return | return | ||||
if line[0] == "PING": # If we are pinged, pong back to the server | if line[0] == "PING": # If we are pinged, pong back to the server | ||||
@@ -2,9 +2,10 @@ | |||||
## Imports | ## Imports | ||||
from config.irc import * | from config.irc import * | ||||
from config.main import * | |||||
from config.watcher import * | from config.watcher import * | ||||
from irc.connection import Connection | |||||
from irc.connection import * | |||||
from irc.rc import RC | from irc.rc import RC | ||||
global frontend_conn | global frontend_conn | ||||
@@ -16,13 +17,12 @@ def get_connection(): | |||||
def main(connection, f_conn): | def main(connection, f_conn): | ||||
global frontend_conn | global frontend_conn | ||||
frontend_conn = f_conn | frontend_conn = f_conn | ||||
connection.connect() | |||||
read_buffer = str() | read_buffer = str() | ||||
while 1: | while 1: | ||||
try: | try: | ||||
read_buffer = read_buffer + connection.get() | read_buffer = read_buffer + connection.get() | ||||
except RuntimeError: # socket broke | |||||
except BrokenSocketException: | |||||
return | return | ||||
lines = read_buffer.split("\n") | lines = read_buffer.split("\n") | ||||
@@ -53,5 +53,6 @@ def check(rc): | |||||
if not results: | if not results: | ||||
return | return | ||||
pretty = rc.get_pretty() | pretty = rc.get_pretty() | ||||
for chan in results: | |||||
frontend_conn.say(chan, pretty) | |||||
if enable_irc_frontend: | |||||
for chan in results: | |||||
frontend_conn.say(chan, pretty) |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# A base class for bot tasks that edit Wikipedia. | |||||
class BaseTask(object): | |||||
def __init__(self): | |||||
"""A base class for bot tasks that edit Wikipedia.""" | |||||
self.task_name = None | |||||
def run(self, **kwargs): | |||||
"""Run this task.""" | |||||
pass |
@@ -0,0 +1,80 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# A module to manage bot tasks. | |||||
import time | |||||
import traceback | |||||
import threading | |||||
import os | |||||
from config import schedule | |||||
task_list = dict() # the key is the task's name, the value is the task's class instance | |||||
def load_tasks(): | |||||
"""Load all valid task classes from wiki/tasks/, and add them to the task_list.""" | |||||
files = os.listdir(os.path.join("wiki", "tasks")) # get all files in wiki/tasks/ | |||||
files.sort() # alphabetically sort list of files | |||||
for f in files: | |||||
if not os.path.isfile(os.path.join("wiki", "tasks", f)): # ignore non-files | |||||
continue | |||||
if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" | |||||
continue | |||||
load_class_from_file(f) | |||||
print "Found %s tasks: %s." % (len(task_list), ', '.join(task_list.keys())) | |||||
def load_class_from_file(f): | |||||
"""Look in a given file for the task class.""" | |||||
global task_list | |||||
module = f[:-3] # strip .py from end | |||||
try: | |||||
exec "from wiki.tasks import %s as m" % module | |||||
except: # importing the file failed for some reason... | |||||
print "Couldn't load task file %s:" % f | |||||
traceback.print_exc() | |||||
return | |||||
try: | |||||
task_class = m.Task() | |||||
except: | |||||
print "Couldn't find or get task class in file %s:" % f | |||||
traceback.print_exc() | |||||
return | |||||
task_name = task_class.task_name | |||||
task_list[task_name] = task_class | |||||
print "Added task %s from wiki/tasks/%s..." % (task_name, f) | |||||
def start_tasks(now=time.gmtime()): | |||||
"""Start all tasks that are supposed to be run at a given time.""" | |||||
tasks = schedule.check(now.tm_min, now.tm_hour, now.tm_mday, now.tm_mon, now.tm_wday) # get list of tasks to run this turn | |||||
for task in tasks: | |||||
if isinstance(task, tuple): # they've specified kwargs, so pass those to start_task | |||||
start_task(task[0], **task[1]) | |||||
else: # otherwise, just pass task_name | |||||
start_task(task) | |||||
def start_task(task_name, **kwargs): | |||||
"""Start a given task in a new thread. Pass args to the task's run function.""" | |||||
print "Starting task '{}' in a new thread...".format(task_name) | |||||
try: | |||||
task = task_list[task_name] # get the class for this task, a subclass of BaseTask | |||||
except KeyError: | |||||
print "Couldn't find task '{}': wiki/tasks/{}.py does not exist.".format(task_name, task_name) | |||||
return | |||||
# task_thread = threading.Thread(target=task_wrapper, args=(task, kwargs)) | |||||
task_thread = threading.Thread(target=lambda: task_wrapper(task, **kwargs)) # Normally we'd do task_wrapper(task, **kwargs), but because of threading we'd have to do Thread(target=task_wrapper, args=(task, **kwargs)), which doesn't work because the **kwargs is inside a tuple, not inside function params. Use lambda to get around the args=tuple nonsense | |||||
task_thread.name = "task {} (spawned at {} UTC)".format(task_name, time.asctime()) | |||||
task_thread.daemon = True # stop bot task threads automagically if the main bot stops | |||||
task_thread.start() | |||||
def task_wrapper(task, **kwargs): | |||||
"""Wrapper for task classes: run the task and catch any errors.""" | |||||
try: | |||||
task.run(**kwargs) | |||||
except: | |||||
print "Task '{}' raised an exception and had to stop:".format(task.task_name) | |||||
traceback.print_exc() | |||||
else: | |||||
print "Task '{}' finished without error.".format(task.task_name) |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# A task to delink mainspace categories in declined [[WP:AFC]] submissions. | |||||
from wiki.base_task import BaseTask | |||||
class Task(BaseTask): | |||||
def __init__(self): | |||||
self.task_name = "afc_catdelink" | |||||
def run(self, **kwargs): | |||||
pass |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# A task to check newly-edited [[WP:AFC]] submissions for copyright violations. | |||||
from wiki.base_task import BaseTask | |||||
class Task(BaseTask): | |||||
def __init__(self): | |||||
self.task_name = "afc_copyvios" | |||||
def run(self, **kwargs): | |||||
pass |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# A task to create daily categories for [[WP:AFC]]. | |||||
from wiki.base_task import BaseTask | |||||
class Task(BaseTask): | |||||
def __init__(self): | |||||
self.task_name = "afc_dailycats" | |||||
def run(self, **kwargs): | |||||
pass |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# A task to generate statistics for [[WP:AFC]] and save them to [[Template:AFC_statistics]]. | |||||
from wiki.base_task import BaseTask | |||||
class Task(BaseTask): | |||||
def __init__(self): | |||||
self.task_name = "afc_statistics" | |||||
def run(self, **kwargs): | |||||
pass |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# A task to clear [[Category:Undated AfC submissions]]. | |||||
from wiki.base_task import BaseTask | |||||
class Task(BaseTask): | |||||
def __init__(self): | |||||
self.task_name = "afc_undated" | |||||
def run(self, **kwargs): | |||||
pass |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# A task to add |blp=yes to {{WPB}} or {{WPBS}} when it is used along with {{WP Biography}}. | |||||
from wiki.base_task import BaseTask | |||||
class Task(BaseTask): | |||||
def __init__(self): | |||||
self.task_name = "blptag" | |||||
def run(self, **kwargs): | |||||
pass |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# A task to create daily categories for [[WP:FEED]]. | |||||
from wiki.base_task import BaseTask | |||||
class Task(BaseTask): | |||||
def __init__(self): | |||||
self.task_name = "feed_dailycats" | |||||
def run(self, **kwargs): | |||||
pass |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# A task to tag files whose extensions do not agree with their MIME type. | |||||
from wiki.base_task import BaseTask | |||||
class Task(BaseTask): | |||||
def __init__(self): | |||||
self.task_name = "wrongmime" | |||||
def run(self, **kwargs): | |||||
pass |