some refactoring, removed some useless stuff, added some useless stuff
This commit is contained in:
parent
04cc2a2084
commit
e6e8d544d2
32
bot.py
32
bot.py
|
@ -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.
|
||||
|
|
|
@ -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
676
irc.py
|
@ -1,411 +1,371 @@
|
|||
# coding=utf-8
|
||||
# irc.py - An Utility IRC Bot
|
||||
# Copyright 2008, Sean B. Palmer, inamidst.com
|
||||
# Copyright 2012, Elsie Powell, http://embolalia.com
|
||||
# Copyright © 2012, Elad Alfassa <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
|
||||
|
|
|
@ -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
2
logger.py
Normal file → Executable 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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
276
modules/clock.py
276
modules/clock.py
|
@ -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)
|
|
@ -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
28
modules/grog.py
Executable 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))
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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.')
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.'
|
||||
|
|
351
run_script.py
351
run_script.py
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue
Block a user