- initial commit

Tue, 13 May 2014 23:17:01 +0300

author
Santeri Piippo <crimsondusk64@gmail.com>
date
Tue, 13 May 2014 23:17:01 +0300
changeset 0
4904744b40eb
child 1
29c7e9d13a30

- 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"
+				},
+			]
+		},
+	]
+}

mercurial