Refactoring around macros (#111)

* Use transcripts for most macro message texts

* Write a get_user_name() util

* Remove Fraudpheus reference

* Use get_user_name() in shipcertqueue.py

* Always impersonate in reopen.py

See 182771acdfd8eca0410f8c012f24e7cd51a71925

* Add user PFP method to slack_user.py

* Handle the case of a username not being present

* Use get_user_profile() to get user PFPs when tickets are (re)opened

* Remove comment

* Fix type error in reopen.py

* Guard against ts being not present in reopen.py

* Ensure consistent handling of profile_pic_512x() being None
This commit is contained in:
Mish 2025-11-16 16:41:47 +00:00 committed by GitHub
parent f25a27afc7
commit fdb729a00b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 97 additions and 70 deletions

View file

@ -11,6 +11,7 @@ from nephthys.macros import run_macro
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
from nephthys.utils.performance import perf_timer
from nephthys.utils.slack_user import get_user_profile
from nephthys.utils.ticket_methods import delete_and_clean_up_ticket
from prisma.enums import TicketStatus
from prisma.models import User
@ -136,42 +137,25 @@ async def handle_new_question(
client (AsyncWebClient): Slack API client.
db_user (User | None): The database user object, or None if user doesn't exist yet.
"""
author_id = event.get("user", "unknown")
text = event.get("text", "")
async with perf_timer("Slack user info fetch"):
user = event.get("user", "unknown")
text = event.get("text", "")
user_info_response = await client.users_info(user=user) or {}
user_info = user_info_response.get("user")
if user_info:
profile_pic: str | None = user_info["profile"].get("image_512", "")
display_name: str = (
user_info["profile"]["display_name"] or user_info["real_name"]
)
else:
profile_pic = None
display_name = "Explorer"
author = await get_user_profile(author_id)
if db_user:
async with perf_timer("Getting ticket count from DB"):
past_tickets = await env.db.ticket.count(where={"openedById": db_user.id})
else:
past_tickets = 0
username = (user_info or {}).get(
"name"
) # this should never actually be empty but if it is, that is a major issue
username = author.display_name()
async with perf_timer("Creating user in DB"):
if not username:
await send_heartbeat(
f"SOMETHING HAS GONE TERRIBLY WRONG <@{user}> has no username found - <@{env.slack_maintainer_id}>"
)
db_user = await env.db.user.upsert(
where={
"slackId": user,
"slackId": author_id,
},
data={
"create": {"slackId": user, "username": username},
"update": {"slackId": user, "username": username},
"create": {"slackId": author_id, "username": username},
"update": {"slackId": author_id, "username": username},
},
)
@ -180,8 +164,8 @@ async def handle_new_question(
event,
client,
past_tickets=past_tickets,
display_name=display_name,
profile_pic=profile_pic,
display_name=author.display_name(),
profile_pic=author.profile_pic_512x() or "",
)
ticket_message_ts = ticket_message["ts"]
@ -190,9 +174,9 @@ async def handle_new_question(
return
user_facing_message_text = (
env.transcript.first_ticket_create.replace("(user)", display_name)
env.transcript.first_ticket_create.replace("(user)", author.display_name())
if past_tickets == 0
else env.transcript.ticket_create.replace("(user)", display_name)
else env.transcript.ticket_create.replace("(user)", author.display_name())
)
ticket_url = f"https://hackclub.slack.com/archives/{env.slack_ticket_channel}/p{ticket_message_ts.replace('.', '')}"

View file

@ -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.slack_user import get_user_profile
from nephthys.utils.ticket_methods import reply_to_ticket
@ -14,14 +15,9 @@ class FAQ(Macro):
sender = await env.db.user.find_first(where={"id": ticket.openedById})
if not sender:
return
user_info = await env.slack_client.users_info(user=sender.slackId)
name = (
user_info["user"]["profile"].get("display_name")
or user_info["user"]["profile"].get("real_name")
or user_info["user"]["name"]
)
user = await get_user_profile(sender.slackId)
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>",
text=env.transcript.faq_macro.replace("(user)", user.display_name()),
ticket=ticket,
client=env.slack_client,
)

View file

@ -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.slack_user import get_user_profile
from nephthys.utils.ticket_methods import reply_to_ticket
@ -9,19 +10,14 @@ class Fraud(Macro):
async def run(self, ticket, helper, **kwargs):
"""
A simple macro telling people to use Fraudpheus
A simple macro telling people to DM the Fraud Squad
"""
sender = await env.db.user.find_first(where={"id": ticket.openedById})
if not sender:
return
user_info = await env.slack_client.users_info(user=sender.slackId)
name = (
user_info["user"]["profile"].get("display_name")
or user_info["user"]["profile"].get("real_name")
or user_info["user"]["name"]
)
user = await get_user_profile(sender.slackId)
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!",
text=env.transcript.fraud_macro.replace("(user)", user.display_name()),
ticket=ticket,
client=env.slack_client,
)

View file

@ -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.slack_user import get_user_profile
from nephthys.utils.ticket_methods import reply_to_ticket
@ -14,14 +15,9 @@ class Identity(Macro):
sender = await env.db.user.find_first(where={"id": ticket.openedById})
if not sender:
return
user_info = await env.slack_client.users_info(user=sender.slackId)
name = (
user_info["user"]["profile"].get("display_name")
or user_info["user"]["profile"].get("real_name")
or user_info["user"]["name"]
)
user = await get_user_profile(sender.slackId)
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!",
text=env.transcript.identity_macro.replace("(user)", user.display_name()),
ticket=ticket,
client=env.slack_client,
)

View file

@ -1,6 +1,9 @@
import logging
from nephthys.macros.types import Macro
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
from nephthys.utils.slack_user import get_user_profile
from nephthys.utils.ticket_methods import reply_to_ticket
from prisma.enums import TicketStatus
@ -31,20 +34,18 @@ class Reopen(Macro):
client=env.slack_client,
)
user_info = await env.slack_client.users_info(user=ticket.openedBy.slackId)
name = (
user_info["user"]["profile"]["display_name"]
or user_info["user"]["real_name"]
or "Explorer"
)
profile_pic = user_info["user"]["profile"].get("image_512", "")
if not ticket.openedBy:
await send_heartbeat(
f"Attempted to reopen ticket (TS {ticket.msgTs}) but ticket author has not been recorded"
)
return
author_id = ticket.openedBy.slackId
author = await get_user_profile(author_id)
thread_url = f"https://hackclub.slack.com/archives/{env.slack_help_channel}/p{ticket.msgTs.replace('.', '')}"
use_impersonation = await env.workspace_admin_available()
backend_message = await env.slack_client.chat_postMessage(
channel=env.slack_ticket_channel,
text=f"Reopened ticket from <@{ticket.openedBy.slackId}>: {ticket.description}",
text=f"Reopened ticket from <@{author_id}>: {ticket.description}",
blocks=[
{
"type": "input",
@ -65,18 +66,21 @@ class Reopen(Macro):
"elements": [
{
"type": "mrkdwn",
"text": f"Reopened by <@{helper.slackId}>. Originally submitted by <@{ticket.openedBy.slackId}>. <{thread_url}|View thread>.",
"text": f"Reopened by <@{helper.slackId}>. Originally submitted by <@{author_id}>. <{thread_url}|View thread>.",
}
],
},
],
username=name if use_impersonation else None,
icon_url=profile_pic if use_impersonation else None,
username=author.display_name(),
icon_url=author.profile_pic_512x() or "",
unfurl_links=True,
unfurl_media=True,
)
new_ticket_ts = backend_message["ts"]
if not new_ticket_ts:
logging.error(f"Invalid Slack message creation response: {backend_message}")
raise ValueError("Invalid Slack message creation response: no ts")
await env.db.ticket.update(
where={"id": ticket.id},
data={"ticketTs": new_ticket_ts},

View file

@ -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.slack_user import get_user_profile
from nephthys.utils.ticket_methods import reply_to_ticket
@ -14,14 +15,9 @@ class ShipCertQueue(Macro):
sender = await env.db.user.find_first(where={"id": ticket.openedById})
if not sender:
return
user_info = await env.slack_client.users_info(user=sender.slackId)
name = (
user_info["user"]["profile"].get("display_name")
or user_info["user"]["profile"].get("real_name")
or user_info["user"]["name"]
)
user = await get_user_profile(sender.slackId)
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.*",
text=f"Hi {user.display_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.*",
ticket=ticket,
client=env.slack_client,
)

View file

@ -77,6 +77,19 @@ class Transcript(BaseModel):
default="hey! please keep your messages *all in one thread* to make it easier to read! i've gone ahead and removed that message from the channel for ya :D",
)
faq_macro: str = Field(
default="", description="Message to be sent when the FAQ macro is used"
)
fraud_macro: str = Field(
default="Hiya (user)! 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!",
description="Message to be sent when the fraud macro is used",
)
identity_macro: str = Field(
default="", description="Message to be sent when the identity macro is used"
)
home_unknown_user_title: str = Field(
default=":upside-down_orpheus: woah, stop right there {name}!",
description="Title for unknown user on home page",
@ -134,6 +147,12 @@ if your question has been answered, please hit the button below to mark it as re
self.ticket_resolve_stale = f""":rac_nooo: it looks like this post is a bit old! if you still need help, please make a new post in <#{self.help_channel}> and someone'll be happy to help you out! ^~^
"""
if not self.faq_macro:
self.faq_macro = f"hey, (user)! this question is answered in the faq i sent earlier, please make sure to check it out! :rac_cute:\n\n<{self.faq_link}|here it is again>"
if not self.identity_macro:
self.identity_macro = f"hey, (user)! please could you ask questions about identity verification in <#{self.identity_help_channel}>? :rac_cute:\n\nit helps the verification team keep track of questions easier!"
if not self.home_unknown_user_text:
self.home_unknown_user_text = f"heyyyy, heidi here! it looks like i'm not allowed to show ya this. sorry! if you think this is a mistake, please reach out to <@{self.program_owner}> and she'll lmk what to do!"

View file

@ -0,0 +1,36 @@
import logging
from slack_sdk.web.async_slack_response import AsyncSlackResponse
from nephthys.utils.env import env
class UserProfileWrapper:
def __init__(self, users_info_response: AsyncSlackResponse):
user_data = users_info_response.get("user")
if not user_data:
raise ValueError(f"Slack user not found: {users_info_response}")
self.raw_data = user_data
def display_name(self) -> str:
display_name = (
self.raw_data["profile"].get("display_name")
or self.raw_data["profile"].get("real_name")
or self.raw_data["name"]
)
# This should never actually be empty but if it is, that is a major issue
if not display_name:
logging.error(
f"SOMETHING HAS GONE TERRIBLY WRONG - user has no username: {self.raw_data}"
)
return "" # idk
return display_name
def profile_pic_512x(self) -> str | None:
return self.raw_data["profile"].get("image_512")
async def get_user_profile(slack_id: str) -> UserProfileWrapper:
"""Retrieve the user's display name from Slack given their Slack ID."""
response = await env.slack_client.users_info(user=slack_id)
return UserProfileWrapper(response)