-
Notifications
You must be signed in to change notification settings - Fork 351
Add Slack interactivity webhook support #6674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
+283
−15
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
86025fd
Add Slack interactivity webhook support
suhaibmujahid 960ce7e
Add slack-sdk to locked dependencies
suhaibmujahid 7046424
Align Slack interaction webhook routing/docs
suhaibmujahid 81e78c3
Enforce non-blank Slack signing secret at startup
suhaibmujahid 0181ef1
Validate Slack clicks with Pydantic models
suhaibmujahid 34804d9
Fix form parsing in Slack interactions endpoint
suhaibmujahid 445f278
Handle Slack block actions for agent runs
suhaibmujahid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| from app.routers.events import router as events_router | ||
| from app.routers.runs import router as runs_router | ||
| from app.routers.slack import router as slack_router | ||
| from app.routers.webhooks import router as webhooks_router | ||
|
|
||
| __all__ = ["events_router", "runs_router", "webhooks_router"] | ||
| __all__ = ["events_router", "runs_router", "slack_router", "webhooks_router"] |
This file contains hidden or 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,54 @@ | ||
| """Inbound Slack interactivity receiver: clicks on the messages hackbot posts. | ||
|
|
||
| ``docs/hackbot/api.md`` covers the endpoint, the delivery shape and the Slack-app | ||
| config; ``docs/hackbot/security.md`` covers the signature and what it does not | ||
| prove. | ||
| """ | ||
|
|
||
| import logging | ||
|
|
||
| from fastapi import APIRouter, Depends, Request, Response, status | ||
|
|
||
| from app.auth import require_slack_signature | ||
| from app.routers.webhooks import get_hackbot_client | ||
| from app.slack_webhook import BlockActionsEvent | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
| # Under `/webhooks` with the Phabricator receiver, since both are signature-verified | ||
| # inbound deliveries, and one level deeper because Slack configures a Request URL per | ||
| # feature: Event Subscriptions and Slash Commands are separate URLs with their own | ||
| # payload shapes, and they belong beside this one rather than sharing it. | ||
| router = APIRouter(prefix="/webhooks/slack") | ||
|
|
||
|
|
||
| @router.post( | ||
| "/interactions", | ||
| status_code=status.HTTP_200_OK, | ||
| dependencies=[Depends(require_slack_signature)], | ||
| ) | ||
| async def slack_interactions(request: Request) -> Response: | ||
| form = await request.form() | ||
| event = BlockActionsEvent.model_validate_json(form["payload"]) | ||
| log.info( | ||
| "Slack action: %s by %s (%s)", | ||
| event.trigger_id, | ||
| event.user.username, | ||
| event.user.id, | ||
| ) | ||
|
|
||
| if len(event.actions) != 1: | ||
| raise ValueError( | ||
| "Expected exactly one action in a click delivery, got %d" | ||
| % len(event.actions) | ||
| ) | ||
|
|
||
| action = event.actions[0] | ||
| match action.value.type: | ||
| case "start_agent_run": | ||
| client = get_hackbot_client() | ||
| client.trigger_run(action.value.agent_name, action.value.inputs) | ||
| case _: | ||
| raise ValueError("Unsupported action type: %s" % action.value.type) | ||
|
|
||
| return Response(status_code=status.HTTP_200_OK) |
This file contains hidden or 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,72 @@ | ||
| """Slack interaction payload handling. | ||
|
|
||
| The models mirror Slack's payload, so each one can be read against the reference | ||
| side by side, and only the fields the receiver actually uses | ||
| are declared. Everything else a delivery carries is ignored. | ||
|
|
||
| https://docs.slack.dev/reference/interaction-payloads/block_actions-payload | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Any, Literal | ||
|
|
||
| from pydantic import BaseModel, Field, Json | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class User(BaseModel): | ||
| """Who clicked.""" | ||
|
|
||
| id: str | ||
| # Only ever logged, so a delivery without it is still a click worth acting on. | ||
| username: str | None = None | ||
|
|
||
|
|
||
| class Channel(BaseModel): | ||
| """Where the message they clicked is.""" | ||
|
|
||
| id: str | ||
|
|
||
|
|
||
| class Message(BaseModel): | ||
| """The message they clicked, identified for a later ``chat.update``.""" | ||
|
|
||
| ts: str | ||
|
|
||
|
|
||
| class ActionValue(BaseModel): | ||
| type: Literal["start_agent_run"] | ||
| agent_name: str | ||
| params: dict[str, Any] = Field(default_factory=dict) | ||
|
|
||
|
|
||
| class Action(BaseModel): | ||
| """The element that was clicked.""" | ||
|
|
||
| action_id: str | ||
| # `Json` decodes the value the drawing side put on the button and checks it is | ||
| # an object, so a button carrying something else fails here rather than at the | ||
| # first use of it. | ||
| value: Json[ActionValue] | ||
|
|
||
|
|
||
| class BlockActionsEvent(BaseModel): | ||
| """A click on one button of a message this app posted. | ||
|
|
||
| Clicks on message elements are the only interaction this app handles. Slack sends | ||
| other types to the same URL (``view_submission`` when a modal is submitted, | ||
| ``block_suggestion`` for a select's options); this app posts nothing that produces | ||
| one, so a delivery carrying one is as unreadable as any other and fails the same | ||
| way. | ||
| """ | ||
|
|
||
| type: Literal["block_actions"] | ||
| user: User | ||
| channel: Channel | None = None | ||
| message: Message | None = None | ||
| actions: list[Action] = Field(default_factory=list) | ||
| response_url: str | None = None | ||
| trigger_id: str |
This file contains hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.