X Tutup

Listening to messages

To listen to messages that your app has access to receive, you can use the message() method which filters out events that aren’t of type message.

message() accepts an argument of type str or RegEx object that filters out any messages that don’t match the pattern.

# This will match any message that contains 👋
@app.message(":wave:")
def say_hello(message, say):
    user = message['user']
    say(f"Hi there, <@{user}>!")

Using a RegEx pattern

The re.compile() method can be used instead of a string for more granular matching.

@app.message(re.compile("(hi|hello|hey)"))
def say_hello_regex(say, context):
    # RegEx matches are inside of context.matches
    greeting = context['matches'][0]
    say(f"{greeting}, how are you?")

Sending messages

Within your listener function, say() is available whenever there is an associated conversation (for example, a conversation where the event or action which triggered the listener occurred). say() accepts a string to post simple messages and JSON payloads to send more complex messages. The message payload you pass in will be sent to the associated conversation.

In the case that you’d like to send a message outside of a listener or you want to do something more advanced (like handle specific errors), you can call client.chat_postMessage using the client attached to your Bolt instance.

# Listens for messages containing "knock knock" and responds with an italicized "who's there?"
@app.message("knock knock")
def ask_who(message, say):
    say("_Who's there?_")

Sending a message with blocks

say() accepts more complex message payloads to make it easy to add functionality and structure to your messages.

To explore adding rich message layouts to your app, read through the guide on our API site and look through templates of common app flows in the Block Kit Builder.

# Sends a section block with datepicker when someone reacts with a 📅 emoji
@app.event("reaction_added")
def show_datepicker(event, say):
  reaction = event["reaction"]
  if reaction == "calendar":
      blocks = [{
          "type": "section",
          "text": {
              "type": "mrkdwn",
              "text": "Pick a date for me to remind you"
          },
          "accessory": {
              "type": "datepicker",
              "action_id": "datepicker_remind",
              "initial_date": "2020-05-04",
              "placeholder": {
                  "type": "plain_text",
                  "text": "Select a date"
              }
          }
      }]

      say(blocks=blocks)

Listening to events

You can listen to any Events API event using the event() method after subscribing to it in your app configuration. This allows your app to take action when something happens in a workspace where it’s installed, like a user reacting to a message or joining a channel.

The event() method requires an eventType of type str.

# When a user joins the team, send a message in a predefined channel asking them to introduce themselves
@app.event("team_join")
def ask_for_introduction(event, say):
    welcome_channel_id = "C12345";
    user_id = event["user"]["id"]
    text = f"Welcome to the team, <@{user_id}>! 🎉 You can introduce yourself in this channel."
    say(text=text, channel=welcome_channel_id)

Filtering on message subtypes

The message() listener is equivalent to event("message").

You can filter on subtypes of events by passing in the additional key subtype. Common message subtypes like bot_message and message_replied can be found on the message event page.

# Matches all messages from bot users
@app.message({"subtype": "message_changed"})
def log_message_change(logger, message):
    logger.info(f"The user {message['user']} changed the message to {message['text']}")

Using the Web API

You can call any Web API method using the WebClient provided to your Bolt app as app.client (given that your app has the appropriate scopes). When you call one the client’s methods, it returns a SlackResponse which contains the response from Slack.

The token used to initialize Bolt can be found in the context object, which is required to call most Web API methods.

@app.message("wake me up")
def say_hello(client, message):
    # Unix Epoch time for September 30, 2020 11:59:59 PM
    when_september_ends = 1601510399
    channel_id = message["channel"]
    client.chat_scheduleMessage(channel=channel_id,
                                post_at=when_september_ends,
                                text="Summer has come and passed")

Listening to actions

Your app can listen to user actions, like button clicks, and menu selects, using the action method.

Actions can be filtered on an action_id of type str or re.Pattern. action_ids act as unique identifiers for interactive components on the Slack platform.

You’ll notice in all action() examples, ack() is used. It is required to call the ack() function within an action listener to acknowledge that the event was received from Slack. This is discussed in the acknowledging events section.

# Your middleware will be called every time an interactive component with the action_id "approve_button" is triggered
@app.action("approve_button")
def update_message(ack):
    ack()
    # Update the message to reflect the action

Listening to actions using a constraint object

You can use a constraints object to listen to callback_ids, block_ids, and action_ids (or any combination of them). Constraints in the object can be of type str or re.Pattern.

# Your function will only be called when the action_id matches 'select_user' AND the block_id matches 'assign_ticket'
@app.action({"action_id": "select_user", "block_id": "assign_ticket"})
def update_message(ack, action, body, client):
    ack()
    client.reactions_add(name='white_check_mark',
                         timestamp=action['action_ts'],
                         channel=body['channel']['id'])

Responding to actions

There are two main ways to respond to actions. The first (and most common) way is to use say(), which sends a message back to the conversation where the incoming event took place.

The second way to respond to actions is using respond(), which is a utility to use the response_url associated with the action.

# Your middleware will be called every time an interactive component with the action_id “approve_button” is triggered
@app.action("approve_button")
def approve_request(ack, say):
    # Acknowledge action request
    ack();
    say("Request approved 👍");

Using respond()

Since respond() is a utility for calling the response_url, it behaves in the same way. You can pass a JSON object with a new message payload that will be published back to the source of the original interaction with optional properties like response_type (which has a value of in_channel or ephemeral), replace_original, and delete_original.

# Listens to actions triggered with action_id of “user_select”
@app.action("user_select")
def select_user(ack, action, respond):
    ack();
    respond(f"You selected <@{action['selected_user']}>")

Acknowledging events

Actions, commands, and options events must always be acknowledged using the ack() function. This lets Slack know that the event was received and updates the Slack user interface accordingly.

Depending on the type of event, your acknowledgement may be different. For example, when acknowledging a menu selection associated with an external data source, you would call ack() with a list of relevant options.

We recommend calling ack() right away before sending a new message or fetching information from your database since you only have 3 seconds to respond.

# Example of responding to an external_select options request
@app.options("menu_selection")
def show_menu_options(ack):
    options = [
        {
            "text": {"type": "plain_text", "text": "Option 1"},
            "value": "1-1",
        },
        {
            "text": {"type": "plain_text", "text": "Option 2"},
            "value": "1-2",
        },
    ]
    ack(options=options)

Listening and responding to shortcuts

The shortcut() method supports both global shortcuts and message shortcuts.

Shortcuts are invokable entry points to apps. Global shortcuts are available from within search in Slack. Message shortcuts are available in the context menus of messages. Your app can use the shortcut() method to listen to incoming shortcut events. The method requires a callback_id parameter of type str or re.Pattern.

Shortcuts must be acknowledged with ack() to inform Slack that your app has received the event.

Shortcuts include a trigger_id which an app can use to open a modal that confirms the action the user is taking.

When setting up shortcuts within your app configuration, as with other URLs, you’ll append /slack/events to your request URL.

⚠️ Note that global shortcuts do not include a channel ID. If your app needs access to a channel ID, you may use a conversations_select element within a modal. Message shortcuts do include a channel ID.


# The open_modal shortcut opens a plain old modal
@app.shortcut("open_modal")
def open_modal(ack, shortcut, client):
    # Acknowledge the shortcut request
    ack()
    # Call the views_open method using one of the built-in WebClients
    client.views_open(
        trigger_id=shortcut["trigger_id"],
        view={
            "type": "modal",
            "title": {
                "type": "plain_text",
                "text": "My App"
            },
            "close": {
                "type": "plain_text",
                "text": "Close"
            },
            "blocks": [
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": "About the simplest modal you could conceive of :smile:\n\nMaybe <https://api.slack.com/reference/block-kit/interactive-components|*make the modal interactive*> or <https://api.slack.com/surfaces/modals/using#modifying|*learn more advanced modal use cases*>."
                    }
                },
                {
                    "type": "context",
                    "elements": [
                        {
                            "type": "mrkdwn",
                            "text": "Psssst this modal was designed using <https://api.slack.com/tools/block-kit-builder|*Block Kit Builder*>"
                        }
                    ]
                }
            ]
        }
    )

Listening to shortcuts using a constraint object

You can use a constraints object to listen to callback_ids, and types. Constraints in the object can be of type str or re.Pattern.


# Your middleware will only be called when the callback_id matches 'open_modal' AND the type matches 'message_action'
@app.message_shortcut("open_modal")
def open_modal(ack, shortcut, client):
    # Acknowledge the shortcut request
    ack()
    # Call the views_open method using one of the built-in WebClients
    client.views_open(
        trigger_id=shortcut["trigger_id"],
        view={
            "type": "modal",
            "title": {
                "type": "plain_text",
                "text": "My App"
            },
            "close": {
                "type": "plain_text",
                "text": "Close"
            },
            "blocks": [
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": "About the simplest modal you could conceive of :smile:\n\nMaybe <https://api.slack.com/reference/block-kit/interactive-components|*make the modal interactive*> or <https://api.slack.com/surfaces/modals/using#modifying|*learn more advanced modal use cases*>."
                    }
                },
                {
                    "type": "context",
                    "elements": [
                        {
                            "type": "mrkdwn",
                            "text": "Psssst this modal was designed using <https://api.slack.com/tools/block-kit-builder|*Block Kit Builder*>"
                        }
                    ]
                }
            ]
        }
    )

Listening and responding to commands

Your app can use the command() method to listen to incoming slash command events. The method requires a command_name of type str.

Commands must be acknowledged with ack() to inform Slack your app has received the event.

There are two ways to respond to slash commands. The first way is to use say(), which accepts a string or JSON payload. The second is respond() which is a utility for the response_url. These are explained in more depth in the responding to actions section.

When setting up commands within your app configuration, you’ll append /slack/events to your request URL.

# The echo command simply echoes on command
@app.command("/echo")
def repeat_text(ack, say, command):
    # Acknowledge command request
    ack()
    say(f"{command['text']}")

Opening modals

Modals are focused surfaces that allow you to collect user data and display dynamic information. You can open a modal by passing a valid trigger_id and a view payload to the built-in client’s views.open method.

Your app receives trigger_ids in payloads sent to your Request URL that are triggered by user invocations, like a shortcut, button press, or interaction with a select menu.

Read more about modal composition in the API documentation.

# Listen for a shortcut invocation
@app.shortcut("open_modal")
def open_modal(ack, body, client):
    # Acknowledge the command request
    ack();

    # Call views_open with the built-in client
    client.views_open(
        # Pass a valid trigger_id within 3 seconds of receiving it
        trigger_id=body["trigger_id"],
        # View payload
        view={
            "type": "modal",
            # View identifier
            "callback_id": "view_1",
            "title": {
                "type": "plain_text",
                "text": "Modal title"
            },
            "blocks": [
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": "Welcome to a modal with _blocks_"
                    },
                    "accessory": {
                        "type": "button",
                        "text": {
                            "type": "plain_text",
                            "text": "Click me!"
                        },
                        "action_id": "button_abc"
                    }
                },
                {
                    "type": "input",
                    "block_id": "input_c",
                    "label": {
                        "type": "plain_text",
                        "text": "What are your hopes and dreams?"
                    },
                    "element": {
                        "type": "plain_text_input",
                        "action_id": "dreamy_input",
                        "multiline": True
                    }
                }
            ],
            "submit": {
                "type": "plain_text",
                "text": "Submit"
            }
        }
    )

Updating and pushing views

Modals contain a stack of views. When you call views_open, you add the root view to the modal. After the initial call, you can dynamically update a view by calling views_update, or stack a new view on top of the root view by calling views_push.

views_update
To update a view, you can use the built-in client to call views_update with the view_id that was generated when you opened the view, and a new view including the updated blocks list. If you’re updating the view when a user interacts with an element inside of an existing view, the view_id will be available in the body of the request.

views_push
To push a new view onto the view stack, you can use the built-in client to call views_push with a valid trigger_id a new view payload. The arguments for views_push is the same as opening modals. After you open a modal, you may only push two additional views onto the view stack.

Learn more about updating and pushing views in our API documentation.

# Listen for a button invocation with action_id `button_abc` (assume it's inside of a modal)
@app.action("button_abc")
def update_modal(ack, view, client):
    # Acknowledge the button request
    ack()
    client.views_update(
        # Pass the view_id
        view_id=view["id"],
        # String that represents view state to protect against race conditions
        hash=view["hash"],
        # View payload with updated blocks
        view={
            "type": "modal",
            # View identifier
            "callback_id": "view_1",
            "title": {
                "type": "plain_text",
                "text": "Updated modal"
            },
            "blocks": [
                {
                    "type": "section",
                    "text": {
                        "type": "plain_text",
                        "text": "You updated the modal!"
                    }
                },
                {
                    "type": "image",
                    "image_url": "https://media.giphy.com/media/SVZGEcYt7brkFUyU90/giphy.gif",
                    "alt_text": "Yay! The modal was updated"
                }
            ]
        }
    )

Listening for view submissions

If a view payload contains any input blocks, you must listen to view_submission events to receive their values. To listen to view_submission events, you can use the built-in view() method. view() requires a callback_id of type str or re.Pattern.

You can access the value of the input blocks by accessing the state object. state contains a values object that uses the block_id and unique action_id to store the input values.

Read more about view submissions in our API documentation.

# Handle a view_submission event
​​@app.view("view_b")
​​def handle_submission(ack, body, client, view):
​​    # Acknowledge the view_submission event
​​    ack()
​​
​​    # Do whatever you want with the input data - here we're saving it to a DB
    # then sending the user a verifcation of their submission
​​
​​    # Assume there's an input block with `block_1` as the block_id and `input_a`
​​    val = view["state"]["values"]["block_1"]["input_a"]
​​    user = body["user"]["id"]
​​
​​    # Message to send user
​​    msg = ""
​​
​​    try:
​​      # Save to DB
​​      msg = f"Your submission of {val} was successful"
​​    except Exception as e:
​​      # Handle error
​​      msg = "There was an error with your submission"
​​    finally:
​​      # Message the user
​​      client.chat_postMessage(channel=user, text=msg)

Listening and responding to options

The options() method listens for incoming option request payloads from Slack. Similar to action(), an action_id or constraints object is required. In order to load external data into your select menus, you must provide an options load URL in your app configuration, appended with /slack/events.

While it’s recommended to use action_id for external_select menus, dialogs do not support Block Kit so you’ll have to use the constraints object to filter on a callback_id.

To respond to options requests, you’ll need to call ack() with a valid options or option_groups list. Both external select response examples and dialog response examples can be found on our API site.

# Example of responding to an external_select options request
@app.options("external_action")
def show_options(ack):
    options = [
        {
            "text": {"type": "plain_text", "text": "Option 1"},
            "value": "1-1",
        },
        {
            "text": {"type": "plain_text", "text": "Option 2"},
            "value": "1-2",
        },
    ]
    ack(options=options)

Authenticating with OAuth

Slack apps installed on multiple workspaces will need to implement OAuth, then store installation information (like access tokens) securely. By providing client_id, client_secret, scopes, installation_store, and state_store when initializing App, Bolt for Python will handle the work of setting up OAuth routes and verifying state. If you’re implementing a custom receiver, you can make use of our OAuth library, which is what Bolt for Python uses under the hood.

Bolt for Python will create a Redirect URL slack/oauth_redirect, which Slack uses to redirect users after they complete your app’s installation flow. You will need to add this Redirect URL in your app configuration settings under OAuth and Permissions. This path can be configured in the OAuthSettings argument described below.

Bolt for Python will also create a slack/install route, where you can find an Add to Slack button for your app to perform direct installs of your app. If you need any additional authorizations (user tokens) from users inside a team when your app is already installed or a reason to dynamically generate an install URL, you can pass your own custom URL generator to oauth_settings as authorize_url_generator.

To learn more about the OAuth installation flow with Slack, read the API documentation.

from slack_bolt.oauth.oauth_settings import OAuthSettings
from slack_sdk.oauth.installation_store import FileInstallationStore
from slack_sdk.oauth.state_store import FileOAuthStateStore

oauth_settings = OAuthSettings(
    client_id=os.environ["SLACK_CLIENT_ID"],
    client_secret=os.environ["SLACK_CLIENT_SECRET"],
    scopes=["channels:read", "groups:read", "chat:write"],
    installation_store=FileInstallationStore(),
    state_store=FileOAuthStateStore(expiration_seconds=120)
)

app = App(signing_secret=os.environ["SIGNING_SECRET"],
          oauth_settings=oauth_settings)

Customizing OAuth defaults

You can override the default OAuth using oauth_settings, which can be passed in during the initialization of App. You can override the following:

  • install_path: Override default path for “Add to Slack” button
  • redirect_uri: Override default redirect url path
  • callback_options: Provide custom success and failure pages at the end of the OAuth flow
  • state_store: Provide a custom state store instead of using the built in FileOAuthStateStore
  • installation_store: Provide a custom installation store instead of the built-in FileInstallationStore
app = App(
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
    installation_store=FileInstallationStore(),
    oauth_settings=OAuthSettings(
        client_id=os.environ.get("SLACK_CLIENT_ID"),
        client_secret=os.environ.get("SLACK_CLIENT_SECRET"),
        scopes=["app_mentions:read", "channels:history", "im:history", "chat:write"],
        user_scopes=[],
        redirect_uri=None,
        install_path="/slack/install",
        redirect_uri_path="/slack/oauth_redirect",
        state_store=FileOAuthStateStore(expiration_seconds=600),
        callback_options=CallbackOptions(success=success,
                                         failure=failure),
    ),
)

Example

An advanced section example.

# We love Python <3

X Tutup