diff options
author | Richard Darst <rkd@zgib.net> | 2009-05-04 04:22:43 (GMT) |
---|---|---|
committer | Richard Darst <rkd@zgib.net> | 2009-05-04 04:22:43 (GMT) |
commit | cea8aae84a420a1e8f289b234fc7fb8c8db7658d (patch) | |
tree | c08908d2fb1d9bdd9dde5b921b87ec5a1df74eba |
Inital Checkin - should be working
darcs-hash:20090504042243-82ea9-350eb26dd8561a18efc98dd5d5f7a730ce37cdc7.gz
-rw-r--r-- | README.txt | 43 | ||||
-rw-r--r-- | __init__.py | 66 | ||||
-rw-r--r-- | config.py | 49 | ||||
-rw-r--r-- | meeting.py | 457 | ||||
-rw-r--r-- | plugin.py | 104 | ||||
-rw-r--r-- | test.py | 37 |
6 files changed, 756 insertions, 0 deletions
diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..dfec6a6 --- /dev/null +++ b/README.txt @@ -0,0 +1,43 @@ +USAGE +~~~~~ +http://wiki.debian.org/MeatBot + +Inspired by http://wiki.debian.org/MeetBot + +/usr/share/doc/supybot/GETTING_STARTED.gz (on Debian systems) provides +information on configuring supybot the first time, including taking +ownership the first time. + + + +INSTALLATION +~~~~~~~~~~~~ + +Requirements: +* pygments (debian package python-pygments) (for pretty IRC logs). + +* Install supybot. You can use supybot-wizard to make a bot + configuration. + + * Don't use a prefix character. (disable this: + supybot.reply.whenAddressedBy.chars: + in the config file - leave it blank afterwards.) + +* Move the MeatBot directory into your plugins directory of Supybot. + +* Make supybot join any channels you are interested in. The wizard + handles this for the first part. After that, I guess you have to + learn about supybot (I don't know enough yet...). If the plugin is + loaded, it is active on ALL channels the bot is on. You can also + command the bot after it's online. + +* Make sure the plugin is loaded. + supybot.plugins: Admin Misc User MeatBot Owner Config Channel + (can also control loading after the bot is started) + +Supybot does a lot, but I don't know much about it. Hopefully Supybot +expert users can enlighten me as to better ways to do things. + +In particular, supybot has a large configuration system, which I know +nothing about. It may be worth hooking MeatBot into that system. + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..a1c2ed1 --- /dev/null +++ b/__init__.py @@ -0,0 +1,66 @@ +### +# Copyright (c) 2009, Richard Darst +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +""" +Add a description of the plugin (to be presented to the user inside the wizard) +here. This should describe *what* the plugin does. +""" + +import supybot +import supybot.world as world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "" + +# XXX Replace this with an appropriate author or supybot.Author instance. +__author__ = supybot.authors.unknown + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +# This is a url where the most recent plugin package can be downloaded. +__url__ = '' # 'http://supybot.com/Members/yourname/MeatBot/download' + +import config +import plugin +reload(plugin) # In case we're being reloaded. +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + import test + +Class = plugin.Class +configure = config.configure + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/config.py b/config.py new file mode 100644 index 0000000..4668d69 --- /dev/null +++ b/config.py @@ -0,0 +1,49 @@ +### +# Copyright (c) 2009, Richard Darst +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + # This will be called by supybot to configure this module. advanced is + # a bool that specifies whether the user identified himself as an advanced + # user or not. You should effect your configuration by manipulating the + # registry as appropriate. + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('MeatBot', True) + + +MeatBot = conf.registerPlugin('MeatBot') +# This is where your configuration variables (if any) should go. For example: +# conf.registerGlobalValue(MeatBot, 'someConfigVariableName', +# registry.Boolean(False, """Help for someConfigVariableName.""")) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/meeting.py b/meeting.py new file mode 100644 index 0000000..f6185da --- /dev/null +++ b/meeting.py @@ -0,0 +1,457 @@ +import cPickle +import time +import os +import re +import stat + +import pygments + +# +# Throw any overrides into meetingLocalConfig.py in this directory: +# +logFileDir = '/home/richard/meatbot/' +logUrlPrefix = 'http://rkd.zgib.net/meatbot/' +MeetBotInfoURL = 'http://wiki.debian.org/MeatBot' +RestrictPerm = stat.S_IRWXO|stat.S_IRWXG # g,o perm zeroed with #restrict +#RestrictPerm = stat.S_IRWXU|stat.S_IRWXO|stat.S_IRWXG # u,g,o perm zeroed. +# used to detect #link : +UrlProtocols = ('http:', 'https:', 'irc:', 'ftp:', 'mailto:', 'ssh:') +# regular expression for parsing commands +command_RE = re.compile('#([\w]+)(?:[ \t]*(.*))?') +usefulCommands = "#action #agreed #halp #info #idea #link #topic" + +# load custom local configurations +try: + from meetingLocalConfig import * +except ImportError: + pass + + +allowedChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%\'()*+,-./:;=?@[\\]^_`{|}~ \t\n\r\x0b\x0c<>&' +htmlEscape = {">":">" , "<":"<" , "&":"&", + '"':""", "'":"'", } +def html(text): + L = [ htmlEscape.get(c, c) for c in text if c in allowedChars] + return "".join(L) + + +class MeetingCommands(object): + # Command Definitions + # generic parameters to these functions: + # nick= + # line= <the payload of the line> + # linenum= <the line number, 1-based index (for logfile)> + # time_= <time it was said> + # Commands for Chairs: + def do_startmeeting(self, nick, time_, **kwargs): + """Begin a meeting.""" + self.reply("Meeting started %s UTC. The chair is %s."%\ + (time.asctime(time_), self.owner)) + self.reply(("Information about MeatBot at %s , Useful Commands: %s.")%\ + (MeetBotInfoURL, usefulCommands)) + self.starttime = time_ + def do_endmeeting(self, nick, time_, **kwargs): + """End the meeting.""" + if not self.isChair(nick): return + self.endtime = time_ + self.save() + self.reply("Meeting ended %s UTC. Information about MeatBot at %s ."%\ + (time.asctime(time_),MeetBotInfoURL)) + self.reply("Minutes: "+self.minutesFilename(url=True)) + self.reply("Log: "+self.logFilename(url=True)) + if hasattr(self, 'oldtopic'): + self.topic(self.oldtopic) + def do_topic(self, nick, line, **kwargs): + """Set a new topic in the channel.""" + if not self.isChair(nick): return + m = Topic(nick=nick, line=line, **kwargs) + self.minutes.append(m) + self.topic(line) + def do_save(self, nick, time_, **kwargs): + """Add a chair to the meeting.""" + if not self.isChair(nick): return + self.endtime = time_ + self.save() + def do_agreed(self, nick, **kwargs): + """Add aggreement to the minutes - chairs only.""" + if not self.isChair(nick): return + m = Agreed(nick, **kwargs) + self.minutes.append(m) + do_agree = do_agreed + def do_chair(self, nick, line, **kwargs): + """Add a chair to the meeting.""" + if not self.isChair(nick): return + for chair in line.strip().split(): + self.addnick(chair, lines=0) + self.chairs.setdefault(chair.strip(), True) + self.reply("Chair added: %s"%chair) + def do_unchair(self, nick, line, **kwargs): + """Remove a chair to the meeting (founder can not be removed).""" + if not self.isChair(nick): return + for chair in line.strip().split(): + if self.chairs.has_key(chair.strip()): + del self.chairs[chair] + self.reply("Chair removed: %s"%chair) + def do_undo(self, nick, **kwargs): + """Remove the last item from the minutes.""" + if not self.isChair(nick): return + if len(self.minutes) == 0: return + self.reply("Removing item from minutes: %s"%str(self.minutes[-1])) + del self.minutes[-1] + def do_restrictlogs(self, nick, **kwargs): + """When saved, remove permissions from the files.""" + if not self.isChair(nick): return + self._restrictlogs = True + self.reply("Restricting permissions on minutes: -%s on next #save"%\ + oct(RestrictPerm)) + def do_lurk(self, nick, **kwargs): + """Don't interact in the channel.""" + if not self.isChair(nick): return + self._lurk = True + def do_unlurk(self, nick, **kwargs): + """Do interact in the channel.""" + if not self.isChair(nick): return + self._lurk = False + # Commands for Anyone: + def do_action(self, **kwargs): + """Add action item to the minutes. + + The line is searched for nicks, and a per-person action item + list is compiled after the meeting. Only nicks which have + been seen during the meeting will have an action item list + made for them, but you can use the #nick command to cause a + nick to be seen.""" + m = Action(**kwargs) + self.minutes.append(m) + def do_info(self, **kwargs): + """Add informational item to the minutes.""" + m = Info(**kwargs) + self.minutes.append(m) + def do_idea(self, **kwargs): + """Add informational item to the minutes.""" + m = Idea(**kwargs) + self.minutes.append(m) + def do_halp(self, *kwargs): + """Add call for halp to the minutes.""" + m = Halp(**kwargs) + self.minutes.append(m) + do_help = do_halp + def do_nick(self, nick, line, **kwargs): + """Make meetbot aware of a nick which hasn't said anything. + + To see where this can be used, see #action command""" + nicks = line.strip().split() + for nick in nicks: + self.addnick(nick, lines=0) + def do_link(self, **kwargs): + """Add informational item to the minutes.""" + m = Link(**kwargs) + self.minutes.append(m) + def do_commands(self, **kwargs): + commands = [ "#"+x[3:] for x in dir(self) if x[:3]=="do_" ] + commands.sort() + self.reply("Available commands: "+(" ".join(commands))) + + + + + +class Meeting(MeetingCommands, object): + _lurk = False + _restrictlogs = False + def __init__(self, channel, owner, testing=False, oldtopic=None): + self.owner = owner + self.channel = channel + self.oldtopic = oldtopic + self.lines = [ ] + self.minutes = [ ] + self.attendees = { } + self.chairs = {owner:True} + if testing or channel == "#meatbot-test": + self.filename = channel.strip('# ') + else: + self.filename = channel.strip('# ') + \ + time.strftime('-%Y-%m-%d-%H.%M', time.gmtime()) + + # These commands are callbacks to manipulate the IRC protocol. + # set self._sendReply and self._setTopic to an callback to do these things. + def reply(self, x): + """Send a reply to the IRC channel.""" + if hasattr(self, '_sendReply') and not self._lurk: + self._sendReply(x) + else: + print "REPLY:", x + def topic(self, x): + """Set the topic in the IRC channel.""" + if hasattr(self, '_setTopic') and not self._lurk: + self._setTopic(x) + else: + print "TOPIC:", x + def addnick(self, nick, lines=1): + """This person has spoken, lines=<how many lines>""" + self.attendees[nick] = self.attendees.get(nick, 0) + lines + def isChair(self, nick): + """Is the nick a chair?""" + return (nick == self.owner or self.chairs.has_key(nick)) + # Primary enttry point for new lines in the log: + def addline(self, nick, line, time_=None): + """This is the way to add lines to the Meeting object. + """ + self.addnick(nick) + line = line.strip(' \x01') # \x01 is present in ACTIONs + # Setting a custom time is useful when replying logs, + # otherwise use our current time: + if time_ is None: time_ = time.gmtime() + + # Handle the logging of the line + if line[:6] == 'ACTION': + logline = "%s * %s %s"%(time.strftime("%H:%M:%S", time_), + nick, line[7:].strip()) + else: + logline = "%s <%s> %s"%(time.strftime("%H:%M:%S", time_), + nick, line.strip()) + self.lines.append(logline) + linenum = len(self.lines) + + # Handle any commands given in the line. + matchobj = command_RE.match(line) + if matchobj is not None: + command, line = matchobj.groups() + command = command.lower() + # to define new commands, define a method do_commandname . + if hasattr(self, "do_"+command): + getattr(self, "do_"+command)(nick=nick, line=line, + linenum=linenum, time_=time_) + else: + # Detect URLs automatically + if line.split('//')[0] in UrlProtocols: + self.do_link(nick=nick, line=line, + linenum=linenum, time_=time_) + + + def save(self): + """Write all output files.""" + self.writePickle() + self.writeLogs() + self.writeMinutes() + def writeLogs(self): + # pygments lexing setup: + # (pygments HTML-formatter handles HTML-escaping) + from pygments.lexers import IrcLogsLexer + from pygments.formatters import HtmlFormatter + formatter = HtmlFormatter(encoding='utf-8', lineanchors='l', + full=True) + lexer = IrcLogsLexer(encoding='utf-8') + out = pygments.highlight("\n".join(self.lines), lexer, formatter) + # Do the writing... + f = file(self.logFilename(), 'w') + # We might want to restrict read-permissions of the files from + # the webserver. + if self._restrictlogs: + f.flush() + newmode = os.stat(f.name).st_mode & (~RestrictPerm) + os.chmod(f.name, newmode) + f.write(out) + def writeMinutes(self): + f = file(self.minutesFilename(), 'w') + # We might want to restrict read-permissions of the files from + # the webserver. + if self._restrictlogs: + f.flush() + newmode = os.stat(f.name).st_mode & (~RestrictPerm) + os.chmod(f.name, newmode) + + + # Header and things stored + print >> f, \ + '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> + <html> + <head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <title>%s Meeting Minutes</title> + </head> + <body> + Meeting started by %s at %s UTC. (<a href="%s">full logs</a>)<br> + \n\n<table border=1>'''%(self.channel, self.owner, + time.strftime("%H:%M:%S", self.starttime), + os.path.basename(self.logFilename())) + # Add all minute items to the table + for m in self.minutes: + print >> f, m.html(self) + # End the log portion + print >> f, """ + </table> + Meeting ended at %s UTC. (<a href="%s">full logs</a>)<br><br>"""%\ + (time.strftime("%H:%M:%S", self.endtime), + os.path.basename(self.logFilename())) + + + # Action Items + print >> f, "<b>Action Items</b><br>\n<ol>" + import meeting + for m in self.minutes: + # The hack below is needed because of pickling problems + if not isinstance(m, (Action, meeting.Action)): continue + print >> f, "<li>%s</li>\n"%html(m.line) + print >> f, "</ol><br>\n\n" + + + # Action Items, by person (This could be made lots more efficient) + print >> f, "<b>Action Items, by person</b><br>\n<ol>" + for nick in sorted(self.attendees.keys()): + headerPrinted = False + for m in self.minutes: + # The hack below is needed because of pickling problems + if not isinstance(m, (Action, meeting.Action)): continue + if m.line.find(nick) == -1: continue + if not headerPrinted: + print >> f, "<li> %s\n<ol>\n"%nick + headerPrinted = True + print >> f, "<li>%s</li>\n"%html(m.line) + m.assigned = True + if headerPrinted: + print >> f, "</ol>\n</li>\n" + # unassigned items: + print >> f, "<li><b>UNASSIGNED</b>\n<ol>\n" + for m in self.minutes: + if not isinstance(m, (Action, meeting.Action)): continue + if getattr(m, 'assigned', False): continue + print >> f, "<li>%s</li>\n"%html(m.line) + print >> f, '</ol>\n</li>' + # clean-up + print >> f, "</ol><br>\n\n" + + + # People Attending + print >> f, """<b>People Present (lines said):</b>\n<ol>\n""" + # sort by number of lines spoken + nicks = [ (n,c) for (n,c) in self.attendees.iteritems() ] + nicks.sort(key=lambda x: x[1], reverse=True) + for nick in nicks: + print >> f, '<li>%s (%s)</li>\n'%(nick[0], nick[1]) + print >> f, "</ol><br><br>\n\n" + print >> f, """Generated by <a href="%s">MeatBot</a>."""%MeetBotInfoURL + print >> f, "</body></html>" + def writePickle(self): + """Write a pickled representation of this meeting (debugging).""" + f = file(os.path.join(logFileDir, self.filename+'.pickle'), 'w') + if self._restrictlogs: + f.flush() + newmode = os.stat(f.name).st_mode & (~RestrictPerm) + os.chmod(f.name, newmode) + savedict = self.__dict__.copy() + if savedict.has_key('_sendReply'): del savedict['_sendReply'] + if savedict.has_key('_setTopic'): del savedict['_setTopic'] + cPickle.dump(savedict, f, cPickle.HIGHEST_PROTOCOL) + def logFilename(self, url=False): + """Name of the meeting logfile""" + filename = self.filename +'.log.html' + if url: + return os.path.join(logUrlPrefix, filename) + return os.path.join(logFileDir, filename) + def minutesFilename(self, url=False): + """Name of the meeting minutes file""" + filename = self.filename +'.html' + if url: + return os.path.join(logUrlPrefix, filename) + return os.path.join(logFileDir, filename) + + + +# +# These are objects which we can add to the meeting minutes. Mainly +# they exist to aid in HTML-formatting. +# +class Topic: + def __init__(self, nick, line, linenum, time_): + self.nick = nick ; self.topic = line ; self.linenum = linenum + self.time = time.strftime("%H:%M:%S", time_) + def html(self, M): + self.link = os.path.basename(M.logFilename()) + self.topic = html(self.topic) + self.anchor = 'l-'+str(self.linenum) + return """<tr><td><a href='%(link)s#%(anchor)s'>%(time)s</a></td> + <th colspan=3>Topic: %(topic)s</th> + </tr>"""%self.__dict__ +class GenericItem: + itemtype = '' + def __init__(self, nick, line, linenum, time_): + self.nick = nick ; self.line = line ; self.linenum = linenum + self.time = time.strftime("%H:%M:%S", time_) + def html(self, M): + self.link = os.path.basename(M.logFilename()) + self.line = html(self.line) + self.anchor = 'l-'+str(self.linenum) + self.__dict__['itemtype'] = self.itemtype + return """<tr><td><a href='%(link)s#%(anchor)s'>%(time)s</a></td> + <td>%(itemtype)s</td><td>%(nick)s</td><td>%(line)s</td> + </tr>"""%self.__dict__ +class Info(GenericItem): + itemtype = 'INFO' +class Idea(GenericItem): + itemtype = 'IDEA' +class Agreed(GenericItem): + itemtype = 'AGREED' +class Action(GenericItem): + itemtype = 'ACTION' +class Halp(GenericItem): + itemtype = 'HALP' +class Link: + itemtype = 'LINK' + def __init__(self, nick, line, linenum, time_): + self.nick = nick ; self.linenum = linenum + self.url, self.line = (line+' ').split(' ', 1) + self.line = self.line.strip() + self.time = time.strftime("%H:%M:%S", time_) + def html(self, M): + self.link = os.path.basename(M.logFilename()) + self.anchor = 'l-'+str(self.linenum) + self.__dict__['itemtype'] = self.itemtype + return """<tr><td><a href='%(link)s#%(anchor)s'>%(time)s</a></td> + <td>%(itemtype)s</td><td>%(nick)s</td><td><a href="%(url)s">%(url)s</a> %(line)s</td> + </tr>"""%self.__dict__ + + +def parse_time(time_): + try: return time.strptime(time_, "%H:%M:%S") + except ValueError: pass + try: return time.strptime(time_, "%H:%M") + except ValueError: pass + +# None of this is very well refined. +if __name__ == '__main__': + import sys + if sys.argv[1] == 'replay': + channel = os.path.basename(sys.argv[2]).split('.')[0] + M = Meeting(channel=channel, owner=None, testing=True) + for line in file(sys.argv[2]): + # match regular spoken lines: + r = re.compile(r'\[?([0-9: ]+)\]? <([ \w]+)> (.*)') + m = r.match(line) + if m: + time_ = parse_time(m.group(1).strip()) + nick = m.group(2).strip() + line = m.group(3).strip() + if M.owner is None: + M.owner = nick ; M.chairs = {nick:True} + M.addline(nick, line, time_=time_) + # match /me lines + r = re.compile(r'\[?([0-9: ]+)\]? \* ([\w]+) (.*)') + m = r.match(line) + if m: + time_ = parse_time(m.group(1).strip()) + nick = m.group(2).strip() + line = m.group(3).strip() + M.addline(nick, "ACTION "+line, time_=time_) + M.save() + + # Load a pickled meeting file and replay it. + # python meeting.py load <blah>.pickle + elif sys.argv[1] == 'load': + fname = sys.argv[2] + + M = Meeting.__new__(Meeting) + M.__dict__ = cPickle.load(file(fname)) + #M.save() + from rkddp.interact import interact ; interact() + diff --git a/plugin.py b/plugin.py new file mode 100644 index 0000000..e3996a5 --- /dev/null +++ b/plugin.py @@ -0,0 +1,104 @@ +### +# Copyright (c) 2009, Richard Darst +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks +import supybot.ircmsgs as ircmsgs + +import time +import meeting +meeting = reload(meeting) + +class MeatBot(callbacks.Plugin): + """Add the help for "@plugin help MeatBot" here + This should describe *how* to use this plugin.""" + + def __init__(self, irc): + self.__parent = super(MeatBot, self) + self.__parent.__init__(irc) + + self.Meetings = { } + + # Instead of using real supybot commands, I just listen to ALL + # messages coming in and respond to those beginning with our + # prefix char. I found this helpful from a not duplicating logic + # standpoint (as well as other things). Ask me if you have more + # questions. + + # This captures all messages coming into the bot. + def doPrivmsg(self, irc, msg): + nick = msg.nick + channel = msg.args[0] + payload = msg.args[1] + + # The following is for debugging. It's excellent to get an + # interactive interperter inside of the live bot. use + # code.interact instead of my souped-up version if you aren't + # on my computer: + if payload == 'interact': + from rkddp.interact import interact ; interact() + + # Get our Meeting object, if one exists. Have to keep track + # of different servers/channels. + # (channel, network) tuple is our lookup key. + Mkey = (channel,irc.msg.tags['receivedOn']) + M = self.Meetings.get(Mkey, None) + + # Start meeting if we are requested + if payload[:13] == '#startmeeting': + if M is not None: + irc.error("Can't start another meeting, one is in progress.") + return + M = meeting.Meeting(channel=channel, owner=nick, + oldtopic=irc.state.channels[channel].topic) + self.Meetings[Mkey] = M + # This callback is used to send data to the channel: + def _setTopic(x): + irc.sendMsg(ircmsgs.topic(channel, x)) + def _sendReply(x): + irc.sendMsg(ircmsgs.privmsg(channel, x)) + M._setTopic = _setTopic + M._sendReply = _sendReply + # If there is no meeting going on, then we quit + if M is None: return + # Add line to our meeting buffer. + M.addline(nick, payload) + # End meeting if requested: + if payload[:11] == '#endmeeting': + #M.save() # now do_endmeeting in M calls the save functions + del self.Meetings[Mkey] + +Class = MeatBot + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: @@ -0,0 +1,37 @@ +### +# Copyright (c) 2009, Richard Darst +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +from supybot.test import * + +class MeatBotTestCase(PluginTestCase): + plugins = ('MeatBot',) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: |