# -*- coding: utf-8 -*-
# Copyright (C) 2013-2015 Samuel Damashek, Peter Foley, James Forcier, Srijay Kasturi, Reed Koser, Christopher Reffett, and Fox Wilson
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import argparse
import functools
import importlib
import logging
import multiprocessing
import signal
import ssl
import sys
import threading
import traceback
from os import path
if sys.version_info < (3, 4, 3):
# Dependency on importlib.reload and urlopen(context=context)
raise Exception("Need Python 3.4.3 or higher.")
import queue # noqa
from irc import bot, client, connection # noqa
from . import backtrace, config, handler, misc, reloader, server # noqa
[docs]class IrcBot(bot.SingleServerIRCBot):
def __init__(self, confdir):
"""Setup everything."""
signal.signal(signal.SIGTERM, self.shutdown)
self.confdir = confdir
config_file = path.join(confdir, 'config.cfg')
if not path.exists(config_file):
logging.info("Setting up config file")
config.do_setup(config_file)
sys.exit(0)
self.config = config.load_config(config_file, logging.info)
if self.config.getboolean('core', 'ssl'):
factory = connection.Factory(wrapper=ssl.wrap_socket, ipv6=self.config.getboolean('core', 'ipv6'))
else:
factory = connection.Factory(ipv6=self.config.getboolean('core', 'ipv6'))
passwd = None if self.config.getboolean('core', 'sasl') else self.config['auth']['serverpass']
serverinfo = bot.ServerSpec(self.config['core']['host'], self.config.getint('core', 'ircport'), passwd)
nick = self.config['core']['nick']
self.reactor_class = functools.partial(client.Reactor, on_connect=self.do_cap)
super().__init__([serverinfo], nick, nick, connect_factory=factory, reconnection_interval=5)
# These allow reload events to be processed when a reload has failed.
self.connection.add_global_handler("pubmsg", self.reload_handler, -30)
self.connection.add_global_handler("privmsg", self.reload_handler, -30)
self.connection.add_global_handler("all_events", self.handle_event, 10)
# We need to get the channels that a nick is currently in before the regular quit event is processed and the nick is removed from self.channels.
self.connection.add_global_handler("quit", self.handle_quit, -21)
self.event_queue = queue.Queue()
# Are we running in bare-bones, reload-only mode?
self.reload_event = threading.Event()
# fix unicode problems
self.connection.buffer_class.errors = 'replace'
if not reloader.load_modules(self.config, confdir):
raise Exception("Failed to load modules.")
self.handler = handler.BotHandler(self.config, self.connection, self.channels, confdir)
if self.config['feature'].getboolean('server'):
self.server = server.init_server(self)
[docs] def handle_event(self, c, e):
handled_types = ['account', 'action', 'authenticate', 'bannedfromchan', 'cap', 'ctcpreply', 'error', 'featurelist', 'join', 'kick',
'mode', 'nicknameinuse', 'nosuchnick', 'nick', 'part', 'privmsg', 'privnotice', 'pubnotice', 'pubmsg', 'topic', 'welcome', 'whospcrpl']
# We only need to do stuff for a sub-set of events.
if e.type not in handled_types:
return
if self.reload_event.is_set():
# Don't queue up failed reloads.
if self.is_reload(e) is None:
self.event_queue.put(e)
else:
# Handle any queued events first.
while not self.event_queue.empty():
self.handle_msg(c, self.event_queue.get_nowait())
self.handle_msg(c, e)
[docs] def get_version(self):
"""Get the version."""
_, version = misc.get_version(self.confdir)
if version is None:
return "Can't get the version."
else:
return "cslbot - %s" % version
[docs] def do_cap(self, _):
self.connection.cap('REQ', 'account-notify')
self.connection.cap('REQ', 'extended-join')
if self.config.getboolean('core', 'sasl'):
self.connection.cap('REQ', 'sasl')
else:
self.connection.cap('END')
@staticmethod
[docs] def get_target(e):
if e.target[0] in ['#', '&', '+', '!']:
return e.target
else:
return e.source.nick
[docs] def shutdown(self, *_):
if hasattr(self, 'connection'):
self.connection.disconnect("Bot received SIGTERM")
self.shutdown_mp(False)
sys.exit(0)
[docs] def shutdown_mp(self, clean=True):
""" Shutdown all the multiprocessing.
:param bool clean: Whether to shutdown things cleanly, or force a quick and dirty shutdown.
"""
# The server runs on a worker thread, so we need to shut it down first.
if hasattr(self, 'server'):
self.server.socket.close()
self.server.shutdown()
if hasattr(self, 'handler'):
self.handler.workers.stop_workers(clean)
[docs] def handle_quit(self, _, e):
# Log quits.
for channel in misc.get_channels(self.channels, e.source.nick):
self.handler.do_log(channel, e.source, e.arguments[0], 'quit')
# If we're the one quiting, shut things down cleanly.
# If it's an Excess Flood or other server-side quit we want to reconnect.
if e.source.nick == self.connection.real_nickname and e.arguments[0] in ['Client Quit', 'Quit: Goodbye, Cruel World!']:
self.connection.close()
self.shutdown_mp()
sys.exit(0)
[docs] def handle_msg(self, c, e):
"""Handles all messages.
| If a exception is thrown, catch it and display a nice traceback instead of crashing.
| Do the appropriate processing for each event type.
"""
try:
self.handler.handle_msg(c, e)
except Exception as ex:
backtrace.handle_traceback(ex, c, self.get_target(e), self.config)
[docs] def is_reload(self, e):
if not e.arguments:
return None
cmd = e.arguments[0].strip()
if not cmd:
return None
cmd = misc.get_cmdchar(self.config, self.connection, cmd, e.type)
cmdchar = self.config['core']['cmdchar']
if cmd.startswith('%sreload' % cmdchar):
return cmd
else:
return None
[docs] def reload_handler(self, c, e):
"""This handles reloads."""
cmd = self.is_reload(e)
cmdchar = self.config['core']['cmdchar']
if cmd is not None:
admins = [x.strip() for x in self.config['auth']['admins'].split(',')]
if e.source.nick not in admins:
c.privmsg(self.get_target(e), "Nope, not gonna do it.")
return
importlib.reload(reloader)
self.reload_event.set()
cmdargs = cmd[len('%sreload' % cmdchar) + 1:]
try:
if reloader.do_reload(self, self.get_target(e), cmdargs):
if self.config.getboolean('feature', 'server'):
self.server = server.init_server(self)
self.reload_event.clear()
logging.info("Successfully reloaded")
except Exception as ex:
backtrace.handle_traceback(ex, c, self.get_target(e), self.config)
[docs]def init(confdir="/etc/cslbot"):
"""The bot's main entry point.
| Initialize the bot and start processing messages.
"""
multiprocessing.set_start_method('spawn')
parser = argparse.ArgumentParser()
parser.add_argument('-d', '--debug', help='Enable debug logging.', action='store_true')
parser.add_argument('--validate', help='Initialize the db and perform other sanity checks.', action='store_true')
args = parser.parse_args()
loglevel = logging.DEBUG if args.debug else logging.INFO
logging.basicConfig(level=loglevel, format="%(asctime)s %(levelname)s:%(module)s:%(message)s")
# We don't need a bunch of output from the requests module.
logging.getLogger("requests").setLevel(logging.WARNING)
cslbot = IrcBot(confdir)
if args.validate:
cslbot.shutdown_mp()
print("Everything is ready to go!")
return
try:
cslbot.start()
except KeyboardInterrupt:
# KeyboardInterrupt means someone tried to ^C, so shut down the bot
cslbot.disconnect('Bot received a Ctrl-C')
cslbot.shutdown_mp()
sys.exit(0)
except Exception as ex:
cslbot.shutdown_mp(False)
logging.error("The bot died! %s", ex)
output = "".join(traceback.format_exc()).strip()
for line in output.split('\n'):
logging.error(line)
sys.exit(1)