some refactoring, removed some useless stuff, added some useless stuff

This commit is contained in:
iou1name 2018-01-05 08:46:52 -05:00
parent 04cc2a2084
commit e6e8d544d2
27 changed files with 682 additions and 1549 deletions

32
bot.py
View File

@ -1,12 +1,8 @@
# coding=utf-8 #! /usr/bin/env python3
# Copyright 2008, Sean B. Palmer, inamidst.com # -*- coding: utf-8 -*-
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org> """
# Copyright 2012-2015, Elsie Powell, http://embolalia.com The core bot class. Say good bye to PYthon 2.
# """
# Licensed under the Eiffel Forum License 2.
from __future__ import unicode_literals, absolute_import, print_function, division
import collections import collections
import os import os
import re import re
@ -27,13 +23,6 @@ import loader
LOGGER = get_logger(__name__) LOGGER = get_logger(__name__)
if sys.version_info.major >= 3:
unicode = str
basestring = str
py3 = True
else:
py3 = False
class _CapReq(object): class _CapReq(object):
def __init__(self, prefix, module, failure=None, arg=None, success=None): 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) self._command_groups = collections.defaultdict(list)
"""A mapping of module names to a list of commands in it.""" """A mapping of module names to a list of commands in it."""
self.stats = {} # deprecated, remove in 7.0
self._times = {} self._times = {}
""" """
A dictionary mapping lower-case'd nicks to dictionaries which map A dictionary mapping lower-case'd nicks to dictionaries which map
@ -261,10 +249,6 @@ class Sopel(irc.Bot):
else: else:
self.write(['JOIN', channel, password]) 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): def say(self, text, recipient, max_messages=1):
"""Send ``text`` as a PRIVMSG to ``recipient``. """Send ``text`` as a PRIVMSG to ``recipient``.
@ -288,7 +272,7 @@ class Sopel(irc.Bot):
max_text_length = 400 max_text_length = 400
max_messages=1000 max_messages=1000
# Encode to bytes, for propper length calculation # Encode to bytes, for propper length calculation
if isinstance(text, unicode): if isinstance(text, str):
encoded_text = text.encode('utf-8') encoded_text = text.encode('utf-8')
else: else:
encoded_text = text encoded_text = text
@ -306,7 +290,7 @@ class Sopel(irc.Bot):
excess = encoded_text[last_space + 1:] excess = encoded_text[last_space + 1:]
encoded_text = encoded_text[:last_space] encoded_text = encoded_text[:last_space]
# We'll then send the excess at the end # 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') text = encoded_text.decode('utf-8')
try: try:
self.sending.acquire() 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 # Now that we've sent the first part, we need to send the rest. Doing
# this recursively seems easier to me than iteratively # this recursively seems easier to me than iteratively
if excess: if excess:
self.msg(recipient, excess, max_messages - 1) self.say(recipient, excess, max_messages - 1)
def notice(self, text, dest): def notice(self, text, dest):
"""Send an IRC NOTICE to a user or a channel. """Send an IRC NOTICE to a user or a channel.

View File

@ -37,10 +37,7 @@ def auth_after_register(bot):
"""Do NickServ/AuthServ auth""" """Do NickServ/AuthServ auth"""
if bot.config.core.auth_method == 'nickserv': if bot.config.core.auth_method == 'nickserv':
nickserv_name = bot.config.core.auth_target or 'NickServ' nickserv_name = bot.config.core.auth_target or 'NickServ'
bot.msg( bot.say('IDENTIFY %s' % bot.config.core.auth_password, nickserv_name)
nickserv_name,
'IDENTIFY %s' % bot.config.core.auth_password
)
elif bot.config.core.auth_method == 'authserv': elif bot.config.core.auth_method == 'authserv':
account = bot.config.core.auth_username 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 " "more secure. If you'd like to do this, make sure you're logged in "
"and reply with \"{}useserviceauth\"" "and reply with \"{}useserviceauth\""
).format(bot.config.core.help_prefix) ).format(bot.config.core.help_prefix)
bot.msg(bot.config.core.owner, msg) bot.say(msg, bot.config.core.owner)
@module.require_privmsg() @module.require_privmsg()
@ -262,7 +259,7 @@ def track_nicks(bot, trigger):
debug_msg = ("Nick changed by server. " debug_msg = ("Nick changed by server. "
"This can cause unexpected behavior. Please restart the bot.") "This can cause unexpected behavior. Please restart the bot.")
LOGGER.critical(debug_msg) LOGGER.critical(debug_msg)
bot.msg(bot.config.core.owner, privmsg) bot.say(privmsg, bot.config.core.owner)
return return
for channel in bot.privileges: for channel in bot.privileges:

676
irc.py
View File

@ -1,411 +1,371 @@
# coding=utf-8 #! /usr/bin/env python3
# irc.py - An Utility IRC Bot # -*- coding: utf-8 -*-
# Copyright 2008, Sean B. Palmer, inamidst.com """
# Copyright 2012, Elsie Powell, http://embolalia.com Core IRC functionality. Support for Python 2 is largely gone.
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org> """
#
# 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
import sys import sys
import os
import time import time
import socket import socket
import asyncore
import asynchat
import os
import codecs import codecs
import traceback import traceback
import threading
from datetime import datetime
import errno
import ssl
import asyncore
import asynchat
from logger import get_logger from logger import get_logger
from tools import stderr, Identifier from tools import stderr, Identifier
from trigger import PreTrigger 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__) LOGGER = get_logger(__name__)
class Bot(asynchat.async_chat): class Bot(asynchat.async_chat):
def __init__(self, config): def __init__(self, config):
ca_certs = config.core.ca_certs ca_certs = config.core.ca_certs
asynchat.async_chat.__init__(self) asynchat.async_chat.__init__(self)
self.set_terminator(b'\n') self.set_terminator(b'\n')
self.buffer = '' self.buffer = ''
self.nick = Identifier(config.core.nick) self.nick = Identifier(config.core.nick)
"""Sopel's current ``Identifier``. Changing this while Sopel is running is """Sopel's current ``Identifier``. Changing this while Sopel is running is
untested.""" untested."""
self.user = config.core.user self.user = config.core.user
"""Sopel's user/ident.""" """Sopel's user/ident."""
self.name = config.core.name self.name = config.core.name
"""Sopel's "real name", as used for whois.""" """Sopel's "real name", as used for whois."""
self.stack = {} self.stack = {}
self.ca_certs = ca_certs self.ca_certs = ca_certs
self.enabled_capabilities = set() self.enabled_capabilities = set()
self.hasquit = False self.hasquit = False
self.sending = threading.RLock() self.sending = threading.RLock()
self.writing_lock = threading.Lock() self.writing_lock = threading.Lock()
self.raw = None self.raw = None
# Right now, only accounting for two op levels. # We need this to prevent error loops in handle_error
# This might be expanded later. self.error_count = 0
# 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.connection_registered = False
self.error_count = 0 """ Set to True when a server has accepted the client connection and
messages can be sent and received. """
self.connection_registered = False def log_raw(self, line, prefix):
""" Set to True when a server has accepted the client connection and """Log raw line to the raw log."""
messages can be sent and received. """ 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 f.write(temp)
if not hasattr(self, "connecting"): f.write("\n")
self.connecting = False f.close()
def log_raw(self, line, prefix): def safe(self, string):
"""Log raw line to the raw log.""" """Remove newlines from a string."""
if not self.config.core.log_raw: if isinstance(string, bytes):
return string = string.decode('utf8')
if not os.path.isdir(self.config.core.logdir): string = string.replace('\n', '')
try: string = string.replace('\r', '')
os.mkdir(self.config.core.logdir) return string
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', '')
f.write(temp) def write(self, args, text=None):
f.write("\n") args = [self.safe(arg) for arg in args]
f.close() 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): # From RFC2812 Internet Relay Chat: Client Protocol
"""Remove newlines from a string.""" # Section 2.3
if sys.version_info.major >= 3 and isinstance(string, bytes): #
string = string.decode('utf8') # https://tools.ietf.org/html/rfc2812.html
elif sys.version_info.major < 3: #
if not isinstance(string, unicode): # IRC messages are always lines of characters terminated with a
string = unicode(string, encoding='utf8') # CR-LF (Carriage Return - Line Feed) pair, and these messages SHALL
string = string.replace('\n', '') # NOT exceed 512 characters in length, counting all characters
string = string.replace('\r', '') # including the trailing CR-LF. Thus, there are 510 characters
return string # maximum allowed for the command and its parameters. There is no
# provision for continuation of message lines.
def write(self, args, text=None): if text is not None:
args = [self.safe(arg) for arg in args] temp = (' '.join(args) + ' :' + text)[:510] + '\r\n'
if text is not None: else:
text = self.safe(text) temp = ' '.join(args)[:510] + '\r\n'
try: self.log_raw(temp, '>>')
self.writing_lock.acquire() # Blocking lock, can't send two things self.send(temp.encode('utf-8'))
# at a time finally:
self.writing_lock.release()
# From RFC2812 Internet Relay Chat: Client Protocol def run(self, host, port=6667):
# Section 2.3 try:
# self.initiate_connect(host, port)
# https://tools.ietf.org/html/rfc2812.html except socket.error as e:
# stderr('Connection error: %s' % e)
# 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.
if text is not None: def initiate_connect(self, host, port):
temp = (' '.join(args) + ' :' + text)[:510] + '\r\n' stderr('Connecting to %s:%s...' % (host, port))
else: source_address = ((self.config.core.bind_host, 0)
temp = ' '.join(args)[:510] + '\r\n' if self.config.core.bind_host else None)
self.log_raw(temp, '>>') self.set_socket(socket.create_connection((host, port),
self.send(temp.encode('utf-8')) source_address=source_address))
finally: if self.config.core.use_ssl:
self.writing_lock.release() 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): def quit(self, message):
try: """Disconnect from IRC and close the bot."""
self.initiate_connect(host, port) self.write(['QUIT'], message)
except socket.error as e: self.hasquit = True
stderr('Connection error: %s' % e) # 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): def handle_close(self):
stderr('Connecting to %s:%s...' % (host, port)) self.connection_registered = False
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 quit(self, message): if hasattr(self, '_shutdown'):
"""Disconnect from IRC and close the bot.""" self._shutdown()
self.write(['QUIT'], message) stderr('Closed!')
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 handle_close(self): # This will eventually call asyncore dispatchers close method, which
self.connection_registered = False # will release the main thread. This should be called last to avoid
# race conditions.
self.close()
if hasattr(self, '_shutdown'): def handle_connect(self):
self._shutdown() if self.config.core.use_ssl:
stderr('Closed!') 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 # Request list of server capabilities. IRCv3 servers will respond with
# will release the main thread. This should be called last to avoid # CAP * LS (which we handle in coretasks). v2 servers will respond with
# race conditions. # 421 Unknown command, which we'll ignore
self.close() self.write(('CAP', 'LS', '302'))
def handle_connect(self): if self.config.core.auth_method == 'server':
if self.config.core.use_ssl and has_ssl: password = self.config.core.auth_password
if not self.config.core.verify_ssl: self.write(('PASS', password))
self.ssl = ssl.wrap_socket(self.socket, self.write(('NICK', self.nick))
do_handshake_on_connect=True, self.write(('USER', self.user, '+iw', self.nick), self.name)
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)
# Request list of server capabilities. IRCv3 servers will respond with stderr('Connected.')
# CAP * LS (which we handle in coretasks). v2 servers will respond with self.last_ping_time = datetime.now()
# 421 Unknown command, which we'll ignore timeout_check_thread = threading.Thread(target=self._timeout_check)
self.write(('CAP', 'LS', '302')) 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': def _timeout_check(self):
password = self.config.core.auth_password while self.connected or self.connecting:
self.write(('PASS', password)) if (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout):
self.write(('NICK', self.nick)) stderr('Ping timeout reached after %s seconds, closing connection' % self.config.core.timeout)
self.write(('USER', self.user, '+iw', self.nick), self.name) self.handle_close()
break
else:
time.sleep(int(self.config.core.timeout))
stderr('Connected.') def _send_ping(self):
self.last_ping_time = datetime.now() while self.connected or self.connecting:
timeout_check_thread = threading.Thread(target=self._timeout_check) if self.connected and (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout) / 2:
timeout_check_thread.daemon = True try:
timeout_check_thread.start() self.write(('PING', self.config.core.host))
ping_thread = threading.Thread(target=self._send_ping) except socket.error:
ping_thread.daemon = True pass
ping_thread.start() time.sleep(int(self.config.core.timeout) / 2)
def _timeout_check(self): def _ssl_send(self, data):
while self.connected or self.connecting: """Replacement for self.send() during SSL connections."""
if (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout): try:
stderr('Ping timeout reached after %s seconds, closing connection' % self.config.core.timeout) result = self.socket.send(data)
self.handle_close() return result
break except ssl.SSLError as why:
else: if why[0] in (asyncore.EWOULDBLOCK, errno.ESRCH):
time.sleep(int(self.config.core.timeout)) return 0
else:
raise why
return 0
def _send_ping(self): def _ssl_recv(self, buffer_size):
while self.connected or self.connecting: """Replacement for self.recv() during SSL connections.
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_send(self, data): From: http://evanfosmark.com/2010/09/ssl-support-in-asynchatasync_chat
"""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 _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
""" if data:
try: self.log_raw(data, '<<')
data = self.socket.read(buffer_size) self.buffer += data
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
def collect_incoming_data(self, data): def found_terminator(self):
# We can't trust clients to pass valid unicode. line = self.buffer
try: if line.endswith('\r'):
data = unicode(data, encoding='utf-8') line = line[:-1]
except UnicodeDecodeError: self.buffer = ''
# not unicode, let's try cp1252 self.last_ping_time = datetime.now()
try: pretrigger = PreTrigger(self.nick, line)
data = unicode(data, encoding='cp1252') if all(cap not in self.enabled_capabilities for cap in ['account-tag', 'extended-join']):
except UnicodeDecodeError: pretrigger.tags.pop('account', None)
# Okay, let's try ISO8859-1
try:
data = unicode(data, encoding='iso8859-1')
except:
# Discard line if encoding is unknown
return
if data: if pretrigger.event == 'PING':
self.log_raw(data, '<<') self.write(('PONG', pretrigger.args[-1]))
self.buffer += data 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): self.dispatch(pretrigger)
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 pretrigger.event == 'PING': def dispatch(self, pretrigger):
self.write(('PONG', pretrigger.args[-1])) pass
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()
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): signature = '%s (%s)' % (report[0], report[1])
pass # 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): if trigger and self.config.core.reply_errors and trigger.sender is not None:
"""Called internally when a module causes an error.""" self.msg(trigger.sender, signature)
try: if trigger:
trace = traceback.format_exc() LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(signature), trigger.raw))
if sys.version_info.major < 3: except Exception as e:
trace = trace.decode('utf-8', errors='xmlcharrefreplace') if trigger and self.config.core.reply_errors and trigger.sender is not None:
stderr(trace) self.msg(trigger.sender, "Got an error.")
try: if trigger:
lines = list(reversed(trace.splitlines())) LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(e), trigger.raw))
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')
signature = '%s (%s)' % (report[0], report[1]) def handle_error(self):
# TODO: make not hardcoded """Handle any uncaptured error in the core.
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))
if trigger and self.config.core.reply_errors and trigger.sender is not None: Overrides asyncore's handle_error.
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))
def handle_error(self): """
"""Handle any uncaptured error in the core. trace = traceback.format_exc()
stderr(trace)
Overrides asyncore's handle_error. LOGGER.error('Fatal error in core, please review exception log')
# TODO: make not hardcoded
""" logfile = codecs.open(
trace = traceback.format_exc() os.path.join(self.config.core.logdir, 'exceptions.log'),
stderr(trace) 'a',
LOGGER.error('Fatal error in core, please review exception log') encoding='utf-8'
# TODO: make not hardcoded )
logfile = codecs.open( logfile.write('Fatal error in core, handle_error() was called\n')
os.path.join(self.config.core.logdir, 'exceptions.log'), logfile.write('last raw line was %s' % self.raw)
'a', logfile.write(trace)
encoding='utf-8' logfile.write('Buffer:\n')
) logfile.write(self.buffer)
logfile.write('Fatal error in core, handle_error() was called\n') logfile.write('----------------------------------------\n\n')
logfile.write('last raw line was %s' % self.raw) logfile.close()
logfile.write(trace) if self.error_count > 10:
logfile.write('Buffer:\n') if (datetime.now() - self.last_error_timestamp).seconds < 5:
logfile.write(self.buffer) stderr("Too many errors, can't continue")
logfile.write('----------------------------------------\n\n') os._exit(1)
logfile.close() self.last_error_timestamp = datetime.now()
if self.error_count > 10: self.error_count = self.error_count + 1
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

View File

@ -1,6 +1,8 @@
# coding=utf-8 #! /usr/bin/env python3
from __future__ import unicode_literals, absolute_import, print_function, division # -*- coding: utf-8 -*-
"""
Methods for loading modules.
"""
import imp import imp
import os.path import os.path
import re import re

2
logger.py Normal file → Executable file
View File

@ -13,7 +13,7 @@ class IrcLoggingHandler(logging.Handler):
def emit(self, record): def emit(self, record):
try: try:
msg = self.format(record) msg = self.format(record)
self._bot.msg(self._channel, msg) self._bot.say(msg, self._channel)
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
except: except:

View File

@ -59,7 +59,7 @@ def interval(*args):
@sopel.module.interval(5) @sopel.module.interval(5)
def spam_every_5s(bot): def spam_every_5s(bot):
if "#here" in bot.channels: 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): def add_attribute(function):

View File

@ -98,7 +98,7 @@ def msg(bot, trigger):
if not channel or not message: if not channel or not message:
return return
bot.msg(channel, message) bot.say(message, channel)
@module.require_privmsg @module.require_privmsg
@ -119,7 +119,7 @@ def me(bot, trigger):
return return
msg = '\x01ACTION %s\x01' % action msg = '\x01ACTION %s\x01' % action
bot.msg(channel, msg) bot.say(msg, channel)
@module.event('INVITE') @module.event('INVITE')

View File

@ -127,62 +127,6 @@ def unban(bot, trigger):
bot.write(['MODE', channel, '-b', banmask]) 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_chanmsg
@require_privilege(OP, 'You are not a channel operator.') @require_privilege(OP, 'You are not a channel operator.')
@commands('kickban', 'kb') @commands('kickban', 'kb')

View File

@ -18,5 +18,5 @@ def announce(bot, trigger):
bot.reply('Sorry, I can\'t let you do that') bot.reply('Sorry, I can\'t let you do that')
return return
for channel in bot.channels: 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.') bot.reply('Announce complete.')

View File

@ -4,6 +4,7 @@
ASCII ASCII
""" """
from io import BytesIO from io import BytesIO
import argparse
import requests import requests
from PIL import Image, ImageFont, ImageDraw 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) image = image.convert(image.palette.mode)
if image.mode == "RGBA": if image.mode == "RGBA":
image = alpha_composite(image).convert("RGB") image = alpha_composite(image).convert("RGB")
if image.mode == "L":
image = image.convert("RGB")
image = scale_image(image) image = scale_image(image)

View File

@ -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)

View File

@ -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)

View File

@ -83,15 +83,15 @@ def f_etymology(bot, trigger):
result = etymology(word) result = etymology(word)
except IOError: except IOError:
msg = "Can't connect to etymonline.com (%s)" % (etyuri % word) msg = "Can't connect to etymonline.com (%s)" % (etyuri % word)
bot.msg(trigger.sender, msg) bot.say(msg)
return NOLIMIT return NOLIMIT
except (AttributeError, TypeError): except (AttributeError, TypeError):
result = None result = None
if result is not None: if result is not None:
bot.msg(trigger.sender, result) bot.say(result)
else: else:
uri = etysearch % word uri = etysearch % word
msg = 'Can\'t find the etymology for "%s". Try %s' % (word, uri) msg = 'Can\'t find the etymology for "%s". Try %s' % (word, uri)
bot.msg(trigger.sender, msg) bot.say(msg)
return NOLIMIT return NOLIMIT

28
modules/grog.py Executable file
View File

@ -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))

View File

@ -8,18 +8,8 @@ Licensed under the Eiffel Forum License 2.
http://sopel.chat 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 from module import commands, rule, example, priority
logger = get_logger(__name__)
@rule('$nick' '(?i)(help|doc) +([A-Za-z]+)(?:\?+)?$') @rule('$nick' '(?i)(help|doc) +([A-Za-z]+)(?:\?+)?$')
@example('.help tell') @example('.help tell')
@commands('help', 'commands') @commands('help', 'commands')
@ -30,23 +20,29 @@ def help(bot, trigger):
name = trigger.group(2) name = trigger.group(2)
name = name.lower() name = name.lower()
if name in bot.doc: if name not in bot.doc:
print(bot.doc[name]) return
newlines = [''] newlines = ['']
lines = list(filter(None, bot.doc[name][0])) lines = list(filter(None, bot.doc[name][0]))
lines = list(map(str.strip, lines)) lines = list(map(str.strip, lines))
for line in lines: for line in lines:
newlines[-1] = newlines[-1] + ' ' + line newlines[-1] = newlines[-1] + ' ' + line
if line[-1] is '.': if line[-1] is '.':
newlines.append('') newlines.append('')
newlines = list(map(str.strip, newlines)) newlines = list(map(str.strip, newlines))
if bot.doc[name][1]: if bot.doc[name][1]:
newlines.append('Ex. ' + bot.doc[name][1]) newlines.append('Ex. ' + bot.doc[name][1])
for msg in newlines: for msg in newlines:
bot.say(msg) bot.say(msg)
else: else:
helps = list(bot.command_groups) command_groups = list(bot.command_groups.values())
helps.sort() commands = []
msg = "Available commands: " + ', '.join(helps) for group in command_groups:
if type(group) == list:
commands += group
else:
commands += [group]
commands.sort()
msg = "Available commands: " + ', '.join(commands)
bot.say(msg) bot.say(msg)

View File

@ -43,7 +43,7 @@ def roomTemp(bot, trigger):
@module.require_admin @module.require_admin
@module.commands('inkwrite') @module.commands('inkwrite')
def roomTemp(bot, trigger): def inkWrite(bot, trigger):
""" """
Writes shit to my e-ink screen. Writes shit to my e-ink screen.
""" """

View File

@ -1,432 +0,0 @@
# 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.')

View File

@ -123,13 +123,13 @@ def pickMovie(bot, trigger):
bot.memory['movie_lock'].acquire() bot.memory['movie_lock'].acquire()
conn = bot.db.connect() conn = bot.db.connect()
cur = conn.cursor() cur = conn.cursor()
cur.execute("SELECT * FROM movie WHERE times_watched < 1 AND shitpost = 0") cur.execute("SELECT movie_title FROM movie WHERE " + \
movieList = cur.fetchall() "times_watched < 1 AND shitpost = 0 ORDER BY RANDOM() LIMIT 1;")
movie = cur.fetchone()
conn.close() conn.close()
roll = random.randint(0, len(movieList)-1)
bot.memory['movie_lock'].release() bot.memory['movie_lock'].release()
bot.reply(movieList[roll][0]) bot.reply(movie[0])
@module.require_admin @module.require_admin

View File

@ -70,9 +70,9 @@ def setup(bot):
for oldtime in oldtimes: for oldtime in oldtimes:
for (channel, nick, message) in bot.rdb[oldtime]: for (channel, nick, message) in bot.rdb[oldtime]:
if message: if message:
bot.msg(channel, nick + ': ' + message) bot.say(nick + ': ' + message)
else: else:
bot.msg(channel, nick + '!') bot.say(nick + '!')
del bot.rdb[oldtime] del bot.rdb[oldtime]
dump_database(bot.rfn, bot.rdb) dump_database(bot.rfn, bot.rdb)
time.sleep(2.5) time.sleep(2.5)

View File

@ -11,118 +11,104 @@ import json
import sys import sys
if sys.version_info.major < 3: if sys.version_info.major < 3:
from urllib import quote_plus from urllib import quote_plus
else: else:
from urllib.parse import quote_plus from urllib.parse import quote_plus
def formatnumber(n): def formatnumber(n):
"""Format a number with beautiful commas.""" """Format a number with beautiful commas."""
parts = list(str(n)) parts = list(str(n))
for i in range((len(parts) - 3), 0, -3): for i in range((len(parts) - 3), 0, -3):
parts.insert(i, ',') parts.insert(i, ',')
return ''.join(parts) return ''.join(parts)
r_bing = re.compile(r'<h3><a href="([^"]+)"') r_bing = re.compile(r'<h3><a href="([^"]+)"')
def bing_search(query, lang='en-GB'): def bing_search(query, lang='en-GB'):
base = 'http://www.bing.com/search?mkt=%s&q=' % lang base = 'http://www.bing.com/search?mkt=%s&q=' % lang
bytes = requests.get(base + query) bytes = requests.get(base + query)
m = r_bing.search(bytes) m = r_bing.search(bytes)
if m: if m:
return m.group(1) return m.group(1)
r_duck = re.compile(r'nofollow" class="[^"]+" href="(?!https?:\/\/r\.search\.yahoo)(.*?)">') r_duck = re.compile(r'nofollow" class="[^"]+" href="(?!https?:\/\/r\.search\.yahoo)(.*?)">')
def duck_search(query): def duck_search(query):
query = query.replace('!', '') query = query.replace('!', '')
uri = 'http://duckduckgo.com/html/?q=%s&kl=uk-en' % query uri = 'http://duckduckgo.com/html/?q=%s&kl=uk-en' % query
bytes = requests.get(uri) bytes = requests.get(uri)
if 'requests-result' in bytes: # filter out the adds on top of the page if 'requests-result' in bytes: # filter out the adds on top of the page
bytes = bytes.split('requests-result')[1] bytes = bytes.split('requests-result')[1]
m = r_duck.search(bytes) m = r_duck.search(bytes)
if m: if m:
return requests.decode(m.group(1)) return requests.decode(m.group(1))
# Alias google_search to duck_search # Alias google_search to duck_search
google_search = duck_search google_search = duck_search
def duck_api(query): def duck_api(query):
if '!bang' in query.lower(): if '!bang' in query.lower():
return 'https://duckduckgo.com/bang.html' return 'https://duckduckgo.com/bang.html'
# This fixes issue #885 (https://github.com/sopel-irc/sopel/issues/885) # This fixes issue #885 (https://github.com/sopel-irc/sopel/issues/885)
# It seems that duckduckgo api redirects to its Instant answer API html page # It seems that duckduckgo api redirects to its Instant answer API html page
# if the query constains special charactares that aren't urlencoded. # if the query constains special charactares that aren't urlencoded.
# So in order to always get a JSON response back the query is urlencoded # So in order to always get a JSON response back the query is urlencoded
query = quote_plus(query) query = quote_plus(query)
uri = 'http://api.duckduckgo.com/?q=%s&format=json&no_html=1&no_redirect=1' % query uri = 'http://api.duckduckgo.com/?q=%s&format=json&no_html=1&no_redirect=1' % query
results = json.loads(requests.get(uri)) results = json.loads(requests.get(uri))
if results['Redirect']: if results['Redirect']:
return results['Redirect'] return results['Redirect']
else: else:
return None return None
@commands('duck', 'ddg', 'g') @commands('duck', 'ddg', 'g')
@example('.duck privacy or .duck !mcwiki obsidian') @example('.duck privacy or .duck !mcwiki obsidian')
def duck(bot, trigger): def duck(bot, trigger):
"""Queries Duck Duck Go for the specified input.""" """Queries Duck Duck Go for the specified input."""
query = trigger.group(2) query = trigger.group(2)
if not query: if not query:
return bot.reply('.ddg what?') return bot.reply('.ddg what?')
# If the API gives us something, say it and stop # If the API gives us something, say it and stop
result = duck_api(query) result = duck_api(query)
if result: if result:
bot.reply(result) bot.reply(result)
return return
# Otherwise, look it up on the HTMl version # Otherwise, look it up on the HTMl version
uri = duck_search(query) uri = duck_search(query)
if uri: if uri:
bot.reply(uri) bot.reply(uri)
if 'last_seen_url' in bot.memory: if 'last_seen_url' in bot.memory:
bot.memory['last_seen_url'][trigger.sender] = uri bot.memory['last_seen_url'][trigger.sender] = uri
else: else:
bot.reply("No results found for '%s'." % query) bot.reply("No results found for '%s'." % query)
@commands('search') @commands('search')
@example('.search nerdfighter') @example('.search nerdfighter')
def search(bot, trigger): def search(bot, trigger):
"""Searches Bing and Duck Duck Go.""" """Searches Bing and Duck Duck Go."""
if not trigger.group(2): if not trigger.group(2):
return bot.reply('.search for what?') return bot.reply('.search for what?')
query = trigger.group(2) query = trigger.group(2)
bu = bing_search(query) or '-' bu = bing_search(query) or '-'
du = duck_search(query) or '-' du = duck_search(query) or '-'
if bu == du: if bu == du:
result = '%s (b, d)' % bu result = '%s (b, d)' % bu
else: else:
if len(bu) > 150: if len(bu) > 150:
bu = '(extremely long link)' bu = '(extremely long link)'
if len(du) > 150: if len(du) > 150:
du = '(extremely long link)' du = '(extremely long link)'
result = '%s (b), %s (d)' % (bu, du) result = '%s (b), %s (d)' % (bu, du)
bot.reply(result) bot.reply(result)
@commands('suggest')
def suggest(bot, trigger):
"""Suggest terms starting with given input"""
if not trigger.group(2):
return bot.reply("No query term.")
query = trigger.group(2)
uri = 'http://requestssitedev.de/temp-bin/suggest.pl?q='
answer = requests.get(uri + query.replace('+', '%2B'))
if answer:
bot.say(answer)
else:
bot.reply('Sorry, no result.')

View File

@ -5,6 +5,8 @@ When was this user last seen.
""" """
import time import time
import datetime import datetime
import argparse
from tools import Identifier from tools import Identifier
from tools.time import get_timezone, format_time, relativeTime from tools.time import get_timezone, format_time, relativeTime
from module import commands, rule, priority, thread from module import commands, rule, priority, thread
@ -13,10 +15,16 @@ from module import commands, rule, priority, thread
@commands('seen') @commands('seen')
def seen(bot, trigger): def seen(bot, trigger):
"""Reports when and where the user was last seen.""" """Reports when and where the user was last seen."""
if not trigger.group(2): parser = argparse.ArgumentParser()
bot.say(".seen <nick> - Reports when <nick> was last seen.") parser.add_argument("nick")
return parser.add_argument("-l", "--last", action="store_true")
nick = trigger.group(2).strip() args = parser.parse_args(trigger.group(2).split())
# if not trigger.group(2):
# bot.say(".seen <nick> - Reports when <nick> was last seen.")
# return
# nick = trigger.group(2).strip()
nick = args.nick
if nick == bot.nick: if nick == bot.nick:
bot.reply("I'm right here!") bot.reply("I'm right here!")
return return
@ -34,6 +42,9 @@ def seen(bot, trigger):
reltime = relativeTime(bot, nick, timestamp) reltime = relativeTime(bot, nick, timestamp)
msg = "Last heard from \x0308{}\x03 at {} (\x0312{}\x03) in \x0312{}".format(nick, timestamp, reltime, channel) msg = "Last heard from \x0308{}\x03 at {} (\x0312{}\x03) in \x0312{}".format(nick, timestamp, reltime, channel)
if args.last:
msg += "\x03 with \"\x0308{}\x03\"".format(message)
bot.reply(msg) bot.reply(msg)
else: else:
bot.say("I haven't seen \x0308{}".format(nick)) bot.say("I haven't seen \x0308{}".format(nick))

View File

@ -124,7 +124,7 @@ def f_remind(bot, trigger):
dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell
def getReminders(bot, channel, key, tellee): def getReminders(bot, key, tellee):
lines = [] lines = []
template = "%s: \x0310%s\x03 (\x0308%s\x03) %s [\x0312%s\x03]" template = "%s: \x0310%s\x03 (\x0308%s\x03) %s [\x0312%s\x03]"
@ -136,7 +136,7 @@ def getReminders(bot, channel, key, tellee):
try: try:
del bot.memory['reminders'][key] del bot.memory['reminders'][key]
except KeyError: except KeyError:
bot.msg(channel, 'Er...') bot.say('Er...')
finally: finally:
bot.memory['tell_lock'].release() bot.memory['tell_lock'].release()
return lines return lines
@ -147,7 +147,6 @@ def getReminders(bot, channel, key, tellee):
def message(bot, trigger): def message(bot, trigger):
tellee = trigger.nick tellee = trigger.nick
channel = trigger.sender
if not os.path.exists(bot.tell_filename): if not os.path.exists(bot.tell_filename):
return return
@ -158,9 +157,9 @@ def message(bot, trigger):
for remkey in remkeys: for remkey in remkeys:
if not remkey.endswith('*') or remkey.endswith(':'): if not remkey.endswith('*') or remkey.endswith(':'):
if tellee == remkey: if tellee == remkey:
reminders.extend(getReminders(bot, channel, remkey, tellee)) reminders.extend(getReminders(bot, remkey, tellee))
elif tellee.startswith(remkey.rstrip('*:')): elif tellee.startswith(remkey.rstrip('*:')):
reminders.extend(getReminders(bot, channel, remkey, tellee)) reminders.extend(getReminders(bot, remkey, tellee))
for line in reminders[:maximum]: for line in reminders[:maximum]:
bot.say(line) bot.say(line)
@ -168,7 +167,7 @@ def message(bot, trigger):
if reminders[maximum:]: if reminders[maximum:]:
bot.say('Further messages sent privately') bot.say('Further messages sent privately')
for line in reminders[maximum:]: for line in reminders[maximum:]:
bot.msg(tellee, line) bot.say(line, tellee)
if len(bot.memory['reminders'].keys()) != remkeys: if len(bot.memory['reminders'].keys()) != remkeys:
dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell

View File

@ -15,6 +15,7 @@ if sys.version_info.major >= 3:
@example('.u ‽', 'U+203D INTERROBANG (‽)') @example('.u ‽', 'U+203D INTERROBANG (‽)')
@example('.u 203D', 'U+203D INTERROBANG (‽)') @example('.u 203D', 'U+203D INTERROBANG (‽)')
def codepoint(bot, trigger): def codepoint(bot, trigger):
"""Looks up unicode information."""
arg = trigger.group(2) arg = trigger.group(2)
if not arg: if not arg:
bot.reply('What code point do you want me to look up?') bot.reply('What code point do you want me to look up?')

View File

@ -122,7 +122,7 @@ def weather(bot, trigger):
if not location: if not location:
woeid = bot.db.get_nick_value(trigger.nick, 'woeid') woeid = bot.db.get_nick_value(trigger.nick, 'woeid')
if not woeid: if not woeid:
return bot.msg(trigger.sender, "I don't know where you live. " + return bot.say("I don't know where you live. " +
'Give me a location, like .weather London, or tell me where you live by saying .setlocation London, for example.') 'Give me a location, like .weather London, or tell me where you live by saying .setlocation London, for example.')
else: else:
location = location.strip() location = location.strip()

View File

@ -16,6 +16,9 @@ import wolframalpha
@example('.wa 2+2', '[W|A] 2+2 = 4') @example('.wa 2+2', '[W|A] 2+2 = 4')
@example('.wa python language release date', '[W|A] Python | date introduced = 1991') @example('.wa python language release date', '[W|A] Python | date introduced = 1991')
def wa_command(bot, trigger): def wa_command(bot, trigger):
"""
Queries WolframAlpha.
"""
msg = None msg = None
if not trigger.group(2): if not trigger.group(2):
msg = 'You must provide a query.' msg = 'You must provide a query.'

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python2.7 #! /usr/bin/env python3
# coding=utf-8 # -*- coding: utf-8 -*-
""" """
Sopel - An IRC Bot Sopel - An IRC Bot
Copyright 2008, Sean B. Palmer, inamidst.com Copyright 2008, Sean B. Palmer, inamidst.com
@ -8,18 +8,8 @@ Licensed under the Eiffel Forum License 2.
http://sopel.chat http://sopel.chat
""" """
from __future__ import unicode_literals, absolute_import, print_function, division
import sys import sys
from tools import stderr from tools import stderr
if sys.version_info < (2, 7):
stderr('Error: Requires Python 2.7 or later. Try python2.7 sopel')
sys.exit(1)
if sys.version_info.major == 3 and sys.version_info.minor < 3:
stderr('Error: When running on Python 3, Python 3.3 is required.')
sys.exit(1)
import os import os
import argparse import argparse
import signal import signal
@ -32,174 +22,209 @@ homedir = os.path.join(os.path.expanduser('~'), '.sopel')
def enumerate_configs(extension='.cfg'): def enumerate_configs(extension='.cfg'):
configfiles = [] configfiles = []
if os.path.isdir(homedir): if os.path.isdir(homedir):
sopel_dotdirfiles = os.listdir(homedir) # Preferred sopel_dotdirfiles = os.listdir(homedir) # Preferred
for item in sopel_dotdirfiles: for item in sopel_dotdirfiles:
if item.endswith(extension): if item.endswith(extension):
configfiles.append(item) configfiles.append(item)
return configfiles return configfiles
def find_config(name, extension='.cfg'): def find_config(name, extension='.cfg'):
if os.path.isfile(name): if os.path.isfile(name):
return name return name
configs = enumerate_configs(extension) configs = enumerate_configs(extension)
if name in configs or name + extension in configs: if name in configs or name + extension in configs:
if name + extension in configs: if name + extension in configs:
name = name + extension name = name + extension
return os.path.join(homedir, name) return os.path.join(homedir, name)
def main(argv=None): def main(argv=None):
global homedir global homedir
# Step One: Parse The Command Line # Step One: Parse The Command Line
try: try:
parser = argparse.ArgumentParser(description='Sopel IRC Bot', parser = argparse.ArgumentParser(
usage='%(prog)s [options]') description='Sopel IRC Bot',
parser.add_argument('-c', '--config', metavar='filename', usage='%(prog)s [options]')
help='use a specific configuration file') parser.add_argument(
parser.add_argument("-d", '--fork', action="store_true", '-c',
dest="daemonize", help="Daemonize sopel") '--config',
parser.add_argument("-q", '--quit', action="store_true", dest="quit", metavar='filename',
help="Gracefully quit Sopel") help='use a specific configuration file')
parser.add_argument("-k", '--kill', action="store_true", dest="kill", parser.add_argument(
help="Kill Sopel") "-d",
parser.add_argument("-l", '--list', action="store_true", '--fork',
dest="list_configs", action="store_true",
help="List all config files found") dest="daemonize",
parser.add_argument("-m", '--migrate', action="store_true", help="Daemonize sopel")
dest="migrate_configs", parser.add_argument(
help="Migrate config files to the new format") "-q",
parser.add_argument('--quiet', action="store_true", dest="quiet", '--quit',
help="Supress all output") action="store_true",
parser.add_argument('-w', '--configure-all', action='store_true', dest="quit",
dest='wizard', help='Run the configuration wizard.') help="Gracefully quit Sopel")
parser.add_argument('--configure-modules', action='store_true', parser.add_argument(
dest='mod_wizard', help=( "-k",
'Run the configuration wizard, but only for the ' '--kill',
'module configuration options.')) action="store_true",
parser.add_argument('-v', '--version', action="store_true", dest="kill",
dest="version", help="Show version number and exit") help="Kill Sopel")
if argv: parser.add_argument(
opts = parser.parse_args(argv) "-l",
else: '--list',
opts = parser.parse_args() action="store_true",
dest="list_configs",
help="List all config files found")
parser.add_argument(
"-m",
'--migrate',
action="store_true",
dest="migrate_configs",
help="Migrate config files to the new format")
parser.add_argument(
'--quiet',
action="store_true",
dest="quiet",
help="Supress all output")
parser.add_argument(
'-w',
'--configure-all',
action='store_true',
dest='wizard',
help='Run the configuration wizard.')
parser.add_argument(
'--configure-modules',
action='store_true',
dest='mod_wizard',
help='Run the configuration wizard, but only for the module ' + \
'configuration options.')
parser.add_argument(
'-v',
'--version',
action="store_true",
dest="version",
help="Show version number and exit")
if argv:
args = parser.parse_args(argv)
else:
args = parser.parse_args()
# Step Two: "Do not run as root" checks. # Step Two: "Do not run as root" checks.
try: try:
# Linux/Mac # Linux/Mac
if os.getuid() == 0 or os.geteuid() == 0: if os.getuid() == 0 or os.geteuid() == 0:
stderr('Error: Do not run Sopel with root privileges.') stderr('Error: Do not run Sopel with root privileges.')
sys.exit(1) sys.exit(1)
except AttributeError: except AttributeError:
# Windows # Windows
if os.environ.get("USERNAME") == "Administrator": if os.environ.get("USERNAME") == "Administrator":
stderr('Error: Do not run Sopel as Administrator.') stderr('Error: Do not run Sopel as Administrator.')
sys.exit(1) sys.exit(1)
if opts.version: if args.version:
py_ver = '%s.%s.%s' % (sys.version_info.major, py_ver = '%s.%s.%s' % (sys.version_info.major,
sys.version_info.minor, sys.version_info.minor,
sys.version_info.micro) sys.version_info.micro)
print('Sopel %s (running on python %s)' % (__version__, py_ver)) print('Sopel %s (running on python %s)' % (__version__, py_ver))
print('http://sopel.chat/') print('http://sopel.chat/')
return return
elif opts.wizard: elif args.wizard:
_wizard('all', opts.config) _wizard('all', args.config)
return return
elif opts.mod_wizard: elif args.mod_wizard:
_wizard('mod', opts.config) _wizard('mod', args.config)
return return
if opts.list_configs: if args.list_configs:
configs = enumerate_configs() configs = enumerate_configs()
print('Config files in ~/.sopel:') print('Config files in ~/.sopel:')
if len(configs) is 0: if len(configs) is 0:
print('\tNone found') print('\tNone found')
else: else:
for config in configs: for config in configs:
print('\t%s' % config) print('\t%s' % config)
print('-------------------------') print('-------------------------')
return return
config_name = opts.config or 'default' config_name = args.config or 'default'
configpath = find_config(config_name) configpath = find_config(config_name)
if not os.path.isfile(configpath): if not os.path.isfile(configpath):
print("Welcome to Sopel!\nI can't seem to find the configuration file, so let's generate it!\n") print("Welcome to Sopel!\nI can't seem to find the configuration file, so let's generate it!\n")
if not configpath.endswith('.cfg'): if not configpath.endswith('.cfg'):
configpath = configpath + '.cfg' configpath = configpath + '.cfg'
_create_config(configpath) _create_config(configpath)
configpath = find_config(config_name) configpath = find_config(config_name)
try: try:
config_module = Config(configpath) config_module = Config(configpath)
except ConfigurationError as e: except ConfigurationError as e:
stderr(e) stderr(e)
sys.exit(2) sys.exit(2)
if config_module.core.not_configured: if config_module.core.not_configured:
stderr('Bot is not configured, can\'t start') stderr('Bot is not configured, can\'t start')
# exit with code 2 to prevent auto restart on fail by systemd # exit with code 2 to prevent auto restart on fail by systemd
sys.exit(2) sys.exit(2)
logfile = os.path.os.path.join(config_module.core.logdir, 'stdio.log') logfile = os.path.os.path.join(config_module.core.logdir, 'stdio.log')
config_module._is_daemonized = opts.daemonize config_module._is_daemonized = args.daemonize
sys.stderr = tools.OutputRedirect(logfile, True, opts.quiet) sys.stderr = tools.OutputRedirect(logfile, True, args.quiet)
sys.stdout = tools.OutputRedirect(logfile, False, opts.quiet) sys.stdout = tools.OutputRedirect(logfile, False, args.quiet)
# Handle --quit, --kill and saving the PID to file # Handle --quit, --kill and saving the PID to file
pid_dir = config_module.core.pid_dir pid_dir = config_module.core.pid_dir
if opts.config is None: if args.config is None:
pid_file_path = os.path.join(pid_dir, 'sopel.pid') pid_file_path = os.path.join(pid_dir, 'sopel.pid')
else: else:
basename = os.path.basename(opts.config) basename = os.path.basename(args.config)
if basename.endswith('.cfg'): if basename.endswith('.cfg'):
basename = basename[:-4] basename = basename[:-4]
pid_file_path = os.path.join(pid_dir, 'sopel-%s.pid' % basename) pid_file_path = os.path.join(pid_dir, 'sopel-%s.pid' % basename)
if os.path.isfile(pid_file_path): if os.path.isfile(pid_file_path):
with open(pid_file_path, 'r') as pid_file: with open(pid_file_path, 'r') as pid_file:
try: try:
old_pid = int(pid_file.read()) old_pid = int(pid_file.read())
except ValueError: except ValueError:
old_pid = None old_pid = None
if old_pid is not None and tools.check_pid(old_pid): if old_pid is not None and tools.check_pid(old_pid):
if not opts.quit and not opts.kill: if not args.quit and not args.kill:
stderr('There\'s already a Sopel instance running with this config file') stderr('There\'s already a Sopel instance running with this config file')
stderr('Try using the --quit or the --kill options') stderr('Try using the --quit or the --kill options')
sys.exit(1) sys.exit(1)
elif opts.kill: elif args.kill:
stderr('Killing the sopel') stderr('Killing the sopel')
os.kill(old_pid, signal.SIGKILL) os.kill(old_pid, signal.SIGKILL)
sys.exit(0) sys.exit(0)
elif opts.quit: elif args.quit:
stderr('Signaling Sopel to stop gracefully') stderr('Signaling Sopel to stop gracefully')
if hasattr(signal, 'SIGUSR1'): if hasattr(signal, 'SIGUSR1'):
os.kill(old_pid, signal.SIGUSR1) os.kill(old_pid, signal.SIGUSR1)
else: else:
os.kill(old_pid, signal.SIGTERM) os.kill(old_pid, signal.SIGTERM)
sys.exit(0) sys.exit(0)
elif opts.kill or opts.quit: elif args.kill or args.quit:
stderr('Sopel is not running!') stderr('Sopel is not running!')
sys.exit(1) sys.exit(1)
elif opts.quit or opts.kill: elif args.quit or args.kill:
stderr('Sopel is not running!') stderr('Sopel is not running!')
sys.exit(1) sys.exit(1)
if opts.daemonize: if args.daemonize:
child_pid = os.fork() child_pid = os.fork()
if child_pid is not 0: if child_pid is not 0:
sys.exit() sys.exit()
with open(pid_file_path, 'w') as pid_file: with open(pid_file_path, 'w') as pid_file:
pid_file.write(str(os.getpid())) pid_file.write(str(os.getpid()))
# Step Five: Initialise And Run sopel # Step Five: Initialise And Run sopel
run(config_module, pid_file_path) run(config_module, pid_file_path)
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n\nInterrupted") print("\n\nInterrupted")
os._exit(1) os._exit(1)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

3
sopel
View File

@ -1,5 +1,4 @@
#!/usr/bin/python3 #! /usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
import sys import sys