- modularization complete!!

Mon, 10 Nov 2014 02:06:06 +0200

author
Teemu Piippo <crimsondusk64@gmail.com>
date
Mon, 10 Nov 2014 02:06:06 +0200
changeset 73
d67cc4fbc3f1
parent 72
2266d6d73de3
child 74
9f14fb6995ff

- modularization complete!!

LICENSE file | annotate | diff | comparison | revisions
bt.py file | annotate | diff | comparison | revisions
cobalt.py file | annotate | diff | comparison | revisions
configfile.py file | annotate | diff | comparison | revisions
hgpoll.py file | annotate | diff | comparison | revisions
irc.py file | annotate | diff | comparison | revisions
mod_admin.py file | annotate | diff | comparison | revisions
mod_bt.py file | annotate | diff | comparison | revisions
mod_config.py file | annotate | diff | comparison | revisions
mod_hgpoll.py file | annotate | diff | comparison | revisions
mod_util.py file | annotate | diff | comparison | revisions
modulecore.py file | annotate | diff | comparison | revisions
--- /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
--- 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 "<unknown>", \
+			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 })
--- 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 <value> as <valuetype>" % 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 <word>' % 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
--- 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()
--- 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
--- /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
--- 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
--- 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 "<unknown>", \
-			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 <ticket>" % 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
--- 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': '<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
+	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
--- 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']
--- /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': '<value> as <valuetype>',
+			'level': 'normal',
+		},
+		
+		{
+			'name': 'ud',
+			'description': 'Looks up a term in urban dictionary',
+			'args': '<term...>',
+			'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
--- 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:

mercurial