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
This commit is contained in:
MMK21 2025-10-21 21:51:43 +01:00 committed by GitHub
parent feb251de91
commit 8a83580fe8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 81 additions and 1 deletions

View file

@ -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"],

View file

@ -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)

View file

@ -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")