Merge pull request #37 from jammons/tzakrajs-master

Merge and fix PR breaking out RtmBot into its own Module
This commit is contained in:
Jeff Ammons 2016-04-24 12:46:43 -07:00
commit 735d0d774a
7 changed files with 130 additions and 97 deletions

5
.gitignore vendored
View file

@ -1,5 +1,10 @@
*.pyc *.pyc
/rtmbot.conf /rtmbot.conf
/plugins/** /plugins/**
/build/**
*.log
env env
.tox .tox
*.un~
0/
tests/.cache

View file

@ -51,7 +51,7 @@ To install the example 'repeat' plugin
The repeat plugin will now be loaded by the bot on startup. The repeat plugin will now be loaded by the bot on startup.
./rtmbot.py ./start_rtmbot.py
Create Plugins Create Plugins
-------- --------

1
rtmbot/__init__.py Normal file
View file

@ -0,0 +1 @@
from core import *

View file

@ -1,36 +1,53 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys import sys
import glob import glob
import yaml
import os import os
import time import time
import logging import logging
from argparse import ArgumentParser
from slackclient import SlackClient from slackclient import SlackClient
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
def dbg(debug_string):
if debug:
logging.info(debug_string)
class RtmBot(object): class RtmBot(object):
def __init__(self, token): def __init__(self, config):
# 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(self.directory)
self.debug = self.config.get('DEBUG', False)
# initialize stateful fields
self.last_ping = 0 self.last_ping = 0
self.token = token
self.bot_plugins = [] self.bot_plugins = []
self.slack_client = None self.slack_client = None
def _dbg(self, debug_string):
if self.debug:
logging.info(debug_string)
def connect(self): def connect(self):
"""Convenience method that creates Server instance""" """Convenience method that creates Server instance"""
self.slack_client = SlackClient(self.token) self.slack_client = SlackClient(self.token)
self.slack_client.rtm_connect() self.slack_client.rtm_connect()
def start(self): def _start(self):
self.connect() self.connect()
self.load_plugins() self.load_plugins()
while True: while True:
@ -41,6 +58,14 @@ class RtmBot(object):
self.autoping() self.autoping()
time.sleep(.1) 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): def autoping(self):
# hardcode the interval to 3 seconds # hardcode the interval to 3 seconds
now = int(time.time()) now = int(time.time())
@ -51,7 +76,7 @@ class RtmBot(object):
def input(self, data): def input(self, data):
if "type" in data: if "type" in data:
function_name = "process_" + data["type"] function_name = "process_" + data["type"]
dbg("got {}".format(function_name)) self._dbg("got {}".format(function_name))
for plugin in self.bot_plugins: for plugin in self.bot_plugins:
plugin.register_jobs() plugin.register_jobs()
plugin.do(function_name, data) plugin.do(function_name, data)
@ -74,39 +99,46 @@ class RtmBot(object):
plugin.do_jobs() plugin.do_jobs()
def load_plugins(self): def load_plugins(self):
for plugin in glob.glob(directory + '/plugins/*'): for plugin in glob.glob(self.directory + '/plugins/*'):
sys.path.insert(0, plugin) sys.path.insert(0, plugin)
sys.path.insert(0, directory + '/plugins/') sys.path.insert(0, self.directory + '/plugins/')
for plugin in glob.glob(directory + '/plugins/*.py') + \ for plugin in glob.glob(self.directory + '/plugins/*.py') + \
glob.glob(directory + '/plugins/*/*.py'): glob.glob(self.directory + '/plugins/*/*.py'):
logging.info(plugin) logging.info(plugin)
name = plugin.split('/')[-1][:-3] name = plugin.split('/')[-1][:-3]
# try: if name in self.config:
self.bot_plugins.append(Plugin(name)) logging.info("config found for: " + name)
# except: plugin_config = self.config.get(name, {})
# print "error loading plugin %s" % name plugin_config['DEBUG'] = self.debug
self.bot_plugins.append(Plugin(name, plugin_config))
class Plugin(object): class Plugin(object):
def __init__(self, name, plugin_config=None): 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: if plugin_config is None:
plugin_config = {} # TODO: is this variable necessary? plugin_config = {}
self.name = name self.name = name
self.jobs = [] self.jobs = []
self.module = __import__(name) self.module = __import__(name)
self.module.config = plugin_config
self.debug = self.module.config.get('DEBUG', False)
self.register_jobs() self.register_jobs()
self.outputs = [] self.outputs = []
if name in config:
logging.info("config found for: " + name)
self.module.config = config[name]
if 'setup' in dir(self.module): if 'setup' in dir(self.module):
self.module.setup() self.module.setup()
def register_jobs(self): def register_jobs(self):
if 'crontable' in dir(self.module): if 'crontable' in dir(self.module):
for interval, function in self.module.crontable: for interval, function in self.module.crontable:
self.jobs.append(Job(interval, eval("self.module." + function))) self.jobs.append(Job(interval, eval("self.module." + function), self.debug))
logging.info(self.module.crontable) logging.info(self.module.crontable)
self.module.crontable = [] self.module.crontable = []
else: else:
@ -114,19 +146,24 @@ class Plugin(object):
def do(self, function_name, data): def do(self, function_name, data):
if function_name in dir(self.module): if function_name in dir(self.module):
if self.debug is True:
# this makes the plugin fail with stack trace in debug mode # this makes the plugin fail with stack trace in debug mode
if not debug: eval("self.module." + function_name)(data)
else:
# otherwise we log the exception and carry on
try: try:
eval("self.module." + function_name)(data) eval("self.module." + function_name)(data)
except: except Exception:
dbg("problem in module {} {}".format(function_name, data)) logging.exception("problem in module {} {}".format(function_name, data))
else:
eval("self.module." + function_name)(data)
if "catch_all" in dir(self.module): 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: try:
self.module.catch_all(data) self.module.catch_all(data)
except: except Exception:
dbg("problem in catch all") logging.exception("problem in catch all: {} {}".format(self.module, data))
def do_jobs(self): def do_jobs(self):
for job in self.jobs: for job in self.jobs:
@ -147,10 +184,11 @@ class Plugin(object):
class Job(object): class Job(object):
def __init__(self, interval, function): def __init__(self, interval, function, debug):
self.function = function self.function = function
self.interval = interval self.interval = interval
self.lastrun = 0 self.lastrun = 0
self.debug = debug
def __str__(self): def __str__(self):
return "{} {} {}".format(self.function, self.interval, self.lastrun) return "{} {} {}".format(self.function, self.interval, self.lastrun)
@ -160,67 +198,17 @@ class Job(object):
def check(self): def check(self):
if self.lastrun + self.interval < time.time(): if self.lastrun + self.interval < time.time():
if not debug: 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: try:
self.function() self.function()
except: except Exception:
dbg("problem") logging.exception("Problem in job check: {}".format(self.function))
else:
self.function()
self.lastrun = time.time() self.lastrun = time.time()
pass
class UnknownChannel(Exception): class UnknownChannel(Exception):
pass pass
def main_loop():
if "LOGFILE" in config:
logging.basicConfig(
filename=config["LOGFILE"],
level=logging.INFO,
format='%(asctime)s %(message)s'
)
logging.info(directory)
try:
bot.start()
except KeyboardInterrupt:
sys.exit(0)
except:
logging.exception('OOPS')
def parse_args():
parser = ArgumentParser()
parser.add_argument(
'-c',
'--config',
help='Full path to config file.',
metavar='path'
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
directory = os.path.dirname(sys.argv[0])
if not directory.startswith('/'):
directory = os.path.abspath("{}/{}".format(os.getcwd(),
directory
))
config = yaml.load(open(args.config or 'rtmbot.conf', 'r'))
debug = config["DEBUG"]
bot = RtmBot(config["SLACK_TOKEN"])
site_plugins = []
files_currently_downloading = []
job_hash = {}
if 'DAEMON' in config:
if config["DAEMON"]:
import daemon
with daemon.DaemonContext():
main_loop()
main_loop()

13
setup.py Executable file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env python
from distutils.core import setup
setup(
name='rtmbot',
version='0.10',
description='A Slack bot written in python that connects via the RTM API.',
author='Ryan Huber',
author_email='rhuber@gmail.com',
url='https://github.com/slackhq/python-rtmbot',
packages=['rtmbot'],
)

26
start_rtmbot.py Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env python
import sys
from argparse import ArgumentParser
import yaml
from rtmbot import RtmBot
def parse_args():
parser = ArgumentParser()
parser.add_argument(
'-c',
'--config',
help='Full path to config file.',
metavar='path'
)
return parser.parse_args()
# load args with config path
args = parse_args()
config = yaml.load(open(args.config or 'rtmbot.conf', 'r'))
bot = RtmBot(config)
try:
bot.start()
except KeyboardInterrupt:
sys.exit(0)

View file

@ -20,4 +20,4 @@ basepython =
[testenv:flake8] [testenv:flake8]
basepython=python basepython=python
deps=flake8 deps=flake8
commands=flake8 {toxinidir}/rtmbot.py {toxinidir}/doc/example-plugins commands=flake8 {toxinidir}/start_rtmbot.py {toxinidir}/rtmbot/core.py {toxinidir}/setup.py {toxinidir}/doc/example-plugins