Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1e44814
Update DB when unthreaded messages are deleted
MMK21Hub Oct 21, 2025
7cae9d8
Add required library libatomic1 to Dockerfile (#71)
MMK21Hub Oct 22, 2025
695698e
Handle quick message deletions
MMK21Hub Oct 22, 2025
832b846
Create a system for tracking bot messages attached to tickets
MMK21Hub Oct 22, 2025
54dcc03
Ensure resolved message is added to userFacingMsgs
MMK21Hub Oct 22, 2025
e498af1
Add a reply_to_ticket() helper for updating the DB
RandomSearch18 Oct 23, 2025
e8671f9
Fix type errors in reply_to_ticket()
RandomSearch18 Oct 23, 2025
5998a02
Use reply_to_ticket when resolving tickets
RandomSearch18 Oct 23, 2025
802dade
Fix type errors in message.py
RandomSearch18 Oct 23, 2025
01c17a7
Create delete_replies_to_ticket()
RandomSearch18 Oct 23, 2025
acbdf63
Remove unused parameter in delete_replies_to_ticket
RandomSearch18 Oct 23, 2025
daf41a3
Rename BotMessage.msgTs to BotMessage.ts
RandomSearch18 Oct 23, 2025
2aec411
Write a stub delete_and_clean_up_ticket function
RandomSearch18 Oct 23, 2025
a6f585b
Partially implement delete_and_clean_up_ticket
RandomSearch18 Oct 23, 2025
cfb3164
Delete "ticket" message in delete_and_clean_up_ticket()
MMK21Hub Oct 23, 2025
efa5b69
Use the new ticket methods where appropriate
MMK21Hub Oct 23, 2025
5a8d5f2
Make function name clearer ("delete_bot_replies")
MMK21Hub Oct 23, 2025
a0bb7bc
Log if a question is deleted but not present in DB
MMK21Hub Oct 23, 2025
bd0363a
Fix error when normal messages are deleted
MMK21Hub Oct 23, 2025
e4526fe
Actually include userFacingMsgs in the query when deleting bot replies
MMK21Hub Oct 23, 2025
a81b67b
Add success heartbeat to delete queue
MMK21Hub Oct 24, 2025
35fb19a
Document the deletion queue needing workspace admin
MMK21Hub Oct 24, 2025
a69cffb
Don't use delete queue in ticket_methods.py
MMK21Hub Oct 24, 2025
1afe037
Fix deleting the backend message on ticket deletion
MMK21Hub Oct 24, 2025
e912655
Always preserve support threads with >3 bot messages
MMK21Hub Oct 24, 2025
ccebf87
Debug-log message event data instead of printing
MMK21Hub Oct 24, 2025
c7cb401
Use the reply_to_ticket util in macros
MMK21Hub Oct 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ADD . /app
WORKDIR /app

RUN apt update
RUN apt install -y curl
RUN apt install -y curl libatomic1

RUN uv python install
RUN uv sync --frozen
Expand Down
8 changes: 4 additions & 4 deletions nephthys/actions/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
from nephthys.utils.permissions import can_resolve
from nephthys.utils.ticket_methods import reply_to_ticket
from prisma.enums import TicketStatus


Expand Down Expand Up @@ -63,14 +64,13 @@ async def resolve(
return

if send_resolved_message:
await client.chat_postMessage(
channel=env.slack_help_channel,
await reply_to_ticket(
ticket=tkt,
client=client,
text=env.transcript.ticket_resolve.format(user_id=resolver)
if not stale
else env.transcript.ticket_resolve_stale.format(user_id=resolver),
thread_ts=ts,
)

if add_reaction:
await client.reactions_add(
channel=env.slack_help_channel,
Expand Down
86 changes: 56 additions & 30 deletions nephthys/events/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
from typing import Any
from typing import Dict

from slack_sdk.errors import SlackApiError
from slack_sdk.web.async_client import AsyncWebClient

from nephthys.macros import run_macro
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
from nephthys.utils.ticket_methods import delete_and_clean_up_ticket
from prisma.enums import TicketStatus

ALLOWED_SUBTYPES = ["file_share", "me_message", "thread_broadcast"]
Expand Down Expand Up @@ -46,18 +48,18 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):

if event.get("thread_ts"):
if db_user and db_user.helper:
ticket = await env.db.ticket.find_first(
ticket_message = await env.db.ticket.find_first(
where={"msgTs": event["thread_ts"]},
include={"openedBy": True, "tagsOnTickets": True},
)
if not ticket or ticket.status == TicketStatus.CLOSED:
if not ticket_message or ticket_message.status == TicketStatus.CLOSED:
return
first_word = text.split()[0].lower()

if first_word[0] == "?" and ticket:
if first_word[0] == "?" and ticket_message:
await run_macro(
name=first_word.lstrip("?"),
ticket=ticket,
ticket=ticket_message,
helper=db_user,
text=text,
macro_ts=event["ts"],
Expand All @@ -70,8 +72,8 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
"status": TicketStatus.IN_PROGRESS,
"assignedAt": (
datetime.now()
if not ticket.assignedAt
else ticket.assignedAt
if not ticket_message.assignedAt
else ticket_message.assignedAt
),
},
)
Expand Down Expand Up @@ -103,17 +105,15 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
},
)

user_info = await client.users_info(user=user)
user_info_response = await client.users_info(user=user) or {}
user_info = user_info_response.get("user")
profile_pic = None
display_name = "Explorer"
if user_info:
profile_pic = user_info["user"]["profile"].get("image_512", "")
display_name = (
user_info["user"]["profile"]["display_name"]
or user_info["user"]["real_name"]
)
profile_pic = user_info["profile"].get("image_512", "")
display_name = user_info["profile"]["display_name"] or user_info["real_name"]

ticket = await client.chat_postMessage(
ticket_message = await client.chat_postMessage(
channel=env.slack_ticket_channel,
text=f"New message from <@{user}>: {text}",
blocks=[
Expand Down Expand Up @@ -143,6 +143,11 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
unfurl_media=True,
)

ticket_message_ts = ticket_message["ts"]
if not ticket_message_ts:
logging.error(f"Ticket message has no ts: {ticket_message}")
return

async with env.session.post(
"https://ai.hackclub.com/chat/completions",
json={
Expand All @@ -167,28 +172,21 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
data = await res.json()
title = data["choices"][0]["message"]["content"].strip()

await env.db.ticket.create(
{
"title": title,
"description": text,
"msgTs": event["ts"],
"ticketTs": ticket["ts"],
"openedBy": {"connect": {"id": db_user.id}},
},
)

text = (
user_facing_message_text = (
env.transcript.first_ticket_create.replace("(user)", display_name)
if past_tickets == 0
else env.transcript.ticket_create.replace("(user)", display_name)
)
ticket_url = f"https://hackclub.slack.com/archives/{env.slack_ticket_channel}/p{ticket['ts'].replace('.', '')}"
ticket_url = f"https://hackclub.slack.com/archives/{env.slack_ticket_channel}/p{ticket_message_ts.replace('.', '')}"

await client.chat_postMessage(
user_facing_message = await client.chat_postMessage(
channel=event["channel"],
text=text,
text=user_facing_message_text,
blocks=[
{"type": "section", "text": {"type": "mrkdwn", "text": text}},
{
"type": "section",
"text": {"type": "mrkdwn", "text": user_facing_message_text},
},
{
"type": "actions",
"elements": [
Expand Down Expand Up @@ -216,10 +214,38 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
unfurl_media=True,
)

await client.reactions_add(
channel=event["channel"], name="thinking_face", timestamp=event["ts"]
user_facing_message_ts = user_facing_message["ts"]
if not user_facing_message_ts:
logging.error(f"User-facing message has no ts: {user_facing_message}")
return

ticket = await env.db.ticket.create(
{
"title": title,
"description": text,
"msgTs": event["ts"],
"ticketTs": ticket_message_ts,
"openedBy": {"connect": {"id": db_user.id}},
"userFacingMsgs": {
"create": {
"channelId": event["channel"],
"ts": user_facing_message_ts,
}
},
},
)

try:
await client.reactions_add(
channel=event["channel"], name="thinking_face", timestamp=event["ts"]
)
except SlackApiError as e:
if e.response.get("error") != "message_not_found":
raise e
# This means the parent message has been deleted while we've been processing it
# therefore we should unsend the bot messages and remove the ticket from the DB
await delete_and_clean_up_ticket(ticket)

if env.uptime_url and env.environment == "production":
async with env.session.get(env.uptime_url) as res:
if res.status != 200:
Expand Down
57 changes: 34 additions & 23 deletions nephthys/events/message_deletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
from nephthys.utils.ticket_methods import delete_and_clean_up_ticket


async def handle_question_deletion(
Expand All @@ -30,41 +31,51 @@ async def handle_question_deletion(
raise e
bot_info = await env.slack_client.auth_test()
bot_user_id = bot_info.get("user_id")
messages_to_delete = []
bot_replies = []
non_bot_replies = []
for msg in thread_history["messages"]:
if msg["ts"] == deleted_msg["ts"]:
continue # Ignore top-level message
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"]})
bot_replies.append(msg)
else:
non_bot_replies.append(msg)

# Delete messages
await send_heartbeat(
f"Removing my {len(messages_to_delete)} message(s) in a thread because the question was deleted."
should_keep_thread = (
# Preserve if there are any human replies
len(non_bot_replies) > 0
# More than 2 bot replies implies someone ran ?faq or something, so we'll preserve the ticket
or len(bot_replies) > 2
)
for msg in messages_to_delete:
await client.chat_delete(
channel=channel,
ts=msg["ts"],
)
if should_keep_thread:
return

# Delete ticket from DB and clean up bot messages
ticket = await env.db.ticket.find_first(where={"msgTs": deleted_msg["ts"]})
if not ticket:
message = f"Deleted question doesn't have an associated ticket in DB, ts={deleted_msg['ts']}"
logging.warning(message)
await send_heartbeat(message)
return
await delete_and_clean_up_ticket(ticket)


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"]
is_in_thread = (
"thread_ts" in deleted_msg and 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
if is_in_thread:
return
if event.get("subtype") == "message_deleted":
# This means the message has been completely deleted with out leaving a "tombstone"
# No thread means no messages to delete, but we should delete any associated ticket from the DB
await env.db.ticket.delete(where={"msgTs": deleted_msg["ts"]})
else:
# A parent message (i.e. top-level message in help channel) has been deleted
await handle_question_deletion(client, event["channel"], deleted_msg)
7 changes: 4 additions & 3 deletions nephthys/macros/faq.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from nephthys.actions.resolve import resolve
from nephthys.macros.types import Macro
from nephthys.utils.env import env
from nephthys.utils.ticket_methods import reply_to_ticket


class FAQ(Macro):
Expand All @@ -19,10 +20,10 @@ async def run(self, ticket, helper, **kwargs):
or user_info["user"]["profile"].get("real_name")
or user_info["user"]["name"]
)
await env.slack_client.chat_postMessage(
await reply_to_ticket(
text=f"hey, {name}! this question is answered in the faq i sent earlier, please make sure to check it out! :rac_cute:\n\n<{env.transcript.faq_link}|here it is again>",
channel=env.slack_help_channel,
thread_ts=ticket.msgTs,
ticket=ticket,
client=env.slack_client,
)
await resolve(
ts=ticket.msgTs,
Expand Down
7 changes: 4 additions & 3 deletions nephthys/macros/fraud.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from nephthys.actions.resolve import resolve
from nephthys.macros.types import Macro
from nephthys.utils.env import env
from nephthys.utils.ticket_methods import reply_to_ticket


class Fraud(Macro):
Expand All @@ -19,10 +20,10 @@ async def run(self, ticket, helper, **kwargs):
or user_info["user"]["profile"].get("real_name")
or user_info["user"]["name"]
)
await env.slack_client.chat_postMessage(
await reply_to_ticket(
text=f"Hiya {name}! Would you mind directing any fraud related queries to <@U091HC53CE8>? :rac_cute:\n\nIt'll keep your case confidential and make it easier for the fraud team to keep track of!",
channel=env.slack_help_channel,
thread_ts=ticket.msgTs,
ticket=ticket,
client=env.slack_client,
)
await resolve(
ts=ticket.msgTs,
Expand Down
7 changes: 4 additions & 3 deletions nephthys/macros/hello_world.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from nephthys.macros.types import Macro
from nephthys.utils.env import env
from nephthys.utils.ticket_methods import reply_to_ticket


class HelloWorld(Macro):
Expand All @@ -15,8 +16,8 @@ async def run(self, ticket, helper, **kwargs):
or user_info["user"]["profile"].get("real_name")
or user_info["user"]["name"]
)
await env.slack_client.chat_postMessage(
await reply_to_ticket(
text=f"hey, {name}! i'm heidi :rac_shy: say hi to orpheus for me would you? :rac_cute:",
channel=env.slack_help_channel,
thread_ts=ticket.msgTs,
ticket=ticket,
client=env.slack_client,
)
7 changes: 4 additions & 3 deletions nephthys/macros/identity.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from nephthys.actions.resolve import resolve
from nephthys.macros.types import Macro
from nephthys.utils.env import env
from nephthys.utils.ticket_methods import reply_to_ticket


class Identity(Macro):
Expand All @@ -19,10 +20,10 @@ async def run(self, ticket, helper, **kwargs):
or user_info["user"]["profile"].get("real_name")
or user_info["user"]["name"]
)
await env.slack_client.chat_postMessage(
await reply_to_ticket(
text=f"hey, {name}! please could you ask questions about identity verification in <#{env.transcript.identity_help_channel}>? :rac_cute:\n\nit helps the verification team keep track of questions easier!",
channel=env.slack_help_channel,
thread_ts=ticket.msgTs,
ticket=ticket,
client=env.slack_client,
)
await resolve(
ts=ticket.msgTs,
Expand Down
7 changes: 4 additions & 3 deletions nephthys/macros/shipcertqueue.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from nephthys.actions.resolve import resolve
from nephthys.macros.types import Macro
from nephthys.utils.env import env
from nephthys.utils.ticket_methods import reply_to_ticket


class ShipCertQueue(Macro):
Expand All @@ -19,10 +20,10 @@ async def run(self, ticket, helper, **kwargs):
or user_info["user"]["profile"].get("real_name")
or user_info["user"]["name"]
)
await env.slack_client.chat_postMessage(
await reply_to_ticket(
text=f"Hi {name}! Unfortunately, there is a backlog of projects awaiting ship certification; please be patient. \n\n *pssst... voting more will move your project further towards the front of the queue.*",
channel=env.slack_help_channel,
thread_ts=ticket.msgTs,
ticket=ticket,
client=env.slack_client,
)
await resolve(
ts=ticket.msgTs,
Expand Down
Loading