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
# Copyright 2008, Sean B. Palmer, inamidst.com
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
# 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.

View File

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

676
irc.py
View File

@ -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 <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
#! /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

View File

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

2
logger.py Normal file → Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -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.
"""

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()
conn = bot.db.connect()
cur = conn.cursor()
cur.execute("SELECT * FROM movie WHERE times_watched < 1 AND shitpost = 0")
movieList = cur.fetchall()
cur.execute("SELECT movie_title FROM movie WHERE " + \
"times_watched < 1 AND shitpost = 0 ORDER BY RANDOM() LIMIT 1;")
movie = cur.fetchone()
conn.close()
roll = random.randint(0, len(movieList)-1)
bot.memory['movie_lock'].release()
bot.reply(movieList[roll][0])
bot.reply(movie[0])
@module.require_admin

View File

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

View File

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

View File

@ -5,6 +5,8 @@ When was this user last seen.
"""
import time
import datetime
import argparse
from tools import Identifier
from tools.time import get_timezone, format_time, relativeTime
from module import commands, rule, priority, thread
@ -13,10 +15,16 @@ from module import commands, rule, priority, thread
@commands('seen')
def seen(bot, trigger):
"""Reports when and where the user was last seen."""
if not trigger.group(2):
bot.say(".seen <nick> - Reports when <nick> was last seen.")
return
nick = trigger.group(2).strip()
parser = argparse.ArgumentParser()
parser.add_argument("nick")
parser.add_argument("-l", "--last", action="store_true")
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:
bot.reply("I'm right here!")
return
@ -34,6 +42,9 @@ def seen(bot, trigger):
reltime = relativeTime(bot, nick, timestamp)
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)
else:
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
def getReminders(bot, channel, key, tellee):
def getReminders(bot, key, tellee):
lines = []
template = "%s: \x0310%s\x03 (\x0308%s\x03) %s [\x0312%s\x03]"
@ -136,7 +136,7 @@ def getReminders(bot, channel, key, tellee):
try:
del bot.memory['reminders'][key]
except KeyError:
bot.msg(channel, 'Er...')
bot.say('Er...')
finally:
bot.memory['tell_lock'].release()
return lines
@ -147,7 +147,6 @@ def getReminders(bot, channel, key, tellee):
def message(bot, trigger):
tellee = trigger.nick
channel = trigger.sender
if not os.path.exists(bot.tell_filename):
return
@ -158,9 +157,9 @@ def message(bot, trigger):
for remkey in remkeys:
if not remkey.endswith('*') or remkey.endswith(':'):
if tellee == remkey:
reminders.extend(getReminders(bot, channel, remkey, tellee))
reminders.extend(getReminders(bot, remkey, tellee))
elif tellee.startswith(remkey.rstrip('*:')):
reminders.extend(getReminders(bot, channel, remkey, tellee))
reminders.extend(getReminders(bot, remkey, tellee))
for line in reminders[:maximum]:
bot.say(line)
@ -168,7 +167,7 @@ def message(bot, trigger):
if reminders[maximum:]:
bot.say('Further messages sent privately')
for line in reminders[maximum:]:
bot.msg(tellee, line)
bot.say(line, tellee)
if len(bot.memory['reminders'].keys()) != remkeys:
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 203D', 'U+203D INTERROBANG (‽)')
def codepoint(bot, trigger):
"""Looks up unicode information."""
arg = trigger.group(2)
if not arg:
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:
woeid = bot.db.get_nick_value(trigger.nick, '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.')
else:
location = location.strip()

View File

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

View File

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

3
sopel
View File

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