Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'develop' into db

Also change quote to a new style plugin.

Conflicts:
	hamper/commander.py
	hamper/plugins/plugin_utils.py
  • Loading branch information...
commit 1f9eb470b234bb0077331581efac085d8e12eb95 2 parents 4a2de84 + fc314d0
@mythmon mythmon authored
View
20 hamper/IHamper.py
@@ -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
1  hamper/__init__.py
@@ -0,0 +1 @@
+version = '0.1'
View
97 hamper/commander.py
@@ -7,19 +7,22 @@
from twisted.internet import protocol, reactor
import sqlalchemy
from sqlalchemy import orm
-
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,)
@@ -28,18 +31,26 @@ 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.
+ # 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-z_\-\[\]\\^{}|`][a-z0-9_\-\[\]\\^{}|`]*)[:,] )? *(.*)$',
+ r'^(?:([A-Za-z_\-\[\]\\^{}|`][A-Za-z0-9_\-\[\]\\^{}|`]*)[:,] )? *(.*)$',
msg).groups()
if user:
@@ -53,28 +64,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
+
+ #matchedPlugins = []
+ #for cmd in self.factory.plugins:
+ # match = cmd.regex.match(msg)
+ # if match and (directed or (not cmd.onlyDirected)):
+ # matchedPlugins.append((match, cmd))
- # High priority plugins first
- matchedPlugins.sort(key=lambda x: x[1].priority, reverse=True)
+ ## High priority plugins first
+ #matchedPlugins.sort(key=lambda x: x[1].priority, reverse=True)
- 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
+ #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
- 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)
+ 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:
@@ -90,10 +108,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,10 +128,12 @@ def __init__(self, config):
self.nickname = config['nickname']
self.history = {}
- self.plugins = set()
-
- self.pluginsToAdd = set()
- self.pluginsToRemove = set()
+ self.plugins = []
+ # These are so plugins can be added/removed at run time. The
+ # addition/removal will happen at a time when the list isn't being
+ # iterated, so nothing breaks.
+ self.pluginsToAdd = []
+ self.pluginsToRemove = []
if 'db' in config:
engine = sqlalchemy.create_engine(config['db'])
@@ -133,11 +158,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
46 hamper/interfaces.py
@@ -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, bot, comm, groups):
+ pass
View
111 hamper/plugins/commands.py
@@ -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()
View
42 hamper/plugins/friendly.py
@@ -0,0 +1,42 @@
+import random
+import re
+
+from zope.interface import implements
+
+from hamper.interfaces import IPlugin
+
+
+class Friendly(object):
+ """Be polite. When people say hello, response."""
+ implements(IPlugin)
+
+ name = 'friendly'
+ priority = 2
+
+ def __init__(self):
+ self.greetings = ['hi', 'hello', 'hey']
+
+ def process(self, bot, comm):
+ if comm['message'].strip() in self.greetings:
+ bot.say('{0} {1[user]}'
+ .format(random.choice(self.greetings), comm))
+ return True
+
+ return False
+
+
+class OmgPonies(object):
+ """The classics never die."""
+ implements(IPlugin)
+
+ name = 'ponies'
+ priority = 3
+
+ def process(self, bot, comm):
+ if re.match(r'.*pon(y|ies).*', comm['message']):
+ bot.say('OMG!!! PONIES!!!')
+ return False
+
+
+friendly = Friendly()
+omgponies = OmgPonies()
View
43 hamper/plugins/plugin_utils.py
@@ -1,19 +1,23 @@
-from zope.interface import implements
+import re
+from zope.interface import implements
from bravo import plugin
-from hamper.plugins.commands import Plugin
-from hamper.IHamper import IPlugin
+from hamper.interfaces import IPlugin
-class PluginUtils(Plugin):
+class PluginUtils(object):
+ implements(IPlugin)
- name = 'Plugin Utils'
- regex = '^plugins?(.*)$'
+ name = 'plugins'
+ priority = 0
- def __call__(self, commander, options):
+ def process(self, bot, comm):
+ match = re.match('^plugins?\w+(.*)$', comm['message'])
+ if not match:
+ return
- args = options['groups'][0].split(' ')
+ args = match.groups()[0].split(' ')
args = [a.strip() for a in args]
args = [a for a in args if a]
@@ -22,22 +26,24 @@ def __call__(self, commander, options):
'reload': self.reloadPlugin,
}
- dispatch[args[0]](commander, args[1:])
+ if args[0] in dispatch:
+ dispatch[args[0]](bot, *args[1:])
+ return True
- def listPlugins(self, commander, args):
+ def listPlugins(self, bot, *args):
"""Reply with a list of all currently loaded plugins."""
- commander.say('Loaded Plugins: {0}.'.format(
- ', '.join([c.name for c in commander.factory.plugins])))
+ bot.say('Loaded Plugins: {0}.'.format(
+ ', '.join([c.name for c in bot.factory.plugins])))
- def reloadPlugin(self, commander, args):
+ def reloadPlugin(self, bot, *args):
"""Reload a named plugin."""
name = ' '.join(args)
- ps = commander.factory.plugins
+ ps = bot.factory.plugins
matched_plugins = [p for p in ps if p.name == name]
if len(matched_plugins) == 0:
- commander.say("I can't find a plugin named %s!" % name)
+ bot.say("I can't find a plugin named %s!" % name)
return
target_plugin = matched_plugins[0]
@@ -45,8 +51,9 @@ def reloadPlugin(self, commander, args):
new_plugin = plugin.retrieve_named_plugins(IPlugin, [name],
'hamper.plugins', {'fresh': True})[0]
- commander.removePlugin(target_plugin)
- commander.addPlugin(new_plugin)
- commander.say('Request reload of {0}.'.format(new_plugin))
+ bot.removePlugin(target_plugin)
+ bot.addPlugin(new_plugin)
+ bot.say('Request reload of {0}.'.format(new_plugin))
+
plugin_utils = PluginUtils()
View
21 hamper/plugins/quote.py
@@ -6,33 +6,34 @@
from sqlalchemy.ext.declarative import declarative_base
import sqlalchemy
-from hamper.plugins.commands import Plugin
+from hamper.interfaces import Command
-class Quotes(Plugin):
+class Quotes(Command):
'''Remember quotes, and recall on demand.'''
name = 'quotes'
- regex = r'^quote(.*)'
+ priority = 0
+ regex = r'^quote(.*)$'
def __init__(self):
- pass
+ super(Quotes, self).__init__()
# TODO: make sure the tables we care about are in the db. how?
- def __call__(self, commander, opts):
- args = opts['groups'][0].strip()
+ def command(self, bot, comm, groups):
+ args = groups[0].strip()
if args.startswith('--add '):
# Add a quote
text = args.split(' ', 1)[1]
quote = Quote(text, opts['user'])
- commander.db.add(quote)
+ bot.db.add(quote)
else:
# Deliver a quote
- index = random.randrange(0, commander.db.query(Quote).count())
- quote = commander.db.query(Quote)[rand]
- commander.say(quote.text)
+ index = random.randrange(0, bot.db.query(Quote).count())
+ quote = bot.db.query(Quote)[rand]
+ bot.say(quote.text)
Base = declarative_base()
View
2  hamper/main.py → scripts/hamper
@@ -1,3 +1,5 @@
+#!/usr/bin/env python2
+
import sys
import yaml
View
16 setup.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python2
+
+from setuptools import setup, find_packages
+
+from hamper import version
+
+setup(name='hamper',
+ version=version.encode('utf8'),
+ description='Yet another IRC bot',
+ install_requires=['pyyaml', 'Twisted'],
+ author='Mike Cooper',
+ author_email='mythmon@gmail.com',
+ url='https://www.github.com/hamperbot/hamper',
+ packages=find_packages(),
+ scripts=['scripts/hamper'],
+ )
Please sign in to comment.
Something went wrong with that request. Please try again.