From 665ee391fb0b66f397defd65807f0801ce30081c Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 6 Jun 2011 21:09:14 -0400 Subject: [PATCH 01/19] deleted old config files and started from scratch with config/main.cfg and config/secure.cfg; the parser will be in core/config.py --- .gitignore | 2 +- config/irc.py | 25 --------------- config/{__init__.py => main.cfg} | 0 config/main.py | 24 -------------- config/schedule.py | 28 ---------------- config/secure.default.py | 9 ------ config/watcher.py | 69 ---------------------------------------- core/config.py | 0 8 files changed, 1 insertion(+), 156 deletions(-) delete mode 100644 config/irc.py rename config/{__init__.py => main.cfg} (100%) delete mode 100644 config/main.py delete mode 100644 config/schedule.py delete mode 100644 config/secure.default.py delete mode 100644 config/watcher.py create mode 100644 core/config.py diff --git a/.gitignore b/.gitignore index 282791f..58121c0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.pyc # Ignore secure config files: -config/secure.py +config/secure.cfg # Ignore pydev's nonsense: .project diff --git a/config/irc.py b/config/irc.py deleted file mode 100644 index ee9ef3e..0000000 --- a/config/irc.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- - -# EarwigBot Configuration File -# This file contains information that the bot uses to connect to IRC. - -# our main (front-end) server's hostname and port -HOST = "irc.freenode.net" -PORT = 6667 - -# our watcher server's hostname, port, and RC channel -WATCHER_HOST = "irc.wikimedia.org" -WATCHER_PORT = 6667 -WATCHER_CHAN = "#en.wikipedia" - -# our nick, ident, and real name, used on both servers -NICK = "EarwigBot" -IDENT = "earwigbot" -REALNAME = "[[w:en:User:EarwigBot]]" - -# channels to join on main server's startup -CHANS = ["##earwigbot", "##earwig", "#wikipedia-en-afc"] - -# hardcoded hostnames of users with certain permissions -OWNERS = ["wikipedia/The-Earwig"] # can use owner-only commands (!restart and !git) -ADMINS = ["wikipedia/The-Earwig", "wikipedia/LeonardBloom"] # can use high-risk commands, e.g. !op diff --git a/config/__init__.py b/config/main.cfg similarity index 100% rename from config/__init__.py rename to config/main.cfg diff --git a/config/main.py b/config/main.py deleted file mode 100644 index 6e8c082..0000000 --- a/config/main.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- - -# EarwigBot Configuration File -# This file tells the bot which of its components should be enabled. - -# The IRC frontend (configured in config/irc.py) sits on a public IRC network, -# responds to commands given to it, and reports edits (if the IRC watcher -# component is enabled). -enable_irc_frontend = True - -# The IRC watcher (connection details configured in config/irc.py as well) sits -# on an IRC network that gives a recent changes feed, usually irc.wikimedia.net. -# It looks for edits matching certain (often regex) patterns (rules configured -# in config/watcher.py), and either reports them to the IRC frontend (if -# enabled), or activates a task on the WikiBot (if configured to do). -enable_irc_watcher = True - -# EarwigBot doesn't have to edit a wiki, although this is its main purpose. If -# the wiki schedule is disabled, it will not be able to handle scheduled tasks -# that involve editing (such as creating a daily category every day at midnight -# UTC), but it can still edit through rules given in the watcher, and bot tasks -# can still be activated by the command line. The schedule is configured in -# config/schedule.py. -enable_wiki_schedule = True diff --git a/config/schedule.py b/config/schedule.py deleted file mode 100644 index 093050b..0000000 --- a/config/schedule.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- - -# EarwigBot Configuration File -# This file tells the bot when to run certain wiki-editing tasks. - -def check(minute, hour, month_day, month, week_day): - tasks = [] # tasks to run this turn, each as a tuple of (task_name, kwargs) or just task_name - - if minute == 0: # run every hour on the hour - tasks.append(("afc_statistics", {"action": "save"})) # save statistics to [[Template:AFC_statistics]] - - if hour == 0: # run every day at midnight - tasks.append("afc_dailycats") # create daily categories for WP:AFC - tasks.append("feed_dailycats") # create daily categories for WP:FEED - - if week_day == 0: # run every Sunday at midnight (that is, the start of Sunday, not the end) - tasks.append("afc_undated") # clear [[Category:Undated AfC submissions]] - - if week_day == 1: # run every Monday at midnight - tasks.append("afc_catdelink") # delink mainspace categories in declined AfC submissions - - if week_day == 2: # run every Tuesday at midnight - tasks.append("wrongmime") # tag files whose extensions do not agree with their MIME type - - if week_day == 3: # run every Wednesday at midnight - tasks.append("blptag") # add |blp=yes to {{WPB}} or {{WPBS}} when it is used along with {{WP Biography}} - - return tasks diff --git a/config/secure.default.py b/config/secure.default.py deleted file mode 100644 index 0db882e..0000000 --- a/config/secure.default.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- - -# EarwigBot Configuration File -# This file contains information that should be kept hidden, including passwords. - -# IRC: identify ourselves to NickServ? -NS_AUTH = False -NS_USER = "" -NS_PASS = "" diff --git a/config/watcher.py b/config/watcher.py deleted file mode 100644 index 6e2fe28..0000000 --- a/config/watcher.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- - -# EarwigBot Configuration File -# This file contains rules for the bot's watcher component. - -import re - -from wiki import task_manager - -# Define different report channels on our front-end server. They /must/ be in CHANS in config/irc.py or the bot will not be able to send messages to them (unless they have -n set). -AFC_CHANS = ["#wikipedia-en-afc"] # report recent AfC changes/give AfC status messages upon join -BOT_CHANS = ["##earwigbot", "#wikipedia-en-afc"] # report edits containing "!earwigbot" - -# Define some commonly used strings. -afc_prefix = "wikipedia( talk)?:(wikiproject )?articles for creation" - -# Define our compiled regexps used when finding certain edits. -r_page = re.compile(afc_prefix) -r_ffu = re.compile("wikipedia( talk)?:files for upload") -r_move1 = re.compile("moved \[\[{}".format(afc_prefix)) # an AFC page was either moved locally or out -r_move2 = re.compile("moved \[\[(.*?)\]\] to \[\[{}".format(afc_prefix)) # an outside page was moved into AFC -r_moved_pages = re.compile("^moved \[\[(.*?)\]\] to \[\[(.*?)\]\]") -r_delete = re.compile("deleted \"\[\[{}".format(afc_prefix)) -r_deleted_page = re.compile("^deleted \"\[\[(.*?)\]\]") -r_restore = re.compile("restored \"\[\[{}".format(afc_prefix)) -r_restored_page = re.compile("^restored \"\[\[(.*?)\]\]") -r_protect = re.compile("protected \"\[\[{}".format(afc_prefix)) - -def process(rc): - chans = set() # channels to report this message to - page_name = rc.page.lower() - comment = rc.comment.lower() - - if "!earwigbot" in rc.msg.lower(): - chans.update(BOT_CHANS) - - if r_page.search(page_name): - task_manager.start_task("afc_statistics", action="process_edit", page=rc.page) - task_manager.start_task("afc_copyvios", action="process_edit", page=rc.page) - chans.update(AFC_CHANS) - - elif r_ffu.match(page_name): - chans.update(AFC_CHANS) - - elif page_name.startswith("template:afc submission"): - chans.update(AFC_CHANS) - - elif rc.flags == "move" and (r_move1.match(comment) or r_move2.match(comment)): - p = r_moved_pages.findall(rc.comment)[0] - task_manager.start_task("afc_statistics", action="process_move", pages=p) - task_manager.start_task("afc_copyvios", action="process_move", pages=p) - chans.update(AFC_CHANS) - - elif rc.flags == "delete" and r_delete.match(comment): - p = r_deleted_page.findall(rc.comment)[0][0] - task_manager.start_task("afc_statistics", action="process_delete", page=p) - task_manager.start_task("afc_copyvios", action="process_delete", page=p) - chans.update(AFC_CHANS) - - elif rc.flags == "restore" and r_restore.match(comment): - p = r_restored_page.findall(rc.comment)[0][0] - task_manager.start_task("afc_statistics", action="process_restore", page=p) - task_manager.start_task("afc_copyvios", action="process_restore", page=p) - chans.update(AFC_CHANS) - - elif rc.flags == "protect" and r_protect.match(comment): - chans.update(AFC_CHANS) - - return chans diff --git a/core/config.py b/core/config.py new file mode 100644 index 0000000..e69de29 From e96189593d31aeaec7d7dd270c8fbca6422d0992 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Tue, 7 Jun 2011 11:34:24 -0400 Subject: [PATCH 02/19] removed config check from earwigbot.py launcher --- earwigbot.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/earwigbot.py b/earwigbot.py index f7852ef..ff93bcc 100644 --- a/earwigbot.py +++ b/earwigbot.py @@ -3,13 +3,6 @@ import time from subprocess import * -try: - from config import irc, main, schedule, secure, watcher -except ImportError: - print """Missing a config file! Make sure you have configured the bot. All *.py.default files in config/ -should have their .default extension removed, and the info inside should be corrected.""" - exit() - def main(): while 1: call(['python', 'core/main.py']) From e105a8dd785ec15fe06989dca667ee748973f444 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Tue, 7 Jun 2011 21:37:56 -0400 Subject: [PATCH 03/19] some stuff; nothing really substantial and probably doesn't work, but I wanted to get this out on paper --- core/config.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ core/main.py | 11 +++++++---- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/core/config.py b/core/config.py index e69de29..f72a4a4 100644 --- a/core/config.py +++ b/core/config.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +## EarwigBot's Config File Parser + +from collections import defaultdict +import ConfigParser as configparser +import os + +main_cfg_path = os.path.join("config", "main.cfg") +secure_cfg_path = os.path.join("config", "secure.cfg") + +config = dict() + +def load_config_file(filename): + parser = configparser.SafeConfigParser() + parser.optionxform = str # don't lowercase option names automatically + parser.read(filename) + return parser + +def make_new_config(): + print "You haven't configured the bot yet!" + choice = raw_input("Would you like to do this now? [y/n] ") + if choice.lower().startswith("y"): + pass + else: + exit() + +def dump_config_to_dict(parsers): + global config + for parser in parsers: + for section in parser.sections(): + for option in parser.options(section): + try: + config[section][option] = parser.get(section, option) + except KeyError: + config[section] = defaultdict(lambda: None) + config[section][option] = parser.get(section, option) + +def load(): + if not os.path.exists(main_cfg_path): + make_new_config() + + main_cfg = load_config_file(main_cfg_path) + secure_cfg = load_config_file(secure_cfg_path) + + dump_config_to_dict([main_cfg, secure_cfg]) diff --git a/core/main.py b/core/main.py index 3000def..a5d7f2f 100644 --- a/core/main.py +++ b/core/main.py @@ -28,7 +28,7 @@ import os parent_dir = os.path.split(sys.path[0])[0] sys.path.append(parent_dir) # make sure we look in the parent directory for modules -from config.main import * +from core import config from irc import frontend, watcher from wiki import task_manager @@ -96,10 +96,13 @@ def irc_frontend(): f_conn.close() def run(): - if enable_irc_frontend: # make the frontend run on our primary thread if enabled, and enable additional components through that function + config.load() + components = config.config["main"] + + if components["enable_irc_frontend"]: # make the frontend run on our primary thread if enabled, and enable additional components through that function irc_frontend() - elif enable_wiki_schedule: # the scheduler is enabled - run it on the main thread, but also run the IRC watcher on another thread if it is enabled + elif components["enable_wiki_schedule"]: # the scheduler is enabled - run it on the main thread, but also run the IRC watcher on another thread if it is enabled print "\nStarting wiki scheduler..." task_manager.load_tasks() if enable_irc_watcher: @@ -109,7 +112,7 @@ def run(): t_watcher.start() wiki_scheduler() - elif enable_irc_watcher: # the IRC watcher is our only enabled component, so run its function only and don't worry about anything else + elif components["enable_irc_watcher"]: # the IRC watcher is our only enabled component, so run its function only and don't worry about anything else irc_watcher() else: # nothing is enabled! From 13cfffb5c07697009313b62f9ae70aa005aec53f Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Tue, 14 Jun 2011 20:44:07 -0400 Subject: [PATCH 04/19] making entire config/ folder ignored by git; there's no reason to track this bot-specific information --- .gitignore | 4 ++-- config/main.cfg | 0 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 config/main.cfg diff --git a/.gitignore b/.gitignore index 58121c0..3a07a6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ # Ignore python bytecode: *.pyc -# Ignore secure config files: -config/secure.cfg +# Ignore bot-specific config files: +config/ # Ignore pydev's nonsense: .project diff --git a/config/main.cfg b/config/main.cfg deleted file mode 100644 index e69de29..0000000 From c8add512f4eaf20acfcf5ec818fc9b5cc8a2076b Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 16 Jun 2011 17:41:43 -0400 Subject: [PATCH 05/19] dropping config/ subdirectory in favor of a single config.xml file, located in the bot's root dir, with encrypted passwords instead of a secure.xml file --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3a07a6a..868c046 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ # Ignore python bytecode: *.pyc -# Ignore bot-specific config files: -config/ +# Ignore bot-specific config file: +config.xml # Ignore pydev's nonsense: .project From a568ec67773b9299be30912a8db850c2ddca6f5b Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 16 Jun 2011 17:45:16 -0400 Subject: [PATCH 06/19] lots of work on config.py, earwigbot.py, and main.py; TODO: actually parse config files; convert components to new config format; make_new_config() --- core/config.py | 122 +++++++++++++++++++++++++++++++++++++++----------------- core/main.py | 124 +++++++++++++++++++++++++++++++++------------------------ earwigbot.py | 49 ++++++++++++++++++++--- 3 files changed, 202 insertions(+), 93 deletions(-) diff --git a/core/config.py b/core/config.py index f72a4a4..f7c5785 100644 --- a/core/config.py +++ b/core/config.py @@ -1,46 +1,94 @@ # -*- coding: utf-8 -*- -## EarwigBot's Config File Parser +""" +EarwigBot's XML Config File Parser -from collections import defaultdict -import ConfigParser as configparser -import os +This handles all tasks involving reading and writing to our config file, +including encrypting and decrypting passwords and making a new config file from +scratch at the inital bot run. +""" -main_cfg_path = os.path.join("config", "main.cfg") -secure_cfg_path = os.path.join("config", "secure.cfg") +from os import makedirs, path +from xml.dom import minidom +from xml.parsers.expat import ExpatError -config = dict() +script_dir = path.dirname(path.abspath(__file__)) +root_dir = path.split(script_dir)[0] +config_path = path.join(root_dir, "config.xml") -def load_config_file(filename): - parser = configparser.SafeConfigParser() - parser.optionxform = str # don't lowercase option names automatically - parser.read(filename) - return parser +_config = None -def make_new_config(): - print "You haven't configured the bot yet!" - choice = raw_input("Would you like to do this now? [y/n] ") - if choice.lower().startswith("y"): - pass +class ConfigParseException(Exception): + """Base exception for when we could not parse the config file.""" + +class TypeMismatchException(ConfigParseException): + """A field does not fit to its expected type; e.g., an aribrary string + where we expected a boolean or integer.""" + +def _load_config(): + """Load data from our XML config file (config.xml) into a DOM object.""" + global _config + _config = minidom.parse(config_path) + +def verify_config(): + """Check to see if we have a valid config file, and if not, notify the + user. If there is no config file at all, offer to make one; otherwise, + exit.""" + if path.exists(config_path): + try: + _load_config() + except ExpatError as error: + print "Could not parse config file {0}:\n{1}".format(config_path, + error) + exit() + else: + return are_passwords_encrypted() else: - exit() - -def dump_config_to_dict(parsers): - global config - for parser in parsers: - for section in parser.sections(): - for option in parser.options(section): - try: - config[section][option] = parser.get(section, option) - except KeyError: - config[section] = defaultdict(lambda: None) - config[section][option] = parser.get(section, option) - -def load(): - if not os.path.exists(main_cfg_path): - make_new_config() - - main_cfg = load_config_file(main_cfg_path) - secure_cfg = load_config_file(secure_cfg_path) + print "You haven't configured the bot yet!" + choice = raw_input("Would you like to do this now? [y/n] ") + if choice.lower().startswith("y"): + return make_new_config() + else: + exit() + +def make_new_config(): + """Make a new XML config file based on the user's input.""" + makedirs(config_dir) - dump_config_to_dict([main_cfg, secure_cfg]) + encrypt = raw_input("Would you like to encrypt passwords stored in " + + "config.xml? [y/n] ") + if encrypt.lower().startswith("y"): + is_encrypted = True + else: + is_encrypted = False + + return is_encrypted + +def are_passwords_encrypted(): + """Determine if the passwords in our config file are encrypted, returning + either True or False.""" + data = _config.getElementsByTagName("config")[0] + element = data.getElementsByTagName("encrypt-passwords")[0] + return attribute_to_bool(element, "enabled") + +def attribute_to_bool(element, attribute): + """Return True if the value of element's attribute is 'true', '1', or 'on'; + return False if it is 'false', '0', or 'off' (regardless of + capitalization); raise TypeMismatchException if it does match any of + those.""" + value = element.getAttribute(attribute).lower() + if value in ["true", "1", "on"]: + return True + elif value in ["false", "0", "off"]: + return False + else: + e = ("Expected a bool in attribute '{0}' of element '{1}', but " + + "got '{2}'.").format(attribute, element.tagName, value) + raise TypeMismatchException(e) + +def parse_config(key): + """Parse config data from a DOM object. The key is used to unencrypt + passwords stored in the config file.""" + _load_config() # we might be re-loading unnecessarily here, but no harm in + # that! + data = _config.getElementsByTagName("config")[0] diff --git a/core/main.py b/core/main.py index a5d7f2f..b4e9729 100644 --- a/core/main.py +++ b/core/main.py @@ -1,23 +1,34 @@ +#! /usr/bin/python # -*- coding: utf-8 -*- -## EarwigBot's Core - -## EarwigBot has three components that can run independently of each other: an -## IRC front-end, an IRC watcher, and a wiki scheduler. -## * The IRC front-end runs on a normal IRC server and expects users to -## interact with it/give it commands. -## * The IRC watcher runs on a wiki recent-changes server and listens for -## edits. Users cannot interact with this part of the bot. -## * The wiki scheduler runs wiki-editing bot tasks in separate threads at -## user-defined times through a cron-like interface. - -## There is a "priority" system here: -## 1. If the IRC frontend is enabled, it will run on the main thread, and the -## IRC watcher and wiki scheduler (if enabled) will run on separate threads. -## 2. If the wiki scheduler is enabled, it will run on the main thread, and the -## IRC watcher (if enabled) will run on a separate thread. -## 3. If the IRC watcher is enabled, it will run on the main (and only) thread. -## Else, the bot will stop, as no components are enabled. +""" +EarwigBot's Core + +This (should) not be run directly; the wrapper in "earwigbot.py" is preferred, +but it should work fine alone, as long as you enter the password-unlock key at +the initial hidden prompt. + +The core is essentially responsible for starting the various bot components +(irc, scheduler, etc) and making sure they are all happy. An explanation of the +different components follows: + +EarwigBot has three components that can run independently of each other: an IRC +front-end, an IRC watcher, and a wiki scheduler. +* The IRC front-end runs on a normal IRC server and expects users to interact + with it/give it commands. +* The IRC watcher runs on a wiki recent-changes server and listens for edits. + Users cannot interact with this part of the bot. +* The wiki scheduler runs wiki-editing bot tasks in separate threads at + user-defined times through a cron-like interface. + +There is a "priority" system here: +1. If the IRC frontend is enabled, it will run on the main thread, and the IRC + watcher and wiki scheduler (if enabled) will run on separate threads. +2. If the wiki scheduler is enabled, it will run on the main thread, and the + IRC watcher (if enabled) will run on a separate thread. +3. If the IRC watcher is enabled, it will run on the main (and only) thread. +Else, the bot will stop, as no components are enabled. +""" import threading import time @@ -25,12 +36,14 @@ import traceback import sys import os -parent_dir = os.path.split(sys.path[0])[0] -sys.path.append(parent_dir) # make sure we look in the parent directory for modules +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 +#from irc import frontend, watcher +#from wiki import task_manager f_conn = None w_conn = None @@ -39,16 +52,15 @@ def irc_watcher(f_conn): """Function to handle the IRC watcher as another thread (if frontend and/or scheduler is enabled), otherwise run as the main thread.""" global w_conn - print "\nStarting IRC watcher..." - while 1: # restart the watcher component if (just) it breaks + while 1: # restart the watcher component if it breaks (and nothing else) w_conn = watcher.get_connection() w_conn.connect() - print # print a blank line here to signify that the bot has finished starting up + print # blank line to signify that the bot has finished starting up try: watcher.main(w_conn, f_conn) except: traceback.print_exc() - time.sleep(5) # sleep a bit before restarting watcher + time.sleep(5) # sleep a bit before restarting watcher print "\nWatcher has stopped; restarting component..." def wiki_scheduler(): @@ -57,24 +69,24 @@ def wiki_scheduler(): while 1: time_start = time.time() now = time.gmtime(time_start) - + task_manager.start_tasks(now) - + time_end = time.time() time_diff = time_start - time_end - if time_diff < 60: # sleep until the next minute + if time_diff < 60: # sleep until the next minute time.sleep(60 - time_diff) -def irc_frontend(): +def irc_frontend(components): """If the IRC frontend is enabled, make it run on our primary thread, and enable the wiki scheduler and IRC watcher on new threads if they are enabled.""" global f_conn - - print "\nStarting IRC frontend..." + + print "Starting IRC frontend..." f_conn = frontend.get_connection() frontend.startup(f_conn) - + if enable_wiki_schedule: print "\nStarting wiki scheduler..." task_manager.load_tasks() @@ -82,8 +94,9 @@ def irc_frontend(): t_scheduler.name = "wiki-scheduler" t_scheduler.daemon = True t_scheduler.start() - + if enable_irc_watcher: + print "\nStarting IRC watcher..." t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) t_watcher.name = "irc-watcher" t_watcher.daemon = True @@ -94,32 +107,41 @@ def irc_frontend(): if enable_irc_watcher: w_conn.close() f_conn.close() - + def run(): - config.load() - components = config.config["main"] - - if components["enable_irc_frontend"]: # make the frontend run on our primary thread if enabled, and enable additional components through that function - irc_frontend() - - elif components["enable_wiki_schedule"]: # the scheduler is enabled - run it on the main thread, but also run the IRC watcher on another thread if it is enabled - print "\nStarting wiki scheduler..." - task_manager.load_tasks() - if enable_irc_watcher: + try: + key = raw_input() # wait for our password unlock key from the bot's + except EOFError: # wrapper + key = None + config.parse_config(key) # load data from the config file and parse it + # using the unlock key + components = None + + if components["irc_frontend"]: # make the frontend run on our primary + irc_frontend(components) # thread if enabled, and enable additional + # components through that function + + elif components["wiki_schedule"]: # 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 + if enable_irc_watcher: # is enabled + print "\nStarting IRC watcher..." t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) t_watcher.name = "irc-watcher" t_watcher.daemon = True t_watcher.start() wiki_scheduler() - - elif components["enable_irc_watcher"]: # the IRC watcher is our only enabled component, so run its function only and don't worry about anything else - irc_watcher() - + + elif components["irc_watcher"]: # the IRC watcher is our only enabled + print "Starting IRC watcher..." # component, so run its function only + irc_watcher() # and don't worry about anything else + else: # nothing is enabled! - exit("\nNo bot parts are enabled; stopping...") + print "No bot parts are enabled; stopping..." if __name__ == "__main__": try: run() except KeyboardInterrupt: - exit("\nKeyboardInterrupt: stopping main bot loop.") + print "\nKeyboardInterrupt: stopping main bot loop." + exit(1) diff --git a/earwigbot.py b/earwigbot.py index ff93bcc..b18f1bc 100644 --- a/earwigbot.py +++ b/earwigbot.py @@ -1,15 +1,54 @@ +#! /usr/bin/python # -*- coding: utf-8 -*- -import time -from subprocess import * +""" +EarwigBot + +A thin wrapper for EarwigBot's main bot code, located in core/main.py. This +wrapper will automatically restart the bot when it shuts down (from !restart, +for example). It requests the bot's password at startup and reuses it every +time the bot restarts internally, so you do not need to re-enter the password +after using !restart. + +For information about the bot as a whole, see the attached README.md file (in +markdown format!) and the LICENSE for licensing information. +""" + +from getpass import getpass +from subprocess import Popen, PIPE +from sys import executable +from time import sleep + +from core.config import verify_config + +__author__ = "Ben Kurtovic" +__copyright__ = "Copyright (c) 2009-2011 by Ben Kurtovic" +__license__ = "MIT License" +__version__ = "0.1dev" +__email__ = "ben.kurtovic@verizon.net" def main(): + print "EarwigBot v{0}\n".format(__version__) + + is_encrypted = verify_config() + if is_encrypted: # passwords in the config file are encrypted + key = getpass("Enter key to unencrypt bot passwords: ") + else: + key = None + while 1: - call(['python', 'core/main.py']) - time.sleep(5) # sleep for five seconds between bot runs + bot = Popen([executable, 'core/main.py'], stdin=PIPE) + bot.communicate(key) # give the key to core.config.load() + return_code = bot.wait() + if return_code == 1: + exit() # let critical exceptions in the subprocess cause us to + # exit as well + else: + sleep(5) # sleep between bot runs following a non-critical + # subprocess exit if __name__ == "__main__": try: main() except KeyboardInterrupt: - exit("\nKeyboardInterrupt: stopping bot wrapper.") + print "\nKeyboardInterrupt: stopping bot wrapper." From 26e0058ca393b5ad6684be860e9b4c945253381a Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 16 Jun 2011 18:09:28 -0400 Subject: [PATCH 07/19] allow a default value for attribute_to_bool(); rework config.xml structure slightly --- core/config.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/config.py b/core/config.py index f7c5785..294d432 100644 --- a/core/config.py +++ b/core/config.py @@ -67,20 +67,21 @@ def make_new_config(): def are_passwords_encrypted(): """Determine if the passwords in our config file are encrypted, returning either True or False.""" - data = _config.getElementsByTagName("config")[0] - element = data.getElementsByTagName("encrypt-passwords")[0] - return attribute_to_bool(element, "enabled") + element = _config.getElementsByTagName("config")[0] + return attribute_to_bool(element, "encrypt-passwords", default=False) -def attribute_to_bool(element, attribute): +def attribute_to_bool(element, attribute, default=None): """Return True if the value of element's attribute is 'true', '1', or 'on'; return False if it is 'false', '0', or 'off' (regardless of - capitalization); raise TypeMismatchException if it does match any of - those.""" + capitalization); return default if it is empty; raise TypeMismatchException + if it does match any of those.""" value = element.getAttribute(attribute).lower() if value in ["true", "1", "on"]: return True elif value in ["false", "0", "off"]: return False + elif value == '': + return default else: e = ("Expected a bool in attribute '{0}' of element '{1}', but " + "got '{2}'.").format(attribute, element.tagName, value) From 7c8d3a33ab78974264669b10b9e868b76fcead8f Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 16 Jun 2011 21:53:42 -0400 Subject: [PATCH 08/19] more config-parsing backbone work; parsing the tag is functional; some other cleanup/changes --- core/config.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- core/main.py | 5 ++-- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/core/config.py b/core/config.py index 294d432..8a2831e 100644 --- a/core/config.py +++ b/core/config.py @@ -6,8 +6,12 @@ EarwigBot's XML Config File Parser This handles all tasks involving reading and writing to our config file, including encrypting and decrypting passwords and making a new config file from scratch at the inital bot run. + +Usually you'll just want to do "from core.config import config" and access +config data from within that object. """ +from collections import defaultdict from os import makedirs, path from xml.dom import minidom from xml.parsers.expat import ExpatError @@ -16,15 +20,25 @@ script_dir = path.dirname(path.abspath(__file__)) root_dir = path.split(script_dir)[0] config_path = path.join(root_dir, "config.xml") -_config = None +_config = None # holds the parsed DOM object for our config file +config = None # holds an instance of Container() with our config data class ConfigParseException(Exception): """Base exception for when we could not parse the config file.""" class TypeMismatchException(ConfigParseException): - """A field does not fit to its expected type; e.g., an aribrary string + """A field does not fit to its expected type; e.g., an arbitrary string where we expected a boolean or integer.""" +class MissingElementException(ConfigParseException): + """An element in the config file is missing a required sub-element.""" + +class MissingAttributeException(ConfigParseException): + """An element is missing a required attribute to be parsed correctly.""" + +class Container(object): + """A class to hold information in a nice, accessable manner.""" + def _load_config(): """Load data from our XML config file (config.xml) into a DOM object.""" global _config @@ -42,6 +56,9 @@ def verify_config(): error) exit() else: + if not _config.getElementsByTagName("config"): + e = "Config file is missing a tag." + raise MissingElementException(e) return are_passwords_encrypted() else: print "You haven't configured the bot yet!" @@ -88,8 +105,55 @@ def attribute_to_bool(element, attribute, default=None): raise TypeMismatchException(e) def parse_config(key): - """Parse config data from a DOM object. The key is used to unencrypt - passwords stored in the config file.""" + """Parse config data from a DOM object into the 'config' global variable. + The key is used to unencrypt passwords stored in the XML config file.""" _load_config() # we might be re-loading unnecessarily here, but no harm in # that! data = _config.getElementsByTagName("config")[0] + + cfg = Container() + cfg.components = parse_components(data) + cfg.wiki = parse_wiki(data, key) + cfg.irc = parse_irc(data, key) + cfg.schedule = parse_schedule(data) + cfg.watcher = parse_watcher(data) + + global config + config = cfg + +def parse_components(data): + """Parse everything within the XML tag of our config file. + The components object here will exist as config.components, and is a dict + of our enabled components: components[name] = True if it is enabled, False + if it is disabled.""" + components = defaultdict(lambda: False) # all components are disabled by + # default + element = data.getElementsByTagName("components") + if not element: + e = " is missing a required tag." + raise MissingElementException(e) + element = element[0] # select the first tag out of our list + # of tags, even though we should only have one + + component_tags = element.getElementsByTagName("component") + for component in component_tags: + name = component.getAttribute("name") + if not name: + e = "A tag is missing the required attribute 'name'." + raise MissingAttributeException(e) + is_enabled = attribute_to_bool(component, "enabled", False) + components[name] = is_enabled + + return components + +def parse_wiki(data, key): + pass + +def parse_irc(data, key): + pass + +def parse_schedule(data): + pass + +def parse_watcher(data): + pass diff --git a/core/main.py b/core/main.py index b4e9729..e55d3b9 100644 --- a/core/main.py +++ b/core/main.py @@ -48,7 +48,7 @@ from core import config f_conn = None w_conn = None -def irc_watcher(f_conn): +def irc_watcher(f_conn=None): """Function to handle the IRC watcher as another thread (if frontend and/or scheduler is enabled), otherwise run as the main thread.""" global w_conn @@ -115,7 +115,7 @@ def run(): key = None config.parse_config(key) # load data from the config file and parse it # using the unlock key - components = None + components = config.config.components if components["irc_frontend"]: # make the frontend run on our primary irc_frontend(components) # thread if enabled, and enable additional @@ -138,6 +138,7 @@ def run(): else: # nothing is enabled! print "No bot parts are enabled; stopping..." + exit(1) if __name__ == "__main__": try: From 6869900b65ce89ce6cc7412a69d4e3b34d69da81 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 16 Jun 2011 23:56:39 -0400 Subject: [PATCH 09/19] core/config.py can now parse the tag fully; added some new functions that I pulled out of parse_components(); added stubs for encrypting/decrypting passwords; nitpick in core/main.py --- core/config.py | 124 +++++++++++++++++++++++++++++++++++++++++++++++++-------- core/main.py | 2 +- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/core/config.py b/core/config.py index 8a2831e..87cef91 100644 --- a/core/config.py +++ b/core/config.py @@ -81,6 +81,18 @@ def make_new_config(): return is_encrypted +def encrypt_password(password, key): + """If passwords are supposed to be encrypted, use this function to do that + using a user-provided key.""" + # TODO: stub + return password + +def decrypt_password(password, key): + """If passwords are encrypted, use this function to decrypt them using a + user-provided key.""" + # TODO: stub + return password + def are_passwords_encrypted(): """Determine if the passwords in our config file are encrypted, returning either True or False.""" @@ -104,6 +116,34 @@ def attribute_to_bool(element, attribute, default=None): "got '{2}'.").format(attribute, element.tagName, value) raise TypeMismatchException(e) +def get_first_element(parent, tag_name): + """Return the first child of the parent element with the given tag name, or + return None.""" + try: + return parent.getElementsByTagName(tag_name)[0] + except IndexError: + return None + +def get_required_element(parent, tag_name): + """Return the first child of the parent element with the given tag name, or + raise MissingElementException() if no child of that name exists.""" + element = get_first_element(parent, tag_name) + if not element: + e = "<{0}> is missing a required <{1}> tag.".format(parent.tagName, + tag_name) + raise MissingElementException(e) + return element + +def get_required_attribute(element, attr_name): + """Return the value of the attribute 'attr_name' in 'element'. If + undefined, raise MissingAttributeException().""" + attribute = element.getAttribute(attr_name) + if not attribute: + e = "A <{0}> tag is missing the required attribute '{1}'.".format( + element.tagName, attr_name) + raise MissingAttributeException(e) + return attribute + def parse_config(key): """Parse config data from a DOM object into the 'config' global variable. The key is used to unencrypt passwords stored in the XML config file.""" @@ -128,32 +168,84 @@ def parse_components(data): if it is disabled.""" components = defaultdict(lambda: False) # all components are disabled by # default - element = data.getElementsByTagName("components") - if not element: - e = " is missing a required tag." - raise MissingElementException(e) - element = element[0] # select the first tag out of our list - # of tags, even though we should only have one - - component_tags = element.getElementsByTagName("component") - for component in component_tags: - name = component.getAttribute("name") - if not name: - e = "A tag is missing the required attribute 'name'." - raise MissingAttributeException(e) - is_enabled = attribute_to_bool(component, "enabled", False) - components[name] = is_enabled + element = get_required_element(data, "components") + + for component in element.getElementsByTagName("component"): + name = get_required_attribute(component, "name") + components[name] = True return components def parse_wiki(data, key): + """Parse everything within the tag of our XML config file.""" pass + +def parse_irc_server(data, key): + """Parse everything within a tag.""" + server = Container() + connection = get_required_element(data, "connection") + server.host = get_required_attribute(connection, "host") + server.port = get_required_attribute(connection, "port") + server.nick = get_required_attribute(connection, "nick") + server.ident = get_required_attribute(connection, "ident") + server.realname = get_required_attribute(connection, "realname") + + nickserv = get_first_element(data, "nickserv") + if nickserv: + server.nickserv = Container() + server.nickserv.username = get_required_attribute(nickserv, "username") + password = get_required_attribute(nickserv, "password") + if are_passwords_encrypted(): + server.nickserv.password = decrypt_password(password, key) + else: + server.nickserv.password = password + + channels = get_first_element(data, "channels") + if channels: + server.channels = list() + for channel in channels.getElementsByTagName("channel"): + name = get_required_attribute(channel, "name") + server.channels.append(name) + + return server + def parse_irc(data, key): - pass + """Parse everything within the tag of our XML config file.""" + irc = Container() + + element = get_first_element(data, "irc") + if not element: + return irc + + servers = get_first_element(element, "servers") + if servers: + for server in servers.getElementsByTagName("server"): + server_name = get_required_attribute(server, "name") + if server_name == "frontend": + irc.frontend = parse_irc_server(server, key) + elif server_name == "watcher": + irc.watcher = parse_irc_server(server, key) + else: + print ("Warning: config file specifies a with " + + "unknown name '{0}'. Ignoring.").format(server_name) + + permissions = get_first_element(element, "permissions") + if permissions: + irc.permissions = dict() + for group in permissions.getElementsByTagName("group"): + group_name = get_required_attribute(group, "name") + irc.permissions[group_name] = list() + for user in group.getElementsByTagName("user"): + hostname = get_required_attribute(user, "host") + irc.permissions[group_name].append(hostname) + + return irc def parse_schedule(data): + """Parse everything within the tag of our XML config file.""" pass def parse_watcher(data): + """Parse everything within the tag of our XML config file.""" pass diff --git a/core/main.py b/core/main.py index e55d3b9..9f0cc7f 100644 --- a/core/main.py +++ b/core/main.py @@ -136,7 +136,7 @@ def run(): print "Starting IRC watcher..." # component, so run its function only irc_watcher() # and don't worry about anything else - else: # nothing is enabled! + else: # nothing is enabled! print "No bot parts are enabled; stopping..." exit(1) From 94848ab0bc2da42535b78b3991a4b6aa87ddcf99 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Fri, 17 Jun 2011 00:10:02 -0400 Subject: [PATCH 10/19] wrap parse_config() to catch ConfigParseExceptions and report them to the user cleanly; some minor doc changes. --- core/config.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/core/config.py b/core/config.py index 87cef91..2169481 100644 --- a/core/config.py +++ b/core/config.py @@ -118,7 +118,7 @@ def attribute_to_bool(element, attribute, default=None): def get_first_element(parent, tag_name): """Return the first child of the parent element with the given tag name, or - return None.""" + return None if no child of that name exists.""" try: return parent.getElementsByTagName(tag_name)[0] except IndexError: @@ -129,8 +129,8 @@ def get_required_element(parent, tag_name): raise MissingElementException() if no child of that name exists.""" element = get_first_element(parent, tag_name) if not element: - e = "<{0}> is missing a required <{1}> tag.".format(parent.tagName, - tag_name) + e = "A <{0}> tag is missing a required <{1}> child tag.".format( + parent.tagName, tag_name) raise MissingElementException(e) return element @@ -145,6 +145,16 @@ def get_required_attribute(element, attr_name): return attribute def parse_config(key): + """A thin wrapper for the actual config parser in _parse_config(): catch + parsing exceptions and report them to the user cleanly.""" + try: + _parse_config(key) + except ConfigParseException as e: + print "\nError parsing config file:" + print e + exit(1) + +def _parse_config(key): """Parse config data from a DOM object into the 'config' global variable. The key is used to unencrypt passwords stored in the XML config file.""" _load_config() # we might be re-loading unnecessarily here, but no harm in From db0b125897d752f11b632e73fd66dd541fa39b98 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sat, 18 Jun 2011 18:55:27 -0400 Subject: [PATCH 11/19] specify core/main.py as an absolute path, so we don't have to run 'python earwigbot.py' from within earwigbot/, but we can also do 'python /path/to/earwigbot.py' --- earwigbot.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/earwigbot.py b/earwigbot.py index b18f1bc..5543221 100644 --- a/earwigbot.py +++ b/earwigbot.py @@ -4,7 +4,7 @@ """ EarwigBot -A thin wrapper for EarwigBot's main bot code, located in core/main.py. This +A thin wrapper for EarwigBot's main bot code, specified by bot_script. This wrapper will automatically restart the bot when it shuts down (from !restart, for example). It requests the bot's password at startup and reuses it every time the bot restarts internally, so you do not need to re-enter the password @@ -16,6 +16,7 @@ markdown format!) and the LICENSE for licensing information. from getpass import getpass from subprocess import Popen, PIPE +from os import path from sys import executable from time import sleep @@ -27,6 +28,8 @@ __license__ = "MIT License" __version__ = "0.1dev" __email__ = "ben.kurtovic@verizon.net" +bot_script = path.join(path.dirname(path.abspath(__file__)), "core", "main.py") + def main(): print "EarwigBot v{0}\n".format(__version__) @@ -37,7 +40,7 @@ def main(): key = None while 1: - bot = Popen([executable, 'core/main.py'], stdin=PIPE) + bot = Popen([executable, bot_script], stdin=PIPE) bot.communicate(key) # give the key to core.config.load() return_code = bot.wait() if return_code == 1: From 8e5af3dff846e6a02b2c64ca5d9efdc8be63dd81 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 19 Jun 2011 00:53:28 -0400 Subject: [PATCH 12/19] import lib.blowfish in core.config and decrypt passwords correctly; Exception -> Error in custom config exception names --- core/config.py | 48 +++++++++++++++++++++--------------------------- earwigbot.py | 2 +- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/core/config.py b/core/config.py index 2169481..df7c25f 100644 --- a/core/config.py +++ b/core/config.py @@ -16,6 +16,8 @@ from os import makedirs, path from xml.dom import minidom from xml.parsers.expat import ExpatError +from lib import blowfish + script_dir = path.dirname(path.abspath(__file__)) root_dir = path.split(script_dir)[0] config_path = path.join(root_dir, "config.xml") @@ -23,17 +25,17 @@ config_path = path.join(root_dir, "config.xml") _config = None # holds the parsed DOM object for our config file config = None # holds an instance of Container() with our config data -class ConfigParseException(Exception): +class ConfigParseError(Exception): """Base exception for when we could not parse the config file.""" -class TypeMismatchException(ConfigParseException): +class TypeMismatchError(ConfigParseError): """A field does not fit to its expected type; e.g., an arbitrary string where we expected a boolean or integer.""" -class MissingElementException(ConfigParseException): +class MissingElementError(ConfigParseError): """An element in the config file is missing a required sub-element.""" -class MissingAttributeException(ConfigParseException): +class MissingAttributeError(ConfigParseError): """An element is missing a required attribute to be parsed correctly.""" class Container(object): @@ -58,7 +60,7 @@ def verify_config(): else: if not _config.getElementsByTagName("config"): e = "Config file is missing a tag." - raise MissingElementException(e) + raise MissingElementError(e) return are_passwords_encrypted() else: print "You haven't configured the bot yet!" @@ -81,18 +83,6 @@ def make_new_config(): return is_encrypted -def encrypt_password(password, key): - """If passwords are supposed to be encrypted, use this function to do that - using a user-provided key.""" - # TODO: stub - return password - -def decrypt_password(password, key): - """If passwords are encrypted, use this function to decrypt them using a - user-provided key.""" - # TODO: stub - return password - def are_passwords_encrypted(): """Determine if the passwords in our config file are encrypted, returning either True or False.""" @@ -102,8 +92,8 @@ def are_passwords_encrypted(): def attribute_to_bool(element, attribute, default=None): """Return True if the value of element's attribute is 'true', '1', or 'on'; return False if it is 'false', '0', or 'off' (regardless of - capitalization); return default if it is empty; raise TypeMismatchException - if it does match any of those.""" + capitalization); return default if it is empty; raise TypeMismatchError if + it does match any of those.""" value = element.getAttribute(attribute).lower() if value in ["true", "1", "on"]: return True @@ -114,7 +104,7 @@ def attribute_to_bool(element, attribute, default=None): else: e = ("Expected a bool in attribute '{0}' of element '{1}', but " + "got '{2}'.").format(attribute, element.tagName, value) - raise TypeMismatchException(e) + raise TypeMismatchError(e) def get_first_element(parent, tag_name): """Return the first child of the parent element with the given tag name, or @@ -126,22 +116,22 @@ def get_first_element(parent, tag_name): def get_required_element(parent, tag_name): """Return the first child of the parent element with the given tag name, or - raise MissingElementException() if no child of that name exists.""" + raise MissingElementError() if no child of that name exists.""" element = get_first_element(parent, tag_name) if not element: e = "A <{0}> tag is missing a required <{1}> child tag.".format( parent.tagName, tag_name) - raise MissingElementException(e) + raise MissingElementError(e) return element def get_required_attribute(element, attr_name): """Return the value of the attribute 'attr_name' in 'element'. If - undefined, raise MissingAttributeException().""" + undefined, raise MissingAttributeError().""" attribute = element.getAttribute(attr_name) if not attribute: e = "A <{0}> tag is missing the required attribute '{1}'.".format( element.tagName, attr_name) - raise MissingAttributeException(e) + raise MissingAttributeError(e) return attribute def parse_config(key): @@ -149,9 +139,13 @@ def parse_config(key): parsing exceptions and report them to the user cleanly.""" try: _parse_config(key) - except ConfigParseException as e: + except ConfigParseError as error: print "\nError parsing config file:" - print e + print error + exit(1) + except blowfish.BlowfishError as error: + print "\nError decrypting passwords:" + print error exit(1) def _parse_config(key): @@ -207,7 +201,7 @@ def parse_irc_server(data, key): server.nickserv.username = get_required_attribute(nickserv, "username") password = get_required_attribute(nickserv, "password") if are_passwords_encrypted(): - server.nickserv.password = decrypt_password(password, key) + server.nickserv.password = blowfish.decrypt(key, password) else: server.nickserv.password = password diff --git a/earwigbot.py b/earwigbot.py index 5543221..3542076 100644 --- a/earwigbot.py +++ b/earwigbot.py @@ -41,7 +41,7 @@ def main(): while 1: bot = Popen([executable, bot_script], stdin=PIPE) - bot.communicate(key) # give the key to core.config.load() + bot.communicate(key) # give the key to core.config.load_config() return_code = bot.wait() if return_code == 1: exit() # let critical exceptions in the subprocess cause us to From 814c9efdce7c9230f4bd97a72f4fa6c314b81d60 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 19 Jun 2011 01:52:21 -0400 Subject: [PATCH 13/19] nicer error message that actually tells us what's going on, thanks to the changes in lib.blowfish upstream (on develop) --- core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.py b/core/config.py index df7c25f..3607502 100644 --- a/core/config.py +++ b/core/config.py @@ -145,7 +145,7 @@ def parse_config(key): exit(1) except blowfish.BlowfishError as error: print "\nError decrypting passwords:" - print error + print "{0}: {1}.".format(error.__class__.__name__, error) exit(1) def _parse_config(key): From 430ba061af892dc6da52a03ea57594b0683c6332 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 19 Jun 2011 23:22:01 -0400 Subject: [PATCH 14/19] store config differently (in five separate global variables instead of one) to make accessing it cleaner; convert core/main.py to new config system as well as irc/frontend.py -- both seem to be working --- core/config.py | 95 ++++++++++++++++++++++++++++++++++++--------------------- core/main.py | 12 ++++---- irc/frontend.py | 71 ++++++++++++++++++++++++++++-------------- 3 files changed, 114 insertions(+), 64 deletions(-) diff --git a/core/config.py b/core/config.py index 3607502..486367a 100644 --- a/core/config.py +++ b/core/config.py @@ -7,8 +7,14 @@ This handles all tasks involving reading and writing to our config file, including encrypting and decrypting passwords and making a new config file from scratch at the inital bot run. -Usually you'll just want to do "from core.config import config" and access -config data from within that object. +Usually you'll just want to do "from core import config" and access config data +from within config's five global variables: + +* config.components +* config.wiki +* config.irc +* config.schedule +* config.watcher """ from collections import defaultdict @@ -23,7 +29,9 @@ root_dir = path.split(script_dir)[0] config_path = path.join(root_dir, "config.xml") _config = None # holds the parsed DOM object for our config file -config = None # holds an instance of Container() with our config data + +# initialize our five global variables to store config data +components, wiki, irc, schedule, watcher = (None, None, None, None, None) class ConfigParseError(Exception): """Base exception for when we could not parse the config file.""" @@ -84,27 +92,14 @@ def make_new_config(): return is_encrypted def are_passwords_encrypted(): - """Determine if the passwords in our config file are encrypted, returning - either True or False.""" + """Determine if the passwords in our config file are encrypted; return + either True or False, or raise an exception if there was a problem reading + the config file.""" element = _config.getElementsByTagName("config")[0] - return attribute_to_bool(element, "encrypt-passwords", default=False) - -def attribute_to_bool(element, attribute, default=None): - """Return True if the value of element's attribute is 'true', '1', or 'on'; - return False if it is 'false', '0', or 'off' (regardless of - capitalization); return default if it is empty; raise TypeMismatchError if - it does match any of those.""" - value = element.getAttribute(attribute).lower() - if value in ["true", "1", "on"]: - return True - elif value in ["false", "0", "off"]: + attribute = element.getAttribute("encrypt-passwords") + if not attribute: return False - elif value == '': - return default - else: - e = ("Expected a bool in attribute '{0}' of element '{1}', but " + - "got '{2}'.").format(attribute, element.tagName, value) - raise TypeMismatchError(e) + return attribute_to_bool(attribute, element, "encrypt-passwords") def get_first_element(parent, tag_name): """Return the first child of the parent element with the given tag name, or @@ -134,6 +129,32 @@ def get_required_attribute(element, attr_name): raise MissingAttributeError(e) return attribute +def attribute_to_bool(value, element, attr_name): + """Return True if 'value' is 'true', '1', or 'on', return False if it is + 'false', '0', or 'off' (regardless of capitalization), or raise + TypeMismatchError() if it does match any of those. 'element' and + 'attr_name' are only used to generate the error message.""" + lcase = value.lower() + if lcase in ["true", "1", "on"]: + return True + elif lcase in ["false", "0", "off"]: + return False + else: + e = ("Expected a bool in attribute '{0}' of tag '{1}', but got '{2}'." + ).format(attr_name, element.tagName, value) + raise TypeMismatchError(e) + +def attribute_to_int(value, element, attr_name): + """Return 'value' after it is converted to an integer. If it could not be + converted, raise TypeMismatchError() using 'element' and 'attr_name' only + to give the user information about what happened.""" + try: + return int(value) + except ValueError: + e = ("Expected an integer in attribute '{0}' of tag '{1}', but got " + + "'{2}'.").format(attr_name, element.tagName, value) + raise TypeMismatchError(e) + def parse_config(key): """A thin wrapper for the actual config parser in _parse_config(): catch parsing exceptions and report them to the user cleanly.""" @@ -149,21 +170,20 @@ def parse_config(key): exit(1) def _parse_config(key): - """Parse config data from a DOM object into the 'config' global variable. - The key is used to unencrypt passwords stored in the XML config file.""" + """Parse config data from a DOM object into the five global variables that + store our config info. The key is used to unencrypt passwords stored in the + XML config file.""" + global components, wiki, irc, schedule, watcher + _load_config() # we might be re-loading unnecessarily here, but no harm in # that! data = _config.getElementsByTagName("config")[0] - cfg = Container() - cfg.components = parse_components(data) - cfg.wiki = parse_wiki(data, key) - cfg.irc = parse_irc(data, key) - cfg.schedule = parse_schedule(data) - cfg.watcher = parse_watcher(data) - - global config - config = cfg + components = parse_components(data) + wiki = parse_wiki(data, key) + irc = parse_irc(data, key) + schedule = parse_schedule(data) + watcher = parse_watcher(data) def parse_components(data): """Parse everything within the XML tag of our config file. @@ -187,14 +207,17 @@ def parse_wiki(data, key): def parse_irc_server(data, key): """Parse everything within a tag.""" server = Container() - connection = get_required_element(data, "connection") + server.host = get_required_attribute(connection, "host") server.port = get_required_attribute(connection, "port") server.nick = get_required_attribute(connection, "nick") server.ident = get_required_attribute(connection, "ident") server.realname = get_required_attribute(connection, "realname") + # convert the port from a string to an int + server.port = attribute_to_int(server.port, connection, "port") + nickserv = get_first_element(data, "nickserv") if nickserv: server.nickserv = Container() @@ -204,10 +227,12 @@ def parse_irc_server(data, key): server.nickserv.password = blowfish.decrypt(key, password) else: server.nickserv.password = password + else: + server.nickserv = None + server.channels = list() channels = get_first_element(data, "channels") if channels: - server.channels = list() for channel in channels.getElementsByTagName("channel"): name = get_required_attribute(channel, "name") server.channels.append(name) diff --git a/core/main.py b/core/main.py index 9f0cc7f..31d6149 100644 --- a/core/main.py +++ b/core/main.py @@ -42,7 +42,7 @@ root_dir = os.path.split(script_dir)[0] # the bot's "root" directory relative 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 irc import frontend#, watcher #from wiki import task_manager f_conn = None @@ -87,7 +87,7 @@ def irc_frontend(components): f_conn = frontend.get_connection() frontend.startup(f_conn) - if enable_wiki_schedule: + if config.components["wiki_schedule"]: print "\nStarting wiki scheduler..." task_manager.load_tasks() t_scheduler = threading.Thread(target=wiki_scheduler) @@ -95,7 +95,7 @@ def irc_frontend(components): t_scheduler.daemon = True t_scheduler.start() - if enable_irc_watcher: + if config.components["irc_watcher"]: print "\nStarting IRC watcher..." t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) t_watcher.name = "irc-watcher" @@ -104,7 +104,7 @@ def irc_frontend(components): frontend.main() - if enable_irc_watcher: + if config.components["irc_watcher"]: w_conn.close() f_conn.close() @@ -115,7 +115,7 @@ def run(): key = None config.parse_config(key) # load data from the config file and parse it # using the unlock key - components = config.config.components + components = config.components if components["irc_frontend"]: # make the frontend run on our primary irc_frontend(components) # thread if enabled, and enable additional @@ -124,7 +124,7 @@ def run(): elif components["wiki_schedule"]: # 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 - if enable_irc_watcher: # is enabled + if components["irc_watcher"]: # is enabled print "\nStarting IRC watcher..." t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) t_watcher.name = "irc-watcher" diff --git a/irc/frontend.py b/irc/frontend.py index 293bc0a..d8d4991 100644 --- a/irc/frontend.py +++ b/irc/frontend.py @@ -1,11 +1,17 @@ # -*- coding: utf-8 -*- -## Imports -import re, time +""" +EarwigBot's Front-end IRC Component -from config.irc import * -from config.secure import * +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 +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 + +from core import config from irc import command_handler from irc.connection import * from irc.data import Data @@ -13,16 +19,24 @@ from irc.data import Data connection = None def get_connection(): - connection = Connection(HOST, PORT, NICK, IDENT, REALNAME) + """Return a new Connection() instance with information about our server + connection, but don't actually connect yet.""" + cf = config.irc.frontend + connection = Connection(cf.host, cf.port, cf.nick, cf.nick, cf.realname) return connection def startup(conn): + """Accept a single arg, a Connection() object, and set our global variable + 'connection' to it. Load all command classes in irc/commands with + command_handler, and then establish a connection with the IRC server.""" global connection connection = conn command_handler.load_commands(connection) connection.connect() def main(): + """Main loop for the Frontend IRC Bot component. get_connection() and + startup() should have already been called.""" read_buffer = str() while 1: @@ -35,41 +49,52 @@ def main(): lines = read_buffer.split("\n") read_buffer = lines.pop() - for line in lines: + for line in lines: # handle a single message from IRC line = line.strip().split() - data = Data() + data = Data() # new Data() instance to store info about this line data.line = line if line[1] == "JOIN": - data.nick, data.ident, data.host = re.findall(":(.*?)!(.*?)@(.*?)\Z", line[0])[0] + data.nick, data.ident, data.host = findall( + ":(.*?)!(.*?)@(.*?)\Z", line[0])[0] data.chan = line[2][1:] - - command_handler.check("join", data) # check if there's anything we can respond to, and if so, respond + command_handler.check("join", data) # check for 'join' hooks in + # our commands if line[1] == "PRIVMSG": - data.nick, data.ident, data.host = re.findall(":(.*?)!(.*?)@(.*?)\Z", line[0])[0] + data.nick, data.ident, data.host = findall( + ":(.*?)!(.*?)@(.*?)\Z", line[0])[0] data.msg = ' '.join(line[3:])[1:] data.chan = line[2] - if data.chan == NICK: # this is a privmsg to us, so set 'chan' as the nick of the sender + 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) # only respond if it's a private message + command_handler.check("msg_private", data) else: - command_handler.check("msg_public", data) # only respond if it's a public (channel) message + # check for public-only command hooks + command_handler.check("msg_public", data) - command_handler.check("msg", data) # check for general messages + # check for command hooks that apply to all messages + command_handler.check("msg", data) - if data.msg.startswith("!restart"): # hardcode the !restart command (we can't restart from within an ordinary command) - if data.host in OWNERS: + # 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 + if line[0] == "PING": # if we are pinged, pong back to the server connection.send("PONG %s" % line[1]) - if line[1] == "376": - if NS_AUTH: # if we're supposed to auth to nickserv, do that - connection.say("NickServ", "IDENTIFY %s %s" % (NS_USER, NS_PASS)) - time.sleep(3) # sleep for a bit so we don't join channels un-authed - for chan in CHANS: # join all of our startup channels + if line[1] == "376": # we've successfully connected to the network + ns = config.irc.frontend.nickserv + if ns: # if we're supposed to auth to nickserv, do that + connection.say("NickServ", "IDENTIFY %s %s" % (ns.username, + ns.password)) + + # join all of our startup channels + for chan in config.irc.frontend.channels: connection.join(chan) From e21a4dfb76b8dbf7f769f0d763d9142ac3da3493 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 19 Jun 2011 23:56:54 -0400 Subject: [PATCH 15/19] converting four IRC command classes to use new config system; everything's been converted except for irc/watcher.py and everything in wiki/ --- irc/commands/afc_status.py | 3 +-- irc/commands/chanops.py | 4 ++-- irc/commands/git.py | 4 ++-- irc/commands/tasks.py | 12 ++++++------ 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/irc/commands/afc_status.py b/irc/commands/afc_status.py index 4b3553a..dd0248c 100644 --- a/irc/commands/afc_status.py +++ b/irc/commands/afc_status.py @@ -6,7 +6,6 @@ import json import re import urllib -from config.watcher import * from irc.base_command import BaseCommand class AFCStatus(BaseCommand): @@ -22,7 +21,7 @@ class AFCStatus(BaseCommand): data.command == "number" or data.command == "afc_status"): return True try: - if data.line[1] == "JOIN" and data.chan in AFC_CHANS: + if data.line[1] == "JOIN" and data.chan == "#wikipedia-en-afc": return True except IndexError: pass diff --git a/irc/commands/chanops.py b/irc/commands/chanops.py index e7b4683..a2a9cc6 100644 --- a/irc/commands/chanops.py +++ b/irc/commands/chanops.py @@ -3,7 +3,7 @@ # Voice/devoice/op/deop users in the channel. from irc.base_command import BaseCommand -from config.irc import * +from core import config class ChanOps(BaseCommand): def get_hooks(self): @@ -19,7 +19,7 @@ class ChanOps(BaseCommand): return False def process(self, data): - if data.host not in ADMINS: + if data.host not in config.irc.permissions["admins"]: self.connection.reply(data, "you must be a bot admin to use this command.") return diff --git a/irc/commands/git.py b/irc/commands/git.py index 7b76cf8..a919904 100644 --- a/irc/commands/git.py +++ b/irc/commands/git.py @@ -4,8 +4,8 @@ import shlex, subprocess, re -from config.irc import * from irc.base_command import BaseCommand +from core import config class Git(BaseCommand): def get_hooks(self): @@ -21,7 +21,7 @@ class Git(BaseCommand): def process(self, data): self.data = data - if data.host not in OWNERS: + if data.host not in config.irc.permissions["owners"]: self.connection.reply(data, "you must be a bot owner to use this command.") return diff --git a/irc/commands/tasks.py b/irc/commands/tasks.py index d1903bc..58050d0 100644 --- a/irc/commands/tasks.py +++ b/irc/commands/tasks.py @@ -2,13 +2,13 @@ # Manage wiki tasks from IRC, and check on thread status. -import threading, re +import threading +import re from irc.base_command import BaseCommand from irc.data import * from wiki import task_manager -from config.main import * -from config.irc import * +from core import config class Tasks(BaseCommand): def get_hooks(self): @@ -24,7 +24,7 @@ class Tasks(BaseCommand): def process(self, data): self.data = data - if data.host not in OWNERS: + if data.host not in config.irc.permissions["owners"]: self.connection.reply(data, "at this time, you must be a bot owner to use this command.") return @@ -116,9 +116,9 @@ class Tasks(BaseCommand): def get_main_thread_name(self): """Return the "proper" name of the MainThread; e.g. "irc-frontend" or "irc-watcher".""" - if enable_irc_frontend: + if config.components["irc_frontend"]: return "irc-frontend" - elif enable_wiki_schedule: + elif config.components["wiki_schedule"]: return "wiki-scheduler" else: return "irc-watcher" From d57f2623d23009bdedfce441abed6266fdf8f095 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Wed, 22 Jun 2011 20:11:29 -0400 Subject: [PATCH 16/19] parse remaining parts of config.xml; getting rid of config.watcher and moving to irc/watcher_logic.py; convert irc/watcher.py and wiki/task_manager.py to new config system; other changes/fixes/whatever --- core/config.py | 53 +++++++++++++++++++++++++++++-------- core/main.py | 6 ++--- irc/frontend.py | 2 +- irc/watcher.py | 66 ++++++++++++++++++++++++++++++---------------- irc/watcher_logic.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ wiki/task_manager.py | 70 ++++++++++++++++++++++++++++++------------------- 6 files changed, 206 insertions(+), 65 deletions(-) create mode 100644 irc/watcher_logic.py diff --git a/core/config.py b/core/config.py index 486367a..b1331ce 100644 --- a/core/config.py +++ b/core/config.py @@ -8,13 +8,12 @@ including encrypting and decrypting passwords and making a new config file from scratch at the inital bot run. Usually you'll just want to do "from core import config" and access config data -from within config's five global variables: +from within config's four global variables: * config.components * config.wiki * config.irc * config.schedule -* config.watcher """ from collections import defaultdict @@ -170,10 +169,10 @@ def parse_config(key): exit(1) def _parse_config(key): - """Parse config data from a DOM object into the five global variables that + """Parse config data from a DOM object into the four global variables that store our config info. The key is used to unencrypt passwords stored in the XML config file.""" - global components, wiki, irc, schedule, watcher + global components, wiki, irc, schedule _load_config() # we might be re-loading unnecessarily here, but no harm in # that! @@ -183,7 +182,6 @@ def _parse_config(key): wiki = parse_wiki(data, key) irc = parse_irc(data, key) schedule = parse_schedule(data) - watcher = parse_watcher(data) def parse_components(data): """Parse everything within the XML tag of our config file. @@ -270,11 +268,44 @@ def parse_irc(data, key): irc.permissions[group_name].append(hostname) return irc - + def parse_schedule(data): - """Parse everything within the tag of our XML config file.""" - pass + """Store the element in schedule.data and the _schedule() + function as schedule.check().""" + schedule = Container() + schedule.check = _schedule + schedule.data = get_first_element(data, "schedule") + return schedule -def parse_watcher(data): - """Parse everything within the tag of our XML config file.""" - pass +def _schedule(minute, hour, month_day, month, week_day): + """Return a list of tasks that are scheduled to run at the time specified + by the function args. The schedule data comes from our config file's + tag, which is stored as schedule.data. Call this function with + config.schedule.check(args).""" + tasks = [] # tasks to run this turn, each as a tuple of (task_name, + # kwargs), or just task_name + + now = {"minute": minute, "hour": hour, "month_day": month_day, + "month": month, "week_day": week_day} + + for when in schedule.data.getElementsByTagName("when"): + do = True + for key, value in now.items(): + if when.hasAttribute(key): + req = when.getAttribute(key) + if attribute_to_int(req, when, key) != value: + do = False + break + if do: + for task in when.getElementsByTagName("task"): + name = get_required_attribute(task, "name") + args = dict() + for key in task.attributes.keys(): + args[key] = task.getAttribute(key) + del args["name"] + if args: + tasks.append((name, args)) + else: + tasks.append(name) + + return tasks diff --git a/core/main.py b/core/main.py index 31d6149..649e1f6 100644 --- a/core/main.py +++ b/core/main.py @@ -42,8 +42,8 @@ root_dir = os.path.split(script_dir)[0] # the bot's "root" directory relative 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 +from irc import frontend, watcher +from wiki import task_manager f_conn = None w_conn = None @@ -126,7 +126,7 @@ def run(): task_manager.load_tasks() # watcher on another thread iff it if components["irc_watcher"]: # is enabled print "\nStarting IRC watcher..." - t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) + t_watcher = threading.Thread(target=irc_watcher, args=()) t_watcher.name = "irc-watcher" t_watcher.daemon = True t_watcher.start() diff --git a/irc/frontend.py b/irc/frontend.py index d8d4991..38d326a 100644 --- a/irc/frontend.py +++ b/irc/frontend.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -EarwigBot's Front-end IRC Component +EarwigBot's IRC Front-end 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 diff --git a/irc/watcher.py b/irc/watcher.py index 2aff8dd..bbc17c9 100644 --- a/irc/watcher.py +++ b/irc/watcher.py @@ -1,20 +1,34 @@ # -*- coding: utf-8 -*- -## Imports -from config.irc import * -from config.main import * -from config.watcher import * +""" +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. +""" + +from core import config from irc.connection import * from irc.rc import RC +from irc import watcher_logic -global frontend_conn +frontend_conn = None def get_connection(): - connection = Connection(WATCHER_HOST, WATCHER_PORT, NICK, IDENT, REALNAME) + """Return a new Connection() instance with information about our server + connection, but don't actually connect yet.""" + cf = config.irc.watcher + connection = Connection(cf.host, cf.port, cf.nick, cf.nick, cf.realname) return connection -def main(connection, f_conn): +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.""" global frontend_conn frontend_conn = f_conn read_buffer = str() @@ -33,26 +47,32 @@ def main(connection, f_conn): if line[1] == "PRIVMSG": chan = line[2] - if chan != WATCHER_CHAN: # if we're getting a msg from another channel, ignore it + + # 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) # create a new RC object to store this change's data - rc.parse() - check(rc) + 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 + if line[0] == "PING": # if we are pinged, pong back to the server connection.send("PONG %s" % line[1]) - if line[1] == "376": # Join the recent changes channel when we've finished starting up - connection.join(WATCHER_CHAN) - -def check(rc): - """check if we're supposed to report this message anywhere""" - results = process(rc) # process the message in config/watcher.py, and get a list of channels to send it to - if not results: - return - pretty = rc.get_pretty() - if enable_irc_frontend: - for chan in results: + # 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.""" + chans = watcher_logic.process(rc) + if chans and frontend_conn: + pretty = rc.get_pretty() + for chan in chans: frontend_conn.say(chan, pretty) diff --git a/irc/watcher_logic.py b/irc/watcher_logic.py new file mode 100644 index 0000000..bcce171 --- /dev/null +++ b/irc/watcher_logic.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +""" +EarwigBot's IRC Watcher Logic + +This file contains (configurable!) rules that EarwigBot's watcher uses after it +recieves an event from IRC. + +This should, ideally, be in config.xml somehow, but Python code makes more +sense for this sort of thing... so... +""" + +import re + +from wiki import task_manager as tasks + +afc_prefix = "wikipedia( talk)?:(wikiproject )?articles for creation" + +# compile some regexps used when finding specific events +r_page = re.compile(afc_prefix) +r_ffu = re.compile("wikipedia( talk)?:files for upload") +r_move1 = re.compile("moved \[\[{}".format(afc_prefix)) +r_move2 = re.compile("moved \[\[(.*?)\]\] to \[\[{}".format(afc_prefix)) +r_moved_pages = re.compile("^moved \[\[(.*?)\]\] to \[\[(.*?)\]\]") +r_delete = re.compile("deleted \"\[\[{}".format(afc_prefix)) +r_deleted_page = re.compile("^deleted \"\[\[(.*?)\]\]") +r_restore = re.compile("restored \"\[\[{}".format(afc_prefix)) +r_restored_page = re.compile("^restored \"\[\[(.*?)\]\]") +r_protect = re.compile("protected \"\[\[{}".format(afc_prefix)) + +def process(rc): + """Given an RC() object, return a list of channels to report this event to. + Also, start any wiki bot tasks within this function if necessary.""" + chans = set() # channels to report this message to + page_name = rc.page.lower() + comment = rc.comment.lower() + + if "!earwigbot" in rc.msg.lower(): + 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) + chans.add("#wikipedia-en-afc") + + elif r_ffu.match(page_name): + chans.add("#wikipedia-en-afc") + + elif page_name.startswith("template:afc submission"): + chans.add("#wikipedia-en-afc") + + elif rc.flags == "move" and (r_move1.match(comment) or + 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) + chans.add("#wikipedia-en-afc") + + elif rc.flags == "delete" and r_delete.match(comment): + p = r_deleted_page.findall(rc.comment)[0][0] + tasks.start_task("afc_statistics", action="process_delete", page=p) + tasks.start_task("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][0] + tasks.start_task("afc_statistics", action="process_restore", page=p) + tasks.start_task("afc_copyvios", action="process_restore", page=p) + chans.add("#wikipedia-en-afc") + + elif rc.flags == "protect" and r_protect.match(comment): + chans.add("#wikipedia-en-afc") + + return chans diff --git a/wiki/task_manager.py b/wiki/task_manager.py index b17cbbb..3c37a63 100644 --- a/wiki/task_manager.py +++ b/wiki/task_manager.py @@ -1,25 +1,32 @@ # -*- coding: utf-8 -*- -# A module to manage bot tasks. +""" +EarwigBot's Wiki Bot Task Manager + +This module provides some functions to run and load bot tasks from wiki/tasks/. +""" import time import traceback import threading import os -from config import schedule +from core import config -task_list = dict() # the key is the task's name, the value is the task's class instance +# 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() def load_tasks(): - """Load all valid task classes from wiki/tasks/, and add them to the task_list.""" - files = os.listdir(os.path.join("wiki", "tasks")) # get all files in wiki/tasks/ - files.sort() # alphabetically sort list of files + """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)): # ignore non-files - continue - if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" - continue + 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())) @@ -27,10 +34,10 @@ def load_class_from_file(f): """Look in a given file for the task class.""" global task_list - module = f[:-3] # strip .py from end + module = f[:-3] # strip .py from end try: exec "from wiki.tasks import %s as m" % module - except: # importing the file failed for some reason... + except: # importing the file failed for some reason... print "Couldn't load task file %s:" % f traceback.print_exc() return @@ -46,26 +53,34 @@ def load_class_from_file(f): def start_tasks(now=time.gmtime()): """Start all tasks that are supposed to be run at a given time.""" - tasks = schedule.check(now.tm_min, now.tm_hour, now.tm_mday, now.tm_mon, now.tm_wday) # get list of tasks to run this turn + tasks = config.schedule.check(now.tm_min, now.tm_hour, now.tm_mday, + now.tm_mon, now.tm_wday) # get list of tasks to run this turn + for task in tasks: - if isinstance(task, tuple): # they've specified kwargs, so pass those to start_task - start_task(task[0], **task[1]) - else: # otherwise, just pass task_name + if isinstance(task, tuple): # they've specified kwargs + start_task(task[0], **task[1]) # so pass those to start_task + else: # otherwise, just pass task_name start_task(task) def start_task(task_name, **kwargs): - """Start a given task in a new thread. Pass args to the task's run function.""" - print "Starting task '{}' in a new thread...".format(task_name) - + """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] # get the class for this task, a subclass of BaseTask + task = task_list[task_name] except KeyError: - print "Couldn't find task '{}': wiki/tasks/{}.py does not exist.".format(task_name, task_name) + print ("Couldn't find task '{0}': wiki/tasks/{1}.py does not " + + "exist.").format(task_name, task_name) return - - task_thread = threading.Thread(target=lambda: task_wrapper(task, **kwargs)) # Normally we'd do task_wrapper(task, **kwargs), but because of threading we'd have to do Thread(target=task_wrapper, args=(task, **kwargs)), which doesn't work because the **kwargs is inside a tuple, not inside function params. Use lambda to get around the args=tuple nonsense - task_thread.name = "{} ({})".format(task_name, time.strftime("%b %d %H:%M:%S")) - task_thread.daemon = True # stop bot task threads automagically if the main bot stops + + task_thread = threading.Thread(target=lambda: task_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): @@ -73,7 +88,8 @@ def task_wrapper(task, **kwargs): try: task.run(**kwargs) except: - print "Task '{}' raised an exception and had to stop:".format(task.task_name) + print "Task '{0}' raised an exception and had to stop:".format( + task.task_name) traceback.print_exc() else: - print "Task '{}' finished without error.".format(task.task_name) + print "Task '{0}' finished without error.".format(task.task_name) From a06e35ecefa00d555195917147d899bc5b40af29 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 23 Jun 2011 12:47:22 -0400 Subject: [PATCH 17/19] moving IRC class modules (base_command.py, connection.py, data.py, rc.py) to irc/classes/ --- irc/classes/__init__.py | 4 ++++ irc/{ => classes}/base_command.py | 0 irc/{ => classes}/connection.py | 0 irc/{ => classes}/data.py | 0 irc/{ => classes}/rc.py | 0 irc/commands/afc_status.py | 2 +- irc/commands/calc.py | 2 +- irc/commands/chanops.py | 2 +- irc/commands/git.py | 6 ++++-- irc/commands/help.py | 3 +-- irc/commands/link.py | 2 +- irc/commands/tasks.py | 3 +-- irc/commands/test.py | 2 +- irc/frontend.py | 3 +-- irc/watcher.py | 3 +-- 15 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 irc/classes/__init__.py rename irc/{ => classes}/base_command.py (100%) rename irc/{ => classes}/connection.py (100%) rename irc/{ => classes}/data.py (100%) rename irc/{ => classes}/rc.py (100%) diff --git a/irc/classes/__init__.py b/irc/classes/__init__.py new file mode 100644 index 0000000..b92db69 --- /dev/null +++ b/irc/classes/__init__.py @@ -0,0 +1,4 @@ +from base_command import * +from connection import * +from data import * +from rc import * diff --git a/irc/base_command.py b/irc/classes/base_command.py similarity index 100% rename from irc/base_command.py rename to irc/classes/base_command.py diff --git a/irc/connection.py b/irc/classes/connection.py similarity index 100% rename from irc/connection.py rename to irc/classes/connection.py diff --git a/irc/data.py b/irc/classes/data.py similarity index 100% rename from irc/data.py rename to irc/classes/data.py diff --git a/irc/rc.py b/irc/classes/rc.py similarity index 100% rename from irc/rc.py rename to irc/classes/rc.py diff --git a/irc/commands/afc_status.py b/irc/commands/afc_status.py index dd0248c..6a7f720 100644 --- a/irc/commands/afc_status.py +++ b/irc/commands/afc_status.py @@ -6,7 +6,7 @@ import json import re import urllib -from irc.base_command import BaseCommand +from irc.classes import BaseCommand class AFCStatus(BaseCommand): def get_hooks(self): diff --git a/irc/commands/calc.py b/irc/commands/calc.py index ceaca78..fbd26a1 100644 --- a/irc/commands/calc.py +++ b/irc/commands/calc.py @@ -5,7 +5,7 @@ import re import urllib -from irc.base_command import BaseCommand +from irc.classes import BaseCommand class Calc(BaseCommand): def get_hooks(self): diff --git a/irc/commands/chanops.py b/irc/commands/chanops.py index a2a9cc6..1bd7a8d 100644 --- a/irc/commands/chanops.py +++ b/irc/commands/chanops.py @@ -2,7 +2,7 @@ # Voice/devoice/op/deop users in the channel. -from irc.base_command import BaseCommand +from irc.classes import BaseCommand from core import config class ChanOps(BaseCommand): diff --git a/irc/commands/git.py b/irc/commands/git.py index a919904..4d09d9c 100644 --- a/irc/commands/git.py +++ b/irc/commands/git.py @@ -2,9 +2,11 @@ # Commands to interface with the bot's git repository; use '!git help' for sub-command list. -import shlex, subprocess, re +import shlex +import subprocess +import re -from irc.base_command import BaseCommand +from irc.classes import BaseCommand from core import config class Git(BaseCommand): diff --git a/irc/commands/help.py b/irc/commands/help.py index 1970337..54eefa2 100644 --- a/irc/commands/help.py +++ b/irc/commands/help.py @@ -2,8 +2,7 @@ # Generates help information. -from irc.base_command import BaseCommand -from irc.data import Data +from irc.classes import BaseCommand, Data from irc import command_handler class Help(BaseCommand): diff --git a/irc/commands/link.py b/irc/commands/link.py index 59f2d6e..4587451 100644 --- a/irc/commands/link.py +++ b/irc/commands/link.py @@ -4,7 +4,7 @@ import re -from irc.base_command import BaseCommand +from irc.classes import BaseCommand class Link(BaseCommand): def get_hooks(self): diff --git a/irc/commands/tasks.py b/irc/commands/tasks.py index 58050d0..5dba593 100644 --- a/irc/commands/tasks.py +++ b/irc/commands/tasks.py @@ -5,8 +5,7 @@ import threading import re -from irc.base_command import BaseCommand -from irc.data import * +from irc.classes import BaseCommand, Data, KwargParseException from wiki import task_manager from core import config diff --git a/irc/commands/test.py b/irc/commands/test.py index 69ecd2f..630a37f 100644 --- a/irc/commands/test.py +++ b/irc/commands/test.py @@ -4,7 +4,7 @@ import random -from irc.base_command import BaseCommand +from irc.classes import BaseCommand class Test(BaseCommand): def get_hooks(self): diff --git a/irc/frontend.py b/irc/frontend.py index 38d326a..56a6f17 100644 --- a/irc/frontend.py +++ b/irc/frontend.py @@ -13,8 +13,7 @@ from re import findall from core import config from irc import command_handler -from irc.connection import * -from irc.data import Data +from irc.classes import Connection, Data, BrokenSocketException connection = None diff --git a/irc/watcher.py b/irc/watcher.py index bbc17c9..c39f47c 100644 --- a/irc/watcher.py +++ b/irc/watcher.py @@ -11,8 +11,7 @@ channels in the IRC frontend. """ from core import config -from irc.connection import * -from irc.rc import RC +from irc.classes import Connection, RC, BrokenSocketException from irc import watcher_logic frontend_conn = None From 7311ae4bb897c5f09e041978406a0eb67ce988d9 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Tue, 5 Jul 2011 00:50:37 -0400 Subject: [PATCH 18/19] switching from XML config to JSON config - it's just much easier to parse and little easier on the eyes (not to mention shorter in length) --- .gitignore | 2 +- core/config.py | 345 +++++++++++++----------------------------------- core/main.py | 30 ++--- irc/commands/chanops.py | 2 +- irc/commands/git.py | 2 +- irc/commands/tasks.py | 6 +- irc/frontend.py | 23 ++-- irc/watcher.py | 9 +- wiki/task_manager.py | 4 +- 9 files changed, 135 insertions(+), 288 deletions(-) diff --git a/.gitignore b/.gitignore index 868c046..1e66a11 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.pyc # Ignore bot-specific config file: -config.xml +config.json # Ignore pydev's nonsense: .project diff --git a/core/config.py b/core/config.py index b1331ce..29c3fe1 100644 --- a/core/config.py +++ b/core/config.py @@ -1,74 +1,56 @@ # -*- coding: utf-8 -*- """ -EarwigBot's XML Config File Parser +EarwigBot's JSON Config File Parser This handles all tasks involving reading and writing to our config file, including encrypting and decrypting passwords and making a new config file from scratch at the inital bot run. Usually you'll just want to do "from core import config" and access config data -from within config's four global variables: +from within config's three global variables and one function: -* config.components -* config.wiki -* config.irc -* config.schedule +* config.components - a list of enabled components +* config.wiki - a dict of config information for wiki-editing +* config.irc - a dict of config information for IRC +* config.schedule() - returns a list of tasks scheduled to run now """ -from collections import defaultdict +import json from os import makedirs, path -from xml.dom import minidom -from xml.parsers.expat import ExpatError from lib import blowfish script_dir = path.dirname(path.abspath(__file__)) root_dir = path.split(script_dir)[0] -config_path = path.join(root_dir, "config.xml") +config_path = path.join(root_dir, "config.json") -_config = None # holds the parsed DOM object for our config file +_config = None # holds data loaded from our config file -# initialize our five global variables to store config data -components, wiki, irc, schedule, watcher = (None, None, None, None, None) +# set our three easy-config-access global variables to None +components, wiki, irc = (None, None, None) -class ConfigParseError(Exception): - """Base exception for when we could not parse the config file.""" - -class TypeMismatchError(ConfigParseError): - """A field does not fit to its expected type; e.g., an arbitrary string - where we expected a boolean or integer.""" - -class MissingElementError(ConfigParseError): - """An element in the config file is missing a required sub-element.""" - -class MissingAttributeError(ConfigParseError): - """An element is missing a required attribute to be parsed correctly.""" - -class Container(object): - """A class to hold information in a nice, accessable manner.""" - -def _load_config(): - """Load data from our XML config file (config.xml) into a DOM object.""" +def load_config(): + """Load data from our JSON config file (config.json) into _config.""" global _config - _config = minidom.parse(config_path) + with open(config_path, 'r') as fp: + try: + _config = json.load(fp) + except ValueError as error: + print "Error parsing config file {0}:".format(config_path) + print error + exit(1) def verify_config(): """Check to see if we have a valid config file, and if not, notify the user. If there is no config file at all, offer to make one; otherwise, exit.""" if path.exists(config_path): + load_config() try: - _load_config() - except ExpatError as error: - print "Could not parse config file {0}:\n{1}".format(config_path, - error) - exit() - else: - if not _config.getElementsByTagName("config"): - e = "Config file is missing a tag." - raise MissingElementError(e) - return are_passwords_encrypted() + return _config["encryptPasswords"] # are passwords encrypted? + except KeyError: + return False # assume passwords are not encrypted by default else: print "You haven't configured the bot yet!" choice = raw_input("Would you like to do this now? [y/n] ") @@ -77,235 +59,94 @@ def verify_config(): else: exit() -def make_new_config(): - """Make a new XML config file based on the user's input.""" - makedirs(config_dir) - - encrypt = raw_input("Would you like to encrypt passwords stored in " + - "config.xml? [y/n] ") - if encrypt.lower().startswith("y"): - is_encrypted = True - else: - is_encrypted = False - - return is_encrypted - -def are_passwords_encrypted(): - """Determine if the passwords in our config file are encrypted; return - either True or False, or raise an exception if there was a problem reading - the config file.""" - element = _config.getElementsByTagName("config")[0] - attribute = element.getAttribute("encrypt-passwords") - if not attribute: - return False - return attribute_to_bool(attribute, element, "encrypt-passwords") +def parse_config(key): + """Store data from our config file in three global variables for easy + access, and use the key to unencrypt passwords. Catch password decryption + errors and report them to the user.""" + global components, wiki, irc -def get_first_element(parent, tag_name): - """Return the first child of the parent element with the given tag name, or - return None if no child of that name exists.""" + load_config() # we might be re-loading unnecessarily here, but no harm in + # that! try: - return parent.getElementsByTagName(tag_name)[0] - except IndexError: - return None - -def get_required_element(parent, tag_name): - """Return the first child of the parent element with the given tag name, or - raise MissingElementError() if no child of that name exists.""" - element = get_first_element(parent, tag_name) - if not element: - e = "A <{0}> tag is missing a required <{1}> child tag.".format( - parent.tagName, tag_name) - raise MissingElementError(e) - return element - -def get_required_attribute(element, attr_name): - """Return the value of the attribute 'attr_name' in 'element'. If - undefined, raise MissingAttributeError().""" - attribute = element.getAttribute(attr_name) - if not attribute: - e = "A <{0}> tag is missing the required attribute '{1}'.".format( - element.tagName, attr_name) - raise MissingAttributeError(e) - return attribute - -def attribute_to_bool(value, element, attr_name): - """Return True if 'value' is 'true', '1', or 'on', return False if it is - 'false', '0', or 'off' (regardless of capitalization), or raise - TypeMismatchError() if it does match any of those. 'element' and - 'attr_name' are only used to generate the error message.""" - lcase = value.lower() - if lcase in ["true", "1", "on"]: - return True - elif lcase in ["false", "0", "off"]: - return False - else: - e = ("Expected a bool in attribute '{0}' of tag '{1}', but got '{2}'." - ).format(attr_name, element.tagName, value) - raise TypeMismatchError(e) - -def attribute_to_int(value, element, attr_name): - """Return 'value' after it is converted to an integer. If it could not be - converted, raise TypeMismatchError() using 'element' and 'attr_name' only - to give the user information about what happened.""" + components = _config["components"] + except KeyError: + components = [] + try: + wiki = _config["wiki"] + except KeyError: + wiki = {} try: - return int(value) - except ValueError: - e = ("Expected an integer in attribute '{0}' of tag '{1}', but got " + - "'{2}'.").format(attr_name, element.tagName, value) - raise TypeMismatchError(e) + irc = _config["irc"] + except KeyError: + irc = {} -def parse_config(key): - """A thin wrapper for the actual config parser in _parse_config(): catch - parsing exceptions and report them to the user cleanly.""" try: - _parse_config(key) - except ConfigParseError as error: - print "\nError parsing config file:" - print error - exit(1) + try: + if _config["encryptPasswords"]: + decrypt(key, "wiki['password']") + decrypt(key, "irc['frontend']['nickservPassword']") + decrypt(key, "irc['watcher']['nickservPassword']") + except KeyError: + pass except blowfish.BlowfishError as error: print "\nError decrypting passwords:" print "{0}: {1}.".format(error.__class__.__name__, error) exit(1) -def _parse_config(key): - """Parse config data from a DOM object into the four global variables that - store our config info. The key is used to unencrypt passwords stored in the - XML config file.""" - global components, wiki, irc, schedule - - _load_config() # we might be re-loading unnecessarily here, but no harm in - # that! - data = _config.getElementsByTagName("config")[0] - - components = parse_components(data) - wiki = parse_wiki(data, key) - irc = parse_irc(data, key) - schedule = parse_schedule(data) - -def parse_components(data): - """Parse everything within the XML tag of our config file. - The components object here will exist as config.components, and is a dict - of our enabled components: components[name] = True if it is enabled, False - if it is disabled.""" - components = defaultdict(lambda: False) # all components are disabled by - # default - element = get_required_element(data, "components") - - for component in element.getElementsByTagName("component"): - name = get_required_attribute(component, "name") - components[name] = True - - return components - -def parse_wiki(data, key): - """Parse everything within the tag of our XML config file.""" - pass - -def parse_irc_server(data, key): - """Parse everything within a tag.""" - server = Container() - connection = get_required_element(data, "connection") - - server.host = get_required_attribute(connection, "host") - server.port = get_required_attribute(connection, "port") - server.nick = get_required_attribute(connection, "nick") - server.ident = get_required_attribute(connection, "ident") - server.realname = get_required_attribute(connection, "realname") - - # convert the port from a string to an int - server.port = attribute_to_int(server.port, connection, "port") - - nickserv = get_first_element(data, "nickserv") - if nickserv: - server.nickserv = Container() - server.nickserv.username = get_required_attribute(nickserv, "username") - password = get_required_attribute(nickserv, "password") - if are_passwords_encrypted(): - server.nickserv.password = blowfish.decrypt(key, password) - else: - server.nickserv.password = password - else: - server.nickserv = None - - server.channels = list() - channels = get_first_element(data, "channels") - if channels: - for channel in channels.getElementsByTagName("channel"): - name = get_required_attribute(channel, "name") - server.channels.append(name) - - return server - -def parse_irc(data, key): - """Parse everything within the tag of our XML config file.""" - irc = Container() - - element = get_first_element(data, "irc") - if not element: - return irc - - servers = get_first_element(element, "servers") - if servers: - for server in servers.getElementsByTagName("server"): - server_name = get_required_attribute(server, "name") - if server_name == "frontend": - irc.frontend = parse_irc_server(server, key) - elif server_name == "watcher": - irc.watcher = parse_irc_server(server, key) - else: - print ("Warning: config file specifies a with " + - "unknown name '{0}'. Ignoring.").format(server_name) - - permissions = get_first_element(element, "permissions") - if permissions: - irc.permissions = dict() - for group in permissions.getElementsByTagName("group"): - group_name = get_required_attribute(group, "name") - irc.permissions[group_name] = list() - for user in group.getElementsByTagName("user"): - hostname = get_required_attribute(user, "host") - irc.permissions[group_name].append(hostname) - - return irc +def decrypt(key, item): + """Decrypt 'item' with blowfish.decrypt() using the given key and set it to + the decrypted result. 'item' should be a string, like + decrypt(key, "wiki['password']"), NOT decrypt(key, wiki['password'), + because that won't work.""" + global irc, wiki + try: + result = blowfish.decrypt(key, eval(item)) + except KeyError: + return + exec "{0} = result".format(item) -def parse_schedule(data): - """Store the element in schedule.data and the _schedule() - function as schedule.check().""" - schedule = Container() - schedule.check = _schedule - schedule.data = get_first_element(data, "schedule") - return schedule - -def _schedule(minute, hour, month_day, month, week_day): +def schedule(minute, hour, month_day, month, week_day): """Return a list of tasks that are scheduled to run at the time specified - by the function args. The schedule data comes from our config file's - tag, which is stored as schedule.data. Call this function with - config.schedule.check(args).""" - tasks = [] # tasks to run this turn, each as a tuple of (task_name, + by the function arguments. The schedule data comes from our config file's + 'schedule' field, which is stored as _config["schedule"]. Call this + function with config.schedule(args).""" + tasks = [] # tasks to run this turn, each as a tuple of either (task_name, # kwargs), or just task_name now = {"minute": minute, "hour": hour, "month_day": month_day, "month": month, "week_day": week_day} - for when in schedule.data.getElementsByTagName("when"): + try: + data = _config["schedule"] + except KeyError: + data = [] + for event in data: do = True for key, value in now.items(): - if when.hasAttribute(key): - req = when.getAttribute(key) - if attribute_to_int(req, when, key) != value: - do = False - break + try: + requirement = event[key] + except KeyError: + continue + if requirement != value: + do = False + break if do: - for task in when.getElementsByTagName("task"): - name = get_required_attribute(task, "name") - args = dict() - for key in task.attributes.keys(): - args[key] = task.getAttribute(key) - del args["name"] - if args: - tasks.append((name, args)) - else: - tasks.append(name) + try: + tasks.extend(event["tasks"]) + except KeyError: + pass return tasks + +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] ") + if encrypt.lower().startswith("y"): + is_encrypted = True + else: + is_encrypted = False + + return is_encrypted diff --git a/core/main.py b/core/main.py index 649e1f6..0b49842 100644 --- a/core/main.py +++ b/core/main.py @@ -77,7 +77,7 @@ def wiki_scheduler(): if time_diff < 60: # sleep until the next minute time.sleep(60 - time_diff) -def irc_frontend(components): +def irc_frontend(): """If the IRC frontend is enabled, make it run on our primary thread, and enable the wiki scheduler and IRC watcher on new threads if they are enabled.""" @@ -87,7 +87,7 @@ def irc_frontend(components): f_conn = frontend.get_connection() frontend.startup(f_conn) - if config.components["wiki_schedule"]: + if "wiki_schedule" in config.components: print "\nStarting wiki scheduler..." task_manager.load_tasks() t_scheduler = threading.Thread(target=wiki_scheduler) @@ -95,7 +95,7 @@ def irc_frontend(components): t_scheduler.daemon = True t_scheduler.start() - if config.components["irc_watcher"]: + if "irc_watcher" in config.components: print "\nStarting IRC watcher..." t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) t_watcher.name = "irc-watcher" @@ -104,7 +104,7 @@ def irc_frontend(components): frontend.main() - if config.components["irc_watcher"]: + if "irc_watcher" in config.components: w_conn.close() f_conn.close() @@ -115,16 +115,16 @@ def run(): key = None config.parse_config(key) # load data from the config file and parse it # using the unlock key - components = config.components + enabled = config.components - if components["irc_frontend"]: # make the frontend run on our primary - irc_frontend(components) # thread if enabled, and enable additional - # components through that function + if "irc_frontend" in enabled: # make the frontend run on our primary + irc_frontend() # thread if enabled, and enable additional + # components through that function - elif components["wiki_schedule"]: # 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 - if components["irc_watcher"]: # is enabled + 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 + if "irc_watcher" in enabled: # is enabled print "\nStarting IRC watcher..." t_watcher = threading.Thread(target=irc_watcher, args=()) t_watcher.name = "irc-watcher" @@ -132,9 +132,9 @@ def run(): t_watcher.start() wiki_scheduler() - elif components["irc_watcher"]: # the IRC watcher is our only enabled - print "Starting IRC watcher..." # component, so run its function only - irc_watcher() # and don't worry about anything else + elif "irc_watcher" in enabled: # the IRC watcher is our only enabled + print "Starting IRC watcher..." # component, so run its function only + irc_watcher() # and don't worry about anything else else: # nothing is enabled! print "No bot parts are enabled; stopping..." diff --git a/irc/commands/chanops.py b/irc/commands/chanops.py index 1bd7a8d..210a830 100644 --- a/irc/commands/chanops.py +++ b/irc/commands/chanops.py @@ -19,7 +19,7 @@ class ChanOps(BaseCommand): return False def process(self, data): - if data.host not in config.irc.permissions["admins"]: + if data.host not in config.irc["permissions"]["admins"]: self.connection.reply(data, "you must be a bot admin to use this command.") return diff --git a/irc/commands/git.py b/irc/commands/git.py index 4d09d9c..5c35739 100644 --- a/irc/commands/git.py +++ b/irc/commands/git.py @@ -23,7 +23,7 @@ class Git(BaseCommand): def process(self, data): self.data = data - if data.host not in config.irc.permissions["owners"]: + if data.host not in config.irc["permissions"]["owners"]: self.connection.reply(data, "you must be a bot owner to use this command.") return diff --git a/irc/commands/tasks.py b/irc/commands/tasks.py index 5dba593..7776db4 100644 --- a/irc/commands/tasks.py +++ b/irc/commands/tasks.py @@ -23,7 +23,7 @@ class Tasks(BaseCommand): def process(self, data): self.data = data - if data.host not in config.irc.permissions["owners"]: + if data.host not in config.irc["permissions"]["owners"]: self.connection.reply(data, "at this time, you must be a bot owner to use this command.") return @@ -115,9 +115,9 @@ class Tasks(BaseCommand): def get_main_thread_name(self): """Return the "proper" name of the MainThread; e.g. "irc-frontend" or "irc-watcher".""" - if config.components["irc_frontend"]: + if "irc_frontend" in config.components: return "irc-frontend" - elif config.components["wiki_schedule"]: + elif "wiki_schedule" in config.components: return "wiki-scheduler" else: return "irc-watcher" diff --git a/irc/frontend.py b/irc/frontend.py index 56a6f17..2a8537d 100644 --- a/irc/frontend.py +++ b/irc/frontend.py @@ -20,8 +20,9 @@ connection = None def get_connection(): """Return a new Connection() instance with information about our server connection, but don't actually connect yet.""" - cf = config.irc.frontend - connection = Connection(cf.host, cf.port, cf.nick, cf.nick, cf.realname) + cf = config.irc["frontend"] + connection = Connection(cf["host"], cf["port"], cf["nick"], cf["ident"], + cf["realname"]) return connection def startup(conn): @@ -66,7 +67,7 @@ def main(): data.msg = ' '.join(line[3:])[1:] data.chan = line[2] - if data.chan == config.irc.frontend.nick: + 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 @@ -81,7 +82,7 @@ def main(): # 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"]: + if data.host in config.irc["permissions"]["owners"]: print "Restarting bot per owner request..." return @@ -89,11 +90,15 @@ def main(): connection.send("PONG %s" % line[1]) if line[1] == "376": # we've successfully connected to the network - ns = config.irc.frontend.nickserv - if ns: # if we're supposed to auth to nickserv, do that - connection.say("NickServ", "IDENTIFY %s %s" % (ns.username, - ns.password)) + 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: + for chan in config.irc["frontend"]["channels"]: connection.join(chan) diff --git a/irc/watcher.py b/irc/watcher.py index c39f47c..b031b20 100644 --- a/irc/watcher.py +++ b/irc/watcher.py @@ -19,8 +19,9 @@ frontend_conn = None def get_connection(): """Return a new Connection() instance with information about our server connection, but don't actually connect yet.""" - cf = config.irc.watcher - connection = Connection(cf.host, cf.port, cf.nick, cf.nick, cf.realname) + cf = config.irc["watcher"] + connection = Connection(cf["host"], cf["port"], cf["nick"], cf["ident"], + cf["realname"]) return connection def main(connection, f_conn=None): @@ -49,7 +50,7 @@ def main(connection, f_conn=None): # ignore messages originating from channels not in our list, to # prevent someone PMing us false data - if chan not in config.irc.watcher.channels: + if chan not in config.irc["watcher"]["channels"]: continue msg = ' '.join(line[3:])[1:] @@ -62,7 +63,7 @@ def main(connection, f_conn=None): # when we've finished starting up, join all watcher channels if line[1] == "376": - for chan in config.irc.watcher.channels: + for chan in config.irc["watcher"]["channels"]: connection.join(chan) def process(rc): diff --git a/wiki/task_manager.py b/wiki/task_manager.py index 3c37a63..de40db1 100644 --- a/wiki/task_manager.py +++ b/wiki/task_manager.py @@ -53,8 +53,8 @@ def load_class_from_file(f): def start_tasks(now=time.gmtime()): """Start all tasks that are supposed to be run at a given time.""" - tasks = config.schedule.check(now.tm_min, now.tm_hour, now.tm_mday, - now.tm_mon, now.tm_wday) # get list of tasks to run this turn + 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, tuple): # they've specified kwargs From e11cbffa92d12fb7812ddad159aeac759e209e0a Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 7 Jul 2011 01:05:16 -0400 Subject: [PATCH 19/19] uh, right... --- earwigbot.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 earwigbot.py diff --git a/earwigbot.py b/earwigbot.py old mode 100644 new mode 100755