
I added unicode and non-unicode message testing for the output() method on RTMBot. I'm concerned about why the encode('ascii', 'ignore') was there in the first place, but not having it seems to do the right thing...
225 lines
7.7 KiB
Python
Executable file
225 lines
7.7 KiB
Python
Executable file
#!/usr/bin/env python
|
|
import sys
|
|
import glob
|
|
import os
|
|
import time
|
|
import logging
|
|
|
|
from slackclient import SlackClient
|
|
|
|
sys.dont_write_bytecode = True
|
|
|
|
|
|
class RtmBot(object):
|
|
def __init__(self, config):
|
|
'''
|
|
Params:
|
|
- config (dict):
|
|
- SLACK_TOKEN: your authentication token from Slack
|
|
- BASE_PATH (optional: defaults to execution directory) RtmBot will
|
|
look in this directory for plugins.
|
|
- LOGFILE (optional: defaults to rtmbot.log) The filename for logs, will
|
|
be stored inside the BASE_PATH directory
|
|
- DEBUG (optional: defaults to False) with debug enabled, RtmBot will
|
|
break on errors
|
|
'''
|
|
# set the config object
|
|
self.config = config
|
|
|
|
# set slack token
|
|
self.token = config.get('SLACK_TOKEN')
|
|
|
|
# set working directory for loading plugins or other files
|
|
working_directory = os.path.dirname(sys.argv[0])
|
|
self.directory = self.config.get('BASE_PATH', working_directory)
|
|
if not self.directory.startswith('/'):
|
|
path = '{}/{}'.format(os.getcwd(), self.directory)
|
|
self.directory = os.path.abspath(path)
|
|
|
|
# establish logging
|
|
log_file = config.get('LOGFILE', 'rtmbot.log')
|
|
logging.basicConfig(filename=log_file,
|
|
level=logging.INFO,
|
|
format='%(asctime)s %(message)s')
|
|
logging.info('Initialized in: {}'.format(self.directory))
|
|
self.debug = self.config.get('DEBUG', False)
|
|
|
|
# initialize stateful fields
|
|
self.last_ping = 0
|
|
self.bot_plugins = []
|
|
self.slack_client = None
|
|
|
|
def _dbg(self, debug_string):
|
|
if self.debug:
|
|
logging.info(debug_string)
|
|
|
|
def connect(self):
|
|
"""Convenience method that creates Server instance"""
|
|
self.slack_client = SlackClient(self.token)
|
|
self.slack_client.rtm_connect()
|
|
|
|
def _start(self):
|
|
self.connect()
|
|
self.load_plugins()
|
|
while True:
|
|
for reply in self.slack_client.rtm_read():
|
|
self.input(reply)
|
|
self.crons()
|
|
self.output()
|
|
self.autoping()
|
|
time.sleep(.1)
|
|
|
|
def start(self):
|
|
if 'DAEMON' in self.config:
|
|
if self.config.get('DAEMON'):
|
|
import daemon
|
|
with daemon.DaemonContext():
|
|
self._start()
|
|
self._start()
|
|
|
|
def autoping(self):
|
|
# hardcode the interval to 3 seconds
|
|
now = int(time.time())
|
|
if now > self.last_ping + 3:
|
|
self.slack_client.server.ping()
|
|
self.last_ping = now
|
|
|
|
def input(self, data):
|
|
if "type" in data:
|
|
function_name = "process_" + data["type"]
|
|
self._dbg("got {}".format(function_name))
|
|
for plugin in self.bot_plugins:
|
|
plugin.register_jobs()
|
|
plugin.do(function_name, data)
|
|
|
|
def output(self):
|
|
for plugin in self.bot_plugins:
|
|
limiter = False
|
|
for output in plugin.do_output():
|
|
channel = self.slack_client.server.channels.find(output[0])
|
|
if channel is not None and output[1] is not None:
|
|
if limiter:
|
|
time.sleep(.1)
|
|
limiter = False
|
|
message = output[1]
|
|
channel.send_message("{}".format(message))
|
|
limiter = True
|
|
|
|
def crons(self):
|
|
for plugin in self.bot_plugins:
|
|
plugin.do_jobs()
|
|
|
|
def load_plugins(self):
|
|
for plugin in glob.glob(self.directory + '/plugins/*'):
|
|
sys.path.insert(0, plugin)
|
|
sys.path.insert(0, self.directory + '/plugins/')
|
|
for plugin in glob.glob(self.directory + '/plugins/*.py') + \
|
|
glob.glob(self.directory + '/plugins/*/*.py'):
|
|
logging.info(plugin)
|
|
name = plugin.split('/')[-1][:-3]
|
|
if name in self.config:
|
|
logging.info("config found for: " + name)
|
|
plugin_config = self.config.get(name, {})
|
|
plugin_config['DEBUG'] = self.debug
|
|
self.bot_plugins.append(Plugin(name, plugin_config))
|
|
|
|
|
|
class Plugin(object):
|
|
|
|
def __init__(self, name, plugin_config=None):
|
|
'''
|
|
A plugin in initialized with:
|
|
- name (str)
|
|
- plugin config (dict) - (from the yaml config)
|
|
Values in config:
|
|
- DEBUG (bool) - this will be overridden if debug is set in config for this plugin
|
|
'''
|
|
if plugin_config is None:
|
|
plugin_config = {}
|
|
self.name = name
|
|
self.jobs = []
|
|
self.module = __import__(name)
|
|
self.module.config = plugin_config
|
|
self.debug = self.module.config.get('DEBUG', False)
|
|
self.register_jobs()
|
|
self.outputs = []
|
|
if 'setup' in dir(self.module):
|
|
self.module.setup()
|
|
|
|
def register_jobs(self):
|
|
if 'crontable' in dir(self.module):
|
|
for interval, function in self.module.crontable:
|
|
self.jobs.append(Job(interval, eval("self.module." + function), self.debug))
|
|
logging.info(self.module.crontable)
|
|
self.module.crontable = []
|
|
else:
|
|
self.module.crontable = []
|
|
|
|
def do(self, function_name, data):
|
|
if function_name in dir(self.module):
|
|
if self.debug is True:
|
|
# this makes the plugin fail with stack trace in debug mode
|
|
eval("self.module." + function_name)(data)
|
|
else:
|
|
# otherwise we log the exception and carry on
|
|
try:
|
|
eval("self.module." + function_name)(data)
|
|
except Exception:
|
|
logging.exception("problem in module {} {}".format(function_name, data))
|
|
if "catch_all" in dir(self.module):
|
|
if self.debug is True:
|
|
# this makes the plugin fail with stack trace in debug mode
|
|
self.module.catch_all(data)
|
|
else:
|
|
try:
|
|
self.module.catch_all(data)
|
|
except Exception:
|
|
logging.exception("problem in catch all: {} {}".format(self.module, data))
|
|
|
|
def do_jobs(self):
|
|
for job in self.jobs:
|
|
job.check()
|
|
|
|
def do_output(self):
|
|
output = []
|
|
while True:
|
|
if 'outputs' in dir(self.module):
|
|
if len(self.module.outputs) > 0:
|
|
logging.info("output from {}".format(self.module))
|
|
output.append(self.module.outputs.pop(0))
|
|
else:
|
|
break
|
|
else:
|
|
self.module.outputs = []
|
|
return output
|
|
|
|
|
|
class Job(object):
|
|
def __init__(self, interval, function, debug):
|
|
self.function = function
|
|
self.interval = interval
|
|
self.lastrun = 0
|
|
self.debug = debug
|
|
|
|
def __str__(self):
|
|
return "{} {} {}".format(self.function, self.interval, self.lastrun)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def check(self):
|
|
if self.lastrun + self.interval < time.time():
|
|
if self.debug is True:
|
|
# this makes the plugin fail with stack trace in debug mode
|
|
self.function()
|
|
else:
|
|
# otherwise we log the exception and carry on
|
|
try:
|
|
self.function()
|
|
except Exception:
|
|
logging.exception("Problem in job check: {}".format(self.function))
|
|
self.lastrun = time.time()
|
|
|
|
|
|
class UnknownChannel(Exception):
|
|
pass
|