Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
mythmon committed Aug 31, 2011
2 parents 142c8d6 + f6adb77 commit c014b88
Show file tree
Hide file tree
Showing 14 changed files with 575 additions and 220 deletions.
52 changes: 45 additions & 7 deletions README.mkd
Expand Up @@ -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 <mythmon@gmail.com>

Ideas, but no code yet:

- Daniel Thornton <merthel@gmail.com>
- Jordan Evans <evans.jordan.m@gmail.com>
- Mike Cooper <mythmon@gmail.com>
6 changes: 0 additions & 6 deletions 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
Expand Down Expand Up @@ -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.
"""
15 changes: 13 additions & 2 deletions 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

20 changes: 0 additions & 20 deletions hamper/IHamper.py

This file was deleted.

2 changes: 1 addition & 1 deletion hamper/__init__.py
@@ -1 +1 @@
version = '0.1'
version = '0.2'
135 changes: 81 additions & 54 deletions 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 "<nick>: ..."
# and "<nick>, ..."
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)
Expand All @@ -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)
Expand All @@ -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

0 comments on commit c014b88

Please sign in to comment.