Search code examples
pythonpython-2.7flaskslack-apislack

How to pass data received via HTTP POST from a Python Flask script to a separate Python script for processing?


Alright guys, so here's my problem.

I am in the process of developing a Slack app with a packaged bot that allows the user to play a game in Slack. I have successfully built the bot and packaged it with the app as per the API guidelines. Once I discovered the Interactive Messages feature, I decided to implement said feature for a more user-friendly take on the game.

The interactive messages feature allows you to post messages with buttons, which the user can click to invoke an action. My bot script, let's call it bot.py, prompts the user (using the Slack chat.postMessage function) with a message containing some buttons from which to choose. This script has one class (I know it should be more modular but all in good time), which opens a web-socket for communication via the Slack RTM API. As such, when the script runs, it is always "listening" for a command from a user in a channel directed as follows: @botname command. The portion of the script that invokes this "always listening" state looks like this:

#bot.py
...
if slack_client.rtm_connect():
        print("MYBOT v1.0 connected and running!")
        while True:
            command, channel, user = self.parse_slack_output(slack_client.rtm_read())
            if command and channel:
                if channel not in self.channel_ids_to_name.keys():
                    #this (most likely) means that this channel is a PM with the bot
                    self.handle_private_message(command, user)
                else:
                    self.handle_command(command, channel, user)
            time.sleep(READ_WEBSOCKET_DELAY)
    else:
        print("Connection failed. Invalid Slack token or bot ID?")

That's all good and fine. Now, let's say the user has used a command to successfully create a game instance and has started playing. At a certain point, the user is prompted for a trump suit like so:

#bot.py
...
attachments =[{
"title":"Please select index for trump suit:",
"fallback":"Your interface does not support interactive messages.",
"callback_id":"prompt_trump_suit", 
"attachment_type":"default", 
"actions":
        [{"name":"diamonds","text":":diamonds:","type":"button","value":"0"},
        {"name":"clubs","text":":clubs:","type":"button","value":"1"},
        {"name":"hearts","text":":hearts:","type":"button","value":"2"},
        {"name":"spades","text":":spades:","type":"button","value":"3"}]
}]
slack.chat.post_message(
        channel=player_id,
        as_user=True,
        attachments=attachments
        )

The interactive message looks like this. The action of clicking on one of the buttons in this message sends a payload via an HTTP POST to a web server. My other script in the project, which we will call app.py, is a Flask script which successfully receives this POST request when the user clicks one of the buttons. The portion of the script that receives the POST request looks like this:

#app.py
...
# handles interactive button responses for mybot
@app.route('/actions', methods=['POST'])
def inbound():
    payload = request.form.get('payload')
    data = json.loads(payload)
    token = data['token']
    if token == SLACK_VERIFICATION_TOKEN:
        print 'TOKEN is good!'
        response_url = data['response_url']
        channel_info = data['channel']
        channel_id = channel_info['id']
        user_info = data['user']
        user_id = user_info['id']
        user_name = user_info['name']
        actions = data['actions'][0]
        value = actions['value']
        print 'User sending message: ',user_name
        print "Value received: ",value
    return Response(), 200

When the button is clicked, I get the expected output:

TOKEN is good!
User sending message:  my_username
Value received:  3

So everything is successful up to this point. Now, what I want to do is to take that POST information and use it to invoke a function in my bot.py script that handles the trump suit selection. The problem is that if I were to invoke that function, let's call it handle_trump_suit_selection(), I would first have to instantiate a Bot() object in the app.py file, which of course would not work as desired because the function would be called with a new Bot() instance, and therefore would not be in the same state as the current game.

So how the heck can I get the POST information back to the desired Bot() instance in bot.py for further processing? I'm new to OOP in Python and especially new to Flask and the Slack API, so go easy on me ;).

Thanks in advance.


Solution

  • Great Success!

    tl;dr: Basically, the solution was to create a Celery task that instantiated the bot instance from the Flask app using the Slack Events API. You set the task to start after the desired input has been entered, promptly return the required Response(200) back to Slack, while in the meantime the bot script (which starts up the RTM API web-socket) launches in parallel.

    The nitty gritty: So, as stated above, it turns out that what was required was a queuing service of some sort. I ended up going with Celery for its relative ease at integrating with Heroku (where I host the Slack app) and its easy-to-follow documentation.

    Developing your Slack app this way requires setting up and using the Slack Events API to receive the command ("play my_game" in this example) from the Slack channel the message was posted in. The Flask app (app.py) portion of the program listens for this event, and when the input matches up with what you're looking for, it launches the Celery task in parallel (in tasks.py, which instantiates a Bot() instance of bot.py in this example). :) Now the bot can listen and respond using both the Slack RTM API and the Slack Events API. This allows you to build rich applications/services within the Slack framework.

    If you are looking to set up something similar, below are my project layout and the important code details. Feel free to use them as a template.

    Project Layout:

    • project_name_folder
      • app_folder
        • static_folder
        • templates_folder
        • __init__.py
        • my_app.py
        • bot.py
        • tasks.py
      • Procfile
      • requirements.txt

    __init__.py:

    from celery import Celery
    app = Celery('tasks')
    import os
    app.conf.update(BROKER_URL=os.environ['RABBITMQ_BIGWIG_URL']) # Heroku Celery broker
    

    my_app.py:

    from flask import Flask, request, Response, render_template
    import app
    from app import tasks
    
    app = Flask(__name__)
    
    @app.route('/events', methods=['POST'])
    def events():
    """
    Handles the inbound event of a post to the main Slack channel
    """
      data = json.loads(request.data)
      try:
        for k, v in data['event'].iteritems():
          ts = data['event']['ts']
          channel = data['event']['channel']
          user_id = data['event']['user']
          team_id = data['team_id']
    
          if 'play my_game' in str(v):
            tasks.launch_bot.delay(user_id, channel, ts, team_id) # launch the bot in parallel
            return Response(), 200
      except Exception as e:
        raise
    

    bot.py:

    from slackclient import SlackClient
    class Bot():
      def main():
        # opening the Slack web-socket connection
        READ_WEBSOCKET_DELAY = 1  # 1 second delay between reading from firehose
        if self.slack_client.rtm_connect():
          while True:
            command, channel, user, ts = self.parse_slack_output()
              if command and channel:
                if channel not in self.channel_ids_to_name.keys():
                  # this (most likely) means that this channel is a PM with the bot
                  self.handle_private_message(command, user, ts)
                else:
                  self.handle_command(command, channel, user, ts)
              time.sleep(READ_WEBSOCKET_DELAY)
    

    tasks.py:

    import bot
    from bot import Bot
    from app import app
    
    @app.task
    def launch_bot(user_id, channel, ts, team_id):
    '''
    Instantiates the necessary objects to play a game
    
    Args:
            [user_id] (str) The id of the user from which the command was sent
            [channel] (str) The channel the command was posted in
            [ts] (str) The timestamp of the command
    
    '''
      print "launch_bot(user_id,channel)"
      app.control.purge()
      bot = Bot()
      bot.initialize(user_id, channel)
      bot.main()
    

    Procfile (if using Heroku):

    web: gunicorn --pythonpath app my_app:app
    worker: celery -A app.tasks worker -B --loglevel=DEBUG
    

    Please let me know if you are having any issues. This took me a little while to figure out, and I would be happy to help you if you are banging your head on this one.