From 7a5480f1a52bf19793c4b0fae9fe93101fa860a5 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 17 Nov 2014 17:05:06 -0800 Subject: [PATCH 01/77] first commit --- README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 From f7d7db682b6a4ed08f0f2d34e204009dfa664302 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 17 Nov 2014 17:07:52 -0800 Subject: [PATCH 02/77] working bot with example plugins --- .gitignore | 2 + plugins/example/counter.py | 9 +++ plugins/example/repeat.py | 9 +++ plugins/example/repeat2.py | 9 +++ rtmbot.py | 109 +++++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 .gitignore create mode 100644 plugins/example/counter.py create mode 100644 plugins/example/repeat.py create mode 100644 plugins/example/repeat2.py create mode 100755 rtmbot.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f41c12e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +rtmbot.conf diff --git a/plugins/example/counter.py b/plugins/example/counter.py new file mode 100644 index 0000000..5846c94 --- /dev/null +++ b/plugins/example/counter.py @@ -0,0 +1,9 @@ +import time +crontable = [] +outputs = [] + +#crontable.append([.5,"add_number"]) +crontable.append([5,"say_time"]) + +def say_time(): + outputs.append(["D030GJLM2", time.time()]) diff --git a/plugins/example/repeat.py b/plugins/example/repeat.py new file mode 100644 index 0000000..9ad8307 --- /dev/null +++ b/plugins/example/repeat.py @@ -0,0 +1,9 @@ +import time +crontable = [] +outputs = [] + +def process_message(data): + print data + if data['channel'].startswith("D"): + outputs.append([data['channel'], "from repeat1: " + data['text']]) + diff --git a/plugins/example/repeat2.py b/plugins/example/repeat2.py new file mode 100644 index 0000000..1150493 --- /dev/null +++ b/plugins/example/repeat2.py @@ -0,0 +1,9 @@ +import time +crontable = [] +outputs = [] + +def process_message(data): + print data + if data['channel'].startswith("D"): + outputs.append([data['channel'], "from repeat2: " + data['text']]) + diff --git a/rtmbot.py b/rtmbot.py new file mode 100755 index 0000000..51e0112 --- /dev/null +++ b/rtmbot.py @@ -0,0 +1,109 @@ +#!/usr/bin/python + +import glob +import yaml +import json +import os +import sys +import time + +from slackclient import SlackClient + +class RtmBot(object): + def __init__(self, token): + self.token = token + self.bot_plugins = [] + self.slack_client = None + def connect(self): + """Convenience method that creates Server instance""" + self.slack_client = SlackClient(self.token) + self.slack_client.connect() + def start(self): + self.connect() + self.load_plugins() + while True: + for reply in self.slack_client.read(): + self.input(reply) + #print self.bot_plugins + self.crons() + self.output() + time.sleep(.1) + def input(self, data): + if "type" in data: + function_name = "process_" + data["type"] + for plugin in self.bot_plugins: + plugin.do(function_name, data) + def output(self): + for plugin in self.bot_plugins: + for output in plugin.do_output(): + channel = self.slack_client.server.channels.find(output[0]) + channel.send_message("%s" % output[1]) + #plugin.do_output() + pass + #print self.bot_plugins[plugin].replies() + def crons(self): + for plugin in self.bot_plugins: + plugin.do_jobs() + pass + #print job + def load_plugins(self): + path = os.path.dirname(sys.argv[0]) + sys.path.insert(0, path + "/plugins") + for plugin in glob.glob(path+'/plugins/*.py'): + name = plugin.split('/')[-1].rstrip('.py') +# try: + self.bot_plugins.append(Plugin(name)) +# except: +# print "error loading plugins" + +class Plugin(object): + def __init__(self, name): + self.name = name + self.jobs = [] + self.module = __import__(name) + self.register_jobs() + self.outputs = [] + 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))) + print self.module.crontable + def do(self, function_name, data): + if function_name in dir(self.module): + eval("self.module."+function_name)(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: + output.append(self.module.outputs.pop(0)) + else: + break + return output + +class Job(object): + def __init__(self, interval, function): + self.function = function + self.interval = interval + self.lastrun = 0 + def __str__(self): + return "%s %s %s" % (self.function, self.interval, self.lastrun) + def __repr__(self): + return self.__str__() + def check(self): + if self.lastrun + self.interval < time.time(): + self.function() + self.lastrun = time.time() + pass + + +if __name__ == "__main__": + proc = {k[8:]: v for k, v in globals().items() if k.startswith("process_")} + config = yaml.load(file('rtmbot.conf', 'r')) + + bot = RtmBot(config["SLACK_TOKEN"]) + bot.start() + From 7f56355ef76c25915a64c4bc8c706ea6d4e44d30 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 17 Nov 2014 17:27:40 -0800 Subject: [PATCH 03/77] cleanup --- rtmbot.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/rtmbot.py b/rtmbot.py index 51e0112..d89a629 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -9,6 +9,10 @@ import time from slackclient import SlackClient +def dbg(debug_string): + if debug: + print debug_string + class RtmBot(object): def __init__(self, token): self.token = token @@ -24,13 +28,13 @@ class RtmBot(object): while True: for reply in self.slack_client.read(): self.input(reply) - #print self.bot_plugins self.crons() self.output() time.sleep(.1) def input(self, data): if "type" in data: function_name = "process_" + data["type"] + dbg(function_name) for plugin in self.bot_plugins: plugin.do(function_name, data) def output(self): @@ -38,23 +42,18 @@ class RtmBot(object): for output in plugin.do_output(): channel = self.slack_client.server.channels.find(output[0]) channel.send_message("%s" % output[1]) - #plugin.do_output() - pass - #print self.bot_plugins[plugin].replies() def crons(self): for plugin in self.bot_plugins: plugin.do_jobs() - pass - #print job def load_plugins(self): path = os.path.dirname(sys.argv[0]) sys.path.insert(0, path + "/plugins") for plugin in glob.glob(path+'/plugins/*.py'): - name = plugin.split('/')[-1].rstrip('.py') -# try: - self.bot_plugins.append(Plugin(name)) -# except: -# print "error loading plugins" + name = plugin.split('/')[-1][:-2] + try: + self.bot_plugins.append(Plugin(name)) + except: + print "error loading plugin %s" % name class Plugin(object): def __init__(self, name): @@ -101,9 +100,8 @@ class Job(object): if __name__ == "__main__": - proc = {k[8:]: v for k, v in globals().items() if k.startswith("process_")} config = yaml.load(file('rtmbot.conf', 'r')) - + debug = config["DEBUG"] bot = RtmBot(config["SLACK_TOKEN"]) bot.start() From 29fbb30c75a5175edfc802abbc39928f005d3e5e Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 17 Nov 2014 17:30:38 -0800 Subject: [PATCH 04/77] remove prints on example --- plugins/example/repeat.py | 1 - plugins/example/repeat2.py | 1 - 2 files changed, 2 deletions(-) diff --git a/plugins/example/repeat.py b/plugins/example/repeat.py index 9ad8307..e13a51b 100644 --- a/plugins/example/repeat.py +++ b/plugins/example/repeat.py @@ -3,7 +3,6 @@ crontable = [] outputs = [] def process_message(data): - print data if data['channel'].startswith("D"): outputs.append([data['channel'], "from repeat1: " + data['text']]) diff --git a/plugins/example/repeat2.py b/plugins/example/repeat2.py index 1150493..95738fc 100644 --- a/plugins/example/repeat2.py +++ b/plugins/example/repeat2.py @@ -3,7 +3,6 @@ crontable = [] outputs = [] def process_message(data): - print data if data['channel'].startswith("D"): outputs.append([data['channel'], "from repeat2: " + data['text']]) From 3d07cf6ecf2882f7ae60b0a174a91f4332bbaa08 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 17 Nov 2014 21:17:37 -0800 Subject: [PATCH 05/77] make plugins dir based --- plugins/example/counter.py | 9 --------- plugins/example/repeat.py | 8 -------- plugins/example/repeat2.py | 8 -------- rtmbot.py | 20 ++++++++++++++------ 4 files changed, 14 insertions(+), 31 deletions(-) delete mode 100644 plugins/example/counter.py delete mode 100644 plugins/example/repeat.py delete mode 100644 plugins/example/repeat2.py diff --git a/plugins/example/counter.py b/plugins/example/counter.py deleted file mode 100644 index 5846c94..0000000 --- a/plugins/example/counter.py +++ /dev/null @@ -1,9 +0,0 @@ -import time -crontable = [] -outputs = [] - -#crontable.append([.5,"add_number"]) -crontable.append([5,"say_time"]) - -def say_time(): - outputs.append(["D030GJLM2", time.time()]) diff --git a/plugins/example/repeat.py b/plugins/example/repeat.py deleted file mode 100644 index e13a51b..0000000 --- a/plugins/example/repeat.py +++ /dev/null @@ -1,8 +0,0 @@ -import time -crontable = [] -outputs = [] - -def process_message(data): - if data['channel'].startswith("D"): - outputs.append([data['channel'], "from repeat1: " + data['text']]) - diff --git a/plugins/example/repeat2.py b/plugins/example/repeat2.py deleted file mode 100644 index 95738fc..0000000 --- a/plugins/example/repeat2.py +++ /dev/null @@ -1,8 +0,0 @@ -import time -crontable = [] -outputs = [] - -def process_message(data): - if data['channel'].startswith("D"): - outputs.append([data['channel'], "from repeat2: " + data['text']]) - diff --git a/rtmbot.py b/rtmbot.py index d89a629..958e1d9 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -1,5 +1,8 @@ #!/usr/bin/python +import sys +sys.dont_write_bytecode = True + import glob import yaml import json @@ -47,13 +50,15 @@ class RtmBot(object): plugin.do_jobs() def load_plugins(self): path = os.path.dirname(sys.argv[0]) - sys.path.insert(0, path + "/plugins") - for plugin in glob.glob(path+'/plugins/*.py'): + for plugin in glob.glob(path+'/plugins/*'): + sys.path.insert(0, plugin) + for plugin in glob.glob(path+'/plugins/*/*.py'): + print plugin name = plugin.split('/')[-1][:-2] - try: - self.bot_plugins.append(Plugin(name)) - except: - print "error loading plugin %s" % name +# try: + self.bot_plugins.append(Plugin(name)) +# except: +# print "error loading plugin %s" % name class Plugin(object): def __init__(self, name): @@ -69,7 +74,10 @@ class Plugin(object): print self.module.crontable def do(self, function_name, data): if function_name in dir(self.module): +# try: eval("self.module."+function_name)(data) +# except: +# dbg("problem in module") def do_jobs(self): for job in self.jobs: job.check() From f7a307c9dd7c85034358f381765f54d0c1a22327 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 17 Nov 2014 21:22:37 -0800 Subject: [PATCH 06/77] examples moved to doc --- doc/example/counter.py | 9 +++++++++ doc/example/repeat.py | 8 ++++++++ doc/example/repeat2.py | 8 ++++++++ 3 files changed, 25 insertions(+) create mode 100644 doc/example/counter.py create mode 100644 doc/example/repeat.py create mode 100644 doc/example/repeat2.py diff --git a/doc/example/counter.py b/doc/example/counter.py new file mode 100644 index 0000000..5846c94 --- /dev/null +++ b/doc/example/counter.py @@ -0,0 +1,9 @@ +import time +crontable = [] +outputs = [] + +#crontable.append([.5,"add_number"]) +crontable.append([5,"say_time"]) + +def say_time(): + outputs.append(["D030GJLM2", time.time()]) diff --git a/doc/example/repeat.py b/doc/example/repeat.py new file mode 100644 index 0000000..e13a51b --- /dev/null +++ b/doc/example/repeat.py @@ -0,0 +1,8 @@ +import time +crontable = [] +outputs = [] + +def process_message(data): + if data['channel'].startswith("D"): + outputs.append([data['channel'], "from repeat1: " + data['text']]) + diff --git a/doc/example/repeat2.py b/doc/example/repeat2.py new file mode 100644 index 0000000..95738fc --- /dev/null +++ b/doc/example/repeat2.py @@ -0,0 +1,8 @@ +import time +crontable = [] +outputs = [] + +def process_message(data): + if data['channel'].startswith("D"): + outputs.append([data['channel'], "from repeat2: " + data['text']]) + From 5608883b456b68c119c0c5e93ba547b87e78cd6d Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 17 Nov 2014 22:26:36 -0800 Subject: [PATCH 07/77] don't die on bad plugin --- rtmbot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rtmbot.py b/rtmbot.py index 958e1d9..f30c460 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -74,10 +74,10 @@ class Plugin(object): print self.module.crontable def do(self, function_name, data): if function_name in dir(self.module): -# try: - eval("self.module."+function_name)(data) -# except: -# dbg("problem in module") + try: + eval("self.module."+function_name)(data) + except: + dbg("problem in module") def do_jobs(self): for job in self.jobs: job.check() From 3cd63159122d5ad65e13e6077420f84ae1bebbbc Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 09:20:00 -0800 Subject: [PATCH 08/77] include dir and example config --- .gitignore | 3 ++- doc/example-config/rtmbot.conf | 3 +++ doc/{example => example-plugins}/counter.py | 0 doc/{example => example-plugins}/repeat.py | 0 doc/{example => example-plugins}/repeat2.py | 0 5 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 doc/example-config/rtmbot.conf rename doc/{example => example-plugins}/counter.py (100%) rename doc/{example => example-plugins}/repeat.py (100%) rename doc/{example => example-plugins}/repeat2.py (100%) diff --git a/.gitignore b/.gitignore index f41c12e..105ecf7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc -rtmbot.conf +/rtmbot.conf +/plugins/** diff --git a/doc/example-config/rtmbot.conf b/doc/example-config/rtmbot.conf new file mode 100644 index 0000000..af4861d --- /dev/null +++ b/doc/example-config/rtmbot.conf @@ -0,0 +1,3 @@ +DEBUG: False + +SLACK_TOKEN: "xoxb-111111111111-2222222222222222222" diff --git a/doc/example/counter.py b/doc/example-plugins/counter.py similarity index 100% rename from doc/example/counter.py rename to doc/example-plugins/counter.py diff --git a/doc/example/repeat.py b/doc/example-plugins/repeat.py similarity index 100% rename from doc/example/repeat.py rename to doc/example-plugins/repeat.py diff --git a/doc/example/repeat2.py b/doc/example-plugins/repeat2.py similarity index 100% rename from doc/example/repeat2.py rename to doc/example-plugins/repeat2.py From 2027b73ba67af8cd4c025703986d35e464f3a885 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 09:38:53 -0800 Subject: [PATCH 09/77] make directory --- plugins/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 plugins/.gitkeep diff --git a/plugins/.gitkeep b/plugins/.gitkeep new file mode 100644 index 0000000..e69de29 From a3b800ffe01eeb61b310e5f5125843576f59788f Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 09:40:49 -0800 Subject: [PATCH 10/77] Add initial documentation. --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/README.md b/README.md index e69de29..9b7652d 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,52 @@ +python-rtmbot +============= +A Slack bot written in python that connects via the RTM API. + +Python-rtmbot is a callback based bot engine. The plugins architecture should be familiar to anyone with knowledge to the [Slack API](https://api.slack.com) and Python. The configuration file format is YAML. + +Dependencies +---------- +* websocket-client https://pypi.python.org/pypi/websocket-client/ +* python-slackclient https://github.com/slackhq/python-slackclient + +Installation +----------- + +1. Download and install the python-slackclient and websocket-client libraries + + git clone https://github.com/liris/websocket-client.git + cd websocket-client + sudo python setup.py install + cd .. + git clone git@github.com:slackhq/python-slackclient.git + cd python-slackclient + sudo python setup.py install + cd .. + +2. Download the python-rtmbot code + + git clone git@github.com:slackhq/python-rtmbot.git + cd python-rtmbot + + +3. Configure rtmbot + + cp doc/example-config/rtmbot.conf . + vi rtmbot.conf + SLACK_TOKEN: "xoxb-11111111111-222222222222222" + +*Note*: At this point rtmbot is ready to run, however no plugins are configured. + +Plugins +------- + +Each plugin should create a directory under ```plugins/``` with .py files for the code you would like to load. libraries can be kept in a subdirectory. + +To install the example 'repeat' plugin + + mkdir plugins/repeat + cp doc/example-plugins/repeat.py plugins/repeat + +The repeat plugin will now be loaded by the bot on startup. + + ./rtmbot.py From 28292b35b08fb845a1371d0158c9b678bd7b4cca Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 09:55:23 -0800 Subject: [PATCH 11/77] create arrays if not created in plugin --- rtmbot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rtmbot.py b/rtmbot.py index f30c460..8e7f5c6 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -72,6 +72,8 @@ class Plugin(object): for interval, function in self.module.crontable: self.jobs.append(Job(interval, eval("self.module."+function))) print self.module.crontable + else: + self.module.crontable = [] def do(self, function_name, data): if function_name in dir(self.module): try: @@ -89,6 +91,8 @@ class Plugin(object): output.append(self.module.outputs.pop(0)) else: break + else: + self.module.outputs = [] return output class Job(object): From 773970ce90f5a41f941ff3b2beeff95a850e1232 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 10:11:25 -0800 Subject: [PATCH 12/77] Add info on writing plugins. --- README.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b7652d..228afe7 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Installation *Note*: At this point rtmbot is ready to run, however no plugins are configured. -Plugins +Add Plugins ------- Each plugin should create a directory under ```plugins/``` with .py files for the code you would like to load. libraries can be kept in a subdirectory. @@ -50,3 +50,34 @@ To install the example 'repeat' plugin The repeat plugin will now be loaded by the bot on startup. ./rtmbot.py + +Create Plugins +-------- + +####Incoming data +Plugins are callback based and respond to any event sent via the rtm websocket. To act on an event, create a function definition called process_(api_method) that accepts a single arg. For example, to handle incoming messages: + + def process_message(data): + print data + +This will print the incoming message json (dict) to the screen where the bot is running. + +####Outgoing data +Plugins can send messages back to any channel, including direct messages. This is done by appending a two item array to the outputs global array. The first item in the array is the channel ID and the second is the message text. Example that writes "hello world" when the plugin is started: + + 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 +Plugins can also run methods on a schedule. This allows a plugin to poll for updates or perform housekeeping during its lifetime. This is done by appending a two item array to the crontable array. The first item is the interval in seconds and the second item is the method to run. For example, this will print "hello world" every 10 seconds. + + outputs = [] + crontable = [] + crontable.append([10, "say_hello"]) + def say_hello(): + outputs.append(["C12345667", "hello world"]) + +####Plugin misc +The data is a plugin persists for the life of the rtmbot process. If you need persistent data, you should use something like sqlite or the python pickle libraries. From 01de198b533a633b69fd53f8c6ebd4984e3e522f Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 10:14:05 -0800 Subject: [PATCH 13/77] Update README.md --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 228afe7..56de019 100644 --- a/README.md +++ b/README.md @@ -57,27 +57,27 @@ Create Plugins ####Incoming data Plugins are callback based and respond to any event sent via the rtm websocket. To act on an event, create a function definition called process_(api_method) that accepts a single arg. For example, to handle incoming messages: - def process_message(data): - print data + def process_message(data): + print data This will print the incoming message json (dict) to the screen where the bot is running. ####Outgoing data Plugins can send messages back to any channel, including direct messages. This is done by appending a two item array to the outputs global array. The first item in the array is the channel ID and the second is the message text. Example that writes "hello world" when the plugin is started: - outputs = [] - outputs.append(["C12345667", "hello world"]) + 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 Plugins can also run methods on a schedule. This allows a plugin to poll for updates or perform housekeeping during its lifetime. This is done by appending a two item array to the crontable array. The first item is the interval in seconds and the second item is the method to run. For example, this will print "hello world" every 10 seconds. - outputs = [] - crontable = [] - crontable.append([10, "say_hello"]) - def say_hello(): - outputs.append(["C12345667", "hello world"]) + outputs = [] + crontable = [] + crontable.append([10, "say_hello"]) + def say_hello(): + outputs.append(["C12345667", "hello world"]) ####Plugin misc The data is a plugin persists for the life of the rtmbot process. If you need persistent data, you should use something like sqlite or the python pickle libraries. From 928a68fab64a89f417ba57380f53d2155a2e5955 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 10:18:26 -0800 Subject: [PATCH 14/77] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 56de019..7c9c26e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ A Slack bot written in python that connects via the RTM API. Python-rtmbot is a callback based bot engine. The plugins architecture should be familiar to anyone with knowledge to the [Slack API](https://api.slack.com) and Python. The configuration file format is YAML. +Some differences to webhooks: +1. Doesn't a webserver to receive messages +2. Logs in as a slack user (or bot) +3. Bot users must be invited to a channel + Dependencies ---------- * websocket-client https://pypi.python.org/pypi/websocket-client/ @@ -81,3 +86,6 @@ Plugins can also run methods on a schedule. This allows a plugin to poll for upd ####Plugin misc The data is a plugin persists for the life of the rtmbot process. If you need persistent data, you should use something like sqlite or the python pickle libraries. + +####Todo: +Some rtm data should be handled upstream, such as channel and user creation. These should create the proper objects on-the-fly. From ab3d1e4cc55b82dbfe38ee3aa1376801153e625d Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 10:20:15 -0800 Subject: [PATCH 15/77] added another example plugin --- doc/example-plugins/counter.py | 4 ++-- doc/example-plugins/repeat2.py | 8 -------- doc/example-plugins/todo.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) delete mode 100644 doc/example-plugins/repeat2.py create mode 100644 doc/example-plugins/todo.py diff --git a/doc/example-plugins/counter.py b/doc/example-plugins/counter.py index 5846c94..84e9012 100644 --- a/doc/example-plugins/counter.py +++ b/doc/example-plugins/counter.py @@ -2,8 +2,8 @@ import time crontable = [] outputs = [] -#crontable.append([.5,"add_number"]) crontable.append([5,"say_time"]) def say_time(): - outputs.append(["D030GJLM2", time.time()]) + #NOTE: you must add a real channel ID for this to work + outputs.append(["D12345678", time.time()]) diff --git a/doc/example-plugins/repeat2.py b/doc/example-plugins/repeat2.py deleted file mode 100644 index 95738fc..0000000 --- a/doc/example-plugins/repeat2.py +++ /dev/null @@ -1,8 +0,0 @@ -import time -crontable = [] -outputs = [] - -def process_message(data): - if data['channel'].startswith("D"): - outputs.append([data['channel'], "from repeat2: " + data['text']]) - diff --git a/doc/example-plugins/todo.py b/doc/example-plugins/todo.py new file mode 100644 index 0000000..cdfc92b --- /dev/null +++ b/doc/example-plugins/todo.py @@ -0,0 +1,31 @@ +outputs = [] +crontabs = [] + +tasks = {} + +def process_message(data): + global tasks + channel = data["channel"] + text = data["text"] + #only accept tasks on DM channels + if channel.startswith("D") or channel.startswith("C"): + if channel not in tasks.keys(): + tasks[channel] = [] + #do command stuff + if text.startswith("todo"): + tasks[channel].append(text[5:]) + outputs.append([channel, "added"]) + if text == "tasks": + output = "" + counter = 1 + for task in tasks[channel]: + output += "%i) %s\n" % (counter, task) + counter += 1 + outputs.append([channel, output]) + if text == "fin": + tasks[channel] = [] + if text.startswith("done"): + num = int(text.split()[1]) - 1 + tasks[channel].pop(num) + if text == "show": + print tasks From 40484f3332ade59ba5c00509f885c14b7d7a770c Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 10:21:40 -0800 Subject: [PATCH 16/77] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7c9c26e..71078dd 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ A Slack bot written in python that connects via the RTM API. Python-rtmbot is a callback based bot engine. The plugins architecture should be familiar to anyone with knowledge to the [Slack API](https://api.slack.com) and Python. The configuration file format is YAML. Some differences to webhooks: + 1. Doesn't a webserver to receive messages 2. Logs in as a slack user (or bot) 3. Bot users must be invited to a channel From 0248ec2c7e43de1fe194a2ecd51cedd63e944e63 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 10:28:15 -0800 Subject: [PATCH 17/77] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71078dd..e9da935 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Python-rtmbot is a callback based bot engine. The plugins architecture should be Some differences to webhooks: -1. Doesn't a webserver to receive messages +1. Doesn't require a webserver to receive messages 2. Logs in as a slack user (or bot) 3. Bot users must be invited to a channel From aade7618af6a62b0c42da6831b24387dd4aeb8a1 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 10:29:35 -0800 Subject: [PATCH 18/77] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9da935..dd2d124 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Installation Add Plugins ------- -Each plugin should create a directory under ```plugins/``` with .py files for the code you would like to load. libraries can be kept in a subdirectory. +Each plugin should create a directory under ```plugins/``` with .py files for the code you would like to load. libraries can be kept in a subdirectory. You can install as many plugins as you like, and each will handle every event received by the bot indepentently. To install the example 'repeat' plugin From 254dc4b13c0a19fef297f0781405fa76aeadff74 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 11:30:27 -0800 Subject: [PATCH 19/77] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dd2d124..6afa2b9 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Plugins can also run methods on a schedule. This allows a plugin to poll for upd outputs.append(["C12345667", "hello world"]) ####Plugin misc -The data is a plugin persists for the life of the rtmbot process. If you need persistent data, you should use something like sqlite or the python pickle libraries. +The data within a plugin persists for the life of the rtmbot process. If you need persistent data, you should use something like sqlite or the python pickle libraries. ####Todo: Some rtm data should be handled upstream, such as channel and user creation. These should create the proper objects on-the-fly. From 980c5266f2eae6bf361f87524e69eb0e7f546c41 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 11:43:57 -0800 Subject: [PATCH 20/77] fix to make comment true --- doc/example-plugins/todo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/example-plugins/todo.py b/doc/example-plugins/todo.py index cdfc92b..dea6e0f 100644 --- a/doc/example-plugins/todo.py +++ b/doc/example-plugins/todo.py @@ -8,7 +8,7 @@ def process_message(data): channel = data["channel"] text = data["text"] #only accept tasks on DM channels - if channel.startswith("D") or channel.startswith("C"): + if channel.startswith("D"): if channel not in tasks.keys(): tasks[channel] = [] #do command stuff From 10ba64e7ccea3b22fc3c08614679c6a696fac706 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 16:26:00 -0800 Subject: [PATCH 21/77] add a catch_all plugin function to see all messages from slack --- rtmbot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rtmbot.py b/rtmbot.py index 8e7f5c6..56be9c4 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -80,6 +80,11 @@ class Plugin(object): eval("self.module."+function_name)(data) except: dbg("problem in module") + if "catch_all" in dir(self.module): + try: + self.module.catch_all(data) + except: + dbg("problem in catch all") def do_jobs(self): for job in self.jobs: job.check() From afaa57878b4d6053fd83bc1682b15a77dbc2162e Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 18 Nov 2014 16:30:04 -0800 Subject: [PATCH 22/77] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6afa2b9..fca006f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ Plugins are callback based and respond to any event sent via the rtm websocket. This will print the incoming message json (dict) to the screen where the bot is running. +Plugins having a method defined as ```catch_all(data)``` will receive ALL events from the websocket. This is useful for learning the names of events and debugging. + ####Outgoing data Plugins can send messages back to any channel, including direct messages. This is done by appending a two item array to the outputs global array. The first item in the array is the channel ID and the second is the message text. Example that writes "hello world" when the plugin is started: From f29bc7764d0c0d9d79e73481492f88024554b7c2 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Fri, 21 Nov 2014 13:35:15 -0800 Subject: [PATCH 23/77] exceptional handling of exception --- doc/example-plugins/canary.py | 8 ++++++++ rtmbot.py | 11 +++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 doc/example-plugins/canary.py diff --git a/doc/example-plugins/canary.py b/doc/example-plugins/canary.py new file mode 100644 index 0000000..2c76784 --- /dev/null +++ b/doc/example-plugins/canary.py @@ -0,0 +1,8 @@ +import time +outputs = [] + +def canary(): + #NOTE: you must add a real channel ID for this to work + outputs.append(["D12345678", "bot started: " + str(time.time())]) + +canary() diff --git a/rtmbot.py b/rtmbot.py index 56be9c4..27ab179 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -44,7 +44,10 @@ class RtmBot(object): for plugin in self.bot_plugins: for output in plugin.do_output(): channel = self.slack_client.server.channels.find(output[0]) - channel.send_message("%s" % output[1]) + if channel != None: + channel.send_message("%s" % output[1]) + else: + raise UnknownChannel def crons(self): for plugin in self.bot_plugins: plugin.do_jobs() @@ -52,7 +55,8 @@ class RtmBot(object): path = os.path.dirname(sys.argv[0]) for plugin in glob.glob(path+'/plugins/*'): sys.path.insert(0, plugin) - for plugin in glob.glob(path+'/plugins/*/*.py'): + sys.path.insert(0, path+'/plugins/') + for plugin in glob.glob(path+'/plugins/*.py') + glob.glob(path+'/plugins/*/*.py'): print plugin name = plugin.split('/')[-1][:-2] # try: @@ -115,6 +119,9 @@ class Job(object): self.lastrun = time.time() pass +class UnknownChannel(Exception): + pass + if __name__ == "__main__": config = yaml.load(file('rtmbot.conf', 'r')) From 8cd6b0405374658eeb0a129dff520f6aab4affcf Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Fri, 21 Nov 2014 17:29:54 -0800 Subject: [PATCH 24/77] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fca006f..7019129 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Installation Add Plugins ------- -Each plugin should create a directory under ```plugins/``` with .py files for the code you would like to load. libraries can be kept in a subdirectory. You can install as many plugins as you like, and each will handle every event received by the bot indepentently. +Plugins can be installed as .py files in the ```plugins/``` directory OR as a .py file in any first level subdirectory. If your plugin uses multiple source files and libraries, it is recommended that you create a directory. You can install as many plugins as you like, and each will handle every event received by the bot indepentently. To install the example 'repeat' plugin From ddabcfa8e177fd54a8802f6b13294e17543e999e Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Fri, 21 Nov 2014 21:25:27 -0800 Subject: [PATCH 25/77] todo example persists data now --- doc/example-plugins/todo.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/example-plugins/todo.py b/doc/example-plugins/todo.py index dea6e0f..616be49 100644 --- a/doc/example-plugins/todo.py +++ b/doc/example-plugins/todo.py @@ -1,8 +1,15 @@ +import os +import pickle + outputs = [] crontabs = [] tasks = {} +FILE="plugins/todo.data" +if os.path.isfile(FILE): + tasks = pickle.load(open(FILE, 'rb')) + def process_message(data): global tasks channel = data["channel"] @@ -29,3 +36,4 @@ def process_message(data): tasks[channel].pop(num) if text == "show": print tasks + pickle.dump(tasks, open(FILE,"wb")) From 2dcb4c575bc244022c205d86aabac4ac05f2698f Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Sat, 22 Nov 2014 12:40:01 -0800 Subject: [PATCH 26/77] use string .format and make debug more useful --- rtmbot.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rtmbot.py b/rtmbot.py index 27ab179..d9b0767 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -37,7 +37,7 @@ class RtmBot(object): def input(self, data): if "type" in data: function_name = "process_" + data["type"] - dbg(function_name) + dbg("got {}".format(function_name)) for plugin in self.bot_plugins: plugin.do(function_name, data) def output(self): @@ -45,7 +45,7 @@ class RtmBot(object): for output in plugin.do_output(): channel = self.slack_client.server.channels.find(output[0]) if channel != None: - channel.send_message("%s" % output[1]) + channel.send_message("{}".format(output[1])) else: raise UnknownChannel def crons(self): @@ -80,10 +80,14 @@ class Plugin(object): self.module.crontable = [] def do(self, function_name, data): if function_name in dir(self.module): - try: + #this makes the plugin fail with stack trace in debug mode + if not debug: + try: + eval("self.module."+function_name)(data) + except: + dbg("problem in module {} {}".format(function_name, data)) + else: eval("self.module."+function_name)(data) - except: - dbg("problem in module") if "catch_all" in dir(self.module): try: self.module.catch_all(data) @@ -110,7 +114,7 @@ class Job(object): self.interval = interval self.lastrun = 0 def __str__(self): - return "%s %s %s" % (self.function, self.interval, self.lastrun) + return "{} {} {}".format(self.function, self.interval, self.lastrun) def __repr__(self): return self.__str__() def check(self): From ec6a3dc90e8a495337bd08252a2c3cb2cec91a1c Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 24 Nov 2014 19:01:41 -0800 Subject: [PATCH 27/77] protect from bad functions in cron --- rtmbot.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rtmbot.py b/rtmbot.py index d9b0767..8a7bedb 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -119,7 +119,13 @@ class Job(object): return self.__str__() def check(self): if self.lastrun + self.interval < time.time(): - self.function() + if not debug: + try: + self.function() + except: + dbg("problem") + else: + self.function() self.lastrun = time.time() pass From efc29d4956201164c188662802c84097573e185c Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Sun, 30 Nov 2014 13:55:06 -0800 Subject: [PATCH 28/77] use requirements file for pip --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8650e12 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +websocket-client +git+ssh://git@github.com/slackhq/python-slackclient.git@master#egg=python-slackclient From e129b0c0d9db349ac83f4b3f350a1912b97d72f0 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 2 Dec 2014 18:34:52 -0800 Subject: [PATCH 29/77] rate limit so we don't get kicked --- rtmbot.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rtmbot.py b/rtmbot.py index 8a7bedb..3838c2d 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -42,10 +42,15 @@ class RtmBot(object): 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 != None: + if channel != None and output[1] != None: + if limiter == True: + time.sleep(1) + limiter = False channel.send_message("{}".format(output[1])) + limiter = True else: raise UnknownChannel def crons(self): From 0a83871ddd780d8aa3b03520ec9142de900207a4 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 2 Dec 2014 18:38:22 -0800 Subject: [PATCH 30/77] don't raise here --- rtmbot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rtmbot.py b/rtmbot.py index 3838c2d..973d76c 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -51,8 +51,6 @@ class RtmBot(object): limiter = False channel.send_message("{}".format(output[1])) limiter = True - else: - raise UnknownChannel def crons(self): for plugin in self.bot_plugins: plugin.do_jobs() From 04c968a69fede1b058dca335a48633507e8ebeda Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Wed, 3 Dec 2014 12:29:28 -0800 Subject: [PATCH 31/77] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7019129..08bdaf9 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,9 @@ Python-rtmbot is a callback based bot engine. The plugins architecture should be Some differences to webhooks: 1. Doesn't require a webserver to receive messages -2. Logs in as a slack user (or bot) -3. Bot users must be invited to a channel +2. Can respond to direct messages from users +3. Logs in as a slack user (or bot) +4. Bot users must be invited to a channel Dependencies ---------- From c63196ca439dd1eef21da166bb40b0e8105acf1e Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Thu, 4 Dec 2014 16:21:13 -0800 Subject: [PATCH 32/77] add license --- LICENSE.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..89de354 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. From d484a8691728e4adf51e3a159ba73b1efa3fde8a Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Thu, 4 Dec 2014 16:23:23 -0800 Subject: [PATCH 33/77] add env to ignore virtualenv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 105ecf7..a044a8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc /rtmbot.conf /plugins/** +env From 53cacf9904cca86270d8b23c042287fed6956ced Mon Sep 17 00:00:00 2001 From: Jeff Oyama Date: Mon, 8 Dec 2014 12:32:13 -0800 Subject: [PATCH 34/77] Use `slackbot.rtm_read` and `slackbot.rtm_connect` instead of `slackbot.read` and `slackbot.connect` --- rtmbot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtmbot.py b/rtmbot.py index 973d76c..3dbd811 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -24,12 +24,12 @@ class RtmBot(object): def connect(self): """Convenience method that creates Server instance""" self.slack_client = SlackClient(self.token) - self.slack_client.connect() + self.slack_client.rtm_connect() def start(self): self.connect() self.load_plugins() while True: - for reply in self.slack_client.read(): + for reply in self.slack_client.rtm_read(): self.input(reply) self.crons() self.output() From e80a3459b264b8ca59dc87a8f02b47f2080bcfe1 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 8 Dec 2014 15:31:55 -0800 Subject: [PATCH 35/77] clean up example and requirements --- doc/example-plugins/repeat.py | 2 +- requirements.txt | 3 ++- rtmbot.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/example-plugins/repeat.py b/doc/example-plugins/repeat.py index e13a51b..54bc581 100644 --- a/doc/example-plugins/repeat.py +++ b/doc/example-plugins/repeat.py @@ -4,5 +4,5 @@ outputs = [] def process_message(data): if data['channel'].startswith("D"): - outputs.append([data['channel'], "from repeat1: " + data['text']]) + outputs.append([data['channel'], "from repeat1 \"{}\" in channel {}".format(data['text'], data['channel']) ]) diff --git a/requirements.txt b/requirements.txt index 8650e12..f771ba9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ +pyyaml websocket-client -git+ssh://git@github.com/slackhq/python-slackclient.git@master#egg=python-slackclient +git+https://github.com/slackhq/python-slackclient.git diff --git a/rtmbot.py b/rtmbot.py index 3dbd811..a5147a2 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import sys sys.dont_write_bytecode = True From 5949fa9f05f5b06b956681fe483794df2b522d9f Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 8 Dec 2014 15:56:47 -0800 Subject: [PATCH 36/77] also requires requests --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f771ba9..76a209d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +requests pyyaml websocket-client git+https://github.com/slackhq/python-slackclient.git From 3eb1d6d9f57e706a5b251a144cdab1255c0b1225 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 8 Dec 2014 16:01:33 -0800 Subject: [PATCH 37/77] Update README.md --- README.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 08bdaf9..203b7a4 100644 --- a/README.md +++ b/README.md @@ -19,22 +19,14 @@ Dependencies Installation ----------- -1. Download and install the python-slackclient and websocket-client libraries - - git clone https://github.com/liris/websocket-client.git - cd websocket-client - sudo python setup.py install - cd .. - git clone git@github.com:slackhq/python-slackclient.git - cd python-slackclient - sudo python setup.py install - cd .. - -2. Download the python-rtmbot code +1. Download the python-rtmbot code git clone git@github.com:slackhq/python-rtmbot.git cd python-rtmbot +2. Install dependencies ([virtualenv](http://virtualenv.readthedocs.org/en/latest/) is recommended.) + + pip install -r requirements.txt 3. Configure rtmbot From 749988b7c1abbda6a04ec90f3072a2496212543c Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 9 Dec 2014 11:52:59 -0800 Subject: [PATCH 38/77] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 203b7a4..f8a19bf 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Installation pip install -r requirements.txt -3. Configure rtmbot +3. Configure rtmbot (token can be found at https://api.slack.com/web) cp doc/example-config/rtmbot.conf . vi rtmbot.conf From 2b7145d0eda275bdfd105f783fe6f22276bf39da Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Tue, 9 Dec 2014 11:55:20 -0800 Subject: [PATCH 39/77] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8a19bf..e078a17 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Installation pip install -r requirements.txt -3. Configure rtmbot (token can be found at https://api.slack.com/web) +3. Configure rtmbot (https://api.slack.com/bot-users) cp doc/example-config/rtmbot.conf . vi rtmbot.conf From 68d3784533552cc02935a543994c0f4243f6769e Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Thu, 11 Dec 2014 16:25:41 -0800 Subject: [PATCH 40/77] add shiny new daemonize mode --- requirements.txt | 1 + rtmbot.py | 51 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index f771ba9..ced3092 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +python-daemon pyyaml websocket-client git+https://github.com/slackhq/python-slackclient.git diff --git a/rtmbot.py b/rtmbot.py index a5147a2..ea706ea 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -9,12 +9,13 @@ import json import os import sys import time +import logging from slackclient import SlackClient def dbg(debug_string): if debug: - print debug_string + logging.info(debug_string) class RtmBot(object): def __init__(self, token): @@ -55,30 +56,34 @@ class RtmBot(object): for plugin in self.bot_plugins: plugin.do_jobs() def load_plugins(self): - path = os.path.dirname(sys.argv[0]) - for plugin in glob.glob(path+'/plugins/*'): + for plugin in glob.glob(directory+'/plugins/*'): sys.path.insert(0, plugin) - sys.path.insert(0, path+'/plugins/') - for plugin in glob.glob(path+'/plugins/*.py') + glob.glob(path+'/plugins/*/*.py'): - print plugin - name = plugin.split('/')[-1][:-2] + sys.path.insert(0, directory+'/plugins/') + for plugin in glob.glob(directory+'/plugins/*.py') + glob.glob(directory+'/plugins/*/*.py'): + logging.info(plugin) + name = plugin.split('/')[-1][:-3] # try: self.bot_plugins.append(Plugin(name)) # except: # print "error loading plugin %s" % name class Plugin(object): - def __init__(self, name): + def __init__(self, name, plugin_config={}): self.name = name self.jobs = [] self.module = __import__(name) 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: self.jobs.append(Job(interval, eval("self.module."+function))) - print self.module.crontable + logging.info(self.module.crontable) else: self.module.crontable = [] def do(self, function_name, data): @@ -104,6 +109,7 @@ class Plugin(object): 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 @@ -136,9 +142,34 @@ class UnknownChannel(Exception): pass +def main_loop(): + 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') + if __name__ == "__main__": + directory = os.path.dirname(sys.argv[0]) + if not directory.startswith('/'): + directory = os.path.abspath("{}/{}".format(os.getcwd(), + directory + )) + config = yaml.load(file('rtmbot.conf', 'r')) debug = config["DEBUG"] bot = RtmBot(config["SLACK_TOKEN"]) - bot.start() + site_plugins = [] + files_currently_downloading = [] + job_hash = {} + + if config.has_key("DAEMON"): + if config["DAEMON"]: + import daemon + with daemon.DaemonContext(): + main_loop() + main_loop() From 240400439e6197a3e5b0ed3bb2bf6f2b66856bd3 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Thu, 11 Dec 2014 16:31:32 -0800 Subject: [PATCH 41/77] allow for no logfile --- rtmbot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rtmbot.py b/rtmbot.py index ea706ea..6642e80 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -143,7 +143,8 @@ class UnknownChannel(Exception): def main_loop(): - logging.basicConfig(filename=config["LOGFILE"], level=logging.INFO, format='%(asctime)s %(message)s') + if "LOGFILE" in config: + logging.basicConfig(filename=config["LOGFILE"], level=logging.INFO, format='%(asctime)s %(message)s') logging.info(directory) try: bot.start() From d89b6e6d327024beadb3489823966642fde444fa Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Thu, 11 Dec 2014 22:35:06 -0800 Subject: [PATCH 42/77] allow job registration after module loads --- rtmbot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rtmbot.py b/rtmbot.py index 6642e80..7cf5956 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -40,6 +40,7 @@ class RtmBot(object): function_name = "process_" + data["type"] 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: @@ -84,6 +85,7 @@ class Plugin(object): for interval, function in self.module.crontable: self.jobs.append(Job(interval, eval("self.module."+function))) logging.info(self.module.crontable) + self.module.crontable = [] else: self.module.crontable = [] def do(self, function_name, data): From 75309dde805874188996a343e4b8c55d95305bd8 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Thu, 25 Dec 2014 11:22:12 -0800 Subject: [PATCH 43/77] ignore unicode extended chars for now --- rtmbot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rtmbot.py b/rtmbot.py index 7cf5956..24f97a2 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -51,7 +51,8 @@ class RtmBot(object): if limiter == True: time.sleep(1) limiter = False - channel.send_message("{}".format(output[1])) + message = output[1].encode('ascii','ignore') + channel.send_message("{}".format(message)) limiter = True def crons(self): for plugin in self.bot_plugins: From 93137f94cc60707ff8bca9acae6332d0c678c8fd Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Sat, 21 Feb 2015 11:24:23 -0800 Subject: [PATCH 44/77] use slackclient from pypi not github --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 441a75f..d740102 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ requests python-daemon pyyaml websocket-client -git+https://github.com/slackhq/python-slackclient.git +slackclient From 82106deeaa8328f8c8edd96f9ee4c11b21d11e84 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Mon, 2 Mar 2015 21:20:29 -0800 Subject: [PATCH 45/77] Lower sleep interval for faster replies. --- rtmbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtmbot.py b/rtmbot.py index 24f97a2..cf81491 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -49,7 +49,7 @@ class RtmBot(object): channel = self.slack_client.server.channels.find(output[0]) if channel != None and output[1] != None: if limiter == True: - time.sleep(1) + time.sleep(.1) limiter = False message = output[1].encode('ascii','ignore') channel.send_message("{}".format(message)) From fa6e0e1c77d6cb0484cf08ce61df34780832280d Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Wed, 4 Mar 2015 13:01:37 -0800 Subject: [PATCH 46/77] ping the server to trigger reconnect on WS fail --- rtmbot.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rtmbot.py b/rtmbot.py index cf81491..ac2c9dd 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -19,6 +19,7 @@ def dbg(debug_string): class RtmBot(object): def __init__(self, token): + self.last_ping = 0 self.token = token self.bot_plugins = [] self.slack_client = None @@ -34,7 +35,15 @@ class RtmBot(object): self.input(reply) self.crons() self.output() + self.autoping() time.sleep(.1) + def autoping(self): + #hardcode the interval to 3 seconds + now = int(time.time()) + if now > self.last_ping + 3: + print 'ping' + self.slack_client.server.ping() + self.last_ping = now def input(self, data): if "type" in data: function_name = "process_" + data["type"] From a907737aa62a50ca1719679966a4e92a976fcc47 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Wed, 4 Mar 2015 13:02:14 -0800 Subject: [PATCH 47/77] remove debug --- rtmbot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rtmbot.py b/rtmbot.py index ac2c9dd..bc0669c 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -41,7 +41,6 @@ class RtmBot(object): #hardcode the interval to 3 seconds now = int(time.time()) if now > self.last_ping + 3: - print 'ping' self.slack_client.server.ping() self.last_ping = now def input(self, data): From 6191182cb30034094de60f04c309125e4f9ff68c Mon Sep 17 00:00:00 2001 From: Matt Skone Date: Thu, 28 May 2015 15:28:44 -0700 Subject: [PATCH 48/77] Support optional command-line argument specifying full path to conf file. --- rtmbot.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rtmbot.py b/rtmbot.py index bc0669c..3bd4184 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -10,6 +10,7 @@ import os import sys import time import logging +from argparse import ArgumentParser from slackclient import SlackClient @@ -164,14 +165,27 @@ def main_loop(): 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('rtmbot.conf', 'r')) + config = yaml.load(file(args.config or 'rtmbot.conf', 'r')) debug = config["DEBUG"] bot = RtmBot(config["SLACK_TOKEN"]) site_plugins = [] From 22fb111bd57700e6be37ff6cecea0225bf339562 Mon Sep 17 00:00:00 2001 From: Ryan Huber Date: Fri, 31 Jul 2015 15:17:36 -0700 Subject: [PATCH 49/77] add an example init script --- doc/example-init/rtmbot.init | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100755 doc/example-init/rtmbot.init diff --git a/doc/example-init/rtmbot.init b/doc/example-init/rtmbot.init new file mode 100755 index 0000000..18fbdc4 --- /dev/null +++ b/doc/example-init/rtmbot.init @@ -0,0 +1,76 @@ +#!/bin/sh + +### BEGIN INIT INFO +# Provides: exampledaemon +# Required-Start: $local_fs $remote_fs $network $syslog +# Required-Stop: $local_fs $remote_fs $network $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Example +# Description: Example start-stop-daemon - Debian +### END INIT INFO + +NAME="rtmbot" +PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin" +APPDIR="/opt/rtmbot" +APPBIN="rtmbot.py" +APPARGS="--config ${APPDIR}/rtmbot.conf" +USER="ubuntu" +GROUP="ubuntu" + +# Include functions +set -e +. /lib/lsb/init-functions + +start() { + printf "Starting '$NAME'... " + start-stop-daemon --start --background --chuid "$USER:$GROUP" --make-pidfile --pidfile /var/run/$NAME.pid --exec "$APPDIR/$APPBIN" -- $APPARGS || true + printf "done\n" +} + +#We need this function to ensure the whole process tree will be killed +killtree() { + local _pid=$1 + local _sig=${2-TERM} + for _child in $(ps -o pid --no-headers --ppid ${_pid}); do + killtree ${_child} ${_sig} + done + kill -${_sig} ${_pid} +} + +stop() { + printf "Stopping '$NAME'... " + [ -z `cat /var/run/$NAME.pid 2>/dev/null` ] || \ + while test -d /proc/$(cat /var/run/$NAME.pid); do + killtree $(cat /var/run/$NAME.pid) 15 + sleep 0.5 + done + [ -z `cat /var/run/$NAME.pid 2>/dev/null` ] || rm /var/run/$NAME.pid + printf "done\n" +} + +status() { + status_of_proc -p /var/run/$NAME.pid "" $NAME && exit 0 || exit $? +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + stop + start + ;; + status) + status + ;; + *) + echo "Usage: $NAME {start|stop|restart|status}" >&2 + exit 1 + ;; +esac + +exit 0 From c6a8b30d2da66b36cf1683565a2fb72e8462b02d Mon Sep 17 00:00:00 2001 From: Thomas Zakrajsek Date: Sun, 2 Aug 2015 18:26:03 -0400 Subject: [PATCH 50/77] 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 51/77] 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 52/77] 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 53/77] 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 54/77] 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 55/77] 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 1a75fa65b371b76cd0f447f807f38bfcdcc518a5 Mon Sep 17 00:00:00 2001 From: Julian Yap Date: Wed, 16 Mar 2016 14:00:00 -1000 Subject: [PATCH 56/77] Modify git clone URL so anyone can clone the repository --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e078a17..00a608a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Installation 1. Download the python-rtmbot code - git clone git@github.com:slackhq/python-rtmbot.git + git clone https://github.com/slackhq/python-rtmbot.git cd python-rtmbot 2. Install dependencies ([virtualenv](http://virtualenv.readthedocs.org/en/latest/) is recommended.) From 7d81a404ed0ac552ada6b1f763fd2186fa49f1c8 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Fri, 15 Apr 2016 12:18:31 -0700 Subject: [PATCH 57/77] Adding scaffolding and example testing file. --- .gitignore | 1 + .travis.yml | 18 ++++++++++++++++++ requirements-dev.txt | 3 +++ tests/test_example.py | 2 ++ tox.ini | 23 +++++++++++++++++++++++ 5 files changed, 47 insertions(+) create mode 100644 .travis.yml create mode 100644 requirements-dev.txt create mode 100644 tests/test_example.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index a044a8c..1e6aa92 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /rtmbot.conf /plugins/** env +.tox \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f8b358d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +sudo: false +language: python +python: + - "3.5" +env: + matrix: + - TOX_ENV=py27 + - TOX_ENV=py34 + - TOX_ENV=py35 + - TOX_ENV=flake8 +cache: pip +install: + - "travis_retry pip install setuptools --upgrade" + - "travis_retry pip install tox" +script: + - tox -e $TOX_ENV +after_script: + - cat .tox/$TOX_ENV/log/*.log \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..c6513a4 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest>=2.8.2 +pytest-pythonpath>=0.3 +tox>=1.8.0 \ No newline at end of file diff --git a/tests/test_example.py b/tests/test_example.py new file mode 100644 index 0000000..c8d72d0 --- /dev/null +++ b/tests/test_example.py @@ -0,0 +1,2 @@ +def test_example(): + assert True \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..a3d40c5 --- /dev/null +++ b/tox.ini @@ -0,0 +1,23 @@ +[tox] +envlist= + py{27,34,35}, + flake8 +skipsdist=true + +[flake8] +max-line-length= 100 +exclude= tests/* + +[testenv] +commands=py.test {posargs:tests} +deps = + -r{toxinidir}/requirements-dev.txt +basepython = + py27: python2.7 + py34: python3.4 + py35: python3.5 + +[testenv:flake8] +basepython=python +deps=flake8 +commands=flake8 {toxinidir}/web3 \ No newline at end of file From c14aa887268874bc048e401eb93b35dfc51a08c9 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Fri, 15 Apr 2016 12:21:54 -0700 Subject: [PATCH 58/77] Adding TravisCI build status image --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e078a17..b1a9a08 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ python-rtmbot ============= + +[![Build Status](https://travis-ci.org/slackhq/python-rtmbot.png)](https://travis-ci.org/slackhq/python-rtmbot) + A Slack bot written in python that connects via the RTM API. Python-rtmbot is a callback based bot engine. The plugins architecture should be familiar to anyone with knowledge to the [Slack API](https://api.slack.com) and Python. The configuration file format is YAML. From a7726151853ba2dd59ed71ae67184d001995d45f Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Fri, 15 Apr 2016 12:25:29 -0700 Subject: [PATCH 59/77] Adding correct directories to linting test --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a3d40c5..0c59058 100644 --- a/tox.ini +++ b/tox.ini @@ -20,4 +20,4 @@ basepython = [testenv:flake8] basepython=python deps=flake8 -commands=flake8 {toxinidir}/web3 \ No newline at end of file +commands=flake8 {toxinidir}/rtmbot.py {toxinidir}/example-plugins \ No newline at end of file From 05ca843d313d98a1e3b11303f4351acc3b4c678f Mon Sep 17 00:00:00 2001 From: Quentin Nerden Date: Mon, 10 Aug 2015 12:00:00 +0200 Subject: [PATCH 60/77] running pycharm code instpector and pycharm reformat code --- rtmbot.py | 63 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/rtmbot.py b/rtmbot.py index 3bd4184..59964a1 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -1,11 +1,11 @@ #!/usr/bin/env python import sys + sys.dont_write_bytecode = True import glob import yaml -import json import os import sys import time @@ -14,20 +14,24 @@ 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): self.last_ping = 0 self.token = token self.bot_plugins = [] self.slack_client = None + 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() @@ -38,12 +42,14 @@ class RtmBot(object): self.output() self.autoping() time.sleep(.1) + def autoping(self): - #hardcode the interval to 3 seconds + # 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"] @@ -51,35 +57,39 @@ class RtmBot(object): 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 != None and output[1] != None: - if limiter == True: + if channel is not None and output[1] is not None: + if limiter: time.sleep(.1) - limiter = False - message = output[1].encode('ascii','ignore') - channel.send_message("{}".format(message)) - limiter = True + limiter = True # TODO: check goal: no sleep for 1st channel, sleep of all after ? + # TODO: find out how to safely encode stuff if needed :( + # message = output[1].encode('utf-8','ignore') + channel.send_message(output[1]) # message + + 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(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, directory + '/plugins/') + for plugin in glob.glob(directory + '/plugins/*.py') + glob.glob(directory + '/plugins/*/*.py'): logging.info(plugin) name = plugin.split('/')[-1][:-3] -# try: + # try: self.bot_plugins.append(Plugin(name)) -# except: -# print "error loading plugin %s" % name + # except: + # print "error loading plugin %s" % name class Plugin(object): - def __init__(self, name, plugin_config={}): + def __init__(self, name): self.name = name self.jobs = [] self.module = __import__(name) @@ -90,32 +100,36 @@ class Plugin(object): 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: - self.jobs.append(Job(interval, eval("self.module."+function))) + self.jobs.append(Job(interval, eval("self.module." + function))) 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): - #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: try: - eval("self.module."+function_name)(data) + eval("self.module." + function_name)(data, bot, config) except: dbg("problem in module {} {}".format(function_name, data)) else: - eval("self.module."+function_name)(data) + eval("self.module." + function_name)(data, bot, config) if "catch_all" in dir(self.module): try: self.module.catch_all(data) except: dbg("problem in catch all") + def do_jobs(self): for job in self.jobs: job.check() + def do_output(self): output = [] while True: @@ -129,15 +143,19 @@ class Plugin(object): self.module.outputs = [] return output + class Job(object): def __init__(self, interval, function): 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: @@ -150,6 +168,7 @@ class Job(object): self.lastrun = time.time() pass + class UnknownChannel(Exception): pass @@ -182,8 +201,8 @@ if __name__ == "__main__": directory = os.path.dirname(sys.argv[0]) if not directory.startswith('/'): directory = os.path.abspath("{}/{}".format(os.getcwd(), - directory - )) + directory + )) config = yaml.load(file(args.config or 'rtmbot.conf', 'r')) debug = config["DEBUG"] @@ -195,7 +214,7 @@ if __name__ == "__main__": if config.has_key("DAEMON"): if config["DAEMON"]: import daemon + with daemon.DaemonContext(): main_loop() main_loop() - From aff0446b8f34202d8b2da4d05d977aa9719502b1 Mon Sep 17 00:00:00 2001 From: Quentin Nerden Date: Fri, 14 Aug 2015 11:35:06 +0200 Subject: [PATCH 61/77] adding deamon to requirements. It is needed in rtmbot.py --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index d740102..ca595ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ python-daemon pyyaml websocket-client slackclient +deamon From 6052333c6482d7629c92776bd3b870fac30daaf2 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Fri, 15 Apr 2016 12:40:27 -0700 Subject: [PATCH 62/77] Removing non-PEP8 changes to clean up PR --- requirements.txt | 1 - rtmbot.py | 12 ++++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index ca595ee..d740102 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,3 @@ python-daemon pyyaml websocket-client slackclient -deamon diff --git a/rtmbot.py b/rtmbot.py index 59964a1..65c2668 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -7,7 +7,6 @@ sys.dont_write_bytecode = True import glob import yaml import os -import sys import time import logging from argparse import ArgumentParser @@ -66,6 +65,9 @@ class RtmBot(object): if channel is not None and output[1] is not None: if limiter: time.sleep(.1) + limiter = False + message = output[1].encode('ascii','ignore') + channel.send_message("{}".format(message)) limiter = True # TODO: check goal: no sleep for 1st channel, sleep of all after ? # TODO: find out how to safely encode stuff if needed :( # message = output[1].encode('utf-8','ignore') @@ -89,7 +91,9 @@ class RtmBot(object): # print "error loading plugin %s" % name class Plugin(object): - def __init__(self, name): + def __init__(self, name, plugin_config=None): + if plugin_config is None: + plugin_config = {} #TODO: is this necessary? self.name = name self.jobs = [] self.module = __import__(name) @@ -115,11 +119,11 @@ class Plugin(object): # this makes the plugin fail with stack trace in debug mode if not debug: try: - eval("self.module." + function_name)(data, bot, config) + eval("self.module." + function_name)(data) except: dbg("problem in module {} {}".format(function_name, data)) else: - eval("self.module." + function_name)(data, bot, config) + eval("self.module." + function_name)(data) if "catch_all" in dir(self.module): try: self.module.catch_all(data) From 25ce082eacfdd70d6e99799cdb3b27ca37443bcf Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Fri, 15 Apr 2016 14:03:20 -0700 Subject: [PATCH 63/77] Fix PEP8 compliance in example plugins and rtmbot --- doc/example-plugins/canary.py | 3 ++- doc/example-plugins/counter.py | 5 +++-- doc/example-plugins/repeat.py | 7 ++++--- doc/example-plugins/todo.py | 10 ++++++---- rtmbot.py | 28 +++++++++++++++++----------- tox.ini | 2 +- 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/doc/example-plugins/canary.py b/doc/example-plugins/canary.py index 2c76784..b2cea10 100644 --- a/doc/example-plugins/canary.py +++ b/doc/example-plugins/canary.py @@ -1,8 +1,9 @@ import time outputs = [] + def canary(): - #NOTE: you must add a real channel ID for this to work + # NOTE: you must add a real channel ID for this to work outputs.append(["D12345678", "bot started: " + str(time.time())]) canary() diff --git a/doc/example-plugins/counter.py b/doc/example-plugins/counter.py index 84e9012..00fac1a 100644 --- a/doc/example-plugins/counter.py +++ b/doc/example-plugins/counter.py @@ -2,8 +2,9 @@ import time crontable = [] outputs = [] -crontable.append([5,"say_time"]) +crontable.append([5, "say_time"]) + def say_time(): - #NOTE: you must add a real channel ID for this to work + # NOTE: you must add a real channel ID for this to work outputs.append(["D12345678", time.time()]) diff --git a/doc/example-plugins/repeat.py b/doc/example-plugins/repeat.py index 54bc581..3106457 100644 --- a/doc/example-plugins/repeat.py +++ b/doc/example-plugins/repeat.py @@ -1,8 +1,9 @@ -import time crontable = [] outputs = [] + def process_message(data): if data['channel'].startswith("D"): - outputs.append([data['channel'], "from repeat1 \"{}\" in channel {}".format(data['text'], data['channel']) ]) - + outputs.append([data['channel'], "from repeat1 \"{}\" in channel {}".format( + data['text'], data['channel'])] + ) diff --git a/doc/example-plugins/todo.py b/doc/example-plugins/todo.py index 616be49..4437840 100644 --- a/doc/example-plugins/todo.py +++ b/doc/example-plugins/todo.py @@ -6,19 +6,21 @@ crontabs = [] tasks = {} -FILE="plugins/todo.data" + +FILE = "plugins/todo.data" if os.path.isfile(FILE): tasks = pickle.load(open(FILE, 'rb')) + def process_message(data): global tasks channel = data["channel"] text = data["text"] - #only accept tasks on DM channels + # only accept tasks on DM channels if channel.startswith("D"): if channel not in tasks.keys(): tasks[channel] = [] - #do command stuff + # do command stuff if text.startswith("todo"): tasks[channel].append(text[5:]) outputs.append([channel, "added"]) @@ -36,4 +38,4 @@ def process_message(data): tasks[channel].pop(num) if text == "show": print tasks - pickle.dump(tasks, open(FILE,"wb")) + pickle.dump(tasks, open(FILE, "wb")) diff --git a/rtmbot.py b/rtmbot.py index 65c2668..c23156a 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -1,18 +1,17 @@ #!/usr/bin/env python - import sys - -sys.dont_write_bytecode = True - import glob import yaml import os import time import logging + from argparse import ArgumentParser from slackclient import SlackClient +sys.dont_write_bytecode = True + def dbg(debug_string): if debug: @@ -66,14 +65,14 @@ class RtmBot(object): if limiter: time.sleep(.1) limiter = False - message = output[1].encode('ascii','ignore') + message = output[1].encode('ascii', 'ignore') channel.send_message("{}".format(message)) - limiter = True # TODO: check goal: no sleep for 1st channel, sleep of all after ? + limiter = True + # TODO: check goal: no sleep for 1st channel, sleep of all after ? # TODO: find out how to safely encode stuff if needed :( # message = output[1].encode('utf-8','ignore') channel.send_message(output[1]) # message - def crons(self): for plugin in self.bot_plugins: plugin.do_jobs() @@ -82,7 +81,8 @@ class RtmBot(object): for plugin in glob.glob(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'): + for plugin in glob.glob(directory + '/plugins/*.py') + \ + glob.glob(directory + '/plugins/*/*.py'): logging.info(plugin) name = plugin.split('/')[-1][:-3] # try: @@ -90,10 +90,12 @@ class RtmBot(object): # except: # print "error loading plugin %s" % name + class Plugin(object): + def __init__(self, name, plugin_config=None): if plugin_config is None: - plugin_config = {} #TODO: is this necessary? + plugin_config = {} # TODO: is this necessary? self.name = name self.jobs = [] self.module = __import__(name) @@ -179,7 +181,11 @@ class UnknownChannel(Exception): def main_loop(): if "LOGFILE" in config: - logging.basicConfig(filename=config["LOGFILE"], level=logging.INFO, format='%(asctime)s %(message)s') + logging.basicConfig( + filename=config["LOGFILE"], + level=logging.INFO, + format='%(asctime)s %(message)s' + ) logging.info(directory) try: bot.start() @@ -215,7 +221,7 @@ if __name__ == "__main__": files_currently_downloading = [] job_hash = {} - if config.has_key("DAEMON"): + if 'DAEMON' in config: if config["DAEMON"]: import daemon diff --git a/tox.ini b/tox.ini index 0c59058..d8924e4 100644 --- a/tox.ini +++ b/tox.ini @@ -20,4 +20,4 @@ basepython = [testenv:flake8] basepython=python deps=flake8 -commands=flake8 {toxinidir}/rtmbot.py {toxinidir}/example-plugins \ No newline at end of file +commands=flake8 {toxinidir}/rtmbot.py {toxinidir}/doc/example-plugins \ No newline at end of file From 5898a379976bfa6bcd8a291229e7d0d800bea03c Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Fri, 15 Apr 2016 14:32:19 -0700 Subject: [PATCH 64/77] Remove spurious change unrelated to PEP8 --- rtmbot.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/rtmbot.py b/rtmbot.py index c23156a..4b77249 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -68,10 +68,6 @@ class RtmBot(object): message = output[1].encode('ascii', 'ignore') channel.send_message("{}".format(message)) limiter = True - # TODO: check goal: no sleep for 1st channel, sleep of all after ? - # TODO: find out how to safely encode stuff if needed :( - # message = output[1].encode('utf-8','ignore') - channel.send_message(output[1]) # message def crons(self): for plugin in self.bot_plugins: @@ -95,7 +91,7 @@ class Plugin(object): def __init__(self, name, plugin_config=None): if plugin_config is None: - plugin_config = {} # TODO: is this necessary? + plugin_config = {} # TODO: is this variable necessary? self.name = name self.jobs = [] self.module = __import__(name) From aa710d2d4ef4cd032253db513321657b7826c947 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Fri, 15 Apr 2016 14:49:38 -0700 Subject: [PATCH 65/77] Fix Python3 linting --- doc/example-plugins/todo.py | 3 ++- rtmbot.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/example-plugins/todo.py b/doc/example-plugins/todo.py index 4437840..cd1db8d 100644 --- a/doc/example-plugins/todo.py +++ b/doc/example-plugins/todo.py @@ -1,3 +1,4 @@ +from __future__ import print_function import os import pickle @@ -37,5 +38,5 @@ def process_message(data): num = int(text.split()[1]) - 1 tasks[channel].pop(num) if text == "show": - print tasks + print(tasks) pickle.dump(tasks, open(FILE, "wb")) diff --git a/rtmbot.py b/rtmbot.py index 4b77249..a275cc5 100755 --- a/rtmbot.py +++ b/rtmbot.py @@ -210,7 +210,7 @@ if __name__ == "__main__": directory )) - config = yaml.load(file(args.config or 'rtmbot.conf', 'r')) + config = yaml.load(open(args.config or 'rtmbot.conf', 'r')) debug = config["DEBUG"] bot = RtmBot(config["SLACK_TOKEN"]) site_plugins = [] From f12a90ab0a8f1777f4d7b8af330d7080a3a4a78a Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Sun, 17 Apr 2016 17:34:17 -0700 Subject: [PATCH 66/77] 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 67/77] 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 68/77] 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 69/77] 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 70/77] 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 71/77] 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 72/77] 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') From b9da306c8f255960eaa0f10fc3f17171be455ec6 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Mon, 25 Apr 2016 11:27:34 -0700 Subject: [PATCH 73/77] Renaming this for backwards compatibility. --- start_rtmbot.py => rtmbot.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename start_rtmbot.py => rtmbot.py (100%) diff --git a/start_rtmbot.py b/rtmbot.py similarity index 100% rename from start_rtmbot.py rename to rtmbot.py From 45e7293eb4463f51257c547956888b4607030811 Mon Sep 17 00:00:00 2001 From: Eyal Levin Date: Tue, 26 Apr 2016 17:20:05 +0300 Subject: [PATCH 74/77] Fix startup command. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f584b5c..f26338d 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ To install the example 'repeat' plugin The repeat plugin will now be loaded by the bot on startup. - ./start_rtmbot.py + ./rtmbot.py Create Plugins -------- From 60b7b2ebadcbff79ef7d63c74ce1465254c932c4 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Fri, 29 Apr 2016 17:31:09 -0700 Subject: [PATCH 75/77] Change filename in tox to updated name --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index acfcca9..a4b8a3d 100644 --- a/tox.ini +++ b/tox.ini @@ -20,4 +20,4 @@ basepython = [testenv:flake8] basepython=python deps=flake8 -commands=flake8 {toxinidir}/start_rtmbot.py {toxinidir}/rtmbot/core.py {toxinidir}/setup.py {toxinidir}/doc/example-plugins +commands=flake8 {toxinidir}/rtmbot.py {toxinidir}/rtmbot/core.py {toxinidir}/setup.py {toxinidir}/doc/example-plugins From ef76bd58d2e8252e4ec2519260a401e5226b0d23 Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Sun, 1 May 2016 16:24:16 -0700 Subject: [PATCH 76/77] Adding initial test for rtmbot and hopefully coveralls --- README.md | 1 + requirements-dev.txt | 8 +++++++- rtmbot/__init__.py | 2 +- rtmbot/core.py | 13 ++++++++++++- tests/test_rtmbot_core.py | 20 ++++++++++++++++++++ tox.ini | 14 ++++++++++++-- 6 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 tests/test_rtmbot_core.py diff --git a/README.md b/README.md index f26338d..19e1aa3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ python-rtmbot ============= [![Build Status](https://travis-ci.org/slackhq/python-rtmbot.png)](https://travis-ci.org/slackhq/python-rtmbot) +[![Coverage Status](https://coveralls.io/repos/github/slackhq/python-rtmbot/badge.svg?branch=master)](https://coveralls.io/github/slackhq/python-rtmbot?branch=master) A Slack bot written in python that connects via the RTM API. diff --git a/requirements-dev.txt b/requirements-dev.txt index c6513a4..8e69fc4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,9 @@ +coveralls==1.1 +ipdb==0.9.3 +ipython==4.1.2 +pdbpp==0.8.3 pytest>=2.8.2 +pytest-cov==2.2.1 pytest-pythonpath>=0.3 -tox>=1.8.0 \ No newline at end of file +testfixtures==4.9.1 +tox>=1.8.0 diff --git a/rtmbot/__init__.py b/rtmbot/__init__.py index 5af2406..bb67a43 100644 --- a/rtmbot/__init__.py +++ b/rtmbot/__init__.py @@ -1 +1 @@ -from core import * +from .core import * diff --git a/rtmbot/core.py b/rtmbot/core.py index bf64782..049bb77 100755 --- a/rtmbot/core.py +++ b/rtmbot/core.py @@ -12,6 +12,17 @@ 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 @@ -30,7 +41,7 @@ class RtmBot(object): logging.basicConfig(filename=log_file, level=logging.INFO, format='%(asctime)s %(message)s') - logging.info(self.directory) + logging.info('Initialized in: {}'.format(self.directory)) self.debug = self.config.get('DEBUG', False) # initialize stateful fields diff --git a/tests/test_rtmbot_core.py b/tests/test_rtmbot_core.py new file mode 100644 index 0000000..0353147 --- /dev/null +++ b/tests/test_rtmbot_core.py @@ -0,0 +1,20 @@ +from testfixtures import LogCapture +from rtmbot.core import RtmBot + + +def test_init(): + with LogCapture() as l: + rtmbot = RtmBot({ + 'SLACK_TOKEN': 'test-12345', + 'BASE_PATH': '/tmp/', + 'LOGFILE': '/tmp/rtmbot.log', + 'DEBUG': True + }) + + assert rtmbot.token == 'test-12345' + assert rtmbot.directory == '/tmp/' + assert rtmbot.debug == True + + l.check( + ('root', 'INFO', 'Initialized in: /tmp/') + ) diff --git a/tox.ini b/tox.ini index a4b8a3d..63b4d9b 100644 --- a/tox.ini +++ b/tox.ini @@ -9,9 +9,14 @@ max-line-length= 100 exclude= tests/* [testenv] -commands=py.test {posargs:tests} +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH +commands = + py.test --cov-report= --cov=rtmbot {posargs:tests} + coveralls + deps = -r{toxinidir}/requirements-dev.txt + -r{toxinidir}/requirements.txt basepython = py27: python2.7 py34: python3.4 @@ -20,4 +25,9 @@ basepython = [testenv:flake8] basepython=python deps=flake8 -commands=flake8 {toxinidir}/rtmbot.py {toxinidir}/rtmbot/core.py {toxinidir}/setup.py {toxinidir}/doc/example-plugins +commands= + flake8 \ + {toxinidir}/rtmbot.py \ + {toxinidir}/rtmbot/core.py \ + {toxinidir}/setup.py \ + {toxinidir}/doc/example-plugins \ No newline at end of file From 1a4b96584e18aa84ab1a0a3fc8c4188335fc53db Mon Sep 17 00:00:00 2001 From: Jeff Ammons Date: Wed, 11 May 2016 15:10:47 -0700 Subject: [PATCH 77/77] Adding testing/coverage files to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6bca4dd..61dfd4e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ env *.un~ 0/ tests/.cache +.coverage +.cache