- revamped commands, added a much more modular system. not everything migrated yet

Wed, 05 Nov 2014 00:19:15 +0200

author
Teemu Piippo <crimsondusk64@gmail.com>
date
Wed, 05 Nov 2014 00:19:15 +0200
changeset 62
052a8a1e3d7d
parent 61
8d1098095dec
child 63
a1a864c25e42

- revamped commands, added a much more modular system. not everything migrated yet

.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
--- a/.hgignore	Tue Nov 04 20:35:25 2014 +0200
+++ b/.hgignore	Wed Nov 05 00:19:15 2014 +0200
@@ -1,3 +1,5 @@
+syntax:glob
 cobalt.json
 untracked
 commits.txt
+*.pyc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd_admin.py	Wed Nov 05 00:19:15 2014 +0200
@@ -0,0 +1,59 @@
+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)
+			excepterm('')
+		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/cmd_config.py	Wed Nov 05 00:19:15 2014 +0200
@@ -0,0 +1,102 @@
+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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd_idgames.py	Wed Nov 05 00:19:15 2014 +0200
@@ -0,0 +1,54 @@
+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	Tue Nov 04 20:35:25 2014 +0200
+++ b/cobalt.py	Wed Nov 05 00:19:15 2014 +0200
@@ -41,6 +41,9 @@
 import suds
 import math
 from datetime import datetime
+import commandhandler as CommandHandler
+
+CommandHandler.init_data()
 
 try:
 	uid = os.geteuid()
@@ -61,7 +64,6 @@
 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'
 g_BotActive = False
 g_needCommitsTxtRebuild = True
 
@@ -97,11 +99,6 @@
 	global btannounce_timeout
 	btannounce_timeout = time.time() + (cfg ('btlatest_checkinterval', 5) * 60)
 
-def bool_from_string (value):
-	if value != 'true' and value != 'false':
-		raise logical_exception ('expected true or false for value')
-	return True if value == 'true' else False
-
 if suds_active:
 	try:
 		sys.stdout.write ('Retrieving latest tracker ticket... ')
@@ -327,6 +324,8 @@
 ' Retrieves and processes commits for zandronum repositories '
 ' Ensure both repositories are OK before using this! '
 def process_zan_repo_updates (repo_name):
+	return
+
 	global repocheck_timeout
 	global suds_client
 	global g_config
@@ -739,7 +738,7 @@
 				command = stuff[0]
 				args = stuff[1:]
 				try:
-					self.handle_command (sender, user, host, replyto, command, args)
+					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:
@@ -801,20 +800,36 @@
 		#fi
 	#enddef
 
+	def save_config (self):
+		save_config()
+
+	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):
-		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':
+	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 = CommandHandler.call_command (self, **kvargs)
+
+			if result:
+				return
+			else:
+				print 'CommandHandler.call_command returned false'
+		except CommandHandler.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 == 'ticket':
 			if len(args) != 1:
 				raise logical_exception ("usage: .%s <ticket>" % command)
 			self.get_ticket_data (replyto, args[0], True)
@@ -823,196 +838,6 @@
 			if len(args) != 1:
 				raise logical_exception ("usage: .%s <ticket>" % command)
 			self.announce_ticket (bt_getissue (args[0]))
-		elif command == 'multierror':
-			raise logical_exception ('a\nb\nc\n')
-		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 ('Update failed: %s' % str (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]
-			chan['logchannel'] = False
-			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) < 1:
-				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]
-
-			if len(args) < 2:
-				try:
-					self.privmsg (replyto, '%s = %s' % (key, channel[key]))
-				except KeyError:
-					self.privmsg (replyto, 'attribute %s is not set' % key)
-			else:
-				value = " ".join (args[1:])
-				if key == 'name':
-					if replyto == channel['name']:
-						replyto = value
-
-					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':
-					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:
-					raise logical_exception ('unknown key ' + key)
-
-				self.privmsg (replyto, '%s is now %s' % (key, channel[key]))
-
-			save_config()
-		elif command == 'devemail':
-			check_admin (sender, ident, host, command)
-
-			if len(args) < 2:
-				raise logical_exception ("usage: .%s <user> <email>" % command)
-			#fi
-
-			if not 'developer_emails' in g_config:
-				g_config['developer_emails'] = {}
-			#fi
-
-			user = ' '.join (args[0:-1])
-
-			if args[0] in g_config['developer_emails']:
-				g_config['developer_emails'][user].append (args[-1])
-			else:
-				g_config['developer_emails'][user] = [args[-1]]
-			#fi
-
-			self.privmsg (replyto, 'Developer emails for %s are now %s' %
-				(user, ', '.join (g_config['developer_emails'][user])))
-			save_config()
-		elif command == 'deldevemail':
-			check_admin (sender, ident, host, command)
-
-			if len(args) < 2:
-				raise logical_exception ("usage: .%s <user> <email>" % command)
-			#fi
-
-			if not 'developer_emails' in g_config:
-				g_config['developer_emails'] = {}
-			#fi
-
-			user = ' '.join (args[0:-1])
-
-			if user in g_config['developer_emails']:
-				try:
-					g_config['developer_emails'][user].remove (args[-1])
-				except:
-					pass
-				#tried
-
-				if len (g_config['developer_emails'][user]) == 0:
-					g_config['developer_emails'].pop (user)
-					self.privmsg (replyto, 'No more developer emails for %s' % user)
-				else:
-					self.privmsg (replyto, 'Developer emails for %s are now %s' %
-						(user, ', '.join (g_config['developer_emails'][user])))
-				#fi
-				save_config()
-			else:
-				self.privmsg (replyto, 'There is no developer \'%s\'' % user)
-			#fi
-		elif command == 'listdevemails':
-			check_admin (sender, ident, host, command)
-
-			if 'developer_emails' in g_config:
-				for dev, emails in g_config['developer_emails'].iteritems():
-					self.privmsg (replyto, 'Emails for %s: %s' % (dev, ', '.join (emails)))
-				#done
-			else:
-				self.privmsg (replyto, 'No dev emails.')
-			#fi
 		elif command == 'checkhg':
 			check_admin (sender, ident, host, command)
 			global repocheck_timeout
@@ -1213,6 +1038,9 @@
 	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))
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/commandhandler.py	Wed Nov 05 00:19:15 2014 +0200
@@ -0,0 +1,156 @@
+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

mercurial