From 8fbf77b9d99834b4fe3fb0857ffef2fa1af035b5 Mon Sep 17 00:00:00 2001 From: Randall Barnhart Date: Mon, 28 Mar 2016 17:37:36 -0600 Subject: [PATCH 1/5] Complete rewrite of the bot to support multiple team deployment on Beep Boop. --- plugins/.gitkeep => bot/__init__.py | 0 bot/app.py | 24 +++ bot/event_handler.py | 52 ++++++ bot/event_handler.pyc | Bin 0 -> 2354 bytes bot/messenger.py | 61 +++++++ bot/messenger.pyc | Bin 0 -> 4460 bytes bot/slack_bot.py | 75 +++++++++ bot/slack_bot.pyc | Bin 0 -> 3426 bytes bot/slack_clients.py | 38 +++++ bot/slack_clients.pyc | Bin 0 -> 2325 bytes plugins/starter.py | 62 -------- requirements.txt | 14 +- rtmbot.py | 236 ---------------------------- 13 files changed, 259 insertions(+), 303 deletions(-) rename plugins/.gitkeep => bot/__init__.py (100%) create mode 100755 bot/app.py create mode 100644 bot/event_handler.py create mode 100644 bot/event_handler.pyc create mode 100644 bot/messenger.py create mode 100644 bot/messenger.pyc create mode 100644 bot/slack_bot.py create mode 100644 bot/slack_bot.pyc create mode 100644 bot/slack_clients.py create mode 100644 bot/slack_clients.pyc delete mode 100644 plugins/starter.py delete mode 100755 rtmbot.py diff --git a/plugins/.gitkeep b/bot/__init__.py similarity index 100% rename from plugins/.gitkeep rename to bot/__init__.py diff --git a/bot/app.py b/bot/app.py new file mode 100755 index 0000000..337981b --- /dev/null +++ b/bot/app.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import logging +import os + +from slack_bot import SlackBot + +logger = logging.getLogger(__name__) + + +if __name__ == "__main__": + + log_level = os.getenv("LOG_LEVEL", "INFO") + logging.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s', level=log_level) + + slack_token = os.getenv("SLACK_TOKEN", "") + logging.info("token: {}".format(slack_token)) + + if slack_token == "": + logging.info("SLACK_TOKEN env var not set, expecting token to be provided by Resourcer events") + slack_token = None + + bot = SlackBot(slack_token) + bot.start({}) diff --git a/bot/event_handler.py b/bot/event_handler.py new file mode 100644 index 0000000..bbe4f15 --- /dev/null +++ b/bot/event_handler.py @@ -0,0 +1,52 @@ +import json +import logging +import re + +logger = logging.getLogger(__name__) + + +class RtmEventHandler(object): + def __init__(self, slack_clients, msg_writer): + self.clients = slack_clients + self.msg_writer = msg_writer + + def handle(self, event): + + if 'type' in event: + self._handle_by_type(event['type'], event) + + def _handle_by_type(self, event_type, event): + # See https://api.slack.com/rtm for a full list of events + if event_type == 'error': + # error + self.msg_writer.write_error(event['channel'], json.dumps(event)) + elif event_type == 'message': + # message was sent to channel + self._handle_message(event) + elif event_type == 'channel_joined': + # you joined a channel + self.msg_writer.write_help_message(event['channel']) + elif event_type == 'group_joined': + # you joined a private group + self.msg_writer.write_help_message(event['channel']) + else: + pass + + def _handle_message(self, event): + # Filter out messages from the bot itself + if not self.clients.is_message_from_me(event['user']): + + msg_txt = event['text'] + + if self.clients.is_bot_mention(msg_txt): + # e.g. user typed: "@pybot tell me a joke!" + if 'help' in msg_txt: + self.msg_writer.write_help_message(event['channel']) + elif re.search('hi|hey|hello|howdy', msg_txt): + self.msg_writer.write_greeting(event['channel'], event['user']) + elif 'joke' in msg_txt: + self.msg_writer.write_joke(event['channel']) + elif 'attachment' in msg_txt: + self.msg_writer.demo_attachment(event['channel']) + else: + self.msg_writer.write_prompt(event['channel']) diff --git a/bot/event_handler.pyc b/bot/event_handler.pyc new file mode 100644 index 0000000000000000000000000000000000000000..825a5c19402f734e0d3c8da29ed3b526b6bf7493 GIT binary patch literal 2354 zcmc&#&5j&35Vk!(n^`6#EKGSJmCK3gVm{jXmXZxvKot=bF)9clUq&?eA|g(SJtx z{Rks`jVZ@hidu-Xoh-$f9P~<@DPg3jM#7B66}BcKhh{3yVKWjx2yrFEw`Y6g<#aKI z$MPS1PU1a?X>2}wZfyKwY4h6nT?P_+#~9%QOa`$z9(*Af{E#rBFeHroFbJG>0W!J( z4TbRuMtFefQi@9-}qT6$Y7eM~+z&y&7qmIF*ffYnAW8&qt-LXf$|h`CNs*4 z24&AUybl5R0g8*M@JED0#mGQ;2;(sG?o8IlvebKJIuapIcwq4jF3d4mF^jL!rT zXLnmjOp!nb3|EM8P;rKXe>Zz#$?6jBR2n% z&By(=w4d|dnD^}9YBIQj2Wf#cqD|Q3r~fzUQKu`ML`Kqk=Q~c~PS(^cHztJT+Qc_u zmvwqwxzd_E&Y@a+w`qIDf_yPE;~S8e$=a}20-_4ea-e+EwBeP~4nYof#|l%ox>xgK z=qgUHq~jw|Atff>QteZEyp(UCNvOFxl5=$*;~@>jZ-H34Z> zkl=gn6L=pL1g3(&rd|V?Ue7Z=JrWg|fYW0^r{#Bl2!L8msoHc1^!jly+#3vsXn1X~ z&!>aSUk1xNgJthB$I&k8?}N$gKPGe)B(wjD`dSRHnM%{W<_+3X_pHs!$O08NC5kY1 zMR{46Ek<3t%fh|Lw`^3o3&UM;8RIf58Z@B=ok^*IMgqMI14Hz=cMTTIQmE0ku(Gjq zmid^zO=qZacoT`G!7P1Nuz5$*wKpb~cFhqx6(>M{8o6nlzR7}l-x= z#WUMe@_KV4mR{>WPm1`0h!tp>%Gr1}nZ323MS33-BVW7q8ih>XB?aY<=JgeH#;1e` om!E7vN@9XrRVIt `hi <@" + bot_uid + ">` - I'll respond with a randomized greeting mentioning your user. :wave:", + "> `<@" + bot_uid + "> joke` - I'll tell you one of my finest jokes, with a typing pause for effect. :laughing:", + "> `<@" + bot_uid + "> attachment` - I'll demo a post with an attachment using the Web API. :paperclip:") + self.send_message(channel_id, txt) + + def write_greeting(self, channel_id, user_id): + greetings = ['Hi', 'Hello', 'Nice to meet you', 'Howdy', 'Salutations'] + txt = '{}, <@{}>!'.format(random.choice(greetings), user_id) + self.send_message(channel_id, txt) + + def write_prompt(self, channel_id): + bot_uid = self.clients.bot_user_id() + txt = "I'm sorry, I didn't quite understand... Can I help you? (e.g. `<@" + bot_uid + "> help`)" + self.send_message(channel_id, txt) + + def write_joke(self, channel_id): + question = "Why did the python cross the road?" + self.send_message(channel_id, question) + self.clients.send_user_typing_pause(channel_id) + answer = "To eat the chicken on the other side! :laughing:" + self.send_message(channel_id, answer) + + + def write_error(self, channel_id, err_msg): + txt = ":face_with_head_bandage: my maker didn't handle this error very well:\n>```{}```".format(err_msg) + self.send_message(channel_id, txt) + + def demo_attachment(self, channel_id): + txt = "Beep Beep Boop is a ridiculously simple hosting platform for your Slackbots." + attachment = { + "pretext": "We bring bots to life. :sunglasses: :thumbsup:", + "title": "Host, deploy and share your bot in seconds.", + "title_link": "https://beepboophq.com/", + "text": txt, + "fallback": txt, + "image_url": "https://storage.googleapis.com/beepboophq/_assets/bot-1.22f6fb.png", + "color": "#7CD197", + } + self.clients.web.chat.post_message(channel_id, txt, attachments=[attachment], as_user='true') diff --git a/bot/messenger.pyc b/bot/messenger.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc978da3036c6f156f23e47fa4971fc900e25a4a GIT binary patch literal 4460 zcmc&%U2_|^6~&SgCCVRi+9a+goet7W$4qLGaUPV0lW}az57snQHLEtBs1K{<0$hUH zU91L=nW|;{v$%;M}wAf8E*n<3IoXLnfAgZTvpP z*F8g_@E?gnh|^U)5~q>SLQGpix5Ya=ZU}|?rWi*;wZ%V#cqhbj z`7Ww|($48B*Uf6Z2(8cYb;l@R7uK;Vtem#U0#(|y09CLOFL1gk)CRX;l})Z}3Dx1s zwoqGK>5BNIw+*9N3&qvnf?KAWV%ibYEiv5|(=PW# zV$l|F+hV#yK?rlEweDZ}4-XD`l!&mM$O4%Z2n-W|Oq4 zbU}}Ngv*7et*0(C#*+b)SGLi;+o<{-dUmFQgQ`q(M3l)qk1K2rJn>|uN3)z8$F?a` zA5&O8d8Y9twF- zN2@-OXA>j8ed-SB;Y{uY3(NWP0vArEvcUvfn!jr$^G0jWaKt4ywxTL;a0Ynvr2-C0&@vam#I6x!yc&syz zMT&HmPtZ4XF`G}lPqRsh9jz8oy0qANZDBT?qLS-ja)t|$vu9rEk$iT12&;8kgIfzz z4+CQP0#H!k?1YX7$i{?o2Fx1O@h{QF80LXXT>GE=4Pe>-0(&7~=wzac`sy}PtGkPW z(A|mJ(Jg#F2*gLRS`pt1Jb>l`o&!JSn}l0r2*4g}2(%M)0bnp*ODsTuD@bpwAx)Ve z(AZ1E{PGe}x9%P_n@kffOCX7G5W4M!ntSs0Nm|T&O4xFIAukt?0MN_DlTU-onbhb? z22?W9WD^UcAueAlR%rSWYZ0OmXPajJNFGXMRQ1ry*E66*&MJkxi`1a{{k}X% zD-0myP+-23J>Adye2)WJP|w-tJu=MsA^wl`;UHdWubFEQ*dR_fGqV>U6_Ik?I^GLSfP zc>%H-?mRg=JG)$pHEA|zH>n;4;7V3`JUtIvGAh6C-&tSq`N)0*E$F5NKkPBKt z-VkDs-1jI+&C5_g(@QuV;=(|V&I&toP&dw$HR3Tr+UDF}q@Ls`r+=0iEPs$^UElR# z@RgRMhDK5oC6&UAHFCL|Re6!(eaQ{w5HAho$j#~@C+WJ;UjO9X?=k%))_EkAt_wSt zfP!>*`Orboz#>U2r!y!i*XJzdjW4t#Zku>tyWwCk!ih#W%j7k`may99QAuH{bJoYS z8sETiS`;IwH~0LS0q1Q4X!dh!^FpV!acpR9caYFIJl>70-+SEu>Z|eoc+{_}oYR>p zF*%tvg`?np_VvMw$KQO-d(CWN8_uX}-Q(B;sbe`4HrLFS0)3_TFZ77JAk;nUdJ<7r zO5#0cVZ8I9*NfZ)tL)9r{HYjJh4^aqKJX_KFv(xGk=Pcg`tF+Wf!iPwb(yCd3 z$Cxe;XSVnO)ri((BBJ;q6`xb_2NdhWa_#4PX~EipLe7$^6Ls6&t!}rwiO+U-qend) zYkI@O{fOyJGUy-ezvs8U_!~6E><5w_b~J^!COv&)XX1E#8zS(8;K(`qc67V7+x{ self.last_ping + 3: + self.clients.rtm.server.ping() + self.last_ping = now + + def stop(self, resource): + """Stop any polling loops on clients, clean up any resources, + close connections if possible. + + Args: + resource (dict of Resource JSON): See payload here - http://linktoresourcejson + """ + self.keep_running = False diff --git a/bot/slack_bot.pyc b/bot/slack_bot.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ab7c5a6983dbfba0e7049b107e55343451891a3 GIT binary patch literal 3426 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 SBpsgE?;CuhLvt8CjQ<8wR~aJ! literal 0 HcmV?d00001 diff --git a/plugins/starter.py b/plugins/starter.py deleted file mode 100644 index fde1562..0000000 --- a/plugins/starter.py +++ /dev/null @@ -1,62 +0,0 @@ -import time -import re -import random -import logging -crontable = [] -outputs = [] -attachments = [] -typing_sleep = 0 - -greetings = ['Hi friend!', 'Hello there.', 'Howdy!', 'Wazzzup!!!', 'Hi!', 'Hey.'] -help_text = "{}\n{}\n{}\n{}\n{}\n{}".format( - "I will respond to the following messages: ", - "`pybot hi` for a random greeting.", - "`pybot joke` for a question, typing indicator, then answer style joke.", - "`pybot attachment` to see a Slack attachment message.", - "`@` to demonstrate detecting a mention.", - "`pybot help` to see this again.") - -# regular expression patterns for string matching -p_bot_hi = re.compile("pybot[\s]*hi") -p_bot_joke = re.compile("pybot[\s]*joke") -p_bot_attach = re.compile("pybot[\s]*attachment") -p_bot_help = re.compile("pybot[\s]*help") - -def process_message(data): - logging.debug("process_message:data: {}".format(data)) - - if p_bot_hi.match(data['text']): - outputs.append([data['channel'], "{}".format(random.choice(greetings))]) - - elif p_bot_joke.match(data['text']): - outputs.append([data['channel'], "Why did the python cross the road?"]) - outputs.append([data['channel'], "__typing__", 5]) - outputs.append([data['channel'], "To eat the chicken on the other side! :laughing:"]) - - elif p_bot_attach.match(data['text']): - txt = "Beep Beep Boop is a ridiculously simple hosting platform for your Slackbots." - attachments.append([data['channel'], txt, build_demo_attachment(txt)]) - - elif p_bot_help.match(data['text']): - outputs.append([data['channel'], "{}".format(help_text)]) - - elif data['text'].startswith("pybot"): - outputs.append([data['channel'], "I'm sorry, I don't know how to: `{}`".format(data['text'])]) - - elif data['channel'].startswith("D"): # direct message channel to the bot - outputs.append([data['channel'], "Hello, I'm the BeepBoop python starter bot.\n{}".format(help_text)]) - -def process_mention(data): - logging.debug("process_mention:data: {}".format(data)) - outputs.append([data['channel'], "You really do care about me. :heart:"]) - -def build_demo_attachment(txt): - return { - "pretext" : "We bring bots to life. :sunglasses: :thumbsup:", - "title" : "Host, deploy and share your bot in seconds.", - "title_link" : "https://beepboophq.com/", - "text" : txt, - "fallback" : txt, - "image_url" : "https://storage.googleapis.com/beepboophq/_assets/bot-1.22f6fb.png", - "color" : "#7CD197", - } diff --git a/requirements.txt b/requirements.txt index d740102..f447a5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,9 @@ -requests -python-daemon -pyyaml -websocket-client -slackclient +docutils==0.12 +lockfile==0.12.2 +python-daemon==2.1.1 +PyYAML==3.11 +requests==2.9.1 +six==1.10.0 +slackclient==1.0.0 +slacker==0.9.9 +websocket-client==0.35.0 diff --git a/rtmbot.py b/rtmbot.py deleted file mode 100755 index 0f5eb72..0000000 --- a/rtmbot.py +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/env python - -import sys -sys.dont_write_bytecode = True - -import glob -import yaml -import json -import os -import sys -import time -import logging -import re -from argparse import ArgumentParser - -from slackclient import SlackClient - -def dbg(debug_string): - if debug: - logging.debug(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() - logging.info(u"Connected {} to {} team at https://{}.slack.com".format( - self.slack_client.server.username, - self.slack_client.server.login_data['team']['name'], - self.slack_client.server.domain)) - def start(self): - self.connect() - self.load_plugins() - while True: - for reply in self.slack_client.rtm_read(): - self.input(reply) - self.crons() - self.output() - self.autoping() - time.sleep(.1) - def autoping(self): - #hardcode the interval to 60 seconds - now = int(time.time()) - if now > self.last_ping + 60: - self.slack_client.server.ping() - self.last_ping = now - def isBotMention(self, message): - botUserName = self.slack_client.server.login_data['self']['id'] - if re.search("@{}".format(botUserName), message): - return True - else: - return False - def input(self, data): - # Make sure we're not responding to ourselves - if "user" in data and data['user'] != self.slack_client.server.login_data['self']['id']: - if "type" in data: - function_name = "process_" + data["type"] - dbg("got {}".format(function_name)) - if "text" in data and self.isBotMention(data["text"]): - function_name = "process_mention" - 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: - time.sleep(.1) - limiter = False - message = output[1].encode('ascii','ignore') - if message.startswith("__typing__"): - user_typing_json = { "type": "typing", "channel": channel.id} - logging.debug(user_typing_json) - self.slack_client.server.send_to_websocket(user_typing_json) - time.sleep(output[2]) - else: - channel.send_message("{}".format(message)) - limiter = True - for attachment in plugin.do_attachment(): - channel = self.slack_client.server.channels.find(attachment[0]) - if channel != None and attachment[1] != None: - attachments = [] - if attachment != None and attachment[2] != None: - attachments.append(attachment[2]) - attachments_json = json.dumps(attachments) - resp = self.slack_client.api_call("chat.postMessage", - text="{}".format(attachment[1]), - channel="{}".format(channel.id), - as_user="true", - attachments=attachments_json, - ) - logging.debug(resp) - def crons(self): - for plugin in self.bot_plugins: - plugin.do_jobs() - def load_plugins(self): - 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'): - 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, plugin_config={}): - self.name = name - self.jobs = [] - self.module = __import__(name) - self.register_jobs() - self.outputs = [] - if 'setup' in dir(self.module): - self.module.setup() - def register_jobs(self): - if 'crontable' in dir(self.module): - for interval, function in self.module.crontable: - self.jobs.append(Job(interval, eval("self.module."+function))) - logging.debug("crontable: {}".format(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 - 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) - 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: - if 'outputs' in dir(self.module): - if len(self.module.outputs) > 0: - logging.debug("output from {}".format(self.module)) - output.append(self.module.outputs.pop(0)) - else: - break - else: - self.module.outputs = [] - return output - def do_attachment(self): - attachment = [] - while True: - if 'attachments' in dir(self.module): - if len(self.module.attachments) > 0: - logging.debug("attachments from {}".format(self.module)) - attachment.append(self.module.attachments.pop(0)) - else: - break - else: - self.module.attachments = [] - return attachment - -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: - try: - self.function() - except: - dbg("problem") - else: - self.function() - self.lastrun = time.time() - pass - -class UnknownChannel(Exception): - pass - - -def main_loop(): - - 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 - )) - - log_level = os.getenv("LOG_LEVEL", "DEBUG") - logging.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s', level=log_level) - debug = False - if log_level == "DEBUG": - debug = True - - slack_token = os.getenv("SLACK_TOKEN", "") - logging.info("token: {}".format(slack_token)) - if slack_token == "": - logging.error("SLACK_TOKEN env var not set!") - sys.exit(1) - bot = RtmBot(slack_token) - site_plugins = [] - files_currently_downloading = [] - job_hash = {} - - main_loop() From da6ba4c9d7ffb7d7c6ffe65d598dbaa38fab916f Mon Sep 17 00:00:00 2001 From: Randall Barnhart Date: Wed, 30 Mar 2016 16:49:53 -0600 Subject: [PATCH 2/5] Import beepboop package and provide integration to support multi-team bot deployment. --- bot/app.py | 14 +++++++++++--- bot/slack_bot.py | 9 +++++++-- requirements.txt | 1 + 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/bot/app.py b/bot/app.py index 337981b..ab9bd25 100755 --- a/bot/app.py +++ b/bot/app.py @@ -3,7 +3,11 @@ import logging import os +from beepboop import resourcer +from beepboop import bot_manager + from slack_bot import SlackBot +from slack_bot import spawn_bot logger = logging.getLogger(__name__) @@ -19,6 +23,10 @@ if slack_token == "": logging.info("SLACK_TOKEN env var not set, expecting token to be provided by Resourcer events") slack_token = None - - bot = SlackBot(slack_token) - bot.start({}) + botManager = bot_manager.BotManager(spawn_bot) + res = resourcer.Resourcer(botManager) + res.start() + else: + # only want to run a single instance of the bot in dev mode + bot = SlackBot(slack_token) + bot.start({}) diff --git a/bot/slack_bot.py b/bot/slack_bot.py index bde8fbb..75bc991 100644 --- a/bot/slack_bot.py +++ b/bot/slack_bot.py @@ -9,6 +9,10 @@ logger = logging.getLogger(__name__) +def spawn_bot(): + return SlackBot() + + class SlackBot(object): def __init__(self, token=None): """Creates Slacker Web and RTM clients with API Bot User token. @@ -29,8 +33,9 @@ def start(self, resource): Args: resource (dict of Resource JSON): See payload here - http://linktoresourcejson """ - if 'SlackBotAccessToken' in resource: - res_access_token = resource['SlackBotAccessToken'] + logger.debug('Starting bot for resource: {}'.format(resource)) + if 'resource' in resource and 'SlackBotAccessToken' in resource['resource']: + res_access_token = resource['resource']['SlackBotAccessToken'] self.clients = SlackClients(res_access_token) if self.clients.rtm.rtm_connect(): diff --git a/requirements.txt b/requirements.txt index f447a5f..9def97a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +beepboop==0.1.1 docutils==0.12 lockfile==0.12.2 python-daemon==2.1.1 From f7fd75a053b13ef3ae7f0b3517ab6a472c1bbb8f Mon Sep 17 00:00:00 2001 From: Randall Barnhart Date: Wed, 30 Mar 2016 16:54:38 -0600 Subject: [PATCH 3/5] Removing compiled modules. --- bot/event_handler.pyc | Bin 2354 -> 0 bytes bot/messenger.pyc | Bin 4460 -> 0 bytes bot/slack_bot.pyc | Bin 3426 -> 0 bytes bot/slack_clients.pyc | Bin 2325 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 bot/event_handler.pyc delete mode 100644 bot/messenger.pyc delete mode 100644 bot/slack_bot.pyc delete mode 100644 bot/slack_clients.pyc diff --git a/bot/event_handler.pyc b/bot/event_handler.pyc deleted file mode 100644 index 825a5c19402f734e0d3c8da29ed3b526b6bf7493..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2354 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 SBpsgE?;CuhLvt8CjQ<8wR~aJ! From 12bd3b313b3fcd9c758e872b49db25074d5323f0 Mon Sep 17 00:00:00 2001 From: Randall Barnhart Date: Fri, 1 Apr 2016 16:06:42 -0600 Subject: [PATCH 4/5] Modified start up CMD for Dockerfile and updated README. --- Dockerfile | 2 +- README.md | 48 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8ed5e1c..7a57013 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,4 +2,4 @@ FROM python:2.7-slim ADD . /src WORKDIR /src RUN pip install -r requirements.txt -CMD python rtmbot.py +CMD python ./bot/app.py diff --git a/README.md b/README.md index 6aa6666..bd69fb8 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ Visit [Beep Boop](https://beepboophq.com/docs/article/overview) to get the scoop Install dependencies ([virtualenv](http://virtualenv.readthedocs.org/en/latest/) is recommended.) pip install -r requirements.txt - export SLACK_TOKEN=; python rtmbot.py + export SLACK_TOKEN=; python ./bot/app.py Things are looking good if the console prints something like: Connected to team at https://.slack.com. -If you want change the logging level, prepend `export LOG_LEVEL=; ` to the `python rtmbot.py` command. +If you want change the logging level, prepend `export LOG_LEVEL=; ` to the `python ./bot/app.py` command. ### Run locally in Docker docker build -t starter-python-bot . @@ -31,15 +31,47 @@ If you want change the logging level, prepend `export LOG_LEVEL=; ` ### Run in BeepBoop If you have linked your local repo with the Beep Boop service (check [here](https://beepboophq.com/0_o/my-projects)), changes pushed to the remote master branch will automatically deploy. -## Customizing the Bot -If you are looking to change what the bot responds to and how they respond, take a look at the `plugins/starter.py` file. You'll see a function that gets called on all "message" type events, which has various regular expression matches that determine when the bot responds and how it responds. Each "Plugin" is registered with the RtmBot on startup by scanning the "plugins/" directory and communicates back to the RtmBot through variables like output[] and attachments[]. +### First Conversations +When you go through the `Add your App to Slack` flow, you'll setup a new Bot User and give them a handle (like @python-rtmbot). -For more information on the Plugins pattern see the sections "Add Plugins" and "Create Plugins" at: https://github.com/slackhq/python-rtmbot/blob/master/README.md +Here is an example interaction dialog that works with this bot: +``` +Joe Dev [3:29 PM] +hi @python-rtmbot -## Acknowledgements +Slacks PythonBot BOT [3:29 PM] +Nice to meet you, @randall.barnhart! -This code was forked from https://github.com/slackhq/python-rtmbot and utilizes the awesome https://github.com/slackhq/python-slackclient project by [@rawdigits](https://github.com/rawdigits). Please see https://github.com/slackhq/python-rtmbot/blob/master/README.md for -a description about the organization of this code and using the plugins architecture. +Joe Dev [3:30 PM] +help @python-rtmbot + +Slacks PythonBot BOT [3:30 PM] +I'm your friendly Slack bot written in Python. I'll ​*​_respond_​*​ to the following commands: +>`hi @python-rtmbot` - I'll respond with a randomized greeting mentioning your user. :wave: +> `@python-rtmbot joke` - I'll tell you one of my finest jokes, with a typing pause for effect. :laughing: +> `@python-rtmbot attachment` - I'll demo a post with an attachment using the Web API. :paperclip: + +Joe Dev [3:31 PM] +@python-rtmbot: joke + +Slacks PythonBot BOT [3:31 PM] +Why did the python cross the road? + +[3:31] +To eat the chicken on the other side! :laughing: +``` + +## Code Organization +If you want to add or change an event that the bot responds (e.g. when the bot is mentioned, when the bot joins a channel, when a user types a message, etc.), you can modify the `_handle_by_type` method in `event_handler.py`. + +If you want to change the responses, then you can modify the `messenger.py` class, and make the corresponding invocation in `event_handler.py`. + +The `slack_clients.py` module provides a facade of two different Slack API clients which can be enriched to access data from Slack that is needed by your Bot: + +1. [slackclient](https://github.com/slackhq/python-slackclient) - Realtime Messaging (RTM) API to Slack via a websocket connection. +2. [slacker](https://github.com/os/slacker) - Web API to Slack via RESTful methods. + +The `slack_bot.py` module implements and interface that is needed to run a multi-team bot using the Beep Boop Resource API client, by implementing an interface that includes `start()` and `stop()` methods and a function that spawns new instances of your bot: `spawn_bot`. It is the main run loop of your bot instance that will listen to a particular Slack team's RTM events, and dispatch them to the `event_handler`. ## License From 4689b27316f309581c3c14cf8ada2a9cbe74de69 Mon Sep 17 00:00:00 2001 From: Randall Barnhart Date: Wed, 6 Apr 2016 11:27:43 -0600 Subject: [PATCH 5/5] Added links to documentation of Resource message payloads. --- bot/slack_bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/slack_bot.py b/bot/slack_bot.py index 75bc991..4ef4582 100644 --- a/bot/slack_bot.py +++ b/bot/slack_bot.py @@ -31,7 +31,7 @@ def start(self, resource): and listens for RTM events. Args: - resource (dict of Resource JSON): See payload here - http://linktoresourcejson + resource (dict of Resource JSON): See message payloads - https://beepboophq.com/docs/article/resourcer-api """ logger.debug('Starting bot for resource: {}'.format(resource)) if 'resource' in resource and 'SlackBotAccessToken' in resource['resource']: @@ -75,6 +75,6 @@ def stop(self, resource): close connections if possible. Args: - resource (dict of Resource JSON): See payload here - http://linktoresourcejson + resource (dict of Resource JSON): See message payloads - https://beepboophq.com/docs/article/resourcer-api """ self.keep_running = False