multi transcripts (#17)

This commit is contained in:
transcental 2025-06-29 02:53:00 +01:00 committed by GitHub
parent 0362cf2d2f
commit 2db250cee0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 266 additions and 81 deletions

View file

@ -1,5 +1,13 @@
SLACK_BOT_TOKEN="xoxb-..."
SLACK_SIGNING_SECRET=""
SLACK_USER_TOKEN="xoxp-..."
SLACK_SIGNING_SECRET="..."
ENVIRONMENT="development"
PORT=3000
SLACK_HEARTBEAT_CHANNEL="C..."
SLACK_TICKET_CHANNEL="C..."
SLACK_BTS_CHANNEL="C..."
SLACK_USER_GROUP="S..."
SLACK_MAINTAINER_ID="U..."
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/nephthys"
SITE_URL="https://summer.hackclub.com"
SITE_API_KEY="..."

View file

@ -2,7 +2,6 @@ from datetime import datetime
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.data.transcript import Transcript
from nephthys.utils.delete_thread import add_thread_to_delete_queue
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
@ -51,7 +50,7 @@ async def resolve(ts: str, resolver: str, client: AsyncWebClient):
await client.chat_postMessage(
channel=env.slack_help_channel,
text=Transcript.ticket_resolve.format(user_id=resolver),
text=env.transcript.ticket_resolve.format(user_id=resolver),
thread_ts=ts,
)

View file

@ -5,7 +5,6 @@ from urllib.parse import quote
from slack_bolt.async_app import AsyncAck
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.data.transcript import Transcript
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
@ -21,7 +20,7 @@ async def dm_magic_link_cmd_callback(
await client.chat_postEphemeral(
channel=body["channel_id"],
user=user_id,
text=Transcript.dm_magic_link_no_permission,
text=env.transcript.dm_magic_link_no_permission,
)
return
@ -30,7 +29,7 @@ async def dm_magic_link_cmd_callback(
await client.chat_postEphemeral(
channel=body["channel_id"],
user=body["user_id"],
text=Transcript.dm_magic_link_no_user,
text=env.transcript.dm_magic_link_no_user,
)
parsed = re.search(r"<@([UW][A-Z0-9]+)(?:\|[^>]+)?>", cmd_text)
slack_id = parsed.group(1) if parsed else None
@ -38,7 +37,7 @@ async def dm_magic_link_cmd_callback(
await client.chat_postEphemeral(
channel=body["channel_id"],
user=body["user_id"],
text=Transcript.dm_magic_link_no_user,
text=env.transcript.dm_magic_link_no_user,
)
return
@ -47,7 +46,7 @@ async def dm_magic_link_cmd_callback(
await client.chat_postEphemeral(
channel=body["channel_id"],
user=body["user_id"],
text=Transcript.dm_magic_link_no_user,
text=env.transcript.dm_magic_link_no_user,
)
return
@ -64,7 +63,7 @@ async def dm_magic_link_cmd_callback(
await client.chat_postEphemeral(
channel=body["channel_id"],
user=body["user_id"],
text=Transcript.dm_magic_link_error.format(status=res.status),
text=env.transcript.dm_magic_link_error.format(status=res.status),
)
return
@ -78,18 +77,18 @@ async def dm_magic_link_cmd_callback(
await client.chat_postEphemeral(
channel=body["channel_id"],
user=body["user_id"],
text=Transcript.dm_magic_link_error.format(status="No link returned"),
text=env.transcript.dm_magic_link_error.format(status="No link returned"),
)
return
await client.chat_postEphemeral(
channel=body["channel_id"],
user=body["user_id"],
text=Transcript.dm_magic_link_success,
text=env.transcript.dm_magic_link_success,
)
await client.chat_postMessage(
channel=slack_id,
text=Transcript.dm_magic_link_message.format(magic_link=magic_link),
text=env.transcript.dm_magic_link_message.format(magic_link=magic_link),
)
logging.info(f"Sent magic link to {slack_id}")

View file

@ -1,37 +0,0 @@
from nephthys.utils.env import env
class Transcript:
FAQ_LINK = "https://hackclub.slack.com/docs/T0266FRGM/F090MQF0H2Q"
EXPLORPHEUS_PFP = "https://hc-cdn.hel1.your-objectstorage.com/s/v3/d6d828d6ba656d09a62add59dc07e2974bfdb38f_image.png"
IDENTITY_HELP_CHANNEL = "C092833JXKK"
first_ticket_create = f"""
oh, hey (user) it looks like this is your first time here, welcome! someone should be along to help you soon but in the mean time i suggest you read the faq <{FAQ_LINK}|here>, it answers a lot of common questions.
if your question has been answered, please hit the button below to mark it as resolved
"""
ticket_create = f"""
someone should be along to help you soon but in the mean time i suggest you read the faq <{FAQ_LINK}|here> to make sure your question hasn't already been answered. if it has been, please hit the button below to mark it as resolved :D
"""
ticket_resolve = f"""
oh, oh! it looks like this post has been marked as resolved by <@{{user_id}}>! if you have any more questions, please make a new post in <#{env.slack_help_channel}> and someone'll be happy to help you out! not me though, i'm just a silly racoon ^-^
"""
home_unknown_user_title = ":upside-down_orpheus: woah, stop right there {name}!"
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 <@{env.slack_maintainer_id}> and she'll lmk what to do!"
not_allowed_channel = f"heya, it looks like you're not supposed to be in that channel, pls talk to <@{env.slack_maintainer_id}> if that's wrong"
dm_magic_link_no_user = ":rac_cute: heya, please provide the user you want me to dm"
dm_magic_link_error = f":rac_nooo: something went wrong while generating the magic link, please bug <@{env.slack_maintainer_id}> (status: {{status}})"
dm_magic_link_success = (
":rac_cute: magic link sent! tell em to check their dms with me :D"
)
dm_magic_link_message = ":rac_cute: hey there! i got told that you got a bit stuck so here's a magic link for ya :D\n{magic_link}"
dm_magic_link_no_permission = f":rac_nooo: you don't have permission to use this command, please bug <@{env.slack_maintainer_id}> if you think this is a mistake"

View file

@ -1,23 +1,15 @@
from slack_bolt.context.ack.async_ack import AsyncAck
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.data.transcript import Transcript
from nephthys.tasks.update_helpers import update_helpers
from nephthys.utils.env import env
async def channel_join(ack: AsyncAck, event: dict, client: AsyncWebClient):
await ack()
user_id = event["user"]
channel_id = event["channel"]
if channel_id in [env.slack_bts_channel, env.slack_ticket_channel]:
users = await client.usergroups_users_list(usergroup=env.slack_user_group)
if user_id not in users.get("users", []):
await client.conversations_kick(channel=channel_id, user=user_id)
await client.chat_postMessage(
channel=user_id, text=Transcript.not_allowed_channel
)
await update_helpers()
await update_helpers()
else:
return

View file

@ -13,18 +13,6 @@ async def channel_left(ack: AsyncAck, event: dict, client: AsyncWebClient):
if channel_id == env.slack_help_channel:
return
users = await client.usergroups_users_list(usergroup=env.slack_user_group)
new_users = users.get("users", [])
try:
new_users.remove(user_id)
except ValueError:
return
await client.usergroups_users_update(
usergroup=env.slack_user_group, users=new_users
)
await env.db.user.update(where={"slackId": user_id}, data={"helper": False})
try:

View file

@ -3,12 +3,11 @@ from typing import Dict
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.data.transcript import Transcript
from nephthys.macros import run_macro
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
ALLOWED_SUBTYPES = ["file_share", "me_message"]
ALLOWED_SUBTYPES = ["file_share", "me_message", "thread_broadcast"]
async def on_message(event: Dict[str, Any], client: AsyncWebClient):
@ -18,6 +17,21 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
if "subtype" in event and event["subtype"] not in ALLOWED_SUBTYPES:
return
if event.get("subtype") == "thread_broadcast":
await client.chat_delete(
channel=event["channel"],
ts=event["ts"],
as_user=True,
token=env.slack_user_token,
broadcast_delete=True,
)
await client.chat_postEphemeral(
channel=event["channel"],
user=event["user"],
text=env.transcript.thread_broadcast_delete,
thread_ts=event["thread_ts"] if "thread_ts" in event else event["ts"],
)
user = event.get("user", "unknown")
text = event.get("text", "")
@ -118,9 +132,9 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
)
text = (
Transcript.first_ticket_create.replace("(user)", display_name)
env.transcript.first_ticket_create.replace("(user)", display_name)
if past_tickets == 0
else Transcript.ticket_create.replace("(user)", display_name)
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('.', '')}"

View file

@ -1,5 +1,4 @@
from nephthys.actions.resolve import resolve
from nephthys.data.transcript import Transcript
from nephthys.macros.types import Macro
from nephthys.utils.env import env
@ -18,7 +17,7 @@ class FAQ(Macro):
or user_info["user"]["name"]
)
await env.slack_client.chat_postMessage(
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<here it is again|{Transcript.FAQ_LINK}>",
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<here it is again|{env.transcript.faq_link}>",
channel=env.slack_help_channel,
thread_ts=ticket.msgTs,
)

View file

@ -1,5 +1,4 @@
from nephthys.actions.resolve import resolve
from nephthys.data.transcript import Transcript
from nephthys.macros.types import Macro
from nephthys.utils.env import env
@ -18,7 +17,7 @@ class Identity(Macro):
or user_info["user"]["name"]
)
await env.slack_client.chat_postMessage(
text=f"hey, {name}! please could you ask questions about identity verification in <#{Transcript.IDENTITY_HELP_CHANNEL}>? :rac_cute:\n\nit helps the verification team keep track of questions easier!",
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,
)

View file

@ -0,0 +1,9 @@
from typing import List
from typing import Type
from nephthys.transcripts.transcript import Transcript
from nephthys.transcripts.transcripts.identity import Identity
from nephthys.transcripts.transcripts.summer_of_making import SummerOfMaking
transcripts: List[Type[Transcript]] = [Identity, SummerOfMaking]

View file

@ -0,0 +1,131 @@
from pydantic import BaseModel
from pydantic import Field
from pydantic import model_validator
class Transcript(BaseModel):
"""Class to hold all the transcript messages and links used in the bot."""
class Config:
"""Configuration for the Pydantic model."""
extra = "forbid"
program_name: str = Field(
default="Summer of Making", description="Name of the program"
)
program_owner: str = Field(
default="U054VC2KM9P",
description="Slack ID of the support manager",
)
help_channel: str = Field(
default="",
description="Slack channel ID for help requests",
)
ticket_channel: str = Field(
default="",
description="Slack channel ID for ticket creation",
)
team_channel: str = Field(
default="",
description="Slack channel ID for team discussions and stats",
)
@property
def program_snake_case(self) -> str:
"""Snake case version of the program name."""
return self.program_name.lower().replace(" ", "_")
faq_link: str = Field(
default="https://hackclub.slack.com/docs/T0266FRGM/F093F8D7EE9",
description="FAQ link URL",
)
summer_help_channel: str = Field(
default="C091D312J85", description="Summer help channel ID"
)
identity_help_channel: str = Field(
default="C092833JXKK", description="Identity help channel ID"
)
first_ticket_create: str = Field(
default="", description="Message for first-time ticket creators"
)
ticket_create: str = Field(default="", description="Message for ticket creation")
ticket_resolve: str = Field(
default="", description="Message when ticket is resolved"
)
thread_broadcast_delete: str = Field(
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",
)
home_unknown_user_title: str = Field(
default=":upside-down_orpheus: woah, stop right there {name}!",
description="Title for unknown user on home page",
)
home_unknown_user_text: str = Field(
default="", description="Text for unknown user on home page"
)
not_allowed_channel: str = Field(
default="", description="Message for unauthorized channel access"
)
# this stuff is only required for summer of making, but it's easier to keep it here :p
dm_magic_link_no_user: str = Field(
default=":rac_cute: heya, please provide the user you want me to dm",
description="Message when no user provided for magic link DM",
)
dm_magic_link_error: str = Field(
default="", description="Error message for magic link generation"
)
dm_magic_link_success: str = Field(
default=":rac_cute: magic link sent! tell em to check their dms with me :D",
description="Success message for magic link DM",
)
dm_magic_link_message: str = Field(
default=":rac_cute: hey there! i got told that you got a bit stuck so here's a magic link for ya :D\n{magic_link}",
description="Magic link DM message",
)
dm_magic_link_no_permission: str = Field(
default="", description="No permission message for magic link command"
)
@model_validator(mode="after")
def set_default_messages(self):
"""Set default values for messages that reference other fields"""
if not self.first_ticket_create:
self.first_ticket_create = f"""oh, hey (user) it looks like this is your first time here, welcome! someone should be along to help you soon but in the mean time i suggest you read the faq <{self.faq_link}|here>, it answers a lot of common questions.
if your question has been answered, please hit the button below to mark it as resolved
"""
if not self.ticket_create:
self.ticket_create = f"""someone should be along to help you soon but in the mean time i suggest you read the faq <{self.faq_link}|here> to make sure your question hasn't already been answered. if it has been, please hit the button below to mark it as resolved :D
"""
if not self.ticket_resolve:
self.ticket_resolve = f"""oh, oh! it looks like this post has been marked as resolved by <@{{user_id}}>! if you have any more questions, please make a new post in <#{self.help_channel}> and someone'll be happy to help you out! not me though, i'm just a silly racoon ^-^
"""
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!"
if not self.not_allowed_channel:
self.not_allowed_channel = f"heya, it looks like you're not supposed to be in that channel, pls talk to <@{self.program_owner}> if that's wrong"
if not self.dm_magic_link_error:
self.dm_magic_link_error = f":rac_nooo: something went wrong while generating the magic link, please bug <@{self.program_owner}> (status: {{status}})"
if not self.dm_magic_link_no_permission:
self.dm_magic_link_no_permission = f":rac_nooo: you don't have permission to use this command, please bug <@{self.program_owner}> if you think this is a mistake"
return self

View file

@ -0,0 +1,30 @@
from nephthys.transcripts.transcript import Transcript
class Identity(Transcript):
"""Transcript for identity-help"""
program_name: str = "Identity"
program_owner: str = "U054VC2KM9P"
help_channel: str = "C092833JXKK"
ticket_channel: str = "C093FV95GGJ"
team_channel: str = "C091URN0G9G"
faq_link: str = "https://hackclub.slack.com/docs/T0266FRGM/F090MQF0H2Q"
summer_help_channel: str = "C091D312J85"
identity_help_channel: str = "C092833JXKK"
first_ticket_create: str = f"""
hi (user)! it looks like this is your first time here, welcome! someone should be along to help you soon but please read the <{faq_link}|faq>, it answers a lot of common questions.
if your question has been answered, please hit the button below to mark it as resolved
"""
ticket_create: str = f"someone should be along to help you soon but in the mean time i suggest you read the faq <{faq_link}|here> to make sure your question hasn't already been answered. if it has been, please hit the button below to mark it as resolved :D"
ticket_resolve: str = f"oh, oh! it looks like this post has been marked as resolved by <@{{user_id}}>! if you have any more questions, please make a new post in <#{help_channel}> and someone'll be happy to help you out! not me though, i'm just a silly racoon ^-^"
home_unknown_user_title: str = (
":upside-down_orpheus: woah, stop right there {name}!"
)
home_unknown_user_text: str = f"oh, oh! it looks like this post has been marked as resolved by <@{{user_id}}>! if you have any more questions, please make a new post in <#{help_channel}> and someone'll be happy to help you out! not me though, i'm just a silly racoon ^-^"
not_allowed_channel: str = f"heya, it looks like you're not supposed to be in that channel, pls talk to <@{program_owner}> if that's wrong"

View file

@ -0,0 +1,30 @@
from nephthys.transcripts.transcript import Transcript
class SummerOfMaking(Transcript):
"""Transcript for Summer of Making program."""
program_name: str = "Summer of Making"
program_owner: str = "U054VC2KM9P"
help_channel: str = "C091D312J85"
ticket_channel: str = "C091D31UC4D"
team_channel: str = "C0917TW3Z2P"
faq_link: str = "https://hackclub.slack.com/docs/T0266FRGM/F090MQF0H2Q"
summer_help_channel: str = "C091D312J85"
identity_help_channel: str = "C092833JXKK"
first_ticket_create: str = f"""
oh, hey (user) it looks like this is your first time here, welcome! someone should be along to help you soon but in the mean time i suggest you read the faq <{faq_link}|here>, it answers a lot of common questions.
if your question has been answered, please hit the button below to mark it as resolved
"""
ticket_create: str = f"someone should be along to help you soon but in the mean time i suggest you read the faq <{faq_link}|here> to make sure your question hasn't already been answered. if it has been, please hit the button below to mark it as resolved :D"
ticket_resolve: str = f"oh, oh! it looks like this post has been marked as resolved by <@{{user_id}}>! if you have any more questions, please make a new post in <#{help_channel}> and someone'll be happy to help you out! not me though, i'm just a silly racoon ^-^"
home_unknown_user_title: str = (
":upside-down_orpheus: woah, stop right there {name}!"
)
home_unknown_user_text: str = f"oh, oh! it looks like this post has been marked as resolved by <@{{user_id}}>! if you have any more questions, please make a new post in <#{help_channel}> and someone'll be happy to help you out! not me though, i'm just a silly racoon ^-^"
not_allowed_channel: str = f"heya, it looks like you're not supposed to be in that channel, pls talk to <@{program_owner}> if that's wrong"

View file

@ -4,6 +4,8 @@ from aiohttp import ClientSession
from dotenv import load_dotenv
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.transcripts import transcripts
from nephthys.transcripts.transcript import Transcript
from prisma import Prisma
load_dotenv(override=True)
@ -26,6 +28,7 @@ class Environment:
self.slack_bts_channel = os.environ.get("SLACK_BTS_CHANNEL", "unset")
self.slack_user_group = os.environ.get("SLACK_USER_GROUP", "unset")
self.slack_maintainer_id = os.environ.get("SLACK_MAINTAINER_ID", "unset")
self.program = os.environ.get("PROGRAM", "summer_of_making")
self.port = int(os.environ.get("PORT", 3000))
@ -36,8 +39,26 @@ class Environment:
if unset:
raise ValueError(f"Missing environment variables: {', '.join(unset)}")
transcript_instances = [program() for program in transcripts]
valid_programs = [
program.program_snake_case for program in transcript_instances
]
if self.program not in valid_programs:
raise ValueError(
f"Invalid PROGRAM environment variable: {self.program}. "
f"Must be one of {valid_programs}"
)
self.session: ClientSession
self.db = Prisma()
self.transcript = next(
(
program
for program in transcript_instances
if program.program_snake_case == self.program
),
Transcript(),
)
self.slack_client = AsyncWebClient(token=self.slack_bot_token)

View file

@ -1,4 +1,4 @@
from nephthys.data.transcript import Transcript
from nephthys.utils.env import env
def get_unknown_user_view(name: str):
@ -9,13 +9,16 @@ def get_unknown_user_view(name: str):
"type": "header",
"text": {
"type": "plain_text",
"text": Transcript.home_unknown_user_title.format(name=name),
"text": env.transcript.home_unknown_user_title.format(name=name),
"emoji": True,
},
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": Transcript.home_unknown_user_text},
"text": {
"type": "mrkdwn",
"text": env.transcript.home_unknown_user_text,
},
},
],
}