Skip to content

Commit

Permalink
feat(slack): add users to threaded case on mention (#5530)
Browse files Browse the repository at this point in the history
  • Loading branch information
whitdog47 authored Nov 25, 2024
1 parent e8f7b7d commit 4d78bcf
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/dispatch/plugins/dispatch_slack/case/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class CaseNotificationActions(DispatchEnum):
triage = "case-notification-triage"
user_mfa = "case-notification-user-mfa"
invite_user_case = ConversationButtonActions.invite_user_case
do_nothing = "case-do-not-add-user"
add_user = "case-add-user"


class CasePaginateActions(DispatchEnum):
Expand Down
139 changes: 139 additions & 0 deletions src/dispatch/plugins/dispatch_slack/case/interactive.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
import re
from datetime import datetime, timedelta, timezone
from functools import partial
from uuid import UUID
Expand All @@ -12,6 +13,7 @@
Divider,
Input,
MarkdownText,
Message,
Modal,
Section,
UsersSelect,
Expand Down Expand Up @@ -80,6 +82,7 @@
)
from dispatch.plugins.dispatch_slack.middleware import (
action_context_middleware,
add_user_middleware,
button_context_middleware,
command_context_middleware,
configuration_middleware,
Expand All @@ -92,6 +95,7 @@
)
from dispatch.plugins.dispatch_slack.modals.common import send_success_modal
from dispatch.plugins.dispatch_slack.models import (
AddUserMetadata,
CaseSubjects,
FormData,
FormMetadata,
Expand Down Expand Up @@ -1652,6 +1656,141 @@ def handle_create_channel_event(
)


def extract_mentioned_users(text: str) -> list[str]:
"""Extracts mentioned users from a message."""
return re.findall(r"<@(\w+)>", text)


def format_emails(emails: list[str]) -> str:
"""Format a list of names into a string with commas and 'and' before the last name."""
usernames = [email.split("@")[0] for email in emails]

if not usernames:
return ""
elif len(usernames) == 1:
return f"@{usernames[0]}"
elif len(usernames) == 2:
return f"@{usernames[0]} and @{usernames[1]}"
else:
return ", ".join(f"@{username}" for username in usernames[:-1]) + f", and @{usernames[-1]}"


@message_dispatcher.add(
subject=CaseSubjects.case, exclude={"subtype": ["channel_join", "group_join"]}
) # we ignore user channel and group join messages
def handle_user_mention(
ack: Ack,
context: BoltContext,
client: WebClient,
db_session: Session,
payload: dict,
) -> None:
"""Handles user posted message events."""
ack()

case = case_service.get(db_session=db_session, case_id=context["subject"].id)
if not case or case.dedicated_channel:
# we do not need to handle mentions for cases with dedicated channels
return

mentioned_users = extract_mentioned_users(payload["text"])
users_not_in_case = []
for user_id in mentioned_users:
user_email = dispatch_slack_service.get_user_email(client, user_id)
if not participant_service.get_by_case_id_and_email(
db_session=db_session, case_id=context["subject"].id, email=user_email
):
users_not_in_case.append(user_email)

if users_not_in_case:
# send a private message to the user who posted the message to see
# if they want to add the mentioned user(s) to the case
button_metadata = AddUserMetadata(
**dict(context["subject"]),
users=users_not_in_case,
).json()
blocks = [
Section(
text=f"You mentioned {format_emails(users_not_in_case)}, but they're not in this case."
),
Actions(
block_id=DefaultBlockIds.add_user_actions,
elements=[
Button(
text="Add Them",
style="primary",
action_id=CaseNotificationActions.add_user,
value=button_metadata,
),
Button(
text="Do Nothing",
action_id=CaseNotificationActions.do_nothing,
),
],
),
]
blocks = Message(blocks=blocks).build()["blocks"]
client.chat_postEphemeral(
channel=payload["channel"],
thread_ts=payload.get("thread_ts"),
user=payload["user"],
blocks=blocks,
)


@app.action(
CaseNotificationActions.add_user,
middleware=[add_user_middleware, button_context_middleware, db_middleware, user_middleware],
)
def add_users_to_case(
ack: Ack,
db_session: Session,
context: BoltContext,
respond: Respond,
):
ack()

case_id = context["subject"].id

case = case_service.get(db_session=db_session, case_id=case_id)
if not case:
log.error(f"Could not find case with id: {case_id}")
return

users = context["users"]
if users:
for user_email in users:
conversation_flows.add_case_participants(case, [user_email], db_session)
participant = participant_service.get_by_case_id_and_email(
db_session=db_session, case_id=case.id, email=user_email
)
if not participant:
participant_flows.add_participant(
user_email,
case,
db_session,
roles=[ParticipantRoleType.participant],
)

# Delete the ephemeral message
respond(delete_original=True)


@app.action(CaseNotificationActions.do_nothing)
def handle_do_nothing_button(
ack: Ack,
respond: Respond,
):
# Acknowledge the action
ack()

try:
# Delete the ephemeral message
respond(delete_original=True)
except SlackApiError as e:
log.error(f"Error deleting ephemeral message: {e.response['error']}")


@app.action(
CaseNotificationActions.join_incident,
middleware=[button_context_middleware, db_middleware, user_middleware],
Expand Down
1 change: 1 addition & 0 deletions src/dispatch/plugins/dispatch_slack/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@


class DefaultBlockIds(DispatchEnum):
add_user_actions = "add-user-actions"
date_picker_input = "date-picker-input"
description_input = "description-input"
hour_picker_input = "hour-picker-input"
Expand Down
8 changes: 8 additions & 0 deletions src/dispatch/plugins/dispatch_slack/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,14 @@ def command_context_middleware(
next()


def add_user_middleware(payload: dict, context: BoltContext, next: Callable):
"""Attempts to determine the user to add to the incident."""
value = payload.get("value")
if value:
context["users"] = json.loads(value).get("users")
next()


def db_middleware(context: BoltContext, next: Callable):
if not context.get("subject"):
slug = get_default_org_slug()
Expand Down
4 changes: 4 additions & 0 deletions src/dispatch/plugins/dispatch_slack/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class SubjectMetadata(BaseModel):
thread_id: Optional[str]


class AddUserMetadata(SubjectMetadata):
users: list[str]


class EngagementMetadata(SubjectMetadata):
signal_instance_id: str
engagement_id: int
Expand Down

0 comments on commit 4d78bcf

Please sign in to comment.