diff --git a/bot.py b/bot.py index 534265f..ca177a9 100755 --- a/bot.py +++ b/bot.py @@ -1,12 +1,8 @@ -# coding=utf-8 -# Copyright 2008, Sean B. Palmer, inamidst.com -# Copyright © 2012, Elad Alfassa -# Copyright 2012-2015, Elsie Powell, http://embolalia.com -# -# Licensed under the Eiffel Forum License 2. - -from __future__ import unicode_literals, absolute_import, print_function, division - +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +The core bot class. Say good bye to PYthon 2. +""" import collections import os import re @@ -27,13 +23,6 @@ import loader LOGGER = get_logger(__name__) -if sys.version_info.major >= 3: - unicode = str - basestring = str - py3 = True -else: - py3 = False - class _CapReq(object): def __init__(self, prefix, module, failure=None, arg=None, success=None): @@ -70,7 +59,6 @@ class Sopel(irc.Bot): """ self._command_groups = collections.defaultdict(list) """A mapping of module names to a list of commands in it.""" - self.stats = {} # deprecated, remove in 7.0 self._times = {} """ A dictionary mapping lower-case'd nicks to dictionaries which map @@ -261,10 +249,6 @@ class Sopel(irc.Bot): else: self.write(['JOIN', channel, password]) - def msg(self, recipient, text, max_messages=1): - # Deprecated, but way too much of a pain to remove. - self.say(text, recipient, max_messages) - def say(self, text, recipient, max_messages=1): """Send ``text`` as a PRIVMSG to ``recipient``. @@ -288,7 +272,7 @@ class Sopel(irc.Bot): max_text_length = 400 max_messages=1000 # Encode to bytes, for propper length calculation - if isinstance(text, unicode): + if isinstance(text, str): encoded_text = text.encode('utf-8') else: encoded_text = text @@ -306,7 +290,7 @@ class Sopel(irc.Bot): excess = encoded_text[last_space + 1:] encoded_text = encoded_text[:last_space] # We'll then send the excess at the end - # Back to unicode again, so we don't screw things up later. + # Back to str again, so we don't screw things up later. text = encoded_text.decode('utf-8') try: self.sending.acquire() @@ -327,7 +311,7 @@ class Sopel(irc.Bot): # Now that we've sent the first part, we need to send the rest. Doing # this recursively seems easier to me than iteratively if excess: - self.msg(recipient, excess, max_messages - 1) + self.say(recipient, excess, max_messages - 1) def notice(self, text, dest): """Send an IRC NOTICE to a user or a channel. diff --git a/coretasks.py b/coretasks.py index e7b2f42..d2ad62f 100755 --- a/coretasks.py +++ b/coretasks.py @@ -37,10 +37,7 @@ def auth_after_register(bot): """Do NickServ/AuthServ auth""" if bot.config.core.auth_method == 'nickserv': nickserv_name = bot.config.core.auth_target or 'NickServ' - bot.msg( - nickserv_name, - 'IDENTIFY %s' % bot.config.core.auth_password - ) + bot.say('IDENTIFY %s' % bot.config.core.auth_password, nickserv_name) elif bot.config.core.auth_method == 'authserv': account = bot.config.core.auth_username @@ -107,7 +104,7 @@ def startup(bot, trigger): "more secure. If you'd like to do this, make sure you're logged in " "and reply with \"{}useserviceauth\"" ).format(bot.config.core.help_prefix) - bot.msg(bot.config.core.owner, msg) + bot.say(msg, bot.config.core.owner) @module.require_privmsg() @@ -262,7 +259,7 @@ def track_nicks(bot, trigger): debug_msg = ("Nick changed by server. " "This can cause unexpected behavior. Please restart the bot.") LOGGER.critical(debug_msg) - bot.msg(bot.config.core.owner, privmsg) + bot.say(privmsg, bot.config.core.owner) return for channel in bot.privileges: diff --git a/irc.py b/irc.py index 631439d..e589fc5 100755 --- a/irc.py +++ b/irc.py @@ -1,411 +1,371 @@ -# coding=utf-8 -# irc.py - An Utility IRC Bot -# Copyright 2008, Sean B. Palmer, inamidst.com -# Copyright 2012, Elsie Powell, http://embolalia.com -# Copyright © 2012, Elad Alfassa -# -# Licensed under the Eiffel Forum License 2. -# -# When working on core IRC protocol related features, consult protocol -# documentation at http://www.irchelp.org/irchelp/rfc/ -from __future__ import unicode_literals, absolute_import, print_function, division - +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Core IRC functionality. Support for Python 2 is largely gone. +""" import sys +import os import time import socket -import asyncore -import asynchat -import os import codecs import traceback +import threading +from datetime import datetime +import errno +import ssl +import asyncore +import asynchat + from logger import get_logger from tools import stderr, Identifier from trigger import PreTrigger -try: - import ssl - if not hasattr(ssl, 'match_hostname'): - # Attempt to import ssl_match_hostname from python-backports - import backports.ssl_match_hostname - ssl.match_hostname = backports.ssl_match_hostname.match_hostname - ssl.CertificateError = backports.ssl_match_hostname.CertificateError - has_ssl = True -except ImportError: - # no SSL support - has_ssl = False - -import errno -import threading -from datetime import datetime -if sys.version_info.major >= 3: - unicode = str LOGGER = get_logger(__name__) class Bot(asynchat.async_chat): - def __init__(self, config): - ca_certs = config.core.ca_certs + def __init__(self, config): + ca_certs = config.core.ca_certs - asynchat.async_chat.__init__(self) - self.set_terminator(b'\n') - self.buffer = '' + asynchat.async_chat.__init__(self) + self.set_terminator(b'\n') + self.buffer = '' - self.nick = Identifier(config.core.nick) - """Sopel's current ``Identifier``. Changing this while Sopel is running is - untested.""" - self.user = config.core.user - """Sopel's user/ident.""" - self.name = config.core.name - """Sopel's "real name", as used for whois.""" + self.nick = Identifier(config.core.nick) + """Sopel's current ``Identifier``. Changing this while Sopel is running is + untested.""" + self.user = config.core.user + """Sopel's user/ident.""" + self.name = config.core.name + """Sopel's "real name", as used for whois.""" - self.stack = {} - self.ca_certs = ca_certs - self.enabled_capabilities = set() - self.hasquit = False + self.stack = {} + self.ca_certs = ca_certs + self.enabled_capabilities = set() + self.hasquit = False - self.sending = threading.RLock() - self.writing_lock = threading.Lock() - self.raw = None + self.sending = threading.RLock() + self.writing_lock = threading.Lock() + self.raw = None - # Right now, only accounting for two op levels. - # This might be expanded later. - # These lists are filled in startup.py, as of right now. - # Are these even touched at all anymore? Remove in 7.0. - self.ops = dict() - """Deprecated. Use bot.channels instead.""" - self.halfplus = dict() - """Deprecated. Use bot.channels instead.""" - self.voices = dict() - """Deprecated. Use bot.channels instead.""" + # We need this to prevent error loops in handle_error + self.error_count = 0 - # We need this to prevent error loops in handle_error - self.error_count = 0 + self.connection_registered = False + """ Set to True when a server has accepted the client connection and + messages can be sent and received. """ - self.connection_registered = False - """ Set to True when a server has accepted the client connection and - messages can be sent and received. """ + def log_raw(self, line, prefix): + """Log raw line to the raw log.""" + if not self.config.core.log_raw: + return + if not os.path.isdir(self.config.core.logdir): + try: + os.mkdir(self.config.core.logdir) + except Exception as e: + stderr('There was a problem creating the logs directory.') + stderr('%s %s' % (str(e.__class__), str(e))) + stderr('Please fix this and then run Sopel again.') + os._exit(1) + f = codecs.open(os.path.join(self.config.core.logdir, 'raw.log'), + 'a', encoding='utf-8') + f.write(prefix + str(time.time()) + "\t") + temp = line.replace('\n', '') - # Work around bot.connecting missing in Python older than 2.7.4 - if not hasattr(self, "connecting"): - self.connecting = False + f.write(temp) + f.write("\n") + f.close() - def log_raw(self, line, prefix): - """Log raw line to the raw log.""" - if not self.config.core.log_raw: - return - if not os.path.isdir(self.config.core.logdir): - try: - os.mkdir(self.config.core.logdir) - except Exception as e: - stderr('There was a problem creating the logs directory.') - stderr('%s %s' % (str(e.__class__), str(e))) - stderr('Please fix this and then run Sopel again.') - os._exit(1) - f = codecs.open(os.path.join(self.config.core.logdir, 'raw.log'), - 'a', encoding='utf-8') - f.write(prefix + unicode(time.time()) + "\t") - temp = line.replace('\n', '') + def safe(self, string): + """Remove newlines from a string.""" + if isinstance(string, bytes): + string = string.decode('utf8') + string = string.replace('\n', '') + string = string.replace('\r', '') + return string - f.write(temp) - f.write("\n") - f.close() + def write(self, args, text=None): + args = [self.safe(arg) for arg in args] + if text is not None: + text = self.safe(text) + try: + self.writing_lock.acquire() # Blocking lock, can't send two things + # at a time - def safe(self, string): - """Remove newlines from a string.""" - if sys.version_info.major >= 3 and isinstance(string, bytes): - string = string.decode('utf8') - elif sys.version_info.major < 3: - if not isinstance(string, unicode): - string = unicode(string, encoding='utf8') - string = string.replace('\n', '') - string = string.replace('\r', '') - return string + # From RFC2812 Internet Relay Chat: Client Protocol + # Section 2.3 + # + # https://tools.ietf.org/html/rfc2812.html + # + # IRC messages are always lines of characters terminated with a + # CR-LF (Carriage Return - Line Feed) pair, and these messages SHALL + # NOT exceed 512 characters in length, counting all characters + # including the trailing CR-LF. Thus, there are 510 characters + # maximum allowed for the command and its parameters. There is no + # provision for continuation of message lines. - def write(self, args, text=None): - args = [self.safe(arg) for arg in args] - if text is not None: - text = self.safe(text) - try: - self.writing_lock.acquire() # Blocking lock, can't send two things - # at a time + if text is not None: + temp = (' '.join(args) + ' :' + text)[:510] + '\r\n' + else: + temp = ' '.join(args)[:510] + '\r\n' + self.log_raw(temp, '>>') + self.send(temp.encode('utf-8')) + finally: + self.writing_lock.release() - # From RFC2812 Internet Relay Chat: Client Protocol - # Section 2.3 - # - # https://tools.ietf.org/html/rfc2812.html - # - # IRC messages are always lines of characters terminated with a - # CR-LF (Carriage Return - Line Feed) pair, and these messages SHALL - # NOT exceed 512 characters in length, counting all characters - # including the trailing CR-LF. Thus, there are 510 characters - # maximum allowed for the command and its parameters. There is no - # provision for continuation of message lines. + def run(self, host, port=6667): + try: + self.initiate_connect(host, port) + except socket.error as e: + stderr('Connection error: %s' % e) - if text is not None: - temp = (' '.join(args) + ' :' + text)[:510] + '\r\n' - else: - temp = ' '.join(args)[:510] + '\r\n' - self.log_raw(temp, '>>') - self.send(temp.encode('utf-8')) - finally: - self.writing_lock.release() + def initiate_connect(self, host, port): + stderr('Connecting to %s:%s...' % (host, port)) + source_address = ((self.config.core.bind_host, 0) + if self.config.core.bind_host else None) + self.set_socket(socket.create_connection((host, port), + source_address=source_address)) + if self.config.core.use_ssl: + self.send = self._ssl_send + self.recv = self._ssl_recv + self.connect((host, port)) + try: + asyncore.loop() + except KeyboardInterrupt: + print('KeyboardInterrupt') + self.quit('KeyboardInterrupt') - def run(self, host, port=6667): - try: - self.initiate_connect(host, port) - except socket.error as e: - stderr('Connection error: %s' % e) + def quit(self, message): + """Disconnect from IRC and close the bot.""" + self.write(['QUIT'], message) + self.hasquit = True + # Wait for acknowledgement from the server. By RFC 2812 it should be + # an ERROR msg, but many servers just close the connection. Either way + # is fine by us. + # Closing the connection now would mean that stuff in the buffers that + # has not yet been processed would never be processed. It would also + # release the main thread, which is problematic because whomever called + # quit might still want to do something before main thread quits. - def initiate_connect(self, host, port): - stderr('Connecting to %s:%s...' % (host, port)) - source_address = ((self.config.core.bind_host, 0) - if self.config.core.bind_host else None) - self.set_socket(socket.create_connection((host, port), - source_address=source_address)) - if self.config.core.use_ssl and has_ssl: - self.send = self._ssl_send - self.recv = self._ssl_recv - elif not has_ssl and self.config.core.use_ssl: - stderr('SSL is not avilable on your system, attempting connection ' - 'without it') - self.connect((host, port)) - try: - asyncore.loop() - except KeyboardInterrupt: - print('KeyboardInterrupt') - self.quit('KeyboardInterrupt') + def handle_close(self): + self.connection_registered = False - def quit(self, message): - """Disconnect from IRC and close the bot.""" - self.write(['QUIT'], message) - self.hasquit = True - # Wait for acknowledgement from the server. By RFC 2812 it should be - # an ERROR msg, but many servers just close the connection. Either way - # is fine by us. - # Closing the connection now would mean that stuff in the buffers that - # has not yet been processed would never be processed. It would also - # release the main thread, which is problematic because whomever called - # quit might still want to do something before main thread quits. + if hasattr(self, '_shutdown'): + self._shutdown() + stderr('Closed!') - def handle_close(self): - self.connection_registered = False + # This will eventually call asyncore dispatchers close method, which + # will release the main thread. This should be called last to avoid + # race conditions. + self.close() - if hasattr(self, '_shutdown'): - self._shutdown() - stderr('Closed!') + def handle_connect(self): + if self.config.core.use_ssl: + if not self.config.core.verify_ssl: + self.ssl = ssl.wrap_socket(self.socket, + do_handshake_on_connect=True, + suppress_ragged_eofs=True) + else: + self.ssl = ssl.wrap_socket(self.socket, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + cert_reqs=ssl.CERT_REQUIRED, + ca_certs=self.ca_certs) + try: + ssl.match_hostname(self.ssl.getpeercert(), self.config.core.host) + except ssl.CertificateError: + stderr("Invalid certficate, hostname mismatch!") + os.unlink(self.config.core.pid_file_path) + os._exit(1) + self.set_socket(self.ssl) - # This will eventually call asyncore dispatchers close method, which - # will release the main thread. This should be called last to avoid - # race conditions. - self.close() + # Request list of server capabilities. IRCv3 servers will respond with + # CAP * LS (which we handle in coretasks). v2 servers will respond with + # 421 Unknown command, which we'll ignore + self.write(('CAP', 'LS', '302')) - def handle_connect(self): - if self.config.core.use_ssl and has_ssl: - if not self.config.core.verify_ssl: - self.ssl = ssl.wrap_socket(self.socket, - do_handshake_on_connect=True, - suppress_ragged_eofs=True) - else: - self.ssl = ssl.wrap_socket(self.socket, - do_handshake_on_connect=True, - suppress_ragged_eofs=True, - cert_reqs=ssl.CERT_REQUIRED, - ca_certs=self.ca_certs) - try: - ssl.match_hostname(self.ssl.getpeercert(), self.config.core.host) - except ssl.CertificateError: - stderr("Invalid certficate, hostname mismatch!") - os.unlink(self.config.core.pid_file_path) - os._exit(1) - self.set_socket(self.ssl) + if self.config.core.auth_method == 'server': + password = self.config.core.auth_password + self.write(('PASS', password)) + self.write(('NICK', self.nick)) + self.write(('USER', self.user, '+iw', self.nick), self.name) - # Request list of server capabilities. IRCv3 servers will respond with - # CAP * LS (which we handle in coretasks). v2 servers will respond with - # 421 Unknown command, which we'll ignore - self.write(('CAP', 'LS', '302')) + stderr('Connected.') + self.last_ping_time = datetime.now() + timeout_check_thread = threading.Thread(target=self._timeout_check) + timeout_check_thread.daemon = True + timeout_check_thread.start() + ping_thread = threading.Thread(target=self._send_ping) + ping_thread.daemon = True + ping_thread.start() - if self.config.core.auth_method == 'server': - password = self.config.core.auth_password - self.write(('PASS', password)) - self.write(('NICK', self.nick)) - self.write(('USER', self.user, '+iw', self.nick), self.name) + def _timeout_check(self): + while self.connected or self.connecting: + if (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout): + stderr('Ping timeout reached after %s seconds, closing connection' % self.config.core.timeout) + self.handle_close() + break + else: + time.sleep(int(self.config.core.timeout)) - stderr('Connected.') - self.last_ping_time = datetime.now() - timeout_check_thread = threading.Thread(target=self._timeout_check) - timeout_check_thread.daemon = True - timeout_check_thread.start() - ping_thread = threading.Thread(target=self._send_ping) - ping_thread.daemon = True - ping_thread.start() + def _send_ping(self): + while self.connected or self.connecting: + if self.connected and (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout) / 2: + try: + self.write(('PING', self.config.core.host)) + except socket.error: + pass + time.sleep(int(self.config.core.timeout) / 2) - def _timeout_check(self): - while self.connected or self.connecting: - if (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout): - stderr('Ping timeout reached after %s seconds, closing connection' % self.config.core.timeout) - self.handle_close() - break - else: - time.sleep(int(self.config.core.timeout)) + def _ssl_send(self, data): + """Replacement for self.send() during SSL connections.""" + try: + result = self.socket.send(data) + return result + except ssl.SSLError as why: + if why[0] in (asyncore.EWOULDBLOCK, errno.ESRCH): + return 0 + else: + raise why + return 0 - def _send_ping(self): - while self.connected or self.connecting: - if self.connected and (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout) / 2: - try: - self.write(('PING', self.config.core.host)) - except socket.error: - pass - time.sleep(int(self.config.core.timeout) / 2) + def _ssl_recv(self, buffer_size): + """Replacement for self.recv() during SSL connections. - def _ssl_send(self, data): - """Replacement for self.send() during SSL connections.""" - try: - result = self.socket.send(data) - return result - except ssl.SSLError as why: - if why[0] in (asyncore.EWOULDBLOCK, errno.ESRCH): - return 0 - else: - raise why - return 0 + From: http://evanfosmark.com/2010/09/ssl-support-in-asynchatasync_chat - def _ssl_recv(self, buffer_size): - """Replacement for self.recv() during SSL connections. + """ + try: + data = self.socket.read(buffer_size) + if not data: + self.handle_close() + return b'' + return data + except ssl.SSLError as why: + if why[0] in (asyncore.ECONNRESET, asyncore.ENOTCONN, + asyncore.ESHUTDOWN): + self.handle_close() + return '' + elif why[0] == errno.ENOENT: + # Required in order to keep it non-blocking + return b'' + else: + raise - From: http://evanfosmark.com/2010/09/ssl-support-in-asynchatasync_chat + def collect_incoming_data(self, data): + # We can't trust clients to pass valid str. + try: + data = str(data, encoding='utf-8') + except strDecodeError: + # not str, let's try cp1252 + try: + data = str(data, encoding='cp1252') + except strDecodeError: + # Okay, let's try ISO8859-1 + try: + data = str(data, encoding='iso8859-1') + except: + # Discard line if encoding is unknown + return - """ - try: - data = self.socket.read(buffer_size) - if not data: - self.handle_close() - return b'' - return data - except ssl.SSLError as why: - if why[0] in (asyncore.ECONNRESET, asyncore.ENOTCONN, - asyncore.ESHUTDOWN): - self.handle_close() - return '' - elif why[0] == errno.ENOENT: - # Required in order to keep it non-blocking - return b'' - else: - raise + if data: + self.log_raw(data, '<<') + self.buffer += data - def collect_incoming_data(self, data): - # We can't trust clients to pass valid unicode. - try: - data = unicode(data, encoding='utf-8') - except UnicodeDecodeError: - # not unicode, let's try cp1252 - try: - data = unicode(data, encoding='cp1252') - except UnicodeDecodeError: - # Okay, let's try ISO8859-1 - try: - data = unicode(data, encoding='iso8859-1') - except: - # Discard line if encoding is unknown - return + def found_terminator(self): + line = self.buffer + if line.endswith('\r'): + line = line[:-1] + self.buffer = '' + self.last_ping_time = datetime.now() + pretrigger = PreTrigger(self.nick, line) + if all(cap not in self.enabled_capabilities for cap in ['account-tag', 'extended-join']): + pretrigger.tags.pop('account', None) - if data: - self.log_raw(data, '<<') - self.buffer += data + if pretrigger.event == 'PING': + self.write(('PONG', pretrigger.args[-1])) + elif pretrigger.event == 'ERROR': + LOGGER.error("ERROR recieved from server: %s", pretrigger.args[-1]) + if self.hasquit: + self.close_when_done() + elif pretrigger.event == '433': + stderr('Nickname already in use!') + self.handle_close() - def found_terminator(self): - line = self.buffer - if line.endswith('\r'): - line = line[:-1] - self.buffer = '' - self.last_ping_time = datetime.now() - pretrigger = PreTrigger(self.nick, line) - if all(cap not in self.enabled_capabilities for cap in ['account-tag', 'extended-join']): - pretrigger.tags.pop('account', None) + self.dispatch(pretrigger) - if pretrigger.event == 'PING': - self.write(('PONG', pretrigger.args[-1])) - elif pretrigger.event == 'ERROR': - LOGGER.error("ERROR recieved from server: %s", pretrigger.args[-1]) - if self.hasquit: - self.close_when_done() - elif pretrigger.event == '433': - stderr('Nickname already in use!') - self.handle_close() + def dispatch(self, pretrigger): + pass - self.dispatch(pretrigger) + def error(self, trigger=None): + """Called internally when a module causes an error.""" + try: + trace = traceback.format_exc() + if sys.version_info.major < 3: + trace = trace.decode('utf-8', errors='xmlcharrefreplace') + stderr(trace) + try: + lines = list(reversed(trace.splitlines())) + report = [lines[0].strip()] + for line in lines: + line = line.strip() + if line.startswith('File "'): + report.append(line[0].lower() + line[1:]) + break + else: + report.append('source unknown') - def dispatch(self, pretrigger): - pass + signature = '%s (%s)' % (report[0], report[1]) + # TODO: make not hardcoded + log_filename = os.path.join(self.config.core.logdir, 'exceptions.log') + with codecs.open(log_filename, 'a', encoding='utf-8') as logfile: + logfile.write('Signature: %s\n' % signature) + if trigger: + logfile.write('from {} at {}. Message was: {}\n'.format( + trigger.nick, str(datetime.now()), trigger.group(0))) + logfile.write(trace) + logfile.write( + '----------------------------------------\n\n' + ) + except Exception as e: + stderr("Could not save full traceback!") + LOGGER.error("Could not save traceback from %s to file: %s", trigger.sender, str(e)) - def error(self, trigger=None): - """Called internally when a module causes an error.""" - try: - trace = traceback.format_exc() - if sys.version_info.major < 3: - trace = trace.decode('utf-8', errors='xmlcharrefreplace') - stderr(trace) - try: - lines = list(reversed(trace.splitlines())) - report = [lines[0].strip()] - for line in lines: - line = line.strip() - if line.startswith('File "'): - report.append(line[0].lower() + line[1:]) - break - else: - report.append('source unknown') + if trigger and self.config.core.reply_errors and trigger.sender is not None: + self.msg(trigger.sender, signature) + if trigger: + LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(signature), trigger.raw)) + except Exception as e: + if trigger and self.config.core.reply_errors and trigger.sender is not None: + self.msg(trigger.sender, "Got an error.") + if trigger: + LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(e), trigger.raw)) - signature = '%s (%s)' % (report[0], report[1]) - # TODO: make not hardcoded - log_filename = os.path.join(self.config.core.logdir, 'exceptions.log') - with codecs.open(log_filename, 'a', encoding='utf-8') as logfile: - logfile.write('Signature: %s\n' % signature) - if trigger: - logfile.write('from {} at {}. Message was: {}\n'.format( - trigger.nick, str(datetime.now()), trigger.group(0))) - logfile.write(trace) - logfile.write( - '----------------------------------------\n\n' - ) - except Exception as e: - stderr("Could not save full traceback!") - LOGGER.error("Could not save traceback from %s to file: %s", trigger.sender, str(e)) + def handle_error(self): + """Handle any uncaptured error in the core. - if trigger and self.config.core.reply_errors and trigger.sender is not None: - self.msg(trigger.sender, signature) - if trigger: - LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(signature), trigger.raw)) - except Exception as e: - if trigger and self.config.core.reply_errors and trigger.sender is not None: - self.msg(trigger.sender, "Got an error.") - if trigger: - LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(e), trigger.raw)) + Overrides asyncore's handle_error. - def handle_error(self): - """Handle any uncaptured error in the core. - - Overrides asyncore's handle_error. - - """ - trace = traceback.format_exc() - stderr(trace) - LOGGER.error('Fatal error in core, please review exception log') - # TODO: make not hardcoded - logfile = codecs.open( - os.path.join(self.config.core.logdir, 'exceptions.log'), - 'a', - encoding='utf-8' - ) - logfile.write('Fatal error in core, handle_error() was called\n') - logfile.write('last raw line was %s' % self.raw) - logfile.write(trace) - logfile.write('Buffer:\n') - logfile.write(self.buffer) - logfile.write('----------------------------------------\n\n') - logfile.close() - if self.error_count > 10: - if (datetime.now() - self.last_error_timestamp).seconds < 5: - stderr("Too many errors, can't continue") - os._exit(1) - self.last_error_timestamp = datetime.now() - self.error_count = self.error_count + 1 + """ + trace = traceback.format_exc() + stderr(trace) + LOGGER.error('Fatal error in core, please review exception log') + # TODO: make not hardcoded + logfile = codecs.open( + os.path.join(self.config.core.logdir, 'exceptions.log'), + 'a', + encoding='utf-8' + ) + logfile.write('Fatal error in core, handle_error() was called\n') + logfile.write('last raw line was %s' % self.raw) + logfile.write(trace) + logfile.write('Buffer:\n') + logfile.write(self.buffer) + logfile.write('----------------------------------------\n\n') + logfile.close() + if self.error_count > 10: + if (datetime.now() - self.last_error_timestamp).seconds < 5: + stderr("Too many errors, can't continue") + os._exit(1) + self.last_error_timestamp = datetime.now() + self.error_count = self.error_count + 1 diff --git a/loader.py b/loader.py index 0b7a678..f81df46 100755 --- a/loader.py +++ b/loader.py @@ -1,6 +1,8 @@ -# coding=utf-8 -from __future__ import unicode_literals, absolute_import, print_function, division - +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Methods for loading modules. +""" import imp import os.path import re diff --git a/logger.py b/logger.py old mode 100644 new mode 100755 index 84ba149..c52c563 --- a/logger.py +++ b/logger.py @@ -13,7 +13,7 @@ class IrcLoggingHandler(logging.Handler): def emit(self, record): try: msg = self.format(record) - self._bot.msg(self._channel, msg) + self._bot.say(msg, self._channel) except (KeyboardInterrupt, SystemExit): raise except: diff --git a/module.py b/module.py index 0fce459..14fa298 100755 --- a/module.py +++ b/module.py @@ -59,7 +59,7 @@ def interval(*args): @sopel.module.interval(5) def spam_every_5s(bot): if "#here" in bot.channels: - bot.msg("#here", "It has been five seconds!") + bot.say("It has been five seconds!", "#here") """ def add_attribute(function): diff --git a/modules/admin.py b/modules/admin.py index 83eea1b..e2f6282 100755 --- a/modules/admin.py +++ b/modules/admin.py @@ -98,7 +98,7 @@ def msg(bot, trigger): if not channel or not message: return - bot.msg(channel, message) + bot.say(message, channel) @module.require_privmsg @@ -119,7 +119,7 @@ def me(bot, trigger): return msg = '\x01ACTION %s\x01' % action - bot.msg(channel, msg) + bot.say(msg, channel) @module.event('INVITE') diff --git a/modules/adminchannel.py b/modules/adminchannel.py index f1d0bc2..25a2116 100755 --- a/modules/adminchannel.py +++ b/modules/adminchannel.py @@ -127,62 +127,6 @@ def unban(bot, trigger): bot.write(['MODE', channel, '-b', banmask]) -@require_chanmsg -@require_privilege(OP, 'You are not a channel operator.') -@commands('quiet') -def quiet(bot, trigger): - """ - This gives admins the ability to quiet a user. - The bot must be a Channel Operator for this command to work. - """ - if bot.privileges[trigger.sender][bot.nick] < OP: - return bot.reply("I'm not a channel operator!") - text = trigger.group().split() - argc = len(text) - if argc < 2: - return - opt = Identifier(text[1]) - quietmask = opt - channel = trigger.sender - if not opt.is_nick(): - if argc < 3: - return - quietmask = text[2] - channel = opt - quietmask = configureHostMask(quietmask) - if quietmask == '': - return - bot.write(['MODE', channel, '+q', quietmask]) - - -@require_chanmsg -@require_privilege(OP, 'You are not a channel operator.') -@commands('unquiet') -def unquiet(bot, trigger): - """ - This gives admins the ability to unquiet a user. - The bot must be a Channel Operator for this command to work. - """ - if bot.privileges[trigger.sender][bot.nick] < OP: - return bot.reply("I'm not a channel operator!") - text = trigger.group().split() - argc = len(text) - if argc < 2: - return - opt = Identifier(text[1]) - quietmask = opt - channel = trigger.sender - if not opt.is_nick(): - if argc < 3: - return - quietmask = text[2] - channel = opt - quietmask = configureHostMask(quietmask) - if quietmask == '': - return - bot.write(['MODE', channel, '-q', quietmask]) - - @require_chanmsg @require_privilege(OP, 'You are not a channel operator.') @commands('kickban', 'kb') diff --git a/modules/announce.py b/modules/announce.py index c5c5aa5..212612c 100755 --- a/modules/announce.py +++ b/modules/announce.py @@ -18,5 +18,5 @@ def announce(bot, trigger): bot.reply('Sorry, I can\'t let you do that') return for channel in bot.channels: - bot.msg(channel, '[ANNOUNCEMENT] %s' % trigger.group(2)) + bot.say('[ANNOUNCEMENT] %s' % trigger.group(2), channel) bot.reply('Announce complete.') diff --git a/modules/ascii.py b/modules/ascii.py index 47a5e74..8fafd88 100755 --- a/modules/ascii.py +++ b/modules/ascii.py @@ -4,6 +4,7 @@ ASCII """ from io import BytesIO +import argparse import requests from PIL import Image, ImageFont, ImageDraw @@ -168,6 +169,8 @@ def image_to_ascii(image=None, reverse=False, brail=False, color=None,**kwargs): image = image.convert(image.palette.mode) if image.mode == "RGBA": image = alpha_composite(image).convert("RGB") + if image.mode == "L": + image = image.convert("RGB") image = scale_image(image) diff --git a/modules/bugzilla.py b/modules/bugzilla.py deleted file mode 100755 index 12329ce..0000000 --- a/modules/bugzilla.py +++ /dev/null @@ -1,97 +0,0 @@ -# coding=utf-8 -"""Bugzilla issue reporting module - -Copyright 2013-2015, Embolalia, embolalia.com -Licensed under the Eiffel Forum License 2. -""" -import re - -import xmltodict -import requests - -import tools -from config.types import StaticSection, ListAttribute -from logger import get_logger -from module import rule - - -regex = None -LOGGER = get_logger(__name__) - - -class BugzillaSection(StaticSection): - domains = ListAttribute('domains') - """The domains of the Bugzilla instances from which to get information.""" - - -def configure(config): - config.define_section('bugzilla', BugzillaSection) - config.bugzilla.configure_setting( - 'domains', - 'Enter the domains of the Bugzillas you want extra information ' - 'from (e.g. bugzilla.gnome.org)' - ) - - -def setup(bot): - global regex - bot.config.define_section('bugzilla', BugzillaSection) - - if not bot.config.bugzilla.domains: - return - if not bot.memory.contains('url_callbacks'): - bot.memory['url_callbacks'] = tools.SopelMemory() - - domains = '|'.join(bot.config.bugzilla.domains) - regex = re.compile((r'https?://(%s)' - '(/show_bug.cgi\?\S*?)' - '(id=\d+)') - % domains) - bot.memory['url_callbacks'][regex] = show_bug - - -def shutdown(bot): - del bot.memory['url_callbacks'][regex] - - -@rule(r'.*https?://(\S+?)' - '(/show_bug.cgi\?\S*?)' - '(id=\d+).*') -def show_bug(bot, trigger, match=None): - """Show information about a Bugzilla bug.""" - match = match or trigger - domain = match.group(1) - if domain not in bot.config.bugzilla.domains: - return - url = 'https://%s%sctype=xml&%s' % match.groups() - data = requests.get(url) - bug = xmltodict.parse(data).get('bugzilla').get('bug') - error = bug.get('@error', None) # error="NotPermitted" - - if error: - LOGGER.warning('Bugzilla error: %s' % error) - bot.say('[BUGZILLA] Unable to get infomation for ' - 'linked bug (%s)' % error) - return - - message = ('[BUGZILLA] %s | Product: %s | Component: %s | Version: %s | ' + - 'Importance: %s | Status: %s | Assigned to: %s | ' + - 'Reported: %s | Modified: %s') - - resolution = bug.get('resolution') - if resolution is not None: - status = bug.get('bug_status') + ' ' + resolution - else: - status = bug.get('bug_status') - - assigned_to = bug.get('assigned_to') - if isinstance(assigned_to, dict): - assigned_to = assigned_to.get('@name') - - message = message % ( - bug.get('short_desc'), bug.get('product'), - bug.get('component'), bug.get('version'), - (bug.get('priority') + ' ' + bug.get('bug_severity')), - status, assigned_to, bug.get('creation_ts'), - bug.get('delta_ts')) - bot.say(message) diff --git a/modules/clock.py b/modules/clock.py deleted file mode 100755 index 31d1e9b..0000000 --- a/modules/clock.py +++ /dev/null @@ -1,276 +0,0 @@ -# coding=utf-8 -# Copyright 2008-9, Sean B. Palmer, inamidst.com -# Copyright 2012, Elsie Powell, embolalia.com -# Licensed under the Eiffel Forum License 2. - -try: - import pytz -except ImportError: - pytz = None - -from module import commands, example, OP -from tools.time import ( - get_timezone, format_time, validate_format, validate_timezone -) -from config.types import StaticSection, ValidatedAttribute - - -class TimeSection(StaticSection): - tz = ValidatedAttribute( - 'tz', - parse=validate_timezone, - serialize=validate_timezone, - default='UTC' - ) - """Default time zone (see http://sopel.chat/tz)""" - time_format = ValidatedAttribute( - 'time_format', - parse=validate_format, - default='%Y-%m-%d - %T%Z' - ) - """Default time format (see http://strftime.net)""" - - -def configure(config): - config.define_section('clock', TimeSection) - config.clock.configure_setting( - 'tz', 'Preferred time zone (http://sopel.chat/tz)') - config.clock.configure_setting( - 'time_format', 'Preferred time format (http://strftime.net)') - - -def setup(bot): - bot.config.define_section('clock', TimeSection) - - -@commands('t', 'time') -@example('.t America/New_York') -def f_time(bot, trigger): - """Returns the current time.""" - if trigger.group(2): - zone = get_timezone(bot.db, bot.config, trigger.group(2).strip(), None, None) - if not zone: - bot.say('Could not find timezone %s.' % trigger.group(2).strip()) - return - else: - zone = get_timezone(bot.db, bot.config, None, trigger.nick, - trigger.sender) - time = format_time(bot.db, bot.config, zone, trigger.nick, trigger.sender) - bot.say(time) - - -@commands('settz', 'settimezone') -@example('.settz America/New_York') -def update_user(bot, trigger): - """ - Set your preferred time zone. Most timezones will work, but it's best to - use one from http://sopel.chat/tz - """ - if not pytz: - bot.reply("Sorry, I don't have timezone support installed.") - else: - tz = trigger.group(2) - if not tz: - bot.reply("What timezone do you want to set? Try one from " - "http://sopel.chat/tz") - return - if tz not in pytz.all_timezones: - bot.reply("I don't know that time zone. Try one from " - "http://sopel.chat/tz") - return - - bot.db.set_nick_value(trigger.nick, 'timezone', tz) - if len(tz) < 7: - bot.say("Okay, {}, but you should use one from http://sopel.chat/tz " - "if you use DST.".format(trigger.nick)) - else: - bot.reply('I now have you in the %s time zone.' % tz) - - -@commands('gettz', 'gettimezone') -@example('.gettz [nick]') -def get_user_tz(bot, trigger): - """ - Gets a user's preferred time zone, will show yours if no user specified - """ - if not pytz: - bot.reply("Sorry, I don't have timezone support installed.") - else: - nick = trigger.group(2) - if not nick: - nick = trigger.nick - - nick = nick.strip() - - tz = bot.db.get_nick_value(nick, 'timezone') - if tz: - bot.say('%s\'s time zone is %s.' % (nick, tz)) - else: - bot.say('%s has not set their time zone' % nick) - - -@commands('settimeformat', 'settf') -@example('.settf %Y-%m-%dT%T%z') -def update_user_format(bot, trigger): - """ - Sets your preferred format for time. Uses the standard strftime format. You - can use http://strftime.net or your favorite search engine to learn more. - """ - tformat = trigger.group(2) - if not tformat: - bot.reply("What format do you want me to use? Try using" - " http://strftime.net to make one.") - return - - tz = get_timezone(bot.db, bot.config, None, trigger.nick, trigger.sender) - - # Get old format as back-up - old_format = bot.db.get_nick_value(trigger.nick, 'time_format') - - # Save the new format in the database so we can test it. - bot.db.set_nick_value(trigger.nick, 'time_format', tformat) - - try: - timef = format_time(db=bot.db, zone=tz, nick=trigger.nick) - except: - bot.reply("That format doesn't work. Try using" - " http://strftime.net to make one.") - # New format doesn't work. Revert save in database. - bot.db.set_nick_value(trigger.nick, 'time_format', old_format) - return - bot.reply("Got it. Your time will now appear as %s. (If the " - "timezone is wrong, you might try the settz command)" - % timef) - - -@commands('gettimeformat', 'gettf') -@example('.gettf [nick]') -def get_user_format(bot, trigger): - """ - Gets a user's preferred time format, will show yours if no user specified - """ - nick = trigger.group(2) - if not nick: - nick = trigger.nick - - nick = nick.strip() - - # Get old format as back-up - format = bot.db.get_nick_value(nick, 'time_format') - - if format: - bot.say("%s's time format: %s." % (nick, format)) - else: - bot.say("%s hasn't set a custom time format" % nick) - - -@commands('setchanneltz', 'setctz') -@example('.setctz America/New_York') -def update_channel(bot, trigger): - """ - Set the preferred time zone for the channel. - """ - if bot.privileges[trigger.sender][trigger.nick] < OP: - return - elif not pytz: - bot.reply("Sorry, I don't have timezone support installed.") - else: - tz = trigger.group(2) - if not tz: - bot.reply("What timezone do you want to set? Try one from " - "http://sopel.chat/tz") - return - if tz not in pytz.all_timezones: - bot.reply("I don't know that time zone. Try one from " - "http://sopel.chat/tz") - return - - bot.db.set_channel_value(trigger.sender, 'timezone', tz) - if len(tz) < 7: - bot.say("Okay, {}, but you should use one from http://sopel.chat/tz " - "if you use DST.".format(trigger.nick)) - else: - bot.reply( - 'I now have {} in the {} time zone.'.format(trigger.sender, tz)) - - -@commands('getchanneltz', 'getctz') -@example('.getctz [channel]') -def get_channel_tz(bot, trigger): - """ - Gets the preferred channel timezone, or the current channel timezone if no - channel given. - """ - if not pytz: - bot.reply("Sorry, I don't have timezone support installed.") - else: - channel = trigger.group(2) - if not channel: - channel = trigger.sender - - channel = channel.strip() - - timezone = bot.db.get_channel_value(channel, 'timezone') - if timezone: - bot.say('%s\'s timezone: %s' % (channel, timezone)) - else: - bot.say('%s has no preferred timezone' % channel) - - -@commands('setchanneltimeformat', 'setctf') -@example('.setctf %Y-%m-%dT%T%z') -def update_channel_format(bot, trigger): - """ - Sets your preferred format for time. Uses the standard strftime format. You - can use http://strftime.net or your favorite search engine to learn more. - """ - if bot.privileges[trigger.sender][trigger.nick] < OP: - return - - tformat = trigger.group(2) - if not tformat: - bot.reply("What format do you want me to use? Try using" - " http://strftime.net to make one.") - - tz = get_timezone(bot.db, bot.config, None, None, trigger.sender) - - # Get old format as back-up - old_format = bot.db.get_channel_value(trigger.sender, 'time_format') - - # Save the new format in the database so we can test it. - bot.db.set_channel_value(trigger.sender, 'time_format', tformat) - - try: - timef = format_time(db=bot.db, zone=tz, channel=trigger.sender) - except: - bot.reply("That format doesn't work. Try using" - " http://strftime.net to make one.") - # New format doesn't work. Revert save in database. - bot.db.set_channel_value(trigger.sender, 'time_format', old_format) - return - bot.db.set_channel_value(trigger.sender, 'time_format', tformat) - bot.reply("Got it. Times in this channel will now appear as %s " - "unless a user has their own format set. (If the timezone" - " is wrong, you might try the settz and channeltz " - "commands)" % timef) - - -@commands('getchanneltimeformat', 'getctf') -@example('.getctf [channel]') -def get_channel_format(bot, trigger): - """ - Gets the channel's preferred time format, will return current channel's if - no channel name is given - """ - - channel = trigger.group(2) - if not channel: - channel = trigger.sender - - channel = channel.strip() - - tformat = bot.db.get_channel_value(channel, 'time_format') - if tformat: - bot.say('%s\'s time format: %s' % (channel, tformat)) - else: - bot.say('%s has no preferred time format' % channel) diff --git a/modules/etymology.py b/modules/etymology.py index 2140c5b..bb7ae2b 100755 --- a/modules/etymology.py +++ b/modules/etymology.py @@ -83,15 +83,15 @@ def f_etymology(bot, trigger): result = etymology(word) except IOError: msg = "Can't connect to etymonline.com (%s)" % (etyuri % word) - bot.msg(trigger.sender, msg) + bot.say(msg) return NOLIMIT except (AttributeError, TypeError): result = None if result is not None: - bot.msg(trigger.sender, result) + bot.say(result) else: uri = etysearch % word msg = 'Can\'t find the etymology for "%s". Try %s' % (word, uri) - bot.msg(trigger.sender, msg) + bot.say(msg) return NOLIMIT diff --git a/modules/grog.py b/modules/grog.py new file mode 100755 index 0000000..f0431bc --- /dev/null +++ b/modules/grog.py @@ -0,0 +1,28 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Selects a random Grog of Substantial Whimsy effect. +""" +import os +import random + +from module import commands, example + +@commands("grog") +@example(".grog") +def grog(bot, trigger): + """ + Picks a random status effect from Grog of Substantial Whimsy effect. + """ + with open(os.path.join(bot.config.homedir, "grog.txt"), "r") as file: + data = file.read().split("\n") + num = 0 + if trigger.group(2): + try: + num = int(trigger.group(2)) - 1 + except: + pass + if num and num < len(data): + bot.say(data[num]) + else: + bot.say(random.choice(data)) diff --git a/modules/help.py b/modules/help.py index e803ae8..e8fd244 100755 --- a/modules/help.py +++ b/modules/help.py @@ -8,18 +8,8 @@ Licensed under the Eiffel Forum License 2. http://sopel.chat """ -from __future__ import unicode_literals, absolute_import, print_function, division - -import textwrap -import collections -import json - -from logger import get_logger from module import commands, rule, example, priority -logger = get_logger(__name__) - - @rule('$nick' '(?i)(help|doc) +([A-Za-z]+)(?:\?+)?$') @example('.help tell') @commands('help', 'commands') @@ -30,23 +20,29 @@ def help(bot, trigger): name = trigger.group(2) name = name.lower() - if name in bot.doc: - print(bot.doc[name]) - newlines = [''] - lines = list(filter(None, bot.doc[name][0])) - lines = list(map(str.strip, lines)) - for line in lines: - newlines[-1] = newlines[-1] + ' ' + line - if line[-1] is '.': - newlines.append('') - newlines = list(map(str.strip, newlines)) - if bot.doc[name][1]: - newlines.append('Ex. ' + bot.doc[name][1]) + if name not in bot.doc: + return + newlines = [''] + lines = list(filter(None, bot.doc[name][0])) + lines = list(map(str.strip, lines)) + for line in lines: + newlines[-1] = newlines[-1] + ' ' + line + if line[-1] is '.': + newlines.append('') + newlines = list(map(str.strip, newlines)) + if bot.doc[name][1]: + newlines.append('Ex. ' + bot.doc[name][1]) - for msg in newlines: - bot.say(msg) + for msg in newlines: + bot.say(msg) else: - helps = list(bot.command_groups) - helps.sort() - msg = "Available commands: " + ', '.join(helps) + command_groups = list(bot.command_groups.values()) + commands = [] + for group in command_groups: + if type(group) == list: + commands += group + else: + commands += [group] + commands.sort() + msg = "Available commands: " + ', '.join(commands) bot.say(msg) diff --git a/modules/iot.py b/modules/iot.py index 9bd340c..519d76e 100755 --- a/modules/iot.py +++ b/modules/iot.py @@ -43,7 +43,7 @@ def roomTemp(bot, trigger): @module.require_admin @module.commands('inkwrite') -def roomTemp(bot, trigger): +def inkWrite(bot, trigger): """ Writes shit to my e-ink screen. """ diff --git a/modules/meetbot.py b/modules/meetbot.py deleted file mode 100755 index af461c2..0000000 --- a/modules/meetbot.py +++ /dev/null @@ -1,432 +0,0 @@ -# coding=utf-8 -""" -meetbot.py - Sopel meeting logger module -Copyright © 2012, Elad Alfassa, -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('\n\n\n\n%TITLE%\n\n\n

%TITLE%

\n'.replace('%TITLE%', title)) - logfile.write('

Meeting started by %s

    \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('
  • ' + item + '
  • \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('
\n

Meeting ended at %s UTC

\n' % current_time) - plainlog_url = meeting_log_baseurl + channel + '/' + figure_logfile_name(channel) + '.log' - logfile.write('Full log' % plainlog_url) - logfile.write('\n\n') - 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

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('

' + trigger.group(2) + '