Sun, 09 Nov 2014 19:13:08 +0200
- modularized the configuration and made it more systematic
.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 | |
configfile.py | file | annotate | diff | comparison | revisions | |
mod_admin.py | file | annotate | diff | comparison | revisions | |
mod_config.py | file | annotate | diff | comparison | revisions | |
mod_idgames.py | file | annotate | diff | comparison | revisions | |
modulecore.py | file | annotate | diff | comparison | revisions |
--- a/.hgignore Wed Nov 05 01:38:08 2014 +0200 +++ b/.hgignore Sun Nov 09 19:13:08 2014 +0200 @@ -1,5 +1,6 @@ syntax:glob -cobalt.json +cobalt*.json* untracked commits.txt *.pyc +zandronum*
--- a/cmd_admin.py Wed Nov 05 01:38:08 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +0,0 @@ -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) - bot.restart() - else: - bot.privmsg (replyto, 'Up to date at %s.' % r2) - except hgapi.HgException as e: - command_error ('Update failed: %s' % str (e))
--- a/cmd_config.py Wed Nov 05 01:38:08 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +0,0 @@ -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
--- a/cmd_idgames.py Wed Nov 05 01:38:08 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -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 Wed Nov 05 01:38:08 2014 +0200 +++ b/cobalt.py Sun Nov 09 19:13:08 2014 +0200 @@ -33,17 +33,19 @@ import sys import traceback import re -import json import urllib import urllib2 import hgapi import os import suds import math +import json from datetime import datetime -import commandhandler as CommandHandler +import modulecore as ModuleCore +import configfile +from configfile import Config -CommandHandler.init_data() +ModuleCore.init_data() try: uid = os.geteuid() @@ -53,17 +55,9 @@ if uid == 0 and raw_input ('Do you seriously want to run cobalt as root? [y/N] ') != 'y': quit() -print 'Loading configuration...' -try: - with open ('cobalt.json', 'r') as fp: - g_config = json.loads (fp.read()) -except IOError as e: - print 'couldn\'t open cobalt.json: %s' % e - quit() - -g_admins = g_config['admins'] -g_mynick = g_config['nickname'] - +configfile.init() +g_admins = Config.get_value ('admins', default=[]) +g_mynick = Config.get_value ('nickname', default='cobalt') g_BotActive = False g_needCommitsTxtRebuild = True @@ -84,35 +78,28 @@ btannounce_active = False btannounce_timeout = 0 -def save_config(): - with open ('cobalt.json', 'w') as fp: - json.dump (g_config, fp, sort_keys = True, indent = 4) - -def cfg (key, default): - if not hasattr (g_config, key): - g_config[key] = default - save_config() - return default - return g_config[key] - def bt_updatechecktimeout(): global btannounce_timeout - btannounce_timeout = time.time() + (cfg ('btlatest_checkinterval', 5) * 60) + btannounce_timeout = time.time() + (Config.get_node ('bt').get_value ('checkinterval', default=5) * 60) + +def bt_credentials(): + bt = Config.get_node ('bt') + user = bt.get_value ('username', '') + password = bt.get_value ('password', '') + return [user, password] if suds_active: - try: - sys.stdout.write ('Retrieving latest tracker ticket... ') - btannounce_id = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config['trackerpassword'], 0) - btannounce_active = True - bt_updatechecktimeout() - print btannounce_id - except Exception as e: - pass + sys.stdout.write ('Retrieving latest tracker ticket... ') + user, password = bt_credentials() + btannounce_id = suds_client.service.mc_issue_get_biggest_id (user, password, 0) + btannounce_active = True + bt_updatechecktimeout() + print btannounce_id def bt_getissue(ticket): global suds_client - global g_config - return suds_client.service.mc_issue_get (g_config['trackeruser'], g_config['trackerpassword'], ticket) + user, password = bt_credentials() + return suds_client.service.mc_issue_get (user, password, ticket) def bt_checklatest(): global btannounce_timeout @@ -122,7 +109,8 @@ bt_updatechecktimeout() newid = btannounce_id try: - newid = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config['trackerpassword'], 0) + user, password = bt_credentials() + newid = suds_client.service.mc_issue_get_biggest_id (user, password, 0) except Exception as e: pass @@ -162,7 +150,7 @@ continue for channel in client.channels: - if channel['logchannel']: + if channel.get_value ('logchannel', default=False): client.write ("PRIVMSG %s :%s" % (channel['name'], line)) # @@ -275,7 +263,7 @@ check_repo_exists ('zandronum-sandbox-stable', 'crimsondusk') check_repo_exists ('zandronum-everything', '') -repocheck_timeout = {'zandronum':(time.time()) + 15, 'zandronum-stable':(time.time() + 15), 'zandronum-sandbox':(time.time()) + 15, 'zandronum-sandbox-stable':(time.time()) + 15} +repocheck_timeout = {(time.time()) + 15} def get_commit_data (zanrepo, rev, template): return zanrepo.hg_command ('log', '-l', '1', '-r', rev, '--template', template) @@ -318,7 +306,7 @@ #enddef def find_developer_by_email (commit_email): - for developer, emails in g_config['developer_emails'].iteritems(): + for developer, emails in Config.get_value ('developer_emails', default={}).iteritems(): for email in emails: if commit_email == email: return developer @@ -334,19 +322,24 @@ def process_zan_repo_updates (repo_name): global repocheck_timeout global suds_client - global g_config global g_clients + + hgns = Config.get_node ('hg') + + if not hgns.get_value ('track', default=True): + return usestable = repo_name == 'zandronum-stable' usesandbox = repo_name == 'zandronum-sandbox' or repo_name == 'zandronum-sandbox-stable' repo_owner = 'Torr_Samaho' if not usesandbox else 'crimsondusk' repo_url = 'https://bitbucket.org/%s/%s' % (repo_owner, repo_name) num_commits = 0 + btuser, btpassword = bt_credentials() - if time.time() < repocheck_timeout[repo_name]: + if time.time() < repocheck_timeout: return - repocheck_timeout[repo_name] = time.time() + (cfg ('hg_checkinterval', 15) * 60) + repocheck_timeout = time.time() + hgns.get_value ('checkinterval', default=15) * 60 zanrepo = hgapi.Repo (repo_name) commit_data = [] delimeter = '@@@@@@@@@@' @@ -484,8 +477,7 @@ commit_email = "" try: - ticket_data = suds_client.service.mc_issue_get (g_config['trackeruser'], - g_config['trackerpassword'], ticket_id) + ticket_data = suds_client.service.mc_issue_get (btuser, btpassword, ticket_id) except Exception as e: chanlog ('error while processing %s: %s' % (commit_node, `e`)) continue @@ -552,12 +544,12 @@ # Announce on IRC for irc_client in g_clients: - for channel in irc_client.cfg['channels']: - if 'btannounce' in channel and channel['btannounce'] == True: - irc_client.privmsg (channel['name'], + for channel in irc_client.channels: + if channel.get_value ('btannounce', default=True): + irc_client.privmsg (channel.get_value ('name'), "%s: commit %s fixes issue %d: %s" % (repo_name, commit_node, ticket_id, commit_message)) - irc_client.privmsg (channel['name'], + irc_client.privmsg (channel.get_value ('name'), "Read all about it here: " + irc_client.get_ticket_url (ticket_id)) #fi #done @@ -567,10 +559,10 @@ # We need to remove the note data, otherwise the ticket notes # will get unnecessary updates. WTF, MantisBT? ticket_data.notes = [] - suds_client.service.mc_issue_update (g_config['trackeruser'], g_config['trackerpassword'], ticket_id, ticket_data) + suds_client.service.mc_issue_update (btuser, btpassword, ticket_id, ticket_data) #fi - suds_client.service.mc_issue_note_add (g_config['trackeruser'], g_config['trackerpassword'], ticket_id, { 'text': message }) + suds_client.service.mc_issue_note_add (btuser, btpassword, ticket_id, { 'text': message }) num_commits += 1 except Exception as e: chanlog ('Error while processing %s: %s' % (commit_node, `e`)) @@ -589,44 +581,33 @@ # class irc_client (asyncore.dispatcher): def __init__ (self, cfg, flags): - self.name = cfg['name'] - self.host = cfg['address'] - self.port = cfg['port'] - self.password = cfg['password'] if 'password' in cfg else '' - self.channels = cfg['channels'] + self.name = cfg.get_value ('name') + self.host = cfg.get_value ('address') + self.port = cfg.get_value ('port', default=6667) + self.password = cfg.get_value ('password', default='') + self.channels = cfg.get_nodelist ('channels') self.flags = flags - self.send_buffer = list() - self.umode = cfg['umode'] if 'umode' in cfg else '' + self.send_buffer = [] + self.umode = cfg.get_value ('umode', default='') self.cfg = cfg - self.mynick = '' - self.verbose = g_config['verbose'] if 'verbose' in g_config else False - self.commandprefix = g_config['commandprefix'][0] if 'commandprefix' in g_config else '.' + self.desired_name = Config.get_value ('nickname', default='cobalt') + self.mynick = self.desired_name + self.verbose = Config.get_value ('verbose', default=False) + self.commandprefix = Config.get_value ('commandprefix', default='.') - for channel in self.channels: - if not 'logchannel' in channel: - channel['logchannel'] = False - channel['namesdone'] = True - #channel['haslinkbot'] = False - - if not 'conflictsuffix' in self.cfg: - self.cfg['conflictsuffix'] = '`' - - self.desired_name = self.cfg['nickname'] if 'nickname' in self.cfg else g_config['nickname'] g_clients.append (self) asyncore.dispatcher.__init__ (self) self.create_socket (socket.AF_INET, socket.SOCK_STREAM) self.connect ((self.host, self.port)) def register_to_irc (self): - ident = self.cfg['ident'] if 'ident' in self.cfg else g_config['ident'] - gecos = self.cfg['gecos'] if 'gecos' in self.cfg else g_config['gecos'] - if 'password' in self.cfg: - self.write ("PASS %s" % self.cfg['password']) + ident = Config.get_value ('ident', default='cobalt') + gecos = Config.get_value ('gecos', default='cobalt') + self.write ("PASS %s" % self.password) self.write ("USER %s * * :%s" % (ident, gecos)) self.write ("NICK %s" % self.mynick) def handle_connect (self): - self.mynick = self.desired_name print "Connected to [%s] %s:%d" % (self.name, self.host, self.port) self.register_to_irc() @@ -675,19 +656,15 @@ if words[1] == "001": self.flags |= CLIF_CONNECTED - for channel in self.cfg['channels']: - self.write ("JOIN %s %s" % (channel['name'], channel['password'] if 'password' in channel else '')) + for channel in self.channels: + self.write ("JOIN %s %s" % (channel.get_value ('name'), channel.get_value ('password', default=''))) - if 'umode' in self.cfg: - self.write ('MODE %s %s' % (self.mynick, self.cfg['umode'])) + umode = self.cfg.get_value ('umode', '') + + if umode != '': + self.write ('MODE %s %s' % (self.mynick, self.cfg.get_value ('umode', ''))) elif words[1] == "PRIVMSG": self.handle_privmsg (line) - elif words[1] == 'JOIN': - rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) JOIN :#(.+)') - match = rex.match (line) - - #if match and match.group(1).toLower() == 'linkbot': - #channel_by_name (match.group(4))['haslinkbot'] = True elif words[1] == 'QUIT': rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) QUIT') match = rex.match (line) @@ -696,14 +673,10 @@ if match and match.group(1) == self.desired_name: self.mynick = self.desired_name self.write ("NICK %s" % self.mynick) - - #if match and match.group(1).toLower() == 'linkbot': - #for channel in self.channels: - #channels['haslinkbot'] = False elif words[1] == "433": #:irc.localhost 433 * cobalt :Nickname is already in use. - self.mynick = '%s%s' % (self.mynick, self.cfg['conflictsuffix']) - self.write ("NICK %s" % self.mynick) + self.mynick += self.cfg.get_value ('conflictsuffix', default='`') + self.write ("NICK " + self.mynick) # Check for new issues on the bugtracker bt_checklatest() @@ -714,7 +687,7 @@ def channel_by_name (self, name): for channel in self.channels: - if channel['name'].upper() == args[0].upper(): + if channel.get_value ('name').upper() == args[0].upper(): return channel else: raise logical_exception ('unknown channel ' + args[0]) @@ -735,7 +708,8 @@ replyto = channel if channel != g_mynick else sender # Check for tracker url in the message - http_regex = re.compile (r'.*http(s?)://%s/view\.php\?id=([0-9]+).*' % g_config['trackerurl']) + url = Config.get_node ('bt').get_value ('url') + http_regex = re.compile (r'.*http(s?)://%s/view\.php\?id=([0-9]+).*' % url) http_match = http_regex.match (line) # Check for command. @@ -759,7 +733,8 @@ # Get the URL for a specified ticket # def get_ticket_url (self, ticket): - return 'https://%s/view.php?id=%s' % (g_config['trackerurl'], ticket) + url = Config.get_node ('bt').get_value ('url') + return 'https://%s/view.php?id=%s' % (url, ticket) # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # @@ -780,7 +755,7 @@ allowprivate = False for channel in self.channels: - if channel['name'] == replyto and 'btprivate' in channel and channel['btprivate'] == True: + if channel.get_value ('name') == replyto and channel.get_value ('btprivate', False): allowprivate = True break #fi @@ -806,9 +781,6 @@ #fi #enddef - def save_config (self): - save_config() - def is_admin (self, ident, host): return ("%s@%s" % (ident, host)) in g_admins @@ -820,13 +792,11 @@ kvargs = {'sender': sender, 'ident': ident, 'host': host, 'replyto': replyto, 'cmdname': command, 'message': message} try: - result = CommandHandler.call_command (self, **kvargs) + result = ModuleCore.call_command (self, **kvargs) if result: return - else: - print 'CommandHandler.call_command returned false' - except CommandHandler.CommandError as e: + except ModuleCore.CommandError as e: lines = str (e).split ('\n') self.privmsg (replyto, 'error: %s' % lines[0]) @@ -1030,9 +1000,9 @@ isprivate = data['view_state']['name'] == 'private' reporter = data['reporter']['name'] if hasattr (data['reporter'], 'name') else '<nobody>' - for channel in self.cfg['channels']: - if 'btannounce' in channel and channel['btannounce'] == True: - if not isprivate or ('btprivate' in channel and channel['btprivate'] == True): + for channel in self.channels: + if channel.get_value ('btannounce', False): + if not isprivate or (channel.get_value ('btprivate', False)): self.write ("PRIVMSG %s :[%s] New issue %s, reported by %s: %s: %s" % \ (channel['name'], data['project']['name'], idstring, reporter, data['summary'], self.get_ticket_url (idstring))) @@ -1068,14 +1038,20 @@ # Main procedure: # try: - for aconn in g_config['autoconnect']: - for conndata in g_config['connections']: - if conndata['name'] == aconn: + autoconnects = Config.get_value ('autoconnect', []) + + if len (autoconnects) == 0: + print "Nowhere to connect." + quit() + + for aconn in autoconnects: + for conndata in Config.get_nodelist ('connections'): + if conndata.get_value ('name') == aconn: irc_client (conndata, 0) break else: - raise logical_exception ("unknown autoconnect entry %s" % (aconn)) - + raise ValueError ("unknown autoconnect entry %s" % (aconn)) + g_BotActive = True asyncore.loop() except KeyboardInterrupt:
--- a/commandhandler.py Wed Nov 05 01:38:08 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,156 +0,0 @@ -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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/configfile.py Sun Nov 09 19:13:08 2014 +0200 @@ -0,0 +1,66 @@ +import json + +class ConfigNode: + def __init__ (self, obj, name, parent): + self.obj = obj + self.name = name + self.root = parent if parent == None or parent.root == None else parent.root + + def keyname (self, key): + if self.name == None: + return key + return self.name + ':' + key + + def get_value (self, key, default=None): + i = 0 + needSave = False + + if not key in self.obj: + if default == None: + raise ValueError ('Mandatory key \'%s\' not found' % self.keyname (key)) + self.obj[key] = default + self.save() + + return self.obj[key] + + def set_value (self, key, value): + self.obj[key] = value + self.save() + + def get_node (self, key): + return ConfigNode (obj=self.get_value (key, {}), name=self.keyname (key), parent=self) + + def get_nodelist (self, key): + data = self.get_value (key) + result = [] + + for entry in data: + node = ConfigNode (obj=entry, name=self.keyname (key), parent=self) + result.append (node) + + return result + + def save (self): + if self.root != None: + self.root.save() + return + + with open ('cobalt.json', 'w') as fp: + json.dump (self.obj, fp, sort_keys = True, indent = 1) + + print "Config saved." + +def init(): + print 'Loading configuration...' + + try: + with open ('cobalt.json', 'r') as fp: + jsondata = json.loads (fp.read()) + except IOError as e: + print 'couldn\'t open cobalt.json: %s' % e + quit() + + global Config + Config = ConfigNode (jsondata, name=None, parent=None) + +init() \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_admin.py Sun Nov 09 19:13:08 2014 +0200 @@ -0,0 +1,59 @@ +from modulecore 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) + bot.restart() + 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/mod_config.py Sun Nov 09 19:13:08 2014 +0200 @@ -0,0 +1,103 @@ +from modulecore import command_error +from configfile import Config +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/mod_idgames.py Sun Nov 09 19:13:08 2014 +0200 @@ -0,0 +1,54 @@ +from modulecore 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modulecore.py Sun Nov 09 19:13:08 2014 +0200 @@ -0,0 +1,156 @@ +import os +import re + +Modules = {} +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 Modules + files = os.listdir ('.') + + for fn in files: + if fn[0:4] != 'mod_' or fn[-3:] != '.py': + continue + + fn = fn[0:-3] + globals = {} + module = __import__ (fn) + Modules[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 module %s" % fn + #done + + print 'Loaded %d commands in %d modules' % (len (Commands), len (Modules)) +#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