From 3c79220ae80115877238493b5430cdd61e9bf965 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Fri, 20 Sep 2024 17:05:08 -0400 Subject: [PATCH 01/12] Improve the SlackBlockActionsPayload model This improves the model so it better captures an interaction with a static select menu in a message. Other types of interactions will need to add to subtypes to SlackBlockActionBase. --- client/src/rubin/squarebot/models/slack.py | 140 +++++++++++++++++++-- server/src/squarebot/services/slack.py | 8 +- 2 files changed, 134 insertions(+), 14 deletions(-) diff --git a/client/src/rubin/squarebot/models/slack.py b/client/src/rubin/squarebot/models/slack.py index db6b036..7052d27 100644 --- a/client/src/rubin/squarebot/models/slack.py +++ b/client/src/rubin/squarebot/models/slack.py @@ -3,6 +3,7 @@ from __future__ import annotations from enum import Enum +from typing import Literal from pydantic import BaseModel, Field @@ -14,10 +15,18 @@ "SlackChannelType", "SlackMessageSubtype", "SlackMessageEventContent", - "SlackBlockAction", + "SlackBlockActionsPayload", "SlackUser", "SlackTeam", "SlackChannel", + "SlackBlockMessageAttachmentContainer", + "SlackBlockActionViewContainer", + "SlackBlockActionMessage", + "SlackBlockActionBase", + "SlackStaticSelectActionSelectedOption", + "SlackStaticSelectAction", + "SlackMessageAttachmentField", + "SlackMessageAttachment", ] @@ -301,17 +310,112 @@ class SlackChannel(BaseModel): name: str = Field(description="Name of the channel.") -class SlackBlockAction(BaseModel): +class SlackBlockMessageAttachmentContainer(BaseModel): + """A model for the container field in Slack interaction payloads triggered + by a block message attachment. + """ + + type: Literal["message_attachment"] = Field( + description="The type of container.", + ) + + message_ts: str = Field(description="The timestamp of the message.") + + channel_id: str = Field(description="The ID of the channel.") + + is_ephemeral: bool = Field(description="Whether the message is ephemeral.") + + is_app_unfurl: bool = Field( + description="Whether the message is an app unfurl." + ) + + +class SlackBlockActionViewContainer(BaseModel): + """A model for the container field in Slack interaction payloads triggered + by a block action view. + """ + + type: Literal["view"] = Field( + description="The type of container.", + ) + + view_id: str = Field(description="The ID of the view.") + + +class SlackBlockActionMessage(BaseModel): + """A model for the message field in Slack interaction payloads.""" + + type: Literal["message"] = Field(description="The type of container.") + + ts: str = Field(..., description="The timestamp of the message.") + + thread_ts: str | None = Field( + None, + description=( + "The timestamp of the parent message. This is only present in " + "threaded messages." + ), + ) + + user: str | None = Field( + None, + description=("The ID of the user or bot that sent the message."), + ) + + bot_id: str | None = Field( + None, + description=( + "The ID of the Slack App integration that sent the message. This " + "is null for non-bot messages." + ), + ) + + +class SlackBlockActionBase(BaseModel): + """A base model for a Slack block action.""" + + type: str = Field(description="The type of action.") + + action_id: str = Field(description="The action ID.") + + block_id: str = Field(description="The block ID.") + + action_ts: str = Field(description="The timestamp of the action.") + + +class SlackStaticSelectActionSelectedOption(BaseModel): + """A model for the selected option in a static select action.""" + + value: str = Field(description="The value of the selected option.") + + +class SlackStaticSelectAction(SlackBlockActionBase): + """A model for a static select action in a Slack block.""" + + type: Literal["static_select"] = Field(description="The type of action.") + + selected_option: SlackStaticSelectActionSelectedOption = Field( + ..., + description=( + "The selected option. This is only present for static select " + "actions." + ), + ) + + +class SlackBlockActionsPayload(BaseModel): """A model for a Slack Block kit interaction. - This isn't yet a full model for a block action payload; experience is + This isn't yet a full model for a block actions payload; experience is needed to fully understand what the payloads are for the types of interactions we use. See https://api.slack.com/reference/interaction-payloads/block-actions """ - type: str = Field(description="Should be `block_actions`.") + type: Literal["block_actions"] = Field( + description="Interaction payload type." + ) trigger_id: str = Field( description="A short-lived ID used to launch modals." @@ -325,20 +429,34 @@ class SlackBlockAction(BaseModel): ) ) - response_url: str = Field( + user: SlackUser = Field( + description=( + "Information about the user that triggered the interaction." + ) + ) + + team: SlackTeam | None = Field( description=( - "A short-lived URL to send message in response to interactions." + "Information about the Slack team. Null for org-installed apps." ) ) - user: SlackUser = Field( + channel: SlackChannel | None = Field( description=( - "Information about the user that triggered the interaction." + "Information about the Slack channel where the interaction " + "occurred." ) ) - team: SlackTeam = Field(description="Information about the Slack team.") + container: ( + SlackBlockMessageAttachmentContainer | SlackBlockActionViewContainer + ) = Field(description="Container where this interaction occurred.") + + message: SlackBlockActionMessage | None = Field( + None, description="The message where the interaction occurred." + ) - channel: SlackChannel = Field( - description="Information about the Slack channel." + # Add more action types as needed. + actions: list[SlackStaticSelectAction] = Field( + description="The actions that were triggered." ) diff --git a/server/src/squarebot/services/slack.py b/server/src/squarebot/services/slack.py index 88fe868..c9e9218 100644 --- a/server/src/squarebot/services/slack.py +++ b/server/src/squarebot/services/slack.py @@ -18,7 +18,7 @@ SquarebotSlackMessageValue, ) from rubin.squarebot.models.slack import ( - SlackBlockAction, + SlackBlockActionsPayload, SlackChannelType, SlackMessageEvent, SlackMessageType, @@ -302,7 +302,9 @@ async def publish_interaction( "type" in interaction_payload and interaction_payload["type"] == "block_actions" ): - action = SlackBlockAction.model_validate(interaction_payload) + action = SlackBlockActionsPayload.model_validate( + interaction_payload + ) # Temporary placeholder; will serialize and publish to Kafka # in reality. self._logger.debug( @@ -310,5 +312,5 @@ async def publish_interaction( type=action.type, trigger_id=action.trigger_id, username=action.user.username, - channel=action.channel.name, + channel=action.channel.name if action.channel else None, ) From 524c28eb1bddc5f218112a8de500a612e08a92da Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Mon, 23 Sep 2024 11:47:14 -0400 Subject: [PATCH 02/12] temp: print unknown interaction schemas --- server/src/squarebot/services/slack.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/squarebot/services/slack.py b/server/src/squarebot/services/slack.py index c9e9218..b73babb 100644 --- a/server/src/squarebot/services/slack.py +++ b/server/src/squarebot/services/slack.py @@ -314,3 +314,6 @@ async def publish_interaction( username=action.user.username, channel=action.channel.name if action.channel else None, ) + else: + self._logger.debug("Did not parse Slack interaction") + print(interaction_payload) # noqa: T201 From 193e5e9c14a596bf8827637cd6b465f861036591 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Mon, 23 Sep 2024 13:23:20 -0400 Subject: [PATCH 03/12] Make Slack verification handle both forms and json This adapts the `verify_slack` method to now handle both JSON payloads (like from the events API), and form-encoded data (like from the interactions API). This idea is based on the tip from https://github.com/encode/starlette/discussions/1933#discussioncomment-8387206 --- server/src/squarebot/services/slack.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/server/src/squarebot/services/slack.py b/server/src/squarebot/services/slack.py index b73babb..bd2a5cf 100644 --- a/server/src/squarebot/services/slack.py +++ b/server/src/squarebot/services/slack.py @@ -6,6 +6,7 @@ import hmac import math import time +import urllib.parse from typing import Any from fastapi import HTTPException, Request, status @@ -132,14 +133,22 @@ async def verify_request(self, request: Request) -> bool: }, ) - # Ensure that no special decoding is done on the body - body_bytes = await request.body() - body = body_bytes.decode(encoding="utf-8") + content_type = request.headers.get("Content-Type", "") + + # Get the payload from the request. This can either be from a form + # (like in interaction endpoint) or from the body (like in events + # endpoints.) See + # https://github.com/encode/starlette/discussions/1933#discussioncomment-8387206 + payload = ( + urllib.parse.urlencode(await request.form()) + if content_type == "application/x-www-form-urlencoded" + else (await request.body()).decode("utf-8") + ) # Compute the hash of the message and compare it ot X-Slack-Signature signing_secret = self._config.slack_signing_secret.get_secret_value() signature_hash = SlackService.compute_slack_signature( - signing_secret, body, timestamp + signing_secret, payload, timestamp ) if hmac.compare_digest( signature_hash, request.headers.get("X-Slack-Signature", "") From 482eabee5f383e013a61d58202f94403d679877d Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Mon, 23 Sep 2024 15:22:27 -0400 Subject: [PATCH 04/12] Support for message container type block action It seems that static selects published with block kit message attachments actually come out as "message" container types. Include an actual interaction payload featuring this behaviour for testing. I think it should be safe to include in the test library because all triggers, IDs, and response URLs should be expired by now. --- client/src/rubin/squarebot/models/slack.py | 117 +++++++++++--- server/tests/client/__init__.py | 0 server/tests/client/models/__init__.py | 0 server/tests/client/models/slack_test.py | 31 ++++ .../block_actions/static_select.json | 149 ++++++++++++++++++ 5 files changed, 277 insertions(+), 20 deletions(-) create mode 100644 server/tests/client/__init__.py create mode 100644 server/tests/client/models/__init__.py create mode 100644 server/tests/client/models/slack_test.py create mode 100644 server/tests/slack_messages/interactions/block_actions/static_select.json diff --git a/client/src/rubin/squarebot/models/slack.py b/client/src/rubin/squarebot/models/slack.py index 7052d27..ef08f63 100644 --- a/client/src/rubin/squarebot/models/slack.py +++ b/client/src/rubin/squarebot/models/slack.py @@ -8,28 +8,79 @@ from pydantic import BaseModel, Field __all__ = [ + "SlackPlainTextObject", + "SlackMrkdwnTextObject", "BaseSlackEvent", - "SlackUrlVerificationEvent", - "SlackMessageEvent", - "SlackMessageType", - "SlackChannelType", - "SlackMessageSubtype", - "SlackMessageEventContent", + "SlackBlockActionBase", + "SlackBlockActionsMessage", + "SlackBlockActionsMessageAttachmentContainer", + "SlackBlockActionsMessageContainer", "SlackBlockActionsPayload", - "SlackUser", - "SlackTeam", + "SlackBlockActionsViewContainer", "SlackChannel", - "SlackBlockMessageAttachmentContainer", - "SlackBlockActionViewContainer", - "SlackBlockActionMessage", - "SlackBlockActionBase", - "SlackStaticSelectActionSelectedOption", - "SlackStaticSelectAction", - "SlackMessageAttachmentField", + "SlackChannelType", "SlackMessageAttachment", + "SlackMessageAttachmentField", + "SlackMessageEvent", + "SlackMessageEventContent", + "SlackMessageSubtype", + "SlackMessageType", + "SlackStaticSelectAction", + "SlackStaticSelectActionSelectedOption", + "SlackTeam", + "SlackUrlVerificationEvent", + "SlackUser", ] +# SlackPlainTextObject and SlackMrkdwnTextObject are composition objects +# that should belong to a Safir Block Kit models library. They are included +# here for the interim. + + +class SlackPlainTextObject(BaseModel): + """A plain_text composition object. + + https://api.slack.com/reference/block-kit/composition-objects#text + """ + + type: Literal["plain_text"] = Field( + "plain_text", description="The type of object." + ) + + text: str = Field(..., description="The text to display.") + + emoji: bool = Field( + True, + description=( + "Indicates whether emojis in text should be escaped into colon " + "emoji format." + ), + ) + + +class SlackMrkdwnTextObject(BaseModel): + """A mrkdwn text composition object. + + https://api.slack.com/reference/block-kit/composition-objects#text + """ + + type: Literal["mrkdwn"] = Field( + "mrkdwn", description="The type of object." + ) + + text: str = Field(..., description="The text to display.") + + verbatim: bool = Field( + False, + description=( + "Indicates whether the text should be treated as verbatim. When " + "`True`, URLs will not be auto-converted into links and " + "channel names will not be auto-converted into links." + ), + ) + + class BaseSlackEvent(BaseModel): """A model for the minimal request payload from Slack for an event. @@ -310,7 +361,23 @@ class SlackChannel(BaseModel): name: str = Field(description="Name of the channel.") -class SlackBlockMessageAttachmentContainer(BaseModel): +class SlackBlockActionsMessageContainer(BaseModel): + """A model for the container field in Slack interaction payloads triggered + by block actions in a message. + """ + + type: Literal["message"] = Field( + description="The type of container.", + ) + + message_ts: str = Field(description="The timestamp of the message.") + + channel_id: str = Field(description="The ID of the channel.") + + is_ephemeral: bool = Field(description="Whether the message is ephemeral.") + + +class SlackBlockActionsMessageAttachmentContainer(BaseModel): """A model for the container field in Slack interaction payloads triggered by a block message attachment. """ @@ -330,7 +397,7 @@ class SlackBlockMessageAttachmentContainer(BaseModel): ) -class SlackBlockActionViewContainer(BaseModel): +class SlackBlockActionsViewContainer(BaseModel): """A model for the container field in Slack interaction payloads triggered by a block action view. """ @@ -342,7 +409,7 @@ class SlackBlockActionViewContainer(BaseModel): view_id: str = Field(description="The ID of the view.") -class SlackBlockActionMessage(BaseModel): +class SlackBlockActionsMessage(BaseModel): """A model for the message field in Slack interaction payloads.""" type: Literal["message"] = Field(description="The type of container.") @@ -386,6 +453,14 @@ class SlackBlockActionBase(BaseModel): class SlackStaticSelectActionSelectedOption(BaseModel): """A model for the selected option in a static select action.""" + text: SlackPlainTextObject = Field( + ..., + description=( + "The text of the selected option. This is only present for static " + "select actions." + ), + ) + value: str = Field(description="The value of the selected option.") @@ -449,10 +524,12 @@ class SlackBlockActionsPayload(BaseModel): ) container: ( - SlackBlockMessageAttachmentContainer | SlackBlockActionViewContainer + SlackBlockActionsMessageContainer + | SlackBlockActionsMessageAttachmentContainer + | SlackBlockActionsViewContainer ) = Field(description="Container where this interaction occurred.") - message: SlackBlockActionMessage | None = Field( + message: SlackBlockActionsMessage | None = Field( None, description="The message where the interaction occurred." ) diff --git a/server/tests/client/__init__.py b/server/tests/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/tests/client/models/__init__.py b/server/tests/client/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/tests/client/models/slack_test.py b/server/tests/client/models/slack_test.py new file mode 100644 index 0000000..908beab --- /dev/null +++ b/server/tests/client/models/slack_test.py @@ -0,0 +1,31 @@ +"""Test parsing Slack payloads.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from rubin.squarebot.models.slack import SlackBlockActionsPayload + + +@pytest.fixture +def samples_dir() -> Path: + """Get the path to the samples directory for interactions.""" + return ( + Path(__file__).parent + / "../../slack_messages/interactions/block_actions" + ) + + +def test_parse_block_actions_static_select(samples_dir: Path) -> None: + """Test parsing a block action with a static select.""" + data = SlackBlockActionsPayload.model_validate_json( + (samples_dir / "static_select.json").read_text() + ) + assert data.type == "block_actions" + assert data.container.type == "message" + assert data.actions[0].type == "static_select" + assert data.actions[0].action_id == "templatebot_select_project_template" + assert data.actions[0].selected_option.value == "fastapi" + assert data.actions[0].selected_option.text.text == "FastAPI" diff --git a/server/tests/slack_messages/interactions/block_actions/static_select.json b/server/tests/slack_messages/interactions/block_actions/static_select.json new file mode 100644 index 0000000..a84282c --- /dev/null +++ b/server/tests/slack_messages/interactions/block_actions/static_select.json @@ -0,0 +1,149 @@ +{ + "type": "block_actions", + "user": { + "id": "U2A73RVCL", + "username": "jonathansick", + "name": "jonathansick", + "team_id": "T06D204F2" + }, + "api_app_id": "A0575THDXFV", + "token": "3fEkJdH0rQhvcHkRxP6rb5vq", + "container": { + "type": "message", + "message_ts": "1727114630.743389", + "channel_id": "D058L2LN1BN", + "is_ephemeral": false + }, + "trigger_id": "7755839979959.6444004512.aa774cb19f437e2e9a31f6e48e875f18", + "team": { + "id": "T06D204F2", + "domain": "lsstc" + }, + "enterprise": null, + "is_enterprise_install": false, + "channel": { + "id": "D058L2LN1BN", + "name": "directmessage" + }, + "message": { + "user": "U058HDHHQQK", + "type": "message", + "ts": "1727114630.743389", + "bot_id": "B058W6LBPKK", + "app_id": "A0575THDXFV", + "text": "Select a project template", + "team": "T06D204F2", + "blocks": [ + { + "type": "section", + "block_id": "kqamX", + "text": { + "type": "mrkdwn", + "text": "Let's create a project", + "verbatim": false + }, + "accessory": { + "type": "static_select", + "action_id": "templatebot_select_project_template", + "placeholder": { + "type": "plain_text", + "text": "Choose a template\u2026", + "emoji": true + }, + "option_groups": [ + { + "label": { + "type": "plain_text", + "text": "SQuaRE", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "FastAPI", + "emoji": true + }, + "value": "fastapi" + }, + { + "text": { + "type": "plain_text", + "text": "PyPI", + "emoji": true + }, + "value": "pypi" + } + ] + }, + { + "label": { + "type": "plain_text", + "text": "Technotes", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "ReStructuredText", + "emoji": true + }, + "value": "rst" + }, + { + "text": { + "type": "plain_text", + "text": "Markdown", + "emoji": true + }, + "value": "md" + } + ] + } + ], + "focus_on_load": false + } + } + ] + }, + "state": { + "values": { + "kqamX": { + "templatebot_select_project_template": { + "type": "static_select", + "selected_option": { + "text": { + "type": "plain_text", + "text": "FastAPI", + "emoji": true + }, + "value": "fastapi" + } + } + } + } + }, + "response_url": "https://hooks.slack.com/actions/T06D204F2/7772886742436/i29HurSZvetWQkglyHkH41es", + "actions": [ + { + "type": "static_select", + "action_id": "templatebot_select_project_template", + "block_id": "kqamX", + "selected_option": { + "text": { + "type": "plain_text", + "text": "FastAPI", + "emoji": true + }, + "value": "fastapi" + }, + "placeholder": { + "type": "plain_text", + "text": "Choose a template\u2026", + "emoji": true + }, + "action_ts": "1727114633.415354" + } + ] +} From 9f7e14b551cd91e5e7f4d6b94b9a79e6bf449b27 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Tue, 24 Sep 2024 11:32:00 -0400 Subject: [PATCH 05/12] Add block actions topic publisher This Kafka topic and publisher is for block_actions interaction types. It replaces the original interim topic for interactions in general since we've decided to make separate Kafka topics for the different types of Kafka interactions (block actions vs views vs message shortcuts). --- server/src/squarebot/config.py | 13 ++++++++----- server/src/squarebot/factory.py | 8 ++++++++ server/src/squarebot/main.py | 2 +- server/src/squarebot/services/slack.py | 14 +++++++++----- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/server/src/squarebot/config.py b/server/src/squarebot/config.py index 3b9ada1..224476e 100644 --- a/server/src/squarebot/config.py +++ b/server/src/squarebot/config.py @@ -284,11 +284,14 @@ class Configuration(BaseSettings): ), ) - interaction_topic: str = Field( - "squarebot.interaction", - title="interaction Kafka topic", - alias="SQUAREBOT_TOPIC_INTERACTION", - description=("Kafka topic name for `interaction` Slack events"), + block_actions_topic: str = Field( + "squarebot.interaction.block_actions", + title="Block Actions interaction Kafka topic", + alias="SQUAREBOT_TOPIC_BLOCK_ACTIONS", + description=( + "Kafka topic name for `interaction` Slack events of type " + "`block_actions`." + ), ) # Slack signing secret diff --git a/server/src/squarebot/factory.py b/server/src/squarebot/factory.py index 51edbf4..e8ea876 100644 --- a/server/src/squarebot/factory.py +++ b/server/src/squarebot/factory.py @@ -38,6 +38,9 @@ class ProcessContext: app_mentions_publisher: Publisher """A Kafka publisher for the Slack ``app_mention`` topic.""" + block_actions_publisher: Publisher + """A Kafka publisher for the Slack block actions topic.""" + @classmethod async def create(cls) -> Self: broker = kafka_router.broker @@ -61,6 +64,10 @@ async def create(cls) -> Self: config.app_mention_topic, description="Slack bot mention messages.", ), + block_actions_publisher=broker.publisher( + config.block_actions_topic, + description="Slack block actions.", + ), ) async def aclose(self) -> None: @@ -89,4 +96,5 @@ def create_slack_service(self) -> SlackService: im_publisher=self._process_context.im_publisher, mpim_publisher=self._process_context.mpim_publisher, groups_publisher=self._process_context.groups_publisher, + block_actions_publisher=self._process_context.block_actions_publisher, ) diff --git a/server/src/squarebot/main.py b/server/src/squarebot/main.py index 90c2913..e4bd201 100644 --- a/server/src/squarebot/main.py +++ b/server/src/squarebot/main.py @@ -45,7 +45,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: "message_im": config.message_im_topic, "message_groups": config.message_groups_topic, "message_mpim": config.message_mpim_topic, - "interaction": config.interaction_topic, + "block_actions": config.block_actions_topic, }, ) diff --git a/server/src/squarebot/services/slack.py b/server/src/squarebot/services/slack.py index bd2a5cf..c793841 100644 --- a/server/src/squarebot/services/slack.py +++ b/server/src/squarebot/services/slack.py @@ -40,6 +40,7 @@ def __init__( groups_publisher: Publisher, im_publisher: Publisher, mpim_publisher: Publisher, + block_actions_publisher: Publisher, ) -> None: self._logger = logger self._config = config @@ -48,6 +49,7 @@ def __init__( self._groups_publisher = groups_publisher self._im_publisher = im_publisher self._mpim_publisher = mpim_publisher + self._block_actions_publisher = block_actions_publisher @staticmethod def compute_slack_signature( @@ -311,17 +313,19 @@ async def publish_interaction( "type" in interaction_payload and interaction_payload["type"] == "block_actions" ): - action = SlackBlockActionsPayload.model_validate( + block_action = SlackBlockActionsPayload.model_validate( interaction_payload ) # Temporary placeholder; will serialize and publish to Kafka # in reality. self._logger.debug( "Got a Slack interaction", - type=action.type, - trigger_id=action.trigger_id, - username=action.user.username, - channel=action.channel.name if action.channel else None, + type=block_action.type, + trigger_id=block_action.trigger_id, + username=block_action.user.username, + channel=block_action.channel.name + if block_action.channel + else None, ) else: self._logger.debug("Did not parse Slack interaction") From a6f90c2f935c389d2ed23717594c109115874375 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Tue, 24 Sep 2024 11:59:32 -0400 Subject: [PATCH 06/12] Create Pydantic models for block action messages The value is based on the full Pydantic model for the block actions payload since we're modelling it well. It also includes the raw json body in the slack_interaction field for clients to get additional details that aren't parsed. --- client/src/rubin/squarebot/models/kafka.py | 79 +++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/client/src/rubin/squarebot/models/kafka.py b/client/src/rubin/squarebot/models/kafka.py index 8b5f83c..9e4cf41 100644 --- a/client/src/rubin/squarebot/models/kafka.py +++ b/client/src/rubin/squarebot/models/kafka.py @@ -8,6 +8,7 @@ from pydantic import BaseModel, Field from .slack import ( + SlackBlockActionsPayload, SlackChannelType, SlackMessageEvent, SlackMessageSubtype, @@ -15,9 +16,11 @@ ) __all__ = [ + "SquarebotSlackAppMentionValue", + "SquarebotSlackBlockActionsKey", + "SquarebotSlackBlockActionsValue", "SquarebotSlackMessageKey", "SquarebotSlackMessageValue", - "SquarebotSlackAppMentionValue", ] @@ -232,3 +235,77 @@ def from_event(cls, event: SlackMessageEvent, raw: dict[str, Any]) -> Self: text=event.event.text, slack_event=json.dumps(raw), ) + + +class SquarebotSlackBlockActionsKey(BaseModel): + """Kafka message key model for Slack block actions sent by Squarebot.""" + + user_id: str = Field( + ..., description="The Slack user ID that triggered the action." + ) + + team: str | None = Field(None, description="The Slack team ID.") + + channel_id: str | None = Field(None, description="The Slack channel ID.") + + @classmethod + def from_block_actions(cls, payload: SlackBlockActionsPayload) -> Self: + """Create a Kafka key for a Slack block action from a payload. + + Parameters + ---------- + payload + The Slack block actions payload. + + Returns + ------- + key + The Squarebot block actions key. + """ + return cls( + user_id=payload.user.id, + team=payload.team.id if payload.team is not None else None, + channel_id=payload.channel.id + if payload.channel is not None + else None, + ) + + def to_key_bytes(self) -> bytes: + """Serialize the key to bytes for use as a Kafka key. + + Returns + ------- + bytes + The serialized key. + """ + key_str = f"{self.user_id}:{self.team}:{self.channel_id}" + return key_str.encode("utf-8") + + +class SquarebotSlackBlockActionsValue(SlackBlockActionsPayload): + """Kafka message value model for Slack block actions sent by Squarebot.""" + + slack_interaction: str = Field( + ..., description="The original Slack block actions JSON string." + ) + + @classmethod + def from_block_actions( + cls, payload: SlackBlockActionsPayload, raw: dict[str, Any] + ) -> Self: + """Create a Kafka value for a Slack block action from a payload. + + Parameters + ---------- + payload + The Slack block actions payload. + + Returns + ------- + value + The Squarebot block actions value. + """ + return cls( + **payload.model_dump(), + slack_interaction=json.dumps(raw), + ) From 019a9d8357a5f06a5e07ca716f6e5db434121169 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Tue, 24 Sep 2024 12:01:16 -0400 Subject: [PATCH 07/12] Publish block actions interactions to Kafka --- server/src/squarebot/services/slack.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/src/squarebot/services/slack.py b/server/src/squarebot/services/slack.py index c793841..34ecc7d 100644 --- a/server/src/squarebot/services/slack.py +++ b/server/src/squarebot/services/slack.py @@ -15,6 +15,8 @@ from rubin.squarebot.models.kafka import ( SquarebotSlackAppMentionValue, + SquarebotSlackBlockActionsKey, + SquarebotSlackBlockActionsValue, SquarebotSlackMessageKey, SquarebotSlackMessageValue, ) @@ -327,6 +329,18 @@ async def publish_interaction( if block_action.channel else None, ) + key = SquarebotSlackBlockActionsKey.from_block_actions( + block_action + ) + value = SquarebotSlackBlockActionsValue.from_block_actions( + block_action, interaction_payload + ) + publisher = self._block_actions_publisher + await publisher.publish( + message=value, + key=key.to_key_bytes(), + headers={"content-type": "application/json"}, + ) else: self._logger.debug("Did not parse Slack interaction") print(interaction_payload) # noqa: T201 From eb5faf75e365a1ea9a1133a9cf2689ca16ce5cb5 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Wed, 25 Sep 2024 17:36:55 -0400 Subject: [PATCH 08/12] Implement Slack view submission interactions - Parse the view_submission interaction type into a Pydantic model (this model needs more work because it'll need to contain a copy of the view, but it'd be easy to base that model off the Block Kit models that we'll eventually put in Safir) - Add a topic configuration for view submissions - Add a publisher for view submissions, including models for the Kafka key and topic. --- client/src/rubin/squarebot/models/kafka.py | 86 +++++++++++++++++++++- client/src/rubin/squarebot/models/slack.py | 36 ++++++++- server/src/squarebot/config.py | 10 +++ server/src/squarebot/factory.py | 8 ++ server/src/squarebot/services/slack.py | 31 +++++++- 5 files changed, 162 insertions(+), 9 deletions(-) diff --git a/client/src/rubin/squarebot/models/kafka.py b/client/src/rubin/squarebot/models/kafka.py index 9e4cf41..99d793b 100644 --- a/client/src/rubin/squarebot/models/kafka.py +++ b/client/src/rubin/squarebot/models/kafka.py @@ -13,12 +13,15 @@ SlackMessageEvent, SlackMessageSubtype, SlackMessageType, + SlackViewSubmissionPayload, ) __all__ = [ "SquarebotSlackAppMentionValue", "SquarebotSlackBlockActionsKey", "SquarebotSlackBlockActionsValue", + "SquarebotSlackViewSubmissionKey", + "SquarebotSlackViewSubmissionValue", "SquarebotSlackMessageKey", "SquarebotSlackMessageValue", ] @@ -283,7 +286,9 @@ def to_key_bytes(self) -> bytes: class SquarebotSlackBlockActionsValue(SlackBlockActionsPayload): - """Kafka message value model for Slack block actions sent by Squarebot.""" + """Kafka message value model for Slack block actions interactions sent by + Squarebot. + """ slack_interaction: str = Field( ..., description="The original Slack block actions JSON string." @@ -298,12 +303,87 @@ def from_block_actions( Parameters ---------- payload - The Slack block actions payload. + The Slack block action payload. + raw + The raw Slack block actions JSON. + + Returns + ------- + value + The Squarebot block actions message value. + """ + return cls( + **payload.model_dump(), + slack_interaction=json.dumps(raw), + ) + + +class SquarebotSlackViewSubmissionKey(BaseModel): + """Kafka message key model for Slack view submissions sent by Squarebot.""" + + user_id: str = Field( + ..., description="The Slack user ID that triggered the action." + ) + + team: str = Field(..., description="The Slack team ID.") + + @classmethod + def from_view_submission(cls, payload: SlackViewSubmissionPayload) -> Self: + """Create a Kafka key for a Slack view submission from a payload. + + Parameters + ---------- + payload + The Slack view_submission payload. + + Returns + ------- + key + The Squarebot view submission message key key. + """ + return cls( + user_id=payload.user.id, + team=payload.team.id, + ) + + def to_key_bytes(self) -> bytes: + """Serialize the key to bytes for use as a Kafka key. + + Returns + ------- + bytes + The serialized key. + """ + key_str = f"{self.user_id}:{self.team}" + return key_str.encode("utf-8") + + +class SquarebotSlackViewSubmissionValue(SlackViewSubmissionPayload): + """Kafka message value model for Slack view_submission events sent by + Squarebot. + """ + + slack_interaction: str = Field( + ..., description="The original Slack view_submission JSON string." + ) + + @classmethod + def from_view_submission( + cls, payload: SlackViewSubmissionPayload, raw: dict[str, Any] + ) -> Self: + """Create a Kafka value for a Slack view submission from a payload. + + Parameters + ---------- + payload + The Slack view submission payload. + raw + The raw Slack view submission JSON. Returns ------- value - The Squarebot block actions value. + The Squarebot view_submission message value. """ return cls( **payload.model_dump(), diff --git a/client/src/rubin/squarebot/models/slack.py b/client/src/rubin/squarebot/models/slack.py index ef08f63..03cdb4e 100644 --- a/client/src/rubin/squarebot/models/slack.py +++ b/client/src/rubin/squarebot/models/slack.py @@ -8,8 +8,6 @@ from pydantic import BaseModel, Field __all__ = [ - "SlackPlainTextObject", - "SlackMrkdwnTextObject", "BaseSlackEvent", "SlackBlockActionBase", "SlackBlockActionsMessage", @@ -25,11 +23,14 @@ "SlackMessageEventContent", "SlackMessageSubtype", "SlackMessageType", + "SlackMrkdwnTextObject", + "SlackPlainTextObject", "SlackStaticSelectAction", "SlackStaticSelectActionSelectedOption", "SlackTeam", "SlackUrlVerificationEvent", "SlackUser", + "SlackViewSubmissionPayload", ] @@ -537,3 +538,34 @@ class SlackBlockActionsPayload(BaseModel): actions: list[SlackStaticSelectAction] = Field( description="The actions that were triggered." ) + + +class SlackViewSubmissionPayload(BaseModel): + """A model for a Slack view submission payload. + + This isn't yet a full model for a view submission payload. + + See https://api.slack.com/reference/interaction-payloads/views#view_submission + """ + + type: Literal["view_submission"] = Field( + description="Interaction payload type." + ) + + team: SlackTeam = Field(description="Information about the Slack team.") + + user: SlackUser = Field( + description=( + "Information about the user that triggered the interaction." + ) + ) + + api_app_id: str = Field( + description=( + "The unique identifier of your installed Slack application. Use " + "this to distinguish which app the event belongs to if you use " + "multiple apps with the same Request URL." + ) + ) + + view: dict = Field(description="The view that was submitted.") diff --git a/server/src/squarebot/config.py b/server/src/squarebot/config.py index 224476e..d3fb69e 100644 --- a/server/src/squarebot/config.py +++ b/server/src/squarebot/config.py @@ -294,6 +294,16 @@ class Configuration(BaseSettings): ), ) + view_submission_topic: str = Field( + "squarebot.interaction.view_submission", + title="View Submission interaction Kafka topic", + alias="SQUAREBOT_TOPIC_VIEW_SUBMISSION", + description=( + "Kafka topic name for `interaction` Slack events of type " + "`view_submission`." + ), + ) + # Slack signing secret slack_signing_secret: SecretStr = Field( title="Slack signing secret", alias="SQUAREBOT_SLACK_SIGNING" diff --git a/server/src/squarebot/factory.py b/server/src/squarebot/factory.py index e8ea876..0a54b4b 100644 --- a/server/src/squarebot/factory.py +++ b/server/src/squarebot/factory.py @@ -41,6 +41,9 @@ class ProcessContext: block_actions_publisher: Publisher """A Kafka publisher for the Slack block actions topic.""" + view_submission_publisher: Publisher + """A Kafka publisher for the Slack view submissions topic.""" + @classmethod async def create(cls) -> Self: broker = kafka_router.broker @@ -68,6 +71,10 @@ async def create(cls) -> Self: config.block_actions_topic, description="Slack block actions.", ), + view_submission_publisher=broker.publisher( + config.view_submission_topic, + description="Slack view submission.", + ), ) async def aclose(self) -> None: @@ -97,4 +104,5 @@ def create_slack_service(self) -> SlackService: mpim_publisher=self._process_context.mpim_publisher, groups_publisher=self._process_context.groups_publisher, block_actions_publisher=self._process_context.block_actions_publisher, + view_submission_publisher=self._process_context.view_submission_publisher, ) diff --git a/server/src/squarebot/services/slack.py b/server/src/squarebot/services/slack.py index 34ecc7d..f22c571 100644 --- a/server/src/squarebot/services/slack.py +++ b/server/src/squarebot/services/slack.py @@ -19,12 +19,15 @@ SquarebotSlackBlockActionsValue, SquarebotSlackMessageKey, SquarebotSlackMessageValue, + SquarebotSlackViewSubmissionKey, + SquarebotSlackViewSubmissionValue, ) from rubin.squarebot.models.slack import ( SlackBlockActionsPayload, SlackChannelType, SlackMessageEvent, SlackMessageType, + SlackViewSubmissionPayload, ) from ..config import Configuration @@ -43,6 +46,7 @@ def __init__( im_publisher: Publisher, mpim_publisher: Publisher, block_actions_publisher: Publisher, + view_submission_publisher: Publisher, ) -> None: self._logger = logger self._config = config @@ -52,6 +56,7 @@ def __init__( self._im_publisher = im_publisher self._mpim_publisher = mpim_publisher self._block_actions_publisher = block_actions_publisher + self._view_submission_publisher = view_submission_publisher @staticmethod def compute_slack_signature( @@ -318,8 +323,6 @@ async def publish_interaction( block_action = SlackBlockActionsPayload.model_validate( interaction_payload ) - # Temporary placeholder; will serialize and publish to Kafka - # in reality. self._logger.debug( "Got a Slack interaction", type=block_action.type, @@ -341,6 +344,26 @@ async def publish_interaction( key=key.to_key_bytes(), headers={"content-type": "application/json"}, ) + elif ( + "type" in interaction_payload + and interaction_payload["type"] == "view_submission" + ): + payload = SlackViewSubmissionPayload.model_validate( + interaction_payload + ) + await self._view_submission_publisher.publish( + message=SquarebotSlackViewSubmissionValue.from_view_submission( + payload, interaction_payload + ), + key=SquarebotSlackViewSubmissionKey.from_view_submission( + payload + ).to_key_bytes(), + headers={"content-type": "application/json"}, + ) + + self._logger.debug("Got a Slack view submission") else: - self._logger.debug("Did not parse Slack interaction") - print(interaction_payload) # noqa: T201 + self._logger.debug( + "Did not parse Slack interaction", + raw_interaction=interaction_payload, + ) From c3387f07600582a8d0c963815fe581a1deed748a Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Wed, 25 Sep 2024 17:49:17 -0400 Subject: [PATCH 09/12] Add a test for parsing the view submission payload --- server/tests/client/models/slack_test.py | 20 ++- .../interactions/view_submission.json | 121 ++++++++++++++++++ 2 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 server/tests/slack_messages/interactions/view_submission.json diff --git a/server/tests/client/models/slack_test.py b/server/tests/client/models/slack_test.py index 908beab..e3bd388 100644 --- a/server/tests/client/models/slack_test.py +++ b/server/tests/client/models/slack_test.py @@ -6,22 +6,22 @@ import pytest -from rubin.squarebot.models.slack import SlackBlockActionsPayload +from rubin.squarebot.models.slack import ( + SlackBlockActionsPayload, + SlackViewSubmissionPayload, +) @pytest.fixture def samples_dir() -> Path: """Get the path to the samples directory for interactions.""" - return ( - Path(__file__).parent - / "../../slack_messages/interactions/block_actions" - ) + return Path(__file__).parent / "../../slack_messages/interactions" def test_parse_block_actions_static_select(samples_dir: Path) -> None: """Test parsing a block action with a static select.""" data = SlackBlockActionsPayload.model_validate_json( - (samples_dir / "static_select.json").read_text() + (samples_dir / "block_actions/static_select.json").read_text() ) assert data.type == "block_actions" assert data.container.type == "message" @@ -29,3 +29,11 @@ def test_parse_block_actions_static_select(samples_dir: Path) -> None: assert data.actions[0].action_id == "templatebot_select_project_template" assert data.actions[0].selected_option.value == "fastapi" assert data.actions[0].selected_option.text.text == "FastAPI" + + +def test_parse_view_submission(samples_dir: Path) -> None: + """Test parsing a view_submission.""" + data = SlackViewSubmissionPayload.model_validate_json( + (samples_dir / "view_submission.json").read_text() + ) + assert data.type == "view_submission" diff --git a/server/tests/slack_messages/interactions/view_submission.json b/server/tests/slack_messages/interactions/view_submission.json new file mode 100644 index 0000000..d2ad73e --- /dev/null +++ b/server/tests/slack_messages/interactions/view_submission.json @@ -0,0 +1,121 @@ +{ + "type": "view_submission", + "team": { + "id": "ABCDEFG", + "domain": "lsstc" + }, + "user": { + "id": "U2A73RVCL", + "username": "jonathansick", + "name": "jonathansick", + "team_id": "ABCDEFG" + }, + "api_app_id": "APPID", + "trigger_id": "7771678089414.6444004512.0b2af9315b97b0e3babcc1f73cd23b15", + "view": { + "id": "V07NW911RBL", + "team_id": "ABCDEFG", + "type": "modal", + "blocks": [ + { + "type": "section", + "block_id": "RRGN9", + "text": { + "type": "mrkdwn", + "text": "Let's create a FastAPI project.", + "verbatim": false + } + }, + { + "type": "input", + "block_id": "license", + "label": { + "type": "plain_text", + "text": "License", + "emoji": true + }, + "hint": { + "type": "plain_text", + "text": "MIT is preferred.", + "emoji": true + }, + "optional": false, + "dispatch_action": false, + "element": { + "type": "static_select", + "action_id": "select_license", + "placeholder": { + "type": "plain_text", + "text": "Choose a license\u2026", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "MIT", + "emoji": true + }, + "value": "mit" + }, + { + "text": { + "type": "plain_text", + "text": "GPLv3", + "emoji": true + }, + "value": "gplv3" + } + ], + "focus_on_load": false + } + } + ], + "private_metadata": "", + "callback_id": "", + "state": { + "values": { + "license": { + "select_license": { + "type": "static_select", + "selected_option": { + "text": { + "type": "plain_text", + "text": "MIT", + "emoji": true + }, + "value": "mit" + } + } + } + } + }, + "hash": "1727216577.fRLCN97Y", + "title": { + "type": "plain_text", + "text": "Set up your project", + "emoji": true + }, + "clear_on_close": false, + "notify_on_close": false, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Create project", + "emoji": true + }, + "previous_view_id": null, + "root_view_id": "V07NW911RBL", + "app_id": "APPID", + "external_id": "", + "app_installed_team_id": "ABCDEFG", + "bot_id": "BOTID" + }, + "response_urls": [], + "is_enterprise_install": false, + "enterprise": null +} From d18bff2881116d18dc104e674dfc3a04f1deb626 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Thu, 26 Sep 2024 13:46:28 -0400 Subject: [PATCH 10/12] Add changelog fragment for interaction handling --- changelog.d/20240926_131950_jsick_DM_46427.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 changelog.d/20240926_131950_jsick_DM_46427.md diff --git a/changelog.d/20240926_131950_jsick_DM_46427.md b/changelog.d/20240926_131950_jsick_DM_46427.md new file mode 100644 index 0000000..1896605 --- /dev/null +++ b/changelog.d/20240926_131950_jsick_DM_46427.md @@ -0,0 +1,9 @@ +### New features + +- Support for Slack [block actions](https://api.slack.com/reference/interaction-payloads/block-actions) interactions. These interactions happen when a user interacts with a message's interactive elements (e.g., buttons, menus). These Slack payloads are parsed into `SlackBlockActionsPayload` objects and published to a block action Kafka topic (`$SQUAREBOT_TOPIC_BLOCK_ACTIONS`) with `SquarebotSlackBlockActionsKey` key and `SquarebotSlackBlockActionsValue` value models. + +- Support for Slack [view submission](https://api.slack.com/reference/interaction-payloads/views) interactions. These interactions happen when a modal is submitted. These Slack payloads are parsed into `SlackViewSubmissionPayload` objects and published to a view submission Kafka topic (`$SQUAREBOT_TOPIC_VIEW_SUBMISSION`) with `SquarebotSlackViewSubmissionKey` key and `SquarebotSlackViewSubmissionValue` value models. The value model doesn't yet fully parse the view into Pydantic models; clients will need to inspect the JSON object to get the submitted state of the model. Once more Pydantic modeling of Slack views and Block Kit blocks and elements is implemented, we can update the value model to provide more fully typed messages. + +### Bug fixes + +- Improved the Slack message verification so that it now handles both JSON-formatted posts and url-encoded form posts. This change is necessary because Slack sends JSON-formatted posts for messages and url-encoded form posts for interactions. The verification now works for both types of posts. From 1d51da1d41431fe37531c6722fd792892b534098 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Thu, 26 Sep 2024 18:03:57 -0400 Subject: [PATCH 11/12] Update base image to 3.12.6 bookworm --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e361c82..7f2cff7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ # - Runs a non-root user. # - Sets up the entrypoint and port. -FROM python:3.12.3-slim-bullseye as base-image +FROM python:3.12.6-slim-bookworm AS base-image # Update system packages COPY scripts/install-base-packages.sh . From 41e06b877ae6822b1b8140f75164034f52b31a88 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Thu, 26 Sep 2024 18:18:01 -0400 Subject: [PATCH 12/12] Prepare change log for 0.10.0 release --- CHANGELOG.md | 24 +++++++++++++++++++ changelog.d/20240919_190157_jsick_DM_46413.md | 17 ------------- changelog.d/20240926_131950_jsick_DM_46427.md | 9 ------- changelog.d/20240926_135225_jsick_DM_45917.md | 3 --- 4 files changed, 24 insertions(+), 29 deletions(-) delete mode 100644 changelog.d/20240919_190157_jsick_DM_46413.md delete mode 100644 changelog.d/20240926_131950_jsick_DM_46427.md delete mode 100644 changelog.d/20240926_135225_jsick_DM_45917.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a8c9a0..020f78f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ + + +## 0.10.0 (2024-09-26) + +### Backwards-incompatible changes + +- `SquarebotSlackMessageValue.user` is now nullable. It will be `null` if the message is a `bot_message` subtype. + +### New features + +- Added `SquarebotSlackMessageValue.bot_id` to capture the ID of the app that send a bot message. + +- Support for Slack [block actions](https://api.slack.com/reference/interaction-payloads/block-actions) interactions. These interactions happen when a user interacts with a message's interactive elements (e.g., buttons, menus). These Slack payloads are parsed into `SlackBlockActionsPayload` objects and published to a block action Kafka topic (`$SQUAREBOT_TOPIC_BLOCK_ACTIONS`) with `SquarebotSlackBlockActionsKey` key and `SquarebotSlackBlockActionsValue` value models. + +- Support for Slack [view submission](https://api.slack.com/reference/interaction-payloads/views) interactions. These interactions happen when a modal is submitted. These Slack payloads are parsed into `SlackViewSubmissionPayload` objects and published to a view submission Kafka topic (`$SQUAREBOT_TOPIC_VIEW_SUBMISSION`) with `SquarebotSlackViewSubmissionKey` key and `SquarebotSlackViewSubmissionValue` value models. The value model doesn't yet fully parse the view into Pydantic models; clients will need to inspect the JSON object to get the submitted state of the model. Once more Pydantic modeling of Slack views and Block Kit blocks and elements is implemented, we can update the value model to provide more fully typed messages. + +- Publish AsyncAPI documentation to the `/asyncapi` endpoint. This documentation site is generated automatically through Faststream. + +### Bug fixes + +- Fix setting the `is_bot` property of `SquarebotSlackMessageValue` to account for messages without the `bot_message` subtype, but which still have a `bot_id` set. + +- Improved the Slack message verification so that it now handles both JSON-formatted posts and url-encoded form posts. This change is necessary because Slack sends JSON-formatted posts for messages and url-encoded form posts for interactions. The verification now works for both types of posts. + ## 0.9.0 (2024-07-25) diff --git a/changelog.d/20240919_190157_jsick_DM_46413.md b/changelog.d/20240919_190157_jsick_DM_46413.md deleted file mode 100644 index 34bdae5..0000000 --- a/changelog.d/20240919_190157_jsick_DM_46413.md +++ /dev/null @@ -1,17 +0,0 @@ - - -### Backwards-incompatible changes - -- `SquarebotSlackMessageValue.user` is now nullable. It will be `null` if the message is a `bot_message` subtype. - -### New features - -- Added `SquarebotSlackMessageValue.bot_id` to capture the ID of the app that send a bot message. - -### Bug fixes - -- Fix setting the `is_bot` property of `SquarebotSlackMessageValue` to account for messages without the `bot_message` subtype, but which still have a `bot_id` set. - -### Other changes - -- diff --git a/changelog.d/20240926_131950_jsick_DM_46427.md b/changelog.d/20240926_131950_jsick_DM_46427.md deleted file mode 100644 index 1896605..0000000 --- a/changelog.d/20240926_131950_jsick_DM_46427.md +++ /dev/null @@ -1,9 +0,0 @@ -### New features - -- Support for Slack [block actions](https://api.slack.com/reference/interaction-payloads/block-actions) interactions. These interactions happen when a user interacts with a message's interactive elements (e.g., buttons, menus). These Slack payloads are parsed into `SlackBlockActionsPayload` objects and published to a block action Kafka topic (`$SQUAREBOT_TOPIC_BLOCK_ACTIONS`) with `SquarebotSlackBlockActionsKey` key and `SquarebotSlackBlockActionsValue` value models. - -- Support for Slack [view submission](https://api.slack.com/reference/interaction-payloads/views) interactions. These interactions happen when a modal is submitted. These Slack payloads are parsed into `SlackViewSubmissionPayload` objects and published to a view submission Kafka topic (`$SQUAREBOT_TOPIC_VIEW_SUBMISSION`) with `SquarebotSlackViewSubmissionKey` key and `SquarebotSlackViewSubmissionValue` value models. The value model doesn't yet fully parse the view into Pydantic models; clients will need to inspect the JSON object to get the submitted state of the model. Once more Pydantic modeling of Slack views and Block Kit blocks and elements is implemented, we can update the value model to provide more fully typed messages. - -### Bug fixes - -- Improved the Slack message verification so that it now handles both JSON-formatted posts and url-encoded form posts. This change is necessary because Slack sends JSON-formatted posts for messages and url-encoded form posts for interactions. The verification now works for both types of posts. diff --git a/changelog.d/20240926_135225_jsick_DM_45917.md b/changelog.d/20240926_135225_jsick_DM_45917.md deleted file mode 100644 index 8da7345..0000000 --- a/changelog.d/20240926_135225_jsick_DM_45917.md +++ /dev/null @@ -1,3 +0,0 @@ -### New features - -- Publish AsyncAPI documentation to the `/asyncapi` endpoint. This documentation site is generated automatically through Faststream.