diff options
author | Joachim Filip Ignacy Bartosik <jbartosik@gmail.com> | 2011-05-18 16:44:11 +0200 |
---|---|---|
committer | Joachim Filip Ignacy Bartosik <jbartosik@gmail.com> | 2011-05-24 19:03:00 +0200 |
commit | e8c39d513356e14b60813c54824a63a6ad516348 (patch) | |
tree | dba08b74ad8470b3d36d813d14e310e512acea57 /bot/ircmeeting | |
parent | Basic meeting participation tracing (diff) | |
download | council-webapp-e8c39d513356e14b60813c54824a63a6ad516348.tar.gz council-webapp-e8c39d513356e14b60813c54824a63a6ad516348.tar.bz2 council-webapp-e8c39d513356e14b60813c54824a63a6ad516348.zip |
MeetBot plugin from Debian
Diffstat (limited to 'bot/ircmeeting')
-rw-r--r-- | bot/ircmeeting/__init__.py | 0 | ||||
-rw-r--r-- | bot/ircmeeting/css-log-default.css | 15 | ||||
-rw-r--r-- | bot/ircmeeting/css-minutes-default.css | 34 | ||||
-rw-r--r-- | bot/ircmeeting/items.py | 292 | ||||
-rw-r--r-- | bot/ircmeeting/meeting.py | 672 | ||||
-rw-r--r-- | bot/ircmeeting/template.html | 102 | ||||
-rw-r--r-- | bot/ircmeeting/template.txt | 55 | ||||
-rw-r--r-- | bot/ircmeeting/writers.py | 1197 |
8 files changed, 2367 insertions, 0 deletions
diff --git a/bot/ircmeeting/__init__.py b/bot/ircmeeting/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/bot/ircmeeting/__init__.py diff --git a/bot/ircmeeting/css-log-default.css b/bot/ircmeeting/css-log-default.css new file mode 100644 index 0000000..ee7c6a3 --- /dev/null +++ b/bot/ircmeeting/css-log-default.css @@ -0,0 +1,15 @@ +/* For the .log.html */ +pre { /*line-height: 125%;*/ + white-space: pre-wrap; } +body { background: #f0f0f0; } + +body .tm { color: #007020 } /* time */ +body .nk { color: #062873; font-weight: bold } /* nick, regular */ +body .nka { color: #007020; font-weight: bold } /* action nick */ +body .ac { color: #00A000 } /* action line */ +body .hi { color: #4070a0 } /* hilights */ +/* Things to make particular MeetBot commands stick out */ +body .topic { color: #007020; font-weight: bold } +body .topicline { color: #000080; font-weight: bold } +body .cmd { color: #007020; font-weight: bold } +body .cmdline { font-weight: bold } diff --git a/bot/ircmeeting/css-minutes-default.css b/bot/ircmeeting/css-minutes-default.css new file mode 100644 index 0000000..c383b14 --- /dev/null +++ b/bot/ircmeeting/css-minutes-default.css @@ -0,0 +1,34 @@ +/* This is for the .html in the HTML2 writer */ +body { + font-family: Helvetica, sans-serif; + font-size:14px; +} +h1 { + text-align: center; +} +a { + color:navy; + text-decoration: none; + border-bottom:1px dotted navy; +} +a:hover { + text-decoration:none; + border-bottom: 0; + color:#0000B9; +} +hr { + border: 1px solid #ccc; +} +/* The (nick, time) item pairs, and other body text things. */ +.details { + font-size: 12px; + font-weight:bold; +} +/* The 'AGREED:', 'IDEA', etc, prefix to lines. */ +.itemtype { + font-style: normal; /* un-italics it */ + font-weight: bold; +} +/* Example: change single item types. Capitalized command name. +/* .TOPIC { color:navy; } */ +/* .AGREED { color:lime; } */ diff --git a/bot/ircmeeting/items.py b/bot/ircmeeting/items.py new file mode 100644 index 0000000..1109fb0 --- /dev/null +++ b/bot/ircmeeting/items.py @@ -0,0 +1,292 @@ +# Richard Darst, June 2009 + +### +# 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 os +import re +import time + +import writers +#from writers import html, rst +import itertools + +def inbase(i, chars='abcdefghijklmnopqrstuvwxyz', place=0): + """Converts an integer into a postfix in base 26 using ascii chars. + + This is used to make a unique postfix for ReStructured Text URL + references, which must be unique. (Yes, this is over-engineering, + but keeps it short and nicely arranged, and I want practice + writing recursive functions.) + """ + div, mod = divmod(i, len(chars)**(place+1)) + if div == 0: + return chars[mod] + else: + return inbase2(div, chars=chars, place=place+1)+chars[mod] + + + +# +# These are objects which we can add to the meeting minutes. Mainly +# they exist to aid in HTML-formatting. +# +class _BaseItem(object): + itemtype = None + starthtml = '' + endhtml = '' + startrst = '' + endrst = '' + starttext = '' + endtext = '' + startmw = '' + endmw = '' + def get_replacements(self, M, escapewith): + replacements = { } + for name in dir(self): + if name[0] == "_": continue + replacements[name] = getattr(self, name) + replacements['nick'] = escapewith(replacements['nick']) + replacements['link'] = self.logURL(M) + for key in ('line', 'prefix', 'suffix', 'topic'): + if key in replacements: + replacements[key] = escapewith(replacements[key]) + if 'url' in replacements: + replacements['url_quoteescaped'] = \ + escapewith(self.url.replace('"', "%22")) + + return replacements + def template(self, M, escapewith): + template = { } + for k,v in self.get_replacements(M, escapewith).iteritems(): + if k not in ('itemtype', 'line', 'topic', + 'url', 'url_quoteescaped', + 'nick', 'time', 'link', 'anchor'): + continue + template[k] = v + return template + def makeRSTref(self, M): + if self.nick[-1] == '_': + rstref = rstref_orig = "%s%s"%(self.nick, self.time) + else: + rstref = rstref_orig = "%s-%s"%(self.nick, self.time) + count = 0 + while rstref in M.rst_refs: + rstref = rstref_orig + inbase(count) + count += 1 + link = self.logURL(M) + M.rst_urls.append(".. _%s: %s"%(rstref, link+"#"+self.anchor)) + M.rst_refs[rstref] = True + return rstref + @property + def anchor(self): + return 'l-'+str(self.linenum) + def logURL(self, M): + return M.config.basename+'.log.html' + +class Topic(_BaseItem): + itemtype = 'TOPIC' + html_template = """<tr><td><a href='%(link)s#%(anchor)s'>%(time)s</a></td> + <th colspan=3>%(starthtml)sTopic: %(topic)s%(endhtml)s</th> + </tr>""" + #html2_template = ("""<b>%(starthtml)s%(topic)s%(endhtml)s</b> """ + # """(%(nick)s, <a href='%(link)s#%(anchor)s'>%(time)s</a>)""") + html2_template = ("""%(starthtml)s%(topic)s%(endhtml)s """ + """<span class="details">""" + """(<a href='%(link)s#%(anchor)s'>%(nick)s</a>, """ + """%(time)s)""" + """</span>""") + rst_template = """%(startrst)s%(topic)s%(endrst)s (%(rstref)s_)""" + text_template = """%(starttext)s%(topic)s%(endtext)s (%(nick)s, %(time)s)""" + mw_template = """%(startmw)s%(topic)s%(endmw)s (%(nick)s, %(time)s)""" + startrst = '**' + endrst = '**' + startmw = "'''" + endmw = "'''" + starthtml = '<b class="TOPIC">' + endhtml = '</b>' + 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 _htmlrepl(self, M): + repl = self.get_replacements(M, escapewith=writers.html) + repl['link'] = self.logURL(M) + return repl + def html(self, M): + return self.html_template%self._htmlrepl(M) + def html2(self, M): + return self.html2_template%self._htmlrepl(M) + def rst(self, M): + self.rstref = self.makeRSTref(M) + repl = self.get_replacements(M, escapewith=writers.rst) + if repl['topic']=='': repl['topic']=' ' + repl['link'] = self.logURL(M) + return self.rst_template%repl + def text(self, M): + repl = self.get_replacements(M, escapewith=writers.text) + repl['link'] = self.logURL(M) + return self.text_template%repl + def mw(self, M): + repl = self.get_replacements(M, escapewith=writers.mw) + return self.mw_template%repl + +class GenericItem(_BaseItem): + itemtype = '' + html_template = """<tr><td><a href='%(link)s#%(anchor)s'>%(time)s</a></td> + <td>%(itemtype)s</td><td>%(nick)s</td><td>%(starthtml)s%(line)s%(endhtml)s</td> + </tr>""" + #html2_template = ("""<i>%(itemtype)s</i>: %(starthtml)s%(line)s%(endhtml)s """ + # """(%(nick)s, <a href='%(link)s#%(anchor)s'>%(time)s</a>)""") + html2_template = ("""<i class="itemtype">%(itemtype)s</i>: """ + """<span class="%(itemtype)s">""" + """%(starthtml)s%(line)s%(endhtml)s</span> """ + """<span class="details">""" + """(<a href='%(link)s#%(anchor)s'>%(nick)s</a>, """ + """%(time)s)""" + """</span>""") + rst_template = """*%(itemtype)s*: %(startrst)s%(line)s%(endrst)s (%(rstref)s_)""" + text_template = """%(itemtype)s: %(starttext)s%(line)s%(endtext)s (%(nick)s, %(time)s)""" + mw_template = """''%(itemtype)s:'' %(startmw)s%(line)s%(endmw)s (%(nick)s, %(time)s)""" + 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 _htmlrepl(self, M): + repl = self.get_replacements(M, escapewith=writers.html) + repl['link'] = self.logURL(M) + return repl + def html(self, M): + return self.html_template%self._htmlrepl(M) + def html2(self, M): + return self.html2_template%self._htmlrepl(M) + def rst(self, M): + self.rstref = self.makeRSTref(M) + repl = self.get_replacements(M, escapewith=writers.rst) + repl['link'] = self.logURL(M) + return self.rst_template%repl + def text(self, M): + repl = self.get_replacements(M, escapewith=writers.text) + repl['link'] = self.logURL(M) + return self.text_template%repl + def mw(self, M): + repl = self.get_replacements(M, escapewith=writers.mw) + return self.mw_template%repl + + +class Info(GenericItem): + itemtype = 'INFO' + html2_template = ("""<span class="%(itemtype)s">""" + """%(starthtml)s%(line)s%(endhtml)s</span> """ + """<span class="details">""" + """(<a href='%(link)s#%(anchor)s'>%(nick)s</a>, """ + """%(time)s)""" + """</span>""") + rst_template = """%(startrst)s%(line)s%(endrst)s (%(rstref)s_)""" + text_template = """%(starttext)s%(line)s%(endtext)s (%(nick)s, %(time)s)""" + mw_template = """%(startmw)s%(line)s%(endmw)s (%(nick)s, %(time)s)""" +class Idea(GenericItem): + itemtype = 'IDEA' +class Agreed(GenericItem): + itemtype = 'AGREED' +class Action(GenericItem): + itemtype = 'ACTION' +class Help(GenericItem): + itemtype = 'HELP' +class Accepted(GenericItem): + itemtype = 'ACCEPTED' + starthtml = '<font color="green">' + endhtml = '</font>' +class Rejected(GenericItem): + itemtype = 'REJECTED' + starthtml = '<font color="red">' + endhtml = '</font>' +class Link(_BaseItem): + itemtype = 'LINK' + html_template = """<tr><td><a href='%(link)s#%(anchor)s'>%(time)s</a></td> + <td>%(itemtype)s</td><td>%(nick)s</td><td>%(starthtml)s%(prefix)s<a href="%(url)s">%(url_readable)s</a>%(suffix)s%(endhtml)s</td> + </tr>""" + html2_template = ("""%(starthtml)s%(prefix)s<a href="%(url)s">%(url_readable)s</a>%(suffix)s%(endhtml)s """ + """<span class="details">""" + """(<a href='%(link)s#%(anchor)s'>%(nick)s</a>, """ + """%(time)s)""" + """</span>""") + rst_template = """*%(itemtype)s*: %(startrst)s%(prefix)s%(url)s%(suffix)s%(endrst)s (%(rstref)s_)""" + text_template = """%(itemtype)s: %(starttext)s%(prefix)s%(url)s%(suffix)s%(endtext)s (%(nick)s, %(time)s)""" + mw_template = """''%(itemtype)s:'' %(startmw)s%(prefix)s%(url)s%(suffix)s%(endmw)s (%(nick)s, %(time)s)""" + def __init__(self, nick, line, linenum, time_, M): + self.nick = nick ; self.linenum = linenum + self.time = time.strftime("%H:%M:%S", time_) + self.line = line + + protocols = M.config.UrlProtocols + protocols = '|'.join(re.escape(p) for p in protocols) + protocols = '(?:'+protocols+')' + # This is gross. + # (.*?) - any prefix, non-greedy + # (%s//[^\s]+ - protocol://... until the next space + # (?<!\.|\)) - but the last character can NOT be . or ) + # (.*) - any suffix + url_re = re.compile(r'(.*?)(%s//[^\s]+(?<!\.|\)))(.*)'%protocols) + m = url_re.match(line) + if m: + self.prefix = m.group(1) + self.url = m.group(2) + self.suffix = m.group(3) + else: + # simple matching, the old way. + self.url, self.suffix = (line+' ').split(' ', 1) + self.suffix = ' '+self.suffix + self.prefix = '' + # URL-sanitization + self.url_readable = self.url # readable line version + self.url = self.url + self.line = self.line.strip() + def _htmlrepl(self, M): + repl = self.get_replacements(M, escapewith=writers.html) + # special: replace doublequote only for the URL. + repl['url'] = writers.html(self.url.replace('"', "%22")) + repl['url_readable'] = writers.html(self.url) + repl['link'] = self.logURL(M) + return repl + def html(self, M): + return self.html_template%self._htmlrepl(M) + def html2(self, M): + return self.html2_template%self._htmlrepl(M) + def rst(self, M): + self.rstref = self.makeRSTref(M) + repl = self.get_replacements(M, escapewith=writers.rst) + repl['link'] = self.logURL(M) + #repl['url'] = writers.rst(self.url) + return self.rst_template%repl + def text(self, M): + repl = self.get_replacements(M, escapewith=writers.text) + repl['link'] = self.logURL(M) + return self.text_template%repl + def mw(self, M): + repl = self.get_replacements(M, escapewith=writers.mw) + return self.mw_template%repl diff --git a/bot/ircmeeting/meeting.py b/bot/ircmeeting/meeting.py new file mode 100644 index 0000000..85880a6 --- /dev/null +++ b/bot/ircmeeting/meeting.py @@ -0,0 +1,672 @@ +# Richard Darst, May 2009 + +### +# 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 time +import os +import re +import stat + +import writers +import items +reload(writers) +reload(items) + +__version__ = "0.1.4" + +class Config(object): + # + # Throw any overrides into meetingLocalConfig.py in this directory: + # + # Where to store files on disk + # Example: logFileDir = '/home/richard/meetbot/' + logFileDir = '.' + # The links to the logfiles are given this prefix + # Example: logUrlPrefix = 'http://rkd.zgib.net/meetbot/' + logUrlPrefix = '' + # Give the pattern to save files into here. Use %(channel)s for + # channel. This will be sent through strftime for substituting it + # times, howover, for strftime codes you must use doubled percent + # signs (%%). This will be joined with the directories above. + filenamePattern = '%(channel)s/%%Y/%(channel)s.%%F-%%H.%%M' + # Where to say to go for more information about MeetBot + MeetBotInfoURL = 'http://wiki.debian.org/MeetBot' + # This is used with the #restrict command to remove permissions from files. + RestrictPerm = stat.S_IRWXO|stat.S_IRWXG # g,o perm zeroed + # 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. First group is the cmd name, + # second group is the rest of the line. + command_RE = re.compile(r'#([\w]+)[ \t]*(.*)') + # The channels which won't have date/time appended to the filename. + specialChannels = ("#meetbot-test", "#meetbot-test2") + specialChannelFilenamePattern = '%(channel)s/%(channel)s' + # HTML irc log highlighting style. `pygmentize -L styles` to list. + pygmentizeStyle = 'friendly' + # Timezone setting. You can use friendly names like 'US/Eastern', etc. + # Check /usr/share/zoneinfo/ . Or `man timezone`: this is the contents + # of the TZ environment variable. + timeZone = 'UTC' + # These are the start and end meeting messages, respectively. + # Some replacements are done before they are used, using the + # %(name)s syntax. Note that since one replacement is done below, + # you have to use doubled percent signs. Also, it gets split by + # '\n' and each part between newlines get said in a separate IRC + # message. + startMeetingMessage = ("Meeting started %(starttime)s %(timeZone)s. " + "The chair is %(chair)s. Information about MeetBot at " + "%(MeetBotInfoURL)s.\n" + "Useful Commands: #action #agreed #help #info #idea #link " + "#topic.") + endMeetingMessage = ("Meeting ended %(endtime)s %(timeZone)s. " + "Information about MeetBot at %(MeetBotInfoURL)s . " + "(v %(__version__)s)\n" + "Minutes: %(urlBasename)s.html\n" + "Minutes (text): %(urlBasename)s.txt\n" + "Log: %(urlBasename)s.log.html") + # Input/output codecs. + input_codec = 'utf-8' + output_codec = 'utf-8' + # Functions to do the i/o conversion. + def enc(self, text): + return text.encode(self.output_codec, 'replace') + def dec(self, text): + return text.decode(self.input_codec, 'replace') + # Write out select logfiles + update_realtime = True + # CSS configs: + cssFile_log = 'default' + cssEmbed_log = True + cssFile_minutes = 'default' + cssEmbed_minutes = True + + # This tells which writers write out which to extensions. + writer_map = { + '.log.html':writers.HTMLlog, + #'.1.html': writers.HTML, + '.html': writers.HTML2, + #'.rst': writers.ReST, + '.txt': writers.Text, + #'.rst.html':writers.HTMLfromReST, + } + + + def __init__(self, M, writeRawLog=False, safeMode=False, + extraConfig={}): + self.M = M + self.writers = { } + # Update config values with anything we may have + for k,v in extraConfig.iteritems(): + setattr(self, k, v) + + if hasattr(self, "init_hook"): + self.init_hook() + if writeRawLog: + self.writers['.log.txt'] = writers.TextLog(self.M) + for extension, writer in self.writer_map.iteritems(): + self.writers[extension] = writer(self.M) + self.safeMode = safeMode + def filename(self, url=False): + # provide a way to override the filename. If it is + # overridden, it must be a full path (and the URL-part may not + # work.): + if getattr(self.M, '_filename', None): + return self.M._filename + # names useful for pathname formatting. + # Certain test channels always get the same name - don't need + # file prolifiration for them + if self.M.channel in self.specialChannels: + pattern = self.specialChannelFilenamePattern + else: + pattern = self.filenamePattern + channel = self.M.channel.strip('# ').lower().replace('/', '') + network = self.M.network.strip(' ').lower().replace('/', '') + if self.M._meetingname: + meetingname = self.M._meetingname.replace('/', '') + else: + meetingname = channel + path = pattern%{'channel':channel, 'network':network, + 'meetingname':meetingname} + path = time.strftime(path, self.M.starttime) + # If we want the URL name, append URL prefix and return + if url: + return os.path.join(self.logUrlPrefix, path) + path = os.path.join(self.logFileDir, path) + # make directory if it doesn't exist... + dirname = os.path.dirname(path) + if not url and dirname and not os.access(dirname, os.F_OK): + os.makedirs(dirname) + return path + @property + def basename(self): + return os.path.basename(self.M.config.filename()) + + def save(self, realtime_update=False): + """Write all output files. + + If `realtime_update` is true, then this isn't a complete save, + it will only update those writers with the update_realtime + attribute true. (default update_realtime=False for this method)""" + if realtime_update and not hasattr(self.M, 'starttime'): + return + rawname = self.filename() + # We want to write the rawlog (.log.txt) first in case the + # other methods break. That way, we have saved enough to + # replay. + writer_names = list(self.writers.keys()) + results = { } + if '.log.txt' in writer_names: + writer_names.remove('.log.txt') + writer_names = ['.log.txt'] + writer_names + for extension in writer_names: + writer = self.writers[extension] + # Why this? If this is a realtime (step-by-step) update, + # then we only want to update those writers which say they + # should be updated step-by-step. + if (realtime_update and + ( not getattr(writer, 'update_realtime', False) or + getattr(self, '_filename', None) ) + ): + continue + # Parse embedded arguments + if '|' in extension: + extension, args = extension.split('|', 1) + args = args.split('|') + args = dict([a.split('=', 1) for a in args] ) + else: + args = { } + + text = writer.format(extension, **args) + results[extension] = text + # If the writer returns a string or unicode object, then + # we should write it to a filename with that extension. + # If it doesn't, then it's assumed that the write took + # care of writing (or publishing or emailing or wikifying) + # it itself. + if isinstance(text, unicode): + text = self.enc(text) + if isinstance(text, (str, unicode)): + # Have a way to override saving, so no disk files are written. + if getattr(self, "dontSave", False): + pass + # ".none" or a single "." disable writing. + elif extension.lower()[:5] in (".none", "."): + pass + else: + filename = rawname + extension + self.writeToFile(text, filename) + if hasattr(self, 'save_hook'): + self.save_hook(realtime_update=realtime_update) + return results + def writeToFile(self, string, filename): + """Write a given string to a file""" + # The reason we have this method just for this is to proxy + # through the _restrictPermissions logic. + f = open(filename, 'w') + if self.M._restrictlogs: + self.restrictPermissions(f) + f.write(string) + f.close() + def restrictPermissions(self, f): + """Remove the permissions given in the variable RestrictPerm.""" + f.flush() + newmode = os.stat(f.name).st_mode & (~self.RestrictPerm) + os.chmod(f.name, newmode) + def findFile(self, fname): + """Find template files by searching paths. + + Expand '+' prefix to the base data directory. + """ + # If `template` begins in '+', then it in relative to the + # MeetBot source directory. + if fname[0] == '+': + basedir = os.path.dirname(__file__) + fname = os.path.join(basedir, fname[1:]) + # If we don't test here, it might fail in the try: block + # below, then f.close() will fail and mask the original + # exception + if not os.access(fname, os.F_OK): + raise IOError('File not found: %s'%fname) + return fname + + + +# Set the timezone, using the variable above +os.environ['TZ'] = Config.timeZone +time.tzset() + +# load custom local configurations +LocalConfig = None +import __main__ +# Two conditions where we do NOT load any local configuration files +if getattr(__main__, 'running_tests', False): pass +elif 'MEETBOT_RUNNING_TESTS' in os.environ: pass +else: + # First source of config: try just plain importing it + try: + import meetingLocalConfig + meetingLocalConfig = reload(meetingLocalConfig) + if hasattr(meetingLocalConfig, 'Config'): + LocalConfig = meetingLocalConfig.Config + except ImportError: + pass + if LocalConfig is None: + for dirname in (os.path.dirname("__file__"), "."): + fname = os.path.join(dirname, "meetingLocalConfig.py") + if os.access(fname, os.F_OK): + meetingLocalConfig = { } + execfile(fname, meetingLocalConfig) + LocalConfig = meetingLocalConfig["Config"] + break + if LocalConfig is not None: + # Subclass Config and LocalConfig, new type overrides Config. + Config = type('Config', (LocalConfig, Config), {}) + + +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_, line, **kwargs): + """Begin a meeting.""" + self.starttime = time_ + repl = self.replacements() + message = self.config.startMeetingMessage%repl + for messageline in message.split('\n'): + self.reply(messageline) + if line.strip(): + self.do_meetingtopic(nick=nick, line=line, time_=time_, **kwargs) + def do_endmeeting(self, nick, time_, **kwargs): + """End the meeting.""" + if not self.isChair(nick): return + if self.oldtopic: + self.topic(self.oldtopic) + self.endtime = time_ + self.config.save() + repl = self.replacements() + message = self.config.endMeetingMessage%repl + for messageline in message.split('\n'): + self.reply(messageline) + self._meetingIsOver = True + def do_topic(self, nick, line, **kwargs): + """Set a new topic in the channel.""" + if not self.isChair(nick): return + self.currenttopic = line + m = items.Topic(nick=nick, line=line, **kwargs) + self.additem(m) + self.settopic() + def do_meetingtopic(self, nick, line, **kwargs): + """Set a meeting topic (included in all sub-topics)""" + if not self.isChair(nick): return + line = line.strip() + if line == '' or line.lower() == 'none' or line.lower() == 'unset': + self._meetingTopic = None + else: + self._meetingTopic = line + self.settopic() + def do_save(self, nick, time_, **kwargs): + """Add a chair to the meeting.""" + if not self.isChair(nick): return + self.endtime = time_ + self.config.save() + def do_agreed(self, nick, **kwargs): + """Add aggreement to the minutes - chairs only.""" + if not self.isChair(nick): return + m = items.Agreed(nick, **kwargs) + self.additem(m) + do_agree = do_agreed + def do_accepted(self, nick, **kwargs): + """Add aggreement to the minutes - chairs only.""" + if not self.isChair(nick): return + m = items.Accepted(nick, **kwargs) + self.additem(m) + do_accept = do_accepted + def do_rejected(self, nick, **kwargs): + """Add aggreement to the minutes - chairs only.""" + if not self.isChair(nick): return + m = items.Rejected(nick, **kwargs) + self.additem(m) + do_reject = do_rejected + def do_chair(self, nick, line, **kwargs): + """Add a chair to the meeting.""" + if not self.isChair(nick): return + for chair in re.split('[, ]+', line.strip()): + chair = chair.strip() + if not chair: continue + if chair not in self.chairs: + if self._channelNicks is not None and \ + ( chair.encode(self.config.input_codec) + not in self._channelNicks()): + self.reply("Warning: Nick not in channel: %s"%chair) + self.addnick(chair, lines=0) + self.chairs.setdefault(chair, True) + chairs = dict(self.chairs) # make a copy + chairs.setdefault(self.owner, True) + self.reply("Current chairs: %s"%(" ".join(sorted(chairs.keys())))) + 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(): + chair = chair.strip() + if chair in self.chairs: + del self.chairs[chair] + chairs = dict(self.chairs) # make a copy + chairs.setdefault(self.owner, True) + self.reply("Current chairs: %s"%(" ".join(sorted(chairs.keys())))) + 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 + def do_meetingname(self, nick, time_, line, **kwargs): + """Set the variable (meetingname) which can be used in save. + + If this isn't set, it defaults to the channel name.""" + meetingname = line.strip().lower().replace(" ", "") + meetingname = "_".join(line.strip().lower().split()) + self._meetingname = meetingname + self.reply("The meeting name has been set to '%s'"%meetingname) + # 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 = items.Action(**kwargs) + self.additem(m) + def do_info(self, **kwargs): + """Add informational item to the minutes.""" + m = items.Info(**kwargs) + self.additem(m) + def do_idea(self, **kwargs): + """Add informational item to the minutes.""" + m = items.Idea(**kwargs) + self.additem(m) + def do_help(self, **kwargs): + """Add call for help to the minutes.""" + m = items.Help(**kwargs) + self.additem(m) + do_halp = do_help + 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 = re.split('[, ]+', line.strip()) + for nick in nicks: + nick = nick.strip() + if not nick: continue + self.addnick(nick, lines=0) + def do_link(self, **kwargs): + """Add informational item to the minutes.""" + m = items.Link(M=self, **kwargs) + self.additem(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, oldtopic=None, + filename=None, writeRawLog=False, + setTopic=None, sendReply=None, getRegistryValue=None, + safeMode=False, channelNicks=None, + extraConfig={}, network='nonetwork'): + if getRegistryValue is not None: + self._registryValue = getRegistryValue + if sendReply is not None: + self._sendReply = sendReply + if setTopic is not None: + self._setTopic = setTopic + self.owner = owner + self.channel = channel + self.network = network + self.currenttopic = "" + self.config = Config(self, writeRawLog=writeRawLog, safeMode=safeMode, + extraConfig=extraConfig) + if oldtopic: + self.oldtopic = self.config.dec(oldtopic) + else: + self.oldtopic = None + self.lines = [ ] + self.minutes = [ ] + self.attendees = { } + self.chairs = { } + self._writeRawLog = writeRawLog + self._meetingTopic = None + self._meetingname = "" + self._meetingIsOver = False + self._channelNicks = channelNicks + if filename: + self._filename = filename + + # 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(self.config.enc(x)) + else: + print "REPLY:", self.config.enc(x) + def topic(self, x): + """Set the topic in the IRC channel.""" + if hasattr(self, '_setTopic') and not self._lurk: + self._setTopic(self.config.enc(x)) + else: + print "TOPIC:", self.config.enc(x) + def settopic(self): + "The actual code to set the topic" + if self._meetingTopic: + topic = '%s (Meeting topic: %s)'%(self.currenttopic, + self._meetingTopic) + else: + topic = self.currenttopic + self.topic(topic) + 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 nick in self.chairs) + def save(self, **kwargs): + return self.config.save(**kwargs) + # 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. + """ + linenum = self.addrawline(nick, line, time_) + + if time_ is None: time_ = time.localtime() + nick = self.config.dec(nick) + line = self.config.dec(line) + + # Handle any commands given in the line. + matchobj = self.config.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 self.config.UrlProtocols: + self.do_link(nick=nick, line=line, + linenum=linenum, time_=time_) + self.save(realtime_update=True) + + def addrawline(self, nick, line, time_=None): + """This adds a line to the log, bypassing command execution. + """ + nick = self.config.dec(nick) + line = self.config.dec(line) + 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.localtime() + + # 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) + return linenum + + def additem(self, m): + """Add an item to the meeting minutes list. + """ + self.minutes.append(m) + def replacements(self): + repl = { } + repl['channel'] = self.channel + repl['network'] = self.network + repl['MeetBotInfoURL'] = self.config.MeetBotInfoURL + repl['timeZone'] = self.config.timeZone + repl['starttime'] = repl['endtime'] = "None" + if getattr(self, "starttime", None) is not None: + repl['starttime'] = time.asctime(self.starttime) + if getattr(self, "endtime", None) is not None: + repl['endtime'] = time.asctime(self.endtime) + repl['__version__'] = __version__ + repl['chair'] = self.owner + repl['urlBasename'] = self.config.filename(url=True) + repl['basename'] = os.path.basename(self.config.filename()) + return repl + + + + + +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 +logline_re = re.compile(r'\[?([0-9: ]*)\]? *<[@+]?([^>]+)> *(.*)') +loglineAction_re = re.compile(r'\[?([0-9: ]*)\]? *\* *([^ ]+) *(.*)') + + +def process_meeting(contents, channel, filename, + extraConfig = {}, + dontSave=False, + safeMode=True): + M = Meeting(channel=channel, owner=None, + filename=filename, writeRawLog=False, safeMode=safeMode, + extraConfig=extraConfig) + if dontSave: + M.config.dontSave = True + # process all lines + for line in contents.split('\n'): + # match regular spoken lines: + m = logline_re.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 + m = loglineAction_re.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_) + return M + +# None of this is very well refined. +if __name__ == '__main__': + import sys + if sys.argv[1] == 'replay': + fname = sys.argv[2] + m = re.match('(.*)\.log\.txt', fname) + if m: + filename = m.group(1) + else: + filename = os.path.splitext(fname)[0] + print 'Saving to:', filename + channel = '#'+os.path.basename(sys.argv[2]).split('.')[0] + + M = Meeting(channel=channel, owner=None, + filename=filename, writeRawLog=False) + for line in file(sys.argv[2]): + # match regular spoken lines: + m = logline_re.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 + m = loglineAction_re.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() # should be done by #endmeeting in the logs! + else: + print 'Command "%s" not found.'%sys.argv[1] + diff --git a/bot/ircmeeting/template.html b/bot/ircmeeting/template.html new file mode 100644 index 0000000..b67b935 --- /dev/null +++ b/bot/ircmeeting/template.html @@ -0,0 +1,102 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns:py="http://genshi.edgewall.org/"> +<head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"/> + <title>${meeting.title}</title> + <style type="text/css"> +/* This is for the .html in the HTML2 writer */ +body { + font-family: Helvetica, sans-serif; + font-size:14px; +} +h1 { + text-align: center; +} +a { + color:navy; + text-decoration: none; + border-bottom:1px dotted navy; +} +a:hover { + text-decoration:none; + border-bottom: 0; + color:#0000B9; +} +hr { + border: 1px solid #ccc; +} +/* The (nick, time) item pairs, and other body text things. */ +.details { + font-size: 12px; + font-weight:bold; +} +/* The 'AGREED:', 'IDEA', etc, prefix to lines. */ +.itemtype { + font-style: normal; /* un-italics it */ + font-weight: bold; +} +/* Example: change single item types. Capitalized command name. +/* .TOPIC { color:navy; } */ +/* .AGREED { color:lime; } */ + + </style> +</head> + +<body> + <h1>${meeting.title}</h1> + <span class="details"> Meeting started by ${meeting.owner} at ${time.start} ${time.timezone} (<a href="${meeting.logs}">full logs</a>).</span> + + <h3>Meeting summary</h3> + <ol> + <li py:for="item in agenda"> + <b class="TOPIC">${item.topic.topic}</b> <span py:if="item.topic.nick" class="details">(<a href='${meeting.logs}#${item.topic.anchor}'>${item.topic.nick}</a>, ${item.topic.time})</span> + <ol type="a"> + <py:if test="len(item.notes) > 0"> + <li py:for="note in item.notes"> + <i class="itemtype">${note.itemtype}</i>: + <py:choose> + <py:when test="note.itemtype == 'LINK'"> + <span class="${note.itemtype}"> + <a href="${note.url}"> + <py:choose> + <py:when test="note.line">${note.line}</py:when> + <py:otherwise>${note.url}</py:otherwise> + </py:choose> + </a> + </span> + </py:when> + <py:otherwise> + <span class="${note.itemtype}">${note.line}</span> + </py:otherwise> + </py:choose> + <span class="details">(<a href='${meeting.logs}#${note.anchor}'>${note.nick}</a>, ${note.time})</span> + </li> + </py:if> + </ol> + </li> + </ol> + + <span class="details">Meeting ended at ${time.end} ${time.timezone} (<a href="${meeting.logs}">full logs</a>).</span> + + <h3>Action items</h3> + <ol> + <li py:for="action in actions">${action}</li> + </ol> + + <h3>Action items, by person</h3> + <ol> + <li py:for="attendee in actions_person">${attendee.nick} + <ol> + <li py:for="action in attendee.actions">${action}</li> + </ol> + </li> + </ol> + + <h3>People present (lines said)</h3> + <ol> + <li py:for="attendee in attendees">${attendee.nick} (${attendee.count})</li> + </ol> + + <span class="details">Generated by <a href="${meetbot.url}">MeetBot</a> ${meetbot.version}.</span> +</body> +</html> diff --git a/bot/ircmeeting/template.txt b/bot/ircmeeting/template.txt new file mode 100644 index 0000000..5cb8bb8 --- /dev/null +++ b/bot/ircmeeting/template.txt @@ -0,0 +1,55 @@ +{% python + heading = "="*len(meeting['title']) + + from textwrap import TextWrapper + def wrap(text, level): + return TextWrapper(width=72, initial_indent=' '*(level-1)*2, subsequent_indent=' '*level*2, break_long_words=False).fill(text) +%} +${heading} +${meeting.title} +${heading} + + +${wrap("Meeting started by %s at %s %s. The full logs are available at %s ."%(meeting.owner, time.start, time.timezone, meeting.logsFullURL), 1)} + + + +Meeting summary +--------------- + +{% for item in agenda %}\ +{% choose %} +{% when item.topic.nick %}${wrap("* %s (%s, %s)"%(item.topic.topic, item.topic.nick, item.topic.time), 1)}{% end %}\ +{% otherwise %}${wrap("* %s"%(item.topic.topic), 1)}{% end %} +{% end %}\ +{% for note in item.notes %}\ +{% choose %}\ +{% when note.itemtype == 'LINK' %}${wrap("* %s: %s %s (%s, %s)"%(note.itemtype, note.url, note.line, note.nick, note.time), 2)}{% end %}\ +{% otherwise %}${wrap("* %s: %s (%s, %s)"%(note.itemtype, note.line, note.nick, note.time), 2)}{% end %} +{% end %}\ +{% end %}\ +{% end %} + +${wrap("Meeting ended at %s %s."%(time.end, time.timezone), 1)} + + + +Action items, by person +----------------------- + +{% for attendee in actions_person %}\ +* ${attendee.nick} +{% for action in attendee.actions %}\ +${wrap("* %s"%action, 2)} +{% end %} +{% end %} + +People present (lines said) +--------------------------- + +{% for attendee in attendees %}\ +* ${attendee.nick} (${attendee.count}) +{% end %} + + +Generated by `MeetBot`_ ${meetbot.version} diff --git a/bot/ircmeeting/writers.py b/bot/ircmeeting/writers.py new file mode 100644 index 0000000..6eed012 --- /dev/null +++ b/bot/ircmeeting/writers.py @@ -0,0 +1,1197 @@ +# Richard Darst, June 2009 + +### +# 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 os +import re +import textwrap +import time + +#from meeting import timeZone, meetBotInfoURL + +# Needed for testing with isinstance() for properly writing. +#from items import Topic, Action +import items + +# Data sanitizing for various output methods +def html(text): + """Escape bad sequences (in HTML) in user-generated lines.""" + return text.replace("&", "&").replace("<", "<").replace(">", ">") +rstReplaceRE = re.compile('_( |-|$)') +def rst(text): + """Escapes bad sequences in reST""" + return rstReplaceRE.sub(r'\_\1', text) +def text(text): + """Escapes bad sequences in text (not implemented yet)""" + return text +def mw(text): + """Escapes bad sequences in MediaWiki markup (not implemented yet)""" + return text + + +# wraping functions (for RST) +class TextWrapper(textwrap.TextWrapper): + wordsep_re = re.compile(r'(\s+)') +def wrapList(item, indent=0): + return TextWrapper(width=72, initial_indent=' '*indent, + subsequent_indent= ' '*(indent+2), + break_long_words=False).fill(item) +def replaceWRAP(item): + re_wrap = re.compile(r'sWRAPs(.*)eWRAPe', re.DOTALL) + def repl(m): + return TextWrapper(width=72, break_long_words=False).fill(m.group(1)) + return re_wrap.sub(repl, item) + +def makeNickRE(nick): + return re.compile('\\b'+re.escape(nick)+'\\b', re.IGNORECASE) + +def MeetBotVersion(): + import meeting + if hasattr(meeting, '__version__'): + return ' '+meeting.__version__ + else: + return '' + + +class _BaseWriter(object): + def __init__(self, M, **kwargs): + self.M = M + + def format(self, extension=None, **kwargs): + """Override this method to implement the formatting. + + For file output writers, the method should return a unicode + object containing the contents of the file to write. + + The argument 'extension' is the key from `writer_map`. For + file writers, this can (and should) be ignored. For non-file + outputs, this can be used to This can be used to pass data, + + **kwargs is a dictionary of keyword arguments which are found + via parsing the extension to the writer. If an extension is + this: + .txt|arg1=val1|arg2=val2 + then kwargs will be passed as {'arg1':'val1', 'arg2':'val2'}. + This can be used for extra configuration for writers. + """ + raise NotImplementedError + + @property + def pagetitle(self): + if self.M._meetingTopic: + return "%s: %s"%(self.M.channel, self.M._meetingTopic) + return "%s Meeting"%self.M.channel + + def replacements(self): + return {'pageTitle':self.pagetitle, + 'owner':self.M.owner, + 'starttime':time.strftime("%H:%M:%S", self.M.starttime), + 'endtime':time.strftime("%H:%M:%S", self.M.endtime), + 'timeZone':self.M.config.timeZone, + 'fullLogs':self.M.config.basename+'.log.html', + 'fullLogsFullURL':self.M.config.filename(url=True)+'.log.html', + 'MeetBotInfoURL':self.M.config.MeetBotInfoURL, + 'MeetBotVersion':MeetBotVersion(), + } + def iterNickCounts(self): + nicks = [ (n,c) for (n,c) in self.M.attendees.iteritems() ] + nicks.sort(key=lambda x: x[1], reverse=True) + return nicks + + def iterActionItemsNick(self): + for nick in sorted(self.M.attendees.keys(), key=lambda x: x.lower()): + nick_re = makeNickRE(nick) + def nickitems(nick_re): + for m in self.M.minutes: + # The hack below is needed because of pickling problems + if m.itemtype != "ACTION": continue + if nick_re.search(m.line) is None: continue + m.assigned = True + yield m + yield nick, nickitems(nick_re=nick_re) + def iterActionItemsUnassigned(self): + for m in self.M.minutes: + if m.itemtype != "ACTION": continue + if getattr(m, 'assigned', False): continue + yield m + + def get_template(self, escape=lambda s: s): + M = self.M + repl = self.replacements() + + + MeetingItems = [ ] + # We can have initial items with NO initial topic. This + # messes up the templating, so, have this null topic as a + # stopgap measure. + nextTopic = {'topic':{'itemtype':'TOPIC', 'topic':'Prologue', + 'nick':'', + 'time':'', 'link':'', 'anchor':''}, + 'items':[] } + haveTopic = False + for m in M.minutes: + if m.itemtype == "TOPIC": + if nextTopic['topic']['nick'] or nextTopic['items']: + MeetingItems.append(nextTopic) + nextTopic = {'topic':m.template(M, escape), 'items':[] } + haveTopic = True + else: + nextTopic['items'].append(m.template(M, escape)) + MeetingItems.append(nextTopic) + repl['MeetingItems'] = MeetingItems + # Format of MeetingItems: + # [ {'topic': {item_dict}, + # 'items': [item_dict, item_object, item_object, ...] + # }, + # { 'topic':... + # 'items':... + # }, + # .... + # ] + # + # an item_dict has: + # item_dict = {'itemtype': TOPIC, ACTION, IDEA, or so on... + # 'line': the actual line that was said + # 'nick': nick of who said the line + # 'time': 10:53:15, for example, the time + # 'link': ${link}#${anchor} is the URL to link to. + # (page name, and bookmark) + # 'anchor': see above + # 'topic': if itemtype is TOPIC, 'line' is not given, + # instead we have 'topic' + # 'url': if itemtype is LINK, the line should be created + # by "${link} ${line}", where 'link' is the URL + # to link to, and 'line' is the rest of the line + # (that isn't a URL) + # 'url_quoteescaped': 'url' but with " escaped for use in + # <a href="$url_quoteescaped"> + ActionItems = [ ] + for m in M.minutes: + if m.itemtype != "ACTION": continue + ActionItems.append(escape(m.line)) + repl['ActionItems'] = ActionItems + # Format of ActionItems: It's just a very simple list of lines. + # [line, line, line, ...] + # line = (string of what it is) + + + ActionItemsPerson = [ ] + numberAssigned = 0 + for nick, items in self.iterActionItemsNick(): + thisNick = {'nick':escape(nick), 'items':[ ] } + for m in items: + numberAssigned += 1 + thisNick['items'].append(escape(m.line)) + if len(thisNick['items']) > 0: + ActionItemsPerson.append(thisNick) + # Work on the unassigned nicks. + thisNick = {'nick':'UNASSIGNED', 'items':[ ] } + for m in self.iterActionItemsUnassigned(): + thisNick['items'].append(escape(m.line)) + if len(thisNick['items']) > 1: + ActionItemsPerson.append(thisNick) + #if numberAssigned == 0: + # ActionItemsPerson = None + repl['ActionItemsPerson'] = ActionItemsPerson + # Format of ActionItemsPerson + # ActionItemsPerson = + # [ {'nick':nick_of_person, + # 'items': [item1, item2, item3, ...], + # }, + # ..., + # ..., + # {'nick':'UNASSIGNED', + # 'items': [item1, item2, item3, ...], + # } + # ] + + + PeoplePresent = [] + # sort by number of lines spoken + for nick, count in self.iterNickCounts(): + PeoplePresent.append({'nick':escape(nick), + 'count':count}) + repl['PeoplePresent'] = PeoplePresent + # Format of PeoplePresent + # [{'nick':the_nick, 'count':count_of_lines_said}, + # ..., + # ..., + # ] + + return repl + + def get_template2(self, escape=lambda s: s): + # let's make the data structure easier to use in the template + repl = self.get_template(escape=escape) + repl = { + 'time': { 'start': repl['starttime'], 'end': repl['endtime'], 'timezone': repl['timeZone'] }, + 'meeting': { 'title': repl['pageTitle'], 'owner': repl['owner'], 'logs': repl['fullLogs'], 'logsFullURL': repl['fullLogsFullURL'] }, + 'attendees': [ person for person in repl['PeoplePresent'] ], + 'agenda': [ { 'topic': item['topic'], 'notes': item['items'] } for item in repl['MeetingItems'] ], + 'actions': [ action for action in repl['ActionItems'] ], + 'actions_person': [ { 'nick': attendee['nick'], 'actions': attendee['items'] } for attendee in repl['ActionItemsPerson'] ], + 'meetbot': { 'version': repl['MeetBotVersion'], 'url': repl['MeetBotInfoURL'] }, + } + return repl + + +class Template(_BaseWriter): + """Format a notes file using the genshi templating engine + + Send an argument template=<filename> to specify which template to + use. If `template` begins in '+', then it is relative to the + MeetBot source directory. Included templates are: + +template.html + +template.txt + + Some examples of using these options are: + writer_map['.txt|template=+template.html'] = writers.Template + writer_map['.txt|template=/home/you/template.txt] = writers.Template + + If a template ends in .txt, parse with a text-based genshi + templater. Otherwise, parse with a HTML-based genshi templater. + """ + def format(self, extension=None, template='+template.html'): + repl = self.get_template2() + + # Do we want to use a text template or HTML ? + import genshi.template + if template[-4:] in ('.txt', '.rst'): + Template = genshi.template.NewTextTemplate # plain text + else: + Template = genshi.template.MarkupTemplate # HTML-like + + template = self.M.config.findFile(template) + + # Do the actual templating work + try: + f = open(template, 'r') + tmpl = Template(f.read()) + stream = tmpl.generate(**repl) + finally: + f.close() + + return stream.render() + + + +class _CSSmanager(object): + _css_head = textwrap.dedent('''\ + <style type="text/css"> + %s + </style> + ''') + def getCSS(self, name): + cssfile = getattr(self.M.config, 'cssFile_'+name, '') + if cssfile.lower() == 'none': + # special string 'None' means no style at all + return '' + elif cssfile in ('', 'default'): + # default CSS file + css_fname = '+css-'+name+'-default.css' + else: + css_fname = cssfile + css_fname = self.M.config.findFile(css_fname) + try: + # Stylesheet specified + if getattr(self.M.config, 'cssEmbed_'+name, True): + # external stylesheet + css = file(css_fname).read() + return self._css_head%css + else: + # linked stylesheet + css_head = ('''<link rel="stylesheet" type="text/css" ''' + '''href="%s">'''%cssfile) + return css_head + except Exception, exc: + if not self.M.config.safeMode: + raise + import traceback + traceback.print_exc() + print "(exception above ignored, continuing)" + try: + css_fname = os.path.join(os.path.dirname(__file__), + 'css-'+name+'-default.css') + css = open(css_fname).read() + return self._css_head%css + except: + if not self.M.config.safeMode: + raise + import traceback + traceback.print_exc() + return '' + + +class TextLog(_BaseWriter): + def format(self, extension=None): + M = self.M + """Write raw text logs.""" + return "\n".join(M.lines) + update_realtime = True + + + +class HTMLlog1(_BaseWriter): + def format(self, extension=None): + """Write pretty HTML logs.""" + M = self.M + # pygments lexing setup: + # (pygments HTML-formatter handles HTML-escaping) + import pygments + from pygments.lexers import IrcLogsLexer + from pygments.formatters import HtmlFormatter + import pygments.token as token + from pygments.lexer import bygroups + # Don't do any encoding in this function with pygments. + # That's only right before the i/o functions in the Config + # object. + formatter = HtmlFormatter(lineanchors='l', + full=True, style=M.config.pygmentizeStyle, + outencoding=self.M.config.output_codec) + Lexer = IrcLogsLexer + Lexer.tokens['msg'][1:1] = \ + [ # match: #topic commands + (r"(\#topic[ \t\f\v]*)(.*\n)", + bygroups(token.Keyword, token.Generic.Heading), '#pop'), + # match: #command (others) + (r"(\#[^\s]+[ \t\f\v]*)(.*\n)", + bygroups(token.Keyword, token.Generic.Strong), '#pop'), + ] + lexer = Lexer() + #from rkddp.interact import interact ; interact() + out = pygments.highlight("\n".join(M.lines), lexer, formatter) + # Hack it to add "pre { white-space: pre-wrap; }", which make + # it wrap the pygments html logs. I think that in a newer + # version of pygmetns, the "prestyles" HTMLFormatter option + # would do this, but I want to maintain compatibility with + # lenny. Thus, I do these substitution hacks to add the + # format in. Thanks to a comment on the blog of Francis + # Giannaros (http://francis.giannaros.org) for the suggestion + # and instructions for how. + out,n = re.subn(r"(\n\s*pre\s*\{[^}]+;\s*)(\})", + r"\1\n white-space: pre-wrap;\2", + out, count=1) + if n == 0: + out = re.sub(r"(\n\s*</style>)", + r"\npre { white-space: pre-wrap; }\1", + out, count=1) + return out + +class HTMLlog2(_BaseWriter, _CSSmanager): + def format(self, extension=None): + """Write pretty HTML logs.""" + M = self.M + lines = [ ] + line_re = re.compile(r"""\s* + (?P<time> \[?[0-9:\s]*\]?)\s* + (?P<nick>\s+<[@+\s]?[^>]+>)\s* + (?P<line>.*) + """, re.VERBOSE) + action_re = re.compile(r"""\s* + (?P<time> \[?[0-9:\s]*\]?)\s* + (?P<nick>\*\s+[@+\s]?[^\s]+)\s* + (?P<line>.*) + """,re.VERBOSE) + command_re = re.compile(r"(#[^\s]+[ \t\f\v]*)(.*)") + command_topic_re = re.compile(r"(#topic[ \t\f\v]*)(.*)") + hilight_re = re.compile(r"([^\s]+:)( .*)") + lineNumber = 0 + for l in M.lines: + lineNumber += 1 # starts from 1 + # is it a regular line? + m = line_re.match(l) + if m is not None: + line = m.group('line') + # Match #topic + m2 = command_topic_re.match(line) + if m2 is not None: + outline = ('<span class="topic">%s</span>' + '<span class="topicline">%s</span>'% + (html(m2.group(1)),html(m2.group(2)))) + # Match other #commands + if m2 is None: + m2 = command_re.match(line) + if m2 is not None: + outline = ('<span class="cmd">%s</span>' + '<span class="cmdline">%s</span>'% + (html(m2.group(1)),html(m2.group(2)))) + # match hilights + if m2 is None: + m2 = hilight_re.match(line) + if m2 is not None: + outline = ('<span class="hi">%s</span>' + '%s'% + (html(m2.group(1)),html(m2.group(2)))) + if m2 is None: + outline = html(line) + lines.append('<a name="l-%(lineno)s"></a>' + '<span class="tm">%(time)s</span>' + '<span class="nk">%(nick)s</span> ' + '%(line)s'%{'lineno':lineNumber, + 'time':html(m.group('time')), + 'nick':html(m.group('nick')), + 'line':outline,}) + continue + m = action_re.match(l) + # is it a action line? + if m is not None: + lines.append('<a name="l-%(lineno)s"></a>' + '<span class="tm">%(time)s</span>' + '<span class="nka">%(nick)s</span> ' + '<span class="ac">%(line)s</span>'% + {'lineno':lineNumber, + 'time':html(m.group('time')), + 'nick':html(m.group('nick')), + 'line':html(m.group('line')),}) + continue + print l + print m.groups() + print "**error**", l + + css = self.getCSS(name='log') + return html_template%{'pageTitle':"%s log"%html(M.channel), + #'body':"<br>\n".join(lines), + 'body':"<pre>"+("\n".join(lines))+"</pre>", + 'headExtra':css, + } +HTMLlog = HTMLlog2 + + + +html_template = textwrap.dedent('''\ + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> + <html> + <head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <title>%(pageTitle)s</title> + %(headExtra)s</head> + + <body> + %(body)s + </body></html> + ''') + + +class HTML1(_BaseWriter): + + body = textwrap.dedent('''\ + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> + <html> + <head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <title>%(pageTitle)s</title> + </head> + <body> + <h1>%(pageTitle)s</h1> + Meeting started by %(owner)s at %(starttime)s %(timeZone)s. + (<a href="%(fullLogs)s">full logs</a>)<br> + + + <table border=1> + %(MeetingItems)s + </table> + Meeting ended at %(endtime)s %(timeZone)s. + (<a href="%(fullLogs)s">full logs</a>) + + <br><br><br> + + <b>Action Items</b><ol> + %(ActionItems)s + </ol> + <br> + + <b>Action Items, by person</b> + <ol> + %(ActionItemsPerson)s + </ol><br> + + <b>People Present (lines said):</b><ol> + %(PeoplePresent)s + </ol> + + <br> + Generated by <a href="%(MeetBotInfoURL)s">MeetBot</a>%(MeetBotVersion)s. + </body></html> + ''') + + def format(self, extension=None): + """Write the minutes summary.""" + M = self.M + + # Add all minute items to the table + MeetingItems = [ ] + for m in M.minutes: + MeetingItems.append(m.html(M)) + MeetingItems = "\n".join(MeetingItems) + + # Action Items + ActionItems = [ ] + for m in M.minutes: + # The hack below is needed because of pickling problems + if m.itemtype != "ACTION": continue + ActionItems.append(" <li>%s</li>"%html(m.line)) + if len(ActionItems) == 0: + ActionItems.append(" <li>(none)</li>") + ActionItems = "\n".join(ActionItems) + + # Action Items, by person (This could be made lots more efficient) + ActionItemsPerson = [ ] + for nick, items in self.iterActionItemsNick(): + headerPrinted = False + for m in items: + if not headerPrinted: + ActionItemsPerson.append(" <li> %s <ol>"%html(nick)) + headerPrinted = True + ActionItemsPerson.append(" <li>%s</li>"%html(m.line)) + if headerPrinted: + ActionItemsPerson.append(" </ol></li>") + # unassigned items: + ActionItemsPerson.append(" <li><b>UNASSIGNED</b><ol>") + numberUnassigned = 0 + for m in self.iterActionItemsUnassigned(): + ActionItemsPerson.append(" <li>%s</li>"%html(m.line)) + numberUnassigned += 1 + if numberUnassigned == 0: + ActionItemsPerson.append(" <li>(none)</li>") + ActionItemsPerson.append(' </ol>\n</li>') + ActionItemsPerson = "\n".join(ActionItemsPerson) + + # People Attending + PeoplePresent = [ ] + # sort by number of lines spoken + for nick, count in self.iterNickCounts(): + PeoplePresent.append(' <li>%s (%s)</li>'%(html(nick), count)) + PeoplePresent = "\n".join(PeoplePresent) + + # Actual formatting and replacement + repl = self.replacements() + repl.update({'MeetingItems':MeetingItems, + 'ActionItems': ActionItems, + 'ActionItemsPerson': ActionItemsPerson, + 'PeoplePresent':PeoplePresent, + }) + body = self.body + body = body%repl + body = replaceWRAP(body) + return body + + + +class HTML2(_BaseWriter, _CSSmanager): + """HTML formatter without tables. + """ + def meetingItems(self): + """Return the main 'Meeting minutes' block.""" + M = self.M + + # Add all minute items to the table + MeetingItems = [ ] + MeetingItems.append(self.heading('Meeting summary')) + MeetingItems.append("<ol>") + + haveTopic = None + inSublist = False + for m in M.minutes: + item = '<li>'+m.html2(M) + if m.itemtype == "TOPIC": + if inSublist: + MeetingItems.append("</ol>") + inSublist = False + if haveTopic: + MeetingItems.append("<br></li>") + item = item + haveTopic = True + else: + if not inSublist: + if not haveTopic: + MeetingItems.append('<li>') + haveTopic = True + MeetingItems.append('<ol type="a">') + inSublist = True + if haveTopic: item = wrapList(item, 2)+"</li>" + else: item = wrapList(item, 0)+"</li>" + MeetingItems.append(item) + #MeetingItems.append("</li>") + + if inSublist: + MeetingItems.append("</ol>") + if haveTopic: + MeetingItems.append("</li>") + + MeetingItems.append("</ol>") + MeetingItems = "\n".join(MeetingItems) + return MeetingItems + + def actionItems(self): + """Return the 'Action items' block.""" + M = self.M + # Action Items + ActionItems = [ ] + ActionItems.append(self.heading('Action items')) + ActionItems.append('<ol>') + numActionItems = 0 + for m in M.minutes: + # The hack below is needed because of pickling problems + if m.itemtype != "ACTION": continue + ActionItems.append(" <li>%s</li>"%html(m.line)) + numActionItems += 1 + if numActionItems == 0: + ActionItems.append(" <li>(none)</li>") + ActionItems.append('</ol>') + ActionItems = "\n".join(ActionItems) + return ActionItems + def actionItemsPerson(self): + """Return the 'Action items, by person' block.""" + M = self.M + # Action Items, by person (This could be made lots more efficient) + ActionItemsPerson = [ ] + ActionItemsPerson.append(self.heading('Action items, by person')) + ActionItemsPerson.append('<ol>') + numberAssigned = 0 + for nick, items in self.iterActionItemsNick(): + headerPrinted = False + for m in items: + numberAssigned += 1 + if not headerPrinted: + ActionItemsPerson.append(" <li> %s <ol>"%html(nick)) + headerPrinted = True + ActionItemsPerson.append(" <li>%s</li>"%html(m.line)) + if headerPrinted: + ActionItemsPerson.append(" </ol></li>") + # unassigned items: + if len(ActionItemsPerson) == 0: + doActionItemsPerson = False + else: + doActionItemsPerson = True + Unassigned = [ ] + Unassigned.append(" <li><b>UNASSIGNED</b><ol>") + numberUnassigned = 0 + for m in self.iterActionItemsUnassigned(): + Unassigned.append(" <li>%s</li>"%html(m.line)) + numberUnassigned += 1 + if numberUnassigned == 0: + Unassigned.append(" <li>(none)</li>") + Unassigned.append(' </ol>\n</li>') + if numberUnassigned > 1: + ActionItemsPerson.extend(Unassigned) + ActionItemsPerson.append('</ol>') + ActionItemsPerson = "\n".join(ActionItemsPerson) + + # Only return anything if there are assigned items. + if numberAssigned == 0: + return None + else: + return ActionItemsPerson + def peoplePresent(self): + """Return the 'People present' block.""" + # People Attending + PeoplePresent = [] + PeoplePresent.append(self.heading('People present (lines said)')) + PeoplePresent.append('<ol>') + # sort by number of lines spoken + for nick, count in self.iterNickCounts(): + PeoplePresent.append(' <li>%s (%s)</li>'%(html(nick), count)) + PeoplePresent.append('</ol>') + PeoplePresent = "\n".join(PeoplePresent) + return PeoplePresent + def heading(self, name): + return '<h3>%s</h3>'%name + + def format(self, extension=None): + """Write the minutes summary.""" + M = self.M + + repl = self.replacements() + + body = [ ] + body.append(textwrap.dedent("""\ + <h1>%(pageTitle)s</h1> + <span class="details"> + Meeting started by %(owner)s at %(starttime)s %(timeZone)s + (<a href="%(fullLogs)s">full logs</a>).</span> + """%repl)) + body.append(self.meetingItems()) + body.append(textwrap.dedent("""\ + <span class="details"> + Meeting ended at %(endtime)s %(timeZone)s + (<a href="%(fullLogs)s">full logs</a>).</span> + """%repl)) + body.append(self.actionItems()) + body.append(self.actionItemsPerson()) + body.append(self.peoplePresent()) + body.append("""<span class="details">""" + """Generated by <a href="%(MeetBotInfoURL)s">MeetBot</a>""" + """%(MeetBotVersion)s.</span>"""%repl) + body = [ b for b in body if b is not None ] + body = "\n<br><br>\n\n\n\n".join(body) + body = replaceWRAP(body) + + + css = self.getCSS(name='minutes') + repl.update({'body': body, + 'headExtra': css, + }) + html = html_template % repl + + return html +HTML = HTML2 + + +class ReST(_BaseWriter): + + body = textwrap.dedent("""\ + %(titleBlock)s + %(pageTitle)s + %(titleBlock)s + + + sWRAPsMeeting started by %(owner)s at %(starttime)s %(timeZone)s. + The `full logs`_ are available.eWRAPe + + .. _`full logs`: %(fullLogs)s + + + + Meeting summary + --------------- + %(MeetingItems)s + + Meeting ended at %(endtime)s %(timeZone)s. + + + + + Action Items + ------------ + %(ActionItems)s + + + + + Action Items, by person + ----------------------- + %(ActionItemsPerson)s + + + + + People Present (lines said) + --------------------------- + %(PeoplePresent)s + + + + + Generated by `MeetBot`_%(MeetBotVersion)s + + .. _`MeetBot`: %(MeetBotInfoURL)s + """) + + def format(self, extension=None): + """Return a ReStructured Text minutes summary.""" + M = self.M + + # Agenda items + MeetingItems = [ ] + M.rst_urls = [ ] + M.rst_refs = { } + haveTopic = None + for m in M.minutes: + item = "* "+m.rst(M) + if m.itemtype == "TOPIC": + if haveTopic: + MeetingItems.append("") + item = wrapList(item, 0) + haveTopic = True + else: + if haveTopic: item = wrapList(item, 2) + else: item = wrapList(item, 0) + MeetingItems.append(item) + MeetingItems = '\n\n'.join(MeetingItems) + MeetingURLs = "\n".join(M.rst_urls) + del M.rst_urls, M.rst_refs + MeetingItems = MeetingItems + '\n\n'+MeetingURLs + + # Action Items + ActionItems = [ ] + for m in M.minutes: + # The hack below is needed because of pickling problems + if m.itemtype != "ACTION": continue + #already escaped + ActionItems.append(wrapList("* %s"%rst(m.line), indent=0)) + ActionItems = "\n\n".join(ActionItems) + + # Action Items, by person (This could be made lots more efficient) + ActionItemsPerson = [ ] + for nick in sorted(M.attendees.keys(), key=lambda x: x.lower()): + nick_re = makeNickRE(nick) + headerPrinted = False + for m in M.minutes: + # The hack below is needed because of pickling problems + if m.itemtype != "ACTION": continue + if nick_re.search(m.line) is None: continue + if not headerPrinted: + ActionItemsPerson.append("* %s"%rst(nick)) + headerPrinted = True + ActionItemsPerson.append(wrapList("* %s"%rst(m.line), 2)) + m.assigned = True + # unassigned items: + Unassigned = [ ] + Unassigned.append("* **UNASSIGNED**") + numberUnassigned = 0 + for m in M.minutes: + if m.itemtype != "ACTION": continue + if getattr(m, 'assigned', False): continue + Unassigned.append(wrapList("* %s"%rst(m.line), 2)) + numberUnassigned += 1 + if numberUnassigned == 0: + Unassigned.append(" * (none)") + if numberUnassigned > 1: + ActionItemsPerson.extend(Unassigned) + ActionItemsPerson = "\n\n".join(ActionItemsPerson) + + # People Attending + PeoplePresent = [ ] + # sort by number of lines spoken + for nick, count in self.iterNickCounts(): + PeoplePresent.append('* %s (%s)'%(rst(nick), count)) + PeoplePresent = "\n\n".join(PeoplePresent) + + # Actual formatting and replacement + repl = self.replacements() + repl.update({'titleBlock':('='*len(repl['pageTitle'])), + 'MeetingItems':MeetingItems, + 'ActionItems': ActionItems, + 'ActionItemsPerson': ActionItemsPerson, + 'PeoplePresent':PeoplePresent, + }) + body = self.body + body = body%repl + body = replaceWRAP(body) + return body + +class HTMLfromReST(_BaseWriter): + + def format(self, extension=None): + M = self.M + import docutils.core + rst = ReST(M).format(extension) + rstToHTML = docutils.core.publish_string(rst, writer_name='html', + settings_overrides={'file_insertion_enabled': 0, + 'raw_enabled': 0, + 'output_encoding':self.M.config.output_codec}) + return rstToHTML + + + +class Text(_BaseWriter): + + def meetingItems(self): + M = self.M + + # Agenda items + MeetingItems = [ ] + MeetingItems.append(self.heading('Meeting summary')) + haveTopic = None + for m in M.minutes: + item = "* "+m.text(M) + if m.itemtype == "TOPIC": + if haveTopic: + MeetingItems.append("") + item = wrapList(item, 0) + haveTopic = True + else: + if haveTopic: item = wrapList(item, 2) + else: item = wrapList(item, 0) + MeetingItems.append(item) + MeetingItems = '\n'.join(MeetingItems) + return MeetingItems + + def actionItems(self): + M = self.M + # Action Items + ActionItems = [ ] + numActionItems = 0 + ActionItems.append(self.heading('Action items')) + for m in M.minutes: + # The hack below is needed because of pickling problems + if m.itemtype != "ACTION": continue + #already escaped + ActionItems.append(wrapList("* %s"%text(m.line), indent=0)) + numActionItems += 1 + if numActionItems == 0: + ActionItems.append("* (none)") + ActionItems = "\n".join(ActionItems) + + def actionItemsPerson(self): + M = self.M + # Action Items, by person (This could be made lots more efficient) + ActionItemsPerson = [ ] + ActionItemsPerson.append(self.heading('Action items, by person')) + numberAssigned = 0 + for nick in sorted(M.attendees.keys(), key=lambda x: x.lower()): + nick_re = makeNickRE(nick) + headerPrinted = False + for m in M.minutes: + # The hack below is needed because of pickling problems + if m.itemtype != "ACTION": continue + if nick_re.search(m.line) is None: continue + if not headerPrinted: + ActionItemsPerson.append("* %s"%text(nick)) + headerPrinted = True + ActionItemsPerson.append(wrapList("* %s"%text(m.line), 2)) + numberAssigned += 1 + m.assigned = True + # unassigned items: + Unassigned = [ ] + Unassigned.append("* **UNASSIGNED**") + numberUnassigned = 0 + for m in M.minutes: + if m.itemtype != "ACTION": continue + if getattr(m, 'assigned', False): continue + Unassigned.append(wrapList("* %s"%text(m.line), 2)) + numberUnassigned += 1 + if numberUnassigned == 0: + Unassigned.append(" * (none)") + if numberUnassigned > 1: + ActionItemsPerson.extend(Unassigned) + ActionItemsPerson = "\n".join(ActionItemsPerson) + + if numberAssigned == 0: + return None + else: + return ActionItemsPerson + + def peoplePresent(self): + M = self.M + # People Attending + PeoplePresent = [ ] + PeoplePresent.append(self.heading('People present (lines said)')) + # sort by number of lines spoken + for nick, count in self.iterNickCounts(): + PeoplePresent.append('* %s (%s)'%(text(nick), count)) + PeoplePresent = "\n".join(PeoplePresent) + return PeoplePresent + + def heading(self, name): + return '%s\n%s\n'%(name, '-'*len(name)) + + + def format(self, extension=None): + """Return a plain text minutes summary.""" + M = self.M + + # Actual formatting and replacement + repl = self.replacements() + repl.update({'titleBlock':('='*len(repl['pageTitle'])), + }) + + + body = [ ] + body.append(textwrap.dedent("""\ + %(titleBlock)s + %(pageTitle)s + %(titleBlock)s + + + sWRAPsMeeting started by %(owner)s at %(starttime)s + %(timeZone)s. The full logs are available at + %(fullLogsFullURL)s .eWRAPe"""%repl)) + body.append(self.meetingItems()) + body.append(textwrap.dedent("""\ + Meeting ended at %(endtime)s %(timeZone)s."""%repl)) + body.append(self.actionItems()) + body.append(self.actionItemsPerson()) + body.append(self.peoplePresent()) + body.append(textwrap.dedent("""\ + Generated by `MeetBot`_%(MeetBotVersion)s"""%repl)) + body = [ b for b in body if b is not None ] + body = "\n\n\n\n".join(body) + body = replaceWRAP(body) + + return body + + +class MediaWiki(_BaseWriter): + """Outputs MediaWiki formats. + """ + def meetingItems(self): + M = self.M + + # Agenda items + MeetingItems = [ ] + MeetingItems.append(self.heading('Meeting summary')) + haveTopic = None + for m in M.minutes: + item = "* "+m.mw(M) + if m.itemtype == "TOPIC": + if haveTopic: + MeetingItems.append("") # line break + haveTopic = True + else: + if haveTopic: item = "*"+item + MeetingItems.append(item) + MeetingItems = '\n'.join(MeetingItems) + return MeetingItems + + def actionItems(self): + M = self.M + # Action Items + ActionItems = [ ] + numActionItems = 0 + ActionItems.append(self.heading('Action items')) + for m in M.minutes: + # The hack below is needed because of pickling problems + if m.itemtype != "ACTION": continue + #already escaped + ActionItems.append("* %s"%mw(m.line)) + numActionItems += 1 + if numActionItems == 0: + ActionItems.append("* (none)") + ActionItems = "\n".join(ActionItems) + return ActionItems + + def actionItemsPerson(self): + M = self.M + # Action Items, by person (This could be made lots more efficient) + ActionItemsPerson = [ ] + ActionItemsPerson.append(self.heading('Action items, by person')) + numberAssigned = 0 + for nick in sorted(M.attendees.keys(), key=lambda x: x.lower()): + nick_re = makeNickRE(nick) + headerPrinted = False + for m in M.minutes: + # The hack below is needed because of pickling problems + if m.itemtype != "ACTION": continue + if nick_re.search(m.line) is None: continue + if not headerPrinted: + ActionItemsPerson.append("* %s"%mw(nick)) + headerPrinted = True + ActionItemsPerson.append("** %s"%mw(m.line)) + numberAssigned += 1 + m.assigned = True + # unassigned items: + Unassigned = [ ] + Unassigned.append("* **UNASSIGNED**") + numberUnassigned = 0 + for m in M.minutes: + if m.itemtype != "ACTION": continue + if getattr(m, 'assigned', False): continue + Unassigned.append("** %s"%mw(m.line)) + numberUnassigned += 1 + if numberUnassigned == 0: + Unassigned.append(" * (none)") + if numberUnassigned > 1: + ActionItemsPerson.extend(Unassigned) + ActionItemsPerson = "\n".join(ActionItemsPerson) + + if numberAssigned == 0: + return None + else: + return ActionItemsPerson + + def peoplePresent(self): + M = self.M + # People Attending + PeoplePresent = [ ] + PeoplePresent.append(self.heading('People present (lines said)')) + # sort by number of lines spoken + for nick, count in self.iterNickCounts(): + PeoplePresent.append('* %s (%s)'%(mw(nick), count)) + PeoplePresent = "\n".join(PeoplePresent) + return PeoplePresent + + def heading(self, name, level=1): + return '%s %s %s\n'%('='*(level+1), name, '='*(level+1)) + + + body_start = textwrap.dedent("""\ + %(pageTitleHeading)s + + sWRAPsMeeting started by %(owner)s at %(starttime)s + %(timeZone)s. The full logs are available at + %(fullLogsFullURL)s .eWRAPe""") + def format(self, extension=None, **kwargs): + """Return a MediaWiki formatted minutes summary.""" + M = self.M + + # Actual formatting and replacement + repl = self.replacements() + repl.update({'titleBlock':('='*len(repl['pageTitle'])), + 'pageTitleHeading':self.heading(repl['pageTitle'],level=0) + }) + + + body = [ ] + body.append(self.body_start%repl) + body.append(self.meetingItems()) + body.append(textwrap.dedent("""\ + Meeting ended at %(endtime)s %(timeZone)s."""%repl)) + body.append(self.actionItems()) + body.append(self.actionItemsPerson()) + body.append(self.peoplePresent()) + body.append(textwrap.dedent("""\ + Generated by MeetBot%(MeetBotVersion)s (%(MeetBotInfoURL)s)"""%repl)) + body = [ b for b in body if b is not None ] + body = "\n\n\n\n".join(body) + body = replaceWRAP(body) + + + # Do we want to upload? + if 'mwpath' in kwargs: + import mwclient + mwsite = kwargs['mwsite'] + mwpath = kwargs['mwpath'] + mwusername = kwargs.get('mwusername', None) + mwpassword = kwargs.get('mwpassword', '') + subpagename = os.path.basename(self.M.config.filename()) + mwfullname = "%s/%s" % (mwpath, subpagename) + force_login = (mwusername != None) + + site = mwclient.Site(mwsite, force_login=force_login) + if(force_login): + site.login(mwusername, mwpassword) + page = site.Pages[mwfullname] + some = page.edit() + page.save(body, summary="Meeting") + + + return body + +class PmWiki(MediaWiki, object): + def heading(self, name, level=1): + return '%s %s\n'%('!'*(level+1), name) + def replacements(self): + #repl = super(PmWiki, self).replacements(self) # fails, type checking + repl = MediaWiki.replacements.im_func(self) + repl['pageTitleHeading'] = self.heading(repl['pageTitle'],level=0) + return repl + + |