Browse Source

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

tags/v0.1^2
Ben Kurtovic 13 years ago
parent
commit
a568ec6777
3 changed files with 202 additions and 93 deletions
  1. +85
    -37
      core/config.py
  2. +73
    -51
      core/main.py
  3. +44
    -5
      earwigbot.py

+ 85
- 37
core/config.py View File

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

+ 73
- 51
core/main.py View File

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

+ 44
- 5
earwigbot.py View File

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

Loading…
Cancel
Save