diff --git a/bot.py b/bot.py index 70eb50c..be435b7 100755 --- a/bot.py +++ b/bot.py @@ -1,71 +1,79 @@ #!/usr/bin/python -network = "irc.synirc.net" +network = "localhost" nickname = "skybot" -channel = "#mongbot" +channel = "#skybot" -import sys, os, glob, imp, re -import thread, Queue, copy -import irc, yaml +import sys +import os +import glob +import imp +import re +import thread +import Queue +import copy + +import irc +import yaml os.chdir(sys.path[0]) #do stuff relative to the installation directory - -class Empty(object): #this is used to store attributes - pass - - class Bot(object): def __init__(self): self.plugins = {} - self.commands = {} - self.listens = {} - self.filters = {} + self.commands = [] # fn, name, func, args + self.listens = {} + self.filters = [] #fn, name, func self.daemons = {} - def command(self, name, func, **filterargs): - self.commands[name] = (func, filterargs) - - def listen(self, name, func): - self.listens[name] = func - - def filter(self, name, func): - self.filters[name] = func - - def daemon(self, name, func): - self.daemons[name] = func - bot = Bot() +bot.nickname = nickname +bot.channel = channel +bot.network = network print 'Loading plugins' +magic_re = re.compile(r'^\s*#(command|filter)(?:: +(\S+) *(\S.*)?)?\s*$') for filename in glob.glob("plugins/*.py"): shortname = os.path.splitext(os.path.basename(filename))[0] try: - bot.plugins[shortname] = imp.load_source(shortname,filename) + bot.plugins[shortname] = imp.load_source(shortname, filename) + plugin = bot.plugins[shortname] + source = open(filename).read().split('\n') + #this is a nasty hack, but it simplifies registration + funcs = [x for x in dir(plugin) + if str(type(getattr(plugin,x))) == ""] + for func in funcs: + #read the line before the function definition, looking for a + # comment that tells the bot about what it does + func = getattr(plugin, func) + lineno = func.func_code.co_firstlineno + if lineno == 1: + continue #can't have a line before the first line... + m = magic_re.match(source[lineno-2]) + if m: + typ, nam, rest = m.groups() + if nam is None: + nam = func.__name__ + if rest is None: + rest = '\s*(.*)' + if typ == 'command': + args = {'name': nam, 'hook': nam + rest} + bot.commands.append((filename, nam, func, args)) + if typ == 'filter': + bot.filters.append((filename, nam, func)) except Exception, e: print e -print bot.plugins - -print 'Registering plugins' -for name, plugin in bot.plugins.iteritems(): - if hasattr(plugin, 'setup'): - try: - plugin.setup(bot) - except Exception, e: - print e - -print 'Connecting to IRC' - -bot.irc = irc.irc(network, nickname) -bot.irc.join(channel) -bot.commandprefix = '^(?:\.|'+nickname+'[:,]*\s*)' - if bot.daemons: print 'Running daemons' for daemon in bot.daemons.itervalues(): thread.start_new_thread(daemon, ()) +print 'Connecting to IRC' +bot.irc = irc.irc(network, nickname) +bot.irc.join(channel) +bot.commandprefix = '^(?:\.|'+nickname+'[:,]*\s*)' + print 'Running main loop' class Input(object): @@ -82,9 +90,14 @@ class Input(object): self.msg = msg class FakeBot(object): - def __init__(self, bot, input): + def __init__(self, bot, input, func): self.bot = bot self.input = input + self.msg = bot.irc.msg + self.cmd = bot.irc.cmd + self.func = func + if input.command == "PRIVMSG": + self.chan = input.paraml[0] def say(self, msg): self.bot.irc.msg(input.paraml[0], msg) @@ -92,22 +105,25 @@ class FakeBot(object): def reply(self, msg): self.say(input.nick + ': ' + msg) + def run(self): + out = self.func(self, self.input) + if out is not None: + self.reply(unicode(out)) + print bot.commands +print bot.filters while True: try: out = bot.irc.out.get(timeout=1) - #print repr(out) - for func, args in bot.commands.itervalues(): + for fn, name, func, args in bot.commands: input = Input(*out) - for filt in bot.filters.itervalues(): + for fn, nam, filt in bot.filters: input = filt(bot, func, args, input) - if input == False: + if input == None: break - if input == False: - continue - thread.start_new_thread(func,(FakeBot(bot, input), input)) + if input == None: + break + thread.start_new_thread(FakeBot(bot, input, func).run, ()) except Queue.Empty: pass - #except KeyboardInterrupt: - # sys.exit() diff --git a/irc.py b/irc.py index d7e7cec..b864236 100644 --- a/irc.py +++ b/irc.py @@ -1,7 +1,21 @@ -import sys, re, thread, Queue -import socket, asyncore, asynchat +import sys +import re +import socket +import thread +import asyncore +import asynchat +import Queue + queue = Queue.Queue +def decode(txt, codecs=['utf-8', 'iso-8859-1', 'shift_jis', 'cp1252']): + if len(codecs) == 0: + return txt.decode('utf-8', 'ignore') + try: + return txt.decode(codecs[0]) + except UnicodeDecodeError: + return decode(txt, codecs[1:]) + class crlf_tcp(asynchat.async_chat): "Handles tcp connections that consist of utf-8 lines ending with crlf" def __init__(self, host, port): @@ -17,30 +31,28 @@ class crlf_tcp(asynchat.async_chat): def run(self): self.connect((self.host, self.port)) - thread.start_new_thread(self.queue_read_loop,()) asyncore.loop() + def handle_connect(self): + thread.start_new_thread(self.queue_read_loop,()) + def queue_read_loop(self): - #this is an attempt at making this thread-safe - #at least with this, only TWO things could be modifying the output - #buffer at the same time while True: line = self.oqueue.get() - print ">>> %r" % line - self.push(line.encode('utf-8')+'\r\n') + print ">>> %r" % line + self.push(line.encode('utf-8','replace')+'\r\n') def collect_incoming_data(self, data): self.buffer += data def found_terminator(self): line = self.buffer - # print repr(line) - self.iqueue.put(line.encode('utf-8')) + self.iqueue.put(decode(line)) self.buffer = '' irc_prefix_re = re.compile(r'(.*?) (.*?) (.*)') irc_noprefix_re = re.compile(r'()(.*?) (.*)') -irc_param_re = re.compile(r'(?:^|(?<= ))(:.*|[^ ]*)') +irc_param_re = re.compile(r'(?:^|(?<= ))(:.*|[^ ]+)') irc_netmask_re = re.compile(r':?([^!@]*)!?([^@]*)@?(.*)') class irc(object): diff --git a/irc.pyc b/irc.pyc deleted file mode 100644 index 55f544e..0000000 Binary files a/irc.pyc and /dev/null differ diff --git a/plugins/filter.py b/plugins/filter.py index 51102ac..bac8703 100644 --- a/plugins/filter.py +++ b/plugins/filter.py @@ -1,13 +1,12 @@ import re -def setup(bot): - bot.filter("default_filters", filter_suite) - +#filter def filter_suite(bot, func, args, input): args.setdefault('events', ['PRIVMSG']) if input.command not in args['events']: - return False + if args['events'] != '*': + return None args.setdefault('hook', r'(.*)') args.setdefault('prefix', True) @@ -16,9 +15,10 @@ def filter_suite(bot, func, args, input): if args['prefix']: hook = bot.commandprefix + args['hook'] - m = re.match(hook, input.msg) - if not m: - return False - - input.re = m + input.re = re.match(hook, input.msg) + if input.re is None: + return None + + input.inp = ' '.join(input.re.groups()) + return input diff --git a/plugins/weather.py b/plugins/weather.py index 75fe09d..509d0ad 100755 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -1,54 +1,28 @@ +#!/usr/bin/python "weather, thanks to google" -import urllib, re, pickle, os -from xml.dom import minidom - -WEATHER_URL = 'http://www.google.com/ig/api' - -def setup(bot): - bot.command("weather", weather, hook=r"weather (.*)") - -def goog_weather(query): - data = urllib.urlencode({'weather':query}) - url = WEATHER_URL + "?" + data - #print url - dom = minidom.parse(urllib.urlopen(url)) - - if len(dom.getElementsByTagName('problem_cause')): - return {'error': True} - - place = dom.getElementsByTagName('city')[0].getAttribute('data') - temp = dom.getElementsByTagName('temp_f')[0].getAttribute('data') - - conditions = dom.getElementsByTagName('current_conditions')[0] - condition = conditions.getElementsByTagName('condition')[0].getAttribute('data') - wind = conditions.getElementsByTagName('wind_condition')[0].getAttribute('data') - humidity = conditions.getElementsByTagName('humidity')[0].getAttribute('data') - - forecast = dom.getElementsByTagName('forecast_conditions')[0] - high = forecast.getElementsByTagName('high')[0].getAttribute('data') - low = forecast.getElementsByTagName('low')[0].getAttribute('data') - - return { - 'place': place, - 'temp': temp, - 'high': high, - 'low': low, - 'condition': condition, - 'wind': wind, - 'humidity': humidity - } +import urllib +from xml.etree import ElementTree +#command: weather def weather(bot, input): - ".weather -- queries google weather API for weather data" - q = input.re.groups()[0] - cond = goog_weather(q) + '''.weather -- queries the google weather API for weather data''' - if cond.has_key('error'): - bot.reply(u'Couldn\'t fetch weather data for "%s", try using a zip/postal code' % (q)) - return + if not input.inp.strip(): # blank line + return "welp" - format = u'%s %sF (%s/%s/%s) (h:%sF,l:%sF)' - args = (cond['place'],cond['temp'],cond['condition'],cond['wind'],cond['humidity'],cond['high'],cond['low']) + data = urllib.urlencode({'weather':input.inp.encode('utf-8')}) + url = 'http://www.google.com/ig/api?' + data + w = ElementTree.parse(urllib.urlopen(url)).find('weather') - bot.reply(format.encode('utf-8') % args) + if w.find('problem_cause') is not None: + return "Couldn't fetch weather data for '%s', try using a zip or " \ + "postal code." % input.inp + + info = dict((e.tag, e.get('data')) for e in w.find('current_conditions')) + info['city'] = w.find('forecast_information/city').get('data') + info['high'] = w.find('forecast_conditions/high').get('data') + info['low'] = w.find('forecast_conditions/low').get('data') + + return '%(city)s: %(condition)s, %(temp_f)sF/%(temp_c)sC (H:%(high)sF, ' \ + 'L:%(low)sF), %(humidity)s, %(wind_condition)s.' % info