Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard Darst <rkd@zgib.net>2009-05-04 04:22:43 (GMT)
committer Richard Darst <rkd@zgib.net>2009-05-04 04:22:43 (GMT)
commitcea8aae84a420a1e8f289b234fc7fb8c8db7658d (patch)
treec08908d2fb1d9bdd9dde5b921b87ec5a1df74eba
Inital Checkin - should be working
darcs-hash:20090504042243-82ea9-350eb26dd8561a18efc98dd5d5f7a730ce37cdc7.gz
-rw-r--r--README.txt43
-rw-r--r--__init__.py66
-rw-r--r--config.py49
-rw-r--r--meeting.py457
-rw-r--r--plugin.py104
-rw-r--r--test.py37
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 = {">":"&gt;" , "<":"&lt;" , "&":"&amp;",
+ '"':"&quot;", "'":"&apos;", }
+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:
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..73c9aae
--- /dev/null
+++ b/test.py
@@ -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: