cobalt.py

Sat, 14 Jun 2014 16:24:03 +0300

author
Santeri Piippo <crimsondusk64@gmail.com>
date
Sat, 14 Jun 2014 16:24:03 +0300
changeset 14
558379fd6d6a
parent 13
4da122a2f79f
child 15
e7999383db5a
permissions
-rwxr-xr-x

- heartbeat the issue announcer even if we just get a ping

#!/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
import urllib
import urllib2
import hgapi
import os
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']

g_idgamesSearchURL = 'http://www.doomworld.com/idgames/api/api.php?action=search&query=%s&type=title&sort=date&out=json'

#
# SOAP stuff
#
suds_active = False

try:
	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)])
	suds_active = True
except Exception:
	pass

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)

if suds_active:
	try:
		btannounce_id = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config ['trackerpassword'], 0)
		btannounce_active = True
		bt_updatechecktimeout()
		print "Latest ticket on tracker: %d" % btannounce_id
	except Exception as e:
		pass

def bt_getissue(ticket):
	global suds_client
	global g_config
	return suds_client.service.mc_issue_get (g_config['trackeruser'], g_config['trackerpassword'], ticket)

def bt_checklatest():
	global btannounce_timeout
	global btannounce_id

	if time.time() >= btannounce_timeout:
		bt_updatechecktimeout()
		newid = btannounce_id
		try:
			newid = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config ['trackerpassword'], 0)
		except Exception as e:
			pass

		while newid > btannounce_id:
			try:
				btannounce_id += 1
				data = bt_getissue (btannounce_id)

				for client in g_clients:
					client.announce_ticket (data)
			except Exception as e:
				pass

#
# 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:
		if len(data) > 0:
			client.exceptdie()
		else:
			client.quit_irc()
	restart_self()
	#quit()

sys.excepthook = handle_exception

def check_admin (sender, ident, host, command):
	if not "%s!%s@%s" % (sender, 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)

#
# Main IRC client class
#
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 = ''
		self.verbose = g_config['verbose'] if 'verbose' in g_config else False
		self.commandprefix = g_config['commandprefix'][0] if 'commandprefix' in g_config else '.'

		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'])
		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()

	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:
			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:
			line = utfline.decode("utf-8").encode("ascii","ignore")
			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.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']))
					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 = '%s%s' % (self.mynick, self.cfg['conflictsuffix'])
						self.write ("NICK %s" % self.mynick)

			# Check for new issues on the bugtracker
			bt_checklatest()

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
	#
	#	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
			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] == 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)
				except logical_exception as msg:
					self.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)

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
	#
	#	Get the URL for a specified ticket
	#
	def get_ticket_url (self, ticket):
		return 'https://%s/view.php?id=%s' % (g_config['trackerurl'], ticket)

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
	#
	#	Retrieve a ticket from mantisbt
	#
	def get_ticket_data (self, replyto, ticket, withlink):
		if suds_active == False:
			return

		data = {}
		try:
			data = bt_getissue (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 if hasattr (data.reporter, 'name') else "<unknown>", \
				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: " + self.get_ticket_url (ticket))

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
	#
	#	Process an IRC command
	#
	def handle_command (self, sender, ident, host, replyto, command, args):
		if command == "raw":
			check_admin (sender, ident, host, command)
			self.write (" ".join (args))
		elif command == "msg":
			check_admin (sender, ident, host, command)
			if len(args) < 2:
				raise logical_exception ("usage: .%s <target> <message...>" % command)
			self.privmsg (args[0], " ".join (args[1:]))
		elif command == 'ticket':
			if len(args) != 1:
				raise logical_exception ("usage: .%s <ticket>" % command)
			self.get_ticket_data (replyto, args[0], True)
		elif command == 'testannounce':
			check_admin (sender, ident, host, command)
			if len(args) != 1:
				raise logical_exception ("usage: .%s <ticket>" % command)
			self.announce_ticket (bt_getissue (args[0]))
		elif command == 'idgames':
			try:
				if len(args) < 1:
					raise logical_exception ('usage: .%s <keywords>' % command)

				url = g_idgamesSearchURL % urllib.quote (" ".join (args[0:]))
				response = urllib2.urlopen (url).read()
				data = json.loads (response)

				if 'content' in data and 'file' in data['content']:
					if type (data['content']['file']) is list:
						files = data['content']['file']
					else:
						files = [data['content']['file']]

					i = 0
					for filedata in files:
						if i >= 5:
							break

						self.privmsg (replyto, '- %s: \'%s\' by \'%s\', rating: %s: %s' % \
							(filedata['filename'], filedata['title'], filedata['author'], filedata['rating'], filedata['url']))

						i += 1
					self.privmsg (replyto, "(%d / %d results posted)" % (i, len(files)))
				elif 'warning' in data and 'message' in data['warning']:
					raise logical_exception (data['warning']['message'])
				elif 'error' in data and 'message' in data['error']:
					raise logical_exception (data['error']['message'])
				else:
					raise logical_exception ("Incomplete JSON response from doomworld.com/idgames")
			except logical_exception as e:
				raise e
			except Exception as e:
				raise logical_exception ('Search failed: %s' % `e`)
		elif command == 'restart':
			check_admin (sender, ident, host, command)
			excepterm('')
		elif command == 'update':
			check_admin (sender, ident, host, command)

			try:
				repo = hgapi.Repo ('.')
				r1 = repo.hg_id()
				repo.hg_pull()
				repo.hg_update('tip', True)
				r2 = repo.hg_id()
				if r1 != r2:
					self.privmsg (replyto, 'Updated to %s, restarting...' % r2)
					excepterm('')
				else:
					self.privmsg (replyto, 'Up to date at %s.' % r2)
			except hgapi.HgException as e:
				raise logical_exception ('Search failed: %s' % `e`)
		elif command == 'addchan':
			check_admin (sender, ident, host, command)
			if len(args) != 1:
				raise logical_exception ("usage: .%s <channel>" % command)

			for channel in self.channels:
				if channel['name'].upper() == args[0].upper():
					raise logical_exception ('I already know of %s!' % args[0])

			chan = {}
			chan['name'] = args[0]
			self.channels.append (chan)
			self.write ('JOIN ' + chan['name'])
			save_config()
		elif command == 'delchan':
			check_admin (sender, ident, host, command)
			if len(args) != 1:
				raise logical_exception ("usage: .%s <channel>" % command)

			for channel in self.channels:
				if channel['name'].upper() == args[0].upper():
					break;
			else:
				raise logical_exception ('unknown channel ' + args[0])

			self.channels.remove (channel)
			self.write ('PART ' + args[0])
			save_config()
		elif command == 'chanattr':
			check_admin (sender, ident, host, command)

			if len(args) < 2:
				raise logical_exception ("usage: .%s <attribute> <value...>" % command)

			for channel in self.channels:
				if channel['name'] == replyto:
					break
			else:
				raise logical_exception ('I don\'t know of a channel named ' + replyto)

			key = args[0]
			value = ' '.join (args[1:])

			if key == 'name':
				self.write ('PART ' + channel['name'])
				channel['name'] = value
				self.write ('JOIN ' + channel['name'] + ' ' + (channel['password'] if hasattr (channel, 'password') else ''))
			elif key == 'password':
				channel['password'] = value
			elif key == 'btannounce':
				if value != 'true' and value != 'false':
					raise logical_exception ('expected true or false for value')
				channel['btannounce'] = True if value == 'true' else False
			else:
				raise logical_exception ('unknown key ' + key)

			save_config()
		elif command == 'die':
			check_admin (sender, ident, host, command)
			quit()
#		else:
#			raise logical_exception ("unknown command `.%s`" % command)

	#
	# Print a ticket announce to appropriate channels
	#
	def announce_ticket (self, data):
		idstring = "%d" % data.id
		while len(idstring) < 7:
			idstring = "0" + idstring

		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:
				self.write ("PRIVMSG %s :New issue %s, reported by %s: %s: %s" % \
					(channel['name'], idstring, reporter, data['summary'], self.get_ticket_url (idstring)))

	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 quit_irc (self):
		if self.flags & CLIF_CONNECTED:
			self.write ("QUIT :Leaving")
		self.send_all_now()
		self.close()

	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()

#
# Main procedure:
#
try:
	for conndata in g_config['connections']:
		if conndata['name'] in g_config['autoconnect']:
			irc_client (conndata, CLIF_CONTROL if conndata['control'] else 0)
	asyncore.loop()
except KeyboardInterrupt:
	for client in g_clients:
		client.keyboardinterrupt()
	quit()

mercurial