# HG changeset patch # User Teemu Piippo # Date 1415577966 -7200 # Node ID d67cc4fbc3f1f213af8c5258bf832ba3be15164e # Parent 2266d6d73de35c8c1b9754eeabea9520c685a0e3 - modularization complete!! diff -r 2266d6d73de3 -r d67cc4fbc3f1 LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE Mon Nov 10 02:06:06 2014 +0200 @@ -0,0 +1,27 @@ + + + Copyright 2014 Teemu Piippo + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff -r 2266d6d73de3 -r d67cc4fbc3f1 bt.py --- a/bt.py Sun Nov 09 19:59:10 2014 +0200 +++ b/bt.py Mon Nov 10 02:06:06 2014 +0200 @@ -1,5 +1,9 @@ import suds -import cobalt +import sys +import time +import re +import irc as Irc +from configfile import Config suds_active = False btannounce_active = False @@ -8,7 +12,54 @@ def is_active(): return suds_active +def get_ticket_data (bot, replyto, ticket, withlink): + if suds_active == False: + print "suds is not active" + return + + data = {} + try: + data = get_issue (ticket) + except Exception, e: + bot.privmsg (replyto, "Failed to get info for issue %s: %s" % (ticket, `e`)) + + if data: + if data['view_state']['name'] == 'private': + allowprivate = False + + for channel in bot.channels: + if channel.get_value ('name') == replyto and channel.get_value ('btprivate', False): + allowprivate = True + break + + if not allowprivate: + bot.privmsg (replyto, 'Error: ticket %s is private' % ticket) + return + + bot.privmsg (replyto, "Issue %s: %s: Reporter: %s, assigned to: %s, status: %s (%s)" % \ + (ticket, \ + data.summary, \ + data.reporter.name if hasattr (data.reporter, 'name') else "", \ + data.handler.name if hasattr (data, 'handler') else "nobody", \ + data.status.name, \ + data.resolution.name)) + + if withlink: + bot.privmsg (replyto, "Read all about it here: " + get_ticket_url (ticket)) + +def process_message (bot, line, replyto): + # Check for tracker url in the message + 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) + + if http_match: + get_ticket_data (bot, replyto, http_match.group (2), False) + def init(): + global suds_active + global suds_client + try: print 'Initializing MantisBT connection...' suds_import = suds.xsd.doctor.Import ('http://schemas.xmlsoap.org/soap/encoding/', 'http://schemas.xmlsoap.org/soap/encoding/') @@ -20,10 +71,10 @@ if suds_active: sys.stdout.write ('Retrieving latest tracker ticket... ') - user, password = bt_credentials() + user, password = credentials() btannounce_id = suds_client.service.mc_issue_get_biggest_id (user, password, 0) btannounce_active = True - bt_updatechecktimeout() + update_checktimeout() print btannounce_id def update_checktimeout(): @@ -36,9 +87,9 @@ password = bt.get_value ('password', '') return [user, password] -def get_issue(ticket): +def get_issue (ticket): global suds_client - user, password = bt_credentials() + user, password = credentials() return suds_client.service.mc_issue_get (user, password, ticket) def poll(): @@ -46,10 +97,10 @@ global btannounce_id if time.time() >= btannounce_timeout: - bt_updatechecktimeout() + update_checktimeout() newid = btannounce_id try: - user, password = bt_credentials() + user, password = credentials() newid = suds_client.service.mc_issue_get_biggest_id (user, password, 0) except Exception as e: pass @@ -57,10 +108,10 @@ while newid > btannounce_id: try: btannounce_id += 1 - data = bt_getissue (btannounce_id) - - for client in cobalt.all_clients: - announce_new_ticket (client, data) + data = get_issue (btannounce_id) + + for client in Irc.all_clients: + announce_new_issue (client, data) except Exception as e: pass @@ -68,7 +119,6 @@ url = Config.get_node ('bt').get_value ('url') return 'https://%s/view.php?id=%s' % (url, ticket) - # # Print a ticket announce to appropriate channels # @@ -84,8 +134,17 @@ 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))) - #fi - #fi - #done + (channel.get_value ('name'), + data['project']['name'], + idstring, + reporter, + data['summary'], + get_ticket_url (idstring))) + +def update_issue (ticket_id, ticket_data): + btuser, btpassword = credentials() + suds_client.service.mc_issue_update (btuser, btpassword, ticket_id, ticket_data) + +def post_note (ticket_id, message): + btuser, btpassword = credentials() + suds_client.service.mc_issue_note_add (btuser, btpassword, ticket_id, { 'text': message }) diff -r 2266d6d73de3 -r d67cc4fbc3f1 cobalt.py --- a/cobalt.py Sun Nov 09 19:59:10 2014 +0200 +++ b/cobalt.py Mon Nov 10 02:06:06 2014 +0200 @@ -28,28 +28,52 @@ ''' import asyncore -import socket -import time import sys import traceback -import re -import urllib -import urllib2 -import hgapi import os -import math -import json -from datetime import datetime import modulecore as ModuleCore -import configfile +import configfile as ConfigFile from configfile import Config import hgpoll as HgPoll import bt as Bt +import irc as Irc + +if __name__ != '__main__': + raise ImportError ('cobalt may not be imported as a module') g_BotActive = False +# +# Exception handling +# +def handle_exception(excType, excValue, trace): + excepterm (traceback.format_exception(excType, excValue, trace)) + +def excepterm (data): + for segment in data: + for line in segment.splitlines(): + print line + Irc.broadcast (line) + + for client in Irc.all_clients: + if len(data) > 0: + client.exceptdie() + else: + client.quit_irc() + + if g_BotActive: + restart_self() + else: + quit() + +def restart_self(): + os.execl (sys.executable, sys.executable, * sys.argv) + def main(): - ModuleCore.init_data() + global g_BotActive + sys.excepthook = handle_exception + all_clients = [] + g_BotActive = False try: uid = os.geteuid() @@ -59,9 +83,10 @@ if uid == 0 and raw_input ('Do you seriously want to run cobalt as root? [y/N] ') != 'y': quit() - configfile.init() - g_admins = Config.get_value ('admins', default=[]) - g_mynick = Config.get_value ('nickname', default='cobalt') + ConfigFile.init() + ModuleCore.init_data() + Bt.init() + HgPoll.init() try: autoconnects = Config.get_value ('autoconnect', []) @@ -73,7 +98,7 @@ for aconn in autoconnects: for conndata in Config.get_nodelist ('connections'): if conndata.get_value ('name') == aconn: - irc_client (conndata, 0) + Irc.irc_client (conndata, 0) break else: raise ValueError ("unknown autoconnect entry %s" % (aconn)) @@ -81,335 +106,11 @@ g_BotActive = True asyncore.loop() except KeyboardInterrupt: - for client in all_clients: + for client in Irc.all_clients: client.keyboardinterrupt() quit() - -# -# irc_client flags -# -CLIF_CONNECTED = (1 << 1) - -# -# List of all clients -# -all_clients = [] - -class channel (object): - name = "" - password = "" - - def __init__ (self, name): - self.name = name - -# -# Prints a line to log channel(s) -# -def chanlog (line): - for client in all_clients: - if not client.flags & CLIF_CONNECTED: - continue - - for channel in client.channels: - if channel.get_value ('logchannel', default=False): - client.write ("PRIVMSG %s :%s" % (channel['name'], line)) - -# -# Exception handling -# -def handle_exception(excType, excValue, trace): - excepterm (traceback.format_exception(excType, excValue, trace)) - -def excepterm(data): - for segment in data: - for line in segment.splitlines(): - print line - chanlog (line) - - for client in all_clients: - if len(data) > 0: - client.exceptdie() - else: - client.quit_irc() - - if g_BotActive: - restart_self() - else: - quit() - -sys.excepthook = handle_exception - -def check_admin (sender, ident, host, command): - if not "%s@%s" % (ident, host) in g_admins: - raise logical_exception (".%s requires admin access" % command) - -class logical_exception (Exception): - def __init__ (self, value): - self.value = value - def __str__ (self): - return self.value - -# from http://www.daniweb.com/software-development/python/code/260268/restart-your-python-program -def restart_self(): - python = sys.executable - os.execl (python, python, * sys.argv) - -def plural (a): - return '' if a == 1 else 's' - -# -# Main IRC client class -# -class irc_client (asyncore.dispatcher): - def __init__ (self, cfg, flags): - 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 = [] - self.umode = cfg.get_value ('umode', default='') - self.cfg = cfg - 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='.') - - all_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 = 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): - print "Connected to [%s] %s:%d" % (self.name, self.host, self.port) - self.register_to_irc() - - def write (self, utfdata): - try: - self.send_buffer.append ("%s" % utfdata.decode("utf-8","ignore").encode("ascii","ignore")) - except UnicodeEncodeError: - pass - - def handle_close (self): - print "Connection to [%s] %s:%d terminated." % (self.name, self.host, self.port) - self.close() - - def handle_write (self): - self.send_all_now() - - def readable (self): - return True - - def writable (self): - return len (self.send_buffer) > 0 - - def send_all_now (self): - for line in self.send_buffer: - if self.verbose: - print "[%s] <- %s" % (self.name, line) - self.send ("%s\n" % line) - self.send_buffer = [] - - def handle_read (self): - lines = self.recv (4096).splitlines() - for utfline in lines: - try: - line = utfline.decode("utf-8","ignore").encode("ascii","ignore") - except UnicodeDecodeError: - continue - - if self.verbose: - print "[%s] -> %s" % (self.name, line) - - if line.startswith ("PING :"): - self.write ("PONG :%s" % line[6:]) - else: - words = line.split(" ") - if len(words) >= 2: - if words[1] == "001": - self.flags |= CLIF_CONNECTED - - for channel in self.channels: - self.write ("JOIN %s %s" % (channel.get_value ('name'), channel.get_value ('password', default=''))) - - 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] == 'QUIT': - rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) QUIT') - match = rex.match (line) - - # Try reclaim our nickname if possible - if match and match.group(1) == self.desired_name: - self.mynick = self.desired_name - self.write ("NICK %s" % self.mynick) - elif words[1] == "433": - #:irc.localhost 433 * cobalt :Nickname is already in use. - self.mynick += self.cfg.get_value ('conflictsuffix', default='`') - self.write ("NICK " + self.mynick) - - # Check for new issues on the bugtracker - bt_checklatest() - - # Check for new commits in the repositories - HgPoll.poll() - - def channel_by_name (self, name): - for channel in self.channels: - if channel.get_value ('name').upper() == args[0].upper(): - return channel - else: - raise logical_exception ('unknown channel ' + args[0]) - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - # - # Handle a PRIVMSG line from the IRC server - # - def handle_privmsg (self, line): - rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) PRIVMSG ([^ ]+) :(.+)$') - match = rex.match (line) - if match: - sender = match.group (1) - user = match.group (2) - host = match.group (3) - channel = match.group (4) - message = match.group (5) - replyto = channel if channel != g_mynick else sender - - # Check for tracker url in the message - 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. - if len(message) >= 2 and message[0] == self.commandprefix and message[1] != self.commandprefix: - stuff = message[1:].split(' ') - command = stuff[0] - args = stuff[1:] - try: - 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: - self.privmsg (replyto, "error: %s" % line) - elif http_match: - self.get_ticket_data (replyto, http_match.group (2), False) - else: - chanlog ("Recieved bad PRIVMSG: %s" % line) - - 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, message): - kvargs = {'sender': sender, 'ident': ident, 'host': host, 'replyto': replyto, 'cmdname': command, 'message': message} - - try: - result = ModuleCore.call_command (self, **kvargs) - - if result: - return - except ModuleCore.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 == 'die': - check_admin (sender, ident, host, command) - quit() - elif command == 'convert': - if len(args) != 3 or args[1] != 'as': - raise logical_exception ("usage: .%s as " % command) - - value = float (args[0]) - valuetype = args[2] - - if valuetype in ['radians', 'degrees']: - if valuetype == 'radians': - radvalue = value - degvalue = (value * 180.) / math.pi - else: - radvalue = (value * math.pi) / 180. - degvalue = value - - self.privmsg (replyto, '%s radians, %s degrees (%s)' %(radvalue, degvalue, degvalue % 360.)) - elif valuetype in ['celsius', 'fahrenheit']: - if valuetype == 'celsius': - celvalue = value - fahrvalue = value * 1.8 + 32 - else: - celvalue = (value - 32) / 1.8 - fahrvalue = value - - self.privmsg (replyto, '%s degrees celsius, %s degrees fahrenheit' %(celvalue, fahrvalue)) - else: - raise logical_exception ('unknown valuetype, expected one of: degrees, radians (angle conversion), ' + - 'celsius, fahrenheit (temperature conversion)') - elif command == 'urban' or command == 'ud': - try: - if len(args) < 1: - raise logical_exception ('usage: %s ' % command) - - url = 'http://api.urbandictionary.com/v0/define?term=%s' % ('%20'.join (args)) - response = urllib2.urlopen (url).read() - data = json.loads (response) - - if 'list' in data and len(data['list']) > 0 and 'word' in data['list'][0] and 'definition' in data['list'][0]: - word = data['list'][0]['word'] - definition = data['list'][0]['definition'].replace ('\r', ' ').replace ('\n', ' ').replace (' ', ' ') - up = data['list'][0]['thumbs_up'] - down = data['list'][0]['thumbs_down'] - self.privmsg (replyto, "\002%s\002: %s\0033 %d\003 up,\0035 %d\003 down" % (word, definition, up, down)) - else: - self.privmsg (replyto, "couldn't find a definition of \002%s\002" % args[0]) - except logical_exception as e: - raise e - except Exception as e: - raise logical_exception ('Urban dictionary lookup failed: %s' % `e`) -# else: -# raise logical_exception ("unknown command `.%s`" % command) - - 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)) - - def close_connection (self, message): - if self.flags & CLIF_CONNECTED: - self.write ("QUIT :" + message) - self.send_all_now() - self.close() - - def quit_irc (self): - self.close_connection ('Leaving') - - def exceptdie (self): - self.close_connection ('Caught exception') - - def keyboardinterrupt (self): - self.close_connection ('KeyboardInterrupt') + except Irc.RestartError as e: + excepterm (e.message) if __name__ == '__main__': main() \ No newline at end of file diff -r 2266d6d73de3 -r d67cc4fbc3f1 configfile.py --- a/configfile.py Sun Nov 09 19:59:10 2014 +0200 +++ b/configfile.py Mon Nov 10 02:06:06 2014 +0200 @@ -40,6 +40,12 @@ return result + def append_nodelist (self, key): + data = self.get_value (key) + obj = {} + data.append (obj) + return ConfigNode (obj=obj, name=self.keyname (key), parent=self) + def save (self): if self.root != None: self.root.save() diff -r 2266d6d73de3 -r d67cc4fbc3f1 hgpoll.py --- a/hgpoll.py Sun Nov 09 19:59:10 2014 +0200 +++ b/hgpoll.py Mon Nov 10 02:06:06 2014 +0200 @@ -1,40 +1,43 @@ -impot hgapi +import hgapi +import time +import re +import bt as Bt +import irc as Irc +from datetime import datetime from configfile import Config g_needCommitsTxtRebuild = True def make_commits_txt(): global g_needCommitsTxtRebuild - + if g_needCommitsTxtRebuild == False: return - + print 'Building commits.txt...' # Update zandronum-everything repo = hgapi.Repo ('zandronum-everything') repo.hg_command ('pull', '../zandronum-sandbox') repo.hg_command ('pull', '../zandronum-sandbox-stable') - data = repo.hg_command ('log', '--template', '{node} {date|hgdate}\n') - f = open ('commits.txt', 'w') - + for line in data.split ('\n'): if line == '': continue - + words = line.split (' ') timestamp = int (words[1]) f.write ('%s %s\n' % (words[0], datetime.utcfromtimestamp (timestamp).strftime ('%y%m%d-%H%M'))) + f.close() g_needCommitsTxtRebuild = False -#enddef ' Check if a repository exists ' def check_repo_exists (repo_name, repo_owner): print 'Checking that %s exists...' % repo_name repo_url = 'https://bitbucket.org/%s/%s' % (repo_owner, repo_name) zanrepo = hgapi.Repo (repo_name) - + try: zanrepo.hg_command ('id', '.') except hgapi.hgapi.HgException: @@ -42,7 +45,7 @@ if repo_name == 'zandronum-everything': if not os.path.exists (repo_name): os.makedirs (repo_name) - + global g_needCommitsTxtRebuild g_needCommitsTxtRebuild = True print 'Init %s' % repo_name @@ -54,29 +57,26 @@ print 'Done' make_commits_txt() return - #fi - + try: print 'Cloning %s...' % repo_name zanrepo.hg_clone (repo_url, repo_name) print 'Cloning done.' except Exception as e: print 'Unable to clone %s from %s: %s' % (repo_name, repo_url, str (`e`)) - quit(1) - #tried -#enddef + quit (1) -check_repo_exists ('zandronum', 'Torr_Samaho') -check_repo_exists ('zandronum-stable', 'Torr_Samaho') -check_repo_exists ('zandronum-sandbox', 'crimsondusk') -check_repo_exists ('zandronum-sandbox-stable', 'crimsondusk') -check_repo_exists ('zandronum-everything', '') - -repocheck_timeout = (time.time()) + 15 +def init(): + check_repo_exists ('zandronum', 'Torr_Samaho') + check_repo_exists ('zandronum-stable', 'Torr_Samaho') + check_repo_exists ('zandronum-sandbox', 'crimsondusk') + check_repo_exists ('zandronum-sandbox-stable', 'crimsondusk') + check_repo_exists ('zandronum-everything', '') + global repocheck_timeout + repocheck_timeout = (time.time()) + 15 def get_commit_data (zanrepo, rev, template): return zanrepo.hg_command ('log', '-l', '1', '-r', rev, '--template', template) -#enddef def decipher_hgapi_error (e): # Blah, hgapi, why must your error messages be so mangled? @@ -86,119 +86,99 @@ return [True, errmsg] except: return [False, ''] - #endtry -#enddef -def bbcodify(commit_diffstat): - result='' +def bbcodify (commit_diffstat): + result = '' + for line in commit_diffstat.split('\n'): - # Add green color-tags for the ++++++++++ stream rex = re.compile (r'^(.*)\|(.*) (\+*)(-*)(.*)$') match = rex.match (line) if match: line = '%s|%s [color=#5F7]%s[/color][color=#F53]%s[/color]%s\n' \ % (match.group (1), match.group (2), match.group (3), match.group (4), match.group (5)) - + # Tracker doesn't seem to like empty color tags line = line.replace ('[color=#5F7][/color]', '').replace ('[color=#F53][/color]', '') - #else: - #rex = re.compile (r'^(.*) ([0-9]+) insertions\(\+\), ([0-9]+) deletions\(\-\)$') - #match = rex.match (line) - #if match: - #line = '%s [b][color=green]%s[/color][/b] insertions, [b][color=red]%s[/color][/b] deletions\n' \ - #% (match.group (1), match.group (2), match.group (3)) - + result += line - #done - + return result -#enddef def find_developer_by_email (commit_email): for developer, emails in Config.get_value ('developer_emails', default={}).iteritems(): - for email in emails: - if commit_email == email: - return developer - #fi - #done - #done - + if commit_email in emails: + return developer + return '' -#enddef -' Retrieves and processes commits for zandronum repositories ' -' Ensure both repositories are OK before using this! ' def poll(): + global repocheck_timeout + if time.time() < repocheck_timeout: + return + for n in ['zandronum-stable', 'zandronum', 'zandronum-sandbox', 'zandronum-sandbox-stable']: - process_one_repo (n) + poll_one_repo (n) + + hgns = Config.get_node ('hg') + repocheck_timeout = time.time() + hgns.get_value ('checkinterval', default=15) * 60 -def process_one_repo (repo_name): +def poll_one_repo (repo_name): global repocheck_timeout - 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: - return - - repocheck_timeout = time.time() + hgns.get_value ('checkinterval', default=15) * 60 zanrepo = hgapi.Repo (repo_name) commit_data = [] delimeter = '@@@@@@@@@@' - + print 'Checking %s for updates' % repo_name + try: data = zanrepo.hg_command ('incoming', '--quiet', '--template', '{node|short} {desc}' + delimeter) except hgapi.hgapi.HgException as e: deciphered = decipher_hgapi_error (e) - + if deciphered[0] and len(deciphered[1]) > 0: - chanlog ("error while using hg import on %s: %s" % (repo_name, deciphered[1])) - #fi - + Irc.broadcast ("error while using hg import on %s: %s" % (repo_name, deciphered[1])) + return except Exception as e: - chanlog ("%s" % `e`) + Irc.broadcast ("%s" % `e`) return - #tried - + for line in data.split (delimeter): if line == '': continue - #fi - + rex = re.compile (r'([^ ]+) (.+)') match = rex.match (line) failed = False if not match: - chanlog ('malformed hg data: %s' % line) + Irc.broadcast ('malformed hg data: %s' % line) continue - #fi - + commit_node = match.group (1) commit_message = match.group (2) commit_data.append ([commit_node, commit_message]) - #done - + + print '%d new commits on %s' % (len (commit_data), repo_name) + if len (commit_data) > 0: pull_args = []; - + for commit in commit_data: pull_args.append ('-r'); pull_args.append (commit[0]); - #done - + + print 'Pulling new commits...' try: zanrepo.hg_command ('pull', *pull_args) @@ -206,8 +186,7 @@ if usestable: devrepo = hgapi.Repo ('zandronum') devrepo.hg_command ('pull', '../zandronum-stable', *pull_args) - #fi - + # Pull everything into sandboxes too if not usesandbox: devrepo = hgapi.Repo ('zandronum-sandbox') @@ -215,130 +194,116 @@ devrepo = hgapi.Repo ('zandronum-sandbox-stable') devrepo.hg_command ('pull', '../%s' % repo_name, *pull_args) - #fi - + devrepo = hgapi.Repo ('zandronum-everything') devrepo.hg_command ('pull', '../%s' % repo_name, *pull_args) global g_needCommitsTxtRebuild g_needCommitsTxtRebuild = True except Exception as e: - chanlog ('Warning: unable to pull: %s' % `e`) + Irc.broadcast ('Warning: unable to pull: %s' % `e`) return - #tried - #fi - + for commit in commit_data: commit_node = commit[0] commit_message = commit[1] - + print 'Processing new commit %s...' % commit_node + try: if usesandbox: commit_author = get_commit_data (zanrepo, commit_node, '{author}') commit_url = '%s/commits/%s' % (repo_url, commit_node) commit_email = '' - + # Remove the email address from the author if possible rex = re.compile (r'^(.+) <([^>]+)>$.*') match = rex.match (commit_author) if match: commit_author = match.group (1) commit_email = match.group (2) - #fi - + commit_trackeruser = find_developer_by_email (commit_email) committer = commit_trackeruser if commit_trackeruser != '' else commit_author - - for irc_client in g_clients: - for channel in irc_client.cfg['channels']: - if 'btprivate' in channel and channel['btprivate'] == True: - irc_client.privmsg (channel['name'], + + for irc_client in Irc.all_clients: + for channel in irc_client.channels: + if channel.get_value ('btprivate', False): + irc_client.privmsg (channel.get_value ('name'), "%s: new commit %s by %s: %s" % (repo_name, commit_node, committer, commit_url)) - + for line in commit_message.split ('\n'): - irc_client.privmsg (channel['name'], line) - #fi - #done - #done - + irc_client.privmsg (channel.get_value ('name'), line) + num_commits += 1 continue - #fi - + rex = re.compile (r'^.*(fixes|resolves|addresses|should fix) ([0-9]+).*$') match = rex.match (commit_message) - + if not match: continue # no "fixes" message in the commit - #fi - + ticket_id = int (match.group (2)) - + # Acquire additional data moredata = get_commit_data (zanrepo, commit_node, '{author|nonempty}\n{date(date, \'%A %d %B %Y %T\')}').split('\n') - + if len (moredata) != 2: - chanlog ('error while processing %s: malformed hg data' % commit_node) + Irc.broadcast ('error while processing %s: malformed hg data' % commit_node) continue - #fi - + commit_author = moredata[0] commit_date = moredata[1] commit_email = "" - + try: - ticket_data = suds_client.service.mc_issue_get (btuser, btpassword, ticket_id) + ticket_data = Bt.get_issue (ticket_id) except Exception as e: - chanlog ('error while processing %s: %s' % (commit_node, `e`)) + Irc.broadcast ('error while processing %s: %s' % (commit_node, `e`)) continue - #tried - + # Remove the email address from the author if possible rex = re.compile (r'^(.+) <([^>]+)>$.*') match = rex.match (commit_author) if match: commit_author = match.group (1) commit_email = match.group (2) - #fi - + commit_diffstat = zanrepo.hg_command ('diff', '--change', commit_node, '--stat') - + if len(commit_diffstat) > 0: # commit_diffstat = 'Changes in files:\n[code]\n' + commit_diffstat + '\n[/code]' commit_diffstat = 'Changes in files:\n' + bbcodify(commit_diffstat) else: commit_diffstat = 'No changes in files.' - + # Compare the email addresses against known developer usernames commit_trackeruser = find_developer_by_email (commit_email) - + if commit_trackeruser != '': commit_author += ' [%s]' % commit_trackeruser - #fi - + message = 'Issue addressed by commit %s: [b][url=%s/commits/%s]%s[/url][/b]' \ % (commit_node, repo_url, commit_node, commit_message) message += "\nCommitted by %s on %s\n\n%s" \ % (commit_author, commit_date, commit_diffstat) - + need_update = False - + # If not already set, set handler if not 'handler' in ticket_data: ticket_data['handler'] = {'name': commit_trackeruser} need_update = True - #fi - + # Find out the status level of the ticket needs_testing_level = 70 if ticket_data['status']['id'] < needs_testing_level: ticket_data.status['id'] = needs_testing_level need_update = True - #fi - + # Set target version if not set if not 'target_version' in ticket_data: ticket_data['target_version'] = '1.4' if repo_name == 'zandronum-stable' else '2.0' @@ -352,37 +317,30 @@ # Fix target version from 2.0-beta to 2.0 ticket_data['target_version'] = '2.0' need_update = True - #fi - + # Announce on IRC - for irc_client in g_clients: + for irc_client in Irc.all_clients: 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.get_value ('name'), - "Read all about it here: " + irc_client.get_ticket_url (ticket_id)) - #fi - #done - #done - + "Read all about it here: " + Bt.get_ticket_url (ticket_id)) + if need_update: # 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 (btuser, btpassword, ticket_id, ticket_data) - #fi - - suds_client.service.mc_issue_note_add (btuser, btpassword, ticket_id, { 'text': message }) + Bt.update_issue (ticket_id, ticket_data) + + Bt.post_note (ticket_id, message) num_commits += 1 except Exception as e: - chanlog ('Error while processing %s: %s' % (commit_node, `e`)) + Irc.broadcast ('Error while processing %s: %s' % (commit_node, e)) continue - #tried - #done -#enddef def force_poll(): + global repocheck_timeout repocheck_timeout = 0 poll() \ No newline at end of file diff -r 2266d6d73de3 -r d67cc4fbc3f1 irc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/irc.py Mon Nov 10 02:06:06 2014 +0200 @@ -0,0 +1,235 @@ +import asyncore +import socket +import sys +import re +import modulecore as ModuleCore +import traceback +from configfile import Config +import bt as Bt +import hgpoll as HgPoll + +CLIF_CONNECTED = (1 << 1) + +all_clients = [] + +class RestartError (Exception): + def __init__ (self, value): + self.message = value + def __str__ (self): + return self.message + +# +# Prints a line to log channel(s) +# +def broadcast (line): + for client in all_clients: + if not client.flags & CLIF_CONNECTED: + continue + + for channel in client.channels: + if channel.get_value ('logchannel', default=False): + client.write ("PRIVMSG %s :%s" % (channel.get_value ('name'), line)) + +class logical_exception (Exception): + def __init__ (self, value): + self.value = value + def __str__ (self): + return self.value + +# +# Main IRC client class +# +class irc_client (asyncore.dispatcher): + def __init__ (self, cfg, flags): + global all_clients + 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 = [] + self.umode = cfg.get_value ('umode', default='') + self.cfg = cfg + 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='.') + all_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 = 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): + print "Connected to [%s] %s:%d" % (self.name, self.host, self.port) + self.register_to_irc() + + def write (self, utfdata): + try: + self.send_buffer.append ("%s" % utfdata.decode("utf-8","ignore").encode("ascii","ignore")) + except UnicodeEncodeError: + pass + + def handle_close (self): + print "Connection to [%s] %s:%d terminated." % (self.name, self.host, self.port) + self.close() + + def handle_write (self): + self.send_all_now() + + def readable (self): + return True + + def writable (self): + return len (self.send_buffer) > 0 + + def send_all_now (self): + for line in self.send_buffer: + if self.verbose: + print "[%s] <- %s" % (self.name, line) + self.send ("%s\n" % line) + self.send_buffer = [] + + def handle_read (self): + lines = self.recv (4096).splitlines() + for utfline in lines: + try: + line = utfline.decode("utf-8","ignore").encode("ascii","ignore") + except UnicodeDecodeError: + continue + + if self.verbose: + print "[%s] -> %s" % (self.name, line) + + if line.startswith ("PING :"): + self.write ("PONG :%s" % line[6:]) + else: + words = line.split(" ") + if len(words) >= 2: + if words[1] == "001": + self.flags |= CLIF_CONNECTED + + for channel in self.channels: + self.write ("JOIN %s %s" % (channel.get_value ('name'), channel.get_value ('password', default=''))) + + 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] == 'QUIT': + rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) QUIT') + match = rex.match (line) + + # Try reclaim our nickname if possible + if match and match.group(1) == self.desired_name: + self.mynick = self.desired_name + self.write ("NICK %s" % self.mynick) + elif words[1] == "433": + #:irc.localhost 433 * cobalt :Nickname is already in use. + self.mynick += self.cfg.get_value ('conflictsuffix', default='`') + self.write ("NICK " + self.mynick) + + # Check for new issues on the bugtracker + Bt.poll() + + # Check for new commits in the repositories + HgPoll.poll() + + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # + # Handle a PRIVMSG line from the IRC server + # + def handle_privmsg (self, line): + rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) PRIVMSG ([^ ]+) :(.+)$') + match = rex.match (line) + + if not match: + broadcast ("Recieved bad PRIVMSG: %s" % line) + + sender = match.group (1) + user = match.group (2) + host = match.group (3) + channel = match.group (4) + message = match.group (5) + replyto = channel if channel != self.mynick else sender + + # Check for command. + if len(message) >= 2 and message[0] == self.commandprefix and message[1] != self.commandprefix: + stuff = message[1:].split(' ') + command = stuff[0] + args = stuff[1:] + self.handle_command (sender, user, host, replyto, command, args, message) + return + + Bt.process_message (self, line, replyto) + + def add_irc_channel (self, channame): + for channel in self.channels: + if channel.get_value ('name').upper() == channame.upper(): + return + + channel = self.cfg.append_nodelist ('channels') + channel.set_value ('name', channame) + self.channels = cfg.get_nodelist ('channels') + self.write ('JOIN ' + channame) + self.save_config() + + def remove_irc_channel (self, channame): + for channel in self.channels: + if channel.get_value ('name') == channame: + self.channels.remove (channel) + break + else: + return + + self.write ('PART ' + channame) + self.save_config() + + 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 = ModuleCore.call_command (self, **kvargs) + + if result: + return + except ModuleCore.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 + + def handle_error(self): + raise RestartError (traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)) + + def restart(self): + raise RestartError() + + def privmsg (self, channel, msg): + self.write ("PRIVMSG %s :%s" % (channel, msg)) + + def close_connection (self, message): + if self.flags & CLIF_CONNECTED: + self.write ("QUIT :" + message) + self.send_all_now() + self.close() + + def quit_irc (self): + self.close_connection ('Leaving') + + def exceptdie (self): + self.close_connection ('Caught exception') + + def keyboardinterrupt (self): + self.close_connection ('KeyboardInterrupt') \ No newline at end of file diff -r 2266d6d73de3 -r d67cc4fbc3f1 mod_admin.py --- a/mod_admin.py Sun Nov 09 19:59:10 2014 +0200 +++ b/mod_admin.py Mon Nov 10 02:06:06 2014 +0200 @@ -30,7 +30,14 @@ 'description': 'Checks for updates on the bot', 'args': None, 'level': 'admin' - } + }, + + { + 'name': 'die', + 'description': 'Shuts the bot down', + 'args': None, + 'level': 'admin', + }, ] } @@ -43,6 +50,9 @@ def cmd_restart (bot, **rest): bot.restart() +def cmd_die (bot, **rest): + quit() + def cmd_update (bot, replyto, **rest): try: repo = hgapi.Repo ('.') @@ -56,4 +66,4 @@ else: bot.privmsg (replyto, 'Up to date at %s.' % r2) except hgapi.HgException as e: - command_error ('Update failed: %s' % str (e)) + command_error ('Update failed: %s' % str (e)) \ No newline at end of file diff -r 2266d6d73de3 -r d67cc4fbc3f1 mod_bt.py --- a/mod_bt.py Sun Nov 09 19:59:10 2014 +0200 +++ b/mod_bt.py Mon Nov 10 02:06:06 2014 +0200 @@ -19,53 +19,8 @@ ] } -def get_ticket_data (bot, replyto, ticket, withlink): - if suds_active == False: - return - - data = {} - try: - data = bt_getissue (ticket) - except Exception, e: - bot.privmsg (replyto, "Failed to get info for issue %s: %s" % (ticket, `e`)) - - if data: - if data['view_state']['name'] == 'private': - allowprivate = False - - for channel in bot.channels: - if channel.get_value ('name') == replyto and channel.get_value ('btprivate', False): - allowprivate = True - break - #fi - #done - - if not allowprivate: - bot.privmsg (replyto, 'Error: ticket %s is private' % ticket) - return - #fi - #fi - - bot.privmsg (replyto, "Issue %s: %s: Reporter: %s, assigned to: %s, status: %s (%s)" % \ - (ticket, \ - data.summary, \ - data.reporter.name if hasattr (data.reporter, 'name') else "", \ - data.handler.name if hasattr (data, 'handler') else "nobody", \ - data.status.name, \ - data.resolution.name)) - - if withlink: - bot.privmsg (replyto, "Read all about it here: " + bot.get_ticket_url (ticket)) - #fi - #fi -#enddef - -def cmd_ticket (bot, args, **rest): +def cmd_ticket (bot, args, replyto, **rest): Bt.get_ticket_data (bot, replyto, args['ticket'], True) -def cmd_testannounce (bot, args, **rest); -elif command == 'testannounce': - check_admin (sender, ident, host, command) - if len(args) != 1: - raise logical_exception ("usage: .%s " % command) - self.announce_ticket (bt_getissue (args[0])) \ No newline at end of file +def cmd_testannounce (bot, args, **rest): + Bt.announce_new_issue (bot, Bt.get_issue (args['ticket'])) \ No newline at end of file diff -r 2266d6d73de3 -r d67cc4fbc3f1 mod_config.py --- a/mod_config.py Sun Nov 09 19:59:10 2014 +0200 +++ b/mod_config.py Mon Nov 10 02:06:06 2014 +0200 @@ -18,86 +18,11 @@ 'args': '', 'level': 'admin', }, - - { - 'name': 'chanattr', - 'description': 'Get or set a channel-specific attribute', - 'args': ' [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 + bot.add_irc_channel (args['channel']) 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 + bot.remove_irc_channel (args['channel']) \ No newline at end of file diff -r 2266d6d73de3 -r d67cc4fbc3f1 mod_hgpoll.py --- a/mod_hgpoll.py Sun Nov 09 19:59:10 2014 +0200 +++ b/mod_hgpoll.py Mon Nov 10 02:06:06 2014 +0200 @@ -1,4 +1,5 @@ from hgapi import hgapi, Repo +from datetime import datetime import hgpoll as HgPoll ModuleData = { @@ -28,7 +29,7 @@ def cmd_checkhg (bot, **rest): HgPoll.force_poll() -def cmd_cset (bot, args, **rest) +def cmd_cset (bot, args, **rest): repo = Repo ('zandronum-everything') data = "" node = args['key'] diff -r 2266d6d73de3 -r d67cc4fbc3f1 mod_util.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_util.py Mon Nov 10 02:06:06 2014 +0200 @@ -0,0 +1,73 @@ +import math +import urllib2 +from modulecore import command_error + +ModuleData = { + 'commands': + [ + { + 'name': 'convert', + 'description': 'Performs numeric conversion', + 'args': ' as ', + 'level': 'normal', + }, + + { + 'name': 'ud', + 'description': 'Looks up a term in urban dictionary', + 'args': '', + 'level': 'normal', + }, + ] +} + +def cmd_convert (bot, args, replyto, **rest): + value = float (args['value']) + valuetype = args['valuetype'] + + if valuetype in ['radians', 'degrees']: + if valuetype == 'radians': + radvalue = value + degvalue = (value * 180.) / math.pi + else: + radvalue = (value * math.pi) / 180. + degvalue = value + + bot.privmsg (replyto, '%s radians, %s degrees (%s)' % + (radvalue, degvalue, degvalue % 360.)) + return + + if valuetype in ['celsius', 'fahrenheit']: + if valuetype == 'celsius': + celvalue = value + fahrvalue = value * 1.8 + 32 + else: + celvalue = (value - 32) / 1.8 + fahrvalue = value + + bot.privmsg (replyto, '%s degrees celsius, %s degrees fahrenheit' % (celvalue, fahrvalue)) + return + + command_error ('unknown valuetype %s, expected one of: degrees, radians (angle conversion), ' + + 'celsius, fahrenheit (temperature conversion)' % valuetype) + +def cmd_ud (bot, args, replyto, **rest): + try: + url = 'http://api.urbandictionary.com/v0/define?term=%s' % ('%20'.join (args)) + response = urllib2.urlopen (url).read() + data = json.loads (response) + + if not 'list' in data \ + or len(data['list']) == 0 \ + or not 'word' in data['list'][0] \ + or not 'definition' in data['list'][0]: + command_error ("couldn't find a definition of \002%s\002" % args['term']) + + word = data['list'][0]['word'] + definition = data['list'][0]['definition'].replace ('\r', ' ').replace ('\n', ' ').replace (' ', ' ') + up = data['list'][0]['thumbs_up'] + down = data['list'][0]['thumbs_down'] + bot.privmsg (replyto, "\002%s\002: %s\0033 %d\003 up,\0035 %d\003 down" % + (word, definition, up, down)) + except Exception as e: + command_error ('Urban dictionary lookup failed: %s' % e) \ No newline at end of file diff -r 2266d6d73de3 -r d67cc4fbc3f1 modulecore.py --- a/modulecore.py Sun Nov 09 19:59:10 2014 +0200 +++ b/modulecore.py Mon Nov 10 02:06:06 2014 +0200 @@ -1,5 +1,6 @@ import os import re +from configfile import Config Modules = {} Commands = {} @@ -75,25 +76,25 @@ 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) - + + if cmd['level'] == 'admin' \ + and not "%s@%s" % (kvargs['ident'], kvargs['host']) in Config.get_value ('admins', default=[]): + command_error ("you may not use %s" % cmdname) + match = re.compile (cmd['regex']).match (message) - + if match == None: # didn't match + print "regex: %s" % cmd['regex'] 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 @@ -116,6 +117,8 @@ if arg == '': continue + + gotliteral = False if arg[0] == '[' and arg[-1] == ']': arg = arg[1:-1] @@ -126,20 +129,20 @@ arg = arg[1:-1] else: - raise CommandError ('badly formed argument list') - #fi + gotliteral = True if arg[-3:] == '...': gotvariadic = True arg = arg[0:-3] - #fi if gotoptional == False: regex += '\s+' else: regex += '\s*' - if gotoptional: + if gotliteral: + regex += arg + elif gotoptional: if gotvariadic: regex += r'(.*)' else: