Skip to content

Commit

Permalink
Merge pull request #6 from BeepBoopHQ/multi-team-bot
Browse files Browse the repository at this point in the history
Bot Rewrite and Multi team bot support
  • Loading branch information
randompi committed Apr 18, 2016
2 parents 6974484 + 4689b27 commit e1bf6ae
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 312 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
48 changes: 40 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<YOUR SLACK TOKEN>; python rtmbot.py
export SLACK_TOKEN=<YOUR SLACK TOKEN>; python ./bot/app.py

Things are looking good if the console prints something like:

Connected <your bot name> to <your slack team> team at https://<your slack team>.slack.com.

If you want change the logging level, prepend `export LOG_LEVEL=<your level>; ` to the `python rtmbot.py` command.
If you want change the logging level, prepend `export LOG_LEVEL=<your level>; ` to the `python ./bot/app.py` command.

### Run locally in Docker
docker build -t starter-python-bot .
Expand All @@ -31,15 +31,47 @@ If you want change the logging level, prepend `export LOG_LEVEL=<your 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

Expand Down
File renamed without changes.
32 changes: 32 additions & 0 deletions bot/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python

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__)


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
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({})
52 changes: 52 additions & 0 deletions bot/event_handler.py
Original file line number Diff line number Diff line change
@@ -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'])
61 changes: 61 additions & 0 deletions bot/messenger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import logging
import random

logger = logging.getLogger(__name__)


class Messenger(object):
def __init__(self, slack_clients):
self.clients = slack_clients

def send_message(self, channel_id, msg):
# in the case of Group and Private channels, RTM channel payload is a complex dictionary
if isinstance(channel_id, dict):
channel_id = channel_id['id']
logger.debug('Sending msg: {} to channel: {}'.format(msg, channel_id))
channel = self.clients.rtm.server.channels.find(channel_id)
channel.send_message("{}".format(msg.encode('ascii', 'ignore')))

def write_help_message(self, channel_id):
bot_uid = self.clients.bot_user_id()
txt = '{}\n{}\n{}\n{}'.format(
"I'm your friendly Slack bot written in Python. I'll *_respond_* to the following commands:",
"> `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')
80 changes: 80 additions & 0 deletions bot/slack_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import time
import logging
import traceback

from slack_clients import SlackClients
from messenger import Messenger
from event_handler import RtmEventHandler

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.
Args:
token (str): Slack API Bot User token (for development token set in env)
"""
self.last_ping = 0
self.keep_running = True
if token is not None:
self.clients = SlackClients(token)

def start(self, resource):
"""Creates Slack Web and RTM clients for the given Resource
using the provided API tokens and configuration, then connects websocket
and listens for RTM events.
Args:
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']:
res_access_token = resource['resource']['SlackBotAccessToken']
self.clients = SlackClients(res_access_token)

if self.clients.rtm.rtm_connect():
logging.info(u'Connected {} to {} team at https://{}.slack.com'.format(
self.clients.rtm.server.username,
self.clients.rtm.server.login_data['team']['name'],
self.clients.rtm.server.domain))

msg_writer = Messenger(self.clients)
event_handler = RtmEventHandler(self.clients, msg_writer)

while self.keep_running:
for event in self.clients.rtm.rtm_read():
try:
event_handler.handle(event)
except:
err_msg = traceback.format_exc()
logging.error('Unexpected error: {}'.format(err_msg))
msg_writer.write_error(event['channel'], err_msg)
continue

self._auto_ping()
time.sleep(.1)

else:
logger.error('Failed to connect to RTM client with token: {}'.format(self.clients.token))

def _auto_ping(self):
# hard code the interval to 3 seconds
now = int(time.time())
if now > 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 message payloads - https://beepboophq.com/docs/article/resourcer-api
"""
self.keep_running = False
38 changes: 38 additions & 0 deletions bot/slack_clients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

import logging
import re
import time

from slacker import Slacker
from slackclient import SlackClient

logger = logging.getLogger(__name__)


class SlackClients(object):
def __init__(self, token):
self.token = token

# Slacker is a Slack Web API Client
self.web = Slacker(token)

# SlackClient is a Slack Websocket RTM API Client
self.rtm = SlackClient(token)

def bot_user_id(self):
return self.rtm.server.login_data['self']['id']

def is_message_from_me(self, user):
return user == self.rtm.server.login_data['self']['id']

def is_bot_mention(self, message):
bot_user_name = self.rtm.server.login_data['self']['id']
if re.search("@{}".format(bot_user_name), message):
return True
else:
return False

def send_user_typing_pause(self, channel_id, sleep_time=3.0):
user_typing_json = {"type": "typing", "channel": channel_id}
self.rtm.server.send_to_websocket(user_typing_json)
time.sleep(sleep_time)
Loading

0 comments on commit e1bf6ae

Please sign in to comment.