sopel.modules.meetbot.log_plain()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 1
nop 2
1
# coding=utf-8
2
"""
3
meetbot.py - Sopel Meeting Logger Module
4
This module is an attempt to implement some of the functionality of Debian's meetbot
5
Copyright © 2012, Elad Alfassa, <[email protected]>
6
Licensed under the Eiffel Forum License 2.
7
8
https://sopel.chat
9
"""
10
from __future__ import absolute_import, division, print_function, unicode_literals
11
12
import codecs
13
import collections
14
import os
15
import re
16
import time
17
from string import punctuation, whitespace
18
19
from sopel import formatting, module, tools
20
from sopel.config.types import (FilenameAttribute, StaticSection,
21
                                ValidatedAttribute)
22
from sopel.modules.url import find_title
23
24
25
UNTITLED_MEETING = "Untitled meeting"
26
27
28
class MeetbotSection(StaticSection):
29
    """Configuration file section definition"""
30
31
    meeting_log_path = FilenameAttribute(
32
        "meeting_log_path", directory=True, default="~/www/meetings"
33
    )
34
    """Path to meeting logs storage directory
35
36
    This should be an absolute path, accessible on a webserver."""
37
38
    meeting_log_baseurl = ValidatedAttribute(
39
        "meeting_log_baseurl", default="http://localhost/~sopel/meetings"
40
    )
41
    """Base URL for the meeting logs directory"""
42
43
44
def configure(config):
45
    """
46
    | name | example | purpose |
47
    | ---- | ------- | ------- |
48
    | meeting\\_log\\_path | /home/sopel/www/meetings | Path to meeting logs storage directory (should be an absolute path, accessible on a webserver) |
49
    | meeting\\_log\\_baseurl | http://example.com/~sopel/meetings | Base URL for the meeting logs directory |
50
    """
51
    config.define_section("meetbot", MeetbotSection)
52
    config.meetbot.configure_setting(
53
        "meeting_log_path", "Enter the directory to store logs in."
54
    )
55
    config.meetbot.configure_setting(
56
        "meeting_log_baseurl", "Enter the base URL for the meeting logs."
57
    )
58
59
60
def setup(bot):
61
    bot.config.define_section("meetbot", MeetbotSection)
62
63
64
meetings_dict = collections.defaultdict(dict)  # Saves metadata about currently running meetings
65
"""
66
meetings_dict is a 2D dict.
67
68
Each meeting should have:
69
channel
70
time of start
71
head (can stop the meeting, plus all abilities of chairs)
72
chairs (can add infolines to the logs)
73
title
74
current subject
75
comments (what people who aren't voiced want to add)
76
77
Using channel as the meeting ID as there can't be more than one meeting in a
78
channel at the same time.
79
"""
80
81
# To be defined on meeting start as part of sanity checks, used by logging
82
# functions so we don't have to pass them bot
83
meeting_log_path = ""
84
meeting_log_baseurl = ""
85
86
# A dict of channels to the actions that have been created in them. This way
87
# we can have .listactions spit them back out later on.
88
meeting_actions = {}
89
90
91
# Get the logfile name for the meeting in the requested channel
92
# Used by all logging functions and web path
93
def figure_logfile_name(channel):
94
    if meetings_dict[channel]["title"] == UNTITLED_MEETING:
95
        name = "untitled"
96
    else:
97
        name = meetings_dict[channel]["title"]
98
    # Real simple sluggifying.
99
    # May not handle unicode or unprintables well. Close enough.
100
    for character in punctuation + whitespace:
101
        name = name.replace(character, "-")
102
    name = name.strip("-")
103
    timestring = time.strftime(
104
        "%Y-%m-%d-%H:%M", time.gmtime(meetings_dict[channel]["start"])
105
    )
106
    filename = timestring + "_" + name
107
    return filename
108
109
110
# Start HTML log
111
def log_html_start(channel):
112
    logfile_filename = os.path.join(
113
        meeting_log_path + channel, figure_logfile_name(channel) + ".html"
114
    )
115
    logfile = codecs.open(logfile_filename, "a", encoding="utf-8")
116
    timestring = time.strftime(
117
        "%Y-%m-%d %H:%M", time.gmtime(meetings_dict[channel]["start"])
118
    )
119
    title = "%s at %s, %s" % (meetings_dict[channel]["title"], channel, timestring)
120
    logfile.write(
121
        (
122
            "<!doctype html><html><head><meta charset='utf-8'>\n"
123
            "<title>{title}</title>\n</head><body>\n<h1>{title}</h1>\n"
124
        ).format(title=title)
125
    )
126
    logfile.write(
127
        "<h4>Meeting started by %s</h4><ul>\n" % meetings_dict[channel]["head"]
128
    )
129
    logfile.close()
130
131
132
# Write a list item in the HTML log
133
def log_html_listitem(item, channel):
134
    logfile_filename = os.path.join(
135
        meeting_log_path + channel, figure_logfile_name(channel) + ".html"
136
    )
137
    logfile = codecs.open(logfile_filename, "a", encoding="utf-8")
138
    logfile.write("<li>" + item + "</li>\n")
139
    logfile.close()
140
141
142
# End the HTML log
143
def log_html_end(channel):
144
    logfile_filename = os.path.join(
145
        meeting_log_path + channel, figure_logfile_name(channel) + ".html"
146
    )
147
    logfile = codecs.open(logfile_filename, "a", encoding="utf-8")
148
    current_time = time.strftime("%H:%M:%S", time.gmtime())
149
    logfile.write("</ul>\n<h4>Meeting ended at %s UTC</h4>\n" % current_time)
150
    plainlog_url = meeting_log_baseurl + tools.web.quote(
151
        channel + "/" + figure_logfile_name(channel) + ".log"
152
    )
153
    logfile.write('<a href="%s">Full log</a>' % plainlog_url)
154
    logfile.write("\n</body>\n</html>\n")
155
    logfile.close()
156
157
158
# Write a string to the plain text log
159
def log_plain(item, channel):
160
    logfile_filename = os.path.join(
161
        meeting_log_path + channel, figure_logfile_name(channel) + ".log"
162
    )
163
    logfile = codecs.open(logfile_filename, "a", encoding="utf-8")
164
    current_time = time.strftime("%H:%M:%S", time.gmtime())
165
    logfile.write("[" + current_time + "] " + item + "\r\n")
166
    logfile.close()
167
168
169
# Check if a meeting is currently running
170
def is_meeting_running(channel):
171
    try:
172
        return meetings_dict[channel]["running"]
173
    except KeyError:
174
        return False
175
176
177
# Check if nick is a chair or head of the meeting
178
def is_chair(nick, channel):
179
    try:
180
        return (
181
            nick.lower() == meetings_dict[channel]["head"] or
182
            nick.lower() in meetings_dict[channel]["chairs"]
183
        )
184
    except KeyError:
185
        return False
186
187
188
# Start meeting (also performs all required sanity checks)
189
@module.commands("startmeeting")
190
@module.example(".startmeeting")
191
@module.example(".startmeeting Meeting Title")
192
@module.require_chanmsg("Meetings can only be started in channels")
193
def startmeeting(bot, trigger):
194
    """
195
    Start a meeting.\
196
    See [meetbot module usage]({% link _usage/meetbot-module.md %})
197
    """
198
    if is_meeting_running(trigger.sender):
199
        bot.say("There is already an active meeting here!")
200
        return
201
    # Start the meeting
202
    meetings_dict[trigger.sender]["start"] = time.time()
203
    if not trigger.group(2):
204
        meetings_dict[trigger.sender]["title"] = UNTITLED_MEETING
205
    else:
206
        meetings_dict[trigger.sender]["title"] = trigger.group(2)
207
    meetings_dict[trigger.sender]["head"] = trigger.nick.lower()
208
    meetings_dict[trigger.sender]["running"] = True
209
    meetings_dict[trigger.sender]["comments"] = []
210
211
    # Set up paths and URLs
212
    global meeting_log_path
213
    meeting_log_path = bot.config.meetbot.meeting_log_path
214
    if not meeting_log_path.endswith(os.sep):
215
        meeting_log_path += os.sep
216
217
    global meeting_log_baseurl
218
    meeting_log_baseurl = bot.config.meetbot.meeting_log_baseurl
219
    if not meeting_log_baseurl.endswith("/"):
220
        meeting_log_baseurl = meeting_log_baseurl + "/"
221
222
    channel_log_path = meeting_log_path + trigger.sender
223
    if not os.path.isdir(channel_log_path):
224
        try:
225
            os.makedirs(channel_log_path)
226
        except Exception:  # TODO: Be specific
227
            bot.say(
228
                "Meeting not started: Couldn't create log directory for this channel"
229
            )
230
            meetings_dict[trigger.sender] = collections.defaultdict(dict)
231
            raise
232
    # Okay, meeting started!
233
    log_plain("Meeting started by " + trigger.nick.lower(), trigger.sender)
234
    log_html_start(trigger.sender)
235
    meeting_actions[trigger.sender] = []
236
    bot.say(
237
        (
238
            formatting.bold("Meeting started!") + " use {0}action, {0}agreed, "
239
            "{0}info, {0}link, {0}chairs, {0}subject, and {0}comments to "
240
            "control the meeting. To end the meeting, type {0}endmeeting"
241
        ).format(bot.config.core.help_prefix)
242
    )
243
    bot.say(
244
        (
245
            "Users without speaking permission can participate by sending me "
246
            "a PM with `{0}comment {1}` followed by their comment."
247
        ).format(bot.config.core.help_prefix, trigger.sender)
248
    )
249
250
251
# Change the current subject (will appear as <h3> in the HTML log)
252
@module.commands("subject")
253
@module.example(".subject roll call")
254
def meetingsubject(bot, trigger):
255
    """
256
    Change the meeting subject.\
257
    See [meetbot module usage]({% link _usage/meetbot-module.md %})
258
    """
259
    if not is_meeting_running(trigger.sender):
260
        bot.say("There is no active meeting")
261
        return
262
    if not trigger.group(2):
263
        bot.say("What is the subject?")
264
        return
265
    if not is_chair(trigger.nick, trigger.sender):
266
        bot.say("Only meeting head or chairs can do that")
267
        return
268
    meetings_dict[trigger.sender]["current_subject"] = trigger.group(2)
269
    logfile_filename = os.path.join(
270
        meeting_log_path + trigger.sender, figure_logfile_name(trigger.sender) + ".html"
271
    )
272
    logfile = codecs.open(logfile_filename, "a", encoding="utf-8")
273
    logfile.write("</ul><h3>" + trigger.group(2) + "</h3><ul>")
274
    logfile.close()
275
    log_plain(
276
        "Current subject: {} (set by {})".format(trigger.group(2), trigger.nick),
277
        trigger.sender,
278
    )
279
    bot.say(formatting.bold("Current subject:") + " " + trigger.group(2))
280
281
282
# End the meeting
283
@module.commands("endmeeting")
284
@module.example(".endmeeting")
285
def endmeeting(bot, trigger):
286
    """
287
    End a meeting.\
288
    See [meetbot module usage]({% link _usage/meetbot-module.md %})
289
    """
290
    if not is_meeting_running(trigger.sender):
291
        bot.say("There is no active meeting")
292
        return
293
    if not is_chair(trigger.nick, trigger.sender):
294
        bot.say("Only meeting head or chairs can do that")
295
        return
296
    meeting_length = time.time() - meetings_dict[trigger.sender]["start"]
297
    bot.say(
298
        formatting.bold("Meeting ended!") +
299
        " Total meeting length %d minutes" % (meeting_length // 60)
300
    )
301
    log_html_end(trigger.sender)
302
    htmllog_url = meeting_log_baseurl + tools.web.quote(
303
        trigger.sender + "/" + figure_logfile_name(trigger.sender) + ".html"
304
    )
305
    log_plain(
306
        "Meeting ended by %s. Total meeting length: %d minutes"
307
        % (trigger.nick, meeting_length // 60),
308
        trigger.sender,
309
    )
310
    bot.say("Meeting minutes: " + htmllog_url)
311
    meetings_dict[trigger.sender] = collections.defaultdict(dict)
312
    del meeting_actions[trigger.sender]
313
314
315
# Set meeting chairs (people who can control the meeting)
316
@module.commands("chairs")
317
@module.example(".chairs Tyrope Jason elad")
318
def chairs(bot, trigger):
319
    """
320
    Set the meeting chairs.\
321
    See [meetbot module usage]({% link _usage/meetbot-module.md %})
322
    """
323
    if not is_meeting_running(trigger.sender):
324
        bot.say("There is no active meeting")
325
        return
326
    if not trigger.group(2):
327
        bot.say(
328
            "Who are the chairs? Try `{}chairs Alice Bob Cindy`".format(
329
                bot.config.core.help_prefix
330
            )
331
        )
332
        return
333
    if trigger.nick.lower() == meetings_dict[trigger.sender]["head"]:
334
        meetings_dict[trigger.sender]["chairs"] = trigger.group(2).lower().split(" ")
335
        chairs_readable = trigger.group(2).lower().replace(" ", ", ")
336
        log_plain("Meeting chairs are: " + chairs_readable, trigger.sender)
337
        log_html_listitem(
338
            "<span style='font-weight: bold'>Meeting chairs are:</span> %s"
339
            % chairs_readable,
340
            trigger.sender,
341
        )
342
        bot.say(formatting.bold("Meeting chairs are:") + " " + chairs_readable)
343
    else:
344
        bot.say("Only meeting head can set chairs")
345
346
347
# Log action item in the HTML log
348
@module.commands("action")
349
@module.example(".action elad will develop a meetbot")
350
def meetingaction(bot, trigger):
351
    """
352
    Log an action in the meeting log.\
353
    See [meetbot module usage]({% link _usage/meetbot-module.md %})
354
    """
355
    if not is_meeting_running(trigger.sender):
356
        bot.say("There is no active meeting")
357
        return
358
    if not trigger.group(2):
359
        bot.say(
360
            "Try `{}action Bob will do something`".format(bot.config.core.help_prefix)
361
        )
362
        return
363
    if not is_chair(trigger.nick, trigger.sender):
364
        bot.say("Only meeting head or chairs can do that")
365
        return
366
    log_plain("ACTION: " + trigger.group(2), trigger.sender)
367
    log_html_listitem(
368
        "<span style='font-weight: bold'>Action: </span>" + trigger.group(2),
369
        trigger.sender,
370
    )
371
    meeting_actions[trigger.sender].append(trigger.group(2))
372
    bot.say(formatting.bold("ACTION:") + " " + trigger.group(2))
373
374
375
@module.commands("listactions")
376
@module.example(".listactions")
377
def listactions(bot, trigger):
378
    if not is_meeting_running(trigger.sender):
379
        bot.say("There is no active meeting")
380
        return
381
    for action in meeting_actions[trigger.sender]:
382
        bot.say(formatting.bold("ACTION:") + " " + action)
383
384
385
# Log agreed item in the HTML log
386 View Code Duplication
@module.commands("agreed")
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
387
@module.example(".agreed Bowties are cool")
388
def meetingagreed(bot, trigger):
389
    """
390
    Log an agreement in the meeting log.\
391
    See [meetbot module usage]({% link _usage/meetbot-module.md %})
392
    """
393
    if not is_meeting_running(trigger.sender):
394
        bot.say("There is no active meeting")
395
        return
396
    if not trigger.group(2):
397
        bot.say("Try `{}agreed Bowties are cool`".format(bot.config.core.help_prefix))
398
        return
399
    if not is_chair(trigger.nick, trigger.sender):
400
        bot.say("Only meeting head or chairs can do that")
401
        return
402
    log_plain("AGREED: " + trigger.group(2), trigger.sender)
403
    log_html_listitem(
404
        "<span style='font-weight: bold'>Agreed: </span>" + trigger.group(2),
405
        trigger.sender,
406
    )
407
    bot.say(formatting.bold("AGREED:") + " " + trigger.group(2))
408
409
410
# Log link item in the HTML log
411
@module.commands("link")
412
@module.example(".link http://example.com")
413
def meetinglink(bot, trigger):
414
    """
415
    Log a link in the meeing log.\
416
    See [meetbot module usage]({% link _usage/meetbot-module.md %})
417
    """
418
    if not is_meeting_running(trigger.sender):
419
        bot.say("There is no active meeting")
420
        return
421
    if not trigger.group(2):
422
        bot.say(
423
            "Try `{}link https://relevant-website.example/`".format(
424
                bot.config.core.help_prefix
425
            )
426
        )
427
        return
428
    if not is_chair(trigger.nick, trigger.sender):
429
        bot.say("Only meeting head or chairs can do that")
430
        return
431
    link = trigger.group(2)
432
    if not link.startswith("http"):
433
        link = "http://" + link
434
    try:
435
        title = find_title(link, verify=bot.config.core.verify_ssl)
436
    except Exception:  # TODO: Be specific
437
        title = ""
438
    log_plain("LINK: %s [%s]" % (link, title), trigger.sender)
439
    log_html_listitem('<a href="%s">%s</a>' % (link, title), trigger.sender)
440
    bot.say(formatting.bold("LINK:") + " " + link)
441
442
443
# Log informational item in the HTML log
444 View Code Duplication
@module.commands("info")
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
445
@module.example(".info all board members present")
446
def meetinginfo(bot, trigger):
447
    """
448
    Log an informational item in the meeting log.\
449
    See [meetbot module usage]({% link _usage/meetbot-module.md %})
450
    """
451
    if not is_meeting_running(trigger.sender):
452
        bot.say("There is no active meeting")
453
        return
454
    if not trigger.group(2):
455
        bot.say(
456
            "Try `{}info some informative thing`".format(bot.config.core.help_prefix)
457
        )
458
        return
459
    if not is_chair(trigger.nick, trigger.sender):
460
        bot.say("Only meeting head or chairs can do that")
461
        return
462
    log_plain("INFO: " + trigger.group(2), trigger.sender)
463
    log_html_listitem(trigger.group(2), trigger.sender)
464
    bot.say(formatting.bold("INFO:") + " " + trigger.group(2))
465
466
467
# called for every single message
468
# Will log to plain text only
469
@module.rule("(.*)")
470
@module.priority("low")
471
def log_meeting(bot, trigger):
472
    if not is_meeting_running(trigger.sender):
473
        return
474
475
    # Handle live prefix changes with cached regex
476
    if (
477
        "meetbot_prefix" not in bot.memory or
478
        bot.memory["meetbot_prefix"] != bot.config.core.prefix
479
    ):
480
        commands = [
481
            "startmeeting",
482
            "subject",
483
            "endmeeting",
484
            "chairs",
485
            "action",
486
            "listactions",
487
            "agreed",
488
            "link",
489
            "info",
490
            "comments",
491
        ]
492
493
        bot.memory["meetbot_command_regex"] = re.compile(
494
            "{}({})( |$)".format(bot.config.core.prefix, "|".join(commands))
495
        )
496
        bot.memory["meetbot_prefix"] = bot.config.core.prefix
497
498
    if bot.memory["meetbot_command_regex"].match(trigger):
499
        return
500
    log_plain("<" + trigger.nick + "> " + trigger, trigger.sender)
501
502
503
@module.commands("comment")
504
@module.require_privmsg()
505
def take_comment(bot, trigger):
506
    """
507
    Log a comment, to be shown with other comments when a chair uses .comments.
508
    Intended to allow commentary from those outside the primary group of people
509
    in the meeting.
510
511
    Used in private message only, as `.comment <#channel> <comment to add>`
512
513
    See [meetbot module usage]({% link _usage/meetbot-module.md %})
514
    """
515
    if not trigger.group(4):  # <2 arguements were given
516
        bot.say(
517
            "Usage: {}comment <#channel> <comment to add>".format(
518
                bot.config.core.help_prefix
519
            )
520
        )
521
        return
522
523
    target, message = trigger.group(2).split(None, 1)
524
    target = tools.Identifier(target)
525
    if not is_meeting_running(target):
526
        bot.say("There is no active meeting in that channel.")
527
    else:
528
        meetings_dict[trigger.group(3)]["comments"].append((trigger.nick, message))
529
        bot.say(
530
            "Your comment has been recorded. It will be shown when the "
531
            "chairs tell me to show the comments."
532
        )
533
        bot.say(
534
            "A new comment has been recorded.", meetings_dict[trigger.group(3)]["head"]
535
        )
536
537
538
@module.commands("comments")
539
def show_comments(bot, trigger):
540
    """
541
    Show the comments that have been logged for this meeting with .comment.
542
543
    See [meetbot module usage]({% link _usage/meetbot-module.md %})
544
    """
545
    if not is_meeting_running(trigger.sender):
546
        return
547
    if not is_chair(trigger.nick, trigger.sender):
548
        bot.say("Only meeting head or chairs can do that")
549
        return
550
    comments = meetings_dict[trigger.sender]["comments"]
551
    if comments:
552
        msg = "The following comments were made:"
553
        bot.say(msg)
554
        log_plain("<%s> %s" % (bot.nick, msg), trigger.sender)
555
        for comment in comments:
556
            msg = "<%s> %s" % comment
557
            bot.say(msg)
558
            log_plain("<%s> %s" % (bot.nick, msg), trigger.sender)
559
        meetings_dict[trigger.sender]["comments"] = []
560
    else:
561
        bot.say("No comments have been recorded")
562