Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.

Commit a8bf5af

Browse files
nathanmyeeNathan Yeewhitdog47Copilot
authored
Manage participants from UI (#6087)
* Refactor participant flow to support cases and incidents * Add logging for participants added to conversations in cases * Update flows to remove participant from Slack * Add API endpoints for adding and removing participants * Add frontend logic to remove and add participants * Fix linting errors * Fix more linting errors * Check for user in channel before removing * Remove trailing spaces * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Nathan Yee <nathanmyee@gmail.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Nathan Yee <nathanmyee@gmail.com> * Update src/dispatch/static/dispatch/src/incident/ParticipantsTab.vue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Nathan Yee <nathanmyee@gmail.com> * Remove console log --------- Signed-off-by: Nathan Yee <nathanmyee@gmail.com> Co-authored-by: Nathan Yee <nyee@netflix.com> Co-authored-by: David Whittaker <84562015+whitdog47@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 5076fb3 commit a8bf5af

16 files changed

Lines changed: 661 additions & 16 deletions

File tree

src/dispatch/case/flows.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,38 @@ def case_remove_participant_flow(
183183
db_session=db_session,
184184
)
185185

186+
# we also try to remove the user from the Slack conversation
187+
try:
188+
slack_conversation_plugin = plugin_service.get_active_instance(
189+
db_session=db_session, project_id=case.project.id, plugin_type="conversation"
190+
)
191+
192+
if not slack_conversation_plugin:
193+
log.warning(f"{user_email} not updated. No conversation plugin enabled.")
194+
return
195+
196+
if not case.conversation:
197+
log.warning("No conversation enabled for this case.")
198+
return
199+
200+
slack_conversation_plugin.instance.remove_user(
201+
conversation_id=case.conversation.channel_id,
202+
user_email=user_email
203+
)
204+
205+
event_service.log_case_event(
206+
db_session=db_session,
207+
source=slack_conversation_plugin.plugin.title,
208+
description=f"{user_email} removed from conversation (channel ID: {case.conversation.channel_id})",
209+
case_id=case.id,
210+
type=EventType.participant_updated,
211+
)
212+
213+
log.info(f"Removed {user_email} from conversation in channel {case.conversation.channel_id}")
214+
215+
except Exception as e:
216+
log.exception(f"Failed to remove user from Slack conversation: {e}")
217+
186218

187219
def update_conversation(case: Case, db_session: Session) -> None:
188220
"""Updates external communication conversation."""

src/dispatch/case/views.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
case_delete_flow,
3939
case_escalated_create_flow,
4040
case_new_create_flow,
41+
case_remove_participant_flow,
4142
case_stable_create_flow,
4243
case_to_incident_endpoint_escalate_flow,
4344
case_triage_create_flow,
@@ -435,6 +436,53 @@ def join_case(
435436
)
436437

437438

439+
@router.delete(
440+
"/{case_id}/remove/{email}",
441+
summary="Removes an individual from a case.",
442+
dependencies=[Depends(PermissionsDependency([CaseEditPermission]))],
443+
)
444+
def remove_participant_from_case(
445+
db_session: DbSession,
446+
organization: OrganizationSlug,
447+
case_id: PrimaryKey,
448+
email: str,
449+
current_case: CurrentCase,
450+
current_user: CurrentUser,
451+
background_tasks: BackgroundTasks,
452+
):
453+
"""Removes an individual from a case."""
454+
background_tasks.add_task(
455+
case_remove_participant_flow,
456+
email,
457+
case_id=current_case.id,
458+
db_session=db_session,
459+
)
460+
461+
462+
@router.post(
463+
"/{case_id}/add/{email}",
464+
summary="Adds an individual to a case.",
465+
dependencies=[Depends(PermissionsDependency([CaseEditPermission]))],
466+
)
467+
def add_participant_to_case(
468+
db_session: DbSession,
469+
organization: OrganizationSlug,
470+
case_id: PrimaryKey,
471+
email: str,
472+
current_case: CurrentCase,
473+
current_user: CurrentUser,
474+
background_tasks: BackgroundTasks,
475+
):
476+
"""Adds an individual to a case."""
477+
background_tasks.add_task(
478+
case_add_or_reactivate_participant_flow,
479+
email,
480+
case_id=current_case.id,
481+
organization_slug=organization,
482+
db_session=db_session,
483+
)
484+
485+
438486
@router.post(
439487
"/{case_id}/event",
440488
summary="Creates a custom event.",

src/dispatch/conversation/flows.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from dispatch.case.models import Case
66
from dispatch.conference.models import Conference
77
from dispatch.document.models import Document
8+
from dispatch.enums import EventType
89
from dispatch.event import service as event_service
910
from dispatch.incident.models import Incident
1011
from dispatch.messaging.strings import MessageType
@@ -490,8 +491,28 @@ def add_case_participants(
490491
case.conversation.thread_id,
491492
participant_emails,
492493
)
494+
495+
# log event for adding participants
496+
event_service.log_case_event(
497+
db_session=db_session,
498+
source=plugin.plugin.title,
499+
description=f"{', '.join(participant_emails)} added to conversation (channel ID: {case.conversation.channel_id}, thread ID: {case.conversation.thread_id})",
500+
case_id=case.id,
501+
type=EventType.participant_updated,
502+
)
503+
log.info(f"{', '.join(participant_emails)} added to conversation (channel ID: {case.conversation.channel_id}, thread ID: {case.conversation.thread_id})")
493504
elif case.has_channel:
494505
plugin.instance.add(case.conversation.channel_id, participant_emails)
506+
507+
# log event for adding participants
508+
event_service.log_case_event(
509+
db_session=db_session,
510+
source=plugin.plugin.title,
511+
description=f"{', '.join(participant_emails)} added to conversation (channel ID: {case.conversation.channel_id})",
512+
case_id=case.id,
513+
type=EventType.participant_updated,
514+
)
515+
log.info(f"{', '.join(participant_emails)} added to conversation (channel ID: {case.conversation.channel_id})")
495516
except Exception as e:
496517
event_service.log_case_event(
497518
db_session=db_session,

src/dispatch/incident/flows.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,29 @@ def incident_add_or_reactivate_participant_flow(
10781078
incident=incident, participant_emails=[user_email], db_session=db_session
10791079
)
10801080

1081+
# log event for adding the participant
1082+
try:
1083+
slack_conversation_plugin = plugin_service.get_active_instance(
1084+
db_session=db_session, project_id=incident.project.id, plugin_type="conversation"
1085+
)
1086+
1087+
if not slack_conversation_plugin:
1088+
log.warning(f"{user_email} not updated. No conversation plugin enabled.")
1089+
return
1090+
1091+
event_service.log_incident_event(
1092+
db_session=db_session,
1093+
source=slack_conversation_plugin.plugin.title,
1094+
description=f"{user_email} added to conversation (channel ID: {incident.conversation.channel_id})",
1095+
incident_id=incident.id,
1096+
type=EventType.participant_updated,
1097+
)
1098+
1099+
log.info(f"Added {user_email} to conversation in (channel ID: {incident.conversation.channel_id})")
1100+
1101+
except Exception as e:
1102+
log.exception(f"Failed to add user to Slack conversation: {e}")
1103+
10811104
# we announce the participant in the conversation
10821105
if send_announcement_message:
10831106
send_participant_announcement_message(
@@ -1153,3 +1176,35 @@ def incident_remove_participant_flow(
11531176
group_member=user_email,
11541177
db_session=db_session,
11551178
)
1179+
1180+
# we also try to remove the user from the Slack conversation
1181+
try:
1182+
slack_conversation_plugin = plugin_service.get_active_instance(
1183+
db_session=db_session, project_id=incident.project.id, plugin_type="conversation"
1184+
)
1185+
1186+
if not slack_conversation_plugin:
1187+
log.warning(f"{user_email} not updated. No conversation plugin enabled.")
1188+
return
1189+
1190+
if not incident.conversation:
1191+
log.warning("No conversation enabled for this incident.")
1192+
return
1193+
1194+
slack_conversation_plugin.instance.remove_user(
1195+
conversation_id=incident.conversation.channel_id,
1196+
user_email=user_email
1197+
)
1198+
1199+
event_service.log_incident_event(
1200+
db_session=db_session,
1201+
source=slack_conversation_plugin.plugin.title,
1202+
description=f"{user_email} removed from conversation (channel ID: {incident.conversation.channel_id})",
1203+
incident_id=incident.id,
1204+
type=EventType.participant_updated,
1205+
)
1206+
1207+
log.info(f"Removed {user_email} from conversation in channel {incident.conversation.channel_id}")
1208+
1209+
except Exception as e:
1210+
log.exception(f"Failed to remove user from Slack conversation: {e}")

src/dispatch/incident/views.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
incident_create_resources_flow,
4040
incident_create_stable_flow,
4141
incident_delete_flow,
42+
incident_remove_participant_flow,
4243
incident_subscribe_participant_flow,
4344
incident_update_flow,
4445
)
@@ -292,6 +293,52 @@ def subscribe_to_incident(
292293
)
293294

294295

296+
@router.delete(
297+
"/{incident_id}/remove/{email}",
298+
summary="Removes an individual from an incident.",
299+
dependencies=[Depends(PermissionsDependency([IncidentEditPermission]))],
300+
)
301+
def remove_participant_from_incident(
302+
db_session: DbSession,
303+
organization: OrganizationSlug,
304+
incident_id: PrimaryKey,
305+
email: str,
306+
current_incident: CurrentIncident,
307+
current_user: CurrentUser,
308+
background_tasks: BackgroundTasks,
309+
):
310+
"""Removes an individual from an incident."""
311+
background_tasks.add_task(
312+
incident_remove_participant_flow,
313+
email,
314+
incident_id=current_incident.id,
315+
organization_slug=organization,
316+
)
317+
318+
319+
@router.post(
320+
"/{incident_id}/add/{email}",
321+
summary="Adds an individual to an incident.",
322+
dependencies=[Depends(PermissionsDependency([IncidentEditPermission]))],
323+
)
324+
def add_participant_to_incident(
325+
db_session: DbSession,
326+
organization: OrganizationSlug,
327+
incident_id: PrimaryKey,
328+
email: str,
329+
current_incident: CurrentIncident,
330+
current_user: CurrentUser,
331+
background_tasks: BackgroundTasks,
332+
):
333+
"""Adds an individual to an incident."""
334+
background_tasks.add_task(
335+
incident_add_or_reactivate_participant_flow,
336+
email,
337+
incident_id=current_incident.id,
338+
organization_slug=organization,
339+
)
340+
341+
295342
@router.post(
296343
"/{incident_id}/report/tactical",
297344
summary="Creates a tactical report.",

src/dispatch/participant/flows.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,18 +188,25 @@ def inactivate_participant(user_email: str, subject: Subject, db_session: Sessio
188188

189189

190190
def reactivate_participant(
191-
user_email: str, incident: Incident, db_session: Session, service_id: int = None
191+
user_email: str, subject: Subject, db_session: Session, service_id: int = None
192192
):
193193
"""Reactivates a participant."""
194-
participant = participant_service.get_by_incident_id_and_email(
195-
db_session=db_session, incident_id=incident.id, email=user_email
196-
)
194+
subject_type = get_table_name_by_class_instance(subject)
195+
196+
if subject_type == "case":
197+
participant = participant_service.get_by_case_id_and_email(
198+
db_session=db_session, case_id=subject.id, email=user_email
199+
)
200+
else:
201+
participant = participant_service.get_by_incident_id_and_email(
202+
db_session=db_session, incident_id=subject.id, email=user_email
203+
)
197204

198205
if not participant:
199-
log.debug(f"{user_email} is not an inactive participant of {incident.name} incident.")
206+
log.debug(f"{user_email} is not an inactive participant of {subject.name} {subject_type}.")
200207
return False
201208

202-
log.debug(f"Reactivating {participant.individual.name} on {incident.name} incident...")
209+
log.debug(f"Reactivating {participant.individual.name} on {subject.name} {subject_type}...")
203210

204211
# we get the last active role
205212
participant_role = participant_role_service.get_last_active_role(
@@ -219,11 +226,11 @@ def reactivate_participant(
219226
db_session.add(participant)
220227
db_session.commit()
221228

222-
event_service.log_incident_event(
229+
event_service.log_subject_event(
230+
subject=subject,
223231
db_session=db_session,
224232
source="Dispatch Core App",
225233
description=f"{participant.individual.name} has been reactivated",
226-
incident_id=incident.id,
227234
type=EventType.participant_updated,
228235
)
229236

src/dispatch/plugins/dispatch_slack/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class SlackAPIErrorCode(DispatchEnum):
3737
FATAL_ERROR = "fatal_error"
3838
IS_ARCHIVED = "is_archived" # Channel is archived
3939
MISSING_SCOPE = "missing_scope"
40+
NOT_IN_CHANNEL = "not_in_channel"
4041
ORG_USER_NOT_IN_TEAM = "org_user_not_in_team"
4142
USERS_NOT_FOUND = "users_not_found"
4243
USER_IN_CHANNEL = "user_in_channel"

src/dispatch/plugins/dispatch_slack/service.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,13 @@ def add_conversation_bookmark(
351351

352352
def remove_member_from_channel(client: WebClient, conversation_id: str, user_id: str) -> None:
353353
"""Removes a user from a channel."""
354+
log.info(f"Attempting to remove user {user_id} from channel {conversation_id}")
355+
356+
# Check if user is actually in the channel before attempting removal
357+
if not is_member_in_channel(client, conversation_id, user_id):
358+
log.info(f"User {user_id} is not in channel {conversation_id}, skipping removal")
359+
return
360+
354361
return make_call(
355362
client, SlackAPIPostEndpoints.conversations_kick, channel=conversation_id, user=user_id
356363
)
@@ -734,3 +741,41 @@ def create_genai_message_metadata_blocks(
734741
)
735742
blocks.append(Divider())
736743
return Message(blocks=blocks).build()["blocks"]
744+
745+
746+
def is_member_in_channel(client: WebClient, conversation_id: str, user_id: str) -> bool:
747+
"""
748+
Check if a user is a member of a specific Slack channel.
749+
750+
Args:
751+
client (WebClient): A Slack WebClient object used to interact with the Slack API.
752+
conversation_id (str): The ID of the Slack channel/conversation to check.
753+
user_id (str): The ID of the user to check for membership.
754+
755+
Returns:
756+
bool: True if the user is a member of the channel, False otherwise.
757+
758+
Raises:
759+
SlackApiError: If there's an error from the Slack API (e.g., channel not found).
760+
"""
761+
try:
762+
response = make_call(
763+
client,
764+
SlackAPIGetEndpoints.conversations_members,
765+
channel=conversation_id,
766+
)
767+
768+
# Check if the user_id is in the list of members
769+
return user_id in response.get("members", [])
770+
771+
except SlackApiError as e:
772+
if e.response["error"] == SlackAPIErrorCode.CHANNEL_NOT_FOUND:
773+
log.warning(f"Channel {conversation_id} not found when checking membership for user {user_id}")
774+
return False
775+
elif e.response["error"] == SlackAPIErrorCode.USER_NOT_IN_CHANNEL:
776+
# The bot itself is not in the channel, so it can't check membership
777+
log.warning(f"Bot not in channel {conversation_id}, cannot check membership for user {user_id}")
778+
return False
779+
else:
780+
log.exception(f"Error checking channel membership for user {user_id} in channel {conversation_id}: {e}")
781+
raise

0 commit comments

Comments
 (0)