433 lines
16 KiB
Python
433 lines
16 KiB
Python
|
# coding=utf-8
|
|||
|
"""
|
|||
|
meetbot.py - Sopel meeting logger module
|
|||
|
Copyright © 2012, Elad Alfassa, <elad@fedoraproject.org>
|
|||
|
Licensed under the Eiffel Forum License 2.
|
|||
|
|
|||
|
This module is an attempt to implement at least some of the functionallity of Debian's meetbot
|
|||
|
"""
|
|||
|
import time
|
|||
|
import os
|
|||
|
from config.types import StaticSection, FilenameAttribute, ValidatedAttribute
|
|||
|
from module import example, commands, rule, priority
|
|||
|
from tools import Ddict, Identifier
|
|||
|
import codecs
|
|||
|
|
|||
|
|
|||
|
class MeetbotSection(StaticSection):
|
|||
|
meeting_log_path = FilenameAttribute('meeting_log_path', directory=True,
|
|||
|
default='~/www/meetings')
|
|||
|
"""Path to meeting logs storage directory
|
|||
|
|
|||
|
This should be an absolute path, accessible on a webserver."""
|
|||
|
meeting_log_baseurl = ValidatedAttribute(
|
|||
|
'meeting_log_baseurl',
|
|||
|
default='http://localhost/~sopel/meetings'
|
|||
|
)
|
|||
|
"""Base URL for the meeting logs directory"""
|
|||
|
|
|||
|
|
|||
|
def configure(config):
|
|||
|
config.define_section('meetbot', MeetbotSection)
|
|||
|
config.meetbot.configure_setting(
|
|||
|
'meeting_log_path',
|
|||
|
'Enter the directory to store logs in.'
|
|||
|
)
|
|||
|
config.meetbot.configure_setting(
|
|||
|
'meeting_log_baseurl',
|
|||
|
'Enter the base URL for the meeting logs.',
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
def setup(bot):
|
|||
|
bot.config.define_section('meetbot', MeetbotSection)
|
|||
|
|
|||
|
|
|||
|
meetings_dict = Ddict(dict) # Saves metadata about currently running meetings
|
|||
|
"""
|
|||
|
meetings_dict is a 2D dict.
|
|||
|
|
|||
|
Each meeting should have:
|
|||
|
channel
|
|||
|
time of start
|
|||
|
head (can stop the meeting, plus all abilities of chairs)
|
|||
|
chairs (can add infolines to the logs)
|
|||
|
title
|
|||
|
current subject
|
|||
|
comments (what people who aren't voiced want to add)
|
|||
|
|
|||
|
Using channel as the meeting ID as there can't be more than one meeting in a channel at the same time.
|
|||
|
"""
|
|||
|
meeting_log_path = '' # To be defined on meeting start as part of sanity checks, used by logging functions so we don't have to pass them bot
|
|||
|
meeting_log_baseurl = '' # To be defined on meeting start as part of sanity checks, used by logging functions so we don't have to pass them bot
|
|||
|
meeting_actions = {} # A dict of channels to the actions that have been created in them. This way we can have .listactions spit them back out later on.
|
|||
|
|
|||
|
|
|||
|
#Get the logfile name for the meeting in the requested channel
|
|||
|
#Used by all logging functions
|
|||
|
def figure_logfile_name(channel):
|
|||
|
if meetings_dict[channel]['title'] is 'Untitled meeting':
|
|||
|
name = 'untitled'
|
|||
|
else:
|
|||
|
name = meetings_dict[channel]['title']
|
|||
|
# Real simple sluggifying. This bunch of characters isn't exhaustive, but
|
|||
|
# whatever. It's close enough for most situations, I think.
|
|||
|
for c in ' ./\\:*?"<>|&*`':
|
|||
|
name = name.replace(c, '-')
|
|||
|
timestring = time.strftime('%Y-%m-%d-%H:%M', time.gmtime(meetings_dict[channel]['start']))
|
|||
|
filename = timestring + '_' + name
|
|||
|
return filename
|
|||
|
|
|||
|
|
|||
|
#Start HTML log
|
|||
|
def logHTML_start(channel):
|
|||
|
logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.html', 'a', encoding='utf-8')
|
|||
|
timestring = time.strftime('%Y-%m-%d %H:%M', time.gmtime(meetings_dict[channel]['start']))
|
|||
|
title = '%s at %s, %s' % (meetings_dict[channel]['title'], channel, timestring)
|
|||
|
logfile.write('<!doctype html>\n<html>\n<head>\n<meta charset="utf-8">\n<title>%TITLE%</title>\n</head>\n<body>\n<h1>%TITLE%</h1>\n'.replace('%TITLE%', title))
|
|||
|
logfile.write('<h4>Meeting started by %s</h4><ul>\n' % meetings_dict[channel]['head'])
|
|||
|
logfile.close()
|
|||
|
|
|||
|
|
|||
|
#Write a list item in the HTML log
|
|||
|
def logHTML_listitem(item, channel):
|
|||
|
logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.html', 'a', encoding='utf-8')
|
|||
|
logfile.write('<li>' + item + '</li>\n')
|
|||
|
logfile.close()
|
|||
|
|
|||
|
|
|||
|
#End the HTML log
|
|||
|
def logHTML_end(channel):
|
|||
|
logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.html', 'a', encoding='utf-8')
|
|||
|
current_time = time.strftime('%H:%M:%S', time.gmtime())
|
|||
|
logfile.write('</ul>\n<h4>Meeting ended at %s UTC</h4>\n' % current_time)
|
|||
|
plainlog_url = meeting_log_baseurl + channel + '/' + figure_logfile_name(channel) + '.log'
|
|||
|
logfile.write('<a href="%s">Full log</a>' % plainlog_url)
|
|||
|
logfile.write('\n</body>\n</html>')
|
|||
|
logfile.close()
|
|||
|
|
|||
|
|
|||
|
#Write a string to the plain text log
|
|||
|
def logplain(item, channel):
|
|||
|
current_time = time.strftime('%H:%M:%S', time.gmtime())
|
|||
|
logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.log', 'a', encoding='utf-8')
|
|||
|
logfile.write('[' + current_time + '] ' + item + '\r\n')
|
|||
|
logfile.close()
|
|||
|
|
|||
|
|
|||
|
#Check if a meeting is currently running
|
|||
|
def ismeetingrunning(channel):
|
|||
|
try:
|
|||
|
if meetings_dict[channel]['running']:
|
|||
|
return True
|
|||
|
else:
|
|||
|
return False
|
|||
|
except:
|
|||
|
return False
|
|||
|
|
|||
|
|
|||
|
#Check if nick is a chair or head of the meeting
|
|||
|
def ischair(nick, channel):
|
|||
|
try:
|
|||
|
if nick.lower() == meetings_dict[channel]['head'] or nick.lower() in meetings_dict[channel]['chairs']:
|
|||
|
return True
|
|||
|
else:
|
|||
|
return False
|
|||
|
except:
|
|||
|
return False
|
|||
|
|
|||
|
|
|||
|
#Start meeting (also preforms all required sanity checks)
|
|||
|
@commands('startmeeting')
|
|||
|
@example('.startmeeting title or .startmeeting')
|
|||
|
def startmeeting(bot, trigger):
|
|||
|
"""
|
|||
|
Start a meeting.
|
|||
|
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
|||
|
"""
|
|||
|
if ismeetingrunning(trigger.sender):
|
|||
|
bot.say('Can\'t do that, there is already a meeting in progress here!')
|
|||
|
return
|
|||
|
if trigger.is_privmsg:
|
|||
|
bot.say('Can only start meetings in channels')
|
|||
|
return
|
|||
|
#Start the meeting
|
|||
|
meetings_dict[trigger.sender]['start'] = time.time()
|
|||
|
if not trigger.group(2):
|
|||
|
meetings_dict[trigger.sender]['title'] = 'Untitled meeting'
|
|||
|
else:
|
|||
|
meetings_dict[trigger.sender]['title'] = trigger.group(2)
|
|||
|
meetings_dict[trigger.sender]['head'] = trigger.nick.lower()
|
|||
|
meetings_dict[trigger.sender]['running'] = True
|
|||
|
meetings_dict[trigger.sender]['comments'] = []
|
|||
|
|
|||
|
global meeting_log_path
|
|||
|
meeting_log_path = bot.config.meetbot.meeting_log_path
|
|||
|
if not meeting_log_path.endswith('/'):
|
|||
|
meeting_log_path = meeting_log_path + '/'
|
|||
|
global meeting_log_baseurl
|
|||
|
meeting_log_baseurl = bot.config.meetbot.meeting_log_baseurl
|
|||
|
if not meeting_log_baseurl.endswith('/'):
|
|||
|
meeting_log_baseurl = meeting_log_baseurl + '/'
|
|||
|
if not os.path.isdir(meeting_log_path + trigger.sender):
|
|||
|
try:
|
|||
|
os.makedirs(meeting_log_path + trigger.sender)
|
|||
|
except Exception:
|
|||
|
bot.say("Can't create log directory for this channel, meeting not started!")
|
|||
|
meetings_dict[trigger.sender] = Ddict(dict)
|
|||
|
raise
|
|||
|
return
|
|||
|
#Okay, meeting started!
|
|||
|
logplain('Meeting started by ' + trigger.nick.lower(), trigger.sender)
|
|||
|
logHTML_start(trigger.sender)
|
|||
|
meeting_actions[trigger.sender] = []
|
|||
|
bot.say('Meeting started! use .action, .agreed, .info, .chairs, .subject and .comments to control the meeting. to end the meeting, type .endmeeting')
|
|||
|
bot.say('Users without speaking permission can use .comment ' +
|
|||
|
trigger.sender + ' followed by their comment in a PM with me to '
|
|||
|
'vocalize themselves.')
|
|||
|
|
|||
|
|
|||
|
#Change the current subject (will appear as <h3> in the HTML log)
|
|||
|
@commands('subject')
|
|||
|
@example('.subject roll call')
|
|||
|
def meetingsubject(bot, trigger):
|
|||
|
"""
|
|||
|
Change the meeting subject.
|
|||
|
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
|||
|
"""
|
|||
|
if not ismeetingrunning(trigger.sender):
|
|||
|
bot.say('Can\'t do that, start meeting first')
|
|||
|
return
|
|||
|
if not trigger.group(2):
|
|||
|
bot.say('what is the subject?')
|
|||
|
return
|
|||
|
if not ischair(trigger.nick, trigger.sender):
|
|||
|
bot.say('Only meeting head or chairs can do that')
|
|||
|
return
|
|||
|
meetings_dict[trigger.sender]['current_subject'] = trigger.group(2)
|
|||
|
logfile = codecs.open(meeting_log_path + trigger.sender + '/' + figure_logfile_name(trigger.sender) + '.html', 'a', encoding='utf-8')
|
|||
|
logfile.write('</ul><h3>' + trigger.group(2) + '</h3><ul>')
|
|||
|
logfile.close()
|
|||
|
logplain('Current subject: ' + trigger.group(2) + ', (set by ' + trigger.nick + ')', trigger.sender)
|
|||
|
bot.say('Current subject: ' + trigger.group(2))
|
|||
|
|
|||
|
|
|||
|
#End the meeting
|
|||
|
@commands('endmeeting')
|
|||
|
@example('.endmeeting')
|
|||
|
def endmeeting(bot, trigger):
|
|||
|
"""
|
|||
|
End a meeting.
|
|||
|
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
|||
|
"""
|
|||
|
if not ismeetingrunning(trigger.sender):
|
|||
|
bot.say('Can\'t do that, start meeting first')
|
|||
|
return
|
|||
|
if not ischair(trigger.nick, trigger.sender):
|
|||
|
bot.say('Only meeting head or chairs can do that')
|
|||
|
return
|
|||
|
meeting_length = time.time() - meetings_dict[trigger.sender]['start']
|
|||
|
#TODO: Humanize time output
|
|||
|
bot.say("Meeting ended! total meeting length %d seconds" % meeting_length)
|
|||
|
logHTML_end(trigger.sender)
|
|||
|
htmllog_url = meeting_log_baseurl + trigger.sender + '/' + figure_logfile_name(trigger.sender) + '.html'
|
|||
|
logplain('Meeting ended by %s, total meeting length %d seconds' % (trigger.nick, meeting_length), trigger.sender)
|
|||
|
bot.say('Meeting minutes: ' + htmllog_url)
|
|||
|
meetings_dict[trigger.sender] = Ddict(dict)
|
|||
|
del meeting_actions[trigger.sender]
|
|||
|
|
|||
|
|
|||
|
#Set meeting chairs (people who can control the meeting)
|
|||
|
@commands('chairs')
|
|||
|
@example('.chairs Tyrope Jason elad')
|
|||
|
def chairs(bot, trigger):
|
|||
|
"""
|
|||
|
Set the meeting chairs.
|
|||
|
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
|||
|
"""
|
|||
|
if not ismeetingrunning(trigger.sender):
|
|||
|
bot.say('Can\'t do that, start meeting first')
|
|||
|
return
|
|||
|
if not trigger.group(2):
|
|||
|
bot.say('Who are the chairs?')
|
|||
|
return
|
|||
|
if trigger.nick.lower() == meetings_dict[trigger.sender]['head']:
|
|||
|
meetings_dict[trigger.sender]['chairs'] = trigger.group(2).lower().split(' ')
|
|||
|
chairs_readable = trigger.group(2).lower().replace(' ', ', ')
|
|||
|
logplain('Meeting chairs are: ' + chairs_readable, trigger.sender)
|
|||
|
logHTML_listitem('<span style="font-weight: bold">Meeting chairs are: </span>' + chairs_readable, trigger.sender)
|
|||
|
bot.say('Meeting chairs are: ' + chairs_readable)
|
|||
|
else:
|
|||
|
bot.say("Only meeting head can set chairs")
|
|||
|
|
|||
|
|
|||
|
#Log action item in the HTML log
|
|||
|
@commands('action')
|
|||
|
@example('.action elad will develop a meetbot')
|
|||
|
def meetingaction(bot, trigger):
|
|||
|
"""
|
|||
|
Log an action in the meeting log
|
|||
|
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
|||
|
"""
|
|||
|
if not ismeetingrunning(trigger.sender):
|
|||
|
bot.say('Can\'t do that, start meeting first')
|
|||
|
return
|
|||
|
if not trigger.group(2):
|
|||
|
bot.say('try .action someone will do something')
|
|||
|
return
|
|||
|
if not ischair(trigger.nick, trigger.sender):
|
|||
|
bot.say('Only meeting head or chairs can do that')
|
|||
|
return
|
|||
|
logplain('ACTION: ' + trigger.group(2), trigger.sender)
|
|||
|
logHTML_listitem('<span style="font-weight: bold">Action: </span>' + trigger.group(2), trigger.sender)
|
|||
|
meeting_actions[trigger.sender].append(trigger.group(2))
|
|||
|
bot.say('ACTION: ' + trigger.group(2))
|
|||
|
|
|||
|
|
|||
|
@commands('listactions')
|
|||
|
@example('.listactions')
|
|||
|
def listactions(bot, trigger):
|
|||
|
if not ismeetingrunning(trigger.sender):
|
|||
|
bot.say('Can\'t do that, start meeting first')
|
|||
|
return
|
|||
|
for action in meeting_actions[trigger.sender]:
|
|||
|
bot.say('ACTION: ' + action)
|
|||
|
|
|||
|
|
|||
|
#Log agreed item in the HTML log
|
|||
|
@commands('agreed')
|
|||
|
@example('.agreed Bowties are cool')
|
|||
|
def meetingagreed(bot, trigger):
|
|||
|
"""
|
|||
|
Log an agreement in the meeting log.
|
|||
|
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
|||
|
"""
|
|||
|
if not ismeetingrunning(trigger.sender):
|
|||
|
bot.say('Can\'t do that, start meeting first')
|
|||
|
return
|
|||
|
if not trigger.group(2):
|
|||
|
bot.say('try .action someone will do something')
|
|||
|
return
|
|||
|
if not ischair(trigger.nick, trigger.sender):
|
|||
|
bot.say('Only meeting head or chairs can do that')
|
|||
|
return
|
|||
|
logplain('AGREED: ' + trigger.group(2), trigger.sender)
|
|||
|
logHTML_listitem('<span style="font-weight: bold">Agreed: </span>' + trigger.group(2), trigger.sender)
|
|||
|
bot.say('AGREED: ' + trigger.group(2))
|
|||
|
|
|||
|
|
|||
|
#Log link item in the HTML log
|
|||
|
@commands('link')
|
|||
|
@example('.link http://example.com')
|
|||
|
def meetinglink(bot, trigger):
|
|||
|
"""
|
|||
|
Log a link in the meeing log.
|
|||
|
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
|||
|
"""
|
|||
|
if not ismeetingrunning(trigger.sender):
|
|||
|
bot.say('Can\'t do that, start meeting first')
|
|||
|
return
|
|||
|
if not trigger.group(2):
|
|||
|
bot.say('try .action someone will do something')
|
|||
|
return
|
|||
|
if not ischair(trigger.nick, trigger.sender):
|
|||
|
bot.say('Only meeting head or chairs can do that')
|
|||
|
return
|
|||
|
link = trigger.group(2)
|
|||
|
if not link.startswith("http"):
|
|||
|
link = "http://" + link
|
|||
|
try:
|
|||
|
#title = find_title(link, verify=bot.config.core.verify_ssl)
|
|||
|
pass
|
|||
|
except:
|
|||
|
title = ''
|
|||
|
logplain('LINK: %s [%s]' % (link, title), trigger.sender)
|
|||
|
logHTML_listitem('<a href="%s">%s</a>' % (link, title), trigger.sender)
|
|||
|
bot.say('LINK: ' + link)
|
|||
|
|
|||
|
|
|||
|
#Log informational item in the HTML log
|
|||
|
@commands('info')
|
|||
|
@example('.info all board members present')
|
|||
|
def meetinginfo(bot, trigger):
|
|||
|
"""
|
|||
|
Log an informational item in the meeting log
|
|||
|
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
|||
|
"""
|
|||
|
if not ismeetingrunning(trigger.sender):
|
|||
|
bot.say('Can\'t do that, start meeting first')
|
|||
|
return
|
|||
|
if not trigger.group(2):
|
|||
|
bot.say('try .info some informative thing')
|
|||
|
return
|
|||
|
if not ischair(trigger.nick, trigger.sender):
|
|||
|
bot.say('Only meeting head or chairs can do that')
|
|||
|
return
|
|||
|
logplain('INFO: ' + trigger.group(2), trigger.sender)
|
|||
|
logHTML_listitem(trigger.group(2), trigger.sender)
|
|||
|
bot.say('INFO: ' + trigger.group(2))
|
|||
|
|
|||
|
|
|||
|
#called for every single message
|
|||
|
#Will log to plain text only
|
|||
|
@rule('(.*)')
|
|||
|
@priority('low')
|
|||
|
def log_meeting(bot, trigger):
|
|||
|
if not ismeetingrunning(trigger.sender):
|
|||
|
return
|
|||
|
if trigger.startswith('.endmeeting') or trigger.startswith('.chairs') or trigger.startswith('.action') or trigger.startswith('.info') or trigger.startswith('.startmeeting') or trigger.startswith('.agreed') or trigger.startswith('.link') or trigger.startswith('.subject'):
|
|||
|
return
|
|||
|
logplain('<' + trigger.nick + '> ' + trigger, trigger.sender)
|
|||
|
|
|||
|
|
|||
|
@commands('comment')
|
|||
|
def take_comment(bot, trigger):
|
|||
|
"""
|
|||
|
Log a comment, to be shown with other comments when a chair uses .comments.
|
|||
|
Intended to allow commentary from those outside the primary group of people
|
|||
|
in the meeting.
|
|||
|
|
|||
|
Used in private message only, as `.comment <#channel> <comment to add>`
|
|||
|
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
|||
|
"""
|
|||
|
if not trigger.sender.is_nick():
|
|||
|
return
|
|||
|
if not trigger.group(4): # <2 arguements were given
|
|||
|
bot.say('Usage: .comment <#channel> <comment to add>')
|
|||
|
return
|
|||
|
|
|||
|
target, message = trigger.group(2).split(None, 1)
|
|||
|
target = Identifier(target)
|
|||
|
if not ismeetingrunning(target):
|
|||
|
bot.say("There's not currently a meeting in that channel.")
|
|||
|
else:
|
|||
|
meetings_dict[trigger.group(3)]['comments'].append((trigger.nick, message))
|
|||
|
bot.say("Your comment has been recorded. It will be shown when the"
|
|||
|
" chairs tell me to show the comments.")
|
|||
|
bot.msg(meetings_dict[trigger.group(3)]['head'], "A new comment has been recorded.")
|
|||
|
|
|||
|
|
|||
|
@commands('comments')
|
|||
|
def show_comments(bot, trigger):
|
|||
|
"""
|
|||
|
Show the comments that have been logged for this meeting with .comment.
|
|||
|
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
|||
|
"""
|
|||
|
if not ismeetingrunning(trigger.sender):
|
|||
|
return
|
|||
|
if not ischair(trigger.nick, trigger.sender):
|
|||
|
bot.say('Only meeting head or chairs can do that')
|
|||
|
return
|
|||
|
comments = meetings_dict[trigger.sender]['comments']
|
|||
|
if comments:
|
|||
|
msg = 'The following comments were made:'
|
|||
|
bot.say(msg)
|
|||
|
logplain('<%s> %s' % (bot.nick, msg), trigger.sender)
|
|||
|
for comment in comments:
|
|||
|
msg = '<%s> %s' % comment
|
|||
|
bot.say(msg)
|
|||
|
logplain('<%s> %s' % (bot.nick, msg), trigger.sender)
|
|||
|
meetings_dict[trigger.sender]['comments'] = []
|
|||
|
else:
|
|||
|
bot.say('No comments have been logged.')
|