-
-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Add incbot command handlers #134
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
Merged
Merged
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
01c2052
Add signing_secret to Slack config
spalmurray f124441
Add slack_app Django app with Bolt wiring and /inc help command
spalmurray 2efed7a
Add Datadog metrics instrumentation for slash commands
spalmurray d2c55fa
Add signing_secret to CI config and fix ruff lint
spalmurray b0db045
tweaks
spalmurray de81cf3
Rename slack app in help command
spalmurray 0ed965e
Remove HTTP-based slack event handling in favor of Socket Mode
spalmurray 354e635
Mock Slack auth_test in tests instead of disabling token verification
spalmurray 9131728
Change /inc to /ft (/testinc -> /ft-test)
spalmurray 252db4d
Scope Slack auth_test patch and normalize metric tags
cursoragent cd94441
Add SlackService methods for channel management
spalmurray 096ef3f
Add incident lifecycle hooks and wire into serializer
spalmurray 6b9562a
Add /inc new command with modal for creating incidents
spalmurray 216b9cb
Add get_incident_from_channel helper and on_title_changed hook
spalmurray 534bcc6
Add mitigated, resolved, reopen, severity, and subject command handlers
spalmurray 8da039c
Wire new command handlers into bolt.py routing and update help text
spalmurray 3914408
Add tests for channel command handlers
spalmurray 3ce2c2d
Address warden feedaback
spalmurray e1ceb7d
Handle validation errors and edge cases in command handlers
spalmurray 9aad8b4
Fix handle_inc imports to use renamed handle_command
spalmurray 27090ed
Use get_bolt_app() instead of nonexistent bolt_app in mitigated and r…
spalmurray 296f264
Remove unused get_channel_history from SlackService
spalmurray 4bdf867
Use IncidentStatus enum instead of raw strings in handlers
spalmurray b09c6ba
Use more precise URL filter in get_incident_from_channel
spalmurray 2be7e4c
Move module-level mock_auth to a proper pytest fixture
spalmurray efe5f7c
Add /ft update command with modal for editing incident metadata
spalmurray 9e4aefb
Add tests for /ft update command handler
spalmurray d8bad00
Add alias notes to help text
spalmurray f70653d
Add stub handlers for statuspage and dumpslack commands
spalmurray d6cbfb7
Fix severity fallback, document all aliases in help, remove signing_s…
spalmurray daac0d3
Notify user when mitigation notes fail to save
spalmurray 8513540
Simplify help text by inlining aliases
spalmurray cc713c4
Fix CI failures in update_incident handler and slack app tests
spalmurray c05125f
Add /ft captain command to set incident captain from Slack
spalmurray b3aa43c
Reorder help text and simplify stub messages
spalmurray ec2e8bc
Add args to help usage line
spalmurray e958c3d
Fix stub command test assertions to match simplified messages
spalmurray c78df9c
Split handler tests into individual files
spalmurray 0b713b0
Accept both mention format and plain user ID for captain command
spalmurray 2b21ec0
Add debug logging for slash command parsing
spalmurray c79f7b0
Convert /ft captain from inline command to modal with user picker
spalmurray 4acea15
Add captain selector to the new incident modal
spalmurray 96c2ee9
Add captain selector to the update incident modal
spalmurray 7bd340a
Remove debug logging for slash command parsing
spalmurray 5b3fd46
Remove description append from mitigated handler, just post to Slack
spalmurray a6075ad
Match opsbot Slack message format for mitigated handler
spalmurray 111a999
Update captain help text to reflect modal-based flow
spalmurray d04cee0
Couple review tweaks
spalmurray e3200fc
Tweak modals
spalmurray 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import logging | ||
| from typing import Any | ||
|
|
||
| from firetower.auth.models import ExternalProfileType | ||
| from firetower.auth.services import get_or_create_user_from_slack_id | ||
| from firetower.incidents.serializers import IncidentWriteSerializer | ||
| from firetower.slack_app.handlers.utils import get_incident_from_channel | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _build_captain_modal( | ||
| incident_number: str, channel_id: str, captain_slack_id: str | None | ||
| ) -> dict: | ||
| captain_element: dict = { | ||
| "type": "users_select", | ||
| "action_id": "captain_select", | ||
| "placeholder": {"type": "plain_text", "text": "Select incident captain"}, | ||
| } | ||
| if captain_slack_id: | ||
| captain_element["initial_user"] = captain_slack_id | ||
|
|
||
| return { | ||
| "type": "modal", | ||
| "callback_id": "captain_incident_modal", | ||
| "private_metadata": channel_id, | ||
| "title": {"type": "plain_text", "text": incident_number}, | ||
| "submit": {"type": "plain_text", "text": "Update"}, | ||
| "close": {"type": "plain_text", "text": "Cancel"}, | ||
| "blocks": [ | ||
| { | ||
| "type": "input", | ||
| "block_id": "captain_block", | ||
| "optional": True, | ||
| "element": captain_element, | ||
| "label": {"type": "plain_text", "text": "Incident Captain"}, | ||
| }, | ||
| ], | ||
| } | ||
|
|
||
|
|
||
| def handle_captain_command(ack: Any, body: dict, command: dict, respond: Any) -> None: | ||
| ack() | ||
| channel_id = body.get("channel_id", "") | ||
| incident = get_incident_from_channel(channel_id) | ||
| if not incident: | ||
| respond("Could not find an incident associated with this channel.") | ||
| return | ||
|
|
||
| trigger_id = body.get("trigger_id") | ||
| if not trigger_id: | ||
| respond("Could not open modal — missing trigger_id.") | ||
| return | ||
|
|
||
| captain_slack_id = None | ||
| if incident.captain: | ||
| slack_profile = incident.captain.external_profiles.filter( | ||
| type=ExternalProfileType.SLACK | ||
| ).first() | ||
| if slack_profile: | ||
| captain_slack_id = slack_profile.external_id | ||
|
|
||
| from firetower.slack_app.bolt import get_bolt_app # noqa: PLC0415 | ||
|
|
||
| get_bolt_app().client.views_open( | ||
| trigger_id=trigger_id, | ||
| view=_build_captain_modal( | ||
| incident.incident_number, channel_id, captain_slack_id | ||
| ), | ||
|
Check warning on line 69 in src/firetower/slack_app/handlers/captain.py
|
||
| ) | ||
|
|
||
|
|
||
| def handle_captain_submission(ack: Any, body: dict, view: dict, client: Any) -> None: | ||
| values = view.get("state", {}).get("values", {}) | ||
| channel_id = view.get("private_metadata", "") | ||
|
|
||
| captain_slack_id = ( | ||
| values.get("captain_block", {}).get("captain_select", {}).get("selected_user") | ||
| ) | ||
|
|
||
| ack() | ||
|
|
||
| incident = get_incident_from_channel(channel_id) | ||
| if not incident: | ||
| logger.error("Captain submission: no incident for channel %s", channel_id) | ||
| return | ||
|
|
||
| if not captain_slack_id: | ||
| client.chat_postMessage( | ||
| channel=channel_id, | ||
| text=f"*{incident.incident_number}* captain was not changed.", | ||
| ) | ||
| return | ||
|
|
||
| captain_user = get_or_create_user_from_slack_id(captain_slack_id) | ||
| if not captain_user: | ||
| logger.error( | ||
| "Could not resolve Slack user %s to a Firetower user", captain_slack_id | ||
| ) | ||
| client.chat_postMessage( | ||
| channel=channel_id, | ||
| text="Failed to resolve the selected captain to a Firetower user.", | ||
| ) | ||
| return | ||
|
|
||
| serializer = IncidentWriteSerializer( | ||
| instance=incident, data={"captain": captain_user.email}, partial=True | ||
| ) | ||
| if not serializer.is_valid(): | ||
| logger.error("Captain update failed: %s", serializer.errors) | ||
| client.chat_postMessage( | ||
| channel=channel_id, | ||
| text=f"Failed to update captain: {serializer.errors}", | ||
| ) | ||
| return | ||
|
|
||
| serializer.save() | ||
|
spalmurray marked this conversation as resolved.
|
||
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,7 @@ | ||
| from typing import Any | ||
|
|
||
|
|
||
| def handle_dumpslack_command(ack: Any, command: dict, respond: Any) -> None: | ||
| ack() | ||
| cmd = command.get("command", "/ft") | ||
| respond(f"`{cmd} dumpslack` is not yet implemented.") |
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 |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import logging | ||
| from typing import Any | ||
|
|
||
| from django.conf import settings | ||
|
|
||
| from firetower.incidents.models import IncidentStatus | ||
| from firetower.incidents.serializers import IncidentWriteSerializer | ||
| from firetower.slack_app.handlers.utils import get_incident_from_channel | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _build_mitigated_modal(incident_number: str, channel_id: str) -> dict: | ||
| return { | ||
| "type": "modal", | ||
| "callback_id": "mitigated_incident_modal", | ||
| "private_metadata": channel_id, | ||
| "title": {"type": "plain_text", "text": incident_number}, | ||
| "submit": {"type": "plain_text", "text": "Submit"}, | ||
| "close": {"type": "plain_text", "text": "Cancel"}, | ||
| "blocks": [ | ||
| { | ||
| "type": "section", | ||
| "text": { | ||
| "type": "mrkdwn", | ||
| "text": "Mark this incident as mitigated. Please provide the current impact and any remaining action items.", | ||
| }, | ||
| }, | ||
| { | ||
| "type": "input", | ||
| "block_id": "impact_block", | ||
| "element": { | ||
| "type": "plain_text_input", | ||
| "action_id": "impact_update", | ||
| "multiline": True, | ||
| "placeholder": { | ||
| "type": "plain_text", | ||
| "text": "What is the current impact after mitigation?", | ||
| }, | ||
| }, | ||
| "label": { | ||
| "type": "plain_text", | ||
| "text": "Current impact post-mitigation", | ||
| }, | ||
| }, | ||
| { | ||
| "type": "input", | ||
| "block_id": "todo_block", | ||
| "element": { | ||
| "type": "plain_text_input", | ||
| "action_id": "todo_update", | ||
| "multiline": True, | ||
| "placeholder": { | ||
| "type": "plain_text", | ||
| "text": "What still needs to be done?", | ||
| }, | ||
| }, | ||
| "label": {"type": "plain_text", "text": "Remaining action items"}, | ||
| }, | ||
| ], | ||
| } | ||
|
|
||
|
|
||
| def handle_mitigated_command(ack: Any, body: dict, command: dict, respond: Any) -> None: | ||
| ack() | ||
| channel_id = body.get("channel_id", "") | ||
| incident = get_incident_from_channel(channel_id) | ||
| if not incident: | ||
| respond("Could not find an incident associated with this channel.") | ||
| return | ||
|
|
||
| trigger_id = body.get("trigger_id") | ||
| if not trigger_id: | ||
| respond("Could not open modal — missing trigger_id.") | ||
| return | ||
|
|
||
| from firetower.slack_app.bolt import get_bolt_app # noqa: PLC0415 | ||
|
|
||
| get_bolt_app().client.views_open( | ||
| trigger_id=trigger_id, | ||
| view=_build_mitigated_modal(incident.incident_number, channel_id), | ||
| ) | ||
|
|
||
|
|
||
| def handle_mitigated_submission(ack: Any, body: dict, view: dict, client: Any) -> None: | ||
| ack() | ||
| values = view.get("state", {}).get("values", {}) | ||
| channel_id = view.get("private_metadata", "") | ||
|
|
||
| impact = values.get("impact_block", {}).get("impact_update", {}).get("value", "") | ||
| todo = values.get("todo_block", {}).get("todo_update", {}).get("value", "") | ||
|
|
||
| incident = get_incident_from_channel(channel_id) | ||
| if not incident: | ||
| logger.error("Mitigated submission: no incident for channel %s", channel_id) | ||
| return | ||
|
|
||
| serializer = IncidentWriteSerializer( | ||
| instance=incident, data={"status": IncidentStatus.MITIGATED}, partial=True | ||
|
sentry-warden[bot] marked this conversation as resolved.
|
||
| ) | ||
| if not serializer.is_valid(): | ||
| logger.error("Mitigated status update failed: %s", serializer.errors) | ||
| client.chat_postMessage( | ||
| channel=channel_id, | ||
| text=f"Failed to update incident status: {serializer.errors}", | ||
| ) | ||
| return | ||
| serializer.save() | ||
|
|
||
| incident_url = f"{settings.FIRETOWER_BASE_URL}/{incident.incident_number}" | ||
| client.chat_postMessage( | ||
| channel=channel_id, | ||
| text=( | ||
| f"<{incident_url}|{incident.incident_number}> has been marked Mitigated.\n" | ||
| f"*Current Impact*:\n" | ||
| f"```{impact}```\n" | ||
| f"*Remaining Action Items*:\n" | ||
| f"```{todo}```" | ||
| ), | ||
| ) | ||
Oops, something went wrong.
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.