diff --git a/bot.py b/bot.py index be435b7..a6c05d4 100755 --- a/bot.py +++ b/bot.py @@ -20,11 +20,8 @@ os.chdir(sys.path[0]) #do stuff relative to the installation directory class Bot(object): def __init__(self): - self.plugins = {} self.commands = [] # fn, name, func, args - self.listens = {} self.filters = [] #fn, name, func - self.daemons = {} bot = Bot() bot.nickname = nickname @@ -33,41 +30,49 @@ 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) - 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 -if bot.daemons: - print 'Running daemons' - for daemon in bot.daemons.itervalues(): - thread.start_new_thread(daemon, ()) +def reload_plugins(mtime=[0]): + new_mtime = os.stat('plugins') + if new_mtime == mtime[0]: + return + + bot.commands = [] + bot.filters = [] + + + for filename in glob.glob("plugins/*.py"): + shortname = os.path.splitext(os.path.basename(filename))[0] + try: + plugin = imp.load_source(shortname, filename) + 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 + + mtime[0] = new_mtime + +reload_plugins() print 'Connecting to IRC' bot.irc = irc.irc(network, nickname) @@ -88,27 +93,32 @@ class Input(object): self.host = host self.paraml = paraml self.msg = msg + self.doreply = True class FakeBot(object): - def __init__(self, bot, input, func): + def __init__(self, bot, input, fn, func): self.bot = bot self.input = input self.msg = bot.irc.msg self.cmd = bot.irc.cmd + self.fn = func self.func = func if input.command == "PRIVMSG": self.chan = input.paraml[0] def say(self, msg): - self.bot.irc.msg(input.paraml[0], msg) + self.bot.irc.msg(self.input.paraml[0], msg) def reply(self, msg): - self.say(input.nick + ': ' + msg) + self.say(self.input.nick + ': ' + msg) def run(self): out = self.func(self, self.input) if out is not None: - self.reply(unicode(out)) + if self.doreply: + self.reply(unicode(out)) + else: + self.say(unicode(out)) print bot.commands print bot.filters @@ -116,6 +126,7 @@ print bot.filters while True: try: out = bot.irc.out.get(timeout=1) + reload_plugins() for fn, name, func, args in bot.commands: input = Input(*out) for fn, nam, filt in bot.filters: @@ -123,7 +134,7 @@ while True: if input == None: break if input == None: - break - thread.start_new_thread(FakeBot(bot, input, func).run, ()) + continue + thread.start_new_thread(FakeBot(bot, input, fn, func).run, ()) except Queue.Empty: pass diff --git a/irc.py b/irc.py index b864236..aad2ddb 100644 --- a/irc.py +++ b/irc.py @@ -72,6 +72,7 @@ class irc(object): def parse_loop(self): while True: msg = self.conn.iqueue.get() + print '>>>', msg if msg.startswith(":"): #has a prefix prefix, command, params = irc_prefix_re.match(msg).groups() else: diff --git a/plugins/dice.py b/plugins/dice.py new file mode 100755 index 0000000..560dd47 --- /dev/null +++ b/plugins/dice.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +""" +dice.py written by Scaevolus 2008, updated 2009 +simulates dicerolls +""" +import re +import random + +whitespace_re = re.compile(r'\s+') +valid_diceroll_re = re.compile(r'^[+-]?(\d+|\d*d\d+)([+-](\d+|\d*d\d+))*$') +sign_re = re.compile(r'[+-]?(?:\d*d)?\d+') +split_re = re.compile(r'([\d+-]*)d?(\d*)') + +def nrolls(count, n): + "roll an n-sided die count times" + if n < 2: #it's a coin + if count < 5000: + return sum(random.randint(0,1) for x in xrange(count)) + else: #fake it + return int(random.normalvariate(.5*count,(.75*count)**.5)) + else: + if count < 5000: + return sum(random.randint(1,n) for x in xrange(count)) + else: #fake it + return int(random.normalvariate(.5*(1+n)*count, + (((n+1)*(2*n+1)/6.-(.5*(1+n))**2)*count)**.5)) + +#command +def dice(bot, input): + ".dice - simulates dicerolls, e.g. .dice 2d20-d5+4 roll 2 " \ + "D20s, subtract 1D5, add 4" + if not input.inp.strip(): + return dice.__doc__ + + spec = whitespace_re.sub('', input.inp) + if not valid_diceroll_re.match(spec): + return "Invalid diceroll" + sum = 0 + groups = sign_re.findall(spec) + for roll in groups: + count, side = split_re.match(roll).groups() + if side == "": + sum += int(count) + else: + count = int(count) if count not in " +-" else 1 + side = int(side) + try: + if count > 0: + sum += nrolls(count, side) + else: + sum -= nrolls(abs(count), side) + except OverflowError: + return "Thanks for overflowing a float, jerk >:[" + + return str(sum) diff --git a/plugins/dice.pyc b/plugins/dice.pyc new file mode 100644 index 0000000..fd8f62f Binary files /dev/null and b/plugins/dice.pyc differ diff --git a/plugins/filter.pyc b/plugins/filter.pyc new file mode 100644 index 0000000..efc2250 Binary files /dev/null and b/plugins/filter.pyc differ diff --git a/plugins/twitter.py b/plugins/twitter.py new file mode 100644 index 0000000..2b83896 --- /dev/null +++ b/plugins/twitter.py @@ -0,0 +1,23 @@ +""" +twitter.py: written by Scaevolus 2009 +retrieves most recent tweets +""" + +import urllib +from xml.etree import ElementTree + +#command +def twitter(bot, input): + '''.twitter - gets most recent tweet from ''' + if not input.inp.strip(): + return twitter.__doc__ + + url = "http://twitter.com/statuses/user_timeline/%s.xml?count=1" \ + % urllib.quote(input.inp) + tweet = ElementTree.parse(urllib.urlopen(url)) + + if tweet.find('error') is not None: + return "can't find that username" + + tweet = tweet.find('status') + bot.say(': '.join(tweet.find(x).text for x in 'user/name created_at text'.split())) diff --git a/plugins/twitter.pyc b/plugins/twitter.pyc new file mode 100644 index 0000000..3960936 Binary files /dev/null and b/plugins/twitter.pyc differ diff --git a/plugins/weather.py b/plugins/weather.py index 509d0ad..6d2f115 100755 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -6,7 +6,7 @@ from xml.etree import ElementTree #command: weather def weather(bot, input): - '''.weather -- queries the google weather API for weather data''' + ".weather -- queries the google weather API for weather data" if not input.inp.strip(): # blank line return "welp" diff --git a/plugins/weather.pyc b/plugins/weather.pyc new file mode 100644 index 0000000..b073fc9 Binary files /dev/null and b/plugins/weather.pyc differ