From 8a83580fe83ddf2ac7225d982891713582ed025f Mon Sep 17 00:00:00 2001 From: MMK21 <50421330+MMK21Hub@users.noreply.github.com> Date: Tue, 21 Oct 2025 21:51:43 +0100 Subject: [PATCH] Delete bot messages and database entries if a thread is deleted (#69) * Differentiate between message creation and deletion * Add a comment to explain thread_broadcast * If top-level message gets deleted, then delete bot messages * Ensure top-level messages being deleted don't cause errors It would previously freak out when the tombstone message gets deleted :P * Remove deleted tickets from DB * Actually delete the correct ticket * Ensure unexpected SlackApiErrors are re-raised * refactor: Move deleted message handling into its own file --- nephthys/events/message.py | 1 + nephthys/events/message_deletion.py | 70 +++++++++++++++++++++++++++++ nephthys/utils/slack.py | 11 ++++- 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 nephthys/events/message_deletion.py diff --git a/nephthys/events/message.py b/nephthys/events/message.py index 32c47de..eaa5414 100644 --- a/nephthys/events/message.py +++ b/nephthys/events/message.py @@ -28,6 +28,7 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient): db_user = await env.db.user.find_first(where={"slackId": user}) + # Messages sent in a thread with the "send to channel" checkbox checked if event.get("subtype") == "thread_broadcast" and not (db_user and db_user.helper): await client.chat_delete( channel=event["channel"], diff --git a/nephthys/events/message_deletion.py b/nephthys/events/message_deletion.py new file mode 100644 index 0000000..40775a3 --- /dev/null +++ b/nephthys/events/message_deletion.py @@ -0,0 +1,70 @@ +import logging +from typing import Any +from typing import Dict + +import slack_sdk.errors +from slack_sdk.web.async_client import AsyncWebClient + +from nephthys.utils.env import env +from nephthys.utils.logging import send_heartbeat + + +async def handle_question_deletion( + client: AsyncWebClient, channel: str, deleted_msg: Dict[str, Any] +) -> None: + """Handle deletion of a top-level question message in the help channel. + + - If the thread has non-bot messages, do nothing. + - Otherwise, deletes the bot messages in the thread and removes the ticket from the DB. + - This behaviour is similar to `?thread`, except it removes the ticket from the DB instead of marking it as resolved. + """ + try: + thread_history = await client.conversations_replies( + channel=channel, ts=deleted_msg["ts"] + ) + except slack_sdk.errors.SlackApiError as e: + if e.response.get("error") == "thread_not_found": + # Nothing to clean up; we good + return + else: + raise e + bot_info = await env.slack_client.auth_test() + bot_user_id = bot_info.get("user_id") + messages_to_delete = [] + for msg in thread_history["messages"]: + if msg["user"] == bot_user_id: + messages_to_delete.append(msg) + elif msg["ts"] != deleted_msg["ts"]: + # Don't clear the thread if there are non-bot messages in there + return + + # Delete ticket from DB + await env.db.ticket.delete(where={"msgTs": deleted_msg["ts"]}) + + # Delete messages + await send_heartbeat( + f"Removing my {len(messages_to_delete)} message(s) in a thread because the question was deleted." + ) + for msg in messages_to_delete: + await client.chat_delete( + channel=channel, + ts=msg["ts"], + ) + + +async def on_message_deletion(event: Dict[str, Any], client: AsyncWebClient) -> None: + """Handles the two types of message deletion events + (i.e. a message being turned into a tombstone, and a message being fully deleted).""" + if event.get("subtype") == "message_deleted": + # This means the message has been completely deleted with out leaving a "tombstone", so no cleanup to do + return + deleted_msg = event.get("previous_message") + if not deleted_msg: + logging.warning("No previous_message found in message deletion event") + return + is_top_level_message = ( + "thread_ts" not in deleted_msg or deleted_msg["ts"] == deleted_msg["thread_ts"] + ) + if is_top_level_message: + # A question (i.e. top-level message in help channel) has been deleted + await handle_question_deletion(client, event["channel"], deleted_msg) diff --git a/nephthys/utils/slack.py b/nephthys/utils/slack.py index 2b51e94..8bcc861 100644 --- a/nephthys/utils/slack.py +++ b/nephthys/utils/slack.py @@ -16,6 +16,7 @@ from nephthys.events.app_home_opened import open_app_home from nephthys.events.channel_join import channel_join from nephthys.events.channel_left import channel_left from nephthys.events.message import on_message +from nephthys.events.message_deletion import on_message_deletion from nephthys.options.tags import get_tags from nephthys.utils.env import env @@ -24,8 +25,16 @@ app = AsyncApp(token=env.slack_bot_token, signing_secret=env.slack_signing_secre @app.event("message") async def handle_message(event: Dict[str, Any], client: AsyncWebClient): + print(event) + is_message_deletion = ( + event.get("subtype") == "message_changed" + and event["message"]["subtype"] == "tombstone" + ) or event.get("subtype") == "message_deleted" if event["channel"] == env.slack_help_channel: - await on_message(event, client) + if is_message_deletion: + await on_message_deletion(event, client) + else: + await on_message(event, client) @app.action("mark_resolved")