Explorar el Código

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)

tags/v0.1^2
Ben Kurtovic hace 13 años
padre
commit
7311ae4bb8
Se han modificado 9 ficheros con 135 adiciones y 288 borrados
  1. +1
    -1
      .gitignore
  2. +93
    -252
      core/config.py
  3. +15
    -15
      core/main.py
  4. +1
    -1
      irc/commands/chanops.py
  5. +1
    -1
      irc/commands/git.py
  6. +3
    -3
      irc/commands/tasks.py
  7. +14
    -9
      irc/frontend.py
  8. +5
    -4
      irc/watcher.py
  9. +2
    -2
      wiki/task_manager.py

+ 1
- 1
.gitignore Ver fichero

@@ -2,7 +2,7 @@
*.pyc

# Ignore bot-specific config file:
config.xml
config.json

# Ignore pydev's nonsense:
.project


+ 93
- 252
core/config.py Ver fichero

@@ -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 <config> 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 <components> 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 <wiki> tag of our XML config file."""
pass

def parse_irc_server(data, key):
"""Parse everything within a <server> 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 <irc> 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 <server> 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 <schedule> 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
<schedule> 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

+ 15
- 15
core/main.py Ver fichero

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


+ 1
- 1
irc/commands/chanops.py Ver fichero

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



+ 1
- 1
irc/commands/git.py Ver fichero

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



+ 3
- 3
irc/commands/tasks.py Ver fichero

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

+ 14
- 9
irc/frontend.py Ver fichero

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

+ 5
- 4
irc/watcher.py Ver fichero

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


+ 2
- 2
wiki/task_manager.py Ver fichero

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


Cargando…
Cancelar
Guardar