diff --git a/bot.py b/bot.py index e430c78..45d5f42 100755 --- a/bot.py +++ b/bot.py @@ -11,17 +11,18 @@ import imp import re import thread import Queue -import copy +import collections import irc import yaml -os.chdir(sys.path[0]) #do stuff relative to the installation directory +os.chdir(sys.path[0]) #do stuff relative to the installation directory +sys.path += ['plugins'] # so 'import hook' works without duplication class Bot(object): def __init__(self, nick, channel, network): - self.commands = [] # fn, name, func, args - self.filters = [] #fn, name, func + self.commands = [] # (fn, funcname, name, line), func, args + self.filters = [] #(fn, funcname, line), func self.nick = nick self.channel = channel self.network = network @@ -30,55 +31,43 @@ bot = Bot(nick, channel, network) print 'Loading plugins' typs = '|'.join('command filter event'.split()) -magic_re = re.compile(r'^\s*#(%s)(?:: +(\S+) *(\S.*)?)?\s*$' % typs) +magic_re = re.compile(r'^\s*#!(%s)(?:: +(\S+) *(\S.*)?)?\s*$' % typs) def reload_plugins(mtime=[0]): new_mtime = os.stat('plugins') if new_mtime == mtime[0]: return - bot.commands = [] - bot.filters = [] - + bot.plugs = collections.defaultdict(lambda: []) 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)) - elif typ == 'filter': - bot.filters.append((filename, nam, func)) - elif typ == 'event': - args = {'name': nam, 'prefix':False, - 'events': [nam] + rest.split()} - bot.commands.append((filename, nam, func, args)) + for thing in dir(plugin): + thing = getattr(plugin, thing) + if hasattr(thing, '_skybot_hook'): + for type, data in thing._skybot_hook: + bot.plugs[type] += [data] except Exception, e: - print e + print ' error:', e mtime[0] = new_mtime reload_plugins() +print ' plugin listing:' +for type, plugs in sorted(bot.plugs.iteritems()): + print ' %s:' % type + for plug in plugs: + out = ' %s:%s:%s' % (plug[0]) + print out, + if len(plug) == 3 and 'hook' in plug[2]: + print '%s%s' % (' ' * (35 - len(out)), plug[2]['hook']) + else: + print +print + print 'Connecting to IRC' bot.irc = irc.irc(network, nick) bot.irc.join(channel) @@ -100,13 +89,12 @@ class Input(object): self.msg = msg class FakeBot(object): - def __init__(self, bot, input, fn, func): + def __init__(self, bot, input, func): self.bot = bot self.input = input self.msg = bot.irc.msg self.cmd = bot.irc.cmd self.join = bot.irc.join - self.fn = func self.func = func self.doreply = True if input.command == "PRIVMSG": @@ -130,22 +118,19 @@ class FakeBot(object): else: self.say(unicode(out)) -print bot.commands -print bot.filters - while True: try: out = bot.irc.out.get(timeout=1) reload_plugins() - for fn, name, func, args in bot.commands: + for csig, func, args in (bot.plugs['command'] + bot.plugs['event']): input = Input(*out) - for fn, nam, filt in bot.filters: - input = filt(bot, func, args, input) + for fsig, sieve in bot.plugs['sieve']: + input = sieve(bot, input, func, args) if input == None: break if input == None: continue print '<<<', input.raw - thread.start_new_thread(FakeBot(bot, input, fn, func).run, ()) + thread.start_new_thread(FakeBot(bot, input, func).run, ()) except Queue.Empty: pass diff --git a/plugins/bf.py b/plugins/bf.py index d921c9f..6cb34f8 100644 --- a/plugins/bf.py +++ b/plugins/bf.py @@ -4,10 +4,12 @@ http://brainfuck.sourceforge.net/brain.py''' import re import random +import hook + BUFFER_SIZE = 5000 MAX_STEPS = 1000000 -#command +@hook.command def bf(input): """Runs a Brainfuck program.""" @@ -50,7 +52,8 @@ def bf(input): if mp > rightmost: rightmost = mp if mp >= len(memory): - memory.extend([0]*BUFFER_SIZE) # no restriction on memory growth! + # no restriction on memory growth! + memory.extend([0]*BUFFER_SIZE) elif c == '<': mp = mp - 1 % len(memory) elif c == '.': diff --git a/plugins/dice.py b/plugins/dice.py index abbfcbe..9cd2aa0 100755 --- a/plugins/dice.py +++ b/plugins/dice.py @@ -6,6 +6,8 @@ simulates dicerolls import re import random +import hook + 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+') @@ -25,7 +27,7 @@ def nrolls(count, n): return int(random.normalvariate(.5*(1+n)*count, (((n+1)*(2*n+1)/6.-(.5*(1+n))**2)*count)**.5)) -#command +@hook.command def dice(input): ".dice - simulates dicerolls, e.g. .dice 2d20-d5+4 roll 2 " \ "D20s, subtract 1D5, add 4" diff --git a/plugins/filter.py b/plugins/filter.py index bac8703..e75e487 100644 --- a/plugins/filter.py +++ b/plugins/filter.py @@ -1,7 +1,9 @@ import re -#filter -def filter_suite(bot, func, args, input): +import hook + +@hook.sieve +def filter_suite(bot, input, func, args): args.setdefault('events', ['PRIVMSG']) if input.command not in args['events']: @@ -15,6 +17,9 @@ def filter_suite(bot, func, args, input): if args['prefix']: hook = bot.commandprefix + args['hook'] + if input.command == 'INVITE': + print func, hook + input.re = re.match(hook, input.msg) if input.re is None: return None diff --git a/plugins/hash.py b/plugins/hash.py index a7916e5..bdbfc08 100644 --- a/plugins/hash.py +++ b/plugins/hash.py @@ -1,14 +1,16 @@ import hashlib -#command +import hook + +@hook.command def md5(input): return hashlib.md5(input).hexdigest() -#command +@hook.command def sha1(input): return hashlib.sha1(input).hexdigest() -#command +@hook.command def hash(input): return ', '.join(x + ": " + getattr(hashlib, x)(input).hexdigest() for x in 'md5 sha1 sha256'.split()) diff --git a/plugins/hook.py b/plugins/hook.py new file mode 100644 index 0000000..c99f045 --- /dev/null +++ b/plugins/hook.py @@ -0,0 +1,58 @@ +def _isfunc(x): + if type(x) == type(_isfunc): + return True + return False + +def _hook_add(func, add): + if not hasattr(func, '_skybot_hook'): + func._skybot_hook = [] + func._skybot_hook.append(add) + +def _make_sig(f): + return f.func_code.co_filename, f.func_name, f.func_code.co_firstlineno + +def sieve(func): + if func.func_code.co_argcount != 4: + raise ValueError, \ + 'sieves must take 4 arguments: (bot, input, func, args)' + _hook_add(func, ['sieve', (_make_sig(func), func)]) + return func + +def command(func, hook=None, **kwargs): + args = {} + def command_wrapper(func): + if func.func_code.co_argcount not in (1, 2): + raise ValueError, \ + 'commands must take 1 or 2 arguments: (inp) or (bot, input)' + args.setdefault('name', func.func_name) + args.setdefault('hook', args['name'] + r'\s*(.*)') + _hook_add(func, ['command', (_make_sig(func), func, args)]) + return func + + if hook is not None or kwargs or not _isfunc(func): + if func is not None: + args['name'] = func + if hook is not None: + args['hook'] = hook + args.update(kwargs) + return command_wrapper + else: + return command_wrapper(func) + +def event(arg): + args = {} + def event_wrapper(func): + if func.func_code.co_argcount != 2: + raise ValueError, \ + 'events must take 2 arguments: (bot, input)' + args['name'] = func.func_name + args['prefix'] = False + args.setdefault('events', '*') + _hook_add(func, ['event', (_make_sig(func), func, args)]) + return func + + if _isfunc(arg): + return event_wrapper(arg) + else: + args['events'] = arg.split() + return event_wrapper diff --git a/plugins/misc.py b/plugins/misc.py index 6b18de3..9487b10 100644 --- a/plugins/misc.py +++ b/plugins/misc.py @@ -1,8 +1,13 @@ -#event: KICK INVITE +import hook + +@hook.event('KICK INVITE') def rejoin(bot, input): + print input.command, input.inp + if input.command == 'KICK': if input.paraml[1] == bot.bot.nick: - bot.join(input.paraml[0]) + if input.paraml[0] == bot.bot.channel: + bot.join(input.paraml[0]) if input.command == 'INVITE': bot.join(input.inp) diff --git a/plugins/pyexec.py b/plugins/pyexec.py index 0280625..4e2f120 100644 --- a/plugins/pyexec.py +++ b/plugins/pyexec.py @@ -1,9 +1,11 @@ import urllib import re +import hook + re_lineends = re.compile(r'[\r\n]*') -#command +@hook.command def py(input): res = urllib.urlopen("http://eval.appspot.com/eval?statement=%s" % urllib.quote(input.strip(),safe='')).readlines() diff --git a/plugins/twitter.py b/plugins/twitter.py index 218e9af..4a511bf 100644 --- a/plugins/twitter.py +++ b/plugins/twitter.py @@ -6,7 +6,9 @@ retrieves most recent tweets import urllib from xml.etree import ElementTree -#command +import hook + +@hook.command def twitter(bot, input): '''.twitter - gets most recent tweet from ''' if not input.inp.strip(): diff --git a/plugins/weather.py b/plugins/weather.py index 6d2f115..18e99d4 100755 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -4,7 +4,9 @@ import urllib from xml.etree import ElementTree -#command: weather +import hook + +@hook.command def weather(bot, input): ".weather -- queries the google weather API for weather data" diff --git a/plugins/youtube.py b/plugins/youtube.py new file mode 100644 index 0000000..cb1bb23 --- /dev/null +++ b/plugins/youtube.py @@ -0,0 +1,17 @@ +import urllib +from xml.etree import ElementTree + +def ytdata(id): + url = 'http://gdata.youtube.com/feeds/api/videos/' + idt + print url + data = urllib.urlopen(url).read() + if len(data) < 50: # it's some error message; ignore + print data + return + global x + def + x = ElementTree.XML(data) + + +if __name__ == '__main__': + ytdata('the0KZLEacs')