aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoachim Filip Ignacy Bartosik <jbartosik@gmail.com>2011-05-18 16:44:11 +0200
committerJoachim Filip Ignacy Bartosik <jbartosik@gmail.com>2011-05-24 19:03:00 +0200
commite8c39d513356e14b60813c54824a63a6ad516348 (patch)
treedba08b74ad8470b3d36d813d14e310e512acea57 /bot/ircmeeting
parentBasic meeting participation tracing (diff)
downloadcouncil-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__.py0
-rw-r--r--bot/ircmeeting/css-log-default.css15
-rw-r--r--bot/ircmeeting/css-minutes-default.css34
-rw-r--r--bot/ircmeeting/items.py292
-rw-r--r--bot/ircmeeting/meeting.py672
-rw-r--r--bot/ircmeeting/template.html102
-rw-r--r--bot/ircmeeting/template.txt55
-rw-r--r--bot/ircmeeting/writers.py1197
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+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
+
+