- modularized the configuration and made it more systematic

Sun, 09 Nov 2014 19:13:08 +0200

author
Teemu Piippo <crimsondusk64@gmail.com>
date
Sun, 09 Nov 2014 19:13:08 +0200
changeset 65
20bd76353eb5
parent 64
384167adad2b
child 66
74d8ca04ff01

- modularized the configuration and made it more systematic

.hgignore file | annotate | diff | comparison | revisions
cmd_admin.py file | annotate | diff | comparison | revisions
cmd_config.py file | annotate | diff | comparison | revisions
cmd_idgames.py file | annotate | diff | comparison | revisions
cobalt.py file | annotate | diff | comparison | revisions
commandhandler.py file | annotate | diff | comparison | revisions
configfile.py file | annotate | diff | comparison | revisions
mod_admin.py file | annotate | diff | comparison | revisions
mod_config.py file | annotate | diff | comparison | revisions
mod_idgames.py file | annotate | diff | comparison | revisions
modulecore.py file | annotate | diff | comparison | revisions
--- a/.hgignore	Wed Nov 05 01:38:08 2014 +0200
+++ b/.hgignore	Sun Nov 09 19:13:08 2014 +0200
@@ -1,5 +1,6 @@
 syntax:glob
-cobalt.json
+cobalt*.json*
 untracked
 commits.txt
 *.pyc
+zandronum*
--- a/cmd_admin.py	Wed Nov 05 01:38:08 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-from commandhandler import command_error
-import hgapi
-
-ModuleData = {
-	'commands':
-	[
-		{
-			'name': 'raw',
-			'description': 'Sends a raw message to the server',
-			'args': '<message...>',
-			'level': 'admin',
-		},
-
-		{
-			'name': 'msg',
-			'description': 'Sends a message to someone',
-			'args': '<recipient> <message...>',
-			'level': 'admin',
-		},
-
-		{
-			'name': 'restart',
-			'description': 'Restarts the bot',
-			'args': None,
-			'level': 'admin',
-		},
-
-		{
-			'name': 'update',
-			'description': 'Checks for updates on the bot',
-			'args': None,
-			'level': 'admin'
-		}
-	]
-}
-
-def cmd_raw (bot, args, **rest):
-	bot.write (args['message'])
-
-def cmd_msg (bot, args, **rest):
-	bot.privmsg (args['recipient'], args['message'])
-
-def cmd_restart (bot, **rest):
-	bot.restart()
-
-def cmd_update (bot, replyto, **rest):
-	try:
-		repo = hgapi.Repo ('.')
-		r1 = repo.hg_id()
-		repo.hg_pull()
-		repo.hg_update('tip', True)
-		r2 = repo.hg_id()
-		if r1 != r2:
-			bot.privmsg (replyto, 'Updated to %s, restarting...' % r2)
-			bot.restart()
-		else:
-			bot.privmsg (replyto, 'Up to date at %s.' % r2)
-	except hgapi.HgException as e:
-		command_error ('Update failed: %s' % str (e))
--- a/cmd_config.py	Wed Nov 05 01:38:08 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-from commandhandler import command_error
-import hgapi
-
-ModuleData = {
-	'commands':
-	[
-		{
-			'name': 'addchan',
-			'description': 'Adds a channel to config',
-			'args': '<channel>',
-			'level': 'admin',
-		},
-
-		{
-			'name': 'delchan',
-			'description': 'Deletes a channel from config',
-			'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
-
-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
--- a/cmd_idgames.py	Wed Nov 05 01:38:08 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-from commandhandler import command_error
-import hgapi
-import urllib
-import urllib2
-import json
-
-ModuleData = {
-	'commands':
-	[
-		{
-			'name': 'idgames',
-			'description': 'Searches doomworld.com/idgames for wads',
-			'args': '<wad...>',
-			'level': 'normal',
-		},
-	]
-}
-
-g_idgamesSearchURL = 'http://www.doomworld.com/idgames/api/api.php?action=search&query=%s&type=title&sort=date&out=json'
-
-def cmd_idgames (bot, args, replyto, **rest):
-	try:
-		url = g_idgamesSearchURL % urllib.quote (args['wad'])
-		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
-
-				bot.privmsg (replyto, '- %s: \'%s\' by \'%s\', rating: %s: %s' % \
-					(filedata['filename'], filedata['title'], filedata['author'], filedata['rating'], filedata['url']))
-
-				i += 1
-			#done
-			bot.privmsg (replyto, "(%d / %d results posted)" % (i, len(files)))
-		elif 'warning' in data and 'message' in data['warning']:
-			command_error (data['warning']['message'])
-		elif 'error' in data and 'message' in data['error']:
-			command_error (data['error']['message'])
-		else:
-			command_error ("Incomplete JSON response from doomworld.com/idgames")
-	except Exception as e:
-		command_error ('search failed: %s' % str (e))
-	#tried
-#enddef
\ No newline at end of file
--- a/cobalt.py	Wed Nov 05 01:38:08 2014 +0200
+++ b/cobalt.py	Sun Nov 09 19:13:08 2014 +0200
@@ -33,17 +33,19 @@
 import sys
 import traceback
 import re
-import json
 import urllib
 import urllib2
 import hgapi
 import os
 import suds
 import math
+import json
 from datetime import datetime
-import commandhandler as CommandHandler
+import modulecore as ModuleCore
+import configfile
+from configfile import Config
 
-CommandHandler.init_data()
+ModuleCore.init_data()
 
 try:
 	uid = os.geteuid()
@@ -53,17 +55,9 @@
 if uid == 0 and raw_input ('Do you seriously want to run cobalt as root? [y/N] ') != 'y':
 	quit()
 
-print 'Loading configuration...'
-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']
-
+configfile.init()
+g_admins = Config.get_value ('admins', default=[])
+g_mynick = Config.get_value ('nickname', default='cobalt')
 g_BotActive = False
 g_needCommitsTxtRebuild = True
 
@@ -84,35 +78,28 @@
 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)
+	btannounce_timeout = time.time() + (Config.get_node ('bt').get_value ('checkinterval', default=5) * 60)
+
+def bt_credentials():
+	bt = Config.get_node ('bt')
+	user = bt.get_value ('username', '')
+	password = bt.get_value ('password', '')
+	return [user, password]
 
 if suds_active:
-	try:
-		sys.stdout.write ('Retrieving latest tracker ticket... ')
-		btannounce_id = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config['trackerpassword'], 0)
-		btannounce_active = True
-		bt_updatechecktimeout()
-		print btannounce_id
-	except Exception as e:
-		pass
+	sys.stdout.write ('Retrieving latest tracker ticket... ')
+	user, password = bt_credentials()
+	btannounce_id = suds_client.service.mc_issue_get_biggest_id (user, password, 0)
+	btannounce_active = True
+	bt_updatechecktimeout()
+	print btannounce_id
 
 def bt_getissue(ticket):
 	global suds_client
-	global g_config
-	return suds_client.service.mc_issue_get (g_config['trackeruser'], g_config['trackerpassword'], ticket)
+	user, password = bt_credentials()
+	return suds_client.service.mc_issue_get (user, password, ticket)
 
 def bt_checklatest():
 	global btannounce_timeout
@@ -122,7 +109,8 @@
 		bt_updatechecktimeout()
 		newid = btannounce_id
 		try:
-			newid = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config['trackerpassword'], 0)
+			user, password = bt_credentials()
+			newid = suds_client.service.mc_issue_get_biggest_id (user, password, 0)
 		except Exception as e:
 			pass
 
@@ -162,7 +150,7 @@
 			continue
 
 		for channel in client.channels:
-			if channel['logchannel']:
+			if channel.get_value ('logchannel', default=False):
 				client.write ("PRIVMSG %s :%s" % (channel['name'], line))
 
 #
@@ -275,7 +263,7 @@
 check_repo_exists ('zandronum-sandbox-stable', 'crimsondusk')
 check_repo_exists ('zandronum-everything', '')
 
-repocheck_timeout = {'zandronum':(time.time()) + 15, 'zandronum-stable':(time.time() + 15), 'zandronum-sandbox':(time.time()) + 15, 'zandronum-sandbox-stable':(time.time()) + 15}
+repocheck_timeout = {(time.time()) + 15}
 
 def get_commit_data (zanrepo, rev, template):
 	return zanrepo.hg_command ('log', '-l', '1', '-r', rev, '--template', template)
@@ -318,7 +306,7 @@
 #enddef
 
 def find_developer_by_email (commit_email):
-	for developer, emails in g_config['developer_emails'].iteritems():
+	for developer, emails in Config.get_value ('developer_emails', default={}).iteritems():
 		for email in emails:
 			if commit_email == email:
 				return developer
@@ -334,19 +322,24 @@
 def process_zan_repo_updates (repo_name):
 	global repocheck_timeout
 	global suds_client
-	global g_config
 	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[repo_name]:
+	if time.time() < repocheck_timeout:
 		return
 
-	repocheck_timeout[repo_name] = time.time() + (cfg ('hg_checkinterval', 15) * 60)
+	repocheck_timeout = time.time() + hgns.get_value ('checkinterval', default=15) * 60
 	zanrepo = hgapi.Repo (repo_name)
 	commit_data = []
 	delimeter = '@@@@@@@@@@'
@@ -484,8 +477,7 @@
 			commit_email = ""
 
 			try:
-				ticket_data = suds_client.service.mc_issue_get (g_config['trackeruser'],
-					g_config['trackerpassword'], ticket_id)
+				ticket_data = suds_client.service.mc_issue_get (btuser, btpassword, ticket_id)
 			except Exception as e:
 				chanlog ('error while processing %s: %s' % (commit_node, `e`))
 				continue
@@ -552,12 +544,12 @@
 
 			# Announce on IRC
 			for irc_client in g_clients:
-				for channel in irc_client.cfg['channels']:
-					if 'btannounce' in channel and channel['btannounce'] == True:
-						irc_client.privmsg (channel['name'],
+				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['name'],
+						irc_client.privmsg (channel.get_value ('name'),
 							"Read all about it here: " + irc_client.get_ticket_url (ticket_id))
 					#fi
 				#done
@@ -567,10 +559,10 @@
 				# 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 (g_config['trackeruser'], g_config['trackerpassword'], ticket_id, ticket_data)
+				suds_client.service.mc_issue_update (btuser, btpassword, ticket_id, ticket_data)
 			#fi
 
-			suds_client.service.mc_issue_note_add (g_config['trackeruser'], g_config['trackerpassword'], ticket_id, { 'text': message })
+			suds_client.service.mc_issue_note_add (btuser, btpassword, ticket_id, { 'text': message })
 			num_commits += 1
 		except Exception as e:
 			chanlog ('Error while processing %s: %s' % (commit_node, `e`))
@@ -589,44 +581,33 @@
 #
 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.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 = list()
-		self.umode = cfg['umode'] if 'umode' in cfg else ''
+		self.send_buffer = []
+		self.umode = cfg.get_value ('umode', default='')
 		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 '.'
+		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='.')
 
-		for channel in self.channels:
-			if not 'logchannel' in channel:
-				channel['logchannel'] = False
-			channel['namesdone'] = True
-			#channel['haslinkbot'] = False
-
-		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'])
+		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):
-		self.mynick = self.desired_name
 		print "Connected to [%s] %s:%d" % (self.name, self.host, self.port)
 		self.register_to_irc()
 
@@ -675,19 +656,15 @@
 					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 ''))
+						for channel in self.channels:
+							self.write ("JOIN %s %s" % (channel.get_value ('name'), channel.get_value ('password', default='')))
 
-						if 'umode' in self.cfg:
-							self.write ('MODE %s %s' % (self.mynick, self.cfg['umode']))
+						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] == 'JOIN':
-						rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) JOIN :#(.+)')
-						match = rex.match (line)
-
-						#if match and match.group(1).toLower() == 'linkbot':
-							#channel_by_name (match.group(4))['haslinkbot'] = True
 					elif words[1] == 'QUIT':
 						rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) QUIT')
 						match = rex.match (line)
@@ -696,14 +673,10 @@
 						if match and match.group(1) == self.desired_name:
 							self.mynick = self.desired_name
 							self.write ("NICK %s" % self.mynick)
-
-						#if match and match.group(1).toLower() == 'linkbot':
-							#for channel in self.channels:
-								#channels['haslinkbot'] = False
 					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)
+						self.mynick += self.cfg.get_value ('conflictsuffix', default='`')
+						self.write ("NICK " + self.mynick)
 
 			# Check for new issues on the bugtracker
 			bt_checklatest()
@@ -714,7 +687,7 @@
 
 	def channel_by_name (self, name):
 		for channel in self.channels:
-			if channel['name'].upper() == args[0].upper():
+			if channel.get_value ('name').upper() == args[0].upper():
 				return channel
 		else:
 			raise logical_exception ('unknown channel ' + args[0])
@@ -735,7 +708,8 @@
 			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'])
+			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.
@@ -759,7 +733,8 @@
 	#	Get the URL for a specified ticket
 	#
 	def get_ticket_url (self, ticket):
-		return 'https://%s/view.php?id=%s' % (g_config['trackerurl'], ticket)
+		url = Config.get_node ('bt').get_value ('url')
+		return 'https://%s/view.php?id=%s' % (url, ticket)
 
 	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 	#
@@ -780,7 +755,7 @@
 				allowprivate = False
 
 				for channel in self.channels:
-					if channel['name'] == replyto and 'btprivate' in channel and channel['btprivate'] == True:
+					if channel.get_value ('name') == replyto and channel.get_value ('btprivate', False):
 						allowprivate = True
 						break
 					#fi
@@ -806,9 +781,6 @@
 		#fi
 	#enddef
 
-	def save_config (self):
-		save_config()
-
 	def is_admin (self, ident, host):
 		return ("%s@%s" % (ident, host)) in g_admins
 
@@ -820,13 +792,11 @@
 		kvargs = {'sender': sender, 'ident': ident, 'host': host, 'replyto': replyto, 'cmdname': command, 'message': message}
 
 		try:
-			result = CommandHandler.call_command (self, **kvargs)
+			result = ModuleCore.call_command (self, **kvargs)
 
 			if result:
 				return
-			else:
-				print 'CommandHandler.call_command returned false'
-		except CommandHandler.CommandError as e:
+		except ModuleCore.CommandError as e:
 			lines = str (e).split ('\n')
 			self.privmsg (replyto, 'error: %s' % lines[0])
 
@@ -1030,9 +1000,9 @@
 		isprivate = data['view_state']['name'] == 'private'
 		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:
-				if not isprivate or ('btprivate' in channel and channel['btprivate'] == True):
+		for channel in self.channels:
+			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)))
@@ -1068,14 +1038,20 @@
 # Main procedure:
 #
 try:
-	for aconn in g_config['autoconnect']:
-		for conndata in g_config['connections']:
-			if conndata['name'] == aconn:
+	autoconnects = Config.get_value ('autoconnect', [])
+	
+	if len (autoconnects) == 0:
+		print "Nowhere to connect."
+		quit()
+	
+	for aconn in autoconnects:
+		for conndata in Config.get_nodelist ('connections'):
+			if conndata.get_value ('name') == aconn:
 				irc_client (conndata, 0)
 				break
 		else:
-			raise logical_exception ("unknown autoconnect entry %s" % (aconn))
-
+			raise ValueError ("unknown autoconnect entry %s" % (aconn))
+	
 	g_BotActive = True
 	asyncore.loop()
 except KeyboardInterrupt:
--- a/commandhandler.py	Wed Nov 05 01:38:08 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,156 +0,0 @@
-import os
-import re
-
-CommandModules = {}
-Commands = {}
-
-class CommandError (Exception):
-	def __init__ (self, value):
-		self.value = value
-	def __str__ (self):
-		return self.value
-
-#
-# init_data()
-#
-# Initializes command module data
-#
-def init_data():
-	global Commands
-	global CommandModules
-	files = os.listdir ('.')
-
-	for fn in files:
-		if fn[0:4] != 'cmd_' or fn[-3:] != '.py':
-			continue
-
-		fn = fn[0:-3]
-		globals = {}
-		module = __import__ (fn)
-		CommandModules[fn] = module
-
-		for cmd in module.ModuleData['commands']:
-			if cmd['args'] == None:
-				cmd['args'] = ''
-
-			cmd['module'] = module
-			cmd['regex'] = make_regex (cmd['args'])
-			cmd['argnames'] = []
-			Commands[cmd['name']] = cmd
-
-			for argname in cmd['args'].split (' '):
-				argname = argname[1:-1]
-
-				if argname[-3:] == '...':
-					argname = argname[0:-3]
-
-				if argname == '':
-					continue
-
-				cmd['argnames'].append (argname)
-			#done
-		#done
-
-		print "Loaded command module %s" % fn
-	#done
-
-	print 'Loaded %d commands in %d modules' % (len (Commands), len (CommandModules))
-#enddef
-
-#
-# command_error (message)
-#
-# Raises a command error
-#
-def command_error (message):
-	raise CommandError (message)
-
-#
-# call_command (bot, message, cmdname, **kvargs)
-#
-# Calls a cobalt command
-#
-def call_command (bot, message, cmdname, **kvargs):
-	try:
-		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)
-
-	match = re.compile (cmd['regex']).match (message)
-
-	if match == None:
-		# didn't match
-		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
-
-#
-# make_regex
-#
-# Takes the argument list and returns a corresponding regular expression
-#
-def make_regex (arglist):
-	if arglist == None:
-		return '^.+$'
-
-	gotoptional = False
-	gotvariadic = False
-	regex = ''
-
-	for arg in arglist.split (' '):
-		if gotvariadic:
-			raise CommandError ('variadic argument is not last')
-
-		if arg == '':
-			continue
-
-		if arg[0] == '[' and arg[-1] == ']':
-			arg = arg[1:-1]
-			gotoptional = True
-		elif arg[0] == '<' and arg[-1] == '>':
-			if gotoptional:
-				raise CommandError ('mandatory argument after optional one')
-
-			arg = arg[1:-1]
-		else:
-			raise CommandError ('badly formed argument list')
-		#fi
-
-		if arg[-3:] == '...':
-			gotvariadic = True
-			arg = arg[0:-3]
-		#fi
-
-		if gotoptional == False:
-			regex += '\s+'
-		else:
-			regex += '\s*'
-
-		if gotoptional:
-			if gotvariadic:
-				regex += r'(.*)'
-			else:
-				regex += r'([^ ]*)'
-		else:
-			if gotvariadic:
-				regex += r'(.+)'
-			else:
-				regex += r'([^ ]+)'
-		#fi
-	#done
-
-	return '^[^ ]+%s$' % regex
-#enddef
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/configfile.py	Sun Nov 09 19:13:08 2014 +0200
@@ -0,0 +1,66 @@
+import json
+
+class ConfigNode:
+	def __init__ (self, obj, name, parent):
+		self.obj = obj
+		self.name = name
+		self.root = parent if parent == None or parent.root == None else parent.root
+	
+	def keyname (self, key):
+		if self.name == None:
+			return key
+		return self.name + ':' + key
+	
+	def get_value (self, key, default=None):
+		i = 0
+		needSave = False
+		
+		if not key in self.obj:
+			if default == None:
+				raise ValueError ('Mandatory key \'%s\' not found' % self.keyname (key))
+			self.obj[key] = default
+			self.save()
+		
+		return self.obj[key]
+	
+	def set_value (self, key, value):
+		self.obj[key] = value
+		self.save()
+	
+	def get_node (self, key):
+		return ConfigNode (obj=self.get_value (key, {}), name=self.keyname (key), parent=self)
+	
+	def get_nodelist (self, key):
+		data = self.get_value (key)
+		result = []
+		
+		for entry in data:
+			node = ConfigNode (obj=entry, name=self.keyname (key), parent=self)
+			result.append (node)
+		
+		return result
+	
+	def save (self):
+		if self.root != None:
+			self.root.save()
+			return
+		
+		with open ('cobalt.json', 'w') as fp:
+			json.dump (self.obj, fp, sort_keys = True, indent = 1)
+		
+		print "Config saved."
+
+def init():
+	print 'Loading configuration...'
+
+	try:
+		with open ('cobalt.json', 'r') as fp:
+			jsondata = json.loads (fp.read())
+	except IOError as e:
+		print 'couldn\'t open cobalt.json: %s' % e
+		quit()
+	
+	global Config
+	Config = ConfigNode (jsondata, name=None, parent=None)
+
+init()
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_admin.py	Sun Nov 09 19:13:08 2014 +0200
@@ -0,0 +1,59 @@
+from modulecore import command_error
+import hgapi
+
+ModuleData = {
+	'commands':
+	[
+		{
+			'name': 'raw',
+			'description': 'Sends a raw message to the server',
+			'args': '<message...>',
+			'level': 'admin',
+		},
+
+		{
+			'name': 'msg',
+			'description': 'Sends a message to someone',
+			'args': '<recipient> <message...>',
+			'level': 'admin',
+		},
+
+		{
+			'name': 'restart',
+			'description': 'Restarts the bot',
+			'args': None,
+			'level': 'admin',
+		},
+
+		{
+			'name': 'update',
+			'description': 'Checks for updates on the bot',
+			'args': None,
+			'level': 'admin'
+		}
+	]
+}
+
+def cmd_raw (bot, args, **rest):
+	bot.write (args['message'])
+
+def cmd_msg (bot, args, **rest):
+	bot.privmsg (args['recipient'], args['message'])
+
+def cmd_restart (bot, **rest):
+	bot.restart()
+
+def cmd_update (bot, replyto, **rest):
+	try:
+		repo = hgapi.Repo ('.')
+		r1 = repo.hg_id()
+		repo.hg_pull()
+		repo.hg_update('tip', True)
+		r2 = repo.hg_id()
+		if r1 != r2:
+			bot.privmsg (replyto, 'Updated to %s, restarting...' % r2)
+			bot.restart()
+		else:
+			bot.privmsg (replyto, 'Up to date at %s.' % r2)
+	except hgapi.HgException as e:
+		command_error ('Update failed: %s' % str (e))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_config.py	Sun Nov 09 19:13:08 2014 +0200
@@ -0,0 +1,103 @@
+from modulecore import command_error
+from configfile import Config
+import hgapi
+
+ModuleData = {
+	'commands':
+	[
+		{
+			'name': 'addchan',
+			'description': 'Adds a channel to config',
+			'args': '<channel>',
+			'level': 'admin',
+		},
+
+		{
+			'name': 'delchan',
+			'description': 'Deletes a channel from config',
+			'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
+
+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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_idgames.py	Sun Nov 09 19:13:08 2014 +0200
@@ -0,0 +1,54 @@
+from modulecore import command_error
+import hgapi
+import urllib
+import urllib2
+import json
+
+ModuleData = {
+	'commands':
+	[
+		{
+			'name': 'idgames',
+			'description': 'Searches doomworld.com/idgames for wads',
+			'args': '<wad...>',
+			'level': 'normal',
+		},
+	]
+}
+
+g_idgamesSearchURL = 'http://www.doomworld.com/idgames/api/api.php?action=search&query=%s&type=title&sort=date&out=json'
+
+def cmd_idgames (bot, args, replyto, **rest):
+	try:
+		url = g_idgamesSearchURL % urllib.quote (args['wad'])
+		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
+
+				bot.privmsg (replyto, '- %s: \'%s\' by \'%s\', rating: %s: %s' % \
+					(filedata['filename'], filedata['title'], filedata['author'], filedata['rating'], filedata['url']))
+
+				i += 1
+			#done
+			bot.privmsg (replyto, "(%d / %d results posted)" % (i, len(files)))
+		elif 'warning' in data and 'message' in data['warning']:
+			command_error (data['warning']['message'])
+		elif 'error' in data and 'message' in data['error']:
+			command_error (data['error']['message'])
+		else:
+			command_error ("Incomplete JSON response from doomworld.com/idgames")
+	except Exception as e:
+		command_error ('search failed: %s' % str (e))
+	#tried
+#enddef
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modulecore.py	Sun Nov 09 19:13:08 2014 +0200
@@ -0,0 +1,156 @@
+import os
+import re
+
+Modules = {}
+Commands = {}
+
+class CommandError (Exception):
+	def __init__ (self, value):
+		self.value = value
+	def __str__ (self):
+		return self.value
+
+#
+# init_data()
+#
+# Initializes command module data
+#
+def init_data():
+	global Commands
+	global Modules
+	files = os.listdir ('.')
+
+	for fn in files:
+		if fn[0:4] != 'mod_' or fn[-3:] != '.py':
+			continue
+
+		fn = fn[0:-3]
+		globals = {}
+		module = __import__ (fn)
+		Modules[fn] = module
+
+		for cmd in module.ModuleData['commands']:
+			if cmd['args'] == None:
+				cmd['args'] = ''
+
+			cmd['module'] = module
+			cmd['regex'] = make_regex (cmd['args'])
+			cmd['argnames'] = []
+			Commands[cmd['name']] = cmd
+
+			for argname in cmd['args'].split (' '):
+				argname = argname[1:-1]
+
+				if argname[-3:] == '...':
+					argname = argname[0:-3]
+
+				if argname == '':
+					continue
+
+				cmd['argnames'].append (argname)
+			#done
+		#done
+
+		print "Loaded module %s" % fn
+	#done
+
+	print 'Loaded %d commands in %d modules' % (len (Commands), len (Modules))
+#enddef
+
+#
+# command_error (message)
+#
+# Raises a command error
+#
+def command_error (message):
+	raise CommandError (message)
+
+#
+# call_command (bot, message, cmdname, **kvargs)
+#
+# Calls a cobalt command
+#
+def call_command (bot, message, cmdname, **kvargs):
+	try:
+		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)
+
+	match = re.compile (cmd['regex']).match (message)
+
+	if match == None:
+		# didn't match
+		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
+
+#
+# make_regex
+#
+# Takes the argument list and returns a corresponding regular expression
+#
+def make_regex (arglist):
+	if arglist == None:
+		return '^.+$'
+
+	gotoptional = False
+	gotvariadic = False
+	regex = ''
+
+	for arg in arglist.split (' '):
+		if gotvariadic:
+			raise CommandError ('variadic argument is not last')
+
+		if arg == '':
+			continue
+
+		if arg[0] == '[' and arg[-1] == ']':
+			arg = arg[1:-1]
+			gotoptional = True
+		elif arg[0] == '<' and arg[-1] == '>':
+			if gotoptional:
+				raise CommandError ('mandatory argument after optional one')
+
+			arg = arg[1:-1]
+		else:
+			raise CommandError ('badly formed argument list')
+		#fi
+
+		if arg[-3:] == '...':
+			gotvariadic = True
+			arg = arg[0:-3]
+		#fi
+
+		if gotoptional == False:
+			regex += '\s+'
+		else:
+			regex += '\s*'
+
+		if gotoptional:
+			if gotvariadic:
+				regex += r'(.*)'
+			else:
+				regex += r'([^ ]*)'
+		else:
+			if gotvariadic:
+				regex += r'(.+)'
+			else:
+				regex += r'([^ ]+)'
+		#fi
+	#done
+
+	return '^[^ ]+%s$' % regex
+#enddef
\ No newline at end of file

mercurial