Flask app - processing Slack requests ===================================== Now that we can send the menu to the user, we are going to learn how to store the data. Slack communicates with our app via `interactive actions`_ (actions, dialogs, message buttons, or message menus) via HTTP POST to URL we set in slack application settings. But first let's write some code. Flask app --------- We are going to write our code inside ``standup_bot/action_app.py`` It will be a simple application POST processing requests via API Gateway triggering ``slack-responses`` Lambda function. .. literalinclude:: ../../../sls_app/serverless.yml :caption: serverless.yml :language: yaml :linenos: :lineno-match: :start-at: functions: :end-at: method: post To serve the flask app from within AWS Lambda we are going to use ``serverless-wsgi`` plugin. (Plugin is able to install itself, but you can do it as well: ``sls plugin install -n serverless-wsgi``) .. literalinclude:: ../../../sls_app/serverless.yml :caption: serverless.yml :language: yaml :linenos: :lineno-match: :start-at: plugins: :end-at: serverless-wsgi :emphasize-lines: 4 Our application will deal with multiple structures which you can look at inside the folder ``example-data`` 1. Requests from slack are sent to API Gateway, which triggers our function and passes ``event`` similar to what you can see inside ``example-data/apigw-block_action.json`` 2. Then ``serverless-wsgi`` and Flask transforms this event into `Flask Request`_ 3. We parse the request body and determine the request type: ``block_actions`` or ``dialog_submission`` Let's break down the ``standup_bot/action_app.py`` file. .. literalinclude:: ../../../sls_app/standup_bot/action_app.py :caption: action_app.py :language: python :linenos: :lineno-match: :emphasize-lines: 6-11 :end-at: from standup_bot.msg_templates import dialog_questions * We are going to need ``urllib.parse`` to help us with url decoding. * Then we import Flask items we are going to need * And Slack web client from `python-slackclient`_ * Next set of imports comes from our cookiecutter template * ``QUESTIONS`` - is a dict of questions we are going to ask. ``{ "question1" : "Question text?" }`` * ``SLACK_TOKEN`` - so we can respond back to the user * ``dialog_questions`` - is a `Slack dialog`_ containing ``QUESTIONS`` We simply iteratively build ``elements`` and so our result dialog looks similar to this .. literalinclude:: ../../../sls_app/example-data/sample_dialog.json :caption: example-data/sample_dialog.json :language: json :linenos: :lineno-match: :emphasize-lines: 5,6 * ``Report`` - is our `DynamoDB`_ database model created with `pynamodb package`_. It is possible to access `DynamoDB` directly via boto3_ however pynamodb friendlier API. .. literalinclude:: ../../../sls_app/standup_bot/models.py :caption: models.py :language: python :linenos: :lineno-match: :emphasize-lines: 9 In our next step, we initialize our SlackClient (``SC``) and Flask app. When a Slack user use an interactive (click button, submit dialog) we configure our Slack application to send a HTTP POST request to our AWS API Gateway URL ``.execute-api..amazonaws.com/actions``. And define a route endpoint ``/actions``. .. literalinclude:: ../../../sls_app/standup_bot/action_app.py :caption: action_app.py :language: python :linenos: :lineno-match: :emphasize-lines: 6,7 :start-at: # synchronous slack client :end-before: prefix = "payload=" Inside function ``actions`` we are going to perform following operations: #. Parse and unquote request body received from AWS API Gateway. * Received request is a URL encoded string which contains a prefix ``payload``. See line 119 inside a file ``example-data/apigw-block_action.json``. ``"body":"payload=%7B%22type%22%3A%22block_actions%22%2C%22team%...`` * To find out how does the parsed action body looks like check file: ``sls_app/example-data/block_action.json`` #. Determine Slack action type ``block_actions`` or ``dialog_submission`` #. Process the action #. Respond back to Slack (user) .. literalinclude:: ../../../sls_app/standup_bot/action_app.py :caption: action_app.py :language: python :linenos: :lineno-match: :pyobject: actions Now we need to write appropriate functions to process our action types. Our first function will be ``process_block_actions`` which triggered when user clicks on a button from the menu. Button triggers a `Slack block action`_. .. image:: ../_files/menu.png :alt: menu image :scale: 50% We will need to create simple logic inside our function ``process_block_actions``, to process each action type correctly. So far we will deal with 2 types: * ``standup.action.open_dialog`` - When clicked on Open Dialog button * ``standup.action.skip_today`` - The idea is to signal that user decided to skip today's meeting. Implementation of this is left for you as a challenge. Following example is a JSON (``dict``) data structure we get after successful parsing of ``slack_request`` in previous method ``actions()``. Message itself contains a lot of data, however we will focus on highlighted parts. * L15 ``container`` - contains data we are going to need to uniquely identify the Slack dialog we create later * L21 ``trigger_id`` - is required to trigger the correct dialog * L26 ``message`` - contains original message with the menu * L72 ``actions`` - contains result value of user's action (click Open Dialog button) .. literalinclude:: ../../../sls_app/example-data/block_action.json :caption: example-data/block_action.json :language: json :linenos: :lineno-match: :emphasize-lines: 15, 21, 26, 72 With the information above we can proceed with implementation of ``process_block_actions``. The logic is following: #. Take the 1st action value from ``actions`` array #. Create a ``state_data`` to match the dialog with a user. #. Determine the action type #. Fill dialog object with ``questions`` and other data #. Send a request back to Slack to open a dialog #. And respond 200 and empty body if successful .. literalinclude:: ../../../sls_app/standup_bot/action_app.py :language: python :linenos: :lineno-match: :pyobject: process_block_actions If impatient and would like to try out your partially implemented app, go ahead to :ref:`second slack setup and deployment ` then come back! Okay, we have successfully showed dialog to the user, now it's time to collect the data. When user submits the dialog Slack sends a HTTP POST request to our endpoint `/actions`. But this time the ``slack_req_type`` type is ``dialog_submission``. Dialog submission message contains data about the user and values for the answers. Note that questions are only represented with IDs ``question1``, ... The important part is a ``callback_id`` field which helps us to identify dialog type. We then use the data previously stored in ``state`` to identify the user. .. literalinclude:: ../../../sls_app/example-data/dialog_submission.json :caption: example-data/block_action.json :language: json :linenos: :lineno-match: Slack does not have implemented automated updates(answers) to actions. This means when user submits the dialog, the menu stays unchanged. .. image:: ../_files/menu.png :alt: menu image :scale: 50% But we would like to inform the user about successful submission therefore we will do the trick and update the previous message using the timestamp to following: .. image:: ../_files/menu_response.png :alt: menu response image :scale: 50% For this purpose we are going to implement a function ``process_dialogs`` as follows: #. Examine ``callback_id`` to identify dialog type #. Parse the ``state_data`` from string to ``dict`` #. Get detailed information about the user from Slack #. Create a ``report`` object represented by our database model ``Report`` * To uniquely identify the report for simple a simple query, our main identifier will be ``report_id``. It's a simple execution timestamp, which comes from a scheduled event, converted into ``YYYYMMDD`` string with suffix of a slack user ID ``UFE4ZTC8J`` (assuming our meeting is once a day). Example .. code-block:: text time: 2016-12-30T18:44:49Z user_id: UFE4ZTC8J becomes: .. code-block:: text 20161230_UFE4ZTC8J This way we can later query the database for a given day to get all reports. #. Save data in the database #. Inform user via updating a chat message. #. Send empty response 200 .. literalinclude:: ../../../sls_app/standup_bot/action_app.py :caption: action_app.py :language: python :linenos: :lineno-match: :pyobject: process_dialogs This concludes 2/3 of our application, you can now try to deploy and play around. To deploy check out :ref:`the second slack setup and deployment ` page! If you are not deploying or you have finished the investigation, please proceed to the last part :ref:`sending the report from all users ` .. _interactive actions: https://api.slack.com/interactive-messages .. _Flask Request: http://flask.pocoo.org/docs/1.0/api/?highlight=request#flask.request .. _python-slackclient: https://github.com/slackapi/python-slackclient .. _Slack dialog: https://api.slack.com/dialogs .. _Slack block action: https://api.slack.com/reference/messaging/blocks#actions .. _DynamoDB: https://docs.aws.amazon.com/dynamodb/index.html .. _pynamodb package: https://github.com/pynamodb/PynamoDB .. _boto3: https://boto3.amazonaws.com/v1/documentation/api/latest/index.html?id=docs_gateway