Wed, 05 Nov 2014 00:19:15 +0200
- revamped commands, added a much more modular system. not everything migrated yet
.hgignore | file | annotate | diff | comparison | revisions | |
cmd_admin.py | file | annotate | diff | comparison | revisions | |
cmd_config.py | file | annotate | diff | comparison | revisions | |
cmd_idgames.py | file | annotate | diff | comparison | revisions | |
cobalt.py | file | annotate | diff | comparison | revisions | |
commandhandler.py | file | annotate | diff | comparison | revisions |
--- a/.hgignore Tue Nov 04 20:35:25 2014 +0200 +++ b/.hgignore Wed Nov 05 00:19:15 2014 +0200 @@ -1,3 +1,5 @@ +syntax:glob cobalt.json untracked commits.txt +*.pyc
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd_admin.py Wed Nov 05 00:19:15 2014 +0200 @@ -0,0 +1,59 @@ +from commandhandler import command_error +import hgapi + +ModuleData = { + 'commands': + [ + { + 'name': 'raw', + 'description': 'Sends a raw message to the server', + 'args': '<message...>', + 'level': 'admin', + }, + + { + 'name': 'msg', + 'description': 'Sends a message to someone', + 'args': '<recipient> <message...>', + 'level': 'admin', + }, + + { + 'name': 'restart', + 'description': 'Restarts the bot', + 'args': None, + 'level': 'admin', + }, + + { + 'name': 'update', + 'description': 'Checks for updates on the bot', + 'args': None, + 'level': 'admin' + } + ] +} + +def cmd_raw (bot, args, **rest): + bot.write (args['message']) + +def cmd_msg (bot, args, **rest): + bot.privmsg (args['recipient'], args['message']) + +def cmd_restart (bot, **rest): + bot.restart() + +def cmd_update (bot, replyto, **rest): + try: + repo = hgapi.Repo ('.') + r1 = repo.hg_id() + repo.hg_pull() + repo.hg_update('tip', True) + r2 = repo.hg_id() + if r1 != r2: + bot.privmsg (replyto, 'Updated to %s, restarting...' % r2) + excepterm('') + else: + bot.privmsg (replyto, 'Up to date at %s.' % r2) + except hgapi.HgException as e: + command_error ('Update failed: %s' % str (e))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd_config.py Wed Nov 05 00:19:15 2014 +0200 @@ -0,0 +1,102 @@ +from commandhandler import command_error +import hgapi + +ModuleData = { + 'commands': + [ + { + 'name': 'addchan', + 'description': 'Adds a channel to config', + 'args': '<channel>', + 'level': 'admin', + }, + + { + 'name': 'delchan', + 'description': 'Deletes a channel from config', + 'args': '<channel>', + 'level': 'admin', + }, + + { + 'name': 'chanattr', + 'description': 'Get or set a channel-specific attribute', + 'args': '<channel> <key> [value...]', + 'level': 'admin', + }, + ] +} + +def cmd_addchan (bot, args, **rest): + for channel in bot.channels: + if channel['name'].upper() == args['channel'].upper(): + command_error ('I already know of %s!' % args['channel']) + #done + + chan = {} + chan['name'] = args['channel'] + chan['logchannel'] = False + bot.channels.append (chan) + bot.write ('JOIN ' + chan['name']) + bot.save_config() +#enddef + +def cmd_delchan (bot, args, **rest): + for channel in bot.channels: + if channel['name'].upper() == args['channel'].upper(): + break; + else: + command_error ('unknown channel ' + args['channel']) + + bot.channels.remove (channel) + bot.write ('PART ' + args['channel']) + bot.save_config() +#enddef + +def bool_from_string (value): + if value != 'true' and value != 'false': + command_error ('expected true or false for value') + + return True if value == 'true' else False +#enddef + +def cmd_chanattr (bot, args, replyto, **rest): + for channel in bot.channels: + if channel['name'] == args['channel']: + break + else: + command_error ('I don\'t know of a channel named ' + args['channel']) + + key = args['key'] + value = args['value'] + + if value == '': + try: + bot.privmsg (replyto, '%s = %s' % (key, channel[key])) + except KeyError: + bot.privmsg (replyto, 'attribute %s is not set' % key) + return + #fi + + if key == 'name': + if replyto == channel['name']: + replyto = value + + bot.write ('PART ' + channel['name']) + channel['name'] = value + bot.write ('JOIN ' + channel['name'] + ' ' + (channel['password'] if hasattr (channel, 'password') else '')) + elif key == 'password': + channel['password'] = value + elif key == 'btannounce': + channel['btannounce'] = bool_from_string (value) + elif key == 'btprivate': + channel['btprivate'] = bool_from_string (value) + elif key == 'logchannel': + channel['logchannel'] = bool_from_string (value) + else: + command_error ('unknown key ' + key) + #fi + + bot.privmsg (replyto, '%s is now %s' % (key, channel[key])) + bot.save_config() +#enddef \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd_idgames.py Wed Nov 05 00:19:15 2014 +0200 @@ -0,0 +1,54 @@ +from commandhandler import command_error +import hgapi +import urllib +import urllib2 +import json + +ModuleData = { + 'commands': + [ + { + 'name': 'idgames', + 'description': 'Searches doomworld.com/idgames for wads', + 'args': '<wad>', + 'level': 'normal', + }, + ] +} + +g_idgamesSearchURL = 'http://www.doomworld.com/idgames/api/api.php?action=search&query=%s&type=title&sort=date&out=json' + +def cmd_idgames (bot, args, replyto, **rest): + try: + url = g_idgamesSearchURL % urllib.quote (args['wad']) + response = urllib2.urlopen (url).read() + data = json.loads (response) + + if 'content' in data and 'file' in data['content']: + if type (data['content']['file']) is list: + files = data['content']['file'] + else: + files = [data['content']['file']] + + i = 0 + + for filedata in files: + if i >= 5: + break + + bot.privmsg (replyto, '- %s: \'%s\' by \'%s\', rating: %s: %s' % \ + (filedata['filename'], filedata['title'], filedata['author'], filedata['rating'], filedata['url'])) + + i += 1 + #done + bot.privmsg (replyto, "(%d / %d results posted)" % (i, len(files))) + elif 'warning' in data and 'message' in data['warning']: + command_error (data['warning']['message']) + elif 'error' in data and 'message' in data['error']: + command_error (data['error']['message']) + else: + command_error ("Incomplete JSON response from doomworld.com/idgames") + except Exception as e: + command_error ('search failed: %s' % str (e)) + #tried +#enddef \ No newline at end of file
--- a/cobalt.py Tue Nov 04 20:35:25 2014 +0200 +++ b/cobalt.py Wed Nov 05 00:19:15 2014 +0200 @@ -41,6 +41,9 @@ import suds import math from datetime import datetime +import commandhandler as CommandHandler + +CommandHandler.init_data() try: uid = os.geteuid() @@ -61,7 +64,6 @@ g_admins = g_config['admins'] g_mynick = g_config['nickname'] -g_idgamesSearchURL = 'http://www.doomworld.com/idgames/api/api.php?action=search&query=%s&type=title&sort=date&out=json' g_BotActive = False g_needCommitsTxtRebuild = True @@ -97,11 +99,6 @@ global btannounce_timeout btannounce_timeout = time.time() + (cfg ('btlatest_checkinterval', 5) * 60) -def bool_from_string (value): - if value != 'true' and value != 'false': - raise logical_exception ('expected true or false for value') - return True if value == 'true' else False - if suds_active: try: sys.stdout.write ('Retrieving latest tracker ticket... ') @@ -327,6 +324,8 @@ ' Retrieves and processes commits for zandronum repositories ' ' Ensure both repositories are OK before using this! ' def process_zan_repo_updates (repo_name): + return + global repocheck_timeout global suds_client global g_config @@ -739,7 +738,7 @@ command = stuff[0] args = stuff[1:] try: - self.handle_command (sender, user, host, replyto, command, args) + self.handle_command (sender, user, host, replyto, command, args, message) except logical_exception as e: for line in e.value.split ('\n'): if len(line) > 0: @@ -801,20 +800,36 @@ #fi #enddef + def save_config (self): + save_config() + + def is_admin (self, ident, host): + return ("%s@%s" % (ident, host)) in g_admins + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Process an IRC command # - def handle_command (self, sender, ident, host, replyto, command, args): - if command == "raw": - check_admin (sender, ident, host, command) - self.write (" ".join (args)) - elif command == "msg": - check_admin (sender, ident, host, command) - if len(args) < 2: - raise logical_exception ("usage: .%s <target> <message...>" % command) - self.privmsg (args[0], " ".join (args[1:])) - elif command == 'ticket': + def handle_command (self, sender, ident, host, replyto, command, args, message): + kvargs = {'sender': sender, 'ident': ident, 'host': host, 'replyto': replyto, 'cmdname': command, 'message': message} + + try: + result = CommandHandler.call_command (self, **kvargs) + + if result: + return + else: + print 'CommandHandler.call_command returned false' + except CommandHandler.CommandError as e: + lines = str (e).split ('\n') + self.privmsg (replyto, 'error: %s' % lines[0]) + + for line in lines[1:]: + self.privmsg (replyto, ' ' + line) + return + #tried + + if command == 'ticket': if len(args) != 1: raise logical_exception ("usage: .%s <ticket>" % command) self.get_ticket_data (replyto, args[0], True) @@ -823,196 +838,6 @@ if len(args) != 1: raise logical_exception ("usage: .%s <ticket>" % command) self.announce_ticket (bt_getissue (args[0])) - elif command == 'multierror': - raise logical_exception ('a\nb\nc\n') - elif command == 'idgames': - try: - if len(args) < 1: - raise logical_exception ('usage: .%s <keywords>' % command) - - url = g_idgamesSearchURL % urllib.quote (" ".join (args[0:])) - response = urllib2.urlopen (url).read() - data = json.loads (response) - - if 'content' in data and 'file' in data['content']: - if type (data['content']['file']) is list: - files = data['content']['file'] - else: - files = [data['content']['file']] - - i = 0 - for filedata in files: - if i >= 5: - break - - self.privmsg (replyto, '- %s: \'%s\' by \'%s\', rating: %s: %s' % \ - (filedata['filename'], filedata['title'], filedata['author'], filedata['rating'], filedata['url'])) - - i += 1 - self.privmsg (replyto, "(%d / %d results posted)" % (i, len(files))) - elif 'warning' in data and 'message' in data['warning']: - raise logical_exception (data['warning']['message']) - elif 'error' in data and 'message' in data['error']: - raise logical_exception (data['error']['message']) - else: - raise logical_exception ("Incomplete JSON response from doomworld.com/idgames") - except logical_exception as e: - raise e - except Exception as e: - raise logical_exception ('Search failed: %s' % `e`) - elif command == 'restart': - check_admin (sender, ident, host, command) - excepterm('') - elif command == 'update': - check_admin (sender, ident, host, command) - - try: - repo = hgapi.Repo ('.') - r1 = repo.hg_id() - repo.hg_pull() - repo.hg_update('tip', True) - r2 = repo.hg_id() - if r1 != r2: - self.privmsg (replyto, 'Updated to %s, restarting...' % r2) - excepterm('') - else: - self.privmsg (replyto, 'Up to date at %s.' % r2) - except hgapi.HgException as e: - raise logical_exception ('Update failed: %s' % str (e)) - elif command == 'addchan': - check_admin (sender, ident, host, command) - if len(args) != 1: - raise logical_exception ("usage: .%s <channel>" % command) - - for channel in self.channels: - if channel['name'].upper() == args[0].upper(): - raise logical_exception ('I already know of %s!' % args[0]) - - chan = {} - chan['name'] = args[0] - chan['logchannel'] = False - self.channels.append (chan) - self.write ('JOIN ' + chan['name']) - save_config() - elif command == 'delchan': - check_admin (sender, ident, host, command) - if len(args) != 1: - raise logical_exception ("usage: .%s <channel>" % command) - - for channel in self.channels: - if channel['name'].upper() == args[0].upper(): - break; - else: - raise logical_exception ('unknown channel ' + args[0]) - - self.channels.remove (channel) - self.write ('PART ' + args[0]) - save_config() - elif command == 'chanattr': - check_admin (sender, ident, host, command) - - if len(args) < 1: - raise logical_exception ("usage: .%s <attribute> [value...]" % command) - - for channel in self.channels: - if channel['name'] == replyto: - break - else: - raise logical_exception ('I don\'t know of a channel named ' + replyto) - - key = args[0] - - if len(args) < 2: - try: - self.privmsg (replyto, '%s = %s' % (key, channel[key])) - except KeyError: - self.privmsg (replyto, 'attribute %s is not set' % key) - else: - value = " ".join (args[1:]) - if key == 'name': - if replyto == channel['name']: - replyto = value - - self.write ('PART ' + channel['name']) - channel['name'] = value - self.write ('JOIN ' + channel['name'] + ' ' + (channel['password'] if hasattr (channel, 'password') else '')) - elif key == 'password': - channel['password'] = value - elif key == 'btannounce': - channel['btannounce'] = bool_from_string (value) - elif key == 'btprivate': - channel['btprivate'] = bool_from_string (value) - elif key == 'logchannel': - channel['logchannel'] = bool_from_string (value) - else: - raise logical_exception ('unknown key ' + key) - - self.privmsg (replyto, '%s is now %s' % (key, channel[key])) - - save_config() - elif command == 'devemail': - check_admin (sender, ident, host, command) - - if len(args) < 2: - raise logical_exception ("usage: .%s <user> <email>" % command) - #fi - - if not 'developer_emails' in g_config: - g_config['developer_emails'] = {} - #fi - - user = ' '.join (args[0:-1]) - - if args[0] in g_config['developer_emails']: - g_config['developer_emails'][user].append (args[-1]) - else: - g_config['developer_emails'][user] = [args[-1]] - #fi - - self.privmsg (replyto, 'Developer emails for %s are now %s' % - (user, ', '.join (g_config['developer_emails'][user]))) - save_config() - elif command == 'deldevemail': - check_admin (sender, ident, host, command) - - if len(args) < 2: - raise logical_exception ("usage: .%s <user> <email>" % command) - #fi - - if not 'developer_emails' in g_config: - g_config['developer_emails'] = {} - #fi - - user = ' '.join (args[0:-1]) - - if user in g_config['developer_emails']: - try: - g_config['developer_emails'][user].remove (args[-1]) - except: - pass - #tried - - if len (g_config['developer_emails'][user]) == 0: - g_config['developer_emails'].pop (user) - self.privmsg (replyto, 'No more developer emails for %s' % user) - else: - self.privmsg (replyto, 'Developer emails for %s are now %s' % - (user, ', '.join (g_config['developer_emails'][user]))) - #fi - save_config() - else: - self.privmsg (replyto, 'There is no developer \'%s\'' % user) - #fi - elif command == 'listdevemails': - check_admin (sender, ident, host, command) - - if 'developer_emails' in g_config: - for dev, emails in g_config['developer_emails'].iteritems(): - self.privmsg (replyto, 'Emails for %s: %s' % (dev, ', '.join (emails))) - #done - else: - self.privmsg (replyto, 'No dev emails.') - #fi elif command == 'checkhg': check_admin (sender, ident, host, command) global repocheck_timeout @@ -1213,6 +1038,9 @@ def handle_error(self): excepterm (traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)) + def restart(self): + excepterm('') + def privmsg (self, channel, msg): self.write ("PRIVMSG %s :%s" % (channel, msg))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/commandhandler.py Wed Nov 05 00:19:15 2014 +0200 @@ -0,0 +1,156 @@ +import os +import re + +CommandModules = {} +Commands = {} + +class CommandError (Exception): + def __init__ (self, value): + self.value = value + def __str__ (self): + return self.value + +# +# init_data() +# +# Initializes command module data +# +def init_data(): + global Commands + global CommandModules + files = os.listdir ('.') + + for fn in files: + if fn[0:4] != 'cmd_' or fn[-3:] != '.py': + continue + + fn = fn[0:-3] + globals = {} + module = __import__ (fn) + CommandModules[fn] = module + + for cmd in module.ModuleData['commands']: + if cmd['args'] == None: + cmd['args'] = '' + + cmd['module'] = module + cmd['regex'] = make_regex (cmd['args']) + cmd['argnames'] = [] + Commands[cmd['name']] = cmd + + for argname in cmd['args'].split (' '): + argname = argname[1:-1] + + if argname[-3:] == '...': + argname = argname[0:-3] + + if argname == '': + continue + + cmd['argnames'].append (argname) + #done + #done + + print "Loaded command module %s" % fn + #done + + print 'Loaded %d commands in %d modules' % (len (Commands), len (CommandModules)) +#enddef + +# +# command_error (message) +# +# Raises a command error +# +def command_error (message): + raise CommandError (message) + +# +# call_command (bot, message, cmdname, **kvargs) +# +# Calls a cobalt command +# +def call_command (bot, message, cmdname, **kvargs): + try: + cmd = Commands[cmdname] + except KeyError: + return False + + if cmd['level'] == 'admin' and not bot.is_admin (kvargs['ident'], kvargs['host']): + command_error ("%s requires admin access" % cmdname) + + match = re.compile (cmd['regex']).match (message) + + if match == None: + # didn't match + command_error ('invalid arguments\nusage: %s %s' % (cmd['name'], cmd['args'])) + #fi + + i = 1 + args = {} + + for argname in cmd['argnames']: + args[argname] = match.group (i) + i += 1 + #done + + getattr (cmd['module'], "cmd_%s" % cmdname) (bot=bot, cmdname=cmdname, args=args, **kvargs) + return True + +# +# make_regex +# +# Takes the argument list and returns a corresponding regular expression +# +def make_regex (arglist): + if arglist == None: + return '^.+$' + + gotoptional = False + gotvariadic = False + regex = '' + + for arg in arglist.split (' '): + if gotvariadic: + raise CommandError ('variadic argument is not last') + + if arg == '': + continue + + if arg[0] == '[' and arg[-1] == ']': + arg = arg[1:-1] + gotoptional = True + elif arg[0] == '<' and arg[-1] == '>': + if gotoptional: + raise CommandError ('mandatory argument after optional one') + + arg = arg[1:-1] + else: + raise CommandError ('badly formed argument list') + #fi + + if arg[-3:] == '...': + gotvariadic = True + arg = arg[0:-3] + #fi + + if gotoptional == False: + regex += '\s+' + else: + regex += '\s*' + + if gotoptional: + if gotvariadic: + regex += r'(.*)' + else: + regex += r'([^ ]*)' + else: + if gotvariadic: + regex += r'(.+)' + else: + regex += r'([^ ]+)' + #fi + #done + + return '^[^ ]+%s$' % regex +#enddef \ No newline at end of file