much done, worky

This commit is contained in:
Amber 2025-06-13 19:26:10 +01:00
parent ccd8407588
commit 3561b91301
No known key found for this signature in database
GPG key ID: 81E4B6CCB9561611
27 changed files with 862 additions and 11 deletions

View file

@ -15,4 +15,6 @@ EXPOSE 3000
ENV PATH="/app/.venv/bin:$PATH"
RUN prisma db push
CMD ["nephthys"]

View file

@ -7,6 +7,7 @@ from aiohttp import ClientSession
from dotenv import load_dotenv
from starlette.applications import Starlette
from nephthys.tasks.update_helpers import update_helpers
from nephthys.utils.delete_thread import process_queue
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
@ -30,6 +31,7 @@ async def main(_app: Starlette):
env.session = session
await env.db.connect()
delete_msg_task = asyncio.create_task(process_queue())
await update_helpers()
handler = None
if env.slack_app_token:
if env.environment == "production":

View file

View file

@ -0,0 +1,87 @@
import logging
from typing import Any
from typing import Dict
from slack_bolt.async_app import AsyncAck
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
async def assign_tag_callback(
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
):
await ack()
user_id = body["user"]["id"]
raw_tags = body["actions"][0]["selected_options"]
tags = [
{"name": tag["text"]["text"], "value": tag["value"]}
for tag in raw_tags
if "value" in tag
]
logging.info(tags)
channel_id = body["channel"]["id"]
ts = body["message"]["ts"]
user = await env.db.user.find_unique(where={"id": user_id})
if not user or not user.helper:
await client.chat_postEphemeral(
channel=channel_id,
user=user_id,
text="You are not authorized to assign tags.",
)
return
ticket = await env.db.ticket.find_unique(
where={"ticketTs": ts}, include={"tagsOnTickets": True}
)
if not ticket:
await send_heartbeat(
f"Failed to find ticket with ts {ts} in channel {channel_id}."
)
return
if ticket.tagsOnTickets:
new_tags = [
tag
for tag in tags
if tag["value"] not in [t.tagId for t in ticket.tagsOnTickets]
]
old_tags = [
tag
for tag in ticket.tagsOnTickets
if tag.tagId not in [t["value"] for t in tags]
]
else:
new_tags = tags
old_tags = []
logging.info(f"New: {new_tags}, Old: {old_tags}")
await env.db.tagsontickets.create_many(
data=[{"tagId": tag["value"], "ticketId": ticket.id} for tag in new_tags]
)
await env.db.tagsontickets.delete_many(
where={"tagId": {"in": [tag.tagId for tag in old_tags]}, "ticketId": ts}
)
tags = await env.db.usertagsubscription.find_many(
where={"tagId": {"in": [tag["value"] for tag in new_tags]}}
)
users = [
{
"id": tag.userId,
"tags": [tag.tagId for tag in tags if tag.userId == tag.userId],
}
for tag in tags
]
url = f"https://hackclub.slack.com/archives/{env.slack_help_channel}/p{ticket.msgTs.replace('.', '')}"
for user in users:
formatted_tags = ", ".join(
[tag["name"] for tag in new_tags if tag["value"] in user["tags"]]
)
await client.chat_postMessage(
channel=user["id"], text=f"New ticket for {formatted_tags}!\n{url}"
)

View file

@ -0,0 +1,44 @@
from slack_bolt.async_app import AsyncAck
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.events.app_home_opened import open_app_home
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
from nephthys.views.modals.create_tag import get_create_tag_modal
async def create_tag_view_callback(ack: AsyncAck, body: dict, client: AsyncWebClient):
"""
Callback for the create tag view submission
"""
await ack()
user_id = body["user"]["id"]
user = await env.db.user.find_unique(where={"id": user_id})
if not user or not user.admin:
await send_heartbeat(f"Attempted to create tag by non-admin user <@{user_id}>")
return
name = body["view"]["state"]["values"]["tag_name"]["tag_name"]["value"]
await env.db.tag.create(data={"name": name})
await open_app_home("tags", client, user_id)
async def create_tag_btn_callback(ack: AsyncAck, body: dict, client: AsyncWebClient):
"""
Open modal to create a tag
"""
await ack()
user_id = body["user"]["id"]
trigger_id = body["trigger_id"]
user = await env.db.user.find_unique(where={"id": user_id})
if not user or not user.admin:
await send_heartbeat(
f"Attempted to open create tag modal by non-admin user <@{user_id}>"
)
return
view = get_create_tag_modal()
await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id)

View file

@ -1,3 +1,5 @@
from datetime import datetime
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.data.transcript import Transcript
@ -9,7 +11,7 @@ from prisma.enums import TicketStatus
async def resolve(ts: str, resolver: str, client: AsyncWebClient):
allowed = can_resolve(resolver, ts)
allowed = await can_resolve(resolver, ts)
if not allowed:
await send_heartbeat(
f"User {resolver} attempted to resolve ticket with ts {ts} without permission.",
@ -22,9 +24,14 @@ async def resolve(ts: str, resolver: str, client: AsyncWebClient):
if not ticket:
return
now = datetime.now()
tkt = await env.db.ticket.update(
where={"msgTs": ts},
data={"status": TicketStatus.CLOSED, "closedBy": {"connect": {"id": resolver}}},
data={
"status": TicketStatus.CLOSED,
"closedBy": {"connect": {"id": resolver}},
"closedAt": now,
},
)
if not tkt:
await send_heartbeat(
@ -35,7 +42,7 @@ async def resolve(ts: str, resolver: str, client: AsyncWebClient):
await client.chat_postMessage(
channel=env.slack_help_channel,
text=Transcript.ticket_resolve,
text=Transcript.ticket_resolve.format(user_id=resolver),
thread_ts=ts,
)

View file

@ -0,0 +1,44 @@
from typing import Any
from typing import Dict
from slack_bolt.async_app import AsyncAck
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.events.app_home_opened import open_app_home
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
async def tag_subscribe_callback(
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
):
"""
Callback for the tag subscribe button
"""
await ack()
user_id = body["user"]["id"]
user = await env.db.user.find_unique(where={"id": user_id})
if not user:
await send_heartbeat(
f"Attempted to subscribe to tag by unknown user <@{user_id}>"
)
return
tag_id, tag_name = body["actions"][0]["value"].split(";")
# check if user is subcribed
if await env.db.usertagsubscription.find_first(
where={"userId": user_id, "tagId": tag_id}
):
await env.db.usertagsubscription.delete(
where={"userId_tagId": {"tagId": tag_id, "userId": user_id}}
)
else:
await env.db.usertagsubscription.create(
data={
"user": {"connect": {"id": user_id}},
"tag": {"connect": {"id": tag_id}},
}
)
await open_app_home("tags", client, user_id)

View file

@ -13,5 +13,10 @@ someone should be along to help you soon but in the mean time i suggest you read
"""
ticket_resolve = f"""
oh, oh! it looks like this post has been marked as resolved by (user)! 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 ^-^
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"

View file

@ -0,0 +1,73 @@
import logging
import traceback
from typing import Any
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
from nephthys.views.home.error import get_error_view
from nephthys.views.home.helper import get_helper_view
from nephthys.views.home.loading import get_loading_view
from nephthys.views.home.stats import get_stats_view
from nephthys.views.home.tags import get_manage_tags_view
from nephthys.views.home.unknown_user import get_unknown_user_view
async def on_app_home_opened(event: dict[str, Any], client: AsyncWebClient):
user_id = event["user"]
await open_app_home("default", client, user_id)
async def open_app_home(home_type: str, client: AsyncWebClient, user_id: str):
try:
await client.views_publish(view=get_loading_view(), user_id=user_id)
user = await env.db.user.find_unique(where={"id": user_id})
if not user:
user_info = await client.users_info(user=user_id) or {}
name = (
user_info.get("user", {}).get("profile", {}).get("display_name")
or user_info.get("user", {}).get("profile", {}).get("real_name")
or "person"
)
view = get_unknown_user_view(name)
else:
logging.info(f"Opening {home_type} for {user_id}")
match home_type:
case "default" | "dashboard":
view = await get_helper_view(user)
case "tags":
if user.admin:
view = await get_manage_tags_view(user)
else:
view = get_error_view(
"You do not have permission to access this page."
)
case "my-stats":
view = await get_stats_view(user)
case _:
await send_heartbeat(
f"Attempted to load unknown app home type {home_type} for <@{user_id}>"
)
view = get_error_view(
f"This shouldn't happen, please tell <@{env.slack_maintainer_id}> that app home case `_` was hit with home type `{home_type}`"
)
except Exception as e:
logging.error(f"Error opening app home: {e}")
tb = traceback.format_exception(e)
tb_str = "".join(tb)
view = get_error_view(
f"An error occurred while opening the app home: {e}",
traceback=tb_str,
)
err_type = type(e).__name__
await send_heartbeat(
f"`{err_type}` opening app home for <@{user_id}>",
messages=[f"```{tb_str}```", f"cc <@{env.slack_maintainer_id}>"],
)
await client.views_publish(user_id=user_id, view=view)

View file

@ -0,0 +1,23 @@
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()
else:
return

View file

@ -0,0 +1,28 @@
from slack_bolt.context.ack.async_ack import AsyncAck
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.utils.env import env
async def channel_left(ack: AsyncAck, event: dict, client: AsyncWebClient):
await ack()
user_id = event["user"]
channel_id = event["channel"]
if channel_id == env.slack_help_channel:
return
users = await client.usergroups_users_list(usergroup=env.slack_user_group)
new_users = users.get("users", [])
new_users.remove(user_id)
await client.usergroups_users_update(
usergroup=env.slack_user_group, users=new_users
)
await env.db.user.update(where={"id": user_id}, data={"helper": False})
match channel_id:
case env.slack_bts_channel:
await client.conversations_kick(channel=channel_id, user=user_id)
case env.slack_ticket_channel:
await client.conversations_kick(channel=channel_id, user=user_id)

View file

@ -5,6 +5,7 @@ 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
ALLOWED_SUBTYPES = ["file_share", "me_message"]
@ -13,7 +14,6 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
"""
Handle incoming messages in Slack.
"""
print("Received message event:", event)
if "subtype" in event and event["subtype"] not in ALLOWED_SUBTYPES:
return
@ -30,11 +30,23 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
past_tickets = await env.db.ticket.count(where={"openedById": user})
else:
past_tickets = 0
user_info = await client.users_info(user=user) or {}
username = user_info.get("user", {})[
"name"
] # this should never actually be empty but if it is, that is a major issue
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={
"id": user,
},
data={"create": {"id": user}, "update": {"id": user}},
data={
"create": {"id": user, "username": username},
"update": {"id": user, "username": username},
},
)
user_info = await client.users_info(user=user)
@ -50,6 +62,16 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
channel=env.slack_ticket_channel,
text=f"New message from <@{user}>: {text}",
blocks=[
{
"type": "input",
"label": {"type": "plain_text", "text": "Tag ticket", "emoji": True},
"element": {
"action_id": "tag-list",
"type": "multi_external_select",
"placeholder": {"type": "plain_text", "text": "Select tags"},
"min_query_length": 1,
},
},
{
"type": "context",
"elements": [
@ -58,7 +80,7 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
"text": f"Submitted by <@{user}>. They have {past_tickets} past tickets. <{thread_url}|View thread>.",
}
],
}
},
],
username=display_name or None,
icon_url=profile_pic or None,

23
nephthys/options/tags.py Normal file
View file

@ -0,0 +1,23 @@
from thefuzz import fuzz
from thefuzz import process
from nephthys.utils.env import env
async def get_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]:
tags = await env.db.tag.find_many()
keyword = payload.get("value")
if keyword:
tag_names = [tag.name for tag in tags]
scores = process.extract(keyword, tag_names, scorer=fuzz.ratio, limit=100)
old_tags = tags
tags = [old_tags[tag_names.index(score[0])] for score in scores]
return [
{
"text": {"type": "plain_text", "text": f"{tag.name}"},
"value": tag.id,
}
for tag in tags
]

View file

@ -0,0 +1,46 @@
import logging
from nephthys.utils.env import env
async def update_helpers():
res = await env.slack_client.conversations_members(channel=env.slack_bts_channel)
team_ids = res.get("members", [])
if not team_ids:
# if this happens then something concerning has happened :p
await env.slack_client.chat_postMessage(
channel=env.slack_bts_channel,
text=f"No members found in the bts channel. <@{env.slack_maintainer_id}>",
)
return
# unset helpers not in the team
await env.db.user.update_many(
where={"helper": True, "id": {"not_in": team_ids}},
data={"helper": False},
)
# update existing users in the db
await env.db.user.update_many(
where={"id": {"in": team_ids}},
data={"helper": True},
)
# create new users not in the db
existing_users_in_db = await env.db.user.find_many(where={"id": {"in": team_ids}})
existing_user_ids_in_db = {user.id for user in existing_users_in_db}
new_member_data_to_create = []
for member_id in team_ids:
if member_id not in existing_user_ids_in_db:
user_info = await env.slack_client.users_info(user=member_id)
logging.info(
f"Creating new helper user {member_id} with info {user_info.get('name')}"
)
new_member_data_to_create.append(
{"id": member_id, "helper": True, "username": user_info["name"]}
)
if new_member_data_to_create:
await env.db.user.create_many(data=new_member_data_to_create)

View file

@ -19,6 +19,9 @@ class Environment:
self.environment = os.environ.get("ENVIRONMENT", "development")
self.slack_help_channel = os.environ.get("SLACK_HELP_CHANNEL", "unset")
self.slack_ticket_channel = os.environ.get("SLACK_TICKET_CHANNEL", "unset")
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.port = int(os.environ.get("PORT", 3000))

View file

@ -5,8 +5,17 @@ from slack_bolt.async_app import AsyncApp
from slack_bolt.context.ack.async_ack import AsyncAck
from slack_sdk.web.async_client import AsyncWebClient
from nephthys.actions.assign_tag import assign_tag_callback
from nephthys.actions.create_tag import create_tag_btn_callback
from nephthys.actions.create_tag import create_tag_view_callback
from nephthys.actions.resolve import resolve
from nephthys.actions.tag_subscribe import tag_subscribe_callback
from nephthys.events.app_home_opened import on_app_home_opened
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.options.tags import get_tags
from nephthys.utils.env import env
app = AsyncApp(token=env.slack_bot_token, signing_secret=env.slack_signing_secret)
@ -26,3 +35,55 @@ async def handle_mark_resolved_button(
value = body["actions"][0]["value"]
resolver = body["user"]["id"]
await resolve(value, resolver, client)
@app.options("tag-list")
async def handle_tag_list_options(ack: AsyncAck, payload: dict):
tags = await get_tags(payload)
await ack(options=tags)
@app.event("app_home_opened")
async def app_home_opened_handler(event: dict[str, Any], client: AsyncWebClient):
await on_app_home_opened(event, client)
@app.action("dashboard")
@app.action("tags")
@app.action("my-stats")
async def manage_home_switcher(ack: AsyncAck, body, client: AsyncWebClient):
await ack()
user_id = body["user"]["id"]
action_id = body["actions"][0]["action_id"]
await open_app_home(action_id, client, user_id)
@app.event("member_joined_channel")
async def handle_member_joined_channel(event: Dict[str, Any], client: AsyncWebClient):
await channel_join(ack=AsyncAck(), event=event, client=client)
@app.event("member_left_channel")
async def handle_member_left_channel(event: Dict[str, Any], client: AsyncWebClient):
await channel_left(ack=AsyncAck(), event=event, client=client)
@app.action("create-tag")
async def create_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
await create_tag_btn_callback(ack, body, client)
@app.view("create_tag")
async def create_tag_view(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
await create_tag_view_callback(ack, body, client)
@app.action("tag-subscribe")
async def tag_subscribe(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
await tag_subscribe_callback(ack, body, client)
@app.action("tag-list")
async def assign_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
await assign_tag_callback(ack, body, client)

View file

@ -0,0 +1,35 @@
from prisma.models import User
def get_buttons(user: User, current: str = "dashboard"):
buttons = []
buttons.append(
{
"type": "button",
"text": {"type": "plain_text", "text": "Dashboard", "emoji": True},
"action_id": "dashboard",
**({"style": "primary"} if current != "dashboard" else {}),
}
)
if user.admin:
buttons.append(
{
"type": "button",
"text": {"type": "plain_text", "text": "Tags", "emoji": True},
"action_id": "tags",
**({"style": "primary"} if current != "tags" else {}),
}
)
buttons.append(
{
"type": "button",
"text": {"type": "plain_text", "text": "My Stats", "emoji": True},
"action_id": "my-stats",
**({"style": "primary"} if current != "my-stats" else {}),
}
)
blocks = {"type": "actions", "elements": buttons}
return blocks

View file

@ -0,0 +1,19 @@
def get_error_view(msg: str, traceback: str | None = None):
if traceback:
msg = f"{msg}\n\nTraceback:\n```{traceback}```"
return {
"type": "home",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Sorry, something went wrong. Please try again later.",
},
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"Error message\n{msg}"},
},
],
}

View file

@ -0,0 +1,42 @@
from nephthys.utils.env import env
from nephthys.views.home.components.buttons import get_buttons
from prisma.enums import TicketStatus
from prisma.models import User
async def get_helper_view(user: User):
tickets = await env.db.ticket.find_many() or []
organised_tkts = {}
for ticket in tickets:
status = ticket.status
if status not in organised_tkts:
organised_tkts[status] = []
organised_tkts[status].append(ticket)
formatted_msg = f"""
*Requests*
{len(tickets)} requests found
{len(organised_tkts.get(TicketStatus.OPEN, []))} open
{len(organised_tkts.get(TicketStatus.IN_PROGRESS, []))} in progress ({len([ticket for ticket in tickets if ticket.status == TicketStatus.IN_PROGRESS and ticket.assignedToId == user.id])} assigned to you)
{len(organised_tkts.get(TicketStatus.CLOSED, []))} closed ({len([ticket for ticket in tickets if ticket.status == TicketStatus.CLOSED and ticket.closedById == user.id])} closed by you)
"""
btns = get_buttons(user, "dashboard")
return {
"type": "home",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":rac_cute: helper heidi",
"emoji": True,
},
},
btns,
{"type": "divider"},
{"type": "section", "text": {"type": "mrkdwn", "text": formatted_msg}},
],
}

View file

@ -0,0 +1,21 @@
def get_loading_view():
return {
"type": "home",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":hourglass_flowing_sand: loading...",
},
},
{
"type": "divider",
},
{
"type": "image",
"image_url": "https://hc-cdn.hel1.your-objectstorage.com/s/v3/1c1fc5fb03b8bf46c6ab047c97f962ed930616f0_loading-hugs.gif",
"alt_text": "Loading...",
},
],
}

View file

@ -0,0 +1,29 @@
from nephthys.views.home.components.buttons import get_buttons
from prisma.models import User
async def get_stats_view(user: User):
btns = get_buttons(user, "my-stats")
return {
"type": "home",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":rac_info: My Stats",
"emoji": True,
},
},
btns,
{"type": "divider"},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rac_cute: weh im just a silly raccoon, did you expect me to have stats :rac_ded: i don't even have a job >:(",
},
},
],
}

106
nephthys/views/home/tags.py Normal file
View file

@ -0,0 +1,106 @@
import logging
from nephthys.utils.env import env
from nephthys.views.home.components.buttons import get_buttons
from prisma.models import User
async def get_manage_tags_view(user: User) -> dict:
btns = get_buttons(user, "tags")
tags = await env.db.tag.find_many(include={"userSubscriptions": True})
blocks = []
if not tags:
blocks.append(
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rac_nooo: i couldn't scrounge up any tags, you can make a new one below though",
},
}
)
else:
blocks.append(
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rac_info: here are the tags i found!",
},
}
)
for tag in tags:
logging.info(f"Tag {tag.name} with id {tag.id} found in the database")
logging.info(
f"Tag {tag.name} has {len(tag.userSubscriptions) if tag.userSubscriptions else 0} subscriptions"
)
if tag.userSubscriptions:
subs = [user.userId for user in tag.userSubscriptions]
else:
subs = []
stringified_subs = [f"<@{user}>" for user in subs]
blocks.append(
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{tag.name}* - {''.join(stringified_subs) if stringified_subs else ':rac_nooo: no subscriptions'}",
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": f":rac_cute: {'subscribe' if user.id not in subs else 'unsubscribe'}",
"emoji": True,
},
"action_id": "tag-subscribe",
"value": f"{tag.id};{tag.name}",
"style": "primary" if user.id not in subs else "danger",
},
}
)
return {
"type": "home",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":rac_info: Manage Tags",
"emoji": True,
},
},
btns,
{"type": "divider"},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rac_thumbs: here you can manage tags and user subscriptions",
},
},
{"type": "divider"},
*blocks,
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": ":rac_cute: add a tag?",
"emoji": True,
},
"action_id": "create-tag",
"style": "primary",
}
],
},
],
}

View file

@ -0,0 +1,21 @@
from nephthys.data.transcript import Transcript
def get_unknown_user_view(name: str):
return {
"type": "home",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": Transcript.home_unknown_user_title.format(name=name),
"emoji": True,
},
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": Transcript.home_unknown_user_text},
},
],
}

View file

@ -0,0 +1,30 @@
def get_create_tag_modal():
return {
"type": "modal",
"callback_id": "create_tag",
"title": {
"type": "plain_text",
"text": ":rac_info: create a tag!",
"emoji": True,
},
"blocks": [
{
"type": "input",
"block_id": "tag_name",
"label": {
"type": "plain_text",
"text": "giv name?",
"emoji": True,
},
"element": {
"type": "plain_text_input",
"action_id": "tag_name",
},
},
],
"submit": {
"type": "plain_text",
"text": ":rac_question: add tag?",
"emoji": True,
},
}

View file

@ -16,13 +16,17 @@ enum TicketStatus {
}
model User {
id String @id @unique
admin Boolean @default(false)
id String @id @unique
username String? @unique
admin Boolean @default(false)
openedTickets Ticket[] @relation("OpenedTickets")
closedTickets Ticket[] @relation("ClosedTickets")
assignedTickets Ticket[] @relation("AssignedTickets")
tagSubscriptions UserTagSubscription[]
helper Boolean @default(false)
createdAt DateTime @default(now())
}
@ -38,12 +42,48 @@ model Ticket {
openedBy User @relation("OpenedTickets", fields: [openedById], references: [id])
openedById String
closedBy User? @relation("ClosedTickets", fields: [closedById], references: [id])
closedBy User? @relation("ClosedTickets", fields: [closedById], references: [id])
closedById String?
closedAt DateTime?
closedAt DateTime?
assignedTo User? @relation("AssignedTickets", fields: [assignedToId], references: [id])
assignedToId String?
tagsOnTickets TagsOnTickets[]
createdAt DateTime @default(now())
}
model Tag {
id String @id @unique @default(cuid())
name String @unique
ticketsOnTags TagsOnTickets[]
userSubscriptions UserTagSubscription[]
createdAt DateTime @default(now())
}
model TagsOnTickets {
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
ticketId String
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
tagId String
assignedAt DateTime @default(now())
@@id([ticketId, tagId])
@@map("tags_on_tickets")
}
model UserTagSubscription {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
tagId String
subscribedAt DateTime @default(now())
@@id([userId, tagId])
@@map("user_tag_subscriptions")
}

View file

@ -11,6 +11,7 @@ dependencies = [
"python-dotenv>=1.1.0",
"slack-bolt>=1.23.0",
"starlette>=0.46.1",
"thefuzz>=0.22.1",
"uvicorn>=0.34.0",
"uvloop>=0.21.0",
]

37
uv.lock generated
View file

@ -313,6 +313,7 @@ dependencies = [
{ name = "python-dotenv" },
{ name = "slack-bolt" },
{ name = "starlette" },
{ name = "thefuzz" },
{ name = "uvicorn" },
{ name = "uvloop" },
]
@ -330,6 +331,7 @@ requires-dist = [
{ name = "python-dotenv", specifier = ">=1.1.0" },
{ name = "slack-bolt", specifier = ">=1.23.0" },
{ name = "starlette", specifier = ">=0.46.1" },
{ name = "thefuzz", specifier = ">=0.22.1" },
{ name = "uvicorn", specifier = ">=0.34.0" },
{ name = "uvloop", specifier = ">=0.21.0" },
]
@ -500,6 +502,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
]
[[package]]
name = "rapidfuzz"
version = "3.13.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/76/606e71e4227790750f1646f3c5c873e18d6cfeb6f9a77b2b8c4dec8f0f66/rapidfuzz-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09e908064d3684c541d312bd4c7b05acb99a2c764f6231bd507d4b4b65226c23", size = 1982282 },
{ url = "https://files.pythonhosted.org/packages/0a/f5/d0b48c6b902607a59fd5932a54e3518dae8223814db8349b0176e6e9444b/rapidfuzz-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:57c390336cb50d5d3bfb0cfe1467478a15733703af61f6dffb14b1cd312a6fae", size = 1439274 },
{ url = "https://files.pythonhosted.org/packages/59/cf/c3ac8c80d8ced6c1f99b5d9674d397ce5d0e9d0939d788d67c010e19c65f/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0da54aa8547b3c2c188db3d1c7eb4d1bb6dd80baa8cdaeaec3d1da3346ec9caa", size = 1399854 },
{ url = "https://files.pythonhosted.org/packages/09/5d/ca8698e452b349c8313faf07bfa84e7d1c2d2edf7ccc67bcfc49bee1259a/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df8e8c21e67afb9d7fbe18f42c6111fe155e801ab103c81109a61312927cc611", size = 5308962 },
{ url = "https://files.pythonhosted.org/packages/66/0a/bebada332854e78e68f3d6c05226b23faca79d71362509dbcf7b002e33b7/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:461fd13250a2adf8e90ca9a0e1e166515cbcaa5e9c3b1f37545cbbeff9e77f6b", size = 1625016 },
{ url = "https://files.pythonhosted.org/packages/de/0c/9e58d4887b86d7121d1c519f7050d1be5eb189d8a8075f5417df6492b4f5/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2b3dd5d206a12deca16870acc0d6e5036abeb70e3cad6549c294eff15591527", size = 1600414 },
{ url = "https://files.pythonhosted.org/packages/9b/df/6096bc669c1311568840bdcbb5a893edc972d1c8d2b4b4325c21d54da5b1/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1343d745fbf4688e412d8f398c6e6d6f269db99a54456873f232ba2e7aeb4939", size = 3053179 },
{ url = "https://files.pythonhosted.org/packages/f9/46/5179c583b75fce3e65a5cd79a3561bd19abd54518cb7c483a89b284bf2b9/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b1b065f370d54551dcc785c6f9eeb5bd517ae14c983d2784c064b3aa525896df", size = 2456856 },
{ url = "https://files.pythonhosted.org/packages/6b/64/e9804212e3286d027ac35bbb66603c9456c2bce23f823b67d2f5cabc05c1/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:11b125d8edd67e767b2295eac6eb9afe0b1cdc82ea3d4b9257da4b8e06077798", size = 7567107 },
{ url = "https://files.pythonhosted.org/packages/8a/f2/7d69e7bf4daec62769b11757ffc31f69afb3ce248947aadbb109fefd9f65/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c33f9c841630b2bb7e69a3fb5c84a854075bb812c47620978bddc591f764da3d", size = 2854192 },
{ url = "https://files.pythonhosted.org/packages/05/21/ab4ad7d7d0f653e6fe2e4ccf11d0245092bef94cdff587a21e534e57bda8/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae4574cb66cf1e85d32bb7e9ec45af5409c5b3970b7ceb8dea90168024127566", size = 3398876 },
{ url = "https://files.pythonhosted.org/packages/0f/a8/45bba94c2489cb1ee0130dcb46e1df4fa2c2b25269e21ffd15240a80322b/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e05752418b24bbd411841b256344c26f57da1148c5509e34ea39c7eb5099ab72", size = 4377077 },
{ url = "https://files.pythonhosted.org/packages/0c/f3/5e0c6ae452cbb74e5436d3445467447e8c32f3021f48f93f15934b8cffc2/rapidfuzz-3.13.0-cp313-cp313-win32.whl", hash = "sha256:0e1d08cb884805a543f2de1f6744069495ef527e279e05370dd7c83416af83f8", size = 1822066 },
{ url = "https://files.pythonhosted.org/packages/96/e3/a98c25c4f74051df4dcf2f393176b8663bfd93c7afc6692c84e96de147a2/rapidfuzz-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9a7c6232be5f809cd39da30ee5d24e6cadd919831e6020ec6c2391f4c3bc9264", size = 1615100 },
{ url = "https://files.pythonhosted.org/packages/60/b1/05cd5e697c00cd46d7791915f571b38c8531f714832eff2c5e34537c49ee/rapidfuzz-3.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:3f32f15bacd1838c929b35c84b43618481e1b3d7a61b5ed2db0291b70ae88b53", size = 858976 },
]
[[package]]
name = "slack-bolt"
version = "1.23.0"
@ -542,6 +567,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
]
[[package]]
name = "thefuzz"
version = "0.22.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "rapidfuzz" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/4b/d3eb25831590d6d7d38c2f2e3561d3ba41d490dc89cd91d9e65e7c812508/thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680", size = 19993 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245 },
]
[[package]]
name = "tomlkit"
version = "0.13.3"