Tue, 13 May 2014 23:17:01 +0300
- initial commit
.hgignore | file | annotate | diff | comparison | revisions | |
cobalt.py | file | annotate | diff | comparison | revisions | |
template.json | file | annotate | diff | comparison | revisions |
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Tue May 13 23:17:01 2014 +0300 @@ -0,0 +1,2 @@ +cobalt.json +untracked
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cobalt.py Tue May 13 23:17:01 2014 +0300 @@ -0,0 +1,267 @@ +#!/usr/bin/env python +''' + Copyright 2014 Santeri 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. +''' + +import asyncore +import socket +import time +import sys +import traceback +import re +import json +from suds.xsd.doctor import Import +from suds.xsd.doctor import ImportDoctor +from suds.client import Client + +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'] + +# +# SOAP stuff +# +suds_import = Import ('http://schemas.xmlsoap.org/soap/encoding/', \ + 'http://schemas.xmlsoap.org/soap/encoding/') +suds_client = Client ('https://zandronum.com/tracker/api/soap/mantisconnect.php?wsdl', \ + plugins=[ImportDoctor (suds_import)]) + +# +# irc_client flags +# +CLIF_CONTROL = (1 << 0) +CLIF_CONNECTED = (1 << 1) + +# +# List of all clients +# +g_clients = [] + +class channel (object): + name = "" + password = "" + + def __init__ (self, name): + self.name = name + +# +# Prints a line to control channel(s) +# +def control (line): + for client in g_clients: + if client.flags & (CLIF_CONTROL|CLIF_CONNECTED) == (CLIF_CONTROL|CLIF_CONNECTED): + client.write ("PRIVMSG %s :%s" % (client.channels[0]['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 + control (line) + for client in g_clients: + client.exceptdie() + quit() + +sys.excepthook = handle_exception + +def check_admin (sender, ident, host): + if not "%s!%s@%s" % (sender, user, host) in g_admins: + raise ".%s requires admin access" % command + +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.flags = flags + self.send_buffer = list() + self.umode = cfg['umode'] if 'umode' in cfg else '' + self.cfg = cfg + self.mynick = '' + g_clients.append (self) + asyncore.dispatcher.__init__ (self) + self.create_socket (socket.AF_INET, socket.SOCK_STREAM) + self.connect ((self.host, self.port)) + + def handle_connect (self): + nick = self.cfg['nickname'] if 'nickname' in self.cfg else g_config['nickname'] + 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'] + self.mynick = nick + print "Connected to [%s] %s:%d" % (self.name, self.host, self.port) + if 'password' in self.cfg: + self.write ("PASS %s" % self.cfg['password']) + self.write ("USER %s * * :%s" % (ident, gecos)) + self.write ("NICK %s" % nick) + + def write (self, data): + self.send_buffer.append ("%s" % data) + + 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: + print "[%s] <- %s" % (self.name, line) + self.send ("%s\n" % line) + self.send_buffer = [] + + def handle_read (self): + lines = self.recv (4096).splitlines() + for line in lines: + print "[%s] -> %s" % (self.name, line) + + if line.startswith ("PING :"): + self.write ("PONG :%s" % line[6:]) + continue + + words = line.split(" ") + if len(words) >= 2 and 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 '')) + + if 'umode' in self.cfg: + self.write ('MODE %s %s' % (self.mynick, self.cfg['umode'])) + + if len(words) >= 2 and words[1] == "PRIVMSG": + self.handle_privmsg (line) + + 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 + http_regex = re.compile (r'.*http(s?)://%s/view\.php\?id=([0-9]+).*' % g_config['trackerurl']) + http_match = http_regex.match (line) + + # Check for command. + if len(message) >= 2 and message[0] == '.' and message[1] != '.': + stuff = message[1:].split(' ') + command = stuff[0] + args = stuff[1:] + try: + handle_command (sender, user, host, replyto, command, args) + except str as msg: + privmsg (replyto, "error: %s" % msg) + elif http_match: + self.get_ticket_data (replyto, http_match.group (2), False) + else: + control ("Recieved bad PRIVMSG: %s" % line) + + def get_ticket_data (self, replyto, ticket, withlink): + data = {} + try: + data = suds_client.service.mc_issue_get (g_config['trackeruser'], g_config ['trackerpassword'], ticket) + except Exception, e: + self.privmsg (replyto, "Failed to get info for issue %s: %s" % (ticket, `e`)) + + if data: + self.privmsg (replyto, "Issue %s: %s: Reporter: %s, assigned to: %s, status: %s (%s)" % \ + (ticket, \ + data.summary, \ + data.reporter.name, \ + data.handler.name if hasattr (data, 'handler') else "nobody", \ + data.status.name, \ + data.resolution.name)) + + if withlink: + self.privmsg (replyto, "Read all about it here: https://%s/view.php?id=%s" % (g_config['trackerurl'], ticket)) + + def handle_command (self, sender, user, host, replyto, command, args): + if command == "raw": + check_admin (sender, ident, host) + self.write (" ".join (args)) + elif command == "msg": + check_admin (sender, ident, host) + if len(args) < 2: + raise "usage: .%s <target> <message...>" % command + self.privmsg (args[0], " ".join (args[1:])) + elif command == "ticket": + if len(args) != 1: + raise "usage: .%s <ticket>" % command + self.get_ticket_data (replyto, args[0], True) + else: + self.privmsg (replyto, "unknown command `.%s`" % command) + + def handle_error(self): + excepterm (traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)) + + def privmsg (self, channel, msg): + self.write ("PRIVMSG %s :%s" % (channel, msg)) + + def exceptdie (self): + if self.flags & CLIF_CONNECTED: + self.write ("QUIT :Caught exception") + self.send_all_now() + self.close() + + def keyboardinterrupt (self): + if self.flags & CLIF_CONNECTED: + self.write ("QUIT :KeyboardInterrupt") + self.send_all_now() + self.close() + +try: + for conndata in g_config['connections']: + irc_client (conndata, CLIF_CONTROL) + asyncore.loop() +except KeyboardInterrupt: + for client in g_clients: + client.keyboardinterrupt() + quit()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/template.json Tue May 13 23:17:01 2014 +0300 @@ -0,0 +1,34 @@ +{ + "nickname": "cobalt-mk2", + "ident": "cobalt", + "gecos": "Cobalt MK2", + "trackerurl": "localhost/mantisbt", + "trackeruser": "cobalt", + "trackerpassword": "<insert password here>", + "commandprefix": ".", + "admins": + [ + "admin!admin@admin", + "admin!admin@admin", + "admin!admin@admin", + ], + "connections": + [ + { + "name": "local", + "address": "127.0.0.1", + "port": 6667, + "umode": "+iw", + "channels": + [ + { + "name": "#foo" + }, + { + "name": "#bar", + "password": "baz" + }, + ] + }, + ] +}