From c6a8b30d2da66b36cf1683565a2fb72e8462b02d Mon Sep 17 00:00:00 2001 From: Thomas Zakrajsek Date: Sun, 2 Aug 2015 18:26:03 -0400 Subject: [PATCH 01/13] ignore build dirs from setup.py --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a044a8c..38e97ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc /rtmbot.conf /plugins/** +/build/** env From 45e9f7780c6fd39dad5166c62f6fed6b65c8ec8c Mon Sep 17 00:00:00 2001 From: Thomas Zakrajsek Date: Sun, 2 Aug 2015 18:29:11 -0400 Subject: [PATCH 02/13] refactored main into start_rtmbot.py, refactored so that RtmBot class is more self sufficient and can be imported and used more easily, removed unused objects from main --- rtmbot/__init__.py | 3 + rtmbot.py => rtmbot/core.py | 125 +++++++++++++++++------------------- start_rtmbot.py | 21 ++++++ 3 files changed, 84 insertions(+), 65 deletions(-) create mode 100644 rtmbot/__init__.py rename rtmbot.py => rtmbot/core.py (68%) create mode 100755 start_rtmbot.py diff --git a/rtmbot/__init__.py b/rtmbot/__init__.py new file mode 100644 index 0000000..56b4646 --- /dev/null +++ b/rtmbot/__init__.py @@ -0,0 +1,3 @@ +from core import * + +site_config = {} diff --git a/rtmbot.py b/rtmbot/core.py similarity index 68% rename from rtmbot.py rename to rtmbot/core.py index 3bd4184..3fa2a39 100755 --- a/rtmbot.py +++ b/rtmbot/core.py @@ -14,21 +14,42 @@ from argparse import ArgumentParser from slackclient import SlackClient -def dbg(debug_string): - if debug: - logging.info(debug_string) - class RtmBot(object): - def __init__(self, token): + def __init__(self, config): + #set the config object + self.config = config + global site_config + site_config = self.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.has_key('DEBUG') + #initialize stateful fields self.last_ping = 0 - self.token = token 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): + + def _start(self): self.connect() self.load_plugins() while True: @@ -38,19 +59,30 @@ class RtmBot(object): self.output() self.autoping() time.sleep(.1) + + def start(self): + if self.config.has_key('DAEMON'): + 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"] - dbg("got {}".format(function_name)) + 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 @@ -63,18 +95,23 @@ class RtmBot(object): message = output[1].encode('ascii','ignore') 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(directory+'/plugins/*'): + for plugin in glob.glob(self.directory+'/plugins/*'): sys.path.insert(0, plugin) - sys.path.insert(0, directory+'/plugins/') - for plugin in glob.glob(directory+'/plugins/*.py') + glob.glob(directory+'/plugins/*/*.py'): + 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] # try: - self.bot_plugins.append(Plugin(name)) + if name in self.config: + logging.info("config found for: " + name) + plugin_config = self.config.get(name) + self.bot_plugins.append(Plugin(name, plugin_config)) # except: # print "error loading plugin %s" % name @@ -83,13 +120,12 @@ class Plugin(object): self.name = name self.jobs = [] self.module = __import__(name) + self.module.config = plugin_config self.register_jobs() self.outputs = [] - if name in config: - logging.info("config found for: " + name) - self.module.config = config[name] 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: @@ -98,6 +134,7 @@ class Plugin(object): self.module.crontable = [] else: self.module.crontable = [] + def do(self, function_name, data): if function_name in dir(self.module): #this makes the plugin fail with stack trace in debug mode @@ -105,17 +142,19 @@ class Plugin(object): try: eval("self.module."+function_name)(data) except: - dbg("problem in module {} {}".format(function_name, data)) + self._dbg("problem in module {} {}".format(function_name, data)) else: eval("self.module."+function_name)(data) if "catch_all" in dir(self.module): try: self.module.catch_all(data) except: - dbg("problem in catch all") + self._dbg("problem in catch all") + def do_jobs(self): for job in self.jobs: job.check() + def do_output(self): output = [] while True: @@ -134,17 +173,20 @@ class Job(object): self.function = function self.interval = interval self.lastrun = 0 + 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 not debug: try: self.function() except: - dbg("problem") + self._dbg("problem") else: self.function() self.lastrun = time.time() @@ -152,50 +194,3 @@ class Job(object): class UnknownChannel(Exception): 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(file(args.config or 'rtmbot.conf', 'r')) - debug = config["DEBUG"] - bot = RtmBot(config["SLACK_TOKEN"]) - site_plugins = [] - files_currently_downloading = [] - job_hash = {} - - if config.has_key("DAEMON"): - if config["DAEMON"]: - import daemon - with daemon.DaemonContext(): - main_loop() - main_loop() - diff --git a/start_rtmbot.py b/start_rtmbot.py new file mode 100755 index 0000000..ace6091 --- /dev/null +++ b/start_rtmbot.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +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(file(args.config or 'rtmbot.conf', 'r')) +bot = RtmBot(config) +bot.start() From 54fe252eefb9f87d569ae6901883ba8d88a73889 Mon Sep 17 00:00:00 2001 From: Thomas Zakrajsek Date: Sun, 2 Aug 2015 18:29:36 -0400 Subject: [PATCH 03/13] added setup.py for installing module now that RtmBot was refactored --- setup.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100755 setup.py diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..24e5936 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup(name='rtmbot', + version='1.0', + 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'], + ) From 96d84e4edf17c4f940f6d6886ba3721bb189abbc Mon Sep 17 00:00:00 2001 From: Thomas Zakrajsek Date: Sun, 2 Aug 2015 18:29:54 -0400 Subject: [PATCH 04/13] ignore log files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 38e97ba..cacb029 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /rtmbot.conf /plugins/** /build/** +*.log env From 08f9feacf6ed698682ef6967a1bbdf1337a616db Mon Sep 17 00:00:00 2001 From: Thomas Zakrajsek Date: Sun, 2 Aug 2015 18:55:15 -0400 Subject: [PATCH 05/13] fixing regression, adding back KeyboardInterrupt exception handler --- start_rtmbot.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/start_rtmbot.py b/start_rtmbot.py index ace6091..941ceec 100755 --- a/start_rtmbot.py +++ b/start_rtmbot.py @@ -18,4 +18,9 @@ def parse_args(): args = parse_args() config = yaml.load(file(args.config or 'rtmbot.conf', 'r')) bot = RtmBot(config) -bot.start() +try: + bot.start() +except KeyboardInterrupt: + sys.exit(0) +except: + logging.exception('OOPS') From bfe7df401a6e92feab7a9769dd4b04a5da324df5 Mon Sep 17 00:00:00 2001 From: Thomas Zakrajsek Date: Sun, 2 Aug 2015 19:08:29 -0400 Subject: [PATCH 06/13] fixed regression, now self.debug exists in Plugin class context --- rtmbot/core.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rtmbot/core.py b/rtmbot/core.py index 3fa2a39..c11d558 100755 --- a/rtmbot/core.py +++ b/rtmbot/core.py @@ -110,7 +110,8 @@ class RtmBot(object): # try: if name in self.config: logging.info("config found for: " + name) - plugin_config = self.config.get(name) + plugin_config = self.config.get(name, {}) + plugin_config['DEBUG'] = self.debug self.bot_plugins.append(Plugin(name, plugin_config)) # except: # print "error loading plugin %s" % name @@ -121,6 +122,7 @@ class Plugin(object): self.jobs = [] self.module = __import__(name) self.module.config = plugin_config + self.debug = self.module.config.get('DEBUG') self.register_jobs() self.outputs = [] if 'setup' in dir(self.module): @@ -138,7 +140,7 @@ class Plugin(object): def do(self, function_name, data): if function_name in dir(self.module): #this makes the plugin fail with stack trace in debug mode - if not debug: + if not self.debug: try: eval("self.module."+function_name)(data) except: From f12a90ab0a8f1777f4d7b8af330d7080a3a4a78a Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Sun, 17 Apr 2016 17:34:17 -0700 Subject: [PATCH 07/13] Fix missing debug context in Job --- rtmbot/core.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/rtmbot/core.py b/rtmbot/core.py index 482562c..9eaf804 100755 --- a/rtmbot/core.py +++ b/rtmbot/core.py @@ -121,8 +121,15 @@ class RtmBot(object): 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 = {} # TODO: is this variable necessary? + plugin_config = {} self.name = name self.jobs = [] self.module = __import__(name) @@ -136,7 +143,7 @@ class Plugin(object): 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.jobs.append(Job(interval, eval("self.module." + function), self.debug)) logging.info(self.module.crontable) self.module.crontable = [] else: @@ -177,10 +184,11 @@ class Plugin(object): class Job(object): - def __init__(self, interval, function): + 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) @@ -190,11 +198,11 @@ class Job(object): def check(self): if self.lastrun + self.interval < time.time(): - if not debug: # TODO: This isn't in scope any more + if self.debug is False: try: self.function() except: - self._dbg("problem") + logging.debug("Problem in job check") else: self.function() self.lastrun = time.time() From 561c3d521b28a06fc2fdd9ed51a3c1f6ce1b16a9 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Sun, 17 Apr 2016 17:50:50 -0700 Subject: [PATCH 08/13] Change the location of file to lint --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d8924e4..5bcf82e 100644 --- a/tox.ini +++ b/tox.ini @@ -20,4 +20,4 @@ basepython = [testenv:flake8] basepython=python deps=flake8 -commands=flake8 {toxinidir}/rtmbot.py {toxinidir}/doc/example-plugins \ No newline at end of file +commands=flake8 {toxinidir}/rtmbot/core.py {toxinidir}/doc/example-plugins From a0fdba13b44fb397f4e39bcc0dccf7df04554374 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Sun, 17 Apr 2016 17:59:22 -0700 Subject: [PATCH 09/13] Update readme file with new ./start_rtmbot.py file name --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b1a9a08..f584b5c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Installation pip install -r requirements.txt 3. Configure rtmbot (https://api.slack.com/bot-users) - + cp doc/example-config/rtmbot.conf . vi rtmbot.conf SLACK_TOKEN: "xoxb-11111111111-222222222222222" @@ -51,7 +51,7 @@ To install the example 'repeat' plugin The repeat plugin will now be loaded by the bot on startup. - ./rtmbot.py + ./start_rtmbot.py Create Plugins -------- @@ -71,7 +71,7 @@ Plugins can send messages back to any channel, including direct messages. This i outputs = [] outputs.append(["C12345667", "hello world"]) - + *Note*: you should always create the outputs array at the start of your program, i.e. ```outputs = []``` ####Timed jobs From 7d718fe54fb78521a9d4927609edce04d569c8b9 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Sun, 17 Apr 2016 18:21:04 -0700 Subject: [PATCH 10/13] Adding start rtmbot to tox.ini for linting Also updating gitignore to ignore some stuff on my machine. --- .gitignore | 5 ++++- start_rtmbot.py | 2 +- tox.ini | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 71c0436..6bca4dd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ /build/** *.log env -.tox \ No newline at end of file +.tox +*.un~ +0/ +tests/.cache diff --git a/start_rtmbot.py b/start_rtmbot.py index 3857d63..b5add21 100755 --- a/start_rtmbot.py +++ b/start_rtmbot.py @@ -19,7 +19,7 @@ def parse_args(): # load args with config path args = parse_args() -config = yaml.load(file(args.config or 'rtmbot.conf', 'r')) +config = yaml.load(open(args.config or 'rtmbot.conf', 'r')) bot = RtmBot(config) try: bot.start() diff --git a/tox.ini b/tox.ini index 5bcf82e..acfcca9 100644 --- a/tox.ini +++ b/tox.ini @@ -20,4 +20,4 @@ basepython = [testenv:flake8] basepython=python deps=flake8 -commands=flake8 {toxinidir}/rtmbot/core.py {toxinidir}/doc/example-plugins +commands=flake8 {toxinidir}/start_rtmbot.py {toxinidir}/rtmbot/core.py {toxinidir}/setup.py {toxinidir}/doc/example-plugins From e1d4816e5537cf44ae44c4eff9d2f7a2a95f4f96 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Sun, 17 Apr 2016 18:29:17 -0700 Subject: [PATCH 11/13] Remove unused global site_config --- rtmbot/__init__.py | 2 -- rtmbot/core.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/rtmbot/__init__.py b/rtmbot/__init__.py index 56b4646..5af2406 100644 --- a/rtmbot/__init__.py +++ b/rtmbot/__init__.py @@ -1,3 +1 @@ from core import * - -site_config = {} diff --git a/rtmbot/core.py b/rtmbot/core.py index 9eaf804..9f991b3 100755 --- a/rtmbot/core.py +++ b/rtmbot/core.py @@ -14,8 +14,6 @@ class RtmBot(object): def __init__(self, config): # set the config object self.config = config - global site_config - site_config = self.config # set slack token self.token = config.get('SLACK_TOKEN') From c7582a7098ff8a16402b7a3151f45a30b240fdc4 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Sun, 17 Apr 2016 18:42:54 -0700 Subject: [PATCH 12/13] Conform all self.debug values to False I really want to remove this dbg() mess. --- rtmbot/core.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/rtmbot/core.py b/rtmbot/core.py index 9f991b3..251cd12 100755 --- a/rtmbot/core.py +++ b/rtmbot/core.py @@ -31,10 +31,7 @@ class RtmBot(object): level=logging.INFO, format='%(asctime)s %(message)s') logging.info(self.directory) - if 'DEBUG' in self.config: - self.debug = self.config.get('DEBUG') - else: - self.debug = False + self.debug = self.config.get('DEBUG', False) # initialize stateful fields self.last_ping = 0 @@ -132,7 +129,7 @@ class Plugin(object): self.jobs = [] self.module = __import__(name) self.module.config = plugin_config - self.debug = self.module.config.get('DEBUG') + self.debug = self.module.config.get('DEBUG', False) self.register_jobs() self.outputs = [] if 'setup' in dir(self.module): @@ -150,7 +147,7 @@ class Plugin(object): def do(self, function_name, data): if function_name in dir(self.module): # this makes the plugin fail with stack trace in debug mode - if not self.debug: + if self.debug is False: try: eval("self.module." + function_name)(data) except: From 84db35fa6f4abd6e01d596600d9505006b9a7708 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Sun, 24 Apr 2016 12:40:37 -0700 Subject: [PATCH 13/13] Improve debug state handling and error messaging If debug=True: - Most plugin methods (if not all) should end the script and report the error to std.out If debug=False: - Plugin methods will log the error, data, and stack trace Future planning: - The debug=False case here has better behavior in my opinion, maybe there's a better way of handling debug=True --- rtmbot/core.py | 36 +++++++++++++++++++++--------------- start_rtmbot.py | 3 --- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/rtmbot/core.py b/rtmbot/core.py index 251cd12..bf64782 100755 --- a/rtmbot/core.py +++ b/rtmbot/core.py @@ -146,19 +146,24 @@ class Plugin(object): def do(self, function_name, data): if function_name in dir(self.module): - # this makes the plugin fail with stack trace in debug mode - if self.debug is False: + 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: - self._dbg("problem in module {} {}".format(function_name, data)) - else: - eval("self.module." + function_name)(data) + except Exception: + logging.exception("problem in module {} {}".format(function_name, data)) if "catch_all" in dir(self.module): - try: + if self.debug is True: + # this makes the plugin fail with stack trace in debug mode self.module.catch_all(data) - except: - self._dbg("problem in catch all") + 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: @@ -193,15 +198,16 @@ class Job(object): def check(self): if self.lastrun + self.interval < time.time(): - if self.debug is False: + 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: - logging.debug("Problem in job check") - else: - self.function() + except Exception: + logging.exception("Problem in job check: {}".format(self.function)) self.lastrun = time.time() - pass class UnknownChannel(Exception): diff --git a/start_rtmbot.py b/start_rtmbot.py index b5add21..77c7fa3 100755 --- a/start_rtmbot.py +++ b/start_rtmbot.py @@ -1,6 +1,5 @@ #!/usr/bin/env python import sys -import logging from argparse import ArgumentParser import yaml @@ -25,5 +24,3 @@ try: bot.start() except KeyboardInterrupt: sys.exit(0) -except: - logging.exception('OOPS')