Sending questions

Let’s write our first part of 1 of ours AWS Lambda functions. Now we are going to work with standup_bot/scheduled.py file which represents AWS Lambda function shecudeld-events from schema

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Standalone lambda function triggered by CWE.
"""
import datetime as dt
import json
import logging

from slack import WebClient

from standup_bot.config import (
    SLACK_TOKEN,
    SLACK_CHANNEL,
    QUESTIONS,
)
from standup_bot.models import Report
from standup_bot.msg_templates import standup_menu_block, report_block

LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)

# synchronous slack client
SC = WebClient(SLACK_TOKEN)

In highlighted lines above, we:

  • import the functionality needed later,
  • initialize logging
  • initialize Slack client - note that we are using synchronous client in order to keep the code beginner friendly. However you it is possible to make the function async using asyncio and asynchronous slack client.

Let’s jump to the very bottom of the file and look at our lambda_handler.

We will pay more attention to 28-83 bit later.
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def lambda_handler(lambda_event, lambda_context):
    """Main lambda handler"""
    LOGGER.debug(lambda_context)
    LOGGER.info(lambda_event)
    LOGGER.info("lambda_event starts:")
    LOGGER.info(json.dumps(lambda_event))

    # is it our CWE event?
    if {"type", "time", "source"}.issubset(lambda_event):

        report_id = dt.datetime.strptime(lambda_event["time"], '%Y-%m-%dT%H:%M:%S%z').strftime("%Y%m%d")
        LOGGER.info("Report ID: %s", report_id)
        if lambda_event["type"] == "send_report":
            send_report(report_id)
        elif lambda_event["type"] == "send_questions":
            send_questions(report_id)

Event source (trigger) of our lambda function will be modified input from a cloudwatch event. We are going to deal with two event types send_questions and send_report.

Code breakdown:

  • L90-93 - Logging of input, so we can investigate what is going on via CloudWatch Logs.
  • L98 - generate report_id in format YYYYMMDD
  • Then trigger entry point function based on event type

We are going to start with Sending a daily menu to the members of our SLACK_CHANNEL. Implement functions: send_menu, send_menus and send_questions.

To send a menu to the user via private message, we need to first open the conversation, then we can send a message. Main body of our message will be a menu_block, which consists of 2 buttons. Where 1 button opens a dialog with questions and second button allows user to skip todays report.

alternate text
28
29
30
31
32
33
34
35
def send_menu(user_id, menu_block):
    """Send menu as private message to the user."""

    response = SC.conversations_open(users=[user_id])
    post_response = SC.chat_postMessage(
        channel=response["channel"]["id"], text="Daily menu", blocks=menu_block
    ),
    return user_id, post_response

On line 33 we are using pre-built layout block from standup_bot/msg_templates.

In function send_menus we ask slack to get a list of channel members and send_menu to each member.

38
39
40
41
42
def send_menus(menu_block):
    """Send menu to all users from the channel."""
    members = SC.conversations_members(channel=SLACK_CHANNEL)
    for user_id in members['members']:
        yield send_menu(user_id, menu_block)

As a last step we define our entry-point function send_questions. Where we generate menu_block part of the slack message and gather the delivery status responses.

45
46
47
48
49
def send_questions(report_id):
    """Entry point for daily menu."""
    menu_block = standup_menu_block(report_id)
    results = list(send_menus(menu_block))
    LOGGER.info(results)

Our menu_block is a function which generates message blocks

msg_templates.standup_menu_block
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def standup_menu_block(report_id):
    """
    Message block with the menu sent on daily basis.

    Contains Open Dialog and Skip buttons.

    """
    return [
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "Hello, it is time report on daily standup.",
            },
        },
        {
            "type": "actions",
            "elements": [
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "emoji": True,
                        "text": "Open Report",
                    },
                    "style": "primary",
                    "value": report_id,
                    "action_id": "standup.action.open_dialog",
                },
                {
                    "type": "button",
                    "text": {"type": "plain_text", "emoji": True, "text": "Skip today"},
                    "action_id": "standup.action.skip_today",
                },
            ],
        },
    ]

Above we have generated 2 blocks with types section and actions. We have given a specific action_id to each element in order to recognize which button was clicked by user. Processing of actions is further explained in the next section.

We can now test this part of our code and invoke our function locally with command: sls invoke local -f scheduled-events --path example-data/cwe-questions.json.

Make sure you are running this command from within same directory where serverless.yml is located. (sls_app)

If the invocation was successful, you should receive a private message from your application which looks similar to what you can see in a picture below.

menu image