diff --git a/README.md b/README.md index 5c036739..8d063eb7 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ class DeploymentPlugin(MachineBasePlugin): **Dropped support for Python 3.8** (v0.38.0) As of [v0.38.0](https://github.com/DonDebonair/slack-machine/releases/tag/v0.38.0), support for Python 3.8 has been -dropped. Python 3.7 has reached end-of-life on 2024-10-07. +dropped. Python 3.8 has reached end-of-life on 2024-10-07. ## Features @@ -59,6 +59,7 @@ dropped. Python 3.7 has reached end-of-life on 2024-10-07. - Support for [blocks](https://api.slack.com/reference/block-kit/blocks) - Support for [message attachments](https://api.slack.com/docs/message-attachments) [Legacy 🏚] - Support for [interactive elements](https://api.slack.com/block-kit) +- Support for [modals](https://api.slack.com/surfaces/modals) - Listen and respond to any [Slack event](https://api.slack.com/events) supported by the Events API - Store and retrieve any kind of data in persistent storage (currently Redis, DynamoDB, SQLite and in-memory storage are supported) @@ -68,7 +69,6 @@ dropped. Python 3.7 has reached end-of-life on 2024-10-07. ### Coming Soon -- Support for modals - Support for shortcuts - ... and much more diff --git a/docs/api.md b/docs/api.md index 286fe646..3fa989ed 100644 --- a/docs/api.md +++ b/docs/api.md @@ -18,6 +18,10 @@ The following classes form the basis for Plugin development. ### ::: machine.plugins.block_action.BlockAction +### ::: machine.plugins.modals.ModalSubmission + +### ::: machine.plugins.modals.ModalClosure + ## Decorators @@ -34,6 +38,8 @@ These classes represent base objects from the Slack API ### ::: machine.models.channel.Channel +### ::: machine.models.interactive.View + ## Storage Storage is exposed to plugins through the `self.storage` field. The following class implements the interface plugins diff --git a/docs/index.md b/docs/index.md index 01291a72..812f0341 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,7 +32,7 @@ class DeploymentPlugin(MachineBasePlugin): **Dropped support for Python 3.8** (v0.38.0) As of [v0.38.0](https://github.com/DonDebonair/slack-machine/releases/tag/v0.38.0), support for Python 3.8 has been -dropped. Python 3.7 has reached end-of-life on 2024-10-07. +dropped. Python 3.8 has reached end-of-life on 2024-10-07. ## Features @@ -59,6 +59,7 @@ dropped. Python 3.7 has reached end-of-life on 2024-10-07. - Support for [blocks](https://api.slack.com/reference/block-kit/blocks) - Support for [message attachments](https://api.slack.com/docs/message-attachments) [Legacy 🏚] - Support for [interactive elements](https://api.slack.com/block-kit) +- Support for [modals](https://api.slack.com/surfaces/modals) - Listen and respond to any [Slack event](https://api.slack.com/events) supported by the Events API - Store and retrieve any kind of data in persistent storage (currently Redis, DynamoDB, SQLite, and in-memory storage are supported) @@ -68,6 +69,5 @@ dropped. Python 3.7 has reached end-of-life on 2024-10-07. ### Coming Soon -- Support for modals - Support for shortcuts - ... and much more diff --git a/docs/plugins/block-kit-actions.md b/docs/plugins/block-kit-actions.md index a9bc26e5..18378657 100644 --- a/docs/plugins/block-kit-actions.md +++ b/docs/plugins/block-kit-actions.md @@ -8,8 +8,9 @@ Slack Machine makes it easy to listen to _actions_ triggered by these interactiv ## 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 +When you're defining [blocks](https://api.slack.com/reference/block-kit ) for your interactive surfaces - either by +providing a [dict][] or by leveraging the [models of the Slack SDK for Python](https://tools.slack.dev/python-slack-sdk/api-docs/slack_sdk/models/blocks/index.html) +- 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. @@ -19,7 +20,7 @@ Whenever the user interacts with these elements, an event is sent to Slack Machi ## Listening to actions -With the [`action`][machine.plugins.decorators.action] decorator you can define which plugin methods should be +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, @@ -34,7 +35,7 @@ Your block action handler will be called with a [`BlockAction`][machine.plugins. 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 +You can optionally add the `logger` argument to your handler 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 diff --git a/docs/plugins/interacting.md b/docs/plugins/interacting.md index 6690560d..d7c8b673 100644 --- a/docs/plugins/interacting.md +++ b/docs/plugins/interacting.md @@ -219,7 +219,7 @@ You can read [the events section][slack-machine-events] to see how your plugin c ## Using the Slack Web API in other ways Sometimes you want to use [Slack Web API](https://api.slack.com/web) in ways that are not directly exposed by -[`MachineBaserPlugin`][machine.plugins.base.MachineBasePlugin]. In these cases you can use +[`MachineBasePlugin`][machine.plugins.base.MachineBasePlugin]. In these cases you can use [`self.web_client`][machine.plugins.base.MachineBasePlugin.web_client]. `self.web_client` references the [`AsyncWebClient`](https://slack.dev/python-slack-sdk/api-docs/slack_sdk/web/async_client.html#slack_sdk.web.async_client.AsyncWebClient) object of the underlying Slack Python SDK. You should be able to call any diff --git a/docs/plugins/modals.md b/docs/plugins/modals.md new file mode 100644 index 00000000..1cbdf4dd --- /dev/null +++ b/docs/plugins/modals.md @@ -0,0 +1,104 @@ +# Modals + +In Slack, [modals](https://api.slack.com/surfaces/modals) are a way to ask users for input or display information in a +dialog/popup-like form. Modals are a great way to collect information from users, display information, or confirm an +action. + +Modals can only be triggered by [actions the user takes](https://api.slack.com/interactivity#user). The most +common types of user actions that can trigger a modal are: + +- Shortcuts +- [Slash Commands][slash-commands] +- [Block Kit interative components][block-kit-actions] + +For each of these actions, Slack provides a `trigger_id` which can be used to open a modal. **This needs to be done +within 3 seconds of receiving the `trigger_id`**. Slack Machine abstracts most of this away for you and lets you +open modals from Slash Commands and Block Kit actions without having to worry about the `trigger_id`. + +## Defining and opening modals + +When you want to open a modal, you first need to define the +[_view_](https://api.slack.com/surfaces/modals#composing_views) with the content you want to show. This view has +some additional properties that define how the modal should behave. One important property is the `callback_id` which +is used to identify the modal when it is submitted or closed. + +You can define a modal view in 2 ways: + +- As a [dict][] that conforms to the [View schema](https://api.slack.com/reference/surfaces/views#modal) +- By constructing a [View](https://tools.slack.dev/python-slack-sdk/api-docs/slack_sdk/models/views/index.html#slack_sdk.models.views.View) + object from the Slack SDK for Python + +When you have defined the view, you can open the modal by calling the `open_modal` method on the +[`Command`][machine.plugins.command.Command] object that is passed to your Slash Command handler or on the +[`BlockAction`][machine.plugins.block_action.BlockAction] object that is passed to your Block Kit action handler. + +## Listening for modal interactions + +Once a modal is opened, the user can interact with the block kit elements within the modal, such as buttons, input +fields, datepickers etc. When the user interacts with these elements, a [block kit action](block-kit-actions.md) can be +triggered which lets you deal with input. + +Additionally, when the user _submits_ the modal, this triggers a `view_submission` event that you can listen to with +the [`@modal`][machine.plugins.decorators.modal] decorator. This decorator takes a `callback_id` as +an argument, which is used to identify the modal that was submitted. The `callback_id` can be a [`str`][str] or a +[`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 `callback_id`s. This is convenient when you want one +handler to process multiple modals, for example. + +Unless you want to listen for changes to specific input fields - for example to update the modal in-place - it's +probably easiest to use the `@modal` decorator and process the entire input upon modal submission. + +### The modal handler function + +The handler function will be called with a +[`ModalSubmission`][machine.plugins.modals.ModalSubmission] object that contains useful information about the +modal that was submitted and the user that submitted it. The `ModalSubmission` object has a property +[`view`][machine.plugins.modals.ModalSubmission.view] that contains the complete view that was submitted, including the +state of the input fields of the modal. The object also has a [`user`][machine.plugins.modals.ModalSubmission.user] +property that corresponds to the user that submitted the modal. + +You can optionally add the `logger` argument to your handler get a +[logger that was enriched by Slack Machine](misc.md#using-loggers-provided-by-slack-machine-in-your-handler-functions) + +The `ModalSubmission` contains methods for +[updating the current modal view][machine.plugins.modals.ModalSubmission.update_modal], +[pushing a new view on top of the current one][machine.plugins.modals.ModalSubmission.push_modal] or even +[opening a completely new modal][machine.plugins.modals.ModalSubmission.open_modal]. + +You can also send a message to the user that submitted the modal with the +[`send_dm`][machine.plugins.modals.ModalSubmission.send_dm] method. + +The modal handler function can be defined as a regular `async` function or a generator. When you define it as a +generator, you can use the `yield` statement to: + +- [Update the modal view in-place](https://api.slack.com/surfaces/modals#updating_response) +- [Push a new view on top of the current one](https://api.slack.com/surfaces/modals#add_response) +- [Close the current view](https://api.slack.com/surfaces/modals#close_current_view) (which is the default behavior when + nothing is yielded) +- [Close all the views on the stack](https://api.slack.com/surfaces/modals#close_all_views) +- [Display errors in the modal](https://api.slack.com/surfaces/modals#displaying_errors), which is useful when you want + to show validation errors to the user. + +!!! warning + + You must yield a response to Slack within 3 seconds of receiving the `view_submission` event. If you don't, Slack + will show an error to the user. + + +## Listening for modal closures + +Sometimes you want to know when a user closes a modal without submitting it. This can be useful to clean up +resources or store the state of the modal for later continuation. You can listen for modal closures with the +[`@modal_closed`][machine.plugins.decorators.modal_closed] decorator. This decorator takes a `callback_id` as parameter +and works the same way as the `@modal` decorator. + +The handler function will be called with a +[`ModalClosure`][machine.plugins.modals.ModalClosure] object that contains information about the modal that was +closed. Just like the `ModalSubmission` object, the `ModalClosure` object has a +[`view`][machine.plugins.modals.ModalSubmission.view] property that contains the complete view of the modal that was +closed, including the state of the input fields. The object also has a +[`user`][machine.plugins.modals.ModalClosure.user] property that corresponds to the user that submitted the +modal. + +You can send a message to the user that submitted the modal with the +[`send_dm`][machine.plugins.modals.ModalClosure.send_dm] method. diff --git a/docs/plugins/slash-commands.md b/docs/plugins/slash-commands.md index 2c3dfb20..696963f8 100644 --- a/docs/plugins/slash-commands.md +++ b/docs/plugins/slash-commands.md @@ -34,7 +34,7 @@ features: ## Defining your Slash Command in code -The next step is to use the [`command`][machine.plugins.decorators.command] decorator on the function that should be +The next step is to use the [`@command`][machine.plugins.decorators.command] decorator on the function that should be triggered when the user uses the Slash Command you defined. The decorator takes only 1 parameter: the slash command that should trigger the decorated function. It should be the same as the Slash Command you just defined in the App dashboard. @@ -55,7 +55,7 @@ information about the slash command invocation. The most important property is p [`text`][machine.plugins.command.Command.text], which contains any additional text that was passed when the slash command was used. -You can optionally pass the `logger` argument to get a +You can optionally add the `logger` argument to your handler get a [logger that was enriched by Slack Machine](misc.md#using-loggers-provided-by-slack-machine-in-your-handler-functions) ### Responding to a command @@ -96,14 +96,10 @@ async def hello_again(self, command): await command.say("This will be sent after the initial acknowledgement") ``` -## Other types of responses +## Opening modals The [`Command`][machine.plugins.command.Command] object that your handler receives, contains an extra piece of information you can use to trigger more varied reponses: the [`trigger_id`][machine.plugins.command.Command.trigger_id] -The `trigger_id` can used specifically to trigger -[_modal responses_](https://api.slack.com/interactivity/handling#modal_responses). For now, creating a modal is -something you have to take care of yourself. More information on this can be found -[here](https://api.slack.com/surfaces/modals/using). - -In future releases, Slack Machine will make working with modals much easier by allowing modals to be opened directly -through the provided command object, and responding to interactions happening in modals through new decorators. +The `trigger_id` can used specifically to trigger [_modal responses_][modals]. You don't have to worry about the +`trigger_id` and instead can just call the [`open_modal`][machine.plugins.command.Command.open_modal] method on the +`Command` object to open a modal. diff --git a/machine/plugins/base.py b/machine/plugins/base.py index 7551c863..d46bea38 100644 --- a/machine/plugins/base.py +++ b/machine/plugins/base.py @@ -269,7 +269,6 @@ async def update( :param text: message text :param attachments: optional attachments (see [attachments]) :param blocks: optional blocks (see [blocks]) - :param thread_ts: optional timestamp of thread, to send a message in that thread :param ephemeral_user: optional user name or id if the message needs to visible to a specific user only :return: Dictionary deserialized from [`chat.update`](https://api.slack.com/methods/chat.update) request diff --git a/mkdocs.yml b/mkdocs.yml index 815f4b8f..0c84101d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,7 @@ nav: - 'plugins/slash-commands.md' - 'plugins/interacting.md' - 'plugins/block-kit-actions.md' + - 'plugins/modals.md' - 'plugins/settings.md' - 'plugins/storage.md' - 'plugins/misc.md' @@ -67,4 +68,3 @@ plugins: members_order: source import: - https://docs.python.org/dev/objects.inv - - https://slack.dev/python-slack-sdk/objects.inv