Skip to content

Commit

Permalink
Merge pull request #33 from lsst-sqre/tickets/DM-46427
Browse files Browse the repository at this point in the history
DM-46427: Handle slack interaction payloads
  • Loading branch information
jonathansick authored Sep 26, 2024
2 parents 8eb185b + 41e06b8 commit 01e5bcd
Show file tree
Hide file tree
Showing 15 changed files with 837 additions and 56 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

<!-- scriv-insert-here -->

<a id='changelog-0.10.0'></a>

## 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.

<a id='changelog-0.9.0'></a>

## 0.9.0 (2024-07-25)
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down
17 changes: 0 additions & 17 deletions changelog.d/20240919_190157_jsick_DM_46413.md

This file was deleted.

3 changes: 0 additions & 3 deletions changelog.d/20240926_135225_jsick_DM_45917.md

This file was deleted.

159 changes: 158 additions & 1 deletion client/src/rubin/squarebot/models/kafka.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,22 @@
from pydantic import BaseModel, Field

from .slack import (
SlackBlockActionsPayload,
SlackChannelType,
SlackMessageEvent,
SlackMessageSubtype,
SlackMessageType,
SlackViewSubmissionPayload,
)

__all__ = [
"SquarebotSlackAppMentionValue",
"SquarebotSlackBlockActionsKey",
"SquarebotSlackBlockActionsValue",
"SquarebotSlackViewSubmissionKey",
"SquarebotSlackViewSubmissionValue",
"SquarebotSlackMessageKey",
"SquarebotSlackMessageValue",
"SquarebotSlackAppMentionValue",
]


Expand Down Expand Up @@ -232,3 +238,154 @@ 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 interactions 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 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 view_submission message value.
"""
return cls(
**payload.model_dump(),
slack_interaction=json.dumps(raw),
)
Loading

0 comments on commit 01e5bcd

Please sign in to comment.