@@ -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 <components> 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 <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() | |||
@@ -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) | |||
@@ -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" | |||
@@ -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) |