| @@ -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 | |||||