From 68af0e796f004d0a932d665599ea30ba0a2d72cc Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 00:49:53 -0400 Subject: [PATCH 01/16] restructuring everything for unit tests, etc; bot doesn't work anymore, but I'll fix that in a bit --- {core => bot}/__init__.py | 0 {lib => bot}/blowfish.py | 0 {irc => bot}/classes/__init__.py | 1 + {irc => bot}/classes/base_command.py | 0 {wiki => bot/classes}/base_task.py | 0 {irc => bot}/classes/connection.py | 0 {irc => bot}/classes/data.py | 0 {irc => bot}/classes/rc.py | 0 irc/command_handler.py => bot/commands/__init__.py | 0 {irc => bot}/commands/_old.py | 0 {irc => bot}/commands/afc_report.py | 0 {irc => bot}/commands/afc_status.py | 0 {irc => bot}/commands/calc.py | 0 {irc => bot}/commands/chanops.py | 0 {irc => bot}/commands/crypt.py | 0 {irc => bot}/commands/git.py | 0 {irc => bot}/commands/help.py | 0 {irc => bot}/commands/link.py | 0 {irc => bot}/commands/remind.py | 0 {irc => bot}/commands/rights.py | 0 {irc => bot}/commands/tasks.py | 0 {irc => bot}/commands/test.py | 0 {core => bot}/config.py | 0 {irc => bot}/frontend.py | 0 {core => bot}/main.py | 0 wiki/task_manager.py => bot/tasks/__init__.py | 0 {wiki => bot}/tasks/afc_catdelink.py | 0 {wiki => bot}/tasks/afc_copyvios.py | 0 {wiki => bot}/tasks/afc_dailycats.py | 0 {wiki => bot}/tasks/afc_statistics.py | 0 {wiki => bot}/tasks/afc_undated.py | 0 {wiki => bot}/tasks/blptag.py | 0 {wiki => bot}/tasks/feed_dailycats.py | 0 {wiki => bot}/tasks/wrongmime.py | 0 {irc => bot}/watcher.py | 0 {irc => bot}/watcher_logic.py | 0 {wiki/tools => bot/wiki}/__init__.py | 0 {wiki/tools => bot/wiki}/category.py | 0 {wiki/tools => bot/wiki}/constants.py | 0 {wiki/tools => bot/wiki}/exceptions.py | 0 {wiki/tools => bot/wiki}/functions.py | 0 {wiki/tools => bot/wiki}/page.py | 0 {wiki/tools => bot/wiki}/site.py | 0 {wiki/tools => bot/wiki}/user.py | 0 irc/__init__.py | 0 irc/commands/__init__.py | 0 lib/__init__.py | 0 wiki/__init__.py | 0 wiki/tasks/__init__.py | 0 49 files changed, 1 insertion(+) rename {core => bot}/__init__.py (100%) rename {lib => bot}/blowfish.py (100%) rename {irc => bot}/classes/__init__.py (78%) rename {irc => bot}/classes/base_command.py (100%) rename {wiki => bot/classes}/base_task.py (100%) rename {irc => bot}/classes/connection.py (100%) rename {irc => bot}/classes/data.py (100%) rename {irc => bot}/classes/rc.py (100%) rename irc/command_handler.py => bot/commands/__init__.py (100%) rename {irc => bot}/commands/_old.py (100%) rename {irc => bot}/commands/afc_report.py (100%) rename {irc => bot}/commands/afc_status.py (100%) rename {irc => bot}/commands/calc.py (100%) rename {irc => bot}/commands/chanops.py (100%) rename {irc => bot}/commands/crypt.py (100%) rename {irc => bot}/commands/git.py (100%) rename {irc => bot}/commands/help.py (100%) rename {irc => bot}/commands/link.py (100%) rename {irc => bot}/commands/remind.py (100%) rename {irc => bot}/commands/rights.py (100%) rename {irc => bot}/commands/tasks.py (100%) rename {irc => bot}/commands/test.py (100%) rename {core => bot}/config.py (100%) rename {irc => bot}/frontend.py (100%) rename {core => bot}/main.py (100%) rename wiki/task_manager.py => bot/tasks/__init__.py (100%) rename {wiki => bot}/tasks/afc_catdelink.py (100%) rename {wiki => bot}/tasks/afc_copyvios.py (100%) rename {wiki => bot}/tasks/afc_dailycats.py (100%) rename {wiki => bot}/tasks/afc_statistics.py (100%) rename {wiki => bot}/tasks/afc_undated.py (100%) rename {wiki => bot}/tasks/blptag.py (100%) rename {wiki => bot}/tasks/feed_dailycats.py (100%) rename {wiki => bot}/tasks/wrongmime.py (100%) rename {irc => bot}/watcher.py (100%) rename {irc => bot}/watcher_logic.py (100%) rename {wiki/tools => bot/wiki}/__init__.py (100%) rename {wiki/tools => bot/wiki}/category.py (100%) rename {wiki/tools => bot/wiki}/constants.py (100%) rename {wiki/tools => bot/wiki}/exceptions.py (100%) rename {wiki/tools => bot/wiki}/functions.py (100%) rename {wiki/tools => bot/wiki}/page.py (100%) rename {wiki/tools => bot/wiki}/site.py (100%) rename {wiki/tools => bot/wiki}/user.py (100%) delete mode 100644 irc/__init__.py delete mode 100644 irc/commands/__init__.py delete mode 100644 lib/__init__.py delete mode 100644 wiki/__init__.py delete mode 100644 wiki/tasks/__init__.py diff --git a/core/__init__.py b/bot/__init__.py similarity index 100% rename from core/__init__.py rename to bot/__init__.py diff --git a/lib/blowfish.py b/bot/blowfish.py similarity index 100% rename from lib/blowfish.py rename to bot/blowfish.py diff --git a/irc/classes/__init__.py b/bot/classes/__init__.py similarity index 78% rename from irc/classes/__init__.py rename to bot/classes/__init__.py index b92db69..95da576 100644 --- a/irc/classes/__init__.py +++ b/bot/classes/__init__.py @@ -1,4 +1,5 @@ from base_command import * +from base_task import * from connection import * from data import * from rc import * diff --git a/irc/classes/base_command.py b/bot/classes/base_command.py similarity index 100% rename from irc/classes/base_command.py rename to bot/classes/base_command.py diff --git a/wiki/base_task.py b/bot/classes/base_task.py similarity index 100% rename from wiki/base_task.py rename to bot/classes/base_task.py diff --git a/irc/classes/connection.py b/bot/classes/connection.py similarity index 100% rename from irc/classes/connection.py rename to bot/classes/connection.py diff --git a/irc/classes/data.py b/bot/classes/data.py similarity index 100% rename from irc/classes/data.py rename to bot/classes/data.py diff --git a/irc/classes/rc.py b/bot/classes/rc.py similarity index 100% rename from irc/classes/rc.py rename to bot/classes/rc.py diff --git a/irc/command_handler.py b/bot/commands/__init__.py similarity index 100% rename from irc/command_handler.py rename to bot/commands/__init__.py diff --git a/irc/commands/_old.py b/bot/commands/_old.py similarity index 100% rename from irc/commands/_old.py rename to bot/commands/_old.py diff --git a/irc/commands/afc_report.py b/bot/commands/afc_report.py similarity index 100% rename from irc/commands/afc_report.py rename to bot/commands/afc_report.py diff --git a/irc/commands/afc_status.py b/bot/commands/afc_status.py similarity index 100% rename from irc/commands/afc_status.py rename to bot/commands/afc_status.py diff --git a/irc/commands/calc.py b/bot/commands/calc.py similarity index 100% rename from irc/commands/calc.py rename to bot/commands/calc.py diff --git a/irc/commands/chanops.py b/bot/commands/chanops.py similarity index 100% rename from irc/commands/chanops.py rename to bot/commands/chanops.py diff --git a/irc/commands/crypt.py b/bot/commands/crypt.py similarity index 100% rename from irc/commands/crypt.py rename to bot/commands/crypt.py diff --git a/irc/commands/git.py b/bot/commands/git.py similarity index 100% rename from irc/commands/git.py rename to bot/commands/git.py diff --git a/irc/commands/help.py b/bot/commands/help.py similarity index 100% rename from irc/commands/help.py rename to bot/commands/help.py diff --git a/irc/commands/link.py b/bot/commands/link.py similarity index 100% rename from irc/commands/link.py rename to bot/commands/link.py diff --git a/irc/commands/remind.py b/bot/commands/remind.py similarity index 100% rename from irc/commands/remind.py rename to bot/commands/remind.py diff --git a/irc/commands/rights.py b/bot/commands/rights.py similarity index 100% rename from irc/commands/rights.py rename to bot/commands/rights.py diff --git a/irc/commands/tasks.py b/bot/commands/tasks.py similarity index 100% rename from irc/commands/tasks.py rename to bot/commands/tasks.py diff --git a/irc/commands/test.py b/bot/commands/test.py similarity index 100% rename from irc/commands/test.py rename to bot/commands/test.py diff --git a/core/config.py b/bot/config.py similarity index 100% rename from core/config.py rename to bot/config.py diff --git a/irc/frontend.py b/bot/frontend.py similarity index 100% rename from irc/frontend.py rename to bot/frontend.py diff --git a/core/main.py b/bot/main.py similarity index 100% rename from core/main.py rename to bot/main.py diff --git a/wiki/task_manager.py b/bot/tasks/__init__.py similarity index 100% rename from wiki/task_manager.py rename to bot/tasks/__init__.py diff --git a/wiki/tasks/afc_catdelink.py b/bot/tasks/afc_catdelink.py similarity index 100% rename from wiki/tasks/afc_catdelink.py rename to bot/tasks/afc_catdelink.py diff --git a/wiki/tasks/afc_copyvios.py b/bot/tasks/afc_copyvios.py similarity index 100% rename from wiki/tasks/afc_copyvios.py rename to bot/tasks/afc_copyvios.py diff --git a/wiki/tasks/afc_dailycats.py b/bot/tasks/afc_dailycats.py similarity index 100% rename from wiki/tasks/afc_dailycats.py rename to bot/tasks/afc_dailycats.py diff --git a/wiki/tasks/afc_statistics.py b/bot/tasks/afc_statistics.py similarity index 100% rename from wiki/tasks/afc_statistics.py rename to bot/tasks/afc_statistics.py diff --git a/wiki/tasks/afc_undated.py b/bot/tasks/afc_undated.py similarity index 100% rename from wiki/tasks/afc_undated.py rename to bot/tasks/afc_undated.py diff --git a/wiki/tasks/blptag.py b/bot/tasks/blptag.py similarity index 100% rename from wiki/tasks/blptag.py rename to bot/tasks/blptag.py diff --git a/wiki/tasks/feed_dailycats.py b/bot/tasks/feed_dailycats.py similarity index 100% rename from wiki/tasks/feed_dailycats.py rename to bot/tasks/feed_dailycats.py diff --git a/wiki/tasks/wrongmime.py b/bot/tasks/wrongmime.py similarity index 100% rename from wiki/tasks/wrongmime.py rename to bot/tasks/wrongmime.py diff --git a/irc/watcher.py b/bot/watcher.py similarity index 100% rename from irc/watcher.py rename to bot/watcher.py diff --git a/irc/watcher_logic.py b/bot/watcher_logic.py similarity index 100% rename from irc/watcher_logic.py rename to bot/watcher_logic.py diff --git a/wiki/tools/__init__.py b/bot/wiki/__init__.py similarity index 100% rename from wiki/tools/__init__.py rename to bot/wiki/__init__.py diff --git a/wiki/tools/category.py b/bot/wiki/category.py similarity index 100% rename from wiki/tools/category.py rename to bot/wiki/category.py diff --git a/wiki/tools/constants.py b/bot/wiki/constants.py similarity index 100% rename from wiki/tools/constants.py rename to bot/wiki/constants.py diff --git a/wiki/tools/exceptions.py b/bot/wiki/exceptions.py similarity index 100% rename from wiki/tools/exceptions.py rename to bot/wiki/exceptions.py diff --git a/wiki/tools/functions.py b/bot/wiki/functions.py similarity index 100% rename from wiki/tools/functions.py rename to bot/wiki/functions.py diff --git a/wiki/tools/page.py b/bot/wiki/page.py similarity index 100% rename from wiki/tools/page.py rename to bot/wiki/page.py diff --git a/wiki/tools/site.py b/bot/wiki/site.py similarity index 100% rename from wiki/tools/site.py rename to bot/wiki/site.py diff --git a/wiki/tools/user.py b/bot/wiki/user.py similarity index 100% rename from wiki/tools/user.py rename to bot/wiki/user.py diff --git a/irc/__init__.py b/irc/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/irc/commands/__init__.py b/irc/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lib/__init__.py b/lib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/wiki/__init__.py b/wiki/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/wiki/tasks/__init__.py b/wiki/tasks/__init__.py deleted file mode 100644 index e69de29..0000000 From 8df073c457cea5b5628efdfab9378b075409fcac Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 00:52:56 -0400 Subject: [PATCH 02/16] .gitigore - minor change/fix --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1884197..ab78225 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ config.json .cookies # Ignore OS X's crud: -*.DS_Store +.DS_Store # Ignore pydev's nonsense: .project From b0da4531b2ef2cc3e525a37926c5d649c05ee841 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 01:33:52 -0400 Subject: [PATCH 03/16] tons of improvements, import fixes, cleanup, etc --- bot/classes/data.py | 6 +-- bot/commands/__init__.py | 60 +++++++++++---------- bot/config.py | 5 +- bot/frontend.py | 134 +++++++++++++++++++++++++---------------------- bot/main.py | 20 +++---- bot/tasks/__init__.py | 81 ++++++++++++++-------------- bot/watcher.py | 87 +++++++++++++++++------------- bot/watcher_logic.py | 20 +++---- earwigbot.py | 6 +-- 9 files changed, 220 insertions(+), 199 deletions(-) diff --git a/bot/classes/data.py b/bot/classes/data.py index 781c2ed..1075fc3 100644 --- a/bot/classes/data.py +++ b/bot/classes/data.py @@ -11,9 +11,9 @@ class KwargParseException(Exception): pass class Data(object): - def __init__(self): - """store data from an individual line received on IRC""" - self.line = str() + def __init__(self, line): + """Store data from an individual line received on IRC.""" + self.line = line self.chan = str() self.nick = str() self.ident = str() diff --git a/bot/commands/__init__.py b/bot/commands/__init__.py index b187185..fc25de5 100644 --- a/bot/commands/__init__.py +++ b/bot/commands/__init__.py @@ -5,29 +5,11 @@ import os import traceback -commands = [] +__all__ = ["load", "get_all", "check"] -def load_commands(connection): - """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.sort() # alphabetically sort list of files +_commands = [] - for f in files: - if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" - continue - module = f[:-3] # strip .py from end - try: - exec "from irc.commands import %s" % module - except: # importing the file failed for some reason... - print "Couldn't load file %s:" % f - traceback.print_exc() - continue - 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) - print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds)) - -def process_module(connection, module): +def _process_module(connection, module): """go through all objects in a module and add valid command classes to the commands variable""" global commands objects = dir(module) @@ -43,19 +25,41 @@ def process_module(connection, module): for base in bases: if base.__name__ == "BaseCommand": # this inherits BaseCommand, so it must be a command class command = obj(connection) # initialize a new command object - commands.append(command) + _commands.append(command) print "Added command class %s from %s..." % (this_obj, module.__name__) continue -def get_commands(): - """get our commands""" - return commands +def load(connection): + """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.sort() # alphabetically sort list of files + + for f in files: + if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" + continue + module = f[:-3] # strip .py from end + try: + exec "from irc.commands import %s" % module + except: # importing the file failed for some reason... + print "Couldn't load file %s:" % f + traceback.print_exc() + continue + 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) + print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds)) + +def get_all(): + """Return our list of all commands.""" + return _commands def check(hook, data): - """given an event on IRC, check if there's anything we can respond to by calling each command class""" - data.parse_args() # parse command arguments into data.command and data.args + """Given an event on IRC, check if there's anything we can respond to by + calling each command class""" + # parse command arguments into data.command and data.args + data.parse_args() - for command in commands: + for command in _commands: if hook in command.get_hooks(): if command.check(data): try: diff --git a/bot/config.py b/bot/config.py index b2c7cdf..82969b6 100644 --- a/bot/config.py +++ b/bot/config.py @@ -18,9 +18,9 @@ from within config's three global variables and one function: """ import json -from os import makedirs, path +from os import path -from lib import blowfish +import blowfish script_dir = path.dirname(path.abspath(__file__)) root_dir = path.split(script_dir)[0] @@ -149,7 +149,6 @@ def schedule(minute, hour, month_day, month, week_day): def make_new_config(): """Make a new config file based on the user's input.""" - makedirs(config_dir) encrypt = raw_input("Would you like to encrypt passwords stored in " + "config.json? [y/n] ") diff --git a/bot/frontend.py b/bot/frontend.py index 2a8537d..b3a6307 100644 --- a/bot/frontend.py +++ b/bot/frontend.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -EarwigBot's IRC Front-end Component +EarwigBot's IRC Frontend Component The IRC frontend runs on a normal IRC server and expects users to interact with it and give it commands. Commands are stored as "command classes", subclasses @@ -9,13 +9,16 @@ of BaseCommand in irc/base_command.py. All command classes are automatically imported by irc/command_handler.py if they are in irc/commands. """ -from re import findall +import re -from core import config -from irc import command_handler -from irc.classes import Connection, Data, BrokenSocketException +import config +import commands +from classes import Connection, Data, BrokenSocketException + +__all__ = ["get_connection", "startup", "main"] connection = None +sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z") def get_connection(): """Return a new Connection() instance with information about our server @@ -31,16 +34,18 @@ def startup(conn): command_handler, and then establish a connection with the IRC server.""" global connection connection = conn - command_handler.load_commands(connection) + commands.load(connection) connection.connect() def main(): - """Main loop for the Frontend IRC Bot component. get_connection() and - startup() should have already been called.""" + """Main loop for the frontend component. + + get_connection() and startup() should have already been called before this. + """ read_buffer = str() while 1: - try: + try: read_buffer = read_buffer + connection.get() except BrokenSocketException: print "Socket has broken on front-end; restarting bot..." @@ -48,57 +53,60 @@ def main(): lines = read_buffer.split("\n") read_buffer = lines.pop() - - for line in lines: # handle a single message from IRC - line = line.strip().split() - data = Data() # new Data() instance to store info about this line - data.line = line - - if line[1] == "JOIN": - data.nick, data.ident, data.host = findall( - ":(.*?)!(.*?)@(.*?)\Z", line[0])[0] - data.chan = line[2][1:] - command_handler.check("join", data) # check for 'join' hooks in - # our commands - - if line[1] == "PRIVMSG": - data.nick, data.ident, data.host = findall( - ":(.*?)!(.*?)@(.*?)\Z", line[0])[0] - data.msg = ' '.join(line[3:])[1:] - data.chan = line[2] - - if data.chan == config.irc["frontend"]["nick"]: - # this is a privmsg to us, so set 'chan' as the nick of the - # sender, then check for private-only command hooks - data.chan = data.nick - command_handler.check("msg_private", data) - else: - # check for public-only command hooks - command_handler.check("msg_public", data) - - # check for command hooks that apply to all messages - command_handler.check("msg", data) - - # hardcode the !restart command (we can't restart from within - # an ordinary command) - if data.msg in ["!restart", ".restart"]: - if data.host in config.irc["permissions"]["owners"]: - print "Restarting bot per owner request..." - return - - if line[0] == "PING": # if we are pinged, pong back to the server - connection.send("PONG %s" % line[1]) - - if line[1] == "376": # we've successfully connected to the network - try: # if we're supposed to auth to nickserv, do that - ns_username = config.irc["frontend"]["nickservUsername"] - ns_password = config.irc["frontend"]["nickservPassword"] - except KeyError: - pass - else: - connection.say("NickServ", "IDENTIFY {0} {1}".format( - ns_username, ns_password)) - - # join all of our startup channels - for chan in config.irc["frontend"]["channels"]: - connection.join(chan) + for line in lines: + _process_message(line) + +def _process_message(line): + """Process a single message from IRC.""" + line = line.strip().split() + data = Data(line) # new Data instance to store info about this line + + if line[1] == "JOIN": + data.nick, data.ident, data.host = sender_regex.findall(line[0])[0] + data.chan = line[2][1:] + # Check for 'join' hooks in our commands: + commands.check("join", data) + + elif line[1] == "PRIVMSG": + data.nick, data.ident, data.host = sender_regex.findall(line[0])[0] + data.msg = ' '.join(line[3:])[1:] + data.chan = line[2] + + if data.chan == config.irc["frontend"]["nick"]: + # This is a privmsg to us, so set 'chan' as the nick of the, sender + # then check for private-only command hooks: + data.chan = data.nick + commands.check("msg_private", data) + else: + # Check for public-only command hooks: + commands.check("msg_public", data) + + # Check for command hooks that apply to all messages: + commands.check("msg", data) + + # Hardcode the !restart command (we can't restart from within an + # ordinary command): + if data.msg in ["!restart", ".restart"]: + if data.host in config.irc["permissions"]["owners"]: + print "Restarting bot per owner request..." + return + + # If we are pinged, pong back to the server: + if line[0] == "PING": + connection.send("PONG %s" % line[1]) + + # On successful connection to the server: + if line[1] == "376": + # If we're supposed to auth to NickServ, do that: + try: + username = config.irc["frontend"]["nickservUsername"] + password = config.irc["frontend"]["nickservPassword"] + except KeyError: + pass + else: + msg = " ".join(("IDENTIFY", username, password)) + connection.say("NickServ", msg) + + # Join all of our startup channels: + for chan in config.irc["frontend"]["channels"]: + connection.join(chan) diff --git a/bot/main.py b/bot/main.py index 0b49842..eb892de 100644 --- a/bot/main.py +++ b/bot/main.py @@ -33,17 +33,11 @@ Else, the bot will stop, as no components are enabled. import threading import time import traceback -import sys -import os -script_dir = os.path.dirname(os.path.abspath(__file__)) -root_dir = os.path.split(script_dir)[0] # the bot's "root" directory relative - # to its different components -sys.path.append(root_dir) # make sure we look in the root dir for modules - -from core import config -from irc import frontend, watcher -from wiki import task_manager +import config +import frontend +import tasks +import watcher f_conn = None w_conn = None @@ -70,7 +64,7 @@ def wiki_scheduler(): time_start = time.time() now = time.gmtime(time_start) - task_manager.start_tasks(now) + tasks.schedule(now) time_end = time.time() time_diff = time_start - time_end @@ -89,7 +83,7 @@ def irc_frontend(): if "wiki_schedule" in config.components: print "\nStarting wiki scheduler..." - task_manager.load_tasks() + tasks.load() t_scheduler = threading.Thread(target=wiki_scheduler) t_scheduler.name = "wiki-scheduler" t_scheduler.daemon = True @@ -123,7 +117,7 @@ def run(): elif "wiki_schedule" in enabled: # run the scheduler on the main print "Starting wiki scheduler..." # thread, but also run the IRC - task_manager.load_tasks() # watcher on another thread iff it + tasks.load() # watcher on another thread iff it if "irc_watcher" in enabled: # is enabled print "\nStarting IRC watcher..." t_watcher = threading.Thread(target=irc_watcher, args=()) diff --git a/bot/tasks/__init__.py b/bot/tasks/__init__.py index 61e8386..502813a 100644 --- a/bot/tasks/__init__.py +++ b/bot/tasks/__init__.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- """ -EarwigBot's Wiki Bot Task Manager +EarwigBot's Wiki Task Manager -This module provides some functions to run and load bot tasks from wiki/tasks/. +This package provides the wiki bot "tasks" EarwigBot runs. Here in __init__, +you can find some functions used to load and run these tasks. """ import time @@ -11,28 +12,17 @@ import traceback import threading import os -from core import config +import config + +__all__ = ["load", "schedule", "start"] # store loaded tasks as a dict where the key is the task name and the value is # an instance of the task class (wiki.tasks.task_file.Task()) -task_list = dict() +_tasks = dict() -def load_tasks(): - """Load all valid task classes from wiki/tasks/, and add them to the - task_list variable.""" - files = os.listdir(os.path.join("wiki", "tasks")) - files.sort() # alphabetically sort all files in wiki/tasks/ - for f in files: - if not os.path.isfile(os.path.join("wiki", "tasks", f)): - continue # ignore non-files - if f.startswith("_") or not f.endswith(".py"): - continue # ignore non-python files or files beginning with an _ - load_class_from_file(f) - print "Found %s tasks: %s." % (len(task_list), ', '.join(task_list.keys())) - -def load_class_from_file(f): +def _load_class_from_file(f): """Look in a given file for the task class.""" - global task_list + global _tasks module = f[:-3] # strip .py from end try: @@ -48,45 +38,58 @@ def load_class_from_file(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) + _tasks[task_name] = task_class() + print "Added task %s from bot/tasks/%s..." % (task_name, f) + +def _wrapper(task, **kwargs): + """Wrapper for task classes: run the task and catch any errors.""" + try: + task.run(**kwargs) + except: + print "Task '{0}' raised an exception and had to stop:".format(task.task_name) + traceback.print_exc() + else: + print "Task '{0}' finished without error.".format(task.task_name) -def start_tasks(now=time.gmtime()): +def load(): + """Load all valid task classes from bot/tasks/, and add them to the + _tasks variable.""" + files = os.listdir(os.path.join("bot", "tasks")) + files.sort() # alphabetically sort all files in wiki/tasks/ + for f in files: + if not os.path.isfile(os.path.join("bot", "tasks", f)): + continue # ignore non-files + if f.startswith("_") or not f.endswith(".py"): + continue # ignore non-python files or files beginning with an _ + load_class_from_file(f) + print "Found %s tasks: %s." % (len(_tasks), ', '.join(_tasks.keys())) + +def schedule(now=time.gmtime()): """Start all tasks that are supposed to be run at a given time.""" tasks = config.schedule(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, list): # they've specified kwargs - start_task(task[0], **task[1]) # so pass those to start_task + if isinstance(task, list): # they've specified kwargs + start(task[0], **task[1]) # so pass those to start_task else: # otherwise, just pass task_name - start_task(task) + start(task) -def start_task(task_name, **kwargs): +def start(task_name, **kwargs): """Start a given task in a new thread. Pass args to the task's run() function.""" print "Starting task '{0}' in a new thread...".format(task_name) try: - task = task_list[task_name] + task = _tasks[task_name] except KeyError: - print ("Couldn't find task '{0}': wiki/tasks/{0}.py does not exist.").format(task_name) + print ("Couldn't find task '{0}': bot/tasks/{0}.py does not exist.").format(task_name) return - task_thread = threading.Thread(target=lambda: task_wrapper(task, **kwargs)) + task_thread = threading.Thread(target=lambda: _wrapper(task, **kwargs)) task_thread.name = "{0} ({1})".format(task_name, time.strftime("%b %d %H:%M:%S")) # stop bot task threads automagically if the main bot stops task_thread.daemon = True 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 '{0}' raised an exception and had to stop:".format(task.task_name) - traceback.print_exc() - else: - print "Task '{0}' finished without error.".format(task.task_name) diff --git a/bot/watcher.py b/bot/watcher.py index b031b20..2e43099 100644 --- a/bot/watcher.py +++ b/bot/watcher.py @@ -10,25 +10,30 @@ wiki bot tasks being started (listed in wiki/tasks/) or messages being sent to channels in the IRC frontend. """ -from core import config -from irc.classes import Connection, RC, BrokenSocketException -from irc import watcher_logic +import config +from classes import Connection, RC, BrokenSocketException +import watcher_logic as logic frontend_conn = None def get_connection(): - """Return a new Connection() instance with information about our server - connection, but don't actually connect yet.""" + """Return a new Connection() instance with connection information. + + Don't actually connect yet. + """ cf = config.irc["watcher"] connection = Connection(cf["host"], cf["port"], cf["nick"], cf["ident"], cf["realname"]) return connection def main(connection, f_conn=None): - """Main loop for the Watcher IRC Bot component. get_connection() should - have already been called and the connection should have been started with - connection.connect(). Accept the frontend connection as well as an optional - parameter in order to send messages directly to frontend IRC channels.""" + """Main loop for the Watcher IRC Bot component. + + get_connection() should have already been called and the connection should + have been started with connection.connect(). Accept the frontend connection + as well as an optional parameter in order to send messages directly to + frontend IRC channels. + """ global frontend_conn frontend_conn = f_conn read_buffer = str() @@ -43,34 +48,42 @@ def main(connection, f_conn=None): read_buffer = lines.pop() for line in lines: - line = line.strip().split() - - if line[1] == "PRIVMSG": - chan = line[2] - - # ignore messages originating from channels not in our list, to - # prevent someone PMing us false data - if chan not in config.irc["watcher"]["channels"]: - continue - - msg = ' '.join(line[3:])[1:] - rc = RC(msg) # new RC object to store this event's data - rc.parse() # parse a message into pagenames, usernames, etc. - process(rc) # report to frontend channels or start tasks - - if line[0] == "PING": # if we are pinged, pong back to the server - connection.send("PONG %s" % line[1]) - - # when we've finished starting up, join all watcher channels - if line[1] == "376": - for chan in config.irc["watcher"]["channels"]: - connection.join(chan) - -def process(rc): - """Process a message from IRC (technically, an RC object). The actual - processing is configurable, so we don't have that hard-coded here. We - simply call irc/watcher_logic.py's process() function and expect a list of - channels back, which we report the event data to.""" + _process_message(line) + +def _process_message(line): + """Process a single message from IRC.""" + line = line.strip().split() + + if line[1] == "PRIVMSG": + chan = line[2] + + # Ignore messages originating from channels not in our list, to prevent + # someone PMing us false data: + if chan not in config.irc["watcher"]["channels"]: + continue + + msg = ' '.join(line[3:])[1:] + rc = RC(msg) # new RC object to store this event's data + rc.parse() # parse a message into pagenames, usernames, etc. + process_rc(rc) # report to frontend channels or start tasks + + # If we are pinged, pong back to the server: + elif line[0] == "PING": + msg = " ".join(("PONG", line[1])) + connection.send(msg) + + # When we've finished starting up, join all watcher channels: + elif line[1] == "376": + for chan in config.irc["watcher"]["channels"]: + connection.join(chan) + +def process_rc(rc): + """Process a recent change event from IRC (or, an RC object). + + The actual processing is configurable, so we don't have that hard-coded + here. We simply call watcher_logic's process() function and expect a list + of channels back, which we report the event data to. + """ chans = watcher_logic.process(rc) if chans and frontend_conn: pretty = rc.get_pretty() diff --git a/bot/watcher_logic.py b/bot/watcher_logic.py index a7a6476..c86ac9e 100644 --- a/bot/watcher_logic.py +++ b/bot/watcher_logic.py @@ -12,7 +12,7 @@ sense for this sort of thing... so... import re -from wiki import task_manager as tasks +import tasks afc_prefix = "wikipedia( talk)?:(wikiproject )?articles for creation" @@ -39,8 +39,8 @@ def process(rc): chans.update(("##earwigbot", "#wikipedia-en-afc")) if r_page.search(page_name): - tasks.start_task("afc_statistics", action="process_edit", page=rc.page) - tasks.start_task("afc_copyvios", action="process_edit", page=rc.page) + tasks.start("afc_statistics", action="process_edit", page=rc.page) + tasks.start("afc_copyvios", action="process_edit", page=rc.page) chans.add("#wikipedia-en-afc") elif r_ffu.match(page_name): @@ -50,22 +50,22 @@ def process(rc): chans.add("#wikipedia-en-afc") 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] - tasks.start_task("afc_statistics", action="process_move", pages=p) - tasks.start_task("afc_copyvios", action="process_move", pages=p) + tasks.start("afc_statistics", action="process_move", pages=p) + tasks.start("afc_copyvios", action="process_move", pages=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_task("afc_statistics", action="process_delete", page=p) - tasks.start_task("afc_copyvios", action="process_delete", page=p) + tasks.start("afc_statistics", action="process_delete", page=p) + tasks.start("afc_copyvios", action="process_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_task("afc_statistics", action="process_restore", page=p) - tasks.start_task("afc_copyvios", action="process_restore", page=p) + tasks.start("afc_statistics", action="process_restore", page=p) + tasks.start("afc_copyvios", action="process_restore", page=p) chans.add("#wikipedia-en-afc") elif rc.flags == "protect" and r_protect.match(comment): diff --git a/earwigbot.py b/earwigbot.py index 9706378..a98f961 100755 --- a/earwigbot.py +++ b/earwigbot.py @@ -20,7 +20,7 @@ from os import path from sys import executable from time import sleep -from core.config import verify_config +from bot import config __author__ = "Ben Kurtovic" __copyright__ = "Copyright (C) 2009, 2010, 2011 by Ben Kurtovic" @@ -28,12 +28,12 @@ __license__ = "MIT License" __version__ = "0.1-dev" __email__ = "ben.kurtovic@verizon.net" -bot_script = path.join(path.dirname(path.abspath(__file__)), "core", "main.py") +bot_script = path.join(path.dirname(path.abspath(__file__)), "bot", "main.py") def main(): print "EarwigBot v{0}\n".format(__version__) - is_encrypted = verify_config() + is_encrypted = config.verify_config() if is_encrypted: # passwords in the config file are encrypted key = getpass("Enter key to unencrypt bot passwords: ") else: From b7b885beb4933fb7af81123304f76aa5fb7c3142 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 02:14:05 -0400 Subject: [PATCH 04/16] watcher_logic -> rules --- bot/{watcher_logic.py => rules.py} | 5 +---- bot/watcher.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) rename bot/{watcher_logic.py => rules.py} (95%) diff --git a/bot/watcher_logic.py b/bot/rules.py similarity index 95% rename from bot/watcher_logic.py rename to bot/rules.py index c86ac9e..f4a7a71 100644 --- a/bot/watcher_logic.py +++ b/bot/rules.py @@ -1,13 +1,10 @@ # -*- coding: utf-8 -*- """ -EarwigBot's IRC Watcher Logic +EarwigBot's IRC Watcher Rules This file contains (configurable!) rules that EarwigBot's watcher uses after it recieves an event from IRC. - -This should, ideally, be in config.json somehow, but Python code makes more -sense for this sort of thing... so... """ import re diff --git a/bot/watcher.py b/bot/watcher.py index 2e43099..c863e36 100644 --- a/bot/watcher.py +++ b/bot/watcher.py @@ -4,15 +4,15 @@ EarwigBot's IRC Watcher Component The IRC watcher runs on a wiki recent-changes server and listens for edits. -Users cannot interact with this part of the bot. When an event occurs, run it -through irc/watcher_logic.py's process() function, which can result in either -wiki bot tasks being started (listed in wiki/tasks/) or messages being sent to -channels in the IRC frontend. +Users cannot interact with this part of the bot. When an event occurs, we run +it through rules.py's process() function, which can result in wiki bot tasks +being started (located in tasks/) or messages being sent to channels on the IRC +frontend. """ import config from classes import Connection, RC, BrokenSocketException -import watcher_logic as logic +import rules frontend_conn = None @@ -81,10 +81,10 @@ def process_rc(rc): """Process a recent change event from IRC (or, an RC object). The actual processing is configurable, so we don't have that hard-coded - here. We simply call watcher_logic's process() function and expect a list - of channels back, which we report the event data to. + here. We simply call rules's process() function and expect a list of + channels back, which we report the event data to. """ - chans = watcher_logic.process(rc) + chans = rules.process(rc) if chans and frontend_conn: pretty = rc.get_pretty() for chan in chans: From 5c954b23d1007b5ebdb29b181af471315597052c Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 02:36:13 -0400 Subject: [PATCH 05/16] syntax error fix --- bot/watcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/watcher.py b/bot/watcher.py index c863e36..1e4152a 100644 --- a/bot/watcher.py +++ b/bot/watcher.py @@ -60,7 +60,7 @@ def _process_message(line): # Ignore messages originating from channels not in our list, to prevent # someone PMing us false data: if chan not in config.irc["watcher"]["channels"]: - continue + return msg = ' '.join(line[3:])[1:] rc = RC(msg) # new RC object to store this event's data From d79d99b5ae2a58872e5fb0b1024c3f8237a212c1 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 03:07:46 -0400 Subject: [PATCH 06/16] cleanup, fixes, etc to Connection and a minor docstring change to BaseCommand --- bot/classes/base_command.py | 5 ++-- bot/classes/connection.py | 68 +++++++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/bot/classes/base_command.py b/bot/classes/base_command.py index 31750f6..e3d4807 100644 --- a/bot/classes/base_command.py +++ b/bot/classes/base_command.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- -# A base class for commands on IRC. - class BaseCommand(object): + """A base class for commands on IRC.""" + def __init__(self, connection): - """A base class for commands on IRC.""" self.connection = connection def get_hooks(self): diff --git a/bot/classes/connection.py b/bot/classes/connection.py index 87be572..c108bb2 100644 --- a/bot/classes/connection.py +++ b/bot/classes/connection.py @@ -1,32 +1,36 @@ # -*- coding: utf-8 -*- -# A class to interface with IRC. - import socket import threading class BrokenSocketException(Exception): - """A socket has broken, because it is not sending data.""" + """A socket has broken, because it is not sending data. Raised by + Connection.get().""" pass class Connection(object): - 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.""" + + def __init__(self, host=None, port=None, nick=None, ident=None, + realname=None): self.host = host self.port = port self.nick = nick self.ident = ident self.realname = realname + # A lock to prevent us from sending two messages at once. + self.lock = threading.Lock() + def connect(self): - """connect to IRC""" + """Connect to our IRC server.""" self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((self.host, self.port)) self.send("NICK %s" % self.nick) self.send("USER %s %s * :%s" % (self.ident, self.host, self.realname)) def close(self): - """close our connection with IRC""" + """Close our connection with the IRC server.""" try: self.sock.shutdown(socket.SHUT_RDWR) # shut down connection first except socket.error: @@ -34,42 +38,52 @@ class Connection(object): self.sock.close() def get(self, size=4096): - """receive (get) data from the server""" + """Receive (i.e. get) data from the server.""" data = self.sock.recv(4096) - if not data: # socket giving us no data, so it is dead/broken + if not data: + # Socket isn't giving us any data, so it is dead or broken: raise BrokenSocketException() return data def send(self, msg): - """send data to the server""" - lock = threading.Lock() - lock.acquire() # ensure that we only send one message at a time (blocking) - try: + """Send data to the server.""" + # Ensure that we only send one message at a time with a blocking lock: + with self.lock: self.sock.sendall(msg + "\r\n") print " %s" % msg - finally: - lock.release() def say(self, target, msg): - """send a message""" - self.send("PRIVMSG %s :%s" % (target, msg)) + """Send a private message to a target on the server.""" + message = "".join(("PRIVMSG ", target, " :", msg)) + self.send(message) def reply(self, data, msg): - """send a message as a reply""" - self.say(data.chan, "%s%s%s: %s" % (chr(2), data.nick, chr(0x0f), msg)) + """Send a private message as a reply to a user on the server. `data` is + a Data object (or anything with chan and nick attributes).""" + message = "".join((chr(2), data.nick, chr(0x0f), ": ", msg)) + self.say(data.chan, message) def action(self, target, msg): - """send a message as an action""" - self.say(target,"%sACTION %s%s" % (chr(1), msg, chr(1))) + """Send a private message to a target on the server as an action.""" + message = "".join((chr(1), "ACTION ", msg, chr(1))) + self.say(target, message) def notice(self, target, msg): - """send a notice""" - self.send("NOTICE %s :%s" % (target, msg)) + """Send a notice to a target on the server.""" + message = "".join(("NOTICE ", target, " :", msg)) + self.send(message) def join(self, chan): - """join a channel""" - self.send("JOIN %s" % chan) + """Join a channel on the server.""" + message = " ".join(("JOIN", chan)) + self.send(message) + + def part(self, chan): + """Part from a channel on the server.""" + message = " ".join(("PART", chan)) + self.send(message) def mode(self, chan, level, msg): - """send a mode message""" - self.send("MODE %s %s %s" % (chan, level, msg)) + """Send a mode message to the server.""" + message = " ".join(("MODE", chan, level, msg)) + self.send(message) From bffa9b673955f8c363d032db674a6d03f035a5cc Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 03:21:21 -0400 Subject: [PATCH 07/16] Cleaned up Data class a bit. --- bot/classes/data.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/bot/classes/data.py b/bot/classes/data.py index 1075fc3..e35a3d4 100644 --- a/bot/classes/data.py +++ b/bot/classes/data.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -# A class to store data from an individual line received on IRC. - import re class KwargParseException(Exception): @@ -11,8 +9,9 @@ class KwargParseException(Exception): pass class Data(object): + """Store data from an individual line received on IRC.""" + def __init__(self, line): - """Store data from an individual line received on IRC.""" self.line = line self.chan = str() self.nick = str() @@ -21,35 +20,42 @@ class Data(object): self.msg = str() def parse_args(self): - """parse command arguments from self.msg into self.command and self.args""" - args = self.msg.strip().split(' ') # strip out extra whitespace and split the message into a list - while '' in args: # remove any empty arguments - args.remove('') + """Parse command args from self.msg into self.command and self.args.""" + args = self.msg.strip().split(" ") + + while "" in args: + args.remove("") - self.args = args[1:] # the command arguments - self.is_command = False # whether this is a real command or not + # Isolate command arguments: + self.args = args[1:] + self.is_command = False # is this message a command? try: - self.command = args[0] # the command itself + self.command = args[0] except IndexError: self.command = None try: if self.command.startswith('!') or self.command.startswith('.'): self.is_command = True - self.command = self.command[1:] # strip '!' or '.' - self.command = self.command.lower() # lowercase command name + self.command = self.command[1:] # Strip the '!' or '.' + self.command = self.command.lower() except AttributeError: pass def parse_kwargs(self): - """parse command arguments from self.args, given as !command key1=value1 key2=value2..., into a dict self.kwargs: {'key1': 'value2', 'key2': 'value2'...}""" + """Parse keyword arguments embedded in self.args. + + Parse a command given as "!command key1=value1 key2=value2..." into a + dict, self.kwargs, like {'key1': 'value2', 'key2': 'value2'...}. + """ self.kwargs = {} for arg in self.args[2:]: try: key, value = re.findall("^(.*?)\=(.*?)$", arg)[0] except IndexError: raise KwargParseException(arg) - if not key or not value: + if key and value: + self.kwargs[key] = value + else: raise KwargParseException(arg) - self.kwargs[key] = value From ff7af4eb6ba38728c93b09b1bce2d19658c84162 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 17:26:20 -0400 Subject: [PATCH 08/16] cleaned up RC class a lot; get_pretty() -> prettify() --- bot/classes/rc.py | 84 +++++++++++++++++++++++++++++++++---------------------- bot/watcher.py | 2 +- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/bot/classes/rc.py b/bot/classes/rc.py index 2c913b0..a2d23b6 100644 --- a/bot/classes/rc.py +++ b/bot/classes/rc.py @@ -1,57 +1,73 @@ # -*- coding: utf-8 -*- -# A class to store data on an individual event received from our IRC watcher. - import re class RC(object): + """A class to store data on an event received from our IRC watcher.""" + re_color = re.compile("\x03([0-9]{1,2}(,[0-9]{1,2})?)?") + re_edit = re.compile("\A\[\[(.*?)\]\]\s(.*?)\s(http://.*?)\s\*\s(.*?)\s\*\s(.*?)\Z") + re_log = re.compile("\A\[\[(.*?)\]\]\s(.*?)\s\*\s(.*?)\s\*\s(.*?)\Z") + def __init__(self, msg): - """store data on an individual event received from our IRC watcher""" self.msg = msg def parse(self): - """parse recent changes log into some variables""" + """Parse a recent change event into some variables.""" + # Strip IRC color codes; we don't want or need 'em: + self.msg = self.re_color.sub("", self.msg).strip() msg = self.msg - msg = re.sub("\x03([0-9]{1,2}(,[0-9]{1,2})?)?", "", msg) # strip IRC color codes; we don't want/need 'em - msg = msg.strip() - self.msg = msg self.is_edit = True - # flags: 'M' for minor edit, 'B' for bot edit, 'create' for a user creation log entry... + # Flags: 'M' for minor edit, 'B' for bot edit, 'create' for a user + # creation log entry, etc: try: - page, flags, url, user, comment = re.findall("\A\[\[(.*?)\]\]\s(.*?)\s(http://.*?)\s\*\s(.*?)\s\*\s(.*?)\Z", msg)[0] - except IndexError: # we're probably missing the http:// part, because it's a log entry, which lacks a url - page, flags, user, comment = re.findall("\A\[\[(.*?)\]\]\s(.*?)\s\*\s(.*?)\s\*\s(.*?)\Z", msg)[0] - url = "http://en.wikipedia.org/wiki/{}".format(page) - flags = flags.strip() # flag tends to have a extraneous whitespace character at the end when it's a log entry - self.is_edit = False # this is a log entry, not edit - - self.page, self.flags, self.url, self.user, self.comment = page, flags, url, user, comment - - def get_pretty(self): - """make a nice, colorful message from self.msg to send to the front-end""" + page, self.flags, url, user, comment = self.re_edit.findall(msg)[0] + except IndexError: + # We're probably missing the http:// part, because it's a log + # entry, which lacks a URL: + page, flags, user, comment = self.re_log.findall(msg)[0] + url = "".join(("http://en.wikipedia.org/wiki/", page)) + + self.is_edit = False # This is a log entry, not edit + + # Flags tends to have extra whitespace at the end when they're + # log entries: + self.flags = flags.strip() + + self.page, self.url, self.user, self.comment = page, url, user, comment + + def prettify(self): + """Make a nice, colorful message to send back to the IRC front-end.""" flags = self.flags - event_type = flags # "New :" if we don't know exactly what happened + # "New :" if we don't know exactly what happened: + event_type = flags if "N" in flags: - event_type = "page" # "New page:" + event_type = "page" # "New page:" elif flags == "delete": - event_type = "deletion" # "New deletion:" + event_type = "deletion" # "New deletion:" elif flags == "protect": - event_type = "protection" # "New protection:" + event_type = "protection" # "New protection:" elif flags == "create": - event_type = "user" # "New user:" + event_type = "user" # "New user:" if self.page == "Special:Log/move": - event_type = "move" # New move: + event_type = "move" # New move: else: - event_type = "edit" # "New edit:" + event_type = "edit" # "New edit:" if "B" in flags: - event_type = "bot {}".format(event_type) # "New bot edit:" + # "New bot edit:" + event_type = "bot {}".format(event_type) if "M" in flags: - event_type = "minor {}".format(event_type) # "New minor edit:" OR "New minor bot edit:" - + # "New minor edit:" OR "New minor bot edit:" + event_type = "minor {}".format(event_type) + + # Example formatting: + # New edit: [[Page title]] * User name * http://en... * edit summary if self.is_edit: - pretty = "\x02New {0}\x0F: \x0314[[\x0307{1}\x0314]]\x0306 *\x0303 {2}\x0306 *\x0302 {3}\x0306 *\x0310 {4}".format(event_type, self.page, self.user, self.url, self.comment) - else: - pretty = "\x02New {0}\x0F: \x0303{1}\x0306 *\x0302 {2}\x0306 *\x0310 {3}".format(event_type, self.user, self.url, self.comment) - - return pretty + return "".join(("\x02New ", event_type, "\x0F: \x0314[[\x0307", + self.page, "\x0314]]\x0306 *\x0303 ", self.user, + "\x0306 *\x0302 ", self.url, "\x0306 *\x0310 ", + self.comment)) + + return "".join(("\x02New ", event_type, "\x0F: \x0303", self.user, + "\x0306 *\x0302 ", self.url, "\x0306 *\x0310 ", + self.comment)) diff --git a/bot/watcher.py b/bot/watcher.py index 1e4152a..9153d9a 100644 --- a/bot/watcher.py +++ b/bot/watcher.py @@ -86,6 +86,6 @@ def process_rc(rc): """ chans = rules.process(rc) if chans and frontend_conn: - pretty = rc.get_pretty() + pretty = rc.prettify() for chan in chans: frontend_conn.say(chan, pretty) From 8c6fb2e8ba7945a9e76c34200e07f4d39be43585 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 19:10:16 -0400 Subject: [PATCH 09/16] more cleanup, improvements, fixes, whatever; restructured command files, but haven't done all of them yet --- bot/classes/base_command.py | 43 ++++++++++++++++++------------ bot/classes/base_task.py | 24 +++++++++++------ bot/classes/connection.py | 5 ++-- bot/classes/data.py | 6 +---- bot/commands/__init__.py | 26 +++++++++++------- bot/commands/help.py | 64 +++++++++++++++++++-------------------------- bot/commands/test.py | 18 +++---------- bot/tasks/__init__.py | 6 ++--- 8 files changed, 95 insertions(+), 97 deletions(-) diff --git a/bot/classes/base_command.py b/bot/classes/base_command.py index e3d4807..710e6eb 100644 --- a/bot/classes/base_command.py +++ b/bot/classes/base_command.py @@ -1,32 +1,41 @@ # -*- coding: utf-8 -*- class BaseCommand(object): - """A base class for commands on IRC.""" + """A base class for commands on IRC. + + This docstring is reported to the user when they use !help . + """ + # This is the command's name, as reported to the user when they use !help: + name = "base_command" + + # Hooks are "msg", "msg_private", "msg_public", and "join". "msg" is the + # default behavior; if you wish to override that, change the value in your + # command subclass: + hooks = ["msg"] def __init__(self, connection): self.connection = connection - def get_hooks(self): - """Hooks are: 'msg', 'msg_private', 'msg_public', and 'join'. Return - the hooks you want this command to be called on.""" - return [] + def check(self, data): + """Returns whether this command should be called in response to 'data'. - def get_help(self, command): - """Return help information for the command, used by !help. return None - for no help. If a given class handles multiple commands, the command - variable can be used to return different help for each one.""" - return None + Given a Data() instance, return True if we should respond to this + activity, or False if we should ignore it or it doesn't apply to us. - def check(self, data): - """Given a Data() object, return True if we should respond to this - activity, or False if we should ignore it/it doesn't apply to us. Most - commands return True if data.command == 'command_name', otherwise - they return False.""" + Most commands return True if data.command == self.name, otherwise they + return False. This is the default behavior of check(); you need only + override it if you wish to change that. + """ + if data.is_command and data.command == self.name: + return True return False def process(self, data): - """Handle an activity (usually a message) on IRC. At this point, thanks + """Main entry point for doing a command. + + Handle an activity (usually a message) on IRC. At this point, thanks to self.check() which is called automatically by command_handler, we know this is something we should respond to, so (usually) a - 'if data.command != "command_name": return' is unnecessary.""" + 'if data.command != "command_name": return' is unnecessary. + """ pass diff --git a/bot/classes/base_task.py b/bot/classes/base_task.py index e5abeba..e470178 100644 --- a/bot/classes/base_task.py +++ b/bot/classes/base_task.py @@ -5,15 +5,23 @@ class BaseTask(object): task_name = None def __init__(self): - """This is called once immediately after the task class is loaded by - the task manager (in wiki.task_manager.load_class_from_file()).""" + """Constructor for new tasks. + + This is called once immediately after the task class is loaded by + the task manager (in tasks._load_class_from_file()). + """ pass def run(self, **kwargs): - """This is called directly by task_manager.start_task() and is the main - way to make a task do stuff. kwargs will be any keyword arguments - passed to start_task(), which are (of course) optional. The same task - instance is preserved between runs, so you can theoretically store data - in self (e.g. start_task('mytask', action='store', data='foo')) and - then use it later (e.g. start_task('mytask', action='save')).""" + """Main entry point to run a given task. + + This is called directly by tasks.start() and is the main way to make a + task do stuff. kwargs will be any keyword arguments passed to start() + which are entirely optional. + + The same task instance is preserved between runs, so you can + theoretically store data in self (e.g. + start('mytask', action='store', data='foo')) and then use it later + (e.g. start('mytask', action='save')). + """ pass diff --git a/bot/classes/connection.py b/bot/classes/connection.py index c108bb2..8e2774a 100644 --- a/bot/classes/connection.py +++ b/bot/classes/connection.py @@ -19,7 +19,7 @@ class Connection(object): self.ident = ident self.realname = realname - # A lock to prevent us from sending two messages at once. + # A lock to prevent us from sending two messages at once: self.lock = threading.Lock() def connect(self): @@ -58,8 +58,7 @@ class Connection(object): self.send(message) def reply(self, data, msg): - """Send a private message as a reply to a user on the server. `data` is - a Data object (or anything with chan and nick attributes).""" + """Send a private message as a reply to a user on the server.""" message = "".join((chr(2), data.nick, chr(0x0f), ": ", msg)) self.say(data.chan, message) diff --git a/bot/classes/data.py b/bot/classes/data.py index e35a3d4..7445097 100644 --- a/bot/classes/data.py +++ b/bot/classes/data.py @@ -13,11 +13,7 @@ class Data(object): def __init__(self, line): self.line = line - self.chan = str() - self.nick = str() - self.ident = str() - self.host = str() - self.msg = str() + self.chan = self.nick = self.ident = self.host = self.msg = "" def parse_args(self): """Parse command args from self.msg into self.command and self.args.""" diff --git a/bot/commands/__init__.py b/bot/commands/__init__.py index fc25de5..c479490 100644 --- a/bot/commands/__init__.py +++ b/bot/commands/__init__.py @@ -1,16 +1,23 @@ # -*- coding: utf-8 -*- -# A module to manage IRC commands. +""" +EarwigBot's IRC Command Manager + +This package provides the IRC "commands" used by the bot's front-end component. +In __init__, you can find some functions used to load and run these commands. +""" import os import traceback __all__ = ["load", "get_all", "check"] -_commands = [] +# Store commands in a dict, where the key is the command's name and the value +# is an instance of the command's class: +_commands = {} -def _process_module(connection, module): - """go through all objects in a module and add valid command classes to the commands variable""" +def _load_class_from_file(connection, module): + """Add.""" global commands objects = dir(module) @@ -30,9 +37,9 @@ def _process_module(connection, module): continue def load(connection): - """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.sort() # alphabetically sort list of files + """Load all valid commands into the _commands global variable.""" + files = os.listdir(os.path.join("bot", "commands")) + files.sort() for f in files: if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" @@ -50,12 +57,11 @@ def load(connection): print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds)) def get_all(): - """Return our list of all commands.""" + """Return our dict of all loaded commands.""" return _commands def check(hook, data): - """Given an event on IRC, check if there's anything we can respond to by - calling each command class""" + """Given an event on IRC, check if there's anything we can respond to.""" # parse command arguments into data.command and data.args data.parse_args() diff --git a/bot/commands/help.py b/bot/commands/help.py index 54eefa2..dc98f16 100644 --- a/bot/commands/help.py +++ b/bot/commands/help.py @@ -1,54 +1,44 @@ # -*- coding: utf-8 -*- -# Generates help information. +from classes import BaseCommand, Data +import commands -from irc.classes import BaseCommand, Data -from irc import command_handler - -class Help(BaseCommand): - def get_hooks(self): - return ["msg"] - - def get_help(self, command): - return "Generates help information." - - def check(self, data): - if data.is_command and data.command == "help": - return True - return False +class Command(BaseCommand): + """Generates help information.""" + name = "help" def process(self, data): + self.cmnds = commands.get_all().keys() if not data.args: - self.do_general_help(data) + self.do_main_help(data) else: - if data.args[0] == "list": - self.do_list_help(data) - else: - self.do_command_help(data) - - def do_general_help(self, data): - self.connection.reply(data, "I am a bot! You can get help for any command with '!help ', or a list of all loaded modules with '!help list'.") + self.do_command_help(data) - def do_list_help(self, data): - commands = command_handler.get_commands() - cmnds = map(lambda c: c.__class__.__name__, commands) - pretty_cmnds = ', '.join(cmnds) - self.connection.reply(data, "%s command classes loaded: %s." % (len(cmnds), pretty_cmnds)) + def do_main_help(self, data): + """Give the user a general help message with a list of all commands.""" + msg = "I am a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help '." + msg.format(len(self.cmnds), ', '.join(self.cmnds)) + self.connection.reply(data, msg) def do_command_help(self, data): + """Give the user help for a specific command.""" command = data.args[0] - commands = command_handler.get_commands() - dummy = Data() # dummy message to test which command classes pick up this command - dummy.command = command.lower() # lowercase command name + # Create a dummy message to test which commands pick up the user's + # input: + dummy = Data() + dummy.command = command.lower() dummy.is_command = True - for cmnd in commands: + for cmnd in self.cmnds: if cmnd.check(dummy): - help = cmnd.get_help(command) + doc = cmnd.__doc__ + if doc: + msg = "info for command \x0303{0}\x0301: \"{1}\"" + msg.format(command, doc) + self.connection.reply(data, msg) + return break - try: - self.connection.reply(data, "info for command \x0303%s\x0301: \"%s\"" % (command, help)) - except UnboundLocalError: - self.connection.reply(data, "sorry, no help for \x0303%s\x0301." % command) + msg = "sorry, no help for \x0303{0}\x0301.".format(command) + self.connection.reply(data, msg) diff --git a/bot/commands/test.py b/bot/commands/test.py index 630a37f..2bb59f8 100644 --- a/bot/commands/test.py +++ b/bot/commands/test.py @@ -1,22 +1,12 @@ # -*- coding: utf-8 -*- -# A very simple command to test the bot. - import random -from irc.classes import BaseCommand - -class Test(BaseCommand): - def get_hooks(self): - return ["msg"] - - def get_help(self, command): - return "Test the bot!" +from classes import BaseCommand - def check(self, data): - if data.is_command and data.command == "test": - return True - return False +class Command(BaseCommand): + """Test the bot!""" + name = "test" def process(self, data): hey = random.randint(0, 1) diff --git a/bot/tasks/__init__.py b/bot/tasks/__init__.py index 502813a..98ae925 100644 --- a/bot/tasks/__init__.py +++ b/bot/tasks/__init__.py @@ -16,9 +16,9 @@ import config __all__ = ["load", "schedule", "start"] -# store loaded tasks as a dict where the key is the task name and the value is -# an instance of the task class (wiki.tasks.task_file.Task()) -_tasks = dict() +# Store loaded tasks as a dict where the key is the task name and the value is +# an instance of the task class: +_tasks = {} def _load_class_from_file(f): """Look in a given file for the task class.""" From 99d0e7588f3cffa1efe3fb809490e9831f67b8e2 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 22:44:20 -0400 Subject: [PATCH 10/16] loading commands now works, along with 'help' and 'test'; docstring fixes, etc --- bot/classes/base_command.py | 13 +++++-- bot/classes/base_task.py | 2 +- bot/commands/__init__.py | 82 ++++++++++++++++++++++++++------------------- bot/commands/help.py | 13 ++++--- bot/tasks/__init__.py | 2 +- 5 files changed, 66 insertions(+), 46 deletions(-) diff --git a/bot/classes/base_command.py b/bot/classes/base_command.py index 710e6eb..f323345 100644 --- a/bot/classes/base_command.py +++ b/bot/classes/base_command.py @@ -14,6 +14,13 @@ class BaseCommand(object): hooks = ["msg"] def __init__(self, connection): + """Constructor for new commands. + + This is called once when the command is loaded (from + commands._load_command()). `connection` is a Connection object, + allowing us to do self.connection.say(), self.connection.send(), etc, + from within a method. + """ self.connection = connection def check(self, data): @@ -34,8 +41,8 @@ class BaseCommand(object): """Main entry point for doing a command. Handle an activity (usually a message) on IRC. At this point, thanks - to self.check() which is called automatically by command_handler, we - know this is something we should respond to, so (usually) a - 'if data.command != "command_name": return' is unnecessary. + to self.check() which is called automatically by the command handler, + we know this is something we should respond to, so (usually) something + like 'if data.command != "command_name": return' is unnecessary. """ pass diff --git a/bot/classes/base_task.py b/bot/classes/base_task.py index e470178..278a77d 100644 --- a/bot/classes/base_task.py +++ b/bot/classes/base_task.py @@ -8,7 +8,7 @@ class BaseTask(object): """Constructor for new tasks. This is called once immediately after the task class is loaded by - the task manager (in tasks._load_class_from_file()). + the task manager (in tasks._load_task()). """ pass diff --git a/bot/commands/__init__.py b/bot/commands/__init__.py index c479490..7cb27be 100644 --- a/bot/commands/__init__.py +++ b/bot/commands/__init__.py @@ -8,53 +8,67 @@ In __init__, you can find some functions used to load and run these commands. """ import os +import sys import traceback +from classes import BaseCommand +import config + __all__ = ["load", "get_all", "check"] +# Base directory when searching for commands: +base_dir = os.path.join(config.root_dir, "bot", "commands") + # Store commands in a dict, where the key is the command's name and the value # is an instance of the command's class: _commands = {} -def _load_class_from_file(connection, module): - """Add.""" - global commands - objects = dir(module) +def _load_command(connection, filename): + """Try to load a specific command from a module, identified by file name. - for this_obj in objects: # go through everything in the file - obj = eval("module.%s" % this_obj) # this_obj is a string, so get the actual object corresponding to that string + Given a Connection object and a filename, we'll first try to import it, + and if that works, make an instance of the 'Command' class inside (assuming + it is an instance of BaseCommand), add it to _commands, and report the + addition to the user. Any problems along the way will either be ignored or + reported. + """ + global _commands - try: - bases = obj.__bases__ - except AttributeError: # object isn't a valid class, so ignore it - continue + # Strip .py from the end of the filename and join with our package name: + name = ".".join(("commands", filename[:-3])) + try: + __import__(name) + except: + print "Couldn't load file {0}:".format(filename) + traceback.print_exc() + return - for base in bases: - if base.__name__ == "BaseCommand": # this inherits BaseCommand, so it must be a command class - command = obj(connection) # initialize a new command object - _commands.append(command) - print "Added command class %s from %s..." % (this_obj, module.__name__) - continue + command = sys.modules[name].Command(connection) + if not isinstance(command, BaseCommand): + return + + _commands[command.name] = command + print "Added command {0}...".format(command.name) def load(connection): - """Load all valid commands into the _commands global variable.""" - files = os.listdir(os.path.join("bot", "commands")) + """Load all valid commands into the _commands global variable. + + `connection` is a Connection object that is given to each command's + constructor. + """ + files = os.listdir(base_dir) files.sort() - for f in files: - if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" + for filename in files: + if filename.startswith("_") or not filename.endswith(".py"): continue - module = f[:-3] # strip .py from end try: - exec "from irc.commands import %s" % module - except: # importing the file failed for some reason... - print "Couldn't load file %s:" % f - traceback.print_exc() - continue - process_module(connection, eval(module)) # 'module' is a string, so get the actual object for processing by eval-ing it + _load_command(connection, filename) + except AttributeError: + pass # The file is doesn't contain a command, so just move on - pretty_cmnds = map(lambda c: c.__class__.__name__, commands) - print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds)) + msg = "Found {0} command classes: {1}." + print msg.format(len(_commands), ", ".join(_commands.keys())) def get_all(): """Return our dict of all loaded commands.""" @@ -62,15 +76,15 @@ def get_all(): def check(hook, data): """Given an event on IRC, check if there's anything we can respond to.""" - # parse command arguments into data.command and data.args + # Parse command arguments into data.command and data.args: data.parse_args() - for command in _commands: - if hook in command.get_hooks(): + for command in _commands.values(): + if hook in command.hooks: if command.check(data): try: command.process(data) except: - print "Error executing command '{}':".format(data.command) - traceback.print_exc() # catch exceptions and print them + print "Error executing command '{0}':".format(data.command) + traceback.print_exc() break diff --git a/bot/commands/help.py b/bot/commands/help.py index dc98f16..6f57381 100644 --- a/bot/commands/help.py +++ b/bot/commands/help.py @@ -4,11 +4,11 @@ from classes import BaseCommand, Data import commands class Command(BaseCommand): - """Generates help information.""" + """Displays help information.""" name = "help" def process(self, data): - self.cmnds = commands.get_all().keys() + self.cmnds = commands.get_all() if not data.args: self.do_main_help(data) else: @@ -17,7 +17,7 @@ class Command(BaseCommand): def do_main_help(self, data): """Give the user a general help message with a list of all commands.""" msg = "I am a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help '." - msg.format(len(self.cmnds), ', '.join(self.cmnds)) + msg = msg.format(len(self.cmnds.keys()), ', '.join(self.cmnds)) self.connection.reply(data, msg) def do_command_help(self, data): @@ -26,17 +26,16 @@ class Command(BaseCommand): # Create a dummy message to test which commands pick up the user's # input: - dummy = Data() + dummy = Data(1) dummy.command = command.lower() dummy.is_command = True - for cmnd in self.cmnds: + for cmnd in self.cmnds.values(): if cmnd.check(dummy): doc = cmnd.__doc__ if doc: msg = "info for command \x0303{0}\x0301: \"{1}\"" - msg.format(command, doc) - self.connection.reply(data, msg) + self.connection.reply(data, msg.format(command, doc)) return break diff --git a/bot/tasks/__init__.py b/bot/tasks/__init__.py index 98ae925..9d03547 100644 --- a/bot/tasks/__init__.py +++ b/bot/tasks/__init__.py @@ -20,7 +20,7 @@ __all__ = ["load", "schedule", "start"] # an instance of the task class: _tasks = {} -def _load_class_from_file(f): +def _load_task(f): """Look in a given file for the task class.""" global _tasks From af2e01a330dad378ed609aa58e9d715125bed83e Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 22:51:05 -0400 Subject: [PATCH 11/16] fixing imports in IRC commands --- bot/commands/afc_report.py | 6 +----- bot/commands/afc_status.py | 8 ++++---- bot/commands/calc.py | 2 +- bot/commands/chanops.py | 4 ++-- bot/commands/crypt.py | 4 ++-- bot/commands/git.py | 4 ++-- bot/commands/link.py | 2 +- bot/commands/remind.py | 2 +- bot/commands/rights.py | 6 +++--- bot/commands/tasks.py | 12 ++++++------ 10 files changed, 23 insertions(+), 27 deletions(-) diff --git a/bot/commands/afc_report.py b/bot/commands/afc_report.py index 09ec914..c32220e 100644 --- a/bot/commands/afc_report.py +++ b/bot/commands/afc_report.py @@ -1,14 +1,10 @@ # -*- coding: utf-8 -*- -""" -Get information about an AFC submission by name. -""" - import json import re import urllib -from irc.classes import BaseCommand +from classes import BaseCommand class AFCReport(BaseCommand): def get_hooks(self): diff --git a/bot/commands/afc_status.py b/bot/commands/afc_status.py index 0f5722e..1258eae 100644 --- a/bot/commands/afc_status.py +++ b/bot/commands/afc_status.py @@ -5,9 +5,9 @@ or a request via !status.""" import re -from core import config -from irc.classes import BaseCommand -from wiki import tools +from classes import BaseCommand +import config +import wiki class AFCStatus(BaseCommand): def get_hooks(self): @@ -28,7 +28,7 @@ class AFCStatus(BaseCommand): return False def process(self, data): - self.site = tools.get_site() + self.site = wiki.get_site() if data.line[1] == "JOIN": notice = self.get_join_notice() diff --git a/bot/commands/calc.py b/bot/commands/calc.py index fbd26a1..2a138a0 100644 --- a/bot/commands/calc.py +++ b/bot/commands/calc.py @@ -5,7 +5,7 @@ import re import urllib -from irc.classes import BaseCommand +from classes import BaseCommand class Calc(BaseCommand): def get_hooks(self): diff --git a/bot/commands/chanops.py b/bot/commands/chanops.py index 210a830..fec8399 100644 --- a/bot/commands/chanops.py +++ b/bot/commands/chanops.py @@ -2,8 +2,8 @@ # Voice/devoice/op/deop users in the channel. -from irc.classes import BaseCommand -from core import config +from classes import BaseCommand +import config class ChanOps(BaseCommand): def get_hooks(self): diff --git a/bot/commands/crypt.py b/bot/commands/crypt.py index 15d6e12..8741342 100644 --- a/bot/commands/crypt.py +++ b/bot/commands/crypt.py @@ -6,8 +6,8 @@ Cryptography functions (hashing and cyphers) for EarwigBot IRC. import hashlib -from irc.classes import BaseCommand -from lib import blowfish +from classes import BaseCommand +import blowfish class Cryptography(BaseCommand): def get_hooks(self): diff --git a/bot/commands/git.py b/bot/commands/git.py index 60c0ecc..d6b67c7 100644 --- a/bot/commands/git.py +++ b/bot/commands/git.py @@ -6,8 +6,8 @@ import shlex import subprocess import re -from irc.classes import BaseCommand -from core import config +from classes import BaseCommand +import config class Git(BaseCommand): def get_hooks(self): diff --git a/bot/commands/link.py b/bot/commands/link.py index b45f4cd..6c972af 100644 --- a/bot/commands/link.py +++ b/bot/commands/link.py @@ -4,7 +4,7 @@ import re -from irc.classes import BaseCommand +from classes import BaseCommand class Link(BaseCommand): def get_hooks(self): diff --git a/bot/commands/remind.py b/bot/commands/remind.py index 963f48c..631f2a1 100644 --- a/bot/commands/remind.py +++ b/bot/commands/remind.py @@ -7,7 +7,7 @@ Set a message to be repeated to you in a certain amount of time. import threading import time -from irc.classes import BaseCommand +from classes import BaseCommand class Remind(BaseCommand): def get_hooks(self): diff --git a/bot/commands/rights.py b/bot/commands/rights.py index 4289002..d84cfa7 100644 --- a/bot/commands/rights.py +++ b/bot/commands/rights.py @@ -4,8 +4,8 @@ Retrieve a list of user rights for a given username via the API. """ -from irc.classes import BaseCommand -from wiki import tools +from classes import BaseCommand +import wiki class Rights(BaseCommand): def get_hooks(self): @@ -25,7 +25,7 @@ class Rights(BaseCommand): return username = ' '.join(data.args) - site = tools.get_site() + site = wiki.get_site() user = site.get_user(username) rights = user.groups() if rights: diff --git a/bot/commands/tasks.py b/bot/commands/tasks.py index 9f33061..a465b84 100644 --- a/bot/commands/tasks.py +++ b/bot/commands/tasks.py @@ -5,9 +5,9 @@ import threading import re -from irc.classes import BaseCommand, Data, KwargParseException -from wiki import task_manager -from core import config +from classes import BaseCommand, Data, KwargParseException +import tasks +import config class Tasks(BaseCommand): def get_hooks(self): @@ -77,7 +77,7 @@ class Tasks(BaseCommand): def do_listall(self): """With !tasks listall or !tasks all, list all loaded tasks, and report whether they are currently running or idle.""" - tasks = task_manager.task_list.keys() + tasks = tasks._tasks.keys() threads = threading.enumerate() tasklist = [] @@ -115,11 +115,11 @@ class Tasks(BaseCommand): self.connection.reply(data, "error parsing argument: \x0303{0}\x0301.".format(arg)) return - if task_name not in task_manager.task_list.keys(): # this task does not exist or hasn't been loaded + if task_name not in tasks._tasks.keys(): # this task does not exist or hasn't been loaded self.connection.reply(data, "task could not be found; either wiki/tasks/{0}.py doesn't exist, or it wasn't loaded correctly.".format(task_name)) return - task_manager.start_task(task_name, **data.kwargs) + tasks.start(task_name, **data.kwargs) self.connection.reply(data, "task \x0302{0}\x0301 started.".format(task_name)) def get_main_thread_name(self): From 9237a7ed9e6b21bd3788fe21ee7891c8500285e7 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 8 Aug 2011 01:12:11 -0400 Subject: [PATCH 12/16] all IRC commands should work now; renamed tasks.py to threads.py to avoid conflicting with bot/tasks/ --- bot/commands/afc_report.py | 14 +---- bot/commands/afc_status.py | 58 ++++++++++-------- bot/commands/calc.py | 20 ++----- bot/commands/chanops.py | 25 ++++---- bot/commands/crypt.py | 47 +++++---------- bot/commands/git.py | 109 +++++++++++++++++++--------------- bot/commands/help.py | 24 +++++--- bot/commands/link.py | 42 +++++++------ bot/commands/remind.py | 35 +++++------ bot/commands/rights.py | 41 +++++++------ bot/commands/{tasks.py => threads.py} | 81 ++++++++++++++----------- bot/tasks/__init__.py | 21 ++++--- bot/wiki/__init__.py | 16 ++--- bot/wiki/category.py | 4 +- bot/wiki/constants.py | 2 +- bot/wiki/exceptions.py | 2 +- bot/wiki/functions.py | 18 +++--- bot/wiki/page.py | 2 +- bot/wiki/site.py | 17 +++--- bot/wiki/user.py | 11 ++-- 20 files changed, 306 insertions(+), 283 deletions(-) rename bot/commands/{tasks.py => threads.py} (50%) diff --git a/bot/commands/afc_report.py b/bot/commands/afc_report.py index c32220e..402b959 100644 --- a/bot/commands/afc_report.py +++ b/bot/commands/afc_report.py @@ -6,17 +6,9 @@ import urllib from classes import BaseCommand -class AFCReport(BaseCommand): - def get_hooks(self): - return ["msg"] - - def get_help(self, command): - return "Get information about an AFC submission by name." - - def check(self, data): - if data.is_command and data.command in ["report", "afc_report"]: - return True - return False +class Command(BaseCommand): + """Get information about an AFC submission by name.""" + name = "report" def process(self, data): self.data = data diff --git a/bot/commands/afc_status.py b/bot/commands/afc_status.py index 1258eae..d476daf 100644 --- a/bot/commands/afc_status.py +++ b/bot/commands/afc_status.py @@ -1,24 +1,22 @@ # -*- coding: utf-8 -*- -"""Report the status of AFC submissions, either as an automatic message on join -or a request via !status.""" - import re from classes import BaseCommand import config import wiki -class AFCStatus(BaseCommand): - def get_hooks(self): - return ["join", "msg"] - - def get_help(self, command): - return "Get the number of pending AfC submissions, open redirect requests, and open file upload requests." +class Command(BaseCommand): + """Get the number of pending AfC submissions, open redirect requests, and + open file upload requests.""" + name = "status" + hooks = ["join", "msg"] def check(self, data): - if data.is_command and data.command in ["status", "count", "num", "number", "afc_status"]: + commands = ["status", "count", "num", "number"] + if data.is_command and data.command in commands: return True + try: if data.line[1] == "JOIN" and data.chan == "#wikipedia-en-afc": if data.nick != config.irc["frontend"]["nick"]: @@ -39,41 +37,48 @@ class AFCStatus(BaseCommand): action = data.args[0].lower() if action.startswith("sub") or action == "s": subs = self.count_submissions() - self.connection.reply(data, "there are currently %s pending AfC submissions." % subs) + msg = "there are currently {0} pending AfC submissions." + self.connection.reply(data, msg.format(subs)) elif action.startswith("redir") or action == "r": redirs = self.count_redirects() - self.connection.reply(data, "there are currently %s open redirect requests." % redirs) + msg = "there are currently {0} open redirect requests." + self.connection.reply(data, msg.format(redirs)) elif action.startswith("file") or action == "f": files = self.count_redirects() - self.connection.reply(data, "there are currently %s open file upload requests." % files) + msg = "there are currently {0} open file upload requests." + self.connection.reply(data, msg.format(files)) elif action.startswith("agg") or action == "a": try: agg_num = int(data.args[1]) except IndexError: - agg_data = (self.count_submissions(), self.count_redirects(), self.count_files()) + agg_data = (self.count_submissions(), + self.count_redirects(), self.count_files()) agg_num = self.get_aggregate_number(agg_data) except ValueError: - self.connection.reply(data, "\x0303%s\x0301 isn't a number!" % data.args[1]) + msg = "\x0303{0}\x0301 isn't a number!" + self.connection.reply(data, msg.format(data.args[1])) return aggregate = self.get_aggregate(agg_num) - self.connection.reply(data, "aggregate is currently %s (AfC %s)." % (agg_num, aggregate)) + msg = "aggregate is currently {0} (AfC {1})." + self.connection.reply(data, msg.format(agg_num, aggregate)) elif action.startswith("join") or action == "j": notice = self.get_join_notice() self.connection.reply(data, notice) else: - self.connection.reply(data, "unknown argument: \x0303%s\x0301. Valid args are 'subs', 'redirs', 'files', 'agg', and 'join'." % data.args[0]) + msg = "unknown argument: \x0303{0}\x0301. Valid args are 'subs', 'redirs', 'files', 'agg', and 'join'." + self.connection.reply(data, msg.format(data.args[0])) else: subs = self.count_submissions() redirs = self.count_redirects() files = self.count_files() - self.connection.reply(data, "there are currently %s pending submissions, %s open redirect requests, and %s open file upload requests." - % (subs, redirs, files)) + msg = "there are currently {0} pending submissions, {1} open redirect requests, and {2} open file upload requests." + self.connection.reply(data, msg.format(subs, redirs, files)) def get_join_notice(self): subs = self.count_submissions() @@ -81,20 +86,25 @@ class AFCStatus(BaseCommand): files = self.count_files() agg_num = self.get_aggregate_number((subs, redirs, files)) aggregate = self.get_aggregate(agg_num) - return ("\x02Current status:\x0F Articles for Creation %s (\x0302AFC\x0301: \x0305%s\x0301; \x0302AFC/R\x0301: \x0305%s\x0301; \x0302FFU\x0301: \x0305%s\x0301)" - % (aggregate, subs, redirs, files)) + + msg = "\x02Current status:\x0F Articles for Creation {0} (\x0302AFC\x0301: \x0305{1}\x0301; \x0302AFC/R\x0301: \x0305{2}\x0301; \x0302FFU\x0301: \x0305{3}\x0301)" + return msg.format(aggregate, subs, redirs, files) def count_submissions(self): """Returns the number of open AFC submissions (count of CAT:PEND).""" cat = self.site.get_category("Pending AfC submissions") - subs = cat.members(limit=500) - subs -= 2 # remove [[Wikipedia:Articles for creation/Redirects]] and [[Wikipedia:Files for upload]], which aren't real submissions + subs = len(cat.members(limit=500)) + + # Remove [[Wikipedia:Articles for creation/Redirects]] and + # [[Wikipedia:Files for upload]], which aren't real submissions: + subs -= 2 return subs def count_redirects(self): """Returns the number of open redirect submissions. Calculated as the total number of submissions minus the closed ones.""" - content = self.site.get_page("Wikipedia:Articles for creation/Redirects").get() + title = "Wikipedia:Articles for creation/Redirects" + content = self.site.get_page(title).get() total = len(re.findall("^\s*==(.*?)==\s*$", content, re.MULTILINE)) closed = content.lower().count("{{afc-c|b}}") redirs = total - closed diff --git a/bot/commands/calc.py b/bot/commands/calc.py index 2a138a0..0fc0fbd 100644 --- a/bot/commands/calc.py +++ b/bot/commands/calc.py @@ -1,23 +1,14 @@ # -*- coding: utf-8 -*- -# A somewhat advanced calculator: http://futureboy.us/fsp/frink.fsp. - import re import urllib from classes import BaseCommand -class Calc(BaseCommand): - def get_hooks(self): - return ["msg"] - - def get_help(self, command): - return "A somewhat advanced calculator: see http://futureboy.us/fsp/frink.fsp for details." - - def check(self, data): - if data.is_command and data.command == "calc": - return True - return False +class Command(BaseCommand): + """A somewhat advanced calculator: see http://futureboy.us/fsp/frink.fsp + for details.""" + name = "calc" def process(self, data): if not data.args: @@ -27,7 +18,8 @@ class Calc(BaseCommand): query = ' '.join(data.args) query = self.cleanup(query) - url = "http://futureboy.us/fsp/frink.fsp?fromVal=%s" % urllib.quote(query) + url = "http://futureboy.us/fsp/frink.fsp?fromVal={0}" + url = url.format(urllib.quote(query)) result = urllib.urlopen(url).read() r_result = re.compile(r'(?i)(.*?)') diff --git a/bot/commands/chanops.py b/bot/commands/chanops.py index fec8399..727ebbf 100644 --- a/bot/commands/chanops.py +++ b/bot/commands/chanops.py @@ -1,31 +1,30 @@ # -*- coding: utf-8 -*- -# Voice/devoice/op/deop users in the channel. - from classes import BaseCommand import config -class ChanOps(BaseCommand): - def get_hooks(self): - return ["msg"] - - def get_help(self, command): - action = command.capitalize() - return "%s users in the channel." % action +class Command(BaseCommand): + """Voice, devoice, op, or deop users in the channel.""" + name = "chanops" def check(self, data): - if data.is_command and data.command in ["voice", "devoice", "op", "deop"]: + commands = ["voice", "devoice", "op", "deop"] + if data.is_command and data.command in commands: return True return False def process(self, data): if data.host not in config.irc["permissions"]["admins"]: - self.connection.reply(data, "you must be a bot admin to use this command.") + msg = "you must be a bot admin to use this command." + self.connection.reply(data, msg) return - if not data.args: # if it is just !op/!devoice/whatever without arguments, assume they want to do this to themselves + # If it is just !op/!devoice/whatever without arguments, assume they + # want to do this to themselves: + if not data.args: target = data.nick else: target = data.args[0] - self.connection.say("ChanServ", "%s %s %s" % (data.command, data.chan, target)) + msg = " ".join((data.command, data.chan, target)) + self.connection.say("ChanServ", msg) diff --git a/bot/commands/crypt.py b/bot/commands/crypt.py index 8741342..8727c6d 100644 --- a/bot/commands/crypt.py +++ b/bot/commands/crypt.py @@ -1,31 +1,14 @@ # -*- coding: utf-8 -*- -""" -Cryptography functions (hashing and cyphers) for EarwigBot IRC. -""" - import hashlib from classes import BaseCommand import blowfish -class Cryptography(BaseCommand): - def get_hooks(self): - return ["msg"] - - def get_help(self, command): - if command == "hash": - return ("Return the hash of a string using a given algorithm, " + - "e.g. '!hash sha512 Hello world!'. Use '!hash list' for " + - "a list of supported algorithms.") - elif command == "encrypt": - return ("Encrypt any string with a given key using an " + - "implementation of Blowfish, e.g. '!encrypt some_key " + - "Hello!'.") - else: - return ("Decrypt any string with a given key using an " + - "implementation of Blowfish, e.g. '!decrypt some_key " + - "762cee8a5239548af18275d6c1184f16'.") +class Command(BaseCommand): + """Provides hash functions with !hash (!hash list for supported algorithms) + and blowfish encryption with !encrypt and !decrypt.""" + name = "cryptography" def check(self, data): if data.is_command and data.command in ["hash", "encrypt", "decrypt"]: @@ -34,31 +17,31 @@ class Cryptography(BaseCommand): def process(self, data): if not data.args: - self.connection.reply(data, "what do you want me to {0}?".format( - data.command)) + msg = "what do you want me to {0}?".format(data.command) + self.connection.reply(data, msg) return if data.command == "hash": algo = data.args[0] if algo == "list": algos = ', '.join(hashlib.algorithms) - self.connection.reply(data, "supported algorithms: " + algos + - ".") + msg = algos.join(("supported algorithms: ", ".")) + self.connection.reply(data, msg) elif algo in hashlib.algorithms: string = ' '.join(data.args[1:]) - result = eval("hashlib.{0}(string)".format(algo)).hexdigest() + result = getattr(hashlib, algo)(string).hexdigest() self.connection.reply(data, result) else: - self.connection.reply(data, "unknown algorithm: '{0}'.".format( - algo)) + msg = "unknown algorithm: '{0}'.".format(algo) + self.connection.reply(data, msg) else: key = data.args[0] text = ' '.join(data.args[1:]) if not text: - self.connection.reply(data, ("a key was provided, but text " + - "to {0} was not.").format(data.command)) + msg = "a key was provided, but text to {0} was not." + self.connection.reply(data, msg.format(data.command)) return try: @@ -67,5 +50,5 @@ class Cryptography(BaseCommand): else: self.connection.reply(data, blowfish.decrypt(key, text)) except blowfish.BlowfishError as error: - self.connection.reply(data, "{0}: {1}.".format( - error.__class__.__name__, error)) + msg = "{0}: {1}.".format(error.__class__.__name__, error) + self.connection.reply(data, msg) diff --git a/bot/commands/git.py b/bot/commands/git.py index d6b67c7..90d7bdc 100644 --- a/bot/commands/git.py +++ b/bot/commands/git.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -# Commands to interface with the bot's git repository; use '!git help' for sub-command list. - import shlex import subprocess import re @@ -9,26 +7,21 @@ import re from classes import BaseCommand import config -class Git(BaseCommand): - def get_hooks(self): - return ["msg"] - - def get_help(self, command): - return "Commands to interface with the bot's git repository; use '!git help' for sub-command list." - - def check(self, data): - if data.is_command and data.command == "git": - return True - return False +class Command(BaseCommand): + """Commands to interface with the bot's git repository; use '!git help' for + a sub-command list.""" + name = "git" def process(self, data): self.data = data if data.host not in config.irc["permissions"]["owners"]: - self.connection.reply(data, "you must be a bot owner to use this command.") + msg = "you must be a bot owner to use this command." + self.connection.reply(data, msg) return if not data.args: - self.connection.reply(data, "no arguments provided. Maybe you wanted '!git help'?") + msg = "no arguments provided. Maybe you wanted '!git help'?" + self.connection.reply(data, msg) return if data.args[0] == "help": @@ -52,19 +45,20 @@ class Git(BaseCommand): elif data.args[0] == "status": self.do_status() - else: # they asked us to do something we don't know - self.connection.reply(data, "unknown argument: \x0303%s\x0301." % data.args[0]) + else: # They asked us to do something we don't know + msg = "unknown argument: \x0303{0}\x0301.".format(data.args[0]) + self.connection.reply(data, msg) def exec_shell(self, command): - """execute a shell command and get the output""" + """Execute a shell command and get the output.""" command = shlex.split(command) result = subprocess.check_output(command, stderr=subprocess.STDOUT) if result: - result = result[:-1] # strip newline + result = result[:-1] # Strip newline return result def do_help(self): - """display all commands""" + """Display all commands.""" help_dict = { "branch": "get current branch", "branches": "get all branches", @@ -82,21 +76,24 @@ class Git(BaseCommand): self.connection.reply(self.data, "sub-commands are: %s." % help) def do_branch(self): - """get our current branch""" + """Get our current branch.""" branch = self.exec_shell("git name-rev --name-only HEAD") - self.connection.reply(self.data, "currently on branch \x0302%s\x0301." % branch) + msg = "currently on branch \x0302{0}\x0301.".format(branch) + self.connection.reply(self.data, msg) def do_branches(self): - """get list of branches""" + """Get a list of branches.""" branches = self.exec_shell("git branch") - branches = branches.replace('\n* ', ', ') # cleanup extraneous characters + # Remove extraneous characters: + branches = branches.replace('\n* ', ', ') branches = branches.replace('* ', ' ') branches = branches.replace('\n ', ', ') branches = branches.strip() - self.connection.reply(self.data, "branches: \x0302%s\x0301." % branches) + msg = "branches: \x0302{0}\x0301.".format(branches) + self.connection.reply(self.data, msg) def do_checkout(self): - """switch branches""" + """Switch branches.""" try: branch = self.data.args[1] except IndexError: # no branch name provided @@ -108,15 +105,20 @@ class Git(BaseCommand): try: result = self.exec_shell("git checkout %s" % branch) if "Already on" in result: - self.connection.reply(self.data, "already on \x0302%s\x0301!" % branch) + msg = "already on \x0302{0}\x0301!".format(branch) + self.connection.reply(self.data, msg) else: - self.connection.reply(self.data, "switched from branch \x0302%s\x0301 to \x0302%s\x0301." % (current_branch, branch)) + ms = "switched from branch \x0302{1}\x0301 to \x0302{1}\x0301." + msg = ms.format(current_branch, branch) + self.connection.reply(self.data, msg) - except subprocess.CalledProcessError: # git couldn't switch branches - self.connection.reply(self.data, "branch \x0302%s\x0301 doesn't exist!" % branch) + except subprocess.CalledProcessError: + # Git couldn't switch branches; assume the branch doesn't exist: + msg = "branch \x0302{0}\x0301 doesn't exist!".format(branch) + self.connection.reply(self.data, msg) def do_delete(self): - """delete a branch, while making sure that we are not on it""" + """Delete a branch, while making sure that we are not already on it.""" try: delete_branch = self.data.args[1] except IndexError: # no branch name provided @@ -126,38 +128,51 @@ class Git(BaseCommand): current_branch = self.exec_shell("git name-rev --name-only HEAD") if current_branch == delete_branch: - self.connection.reply(self.data, "you're currently on this branch; please checkout to a different branch before deleting.") + msg = "you're currently on this branch; please checkout to a different branch before deleting." + self.connection.reply(self.data, msg) return try: self.exec_shell("git branch -d %s" % delete_branch) - self.connection.reply(self.data, "branch \x0302%s\x0301 has been deleted locally." % delete_branch) - except subprocess.CalledProcessError: # git couldn't delete - self.connection.reply(self.data, "branch \x0302%s\x0301 doesn't exist!" % delete_branch) + msg = "branch \x0302{0}\x0301 has been deleted locally." + self.connection.reply(self.data, msg.format(delete_branch)) + except subprocess.CalledProcessError: + # Git couldn't switch branches; assume the branch doesn't exist: + msg = "branch \x0302{0}\x0301 doesn't exist!".format(delete_branch) + self.connection.reply(self.data, msg) def do_pull(self): - """pull from remote repository""" + """Pull from our remote repository.""" branch = self.exec_shell("git name-rev --name-only HEAD") - self.connection.reply(self.data, "pulling from remote (currently on \x0302%s\x0301)..." % branch) + msg = "pulling from remote (currently on \x0302{0}\x0301)..." + self.connection.reply(self.data, msg.format(branch)) result = self.exec_shell("git pull") if "Already up-to-date." in result: self.connection.reply(self.data, "done; no new changes.") else: - changes = re.findall("\s*((.*?)\sfile(.*?)tions?\(-\))", result)[0][0] # find the changes + regex = "\s*((.*?)\sfile(.*?)tions?\(-\))" + changes = re.findall(regex, result)[0][0] try: - remote = self.exec_shell("git config --get branch.%s.remote" % branch) - url = self.exec_shell("git config --get remote.%s.url" % remote) - 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 + cmnd_remt = "git config --get branch.{0}.remote".format(branch) + remote = self.exec_shell(cmnd_rmt) + cmnd_url = "git config --get remote.{0}.url".format(remote) + url = self.exec_shell(cmnd_url) + msg = "done; {0} [from {1}].".format(changes, url) + self.connection.reply(self.data, msg) + except subprocess.CalledProcessError: + # Something in .git/config is not specified correctly, so we + # cannot get the remote's URL. However, pull was a success: self.connection.reply(self.data, "done; %s." % changes) def do_status(self): - """check whether we have anything to pull""" - last = self.exec_shell("git log -n 1 --pretty=\"%ar\"") + """Check whether we have anything to pull.""" + last = self.exec_shell('git log -n 1 --pretty="%ar"') result = self.exec_shell("git fetch --dry-run") - if not result: # nothing was fetched, so remote and local are equal - self.connection.reply(self.data, "last commit was %s. Local copy is \x02up-to-date\x0F with remote." % last) + if not result: # Nothing was fetched, so remote and local are equal + msg = "last commit was {0}. Local copy is \x02up-to-date\x0F with remote." + self.connection.reply(self.data, msg.format(last)) else: - self.connection.reply(self.data, "last local commit was %s. Remote is \x02ahead\x0F of local copy." % last) + msg = "last local commit was {0}. Remote is \x02ahead\x0F of local copy." + self.connection.reply(self.data, msg.format(last)) diff --git a/bot/commands/help.py b/bot/commands/help.py index 6f57381..484dcb4 100644 --- a/bot/commands/help.py +++ b/bot/commands/help.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import re + from classes import BaseCommand, Data import commands @@ -17,7 +19,9 @@ class Command(BaseCommand): def do_main_help(self, data): """Give the user a general help message with a list of all commands.""" msg = "I am a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help '." - msg = msg.format(len(self.cmnds.keys()), ', '.join(self.cmnds)) + cmnds = self.cmnds.keys() + cmnds.sort() + msg = msg.format(len(cmnds), ', '.join(cmnds)) self.connection.reply(data, msg) def do_command_help(self, data): @@ -26,18 +30,20 @@ class Command(BaseCommand): # Create a dummy message to test which commands pick up the user's # input: - dummy = Data(1) + dummy = Data("PRIVMSG #fake-channel :Fake messsage!") dummy.command = command.lower() dummy.is_command = True for cmnd in self.cmnds.values(): - if cmnd.check(dummy): - doc = cmnd.__doc__ - if doc: - msg = "info for command \x0303{0}\x0301: \"{1}\"" - self.connection.reply(data, msg.format(command, doc)) - return - break + if not cmnd.check(dummy): + continue + if cmnd.__doc__: + doc = cmnd.__doc__.replace("\n", "") + doc = re.sub("\s\s+", " ", doc) + msg = "info for command \x0303{0}\x0301: \"{1}\"" + self.connection.reply(data, msg.format(command, doc)) + return + break msg = "sorry, no help for \x0303{0}\x0301.".format(command) self.connection.reply(data, msg) diff --git a/bot/commands/link.py b/bot/commands/link.py index 6c972af..749da14 100644 --- a/bot/commands/link.py +++ b/bot/commands/link.py @@ -1,17 +1,13 @@ # -*- coding: utf-8 -*- -# Convert a Wikipedia page name into a URL. - import re +from urllib import quote from classes import BaseCommand -class Link(BaseCommand): - def get_hooks(self): - return ["msg"] - - def get_help(self, command): - return "Convert a Wikipedia page name into a URL." +class Command(BaseCommand): + """Convert a Wikipedia page name into a URL.""" + name = "link" def check(self, data): if ((data.is_command and data.command == "link") or @@ -37,29 +33,31 @@ class Link(BaseCommand): self.connection.reply(data, link) def parse_line(self, line): - results = list() + results = [] - line = re.sub("\{\{\{(.*?)\}\}\}", "", line) # destroy {{{template parameters}}} + # Destroy {{{template parameters}}}: + line = re.sub("\{\{\{(.*?)\}\}\}", "", line) - links = re.findall("(\[\[(.*?)(\||\]\]))", line) # find all [[links]] + # Find all [[links]]: + links = re.findall("(\[\[(.*?)(\||\]\]))", line) if links: - links = map(lambda x: x[1], links) # re.findall() returns a list of tuples, but we only want the 2nd item in each tuple - results.extend(map(self.parse_link, links)) + # re.findall() returns a list of tuples, but we only want the 2nd + # item in each tuple: + links = [i[1] for i in links] + results = map(self.parse_link, links) - templates = re.findall("(\{\{(.*?)(\||\}\}))", line) # find all {{templates}} + # Find all {{templates}} + templates = re.findall("(\{\{(.*?)(\||\}\}))", line) if templates: - templates = map(lambda x: x[1], templates) + templates = [i[1] for i in templates] results.extend(map(self.parse_template, templates)) return results def parse_link(self, pagename): - pagename = pagename.strip() - link = "http://enwp.org/" + pagename - link = link.replace(" ", "_") - return link + link = quote(pagename.replace(" ", "_"), safe="/:") + return "".join(("http://enwp.org/", link)) def parse_template(self, pagename): - pagename = "Template:%s" % pagename # TODO: implement an actual namespace check - link = self.parse_link(pagename) - return link + pagename = "".join(("Template:", pagename)) + return self.parse_link(pagename) diff --git a/bot/commands/remind.py b/bot/commands/remind.py index 631f2a1..fd0234d 100644 --- a/bot/commands/remind.py +++ b/bot/commands/remind.py @@ -1,20 +1,13 @@ # -*- coding: utf-8 -*- -""" -Set a message to be repeated to you in a certain amount of time. -""" - import threading import time from classes import BaseCommand -class Remind(BaseCommand): - def get_hooks(self): - return ["msg"] - - def get_help(self, command): - return "Set a message to be repeated to you in a certain amount of time." +class Command(BaseCommand): + """Set a message to be repeated to you in a certain amount of time.""" + name = "remind" def check(self, data): if data.is_command and data.command in ["remind", "reminder"]: @@ -23,24 +16,32 @@ class Remind(BaseCommand): def process(self, data): if not data.args: - self.connection.reply(data, "please specify a time (in seconds) and a message in the following format: !remind