@@ -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 | including encrypting and decrypting passwords and making a new config file from | ||||
scratch at the inital bot run. | 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 | from collections import defaultdict | ||||
@@ -23,7 +29,9 @@ root_dir = path.split(script_dir)[0] | |||||
config_path = path.join(root_dir, "config.xml") | config_path = path.join(root_dir, "config.xml") | ||||
_config = None # holds the parsed DOM object for our config file | _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): | class ConfigParseError(Exception): | ||||
"""Base exception for when we could not parse the config file.""" | """Base exception for when we could not parse the config file.""" | ||||
@@ -84,27 +92,14 @@ def make_new_config(): | |||||
return is_encrypted | return is_encrypted | ||||
def are_passwords_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] | 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 | 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): | def get_first_element(parent, tag_name): | ||||
"""Return the first child of the parent element with the given tag name, or | """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) | raise MissingAttributeError(e) | ||||
return attribute | 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): | def parse_config(key): | ||||
"""A thin wrapper for the actual config parser in _parse_config(): catch | """A thin wrapper for the actual config parser in _parse_config(): catch | ||||
parsing exceptions and report them to the user cleanly.""" | parsing exceptions and report them to the user cleanly.""" | ||||
@@ -149,21 +170,20 @@ def parse_config(key): | |||||
exit(1) | exit(1) | ||||
def _parse_config(key): | 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 | _load_config() # we might be re-loading unnecessarily here, but no harm in | ||||
# that! | # that! | ||||
data = _config.getElementsByTagName("config")[0] | 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): | def parse_components(data): | ||||
"""Parse everything within the <components> XML tag of our config file. | """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): | def parse_irc_server(data, key): | ||||
"""Parse everything within a <server> tag.""" | """Parse everything within a <server> tag.""" | ||||
server = Container() | server = Container() | ||||
connection = get_required_element(data, "connection") | connection = get_required_element(data, "connection") | ||||
server.host = get_required_attribute(connection, "host") | server.host = get_required_attribute(connection, "host") | ||||
server.port = get_required_attribute(connection, "port") | server.port = get_required_attribute(connection, "port") | ||||
server.nick = get_required_attribute(connection, "nick") | server.nick = get_required_attribute(connection, "nick") | ||||
server.ident = get_required_attribute(connection, "ident") | server.ident = get_required_attribute(connection, "ident") | ||||
server.realname = get_required_attribute(connection, "realname") | 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") | nickserv = get_first_element(data, "nickserv") | ||||
if nickserv: | if nickserv: | ||||
server.nickserv = Container() | server.nickserv = Container() | ||||
@@ -204,10 +227,12 @@ def parse_irc_server(data, key): | |||||
server.nickserv.password = blowfish.decrypt(key, password) | server.nickserv.password = blowfish.decrypt(key, password) | ||||
else: | else: | ||||
server.nickserv.password = password | server.nickserv.password = password | ||||
else: | |||||
server.nickserv = None | |||||
server.channels = list() | |||||
channels = get_first_element(data, "channels") | channels = get_first_element(data, "channels") | ||||
if channels: | if channels: | ||||
server.channels = list() | |||||
for channel in channels.getElementsByTagName("channel"): | for channel in channels.getElementsByTagName("channel"): | ||||
name = get_required_attribute(channel, "name") | name = get_required_attribute(channel, "name") | ||||
server.channels.append(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 | sys.path.append(root_dir) # make sure we look in the root dir for modules | ||||
from core import config | from core import config | ||||
#from irc import frontend, watcher | |||||
from irc import frontend#, watcher | |||||
#from wiki import task_manager | #from wiki import task_manager | ||||
f_conn = None | f_conn = None | ||||
@@ -87,7 +87,7 @@ def irc_frontend(components): | |||||
f_conn = frontend.get_connection() | f_conn = frontend.get_connection() | ||||
frontend.startup(f_conn) | frontend.startup(f_conn) | ||||
if enable_wiki_schedule: | |||||
if config.components["wiki_schedule"]: | |||||
print "\nStarting wiki scheduler..." | print "\nStarting wiki scheduler..." | ||||
task_manager.load_tasks() | task_manager.load_tasks() | ||||
t_scheduler = threading.Thread(target=wiki_scheduler) | t_scheduler = threading.Thread(target=wiki_scheduler) | ||||
@@ -95,7 +95,7 @@ def irc_frontend(components): | |||||
t_scheduler.daemon = True | t_scheduler.daemon = True | ||||
t_scheduler.start() | t_scheduler.start() | ||||
if enable_irc_watcher: | |||||
if config.components["irc_watcher"]: | |||||
print "\nStarting IRC watcher..." | print "\nStarting IRC watcher..." | ||||
t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) | t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) | ||||
t_watcher.name = "irc-watcher" | t_watcher.name = "irc-watcher" | ||||
@@ -104,7 +104,7 @@ def irc_frontend(components): | |||||
frontend.main() | frontend.main() | ||||
if enable_irc_watcher: | |||||
if config.components["irc_watcher"]: | |||||
w_conn.close() | w_conn.close() | ||||
f_conn.close() | f_conn.close() | ||||
@@ -115,7 +115,7 @@ def run(): | |||||
key = None | key = None | ||||
config.parse_config(key) # load data from the config file and parse it | config.parse_config(key) # load data from the config file and parse it | ||||
# using the unlock key | # using the unlock key | ||||
components = config.config.components | |||||
components = config.components | |||||
if components["irc_frontend"]: # make the frontend run on our primary | if components["irc_frontend"]: # make the frontend run on our primary | ||||
irc_frontend(components) # thread if enabled, and enable additional | 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 | elif components["wiki_schedule"]: # run the scheduler on the main | ||||
print "Starting wiki scheduler..." # thread, but also run the IRC | print "Starting wiki scheduler..." # thread, but also run the IRC | ||||
task_manager.load_tasks() # watcher on another thread iff it | 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..." | print "\nStarting IRC watcher..." | ||||
t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) | t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) | ||||
t_watcher.name = "irc-watcher" | t_watcher.name = "irc-watcher" | ||||
@@ -1,11 +1,17 @@ | |||||
# -*- coding: utf-8 -*- | # -*- 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 import command_handler | ||||
from irc.connection import * | from irc.connection import * | ||||
from irc.data import Data | from irc.data import Data | ||||
@@ -13,16 +19,24 @@ from irc.data import Data | |||||
connection = None | connection = None | ||||
def get_connection(): | 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 | return connection | ||||
def startup(conn): | 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 | global connection | ||||
connection = conn | connection = conn | ||||
command_handler.load_commands(connection) | command_handler.load_commands(connection) | ||||
connection.connect() | connection.connect() | ||||
def main(): | def main(): | ||||
"""Main loop for the Frontend IRC Bot component. get_connection() and | |||||
startup() should have already been called.""" | |||||
read_buffer = str() | read_buffer = str() | ||||
while 1: | while 1: | ||||
@@ -35,41 +49,52 @@ def main(): | |||||
lines = read_buffer.split("\n") | lines = read_buffer.split("\n") | ||||
read_buffer = lines.pop() | read_buffer = lines.pop() | ||||
for line in lines: | |||||
for line in lines: # handle a single message from IRC | |||||
line = line.strip().split() | line = line.strip().split() | ||||
data = Data() | |||||
data = Data() # new Data() instance to store info about this line | |||||
data.line = line | data.line = line | ||||
if line[1] == "JOIN": | 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:] | 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": | 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.msg = ' '.join(line[3:])[1:] | ||||
data.chan = line[2] | 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 | 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: | 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..." | print "Restarting bot per owner request..." | ||||
return | return | ||||
if line[0] == "PING": # If we are pinged, pong back to the server | |||||
if line[0] == "PING": # if we are pinged, pong back to the server | |||||
connection.send("PONG %s" % line[1]) | 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) | connection.join(chan) |