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
|
#! /usr/bin/env python3
|
||||||
# Copyright 2008, Sean B. Palmer, inamidst.com
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
|
"""
|
||||||
# Copyright 2012-2015, Elsie Powell, http://embolalia.com
|
The core bot class. Say good bye to PYthon 2.
|
||||||
#
|
"""
|
||||||
# Licensed under the Eiffel Forum License 2.
|
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import, print_function, division
|
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -27,13 +23,6 @@ import loader
|
||||||
|
|
||||||
LOGGER = get_logger(__name__)
|
LOGGER = get_logger(__name__)
|
||||||
|
|
||||||
if sys.version_info.major >= 3:
|
|
||||||
unicode = str
|
|
||||||
basestring = str
|
|
||||||
py3 = True
|
|
||||||
else:
|
|
||||||
py3 = False
|
|
||||||
|
|
||||||
|
|
||||||
class _CapReq(object):
|
class _CapReq(object):
|
||||||
def __init__(self, prefix, module, failure=None, arg=None, success=None):
|
def __init__(self, prefix, module, failure=None, arg=None, success=None):
|
||||||
|
@ -70,7 +59,6 @@ class Sopel(irc.Bot):
|
||||||
"""
|
"""
|
||||||
self._command_groups = collections.defaultdict(list)
|
self._command_groups = collections.defaultdict(list)
|
||||||
"""A mapping of module names to a list of commands in it."""
|
"""A mapping of module names to a list of commands in it."""
|
||||||
self.stats = {} # deprecated, remove in 7.0
|
|
||||||
self._times = {}
|
self._times = {}
|
||||||
"""
|
"""
|
||||||
A dictionary mapping lower-case'd nicks to dictionaries which map
|
A dictionary mapping lower-case'd nicks to dictionaries which map
|
||||||
|
@ -261,10 +249,6 @@ class Sopel(irc.Bot):
|
||||||
else:
|
else:
|
||||||
self.write(['JOIN', channel, password])
|
self.write(['JOIN', channel, password])
|
||||||
|
|
||||||
def msg(self, recipient, text, max_messages=1):
|
|
||||||
# Deprecated, but way too much of a pain to remove.
|
|
||||||
self.say(text, recipient, max_messages)
|
|
||||||
|
|
||||||
def say(self, text, recipient, max_messages=1):
|
def say(self, text, recipient, max_messages=1):
|
||||||
"""Send ``text`` as a PRIVMSG to ``recipient``.
|
"""Send ``text`` as a PRIVMSG to ``recipient``.
|
||||||
|
|
||||||
|
@ -288,7 +272,7 @@ class Sopel(irc.Bot):
|
||||||
max_text_length = 400
|
max_text_length = 400
|
||||||
max_messages=1000
|
max_messages=1000
|
||||||
# Encode to bytes, for propper length calculation
|
# Encode to bytes, for propper length calculation
|
||||||
if isinstance(text, unicode):
|
if isinstance(text, str):
|
||||||
encoded_text = text.encode('utf-8')
|
encoded_text = text.encode('utf-8')
|
||||||
else:
|
else:
|
||||||
encoded_text = text
|
encoded_text = text
|
||||||
|
@ -306,7 +290,7 @@ class Sopel(irc.Bot):
|
||||||
excess = encoded_text[last_space + 1:]
|
excess = encoded_text[last_space + 1:]
|
||||||
encoded_text = encoded_text[:last_space]
|
encoded_text = encoded_text[:last_space]
|
||||||
# We'll then send the excess at the end
|
# We'll then send the excess at the end
|
||||||
# Back to unicode again, so we don't screw things up later.
|
# Back to str again, so we don't screw things up later.
|
||||||
text = encoded_text.decode('utf-8')
|
text = encoded_text.decode('utf-8')
|
||||||
try:
|
try:
|
||||||
self.sending.acquire()
|
self.sending.acquire()
|
||||||
|
@ -327,7 +311,7 @@ class Sopel(irc.Bot):
|
||||||
# Now that we've sent the first part, we need to send the rest. Doing
|
# Now that we've sent the first part, we need to send the rest. Doing
|
||||||
# this recursively seems easier to me than iteratively
|
# this recursively seems easier to me than iteratively
|
||||||
if excess:
|
if excess:
|
||||||
self.msg(recipient, excess, max_messages - 1)
|
self.say(recipient, excess, max_messages - 1)
|
||||||
|
|
||||||
def notice(self, text, dest):
|
def notice(self, text, dest):
|
||||||
"""Send an IRC NOTICE to a user or a channel.
|
"""Send an IRC NOTICE to a user or a channel.
|
||||||
|
|
|
@ -37,10 +37,7 @@ def auth_after_register(bot):
|
||||||
"""Do NickServ/AuthServ auth"""
|
"""Do NickServ/AuthServ auth"""
|
||||||
if bot.config.core.auth_method == 'nickserv':
|
if bot.config.core.auth_method == 'nickserv':
|
||||||
nickserv_name = bot.config.core.auth_target or 'NickServ'
|
nickserv_name = bot.config.core.auth_target or 'NickServ'
|
||||||
bot.msg(
|
bot.say('IDENTIFY %s' % bot.config.core.auth_password, nickserv_name)
|
||||||
nickserv_name,
|
|
||||||
'IDENTIFY %s' % bot.config.core.auth_password
|
|
||||||
)
|
|
||||||
|
|
||||||
elif bot.config.core.auth_method == 'authserv':
|
elif bot.config.core.auth_method == 'authserv':
|
||||||
account = bot.config.core.auth_username
|
account = bot.config.core.auth_username
|
||||||
|
@ -107,7 +104,7 @@ def startup(bot, trigger):
|
||||||
"more secure. If you'd like to do this, make sure you're logged in "
|
"more secure. If you'd like to do this, make sure you're logged in "
|
||||||
"and reply with \"{}useserviceauth\""
|
"and reply with \"{}useserviceauth\""
|
||||||
).format(bot.config.core.help_prefix)
|
).format(bot.config.core.help_prefix)
|
||||||
bot.msg(bot.config.core.owner, msg)
|
bot.say(msg, bot.config.core.owner)
|
||||||
|
|
||||||
|
|
||||||
@module.require_privmsg()
|
@module.require_privmsg()
|
||||||
|
@ -262,7 +259,7 @@ def track_nicks(bot, trigger):
|
||||||
debug_msg = ("Nick changed by server. "
|
debug_msg = ("Nick changed by server. "
|
||||||
"This can cause unexpected behavior. Please restart the bot.")
|
"This can cause unexpected behavior. Please restart the bot.")
|
||||||
LOGGER.critical(debug_msg)
|
LOGGER.critical(debug_msg)
|
||||||
bot.msg(bot.config.core.owner, privmsg)
|
bot.say(privmsg, bot.config.core.owner)
|
||||||
return
|
return
|
||||||
|
|
||||||
for channel in bot.privileges:
|
for channel in bot.privileges:
|
||||||
|
|
676
irc.py
676
irc.py
|
@ -1,411 +1,371 @@
|
||||||
# coding=utf-8
|
#! /usr/bin/env python3
|
||||||
# irc.py - An Utility IRC Bot
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2008, Sean B. Palmer, inamidst.com
|
"""
|
||||||
# Copyright 2012, Elsie Powell, http://embolalia.com
|
Core IRC functionality. Support for Python 2 is largely gone.
|
||||||
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
|
"""
|
||||||
#
|
|
||||||
# Licensed under the Eiffel Forum License 2.
|
|
||||||
#
|
|
||||||
# When working on core IRC protocol related features, consult protocol
|
|
||||||
# documentation at http://www.irchelp.org/irchelp/rfc/
|
|
||||||
from __future__ import unicode_literals, absolute_import, print_function, division
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import socket
|
import socket
|
||||||
import asyncore
|
|
||||||
import asynchat
|
|
||||||
import os
|
|
||||||
import codecs
|
import codecs
|
||||||
import traceback
|
import traceback
|
||||||
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
import errno
|
||||||
|
import ssl
|
||||||
|
import asyncore
|
||||||
|
import asynchat
|
||||||
|
|
||||||
from logger import get_logger
|
from logger import get_logger
|
||||||
from tools import stderr, Identifier
|
from tools import stderr, Identifier
|
||||||
from trigger import PreTrigger
|
from trigger import PreTrigger
|
||||||
try:
|
|
||||||
import ssl
|
|
||||||
if not hasattr(ssl, 'match_hostname'):
|
|
||||||
# Attempt to import ssl_match_hostname from python-backports
|
|
||||||
import backports.ssl_match_hostname
|
|
||||||
ssl.match_hostname = backports.ssl_match_hostname.match_hostname
|
|
||||||
ssl.CertificateError = backports.ssl_match_hostname.CertificateError
|
|
||||||
has_ssl = True
|
|
||||||
except ImportError:
|
|
||||||
# no SSL support
|
|
||||||
has_ssl = False
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import threading
|
|
||||||
from datetime import datetime
|
|
||||||
if sys.version_info.major >= 3:
|
|
||||||
unicode = str
|
|
||||||
|
|
||||||
LOGGER = get_logger(__name__)
|
LOGGER = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Bot(asynchat.async_chat):
|
class Bot(asynchat.async_chat):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
ca_certs = config.core.ca_certs
|
ca_certs = config.core.ca_certs
|
||||||
|
|
||||||
asynchat.async_chat.__init__(self)
|
asynchat.async_chat.__init__(self)
|
||||||
self.set_terminator(b'\n')
|
self.set_terminator(b'\n')
|
||||||
self.buffer = ''
|
self.buffer = ''
|
||||||
|
|
||||||
self.nick = Identifier(config.core.nick)
|
self.nick = Identifier(config.core.nick)
|
||||||
"""Sopel's current ``Identifier``. Changing this while Sopel is running is
|
"""Sopel's current ``Identifier``. Changing this while Sopel is running is
|
||||||
untested."""
|
untested."""
|
||||||
self.user = config.core.user
|
self.user = config.core.user
|
||||||
"""Sopel's user/ident."""
|
"""Sopel's user/ident."""
|
||||||
self.name = config.core.name
|
self.name = config.core.name
|
||||||
"""Sopel's "real name", as used for whois."""
|
"""Sopel's "real name", as used for whois."""
|
||||||
|
|
||||||
self.stack = {}
|
self.stack = {}
|
||||||
self.ca_certs = ca_certs
|
self.ca_certs = ca_certs
|
||||||
self.enabled_capabilities = set()
|
self.enabled_capabilities = set()
|
||||||
self.hasquit = False
|
self.hasquit = False
|
||||||
|
|
||||||
self.sending = threading.RLock()
|
self.sending = threading.RLock()
|
||||||
self.writing_lock = threading.Lock()
|
self.writing_lock = threading.Lock()
|
||||||
self.raw = None
|
self.raw = None
|
||||||
|
|
||||||
# Right now, only accounting for two op levels.
|
# We need this to prevent error loops in handle_error
|
||||||
# This might be expanded later.
|
self.error_count = 0
|
||||||
# These lists are filled in startup.py, as of right now.
|
|
||||||
# Are these even touched at all anymore? Remove in 7.0.
|
|
||||||
self.ops = dict()
|
|
||||||
"""Deprecated. Use bot.channels instead."""
|
|
||||||
self.halfplus = dict()
|
|
||||||
"""Deprecated. Use bot.channels instead."""
|
|
||||||
self.voices = dict()
|
|
||||||
"""Deprecated. Use bot.channels instead."""
|
|
||||||
|
|
||||||
# We need this to prevent error loops in handle_error
|
self.connection_registered = False
|
||||||
self.error_count = 0
|
""" Set to True when a server has accepted the client connection and
|
||||||
|
messages can be sent and received. """
|
||||||
|
|
||||||
self.connection_registered = False
|
def log_raw(self, line, prefix):
|
||||||
""" Set to True when a server has accepted the client connection and
|
"""Log raw line to the raw log."""
|
||||||
messages can be sent and received. """
|
if not self.config.core.log_raw:
|
||||||
|
return
|
||||||
|
if not os.path.isdir(self.config.core.logdir):
|
||||||
|
try:
|
||||||
|
os.mkdir(self.config.core.logdir)
|
||||||
|
except Exception as e:
|
||||||
|
stderr('There was a problem creating the logs directory.')
|
||||||
|
stderr('%s %s' % (str(e.__class__), str(e)))
|
||||||
|
stderr('Please fix this and then run Sopel again.')
|
||||||
|
os._exit(1)
|
||||||
|
f = codecs.open(os.path.join(self.config.core.logdir, 'raw.log'),
|
||||||
|
'a', encoding='utf-8')
|
||||||
|
f.write(prefix + str(time.time()) + "\t")
|
||||||
|
temp = line.replace('\n', '')
|
||||||
|
|
||||||
# Work around bot.connecting missing in Python older than 2.7.4
|
f.write(temp)
|
||||||
if not hasattr(self, "connecting"):
|
f.write("\n")
|
||||||
self.connecting = False
|
f.close()
|
||||||
|
|
||||||
def log_raw(self, line, prefix):
|
def safe(self, string):
|
||||||
"""Log raw line to the raw log."""
|
"""Remove newlines from a string."""
|
||||||
if not self.config.core.log_raw:
|
if isinstance(string, bytes):
|
||||||
return
|
string = string.decode('utf8')
|
||||||
if not os.path.isdir(self.config.core.logdir):
|
string = string.replace('\n', '')
|
||||||
try:
|
string = string.replace('\r', '')
|
||||||
os.mkdir(self.config.core.logdir)
|
return string
|
||||||
except Exception as e:
|
|
||||||
stderr('There was a problem creating the logs directory.')
|
|
||||||
stderr('%s %s' % (str(e.__class__), str(e)))
|
|
||||||
stderr('Please fix this and then run Sopel again.')
|
|
||||||
os._exit(1)
|
|
||||||
f = codecs.open(os.path.join(self.config.core.logdir, 'raw.log'),
|
|
||||||
'a', encoding='utf-8')
|
|
||||||
f.write(prefix + unicode(time.time()) + "\t")
|
|
||||||
temp = line.replace('\n', '')
|
|
||||||
|
|
||||||
f.write(temp)
|
def write(self, args, text=None):
|
||||||
f.write("\n")
|
args = [self.safe(arg) for arg in args]
|
||||||
f.close()
|
if text is not None:
|
||||||
|
text = self.safe(text)
|
||||||
|
try:
|
||||||
|
self.writing_lock.acquire() # Blocking lock, can't send two things
|
||||||
|
# at a time
|
||||||
|
|
||||||
def safe(self, string):
|
# From RFC2812 Internet Relay Chat: Client Protocol
|
||||||
"""Remove newlines from a string."""
|
# Section 2.3
|
||||||
if sys.version_info.major >= 3 and isinstance(string, bytes):
|
#
|
||||||
string = string.decode('utf8')
|
# https://tools.ietf.org/html/rfc2812.html
|
||||||
elif sys.version_info.major < 3:
|
#
|
||||||
if not isinstance(string, unicode):
|
# IRC messages are always lines of characters terminated with a
|
||||||
string = unicode(string, encoding='utf8')
|
# CR-LF (Carriage Return - Line Feed) pair, and these messages SHALL
|
||||||
string = string.replace('\n', '')
|
# NOT exceed 512 characters in length, counting all characters
|
||||||
string = string.replace('\r', '')
|
# including the trailing CR-LF. Thus, there are 510 characters
|
||||||
return string
|
# maximum allowed for the command and its parameters. There is no
|
||||||
|
# provision for continuation of message lines.
|
||||||
|
|
||||||
def write(self, args, text=None):
|
if text is not None:
|
||||||
args = [self.safe(arg) for arg in args]
|
temp = (' '.join(args) + ' :' + text)[:510] + '\r\n'
|
||||||
if text is not None:
|
else:
|
||||||
text = self.safe(text)
|
temp = ' '.join(args)[:510] + '\r\n'
|
||||||
try:
|
self.log_raw(temp, '>>')
|
||||||
self.writing_lock.acquire() # Blocking lock, can't send two things
|
self.send(temp.encode('utf-8'))
|
||||||
# at a time
|
finally:
|
||||||
|
self.writing_lock.release()
|
||||||
|
|
||||||
# From RFC2812 Internet Relay Chat: Client Protocol
|
def run(self, host, port=6667):
|
||||||
# Section 2.3
|
try:
|
||||||
#
|
self.initiate_connect(host, port)
|
||||||
# https://tools.ietf.org/html/rfc2812.html
|
except socket.error as e:
|
||||||
#
|
stderr('Connection error: %s' % e)
|
||||||
# IRC messages are always lines of characters terminated with a
|
|
||||||
# CR-LF (Carriage Return - Line Feed) pair, and these messages SHALL
|
|
||||||
# NOT exceed 512 characters in length, counting all characters
|
|
||||||
# including the trailing CR-LF. Thus, there are 510 characters
|
|
||||||
# maximum allowed for the command and its parameters. There is no
|
|
||||||
# provision for continuation of message lines.
|
|
||||||
|
|
||||||
if text is not None:
|
def initiate_connect(self, host, port):
|
||||||
temp = (' '.join(args) + ' :' + text)[:510] + '\r\n'
|
stderr('Connecting to %s:%s...' % (host, port))
|
||||||
else:
|
source_address = ((self.config.core.bind_host, 0)
|
||||||
temp = ' '.join(args)[:510] + '\r\n'
|
if self.config.core.bind_host else None)
|
||||||
self.log_raw(temp, '>>')
|
self.set_socket(socket.create_connection((host, port),
|
||||||
self.send(temp.encode('utf-8'))
|
source_address=source_address))
|
||||||
finally:
|
if self.config.core.use_ssl:
|
||||||
self.writing_lock.release()
|
self.send = self._ssl_send
|
||||||
|
self.recv = self._ssl_recv
|
||||||
|
self.connect((host, port))
|
||||||
|
try:
|
||||||
|
asyncore.loop()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('KeyboardInterrupt')
|
||||||
|
self.quit('KeyboardInterrupt')
|
||||||
|
|
||||||
def run(self, host, port=6667):
|
def quit(self, message):
|
||||||
try:
|
"""Disconnect from IRC and close the bot."""
|
||||||
self.initiate_connect(host, port)
|
self.write(['QUIT'], message)
|
||||||
except socket.error as e:
|
self.hasquit = True
|
||||||
stderr('Connection error: %s' % e)
|
# Wait for acknowledgement from the server. By RFC 2812 it should be
|
||||||
|
# an ERROR msg, but many servers just close the connection. Either way
|
||||||
|
# is fine by us.
|
||||||
|
# Closing the connection now would mean that stuff in the buffers that
|
||||||
|
# has not yet been processed would never be processed. It would also
|
||||||
|
# release the main thread, which is problematic because whomever called
|
||||||
|
# quit might still want to do something before main thread quits.
|
||||||
|
|
||||||
def initiate_connect(self, host, port):
|
def handle_close(self):
|
||||||
stderr('Connecting to %s:%s...' % (host, port))
|
self.connection_registered = False
|
||||||
source_address = ((self.config.core.bind_host, 0)
|
|
||||||
if self.config.core.bind_host else None)
|
|
||||||
self.set_socket(socket.create_connection((host, port),
|
|
||||||
source_address=source_address))
|
|
||||||
if self.config.core.use_ssl and has_ssl:
|
|
||||||
self.send = self._ssl_send
|
|
||||||
self.recv = self._ssl_recv
|
|
||||||
elif not has_ssl and self.config.core.use_ssl:
|
|
||||||
stderr('SSL is not avilable on your system, attempting connection '
|
|
||||||
'without it')
|
|
||||||
self.connect((host, port))
|
|
||||||
try:
|
|
||||||
asyncore.loop()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print('KeyboardInterrupt')
|
|
||||||
self.quit('KeyboardInterrupt')
|
|
||||||
|
|
||||||
def quit(self, message):
|
if hasattr(self, '_shutdown'):
|
||||||
"""Disconnect from IRC and close the bot."""
|
self._shutdown()
|
||||||
self.write(['QUIT'], message)
|
stderr('Closed!')
|
||||||
self.hasquit = True
|
|
||||||
# Wait for acknowledgement from the server. By RFC 2812 it should be
|
|
||||||
# an ERROR msg, but many servers just close the connection. Either way
|
|
||||||
# is fine by us.
|
|
||||||
# Closing the connection now would mean that stuff in the buffers that
|
|
||||||
# has not yet been processed would never be processed. It would also
|
|
||||||
# release the main thread, which is problematic because whomever called
|
|
||||||
# quit might still want to do something before main thread quits.
|
|
||||||
|
|
||||||
def handle_close(self):
|
# This will eventually call asyncore dispatchers close method, which
|
||||||
self.connection_registered = False
|
# will release the main thread. This should be called last to avoid
|
||||||
|
# race conditions.
|
||||||
|
self.close()
|
||||||
|
|
||||||
if hasattr(self, '_shutdown'):
|
def handle_connect(self):
|
||||||
self._shutdown()
|
if self.config.core.use_ssl:
|
||||||
stderr('Closed!')
|
if not self.config.core.verify_ssl:
|
||||||
|
self.ssl = ssl.wrap_socket(self.socket,
|
||||||
|
do_handshake_on_connect=True,
|
||||||
|
suppress_ragged_eofs=True)
|
||||||
|
else:
|
||||||
|
self.ssl = ssl.wrap_socket(self.socket,
|
||||||
|
do_handshake_on_connect=True,
|
||||||
|
suppress_ragged_eofs=True,
|
||||||
|
cert_reqs=ssl.CERT_REQUIRED,
|
||||||
|
ca_certs=self.ca_certs)
|
||||||
|
try:
|
||||||
|
ssl.match_hostname(self.ssl.getpeercert(), self.config.core.host)
|
||||||
|
except ssl.CertificateError:
|
||||||
|
stderr("Invalid certficate, hostname mismatch!")
|
||||||
|
os.unlink(self.config.core.pid_file_path)
|
||||||
|
os._exit(1)
|
||||||
|
self.set_socket(self.ssl)
|
||||||
|
|
||||||
# This will eventually call asyncore dispatchers close method, which
|
# Request list of server capabilities. IRCv3 servers will respond with
|
||||||
# will release the main thread. This should be called last to avoid
|
# CAP * LS (which we handle in coretasks). v2 servers will respond with
|
||||||
# race conditions.
|
# 421 Unknown command, which we'll ignore
|
||||||
self.close()
|
self.write(('CAP', 'LS', '302'))
|
||||||
|
|
||||||
def handle_connect(self):
|
if self.config.core.auth_method == 'server':
|
||||||
if self.config.core.use_ssl and has_ssl:
|
password = self.config.core.auth_password
|
||||||
if not self.config.core.verify_ssl:
|
self.write(('PASS', password))
|
||||||
self.ssl = ssl.wrap_socket(self.socket,
|
self.write(('NICK', self.nick))
|
||||||
do_handshake_on_connect=True,
|
self.write(('USER', self.user, '+iw', self.nick), self.name)
|
||||||
suppress_ragged_eofs=True)
|
|
||||||
else:
|
|
||||||
self.ssl = ssl.wrap_socket(self.socket,
|
|
||||||
do_handshake_on_connect=True,
|
|
||||||
suppress_ragged_eofs=True,
|
|
||||||
cert_reqs=ssl.CERT_REQUIRED,
|
|
||||||
ca_certs=self.ca_certs)
|
|
||||||
try:
|
|
||||||
ssl.match_hostname(self.ssl.getpeercert(), self.config.core.host)
|
|
||||||
except ssl.CertificateError:
|
|
||||||
stderr("Invalid certficate, hostname mismatch!")
|
|
||||||
os.unlink(self.config.core.pid_file_path)
|
|
||||||
os._exit(1)
|
|
||||||
self.set_socket(self.ssl)
|
|
||||||
|
|
||||||
# Request list of server capabilities. IRCv3 servers will respond with
|
stderr('Connected.')
|
||||||
# CAP * LS (which we handle in coretasks). v2 servers will respond with
|
self.last_ping_time = datetime.now()
|
||||||
# 421 Unknown command, which we'll ignore
|
timeout_check_thread = threading.Thread(target=self._timeout_check)
|
||||||
self.write(('CAP', 'LS', '302'))
|
timeout_check_thread.daemon = True
|
||||||
|
timeout_check_thread.start()
|
||||||
|
ping_thread = threading.Thread(target=self._send_ping)
|
||||||
|
ping_thread.daemon = True
|
||||||
|
ping_thread.start()
|
||||||
|
|
||||||
if self.config.core.auth_method == 'server':
|
def _timeout_check(self):
|
||||||
password = self.config.core.auth_password
|
while self.connected or self.connecting:
|
||||||
self.write(('PASS', password))
|
if (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout):
|
||||||
self.write(('NICK', self.nick))
|
stderr('Ping timeout reached after %s seconds, closing connection' % self.config.core.timeout)
|
||||||
self.write(('USER', self.user, '+iw', self.nick), self.name)
|
self.handle_close()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
time.sleep(int(self.config.core.timeout))
|
||||||
|
|
||||||
stderr('Connected.')
|
def _send_ping(self):
|
||||||
self.last_ping_time = datetime.now()
|
while self.connected or self.connecting:
|
||||||
timeout_check_thread = threading.Thread(target=self._timeout_check)
|
if self.connected and (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout) / 2:
|
||||||
timeout_check_thread.daemon = True
|
try:
|
||||||
timeout_check_thread.start()
|
self.write(('PING', self.config.core.host))
|
||||||
ping_thread = threading.Thread(target=self._send_ping)
|
except socket.error:
|
||||||
ping_thread.daemon = True
|
pass
|
||||||
ping_thread.start()
|
time.sleep(int(self.config.core.timeout) / 2)
|
||||||
|
|
||||||
def _timeout_check(self):
|
def _ssl_send(self, data):
|
||||||
while self.connected or self.connecting:
|
"""Replacement for self.send() during SSL connections."""
|
||||||
if (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout):
|
try:
|
||||||
stderr('Ping timeout reached after %s seconds, closing connection' % self.config.core.timeout)
|
result = self.socket.send(data)
|
||||||
self.handle_close()
|
return result
|
||||||
break
|
except ssl.SSLError as why:
|
||||||
else:
|
if why[0] in (asyncore.EWOULDBLOCK, errno.ESRCH):
|
||||||
time.sleep(int(self.config.core.timeout))
|
return 0
|
||||||
|
else:
|
||||||
|
raise why
|
||||||
|
return 0
|
||||||
|
|
||||||
def _send_ping(self):
|
def _ssl_recv(self, buffer_size):
|
||||||
while self.connected or self.connecting:
|
"""Replacement for self.recv() during SSL connections.
|
||||||
if self.connected and (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout) / 2:
|
|
||||||
try:
|
|
||||||
self.write(('PING', self.config.core.host))
|
|
||||||
except socket.error:
|
|
||||||
pass
|
|
||||||
time.sleep(int(self.config.core.timeout) / 2)
|
|
||||||
|
|
||||||
def _ssl_send(self, data):
|
From: http://evanfosmark.com/2010/09/ssl-support-in-asynchatasync_chat
|
||||||
"""Replacement for self.send() during SSL connections."""
|
|
||||||
try:
|
|
||||||
result = self.socket.send(data)
|
|
||||||
return result
|
|
||||||
except ssl.SSLError as why:
|
|
||||||
if why[0] in (asyncore.EWOULDBLOCK, errno.ESRCH):
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
raise why
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def _ssl_recv(self, buffer_size):
|
"""
|
||||||
"""Replacement for self.recv() during SSL connections.
|
try:
|
||||||
|
data = self.socket.read(buffer_size)
|
||||||
|
if not data:
|
||||||
|
self.handle_close()
|
||||||
|
return b''
|
||||||
|
return data
|
||||||
|
except ssl.SSLError as why:
|
||||||
|
if why[0] in (asyncore.ECONNRESET, asyncore.ENOTCONN,
|
||||||
|
asyncore.ESHUTDOWN):
|
||||||
|
self.handle_close()
|
||||||
|
return ''
|
||||||
|
elif why[0] == errno.ENOENT:
|
||||||
|
# Required in order to keep it non-blocking
|
||||||
|
return b''
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
From: http://evanfosmark.com/2010/09/ssl-support-in-asynchatasync_chat
|
def collect_incoming_data(self, data):
|
||||||
|
# We can't trust clients to pass valid str.
|
||||||
|
try:
|
||||||
|
data = str(data, encoding='utf-8')
|
||||||
|
except strDecodeError:
|
||||||
|
# not str, let's try cp1252
|
||||||
|
try:
|
||||||
|
data = str(data, encoding='cp1252')
|
||||||
|
except strDecodeError:
|
||||||
|
# Okay, let's try ISO8859-1
|
||||||
|
try:
|
||||||
|
data = str(data, encoding='iso8859-1')
|
||||||
|
except:
|
||||||
|
# Discard line if encoding is unknown
|
||||||
|
return
|
||||||
|
|
||||||
"""
|
if data:
|
||||||
try:
|
self.log_raw(data, '<<')
|
||||||
data = self.socket.read(buffer_size)
|
self.buffer += data
|
||||||
if not data:
|
|
||||||
self.handle_close()
|
|
||||||
return b''
|
|
||||||
return data
|
|
||||||
except ssl.SSLError as why:
|
|
||||||
if why[0] in (asyncore.ECONNRESET, asyncore.ENOTCONN,
|
|
||||||
asyncore.ESHUTDOWN):
|
|
||||||
self.handle_close()
|
|
||||||
return ''
|
|
||||||
elif why[0] == errno.ENOENT:
|
|
||||||
# Required in order to keep it non-blocking
|
|
||||||
return b''
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def collect_incoming_data(self, data):
|
def found_terminator(self):
|
||||||
# We can't trust clients to pass valid unicode.
|
line = self.buffer
|
||||||
try:
|
if line.endswith('\r'):
|
||||||
data = unicode(data, encoding='utf-8')
|
line = line[:-1]
|
||||||
except UnicodeDecodeError:
|
self.buffer = ''
|
||||||
# not unicode, let's try cp1252
|
self.last_ping_time = datetime.now()
|
||||||
try:
|
pretrigger = PreTrigger(self.nick, line)
|
||||||
data = unicode(data, encoding='cp1252')
|
if all(cap not in self.enabled_capabilities for cap in ['account-tag', 'extended-join']):
|
||||||
except UnicodeDecodeError:
|
pretrigger.tags.pop('account', None)
|
||||||
# Okay, let's try ISO8859-1
|
|
||||||
try:
|
|
||||||
data = unicode(data, encoding='iso8859-1')
|
|
||||||
except:
|
|
||||||
# Discard line if encoding is unknown
|
|
||||||
return
|
|
||||||
|
|
||||||
if data:
|
if pretrigger.event == 'PING':
|
||||||
self.log_raw(data, '<<')
|
self.write(('PONG', pretrigger.args[-1]))
|
||||||
self.buffer += data
|
elif pretrigger.event == 'ERROR':
|
||||||
|
LOGGER.error("ERROR recieved from server: %s", pretrigger.args[-1])
|
||||||
|
if self.hasquit:
|
||||||
|
self.close_when_done()
|
||||||
|
elif pretrigger.event == '433':
|
||||||
|
stderr('Nickname already in use!')
|
||||||
|
self.handle_close()
|
||||||
|
|
||||||
def found_terminator(self):
|
self.dispatch(pretrigger)
|
||||||
line = self.buffer
|
|
||||||
if line.endswith('\r'):
|
|
||||||
line = line[:-1]
|
|
||||||
self.buffer = ''
|
|
||||||
self.last_ping_time = datetime.now()
|
|
||||||
pretrigger = PreTrigger(self.nick, line)
|
|
||||||
if all(cap not in self.enabled_capabilities for cap in ['account-tag', 'extended-join']):
|
|
||||||
pretrigger.tags.pop('account', None)
|
|
||||||
|
|
||||||
if pretrigger.event == 'PING':
|
def dispatch(self, pretrigger):
|
||||||
self.write(('PONG', pretrigger.args[-1]))
|
pass
|
||||||
elif pretrigger.event == 'ERROR':
|
|
||||||
LOGGER.error("ERROR recieved from server: %s", pretrigger.args[-1])
|
|
||||||
if self.hasquit:
|
|
||||||
self.close_when_done()
|
|
||||||
elif pretrigger.event == '433':
|
|
||||||
stderr('Nickname already in use!')
|
|
||||||
self.handle_close()
|
|
||||||
|
|
||||||
self.dispatch(pretrigger)
|
def error(self, trigger=None):
|
||||||
|
"""Called internally when a module causes an error."""
|
||||||
|
try:
|
||||||
|
trace = traceback.format_exc()
|
||||||
|
if sys.version_info.major < 3:
|
||||||
|
trace = trace.decode('utf-8', errors='xmlcharrefreplace')
|
||||||
|
stderr(trace)
|
||||||
|
try:
|
||||||
|
lines = list(reversed(trace.splitlines()))
|
||||||
|
report = [lines[0].strip()]
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('File "'):
|
||||||
|
report.append(line[0].lower() + line[1:])
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
report.append('source unknown')
|
||||||
|
|
||||||
def dispatch(self, pretrigger):
|
signature = '%s (%s)' % (report[0], report[1])
|
||||||
pass
|
# TODO: make not hardcoded
|
||||||
|
log_filename = os.path.join(self.config.core.logdir, 'exceptions.log')
|
||||||
|
with codecs.open(log_filename, 'a', encoding='utf-8') as logfile:
|
||||||
|
logfile.write('Signature: %s\n' % signature)
|
||||||
|
if trigger:
|
||||||
|
logfile.write('from {} at {}. Message was: {}\n'.format(
|
||||||
|
trigger.nick, str(datetime.now()), trigger.group(0)))
|
||||||
|
logfile.write(trace)
|
||||||
|
logfile.write(
|
||||||
|
'----------------------------------------\n\n'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
stderr("Could not save full traceback!")
|
||||||
|
LOGGER.error("Could not save traceback from %s to file: %s", trigger.sender, str(e))
|
||||||
|
|
||||||
def error(self, trigger=None):
|
if trigger and self.config.core.reply_errors and trigger.sender is not None:
|
||||||
"""Called internally when a module causes an error."""
|
self.msg(trigger.sender, signature)
|
||||||
try:
|
if trigger:
|
||||||
trace = traceback.format_exc()
|
LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(signature), trigger.raw))
|
||||||
if sys.version_info.major < 3:
|
except Exception as e:
|
||||||
trace = trace.decode('utf-8', errors='xmlcharrefreplace')
|
if trigger and self.config.core.reply_errors and trigger.sender is not None:
|
||||||
stderr(trace)
|
self.msg(trigger.sender, "Got an error.")
|
||||||
try:
|
if trigger:
|
||||||
lines = list(reversed(trace.splitlines()))
|
LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(e), trigger.raw))
|
||||||
report = [lines[0].strip()]
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
if line.startswith('File "'):
|
|
||||||
report.append(line[0].lower() + line[1:])
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
report.append('source unknown')
|
|
||||||
|
|
||||||
signature = '%s (%s)' % (report[0], report[1])
|
def handle_error(self):
|
||||||
# TODO: make not hardcoded
|
"""Handle any uncaptured error in the core.
|
||||||
log_filename = os.path.join(self.config.core.logdir, 'exceptions.log')
|
|
||||||
with codecs.open(log_filename, 'a', encoding='utf-8') as logfile:
|
|
||||||
logfile.write('Signature: %s\n' % signature)
|
|
||||||
if trigger:
|
|
||||||
logfile.write('from {} at {}. Message was: {}\n'.format(
|
|
||||||
trigger.nick, str(datetime.now()), trigger.group(0)))
|
|
||||||
logfile.write(trace)
|
|
||||||
logfile.write(
|
|
||||||
'----------------------------------------\n\n'
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
stderr("Could not save full traceback!")
|
|
||||||
LOGGER.error("Could not save traceback from %s to file: %s", trigger.sender, str(e))
|
|
||||||
|
|
||||||
if trigger and self.config.core.reply_errors and trigger.sender is not None:
|
Overrides asyncore's handle_error.
|
||||||
self.msg(trigger.sender, signature)
|
|
||||||
if trigger:
|
|
||||||
LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(signature), trigger.raw))
|
|
||||||
except Exception as e:
|
|
||||||
if trigger and self.config.core.reply_errors and trigger.sender is not None:
|
|
||||||
self.msg(trigger.sender, "Got an error.")
|
|
||||||
if trigger:
|
|
||||||
LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(e), trigger.raw))
|
|
||||||
|
|
||||||
def handle_error(self):
|
"""
|
||||||
"""Handle any uncaptured error in the core.
|
trace = traceback.format_exc()
|
||||||
|
stderr(trace)
|
||||||
Overrides asyncore's handle_error.
|
LOGGER.error('Fatal error in core, please review exception log')
|
||||||
|
# TODO: make not hardcoded
|
||||||
"""
|
logfile = codecs.open(
|
||||||
trace = traceback.format_exc()
|
os.path.join(self.config.core.logdir, 'exceptions.log'),
|
||||||
stderr(trace)
|
'a',
|
||||||
LOGGER.error('Fatal error in core, please review exception log')
|
encoding='utf-8'
|
||||||
# TODO: make not hardcoded
|
)
|
||||||
logfile = codecs.open(
|
logfile.write('Fatal error in core, handle_error() was called\n')
|
||||||
os.path.join(self.config.core.logdir, 'exceptions.log'),
|
logfile.write('last raw line was %s' % self.raw)
|
||||||
'a',
|
logfile.write(trace)
|
||||||
encoding='utf-8'
|
logfile.write('Buffer:\n')
|
||||||
)
|
logfile.write(self.buffer)
|
||||||
logfile.write('Fatal error in core, handle_error() was called\n')
|
logfile.write('----------------------------------------\n\n')
|
||||||
logfile.write('last raw line was %s' % self.raw)
|
logfile.close()
|
||||||
logfile.write(trace)
|
if self.error_count > 10:
|
||||||
logfile.write('Buffer:\n')
|
if (datetime.now() - self.last_error_timestamp).seconds < 5:
|
||||||
logfile.write(self.buffer)
|
stderr("Too many errors, can't continue")
|
||||||
logfile.write('----------------------------------------\n\n')
|
os._exit(1)
|
||||||
logfile.close()
|
self.last_error_timestamp = datetime.now()
|
||||||
if self.error_count > 10:
|
self.error_count = self.error_count + 1
|
||||||
if (datetime.now() - self.last_error_timestamp).seconds < 5:
|
|
||||||
stderr("Too many errors, can't continue")
|
|
||||||
os._exit(1)
|
|
||||||
self.last_error_timestamp = datetime.now()
|
|
||||||
self.error_count = self.error_count + 1
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# coding=utf-8
|
#! /usr/bin/env python3
|
||||||
from __future__ import unicode_literals, absolute_import, print_function, division
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Methods for loading modules.
|
||||||
|
"""
|
||||||
import imp
|
import imp
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
|
|
2
logger.py
Normal file → Executable file
2
logger.py
Normal file → Executable file
|
@ -13,7 +13,7 @@ class IrcLoggingHandler(logging.Handler):
|
||||||
def emit(self, record):
|
def emit(self, record):
|
||||||
try:
|
try:
|
||||||
msg = self.format(record)
|
msg = self.format(record)
|
||||||
self._bot.msg(self._channel, msg)
|
self._bot.say(msg, self._channel)
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
raise
|
raise
|
||||||
except:
|
except:
|
||||||
|
|
|
@ -59,7 +59,7 @@ def interval(*args):
|
||||||
@sopel.module.interval(5)
|
@sopel.module.interval(5)
|
||||||
def spam_every_5s(bot):
|
def spam_every_5s(bot):
|
||||||
if "#here" in bot.channels:
|
if "#here" in bot.channels:
|
||||||
bot.msg("#here", "It has been five seconds!")
|
bot.say("It has been five seconds!", "#here")
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def add_attribute(function):
|
def add_attribute(function):
|
||||||
|
|
|
@ -98,7 +98,7 @@ def msg(bot, trigger):
|
||||||
if not channel or not message:
|
if not channel or not message:
|
||||||
return
|
return
|
||||||
|
|
||||||
bot.msg(channel, message)
|
bot.say(message, channel)
|
||||||
|
|
||||||
|
|
||||||
@module.require_privmsg
|
@module.require_privmsg
|
||||||
|
@ -119,7 +119,7 @@ def me(bot, trigger):
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = '\x01ACTION %s\x01' % action
|
msg = '\x01ACTION %s\x01' % action
|
||||||
bot.msg(channel, msg)
|
bot.say(msg, channel)
|
||||||
|
|
||||||
|
|
||||||
@module.event('INVITE')
|
@module.event('INVITE')
|
||||||
|
|
|
@ -127,62 +127,6 @@ def unban(bot, trigger):
|
||||||
bot.write(['MODE', channel, '-b', banmask])
|
bot.write(['MODE', channel, '-b', banmask])
|
||||||
|
|
||||||
|
|
||||||
@require_chanmsg
|
|
||||||
@require_privilege(OP, 'You are not a channel operator.')
|
|
||||||
@commands('quiet')
|
|
||||||
def quiet(bot, trigger):
|
|
||||||
"""
|
|
||||||
This gives admins the ability to quiet a user.
|
|
||||||
The bot must be a Channel Operator for this command to work.
|
|
||||||
"""
|
|
||||||
if bot.privileges[trigger.sender][bot.nick] < OP:
|
|
||||||
return bot.reply("I'm not a channel operator!")
|
|
||||||
text = trigger.group().split()
|
|
||||||
argc = len(text)
|
|
||||||
if argc < 2:
|
|
||||||
return
|
|
||||||
opt = Identifier(text[1])
|
|
||||||
quietmask = opt
|
|
||||||
channel = trigger.sender
|
|
||||||
if not opt.is_nick():
|
|
||||||
if argc < 3:
|
|
||||||
return
|
|
||||||
quietmask = text[2]
|
|
||||||
channel = opt
|
|
||||||
quietmask = configureHostMask(quietmask)
|
|
||||||
if quietmask == '':
|
|
||||||
return
|
|
||||||
bot.write(['MODE', channel, '+q', quietmask])
|
|
||||||
|
|
||||||
|
|
||||||
@require_chanmsg
|
|
||||||
@require_privilege(OP, 'You are not a channel operator.')
|
|
||||||
@commands('unquiet')
|
|
||||||
def unquiet(bot, trigger):
|
|
||||||
"""
|
|
||||||
This gives admins the ability to unquiet a user.
|
|
||||||
The bot must be a Channel Operator for this command to work.
|
|
||||||
"""
|
|
||||||
if bot.privileges[trigger.sender][bot.nick] < OP:
|
|
||||||
return bot.reply("I'm not a channel operator!")
|
|
||||||
text = trigger.group().split()
|
|
||||||
argc = len(text)
|
|
||||||
if argc < 2:
|
|
||||||
return
|
|
||||||
opt = Identifier(text[1])
|
|
||||||
quietmask = opt
|
|
||||||
channel = trigger.sender
|
|
||||||
if not opt.is_nick():
|
|
||||||
if argc < 3:
|
|
||||||
return
|
|
||||||
quietmask = text[2]
|
|
||||||
channel = opt
|
|
||||||
quietmask = configureHostMask(quietmask)
|
|
||||||
if quietmask == '':
|
|
||||||
return
|
|
||||||
bot.write(['MODE', channel, '-q', quietmask])
|
|
||||||
|
|
||||||
|
|
||||||
@require_chanmsg
|
@require_chanmsg
|
||||||
@require_privilege(OP, 'You are not a channel operator.')
|
@require_privilege(OP, 'You are not a channel operator.')
|
||||||
@commands('kickban', 'kb')
|
@commands('kickban', 'kb')
|
||||||
|
|
|
@ -18,5 +18,5 @@ def announce(bot, trigger):
|
||||||
bot.reply('Sorry, I can\'t let you do that')
|
bot.reply('Sorry, I can\'t let you do that')
|
||||||
return
|
return
|
||||||
for channel in bot.channels:
|
for channel in bot.channels:
|
||||||
bot.msg(channel, '[ANNOUNCEMENT] %s' % trigger.group(2))
|
bot.say('[ANNOUNCEMENT] %s' % trigger.group(2), channel)
|
||||||
bot.reply('Announce complete.')
|
bot.reply('Announce complete.')
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
ASCII
|
ASCII
|
||||||
"""
|
"""
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
import argparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image, ImageFont, ImageDraw
|
from PIL import Image, ImageFont, ImageDraw
|
||||||
|
@ -168,6 +169,8 @@ def image_to_ascii(image=None, reverse=False, brail=False, color=None,**kwargs):
|
||||||
image = image.convert(image.palette.mode)
|
image = image.convert(image.palette.mode)
|
||||||
if image.mode == "RGBA":
|
if image.mode == "RGBA":
|
||||||
image = alpha_composite(image).convert("RGB")
|
image = alpha_composite(image).convert("RGB")
|
||||||
|
if image.mode == "L":
|
||||||
|
image = image.convert("RGB")
|
||||||
|
|
||||||
image = scale_image(image)
|
image = scale_image(image)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
result = etymology(word)
|
||||||
except IOError:
|
except IOError:
|
||||||
msg = "Can't connect to etymonline.com (%s)" % (etyuri % word)
|
msg = "Can't connect to etymonline.com (%s)" % (etyuri % word)
|
||||||
bot.msg(trigger.sender, msg)
|
bot.say(msg)
|
||||||
return NOLIMIT
|
return NOLIMIT
|
||||||
except (AttributeError, TypeError):
|
except (AttributeError, TypeError):
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
if result is not None:
|
if result is not None:
|
||||||
bot.msg(trigger.sender, result)
|
bot.say(result)
|
||||||
else:
|
else:
|
||||||
uri = etysearch % word
|
uri = etysearch % word
|
||||||
msg = 'Can\'t find the etymology for "%s". Try %s' % (word, uri)
|
msg = 'Can\'t find the etymology for "%s". Try %s' % (word, uri)
|
||||||
bot.msg(trigger.sender, msg)
|
bot.say(msg)
|
||||||
return NOLIMIT
|
return NOLIMIT
|
||||||
|
|
28
modules/grog.py
Executable file
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
|
http://sopel.chat
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals, absolute_import, print_function, division
|
|
||||||
|
|
||||||
import textwrap
|
|
||||||
import collections
|
|
||||||
import json
|
|
||||||
|
|
||||||
from logger import get_logger
|
|
||||||
from module import commands, rule, example, priority
|
from module import commands, rule, example, priority
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@rule('$nick' '(?i)(help|doc) +([A-Za-z]+)(?:\?+)?$')
|
@rule('$nick' '(?i)(help|doc) +([A-Za-z]+)(?:\?+)?$')
|
||||||
@example('.help tell')
|
@example('.help tell')
|
||||||
@commands('help', 'commands')
|
@commands('help', 'commands')
|
||||||
|
@ -30,23 +20,29 @@ def help(bot, trigger):
|
||||||
name = trigger.group(2)
|
name = trigger.group(2)
|
||||||
name = name.lower()
|
name = name.lower()
|
||||||
|
|
||||||
if name in bot.doc:
|
if name not in bot.doc:
|
||||||
print(bot.doc[name])
|
return
|
||||||
newlines = ['']
|
newlines = ['']
|
||||||
lines = list(filter(None, bot.doc[name][0]))
|
lines = list(filter(None, bot.doc[name][0]))
|
||||||
lines = list(map(str.strip, lines))
|
lines = list(map(str.strip, lines))
|
||||||
for line in lines:
|
for line in lines:
|
||||||
newlines[-1] = newlines[-1] + ' ' + line
|
newlines[-1] = newlines[-1] + ' ' + line
|
||||||
if line[-1] is '.':
|
if line[-1] is '.':
|
||||||
newlines.append('')
|
newlines.append('')
|
||||||
newlines = list(map(str.strip, newlines))
|
newlines = list(map(str.strip, newlines))
|
||||||
if bot.doc[name][1]:
|
if bot.doc[name][1]:
|
||||||
newlines.append('Ex. ' + bot.doc[name][1])
|
newlines.append('Ex. ' + bot.doc[name][1])
|
||||||
|
|
||||||
for msg in newlines:
|
for msg in newlines:
|
||||||
bot.say(msg)
|
bot.say(msg)
|
||||||
else:
|
else:
|
||||||
helps = list(bot.command_groups)
|
command_groups = list(bot.command_groups.values())
|
||||||
helps.sort()
|
commands = []
|
||||||
msg = "Available commands: " + ', '.join(helps)
|
for group in command_groups:
|
||||||
|
if type(group) == list:
|
||||||
|
commands += group
|
||||||
|
else:
|
||||||
|
commands += [group]
|
||||||
|
commands.sort()
|
||||||
|
msg = "Available commands: " + ', '.join(commands)
|
||||||
bot.say(msg)
|
bot.say(msg)
|
||||||
|
|
|
@ -43,7 +43,7 @@ def roomTemp(bot, trigger):
|
||||||
|
|
||||||
@module.require_admin
|
@module.require_admin
|
||||||
@module.commands('inkwrite')
|
@module.commands('inkwrite')
|
||||||
def roomTemp(bot, trigger):
|
def inkWrite(bot, trigger):
|
||||||
"""
|
"""
|
||||||
Writes shit to my e-ink screen.
|
Writes shit to my e-ink screen.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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()
|
bot.memory['movie_lock'].acquire()
|
||||||
conn = bot.db.connect()
|
conn = bot.db.connect()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute("SELECT * FROM movie WHERE times_watched < 1 AND shitpost = 0")
|
cur.execute("SELECT movie_title FROM movie WHERE " + \
|
||||||
movieList = cur.fetchall()
|
"times_watched < 1 AND shitpost = 0 ORDER BY RANDOM() LIMIT 1;")
|
||||||
|
movie = cur.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
roll = random.randint(0, len(movieList)-1)
|
|
||||||
bot.memory['movie_lock'].release()
|
bot.memory['movie_lock'].release()
|
||||||
bot.reply(movieList[roll][0])
|
bot.reply(movie[0])
|
||||||
|
|
||||||
|
|
||||||
@module.require_admin
|
@module.require_admin
|
||||||
|
|
|
@ -70,9 +70,9 @@ def setup(bot):
|
||||||
for oldtime in oldtimes:
|
for oldtime in oldtimes:
|
||||||
for (channel, nick, message) in bot.rdb[oldtime]:
|
for (channel, nick, message) in bot.rdb[oldtime]:
|
||||||
if message:
|
if message:
|
||||||
bot.msg(channel, nick + ': ' + message)
|
bot.say(nick + ': ' + message)
|
||||||
else:
|
else:
|
||||||
bot.msg(channel, nick + '!')
|
bot.say(nick + '!')
|
||||||
del bot.rdb[oldtime]
|
del bot.rdb[oldtime]
|
||||||
dump_database(bot.rfn, bot.rdb)
|
dump_database(bot.rfn, bot.rdb)
|
||||||
time.sleep(2.5)
|
time.sleep(2.5)
|
||||||
|
|
|
@ -11,118 +11,104 @@ import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.version_info.major < 3:
|
if sys.version_info.major < 3:
|
||||||
from urllib import quote_plus
|
from urllib import quote_plus
|
||||||
else:
|
else:
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
|
|
||||||
def formatnumber(n):
|
def formatnumber(n):
|
||||||
"""Format a number with beautiful commas."""
|
"""Format a number with beautiful commas."""
|
||||||
parts = list(str(n))
|
parts = list(str(n))
|
||||||
for i in range((len(parts) - 3), 0, -3):
|
for i in range((len(parts) - 3), 0, -3):
|
||||||
parts.insert(i, ',')
|
parts.insert(i, ',')
|
||||||
return ''.join(parts)
|
return ''.join(parts)
|
||||||
|
|
||||||
r_bing = re.compile(r'<h3><a href="([^"]+)"')
|
r_bing = re.compile(r'<h3><a href="([^"]+)"')
|
||||||
|
|
||||||
|
|
||||||
def bing_search(query, lang='en-GB'):
|
def bing_search(query, lang='en-GB'):
|
||||||
base = 'http://www.bing.com/search?mkt=%s&q=' % lang
|
base = 'http://www.bing.com/search?mkt=%s&q=' % lang
|
||||||
bytes = requests.get(base + query)
|
bytes = requests.get(base + query)
|
||||||
m = r_bing.search(bytes)
|
m = r_bing.search(bytes)
|
||||||
if m:
|
if m:
|
||||||
return m.group(1)
|
return m.group(1)
|
||||||
|
|
||||||
r_duck = re.compile(r'nofollow" class="[^"]+" href="(?!https?:\/\/r\.search\.yahoo)(.*?)">')
|
r_duck = re.compile(r'nofollow" class="[^"]+" href="(?!https?:\/\/r\.search\.yahoo)(.*?)">')
|
||||||
|
|
||||||
|
|
||||||
def duck_search(query):
|
def duck_search(query):
|
||||||
query = query.replace('!', '')
|
query = query.replace('!', '')
|
||||||
uri = 'http://duckduckgo.com/html/?q=%s&kl=uk-en' % query
|
uri = 'http://duckduckgo.com/html/?q=%s&kl=uk-en' % query
|
||||||
bytes = requests.get(uri)
|
bytes = requests.get(uri)
|
||||||
if 'requests-result' in bytes: # filter out the adds on top of the page
|
if 'requests-result' in bytes: # filter out the adds on top of the page
|
||||||
bytes = bytes.split('requests-result')[1]
|
bytes = bytes.split('requests-result')[1]
|
||||||
m = r_duck.search(bytes)
|
m = r_duck.search(bytes)
|
||||||
if m:
|
if m:
|
||||||
return requests.decode(m.group(1))
|
return requests.decode(m.group(1))
|
||||||
|
|
||||||
# Alias google_search to duck_search
|
# Alias google_search to duck_search
|
||||||
google_search = duck_search
|
google_search = duck_search
|
||||||
|
|
||||||
|
|
||||||
def duck_api(query):
|
def duck_api(query):
|
||||||
if '!bang' in query.lower():
|
if '!bang' in query.lower():
|
||||||
return 'https://duckduckgo.com/bang.html'
|
return 'https://duckduckgo.com/bang.html'
|
||||||
|
|
||||||
# This fixes issue #885 (https://github.com/sopel-irc/sopel/issues/885)
|
# This fixes issue #885 (https://github.com/sopel-irc/sopel/issues/885)
|
||||||
# It seems that duckduckgo api redirects to its Instant answer API html page
|
# It seems that duckduckgo api redirects to its Instant answer API html page
|
||||||
# if the query constains special charactares that aren't urlencoded.
|
# if the query constains special charactares that aren't urlencoded.
|
||||||
# So in order to always get a JSON response back the query is urlencoded
|
# So in order to always get a JSON response back the query is urlencoded
|
||||||
query = quote_plus(query)
|
query = quote_plus(query)
|
||||||
uri = 'http://api.duckduckgo.com/?q=%s&format=json&no_html=1&no_redirect=1' % query
|
uri = 'http://api.duckduckgo.com/?q=%s&format=json&no_html=1&no_redirect=1' % query
|
||||||
results = json.loads(requests.get(uri))
|
results = json.loads(requests.get(uri))
|
||||||
if results['Redirect']:
|
if results['Redirect']:
|
||||||
return results['Redirect']
|
return results['Redirect']
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@commands('duck', 'ddg', 'g')
|
@commands('duck', 'ddg', 'g')
|
||||||
@example('.duck privacy or .duck !mcwiki obsidian')
|
@example('.duck privacy or .duck !mcwiki obsidian')
|
||||||
def duck(bot, trigger):
|
def duck(bot, trigger):
|
||||||
"""Queries Duck Duck Go for the specified input."""
|
"""Queries Duck Duck Go for the specified input."""
|
||||||
query = trigger.group(2)
|
query = trigger.group(2)
|
||||||
if not query:
|
if not query:
|
||||||
return bot.reply('.ddg what?')
|
return bot.reply('.ddg what?')
|
||||||
|
|
||||||
# If the API gives us something, say it and stop
|
# If the API gives us something, say it and stop
|
||||||
result = duck_api(query)
|
result = duck_api(query)
|
||||||
if result:
|
if result:
|
||||||
bot.reply(result)
|
bot.reply(result)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Otherwise, look it up on the HTMl version
|
# Otherwise, look it up on the HTMl version
|
||||||
uri = duck_search(query)
|
uri = duck_search(query)
|
||||||
|
|
||||||
if uri:
|
if uri:
|
||||||
bot.reply(uri)
|
bot.reply(uri)
|
||||||
if 'last_seen_url' in bot.memory:
|
if 'last_seen_url' in bot.memory:
|
||||||
bot.memory['last_seen_url'][trigger.sender] = uri
|
bot.memory['last_seen_url'][trigger.sender] = uri
|
||||||
else:
|
else:
|
||||||
bot.reply("No results found for '%s'." % query)
|
bot.reply("No results found for '%s'." % query)
|
||||||
|
|
||||||
|
|
||||||
@commands('search')
|
@commands('search')
|
||||||
@example('.search nerdfighter')
|
@example('.search nerdfighter')
|
||||||
def search(bot, trigger):
|
def search(bot, trigger):
|
||||||
"""Searches Bing and Duck Duck Go."""
|
"""Searches Bing and Duck Duck Go."""
|
||||||
if not trigger.group(2):
|
if not trigger.group(2):
|
||||||
return bot.reply('.search for what?')
|
return bot.reply('.search for what?')
|
||||||
query = trigger.group(2)
|
query = trigger.group(2)
|
||||||
bu = bing_search(query) or '-'
|
bu = bing_search(query) or '-'
|
||||||
du = duck_search(query) or '-'
|
du = duck_search(query) or '-'
|
||||||
|
|
||||||
if bu == du:
|
if bu == du:
|
||||||
result = '%s (b, d)' % bu
|
result = '%s (b, d)' % bu
|
||||||
else:
|
else:
|
||||||
if len(bu) > 150:
|
if len(bu) > 150:
|
||||||
bu = '(extremely long link)'
|
bu = '(extremely long link)'
|
||||||
if len(du) > 150:
|
if len(du) > 150:
|
||||||
du = '(extremely long link)'
|
du = '(extremely long link)'
|
||||||
result = '%s (b), %s (d)' % (bu, du)
|
result = '%s (b), %s (d)' % (bu, du)
|
||||||
|
|
||||||
bot.reply(result)
|
bot.reply(result)
|
||||||
|
|
||||||
|
|
||||||
@commands('suggest')
|
|
||||||
def suggest(bot, trigger):
|
|
||||||
"""Suggest terms starting with given input"""
|
|
||||||
if not trigger.group(2):
|
|
||||||
return bot.reply("No query term.")
|
|
||||||
query = trigger.group(2)
|
|
||||||
uri = 'http://requestssitedev.de/temp-bin/suggest.pl?q='
|
|
||||||
answer = requests.get(uri + query.replace('+', '%2B'))
|
|
||||||
if answer:
|
|
||||||
bot.say(answer)
|
|
||||||
else:
|
|
||||||
bot.reply('Sorry, no result.')
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ When was this user last seen.
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
import datetime
|
import datetime
|
||||||
|
import argparse
|
||||||
|
|
||||||
from tools import Identifier
|
from tools import Identifier
|
||||||
from tools.time import get_timezone, format_time, relativeTime
|
from tools.time import get_timezone, format_time, relativeTime
|
||||||
from module import commands, rule, priority, thread
|
from module import commands, rule, priority, thread
|
||||||
|
@ -13,10 +15,16 @@ from module import commands, rule, priority, thread
|
||||||
@commands('seen')
|
@commands('seen')
|
||||||
def seen(bot, trigger):
|
def seen(bot, trigger):
|
||||||
"""Reports when and where the user was last seen."""
|
"""Reports when and where the user was last seen."""
|
||||||
if not trigger.group(2):
|
parser = argparse.ArgumentParser()
|
||||||
bot.say(".seen <nick> - Reports when <nick> was last seen.")
|
parser.add_argument("nick")
|
||||||
return
|
parser.add_argument("-l", "--last", action="store_true")
|
||||||
nick = trigger.group(2).strip()
|
args = parser.parse_args(trigger.group(2).split())
|
||||||
|
|
||||||
|
# if not trigger.group(2):
|
||||||
|
# bot.say(".seen <nick> - Reports when <nick> was last seen.")
|
||||||
|
# return
|
||||||
|
# nick = trigger.group(2).strip()
|
||||||
|
nick = args.nick
|
||||||
if nick == bot.nick:
|
if nick == bot.nick:
|
||||||
bot.reply("I'm right here!")
|
bot.reply("I'm right here!")
|
||||||
return
|
return
|
||||||
|
@ -34,6 +42,9 @@ def seen(bot, trigger):
|
||||||
reltime = relativeTime(bot, nick, timestamp)
|
reltime = relativeTime(bot, nick, timestamp)
|
||||||
msg = "Last heard from \x0308{}\x03 at {} (\x0312{}\x03) in \x0312{}".format(nick, timestamp, reltime, channel)
|
msg = "Last heard from \x0308{}\x03 at {} (\x0312{}\x03) in \x0312{}".format(nick, timestamp, reltime, channel)
|
||||||
|
|
||||||
|
if args.last:
|
||||||
|
msg += "\x03 with \"\x0308{}\x03\"".format(message)
|
||||||
|
|
||||||
bot.reply(msg)
|
bot.reply(msg)
|
||||||
else:
|
else:
|
||||||
bot.say("I haven't seen \x0308{}".format(nick))
|
bot.say("I haven't seen \x0308{}".format(nick))
|
||||||
|
|
|
@ -124,7 +124,7 @@ def f_remind(bot, trigger):
|
||||||
dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell
|
dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell
|
||||||
|
|
||||||
|
|
||||||
def getReminders(bot, channel, key, tellee):
|
def getReminders(bot, key, tellee):
|
||||||
lines = []
|
lines = []
|
||||||
template = "%s: \x0310%s\x03 (\x0308%s\x03) %s [\x0312%s\x03]"
|
template = "%s: \x0310%s\x03 (\x0308%s\x03) %s [\x0312%s\x03]"
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ def getReminders(bot, channel, key, tellee):
|
||||||
try:
|
try:
|
||||||
del bot.memory['reminders'][key]
|
del bot.memory['reminders'][key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
bot.msg(channel, 'Er...')
|
bot.say('Er...')
|
||||||
finally:
|
finally:
|
||||||
bot.memory['tell_lock'].release()
|
bot.memory['tell_lock'].release()
|
||||||
return lines
|
return lines
|
||||||
|
@ -147,7 +147,6 @@ def getReminders(bot, channel, key, tellee):
|
||||||
def message(bot, trigger):
|
def message(bot, trigger):
|
||||||
|
|
||||||
tellee = trigger.nick
|
tellee = trigger.nick
|
||||||
channel = trigger.sender
|
|
||||||
|
|
||||||
if not os.path.exists(bot.tell_filename):
|
if not os.path.exists(bot.tell_filename):
|
||||||
return
|
return
|
||||||
|
@ -158,9 +157,9 @@ def message(bot, trigger):
|
||||||
for remkey in remkeys:
|
for remkey in remkeys:
|
||||||
if not remkey.endswith('*') or remkey.endswith(':'):
|
if not remkey.endswith('*') or remkey.endswith(':'):
|
||||||
if tellee == remkey:
|
if tellee == remkey:
|
||||||
reminders.extend(getReminders(bot, channel, remkey, tellee))
|
reminders.extend(getReminders(bot, remkey, tellee))
|
||||||
elif tellee.startswith(remkey.rstrip('*:')):
|
elif tellee.startswith(remkey.rstrip('*:')):
|
||||||
reminders.extend(getReminders(bot, channel, remkey, tellee))
|
reminders.extend(getReminders(bot, remkey, tellee))
|
||||||
|
|
||||||
for line in reminders[:maximum]:
|
for line in reminders[:maximum]:
|
||||||
bot.say(line)
|
bot.say(line)
|
||||||
|
@ -168,7 +167,7 @@ def message(bot, trigger):
|
||||||
if reminders[maximum:]:
|
if reminders[maximum:]:
|
||||||
bot.say('Further messages sent privately')
|
bot.say('Further messages sent privately')
|
||||||
for line in reminders[maximum:]:
|
for line in reminders[maximum:]:
|
||||||
bot.msg(tellee, line)
|
bot.say(line, tellee)
|
||||||
|
|
||||||
if len(bot.memory['reminders'].keys()) != remkeys:
|
if len(bot.memory['reminders'].keys()) != remkeys:
|
||||||
dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell
|
dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell
|
||||||
|
|
|
@ -15,6 +15,7 @@ if sys.version_info.major >= 3:
|
||||||
@example('.u ‽', 'U+203D INTERROBANG (‽)')
|
@example('.u ‽', 'U+203D INTERROBANG (‽)')
|
||||||
@example('.u 203D', 'U+203D INTERROBANG (‽)')
|
@example('.u 203D', 'U+203D INTERROBANG (‽)')
|
||||||
def codepoint(bot, trigger):
|
def codepoint(bot, trigger):
|
||||||
|
"""Looks up unicode information."""
|
||||||
arg = trigger.group(2)
|
arg = trigger.group(2)
|
||||||
if not arg:
|
if not arg:
|
||||||
bot.reply('What code point do you want me to look up?')
|
bot.reply('What code point do you want me to look up?')
|
||||||
|
|
|
@ -122,7 +122,7 @@ def weather(bot, trigger):
|
||||||
if not location:
|
if not location:
|
||||||
woeid = bot.db.get_nick_value(trigger.nick, 'woeid')
|
woeid = bot.db.get_nick_value(trigger.nick, 'woeid')
|
||||||
if not woeid:
|
if not woeid:
|
||||||
return bot.msg(trigger.sender, "I don't know where you live. " +
|
return bot.say("I don't know where you live. " +
|
||||||
'Give me a location, like .weather London, or tell me where you live by saying .setlocation London, for example.')
|
'Give me a location, like .weather London, or tell me where you live by saying .setlocation London, for example.')
|
||||||
else:
|
else:
|
||||||
location = location.strip()
|
location = location.strip()
|
||||||
|
|
|
@ -16,6 +16,9 @@ import wolframalpha
|
||||||
@example('.wa 2+2', '[W|A] 2+2 = 4')
|
@example('.wa 2+2', '[W|A] 2+2 = 4')
|
||||||
@example('.wa python language release date', '[W|A] Python | date introduced = 1991')
|
@example('.wa python language release date', '[W|A] Python | date introduced = 1991')
|
||||||
def wa_command(bot, trigger):
|
def wa_command(bot, trigger):
|
||||||
|
"""
|
||||||
|
Queries WolframAlpha.
|
||||||
|
"""
|
||||||
msg = None
|
msg = None
|
||||||
if not trigger.group(2):
|
if not trigger.group(2):
|
||||||
msg = 'You must provide a query.'
|
msg = 'You must provide a query.'
|
||||||
|
|
351
run_script.py
351
run_script.py
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env python2.7
|
#! /usr/bin/env python3
|
||||||
# coding=utf-8
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Sopel - An IRC Bot
|
Sopel - An IRC Bot
|
||||||
Copyright 2008, Sean B. Palmer, inamidst.com
|
Copyright 2008, Sean B. Palmer, inamidst.com
|
||||||
|
@ -8,18 +8,8 @@ Licensed under the Eiffel Forum License 2.
|
||||||
|
|
||||||
http://sopel.chat
|
http://sopel.chat
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals, absolute_import, print_function, division
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from tools import stderr
|
from tools import stderr
|
||||||
|
|
||||||
if sys.version_info < (2, 7):
|
|
||||||
stderr('Error: Requires Python 2.7 or later. Try python2.7 sopel')
|
|
||||||
sys.exit(1)
|
|
||||||
if sys.version_info.major == 3 and sys.version_info.minor < 3:
|
|
||||||
stderr('Error: When running on Python 3, Python 3.3 is required.')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import signal
|
import signal
|
||||||
|
@ -32,174 +22,209 @@ homedir = os.path.join(os.path.expanduser('~'), '.sopel')
|
||||||
|
|
||||||
|
|
||||||
def enumerate_configs(extension='.cfg'):
|
def enumerate_configs(extension='.cfg'):
|
||||||
configfiles = []
|
configfiles = []
|
||||||
if os.path.isdir(homedir):
|
if os.path.isdir(homedir):
|
||||||
sopel_dotdirfiles = os.listdir(homedir) # Preferred
|
sopel_dotdirfiles = os.listdir(homedir) # Preferred
|
||||||
for item in sopel_dotdirfiles:
|
for item in sopel_dotdirfiles:
|
||||||
if item.endswith(extension):
|
if item.endswith(extension):
|
||||||
configfiles.append(item)
|
configfiles.append(item)
|
||||||
|
|
||||||
return configfiles
|
return configfiles
|
||||||
|
|
||||||
|
|
||||||
def find_config(name, extension='.cfg'):
|
def find_config(name, extension='.cfg'):
|
||||||
if os.path.isfile(name):
|
if os.path.isfile(name):
|
||||||
return name
|
return name
|
||||||
configs = enumerate_configs(extension)
|
configs = enumerate_configs(extension)
|
||||||
if name in configs or name + extension in configs:
|
if name in configs or name + extension in configs:
|
||||||
if name + extension in configs:
|
if name + extension in configs:
|
||||||
name = name + extension
|
name = name + extension
|
||||||
|
|
||||||
return os.path.join(homedir, name)
|
return os.path.join(homedir, name)
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
global homedir
|
global homedir
|
||||||
# Step One: Parse The Command Line
|
# Step One: Parse The Command Line
|
||||||
try:
|
try:
|
||||||
parser = argparse.ArgumentParser(description='Sopel IRC Bot',
|
parser = argparse.ArgumentParser(
|
||||||
usage='%(prog)s [options]')
|
description='Sopel IRC Bot',
|
||||||
parser.add_argument('-c', '--config', metavar='filename',
|
usage='%(prog)s [options]')
|
||||||
help='use a specific configuration file')
|
parser.add_argument(
|
||||||
parser.add_argument("-d", '--fork', action="store_true",
|
'-c',
|
||||||
dest="daemonize", help="Daemonize sopel")
|
'--config',
|
||||||
parser.add_argument("-q", '--quit', action="store_true", dest="quit",
|
metavar='filename',
|
||||||
help="Gracefully quit Sopel")
|
help='use a specific configuration file')
|
||||||
parser.add_argument("-k", '--kill', action="store_true", dest="kill",
|
parser.add_argument(
|
||||||
help="Kill Sopel")
|
"-d",
|
||||||
parser.add_argument("-l", '--list', action="store_true",
|
'--fork',
|
||||||
dest="list_configs",
|
action="store_true",
|
||||||
help="List all config files found")
|
dest="daemonize",
|
||||||
parser.add_argument("-m", '--migrate', action="store_true",
|
help="Daemonize sopel")
|
||||||
dest="migrate_configs",
|
parser.add_argument(
|
||||||
help="Migrate config files to the new format")
|
"-q",
|
||||||
parser.add_argument('--quiet', action="store_true", dest="quiet",
|
'--quit',
|
||||||
help="Supress all output")
|
action="store_true",
|
||||||
parser.add_argument('-w', '--configure-all', action='store_true',
|
dest="quit",
|
||||||
dest='wizard', help='Run the configuration wizard.')
|
help="Gracefully quit Sopel")
|
||||||
parser.add_argument('--configure-modules', action='store_true',
|
parser.add_argument(
|
||||||
dest='mod_wizard', help=(
|
"-k",
|
||||||
'Run the configuration wizard, but only for the '
|
'--kill',
|
||||||
'module configuration options.'))
|
action="store_true",
|
||||||
parser.add_argument('-v', '--version', action="store_true",
|
dest="kill",
|
||||||
dest="version", help="Show version number and exit")
|
help="Kill Sopel")
|
||||||
if argv:
|
parser.add_argument(
|
||||||
opts = parser.parse_args(argv)
|
"-l",
|
||||||
else:
|
'--list',
|
||||||
opts = parser.parse_args()
|
action="store_true",
|
||||||
|
dest="list_configs",
|
||||||
|
help="List all config files found")
|
||||||
|
parser.add_argument(
|
||||||
|
"-m",
|
||||||
|
'--migrate',
|
||||||
|
action="store_true",
|
||||||
|
dest="migrate_configs",
|
||||||
|
help="Migrate config files to the new format")
|
||||||
|
parser.add_argument(
|
||||||
|
'--quiet',
|
||||||
|
action="store_true",
|
||||||
|
dest="quiet",
|
||||||
|
help="Supress all output")
|
||||||
|
parser.add_argument(
|
||||||
|
'-w',
|
||||||
|
'--configure-all',
|
||||||
|
action='store_true',
|
||||||
|
dest='wizard',
|
||||||
|
help='Run the configuration wizard.')
|
||||||
|
parser.add_argument(
|
||||||
|
'--configure-modules',
|
||||||
|
action='store_true',
|
||||||
|
dest='mod_wizard',
|
||||||
|
help='Run the configuration wizard, but only for the module ' + \
|
||||||
|
'configuration options.')
|
||||||
|
parser.add_argument(
|
||||||
|
'-v',
|
||||||
|
'--version',
|
||||||
|
action="store_true",
|
||||||
|
dest="version",
|
||||||
|
help="Show version number and exit")
|
||||||
|
if argv:
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
else:
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Step Two: "Do not run as root" checks.
|
# Step Two: "Do not run as root" checks.
|
||||||
try:
|
try:
|
||||||
# Linux/Mac
|
# Linux/Mac
|
||||||
if os.getuid() == 0 or os.geteuid() == 0:
|
if os.getuid() == 0 or os.geteuid() == 0:
|
||||||
stderr('Error: Do not run Sopel with root privileges.')
|
stderr('Error: Do not run Sopel with root privileges.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Windows
|
# Windows
|
||||||
if os.environ.get("USERNAME") == "Administrator":
|
if os.environ.get("USERNAME") == "Administrator":
|
||||||
stderr('Error: Do not run Sopel as Administrator.')
|
stderr('Error: Do not run Sopel as Administrator.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if opts.version:
|
if args.version:
|
||||||
py_ver = '%s.%s.%s' % (sys.version_info.major,
|
py_ver = '%s.%s.%s' % (sys.version_info.major,
|
||||||
sys.version_info.minor,
|
sys.version_info.minor,
|
||||||
sys.version_info.micro)
|
sys.version_info.micro)
|
||||||
print('Sopel %s (running on python %s)' % (__version__, py_ver))
|
print('Sopel %s (running on python %s)' % (__version__, py_ver))
|
||||||
print('http://sopel.chat/')
|
print('http://sopel.chat/')
|
||||||
return
|
return
|
||||||
elif opts.wizard:
|
elif args.wizard:
|
||||||
_wizard('all', opts.config)
|
_wizard('all', args.config)
|
||||||
return
|
return
|
||||||
elif opts.mod_wizard:
|
elif args.mod_wizard:
|
||||||
_wizard('mod', opts.config)
|
_wizard('mod', args.config)
|
||||||
return
|
return
|
||||||
|
|
||||||
if opts.list_configs:
|
if args.list_configs:
|
||||||
configs = enumerate_configs()
|
configs = enumerate_configs()
|
||||||
print('Config files in ~/.sopel:')
|
print('Config files in ~/.sopel:')
|
||||||
if len(configs) is 0:
|
if len(configs) is 0:
|
||||||
print('\tNone found')
|
print('\tNone found')
|
||||||
else:
|
else:
|
||||||
for config in configs:
|
for config in configs:
|
||||||
print('\t%s' % config)
|
print('\t%s' % config)
|
||||||
print('-------------------------')
|
print('-------------------------')
|
||||||
return
|
return
|
||||||
|
|
||||||
config_name = opts.config or 'default'
|
config_name = args.config or 'default'
|
||||||
|
|
||||||
configpath = find_config(config_name)
|
configpath = find_config(config_name)
|
||||||
if not os.path.isfile(configpath):
|
if not os.path.isfile(configpath):
|
||||||
print("Welcome to Sopel!\nI can't seem to find the configuration file, so let's generate it!\n")
|
print("Welcome to Sopel!\nI can't seem to find the configuration file, so let's generate it!\n")
|
||||||
if not configpath.endswith('.cfg'):
|
if not configpath.endswith('.cfg'):
|
||||||
configpath = configpath + '.cfg'
|
configpath = configpath + '.cfg'
|
||||||
_create_config(configpath)
|
_create_config(configpath)
|
||||||
configpath = find_config(config_name)
|
configpath = find_config(config_name)
|
||||||
try:
|
try:
|
||||||
config_module = Config(configpath)
|
config_module = Config(configpath)
|
||||||
except ConfigurationError as e:
|
except ConfigurationError as e:
|
||||||
stderr(e)
|
stderr(e)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
if config_module.core.not_configured:
|
if config_module.core.not_configured:
|
||||||
stderr('Bot is not configured, can\'t start')
|
stderr('Bot is not configured, can\'t start')
|
||||||
# exit with code 2 to prevent auto restart on fail by systemd
|
# exit with code 2 to prevent auto restart on fail by systemd
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
logfile = os.path.os.path.join(config_module.core.logdir, 'stdio.log')
|
logfile = os.path.os.path.join(config_module.core.logdir, 'stdio.log')
|
||||||
|
|
||||||
config_module._is_daemonized = opts.daemonize
|
config_module._is_daemonized = args.daemonize
|
||||||
|
|
||||||
sys.stderr = tools.OutputRedirect(logfile, True, opts.quiet)
|
sys.stderr = tools.OutputRedirect(logfile, True, args.quiet)
|
||||||
sys.stdout = tools.OutputRedirect(logfile, False, opts.quiet)
|
sys.stdout = tools.OutputRedirect(logfile, False, args.quiet)
|
||||||
|
|
||||||
# Handle --quit, --kill and saving the PID to file
|
# Handle --quit, --kill and saving the PID to file
|
||||||
pid_dir = config_module.core.pid_dir
|
pid_dir = config_module.core.pid_dir
|
||||||
if opts.config is None:
|
if args.config is None:
|
||||||
pid_file_path = os.path.join(pid_dir, 'sopel.pid')
|
pid_file_path = os.path.join(pid_dir, 'sopel.pid')
|
||||||
else:
|
else:
|
||||||
basename = os.path.basename(opts.config)
|
basename = os.path.basename(args.config)
|
||||||
if basename.endswith('.cfg'):
|
if basename.endswith('.cfg'):
|
||||||
basename = basename[:-4]
|
basename = basename[:-4]
|
||||||
pid_file_path = os.path.join(pid_dir, 'sopel-%s.pid' % basename)
|
pid_file_path = os.path.join(pid_dir, 'sopel-%s.pid' % basename)
|
||||||
if os.path.isfile(pid_file_path):
|
if os.path.isfile(pid_file_path):
|
||||||
with open(pid_file_path, 'r') as pid_file:
|
with open(pid_file_path, 'r') as pid_file:
|
||||||
try:
|
try:
|
||||||
old_pid = int(pid_file.read())
|
old_pid = int(pid_file.read())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
old_pid = None
|
old_pid = None
|
||||||
if old_pid is not None and tools.check_pid(old_pid):
|
if old_pid is not None and tools.check_pid(old_pid):
|
||||||
if not opts.quit and not opts.kill:
|
if not args.quit and not args.kill:
|
||||||
stderr('There\'s already a Sopel instance running with this config file')
|
stderr('There\'s already a Sopel instance running with this config file')
|
||||||
stderr('Try using the --quit or the --kill options')
|
stderr('Try using the --quit or the --kill options')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif opts.kill:
|
elif args.kill:
|
||||||
stderr('Killing the sopel')
|
stderr('Killing the sopel')
|
||||||
os.kill(old_pid, signal.SIGKILL)
|
os.kill(old_pid, signal.SIGKILL)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif opts.quit:
|
elif args.quit:
|
||||||
stderr('Signaling Sopel to stop gracefully')
|
stderr('Signaling Sopel to stop gracefully')
|
||||||
if hasattr(signal, 'SIGUSR1'):
|
if hasattr(signal, 'SIGUSR1'):
|
||||||
os.kill(old_pid, signal.SIGUSR1)
|
os.kill(old_pid, signal.SIGUSR1)
|
||||||
else:
|
else:
|
||||||
os.kill(old_pid, signal.SIGTERM)
|
os.kill(old_pid, signal.SIGTERM)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif opts.kill or opts.quit:
|
elif args.kill or args.quit:
|
||||||
stderr('Sopel is not running!')
|
stderr('Sopel is not running!')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif opts.quit or opts.kill:
|
elif args.quit or args.kill:
|
||||||
stderr('Sopel is not running!')
|
stderr('Sopel is not running!')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if opts.daemonize:
|
if args.daemonize:
|
||||||
child_pid = os.fork()
|
child_pid = os.fork()
|
||||||
if child_pid is not 0:
|
if child_pid is not 0:
|
||||||
sys.exit()
|
sys.exit()
|
||||||
with open(pid_file_path, 'w') as pid_file:
|
with open(pid_file_path, 'w') as pid_file:
|
||||||
pid_file.write(str(os.getpid()))
|
pid_file.write(str(os.getpid()))
|
||||||
|
|
||||||
# Step Five: Initialise And Run sopel
|
# Step Five: Initialise And Run sopel
|
||||||
run(config_module, pid_file_path)
|
run(config_module, pid_file_path)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n\nInterrupted")
|
print("\n\nInterrupted")
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user