diff --git a/README.mkd b/README.mkd index 62eb7c9..4352818 100644 --- a/README.mkd +++ b/README.mkd @@ -8,29 +8,67 @@ many commits on the master branch. Installation ============ -As of right now, you can't install hamper, unless you are clever. +You can install the latest official version of hamper from the [Python Package +Index][pypi]. I suggest using `pip`, but I am told `easy_install` will work as +well. + + sudo pip install hamper + +If you want the git version of hamper, then checkout out the develop branch, +and run + + sudo python setup.py install + +[pypi]: http://pypi.python.org/pypi + +Dependencies +------------ +These dependencies will be taken care of automatically if you install with +`pip`. They are only a concern if you install from git. + +- Twisted +- SQLAlchemy +- PyYaml +- The plugin loader of [Bravo][bravo] (included) +- Exocet (included) + +[bravo]: https://github.com/MostAwesomeDude/bravo Configuration ============= -Make a file named `hamper.conf`. This should be a YAML file containing these, -hopefully self-explanatory, fields: +Make a file named `hamper.conf`. This should be a YAML file containing these +fields: - `nickname` - `channel` - `server` - `port` +- `db` - A database URL as described [here][dburl] For an example check out `hamper.conf.dist`. +[dburl]: http://www.sqlalchemy.org/docs/core/engines.html#sqlalchemy.create_engine + Usage ===== -Then, with `hamper.conf` in your current directory and hamper on your python -path, run `main.py`. I like to use this command +Run hamper from a directory containing `hamper.conf`. If you installed it with +pip, you can just say `hamper`, but if you are running from git, you need to +make sure that hamper is on your python path. I like to use this command: - PYTHONPATH="~/git/hamper" python2 ~/git/hamper/hamper/main.py + PYTHONPATH="~/git/hamper" python2 ~/git/hamper/hamper/scripts/hamper + +Plugin Development +================== +Read `hamper/plugins/friendly.py`. Add a file to `hamper/plugins`, and write +plugins in it. Don't forget to create an instance of each one at the bottom. ###Credits +Code and design: + +- Mike Cooper + +Ideas, but no code yet: + - Daniel Thornton - Jordan Evans -- Mike Cooper diff --git a/bravo/ibravo.py b/bravo/ibravo.py index 141c327..b8eb281 100644 --- a/bravo/ibravo.py +++ b/bravo/ibravo.py @@ -1,5 +1,4 @@ from twisted.python.components import registerAdapter -from twisted.web.resource import IResource from zope.interface import implements, invariant, Attribute, Interface from bravo.errors import InvariantException @@ -449,8 +448,3 @@ def stop(): After this method is called, the automaton should not continue processing data; it needs to stop immediately. """ - -class IWorldResource(IBravoPlugin, IResource): - """ - Interface for a world specific web resource. - """ diff --git a/hamper.conf.dist b/hamper.conf.dist index 13f1439..a609e75 100644 --- a/hamper.conf.dist +++ b/hamper.conf.dist @@ -1,4 +1,15 @@ -nickname: hamper -channel: #hamper +nickname: cool_bot server: irc.freenode.net port: 6667 +db: "sqlite:///hamper.db" + +channels: ["#awesome-channel", "#cool-channel"] +plugins: + - quit + - sed + - lmgtfy + - friendly + - ponies + - botsnack + - plugins + diff --git a/hamper/IHamper.py b/hamper/IHamper.py deleted file mode 100644 index da313a4..0000000 --- a/hamper/IHamper.py +++ /dev/null @@ -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. - """ diff --git a/hamper/__init__.py b/hamper/__init__.py index 183fe5f..0bc977e 100644 --- a/hamper/__init__.py +++ b/hamper/__init__.py @@ -1 +1 @@ -version = '0.1' +version = '0.2' diff --git a/hamper/commander.py b/hamper/commander.py index 7588af6..5a25249 100644 --- a/hamper/commander.py +++ b/hamper/commander.py @@ -1,45 +1,64 @@ import sys import re from collections import deque -import yaml +import traceback +import yaml from twisted.words.protocols import irc 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) + for c in self.factory.channels: + self.join(c) print "Signed on as %s." % (self.nickname,) def joined(self, channel): - print "Joined %s." % (channel,) + print "Joined {}.".format(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) # 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 ": ..." + # and ", ..." target, msg = re.match( - r'^(?:([a-z_\-\[\]\\^{}|`][a-z0-9_\-\[\]\\^{}|`]*)[:,] )? *(.*)$', - msg).groups() + r'^(?:([a-z_\-\[\]\\^{}|`]' # First letter can't be a number + '[a-z0-9_\-\[\]\\^{}|`]*)' # The rest can be many things + '[:,] )? *(.*)$', # The actual message + msg, re.I).groups() + + pm = channel == self.nickname + if target: + directed = target.lower() == self.nickname.lower() + else: + directed = False + if msg.startswith('!'): + msg = msg[1:] + directed = True if user: user, mask = user.split('!', 1) @@ -52,68 +71,79 @@ 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)) - - # 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 + # Plugins are already sorted by priority + stop = False + for plugin in self.factory.plugins: + try: + stop = plugin.process(self, comm) + if stop: + break + except: + traceback.print_exc() - 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: - self.factory.plugins.remove(self.factory.pluginsToRemove.pop()) + p = self.factory.pluginsToRemove.pop() + print("Unloading " + repr(p)) + self.factory.plugins.remove(p) while self.factory.pluginsToAdd: - self.factory.registerPlugin(self.factory.pluginsToAdd.pop()) + p = self.factory.pluginsToAdd.pop() + print('Loading ' + repr(p)) + self.factory.registerPlugin(p) def connectionLost(self, reason): + self.factory.db.commit() reactor.stop() - 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): protocol = CommanderProtocol - def __init__(self, channel, nickname): - self.channel = channel - self.nickname = nickname + def __init__(self, config): + self.channels = config['channels'] + self.nickname = config['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 = [] + + if 'db' in config: + print('Loading db from config: ' + config['db']) + self.db_engine = sqlalchemy.create_engine(config['db']) + else: + print('Using in-memory db') + self.db_engine = sqlalchemy.create_engine('sqlite:///:memory:') + DBSession = orm.sessionmaker(self.db_engine) + self.db = DBSession() for _, plugin in retrieve_plugins(IPlugin, 'hamper.plugins').items(): - self.registerPlugin(plugin) + if plugin.name in config['plugins']: + self.registerPlugin(plugin) def clientConnectionLost(self, connector, reason): print "Lost connection (%s)." % (reason) @@ -124,11 +154,8 @@ 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 + plugin.setup(self) + self.plugins.append(plugin) + self.plugins.sort() + print 'registered plugin', plugin.name diff --git a/hamper/interfaces.py b/hamper/interfaces.py new file mode 100644 index 0000000..bdaf145 --- /dev/null +++ b/hamper/interfaces.py @@ -0,0 +1,94 @@ +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 setup(factory): + """ + Called when the factory loads the plugin. + """ + + 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 Plugin(object): + """ + Base class for a plugin. + + If any of a classes children are Command classes, automatically call out to + them. + """ + implements(IPlugin) + + priority = 0 + + def __init__(self): + self.commands = [] + for name in dir(self): + cls = self.__getattribute__(name) + try: + if ICommand.implementedBy(cls): + self.commands.append(cls()) + except (AttributeError, TypeError): + pass + + def setup(self, factory): + pass + + def process(self, bot, comm): + for cmd in self.commands: + stop = cmd.process(bot, comm) + if stop: + return stop + + +class ICommand(Interface): + """Interface for a command.""" + + regex = Attribute('The regex to trigger this command for.') + caseSensitive = Attribute("The case sensitivity of the trigger regex.") + onlyDirected = Attribute("Only respond to command directed at the bot.") + + def process(bot, comm): + """Chooses whether or not to trigger the command.""" + + def command(bot, comm, groups): + """This function gets called when the command is triggered.""" + + +class Command(object): + """ + A convenience wrapper to implement a single command. + + To use it, define a clas that inherits from Command inside a Plugin. + """ + implements(ICommand) + + caseSensitive = False + 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 diff --git a/hamper/plugins/commands.py b/hamper/plugins/commands.py index 22157c9..dba585c 100644 --- a/hamper/plugins/commands.py +++ b/hamper/plugins/commands.py @@ -3,112 +3,81 @@ from zope.interface import implements, Interface, Attribute -from hamper.IHamper import IPlugin +from hamper.interfaces import Command, Plugin -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'] +class Quit(Plugin): + """Know when hamper isn't wanted.""" + name = 'quit' - 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 + class QuitCommand(Command): + regex = 'go away' - return True + def command(self, bot, comm, groups): + bot.msg(comm['channel'], 'Bye!') + bot.leaveChannel(comm['channel']) + return True -class QuitCommand(Plugin): - """Know when hamper isn't wanted.""" +class Sed(Plugin): + """To be honest, I feel strange in channels that don't have this.""" - name = 'Quit' - regex = 'go away' + name = 'sed' + priority = -1 - def __call__(self, commander, options): - commander.say('Bye!') - commander.quit() + class SedCommand(Command): + regex = r's/(.*)/(.*)/(g?i?m?)' + onlyDirected = False + def command(self, bot, comm, groups): + options = groups[2] -class OmgPonies(Plugin): - """The classics never die.""" + regex_opts = re.I if 'i' in options else 0 + usr_regex = re.compile(groups[0], regex_opts) + usr_replace = groups[1] - name = 'OMG!!! Ponies!!!' - regex = r'.*pon(y|ies).*' - onlyDirected = False + g = 0 if 'g' in options else 1 - def __call__(self, commander, options): - commander.say('OMG!!! PONIES!!!') + key = comm['channel'] + if key not in bot.factory.history: + bot.msg(comm['channel'], 'Who are you?! How did you get in my ' + 'house?!') + return False -class Sed(Plugin): - """To be honest, I feel strange in channels that don't have this.""" + for hist in reversed(bot.factory.history[key]): + if 'm' in options and hist['user'] != comm['user']: + # Only look at the user's messages + continue - name = 'sed' - regex = r'^!s/(.*)/(.*)/(g?i?)' - onlyDirected = False - priority = -1 + # Don't look at other sed commands + if hist['directed'] and hist['message'].startswith('s/'): + continue - 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] - - key = opts['channel'] if opts['channel'] else opts['user'] - - if key not in commander.factory.history: - commander.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']: - continue - # Don't look at other sed commands - if comm['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)) - break - else: - commander.say("Sorry, I couldn't match /{0}/.".format(usr_regex.pattern)) + if usr_regex.search(hist['message']): + new_msg = usr_regex.sub(usr_replace, hist['message'], g) + bot.msg(comm['channel'], '{0} actually meant: {1}' + .format(hist['user'], new_msg)) + break + else: + bot.msg(comm['channel'], + "Sorry, I couldn't match /{0}/.".format(usr_regex.pattern)) class LetMeGoogleThatForYou(Plugin): """Link to the sarcastic letmegooglethatforyou.com.""" name = 'lmgtfy' - regex = '.*lmgtfy\s+(.*)' - onlyDirected = False - def __call__(self, commander, options): - target = '' - if options['target']: - target = options['target'] + ': ' - commander.say(target + 'http://lmgtfy.com/?q=' + options['groups'][0]) + class LMGTFYCommand(Command): + regex = '^lmgtfy\s+(.*)' + onlyDirected = False + + def command(self, bot, comm, groups): + target = '' + if comm['target']: + target = comm['target'] + ': ' + args = groups[0].replace(' ', '+') + bot.msg(comm['channel'], target + 'http://lmgtfy.com/?q=' + args) lmgtfy = LetMeGoogleThatForYou() sed = Sed() -omgponies = OmgPonies() -quit = QuitCommand() -hi = Friendly() +quit = Quit() diff --git a/hamper/plugins/friendly.py b/hamper/plugins/friendly.py new file mode 100644 index 0000000..1331b76 --- /dev/null +++ b/hamper/plugins/friendly.py @@ -0,0 +1,80 @@ +import random +import re +from datetime import datetime + +from zope.interface import implements + +from hamper.interfaces import Plugin + + +class Friendly(Plugin): + """Be polite. When people say hello, response.""" + + name = 'friendly' + priority = 2 + + def setup(self, factory): + self.greetings = ['hi', 'hello', 'hey', 'sup', 'yo', 'hola'] + + def process(self, bot, comm): + if not comm['directed']: + return + + if comm['message'].strip() in self.greetings: + bot.msg(comm['channel'], '{0} {1[user]}' + .format(random.choice(self.greetings), comm)) + return True + + +class OmgPonies(Plugin): + """The classics never die.""" + + name = 'ponies' + priority = 3 + + cooldown = 30 #seconds + + def setup(self, factory): + self.last_pony_time = datetime.now() + + def process(self, bot, comm): + if re.match(r'.*pon(y|ies).*', comm['message'], re.I): + now = datetime.now() + since_last = now - self.last_pony_time + since_last = since_last.seconds + 24*3600*since_last.days + + if since_last >= self.cooldown: + bot.msg(comm['channel'], 'OMG!!! PONIES!!!') + self.last_pony_time = now + else: + print('too many ponies') + + # Always let the other plugins run + return False + + +class BotSnack(Plugin): + """Reward a good bot.""" + + name = 'botsnack' + priority = 0 + + def setup(self, factory): + self.rewards = { + 'botsnack': ['yummy', 'my favorite!'], + 'goodhamper': ['^_^', ':D'], + } + + def process(self, bot, comm): + slug = comm['message'].lower().replace(' ', '') + for k, v in self.rewards.items(): + if k in slug: + bot.say(comm['channel'], random.choice(v)) + return True + + return False + + +friendly = Friendly() +omgponies = OmgPonies() +botsnack = BotSnack() diff --git a/hamper/plugins/plugin_utils.py b/hamper/plugins/plugin_utils.py index 20ba37d..f6a473c 100644 --- a/hamper/plugins/plugin_utils.py +++ b/hamper/plugins/plugin_utils.py @@ -1,52 +1,82 @@ -from zope.interface import implements, Interface, Attribute +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 Command, Plugin, IPlugin class PluginUtils(Plugin): - name = 'Plugin Utils' - regex = '^plugins?(.*)$' + name = 'plugins' + priority = 0 + + class ListPlugins(Command): + regex = r'^plugins?(?: list)?$' + + def command(self, bot, comm, groups): + """Reply with a list of all currently loaded plugins.""" + bot.msg(comm['channel'], 'Loaded Plugins: {0}.'.format( + ', '.join([c.name for c in bot.factory.plugins]))) + return True + + class ReloadPlugins(Command): + regex = r'^plugins? reload (.*)$' + def command(self, bot, comm, groups): + """Reload a named plugin.""" + name = groups[0] + + ps = bot.factory.plugins + matched_plugins = [p for p in ps if p.name == name] + if len(matched_plugins) == 0: + bot.msg(comm['channel'], "I can't find a plugin named {0}!" + .format(name)) + return False + + target_plugin = matched_plugins[0] + # Fun fact: the fresh thing is just a dummy. It just can't be None + new_plugin = plugin.retrieve_named_plugins(IPlugin, [name], + 'hamper.plugins', {'fresh': True})[0] + + bot.removePlugin(target_plugin) + bot.addPlugin(new_plugin) + bot.msg(comm['channel'], 'Reloading {0}.'.format(new_plugin)) + return True + + class LoadPlugin(Command): + regex = r'^plugins? load (.*)$' + def command(self, bot, comm, groups): + """Load a named plugin.""" + name = ' '.join(args[1:]) + ps = bot.factory.plugins + matched_plugins = [p for p in ps if p.name == name] + if len(matched_plugins) != 0: + bot.msg(comm['channel'], "%s is already loaded." % name) + return False + + new_plugin = plugin.retrieve_named_plugins(IPlugin, [name], + 'hamper.plugins', {'fresh': True})[0] + bot.addPlugin(new_plugin) + bot.msg(comm['channel'], 'Loading {0}.'.format(new_plugin)) + return True + + class UnloadPlugin(Command): + regex = r'^plugins? unload (.*)$' + def unloadPlugin(self, bot, comm, groups): + """Unload a named plugin.""" + name = groups[0] + ps = bot.factory.plugins + matched_plugins = [p for p in ps if p.name == name] + if len(matched_plugins) == 0: + bot.msg(comm['channel'], "I can't find a plugin named {0}!" + .format(name)) + return False + + target_plugin = matched_plugins[0] + + bot.removePlugin(target_plugin) + bot.msg(comm['channel'], 'Unloading {0}.'.format(new_plugin)) + return True - def __call__(self, commander, options): - - args = options['groups'][0].split(' ') - args = [a.strip() for a in args] - args = [a for a in args if a] - - dispatch = { - 'list': self.listPlugins, - 'reload': self.reloadPlugin, - } - - dispatch[args[0]](commander, args[1:]) - - def listPlugins(self, commander, 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]))) - - def reloadPlugin(self, commander, args): - """Reload a named plugin.""" - name = ' '.join(args) - - ps = commander.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) - return - - target_plugin = matched_plugins[0] - # Fun fact: the fresh thing is just a dummy. It just can't be None - 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)) plugin_utils = PluginUtils() diff --git a/hamper/plugins/questions.py b/hamper/plugins/questions.py new file mode 100644 index 0000000..f455068 --- /dev/null +++ b/hamper/plugins/questions.py @@ -0,0 +1,62 @@ +from random import random + +from hamper.interfaces import Plugin + + +class Questions(Plugin): + + name = 'questions' + + def setup(self, *args): + """ + Set up the list of responses, with weights. If the weight of a response + is 'eq', it will be assigned equal value after everything that has a + number is assigned. If it's weight is some fraction of 'eq' (ie: 'eq/2' + or 'eq/3'), then it will be assigned 1/2, 1/3, etc of the 'eq' weight. + All probabilities will up to 1.0 (plus/minus any rounding errors). + """ + + responses = [ + ('I think... Yes.', 'eq'), ('Maybe. Possibly. It could be.', 'eq'), + ("No. No, I don't think so.", 'eq'), ("I don't know.", 'eq'), + ('Ask again later.', 'eq/2'), ('Without a doubt.', 'eq/2'), + ('Heck yes!', 'eq/2'), ("I'm sorry, I was thinking of bananas", .01), + ] + + total_prob = 0 + real_resp = [] + evens = [] + for resp, prob in responses: + if isinstance(prob, str): + if prob.startswith('eq'): + sp = prob.split('/') + if len(sp) == 1: + evens.append((resp, 1)) + else: + div = int(sp[1]) + evens.append((resp, 1.0/div)) + + else: + real_resp.append((resp, prob)) + total_prob += prob + + # Share is the probability of a "eq" probability. Share/2 would be the + # probability of a "eq/2" probability. + share = (1 - total_prob) / sum(div for _, div in evens) + for resp, divisor in evens: + real_resp.append((resp, share*divisor)) + + self.responses = real_resp + + def process(self, bot, comm): + if comm['directed'] and comm['message'].strip()[-1] == '?': + r = random() + for resp, prob in self.responses: + r -= prob + if r < 0: + bot.say(comm['channel'], '{0}: {1}' + .format(comm['user'], resp)) + return True + return False + +questions = Questions() diff --git a/hamper/plugins/quote.py b/hamper/plugins/quote.py new file mode 100644 index 0000000..365f680 --- /dev/null +++ b/hamper/plugins/quote.py @@ -0,0 +1,70 @@ +from datetime import datetime +import random + +from zope.interface import implements +from sqlalchemy import Integer, String, Date, Column +from sqlalchemy.ext.declarative import declarative_base +import sqlalchemy + +from hamper.interfaces import Command, Plugin + + +SQLAlchemyBase = declarative_base() + + +class Quotes(Plugin): + '''Remember quotes, and recall on demand.''' + + name = 'quotes' + priority = 0 + + def setup(self, factory): + SQLAlchemyBase.metadata.create_all(factory.db_engine) + + class DeliverQuote(Command): + """Deliver a quote.""" + regex = r'^quotes?$' + def command(self, bot, comm, groups): + index = random.randrange(0, bot.db.query(Quote).count() + 1) + quote = bot.factory.db.query(Quote)[index] + # Lame twisted irc doesn't support unicode. + bot.msg(comm['channel'], str(quote.text)) + return True + + class AddQuote(Command): + """Add a quote.""" + regex = r'^quotes? --add (.*)$' + def command(self, bot, comm, groups): + text = ' '.join(groups[0]) + quote = Quote(text, comm['user']) + bot.factory.db.add(quote) + bot.msg(comm['channel'], 'Succesfully added quote.') + + class CountQuotes(Command): + """Count how many quotes the bot knows.""" + regex = r'^quotes? --count$' + def command(self, bot, comm, groups): + count = bot.db.query(Quote).count() + bot.msg(comm['channel'], 'I know {0} quotes.'.format(count)) + + +class Quote(SQLAlchemyBase): + '''The object that will get persisted by the database.''' + + __tablename__ = 'quotes' + + id = Column(Integer, primary_key=True) + text = Column(String) + adder = Column(String) + added = Column(Date) + + def __init__(self, text, adder, added=None): + if not added: + added = datetime.now() + + self.text = text + self.adder = adder + self.added = added + + +quotes = Quotes() diff --git a/scripts/hamper b/scripts/hamper index fcd45fc..f3a5b66 100644 --- a/scripts/hamper +++ b/scripts/hamper @@ -13,11 +13,11 @@ if __name__ == '__main__': config = yaml.load(open('hamper.conf')) - for key in ['server', 'port', 'nickname', 'channel']: + for key in ['server', 'port', 'nickname', 'channels']: if (key not in config) or (not config[key]): print('You need to define {0} in the config file.'.format(key)) sys.exit(); reactor.connectTCP(config['server'], config['port'], - CommanderFactory(config['channel'], config['nickname'])) + CommanderFactory(config)) reactor.run() diff --git a/setup.py b/setup.py index 388d30c..5d84bc6 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup(name='hamper', version=version.encode('utf8'), description='Yet another IRC bot', - install_requires=['pyyaml', 'Twisted'], + install_requires=['pyyaml', 'Twisted', 'SQLAlchemy'], author='Mike Cooper', author_email='mythmon@gmail.com', url='https://www.github.com/hamperbot/hamper',