-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Add block action payloads for testing - Add models with validation for block_actions payloads - Add decorator for block actions - Register block actions during plugin registration - Handle block actions events - Add plugin methods to update and delete messages (Thanks to @pawelros) - Document block actions Co-authored-by: Pawel Rosinski <[email protected]>
- Loading branch information
1 parent
9657817
commit ce5a823
Showing
55 changed files
with
5,002 additions
and
440 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
# Block Kit actions | ||
|
||
Slack lets you build interactivity into your Slack app using [**Block Kit**](https://api.slack.com/block-kit). Block | ||
Kit is a UI framework that lets you add interactive elements, such as buttons, input fields, datepickers etc. to | ||
_surfaces_ like messages, modals and the App Home tab. | ||
|
||
Slack Machine makes it easy to listen to _actions_ triggered by these interactive elements. | ||
|
||
## Defining actions | ||
|
||
When you're defining [blocks](https://api.slack.com/reference/block-kit ) for your interactive surfaces, each of | ||
these blocks can be given a `block_id`. Within certain blocks, you can place | ||
[block elements](https://api.slack.com/reference/block-kit/block-elements) that are interactive. These interactive | ||
elements can be given an `action_id`. Given that one block can contain multiple action elements, each `block_id` can | ||
be linked to multiple `action_id`s. | ||
|
||
Whenever the user interacts with these elements, an event is sent to Slack Machine that contains the `block_id` and | ||
`action_id` corresponding to the block and element in which an action happened. | ||
|
||
## Listening to actions | ||
|
||
With the [`action`][machine.plugins.decorators.action] decorator you can define which plugin methods should be | ||
called when a certain action is triggered. The decorator takes 2 arguments: the `block_id` and the `action_id` that | ||
you want to listen to. Both arguments are optional, but **one of them always needs to be set**. Both arguments accept a | ||
[`str`][str] or [`re.Pattern`][re.Pattern]. When a string is provided, the handler only fires upon an exact match, | ||
whereas with a regex pattern, you can have the handler fired for multiple matching `block_id`s or `action_id`s. This | ||
is convenient when you want one handler to process multiple actions within a block, for example. | ||
|
||
If only `action_id` or `block_id` is provided, the other defaults to `None`, which means it **always matches**. | ||
|
||
### Parameters of your action handler | ||
|
||
Your block action handler will be called with a [`BlockAction`][machine.plugins.block_action.BlockAction] object that | ||
contains useful information about the action that was triggered and the message or other surface in which the action | ||
was triggered. | ||
|
||
You can optionally pass the `logger` argument to get a | ||
[logger that was enriched by Slack Machine](misc.md#using-loggers-provided-by-slack-machine-in-your-handler-functions) | ||
|
||
The [`BlockAction`][machine.plugins.block_action.BlockAction] contains various useful fields and properties about | ||
the action that was triggered and the context in which that happened. The | ||
[`user`][machine.plugins.block_action.BlockAction.user] property corresponds to the user that triggered the action | ||
(e.g. clicked a button) and the [`channel`][machine.plugins.block_action.BlockAction.channel] property corresponds | ||
to the channel in which the message was posted where the action was triggered. This property is `None` when the | ||
action happened in a modal or the App Home tab. | ||
The [`triggered_action`][machine.plugins.block_action.BlockAction.triggered_action] field holds information on the | ||
action that triggered the handler, including any value that was the result of the triggered action - such as the | ||
value of the button that was clicked. Lastly, the | ||
[`payload`][machine.plugins.block_action.BlockAction.payload] holds the complete payload the was received by Slack | ||
Machine when the action was triggered. Among other things, it holds the complete _state_ of the interactive blocks | ||
within the message or modal where the action was triggered. This is especially useful when dealing with a _submit_ | ||
button that was triggered, where you want to collect all the information in a form for example. | ||
|
||
### Example | ||
|
||
Let's imagine you're building a plugin for your Slack Machine bot that allows users to vote for what to have for | ||
lunch. You designed the following interaction: | ||
|
||
![block-kit-example](../img/block-kit-example.png) | ||
|
||
Each lunch option has a vote button. Due to the way Block Kit works, to represent each option like this, they should | ||
be in their own [section](https://api.slack.com/reference/block-kit/blocks#section). Each section will have the | ||
description of the lunch option, the emoji and a button to vote. Sections are blocks, so we want to listen for | ||
actions within different blocks. | ||
|
||
This is what the handler could look like: | ||
|
||
```python | ||
@action(action_id=None, block_id=re.compile(r"lunch.*", re.IGNORECASE)) | ||
async def lunch_action(self, action: BlockAction, logger: BoundLogger): | ||
logger.info("Action triggered", triggered_action=action.triggered_action) | ||
food_block = [block for block in action.payload.message.blocks if block.block_id == action.triggered_action.block_id][0] | ||
food_block_section = cast(blocks.SectionBlock, food_block) | ||
food_description = str(food_block_section.text.text) | ||
msg = f"{action.user.fmt_mention()} has voted for '{food_description}'" | ||
await action.say(msg, ephemeral=False) | ||
``` | ||
|
||
As you can see, we only care about the `block_id` here and not about the `action_id`. In the blocks that show the | ||
lunch options, `block_id`s would be set like `lunch_ramen`, `lunch_hamburger` etc. | ||
|
||
## Responding to an action | ||
|
||
As you can see in the example, if you want to send a message to the user after an action was triggered, you can do | ||
so by calling the [`say()`][machine.plugins.block_action.BlockAction.say] method on the _action_ object your handler | ||
received from Slack Machine. | ||
This works just like any other way Slack provides for sending messages. You can include just text, but also rich | ||
content using [_Block Kit_](https://api.slack.com/block-kit) | ||
|
||
!!! info | ||
|
||
The [`response_url`][machine.plugins.block_action.BlockAction.response_url] property is used by the | ||
[`say()`][machine.plugins.block_action.BlockAction.say] method to send messages to a channel after receiving a | ||
command. It does so by invoking a _Webhook_ using this `response_url` This is different from how | ||
[`message.say()`][machine.plugins.message.Message.say] works - which uses the Slack Web API. | ||
|
||
The reason for this is to keep consistency with how Slack recommends interacting with a user. For block actions, | ||
using the `response_url` is the [recommended way](https://api.slack.com/interactivity/handling#message_responses) | ||
|
||
!!! warning | ||
|
||
The `response_url` is only available when the action was triggered in a message - as opposed to in a modal or | ||
the App Home tab. The reason is of course that in the other two cases there is no channel to send the message to. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from .command_handler import create_slash_command_handler # noqa | ||
from .event_handler import create_generic_event_handler # noqa | ||
from .interactive_handler import create_interactive_handler # noqa | ||
from .logging import log_request # noqa | ||
from .message_handler import create_message_handler # noqa |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from __future__ import annotations | ||
|
||
import contextlib | ||
from typing import Any, AsyncGenerator, Awaitable, Callable, Union, cast | ||
|
||
from slack_sdk.models import JsonObject | ||
from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient | ||
from slack_sdk.socket_mode.request import SocketModeRequest | ||
from slack_sdk.socket_mode.response import SocketModeResponse | ||
from structlog.stdlib import get_logger | ||
|
||
from machine.clients.slack import SlackClient | ||
from machine.handlers.logging import create_scoped_logger | ||
from machine.models.core import RegisteredActions | ||
from machine.plugins.command import Command | ||
|
||
logger = get_logger(__name__) | ||
|
||
|
||
def create_slash_command_handler( | ||
plugin_actions: RegisteredActions, | ||
slack_client: SlackClient, | ||
) -> Callable[[AsyncBaseSocketModeClient, SocketModeRequest], Awaitable[None]]: | ||
async def handle_slash_command_request(client: AsyncBaseSocketModeClient, request: SocketModeRequest) -> None: | ||
if request.type == "slash_commands": | ||
logger.debug("slash command received", payload=request.payload) | ||
# We only acknowledge request if we know about this command | ||
if request.payload["command"] in plugin_actions.command: | ||
cmd = plugin_actions.command[request.payload["command"]] | ||
command_obj = _gen_command(request.payload, slack_client) | ||
if "logger" in cmd.function_signature.parameters: | ||
command_logger = create_scoped_logger( | ||
cmd.class_name, | ||
cmd.function.__name__, | ||
user_id=command_obj.sender.id, | ||
user_name=command_obj.sender.name, | ||
) | ||
extra_args = {"logger": command_logger} | ||
else: | ||
extra_args = {} | ||
# Check if the handler is a generator. In this case we have an immediate response we can send back | ||
if cmd.is_generator: | ||
gen_fn = cast(Callable[..., AsyncGenerator[Union[dict, JsonObject, str], None]], cmd.function) | ||
logger.debug("Slash command handler is generator, returning immediate ack") | ||
gen = gen_fn(command_obj, **extra_args) | ||
# return immediate reponse | ||
payload = await gen.__anext__() | ||
ack_response = SocketModeResponse(envelope_id=request.envelope_id, payload=payload) | ||
await client.send_socket_mode_response(ack_response) | ||
# Now run the rest of the function | ||
with contextlib.suppress(StopAsyncIteration): | ||
await gen.__anext__() | ||
else: | ||
ack_response = SocketModeResponse(envelope_id=request.envelope_id) | ||
await client.send_socket_mode_response(ack_response) | ||
fn = cast(Callable[..., Awaitable[None]], cmd.function) | ||
await fn(command_obj, **extra_args) | ||
|
||
return handle_slash_command_request | ||
|
||
|
||
def _gen_command(cmd_payload: dict[str, Any], slack_client: SlackClient) -> Command: | ||
return Command(slack_client, cmd_payload) |
Oops, something went wrong.