Permalink
Browse files

Another big plugin change. Will make more advanced features easier.

There are now plugins, which contain there own code to decide if they
care about a message. A helper class has been made for easily making
commands.

Also update all the current commands/plugins to use the new system.
  • Loading branch information...
mythmon committed Jul 23, 2011
1 parent b6001c4 commit fc314d095271ad92d20053d81e868351d4f064dd
Showing with 204 additions and 149 deletions.
  1. +0 −20 hamper/IHamper.py
  2. +55 −36 hamper/commander.py
  3. +46 −0 hamper/interfaces.py
  4. +36 −75 hamper/plugins/commands.py
  5. +42 −0 hamper/plugins/friendly.py
  6. +25 −18 hamper/plugins/plugin_utils.py
View
@@ -1,20 +0,0 @@
-from zope.interface import implements, Interface, Attribute
-
-
-class IPlugin(Interface):
- """Interface for a plugin.."""
-
- name = Attribute('Human readable name for the plugin.')
- onlyDirected = Attribute('Only respond to messages directed at the bot.')
- caseSensitive = Attribute('Compile the regex to be caseSensitive if True.')
- regex = Attribute('What messages the plugin will be called for.')
- priority = Attribute('Higher numbers are called first.')
-
- def __call__(commander, options):
- """
- Called when a matching message comes in to the bot.
-
- Return `True` if the next plugin should be called, when there are
- multiple plugins with the same priority. Returning `False` or not
- returing a value will cause execution to stop.
- """
View
@@ -8,17 +8,20 @@
from bravo.plugin import retrieve_plugins
-from hamper.IHamper import IPlugin
+from hamper.interfaces import IPlugin
class CommanderProtocol(irc.IRCClient):
- """Runs the IRC interactions, and calls out to plugins."""
+ """Interacts with a single server, and delegates to the plugins."""
def _get_nickname(self):
return self.factory.nickname
-
nickname = property(_get_nickname)
+ def _get_db(self):
+ return self.factory.db
+ db = property(_get_db)
+
def signedOn(self):
self.join(self.factory.channel)
print "Signed on as %s." % (self.nickname,)
@@ -27,16 +30,24 @@ def joined(self, channel):
print "Joined %s." % (channel,)
def privmsg(self, user, channel, msg):
+ """I received a message."""
print channel, user, msg
- """On message received (from channel or user)."""
if not user:
# ignore server messages
return
- directed = msg.startswith(self.nickname)
+ pm = channel == self.nickname
+ directed = msg.startswith(self.nickname) or pm
+ if msg.startswith('!'):
+ msg = msg[1:]
+ directed = True
+
# This monster of a regex extracts msg and target from a message, where
- # the target may not be there, and the target is a valid irc name.
+ # the target may not be there, and the target is a valid irc name. A
+ # valid nickname consists of letters, numbers, _-[]\^{}|`, and cannot
+ # start with a number. Valid ways to target someone are "<nick>: ..."
+ # and "<nick>, ..."
target, msg = re.match(
r'^(?:([A-Za-z_\-\[\]\\^{}|`][A-Za-z0-9_\-\[\]\\^{}|`]*)[:,] )? *(.*)$',
msg).groups()
@@ -52,28 +63,35 @@ def privmsg(self, user, channel, msg):
'target': target,
'message': msg,
'channel': channel,
+ 'directed': directed,
+ 'pm': pm,
}
- matchedPlugins = []
- for cmd in self.factory.plugins:
- match = cmd.regex.match(msg)
- if match and (directed or (not cmd.onlyDirected)):
- matchedPlugins.append((match, cmd))
+ # Plugins are already sorted by priority
+ for plugin in self.factory.plugins:
+ stop = plugin.process(self, comm)
+ if stop:
+ break
- # High priority plugins first
- matchedPlugins.sort(key=lambda x: x[1].priority, reverse=True)
+ #matchedPlugins = []
+ #for cmd in self.factory.plugins:
+ # match = cmd.regex.match(msg)
+ # if match and (directed or (not cmd.onlyDirected)):
+ # matchedPlugins.append((match, cmd))
- for match, cmd in matchedPlugins:
- proc_comm = comm.copy()
- proc_comm.update({'groups': match.groups()})
- if not cmd(self, proc_comm):
- # The plugin asked us to not run any more.
- break
+ ## High priority plugins first
+ #matchedPlugins.sort(key=lambda x: x[1].priority, reverse=True)
- key = channel if channel else user
- if not key in self.factory.history:
- self.factory.history[key] = deque(maxlen=100)
- self.factory.history[key].append(comm)
+ #for match, cmd in matchedPlugins:
+ # proc_comm = comm.copy()
+ # proc_comm.update({'groups': match.groups()})
+ # if not cmd(self, proc_comm):
+ # # The plugin asked us to not run any more.
+ # break
+
+ if not channel in self.factory.history:
+ self.factory.history[channel] = deque(maxlen=100)
+ self.factory.history[channel].append(comm)
# We can't remove/add plugins while we are in the loop, so do it here.
while self.factory.pluginsToRemove:
@@ -89,10 +107,15 @@ def say(self, msg):
self.msg(self.factory.channel, msg)
def removePlugin(self, plugin):
- self.factory.pluginsToRemove.add(plugin)
+ self.factory.pluginsToRemove.append(plugin)
def addPlugin(self, plugin):
- self.factory.pluginsToAdd.add(plugin)
+ self.factory.pluginsToAdd.append(plugin)
+
+ def leaveChannel(self, channel):
+ """Leave the specified channel."""
+ # For now just quit.
+ self.quit()
class CommanderFactory(protocol.ClientFactory):
@@ -105,12 +128,12 @@ def __init__(self, channel, nickname):
self.history = {}
- self.plugins = set()
+ self.plugins = []
# These are so plugins can be added/removed at run time. The
- # addition/removal will happen at a time when the set isn't being
+ # addition/removal will happen at a time when the list isn't being
# iterated, so nothing breaks.
- self.pluginsToAdd = set()
- self.pluginsToRemove = set()
+ self.pluginsToAdd = []
+ self.pluginsToRemove = []
for _, plugin in retrieve_plugins(IPlugin, 'hamper.plugins').items():
self.registerPlugin(plugin)
@@ -124,11 +147,7 @@ def clientConnectionFailed(self, connector, reason):
def registerPlugin(self, plugin):
"""
Registers a plugin.
-
- Also sets up the regex and other options for the plugin.
"""
- options = re.I if not plugin.caseSensitive else 0
- plugin.regex = re.compile(plugin.regex, options)
-
- self.plugins.add(plugin)
- print 'registered', plugin.name
+ self.plugins.append(plugin)
+ self.plugins.sort()
+ print 'registered plugin', plugin.name
View
@@ -0,0 +1,46 @@
+import re
+
+from zope.interface import implements, Interface, Attribute
+
+
+class IPlugin(Interface):
+ """Interface for a plugin.."""
+
+ name = Attribute('Human readable name for the plugin.')
+ priority = Attribute('Priority of plugins. High numbers are called first')
+
+ def process(bot, comm):
+ """
+ Called when a matching message comes in to the bot.
+
+ Return `True` if execution of plugins should stop after this. A return
+ value of `False` or no return value (implicit `None`) will cause the
+ next plugin to be called.
+ """
+
+
+class Command(object):
+ """Specialized plugin to implement a simple command"""
+ implements(IPlugin)
+
+ priority = 0
+
+ caseSensitive = False
+ regex = ''
+ onlyDirected = True
+
+ def __init__(self):
+ if type(self.regex) == str:
+ opts = 0 if self.caseSensitive else re.I
+ self.regex = re.compile(self.regex, opts)
+
+ def process(self, bot, comm):
+ if self.onlyDirected and not comm['directed']:
+ return
+ match = self.regex.match(comm['message'])
+ if match:
+ self.command(bot, comm, match.groups())
+ return True
+
+ def command(self, comm, groups):
+ pass
View
@@ -3,112 +3,73 @@
from zope.interface import implements, Interface, Attribute
-from hamper.IHamper import IPlugin
+from hamper.interfaces import Command
-class Plugin(object):
-
- implements(IPlugin)
-
- name = 'Generic Plugin'
- onlyDirected = True
- caseSensitive = False
- regex = ''
- priority = 0
-
- def __call__(self, commander, options):
- return True
-
-
-class Friendly(Plugin):
- """Be polite. When people say hello, response."""
-
- name = 'Friendly'
- regex = '.*'
- priority = 2
-
- def __init__(self):
- self.greetings = ['hi', 'hello', 'hey']
-
- def __call__(self, commander, options):
- if options['message'].strip() in self.greetings:
- commander.say('{0} {1[user]}'
- .format(random.choice(self.greetings), options))
- return False
-
- return True
-
-
-class QuitCommand(Plugin):
+class QuitCommand(Command):
"""Know when hamper isn't wanted."""
- name = 'Quit'
+ name = 'quit'
regex = 'go away'
- def __call__(self, commander, options):
- commander.say('Bye!')
- commander.quit()
-
-
-class OmgPonies(Plugin):
- """The classics never die."""
-
- name = 'OMG!!! Ponies!!!'
- regex = r'.*pon(y|ies).*'
- onlyDirected = False
+ def command(self, bot, comm, groups):
+ bot.say('Bye!')
+ bot.leaveChannel(comm['channel'])
+ return True
- def __call__(self, commander, options):
- commander.say('OMG!!! PONIES!!!')
-class Sed(Plugin):
+class Sed(Command):
"""To be honest, I feel strange in channels that don't have this."""
name = 'sed'
- regex = r'^!s/(.*)/(.*)/(g?i?)'
+ regex = r's/(.*)/(.*)/(g?i?m?)'
onlyDirected = False
priority = -1
- def __call__(self, commander, opts):
- regex_opts = re.I if 'i' in opts['groups'][2] else 0
- usr_regex = re.compile(opts['groups'][0], regex_opts)
- usr_replace = opts['groups'][1]
+ def command(self, bot, comm, groups):
+ options = groups[2]
- key = opts['channel'] if opts['channel'] else opts['user']
+ regex_opts = re.I if 'i' in options else 0
+ usr_regex = re.compile(groups[0], regex_opts)
+ usr_replace = groups[1]
- if key not in commander.factory.history:
- commander.say('Who are you?! How did you get in my house?!')
+ count = 0 if 'g' in options else 1
+
+ key = comm['channel']
+ if key not in bot.factory.history:
+ bot.say('Who are you?! How did you get in my house?!')
return
- for comm in reversed(commander.factory.history[key]):
- # Only look at our own, unless global was specified
- if 'g' not in opts['groups'][2] and comm['user'] != opts['user']:
+ for hist in reversed(bot.factory.history[key]):
+ # Only look at our own if only-me was specified
+ if 'm' in options and hist['user'] != comm['user']:
continue
# Don't look at other sed commands
- if comm['message'].startswith('!s/'):
+ if hist['directed'] and hist['message'].startswith('s/'):
continue
- if usr_regex.search(comm['message']):
- new_msg = usr_regex.sub(usr_replace, comm['message'])
- commander.say('{0} actually meant: {1}'
- .format(comm['user'], new_msg))
+
+ if usr_regex.search(hist['message']):
+ new_msg = usr_regex.sub(usr_replace, hist['message'], count)
+ bot.say('{0} actually meant: {1}'
+ .format(hist['user'], new_msg))
break
else:
- commander.say("Sorry, I couldn't match /{0}/.".format(usr_regex.pattern))
+ bot.say("Sorry, I couldn't match /{0}/.".format(usr_regex.pattern))
-class LetMeGoogleThatForYou(Plugin):
+class LetMeGoogleThatForYou(Command):
"""Link to the sarcastic letmegooglethatforyou.com."""
name = 'lmgtfy'
- regex = '.*lmgtfy\s+(.*)'
+ regex = '^lmgtfy\s+(.*)'
onlyDirected = False
- def __call__(self, commander, options):
+ def command(self, bot, comm, groups):
target = ''
- if options['target']:
- target = options['target'] + ': '
- commander.say(target + 'http://lmgtfy.com/?q=' + options['groups'][0])
+ if comm['target']:
+ target = comm['target'] + ': '
+ args = groups[0].replace(' ', '+')
+ bot.say(target + 'http://lmgtfy.com/?q=' + args)
lmgtfy = LetMeGoogleThatForYou()
sed = Sed()
-omgponies = OmgPonies()
quit = QuitCommand()
-hi = Friendly()
Oops, something went wrong.

0 comments on commit fc314d0

Please sign in to comment.