# 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 import agenda 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. # Meeting management urls voters_url = 'http://localhost:3000/users/voters' agenda_url = 'http://localhost:3000/agendas/current_items' result_url = 'http://localhost:3000/agendas/current_items' # Credentials for posting voting results voting_results_user = 'user' voting_results_password = 'password' 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 self.agenda = agenda.Agenda(self) 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= # linenum= # time_=