mirror of
https://github.com/System-End/nephthys.git
synced 2026-04-19 15:18:21 +00:00
?tag macro (#174)
* add lynx * stale thread fix, add ?tag amcro, config categories from menu * copilot actually right * comment out stale ticket logic so doesnt cause havoc * pre commit * ratelimit
This commit is contained in:
parent
0d5a810707
commit
575fa89192
12 changed files with 272 additions and 22 deletions
|
|
@ -34,9 +34,7 @@ Sometimes it’s nice to be able to do things quickly... Here’s where macros c
|
|||
|
||||
### Stale
|
||||
|
||||
~~Tickets that have been not had a response for more than 3 days will automatically be closed as stale. The last helper to respond in the thread gets credit for closing them~~
|
||||
|
||||
Stale ticket handling is not working at the moment, but more features for dealing with stale tickets are planned.
|
||||
Tickets that have not had a response for more than 3 days will automatically be closed as stale. The last helper to respond in the thread gets credit for closing them
|
||||
|
||||
### Leaderboard
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import uvicorn
|
||||
from aiohttp import ClientSession
|
||||
|
|
@ -9,7 +8,6 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|||
from dotenv import load_dotenv
|
||||
from starlette.applications import Starlette
|
||||
|
||||
from nephthys.tasks.close_stale import close_stale_tickets
|
||||
from nephthys.tasks.daily_stats import send_daily_stats
|
||||
from nephthys.tasks.fulfillment_reminder import send_fulfillment_reminder
|
||||
from nephthys.tasks.update_helpers import update_helpers
|
||||
|
|
@ -56,13 +54,13 @@ async def main(_app: Starlette):
|
|||
timezone="Europe/London",
|
||||
)
|
||||
|
||||
scheduler.add_job(
|
||||
close_stale_tickets,
|
||||
"interval",
|
||||
hours=1,
|
||||
max_instances=1,
|
||||
next_run_time=datetime.now(),
|
||||
)
|
||||
# scheduler.add_job(
|
||||
# close_stale_tickets,
|
||||
# "interval",
|
||||
# hours=1,
|
||||
# max_instances=1,
|
||||
# next_run_time=datetime.now(),
|
||||
# )
|
||||
scheduler.start()
|
||||
|
||||
delete_msg_task = asyncio.create_task(process_queue())
|
||||
|
|
|
|||
73
nephthys/actions/create_category_tag.py
Normal file
73
nephthys/actions/create_category_tag.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import logging
|
||||
|
||||
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
|
||||
from nephthys.views.modals.create_category_tag import get_create_category_tag_modal
|
||||
from prisma.errors import UniqueViolationError
|
||||
|
||||
|
||||
async def create_category_tag_btn_callback(
|
||||
ack: AsyncAck, body: dict, client: AsyncWebClient
|
||||
):
|
||||
await ack()
|
||||
user_id = body["user"]["id"]
|
||||
trigger_id = body["trigger_id"]
|
||||
|
||||
user = await env.db.user.find_unique(where={"slackId": user_id})
|
||||
if not user or not user.admin:
|
||||
await send_heartbeat(
|
||||
f"Attempted to open create category tag modal by non-admin user <@{user_id}>"
|
||||
)
|
||||
return
|
||||
|
||||
view = get_create_category_tag_modal()
|
||||
await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id)
|
||||
|
||||
|
||||
async def create_category_tag_view_callback(
|
||||
ack: AsyncAck, body: dict, client: AsyncWebClient
|
||||
):
|
||||
user_id = body["user"]["id"]
|
||||
|
||||
raw_name = body["view"]["state"]["values"]["category_tag_name"][
|
||||
"category_tag_name"
|
||||
]["value"]
|
||||
name = raw_name.strip() if raw_name else ""
|
||||
|
||||
if not name:
|
||||
await ack(
|
||||
response_action="errors",
|
||||
errors={"category_tag_name": "Category name cannot be empty."},
|
||||
)
|
||||
return
|
||||
|
||||
user = await env.db.user.find_unique(where={"slackId": user_id})
|
||||
if not user or not user.admin:
|
||||
await ack()
|
||||
await send_heartbeat(
|
||||
f"Attempted to create category tag by non-admin user <@{user_id}>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await env.db.categorytag.create(
|
||||
data={"name": name, "createdBy": {"connect": {"id": user.id}}}
|
||||
)
|
||||
except UniqueViolationError:
|
||||
logging.warning(f"Duplicate category tag name: {name}")
|
||||
await ack(
|
||||
response_action="errors",
|
||||
errors={
|
||||
"category_tag_name": f"A category tag named '{name}' already exists."
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
await ack()
|
||||
|
||||
from nephthys.events.app_home_opened import open_app_home
|
||||
|
||||
await open_app_home("category-tags", client, user_id)
|
||||
|
|
@ -10,6 +10,7 @@ from nephthys.utils.env import env
|
|||
from nephthys.utils.logging import send_heartbeat
|
||||
from nephthys.utils.performance import perf_timer
|
||||
from nephthys.views.home.assigned import get_assigned_tickets_view
|
||||
from nephthys.views.home.category_tags import get_category_tags_view
|
||||
from nephthys.views.home.dashboard import get_dashboard_view
|
||||
from nephthys.views.home.error import get_error_view
|
||||
from nephthys.views.home.loading import get_loading_view
|
||||
|
|
@ -56,6 +57,8 @@ async def open_app_home(home_type: str, client: AsyncWebClient, user_id: str):
|
|||
view = await get_assigned_tickets_view(user)
|
||||
case "team-tags":
|
||||
view = await get_team_tags_view(user)
|
||||
case "category-tags":
|
||||
view = await get_category_tags_view(user)
|
||||
case "my-stats":
|
||||
view = await get_stats_view(user)
|
||||
case _:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from nephthys.macros.reopen import Reopen
|
|||
from nephthys.macros.resolve import Resolve
|
||||
from nephthys.macros.shipcertqueue import ShipCertQueue
|
||||
from nephthys.macros.shipwrights import Shipwrights
|
||||
from nephthys.macros.team_tag import TeamTag
|
||||
from nephthys.macros.thread import Thread
|
||||
from nephthys.macros.trigger_daily_stats import DailyStats
|
||||
from nephthys.macros.trigger_fulfillment_reminder import FulfillmentReminder
|
||||
|
|
@ -18,7 +19,6 @@ from prisma.enums import TicketStatus
|
|||
from prisma.models import Ticket
|
||||
from prisma.models import User
|
||||
|
||||
|
||||
macro_list: list[type[Macro]] = [
|
||||
Resolve,
|
||||
HelloWorld,
|
||||
|
|
@ -31,6 +31,7 @@ macro_list: list[type[Macro]] = [
|
|||
DailyStats,
|
||||
FulfillmentReminder,
|
||||
Shipwrights,
|
||||
TeamTag,
|
||||
]
|
||||
|
||||
macros = [macro() for macro in macro_list]
|
||||
|
|
|
|||
84
nephthys/macros/team_tag.py
Normal file
84
nephthys/macros/team_tag.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
from nephthys.macros.types import Macro
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.ticket_methods import get_backend_message_link
|
||||
from nephthys.utils.ticket_methods import get_question_message_link
|
||||
|
||||
|
||||
class TeamTag(Macro):
|
||||
name = "tag"
|
||||
|
||||
async def run(self, ticket, helper, **kwargs):
|
||||
text: str = kwargs.get("text", "")
|
||||
parts = text.split(maxsplit=1)
|
||||
if len(parts) < 2 or not parts[1].strip():
|
||||
await env.slack_client.chat_postEphemeral(
|
||||
channel=env.slack_help_channel,
|
||||
thread_ts=ticket.msgTs,
|
||||
user=helper.slackId,
|
||||
text="Usage: `?tag <tag name>`",
|
||||
)
|
||||
return
|
||||
|
||||
tag_name = parts[1].strip()
|
||||
|
||||
all_tags = await env.db.tag.find_many()
|
||||
|
||||
tag = next((t for t in all_tags if t.name == tag_name), None)
|
||||
if not tag:
|
||||
matches = [t for t in all_tags if t.name.lower() == tag_name.lower()]
|
||||
tag = matches[0] if matches else None
|
||||
|
||||
if not tag:
|
||||
names = ", ".join(f"`{t.name}`" for t in all_tags)
|
||||
await env.slack_client.chat_postEphemeral(
|
||||
channel=env.slack_help_channel,
|
||||
thread_ts=ticket.msgTs,
|
||||
user=helper.slackId,
|
||||
text=f"Tag `{tag_name}` not found. Available tags: {names}"
|
||||
if names
|
||||
else f"Tag `{tag_name}` not found. No tags exist yet.",
|
||||
)
|
||||
return
|
||||
|
||||
existing = await env.db.tagsontickets.find_first(
|
||||
where={"ticketId": ticket.id, "tagId": tag.id}
|
||||
)
|
||||
if existing:
|
||||
await env.slack_client.chat_postEphemeral(
|
||||
channel=env.slack_help_channel,
|
||||
thread_ts=ticket.msgTs,
|
||||
user=helper.slackId,
|
||||
text=f"Tag `{tag.name}` is already on this ticket.",
|
||||
)
|
||||
return
|
||||
|
||||
await env.db.tagsontickets.create(
|
||||
data={
|
||||
"tag": {"connect": {"id": tag.id}},
|
||||
"ticket": {"connect": {"id": ticket.id}},
|
||||
}
|
||||
)
|
||||
|
||||
subscriptions = await env.db.usertagsubscription.find_many(
|
||||
where={"tagId": tag.id}
|
||||
)
|
||||
subscriber_ids = [s.userId for s in subscriptions if s.userId != helper.id]
|
||||
|
||||
if subscriber_ids:
|
||||
subscribers = await env.db.user.find_many(
|
||||
where={"id": {"in": subscriber_ids}}
|
||||
)
|
||||
url = get_question_message_link(ticket)
|
||||
ticket_url = get_backend_message_link(ticket)
|
||||
for user in subscribers:
|
||||
await env.slack_client.chat_postMessage(
|
||||
channel=user.slackId,
|
||||
text=f"New ticket for *{tag.name}*: *{ticket.title}*\n<{url}|ticket> <{ticket_url}|bts ticket>",
|
||||
)
|
||||
|
||||
await env.slack_client.chat_postEphemeral(
|
||||
channel=env.slack_help_channel,
|
||||
thread_ts=ticket.msgTs,
|
||||
user=helper.slackId,
|
||||
text=f"Tagged `{tag.name}`.",
|
||||
)
|
||||
|
|
@ -42,14 +42,14 @@ async def get_is_stale(ts: str, max_retries: int = 3) -> bool:
|
|||
if attempt == max_retries - 1:
|
||||
logging.error(f"Max retries exceeded for ticket {ts}")
|
||||
return False
|
||||
if e.response["error"] == "thread_not_found":
|
||||
continue
|
||||
elif e.response["error"] == "thread_not_found":
|
||||
logging.warning(
|
||||
f"Thread not found for ticket {ts}. This might be a deleted thread."
|
||||
)
|
||||
await send_heartbeat(f"Thread not found for ticket {ts}.")
|
||||
maintainer_user = await env.db.user.find_unique(
|
||||
# where={"slackId": env.slack_maintainer_id}
|
||||
where={"slackId": "U054VC2KM9P"}
|
||||
where={"slackId": env.slack_maintainer_id}
|
||||
)
|
||||
if maintainer_user:
|
||||
await env.db.ticket.update(
|
||||
|
|
@ -109,14 +109,17 @@ async def close_stale_tickets():
|
|||
|
||||
if await get_is_stale(ticket.msgTs):
|
||||
stale += 1
|
||||
resolver = (
|
||||
ticket.assignedToId
|
||||
if ticket.assignedToId
|
||||
else ticket.openedById
|
||||
resolver_user = (
|
||||
ticket.assignedTo if ticket.assignedTo else ticket.openedBy
|
||||
)
|
||||
if not resolver_user:
|
||||
logging.warning(
|
||||
f"Skipping stale ticket {ticket.msgTs}: no assigned or opened user"
|
||||
)
|
||||
continue
|
||||
await resolve(
|
||||
ticket.msgTs,
|
||||
resolver, # type: ignore (this is explicitly fetched in the db call)
|
||||
resolver_user.slackId,
|
||||
env.slack_client,
|
||||
stale=True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,6 @@ if your question has been answered, please hit the button below to mark it as re
|
|||
"""
|
||||
ticket_create: str = f"someone should be along to help you soon but in the meantime 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"
|
||||
resolve_ticket_button: str = "i get it now"
|
||||
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 raccoon ^-^"
|
||||
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 capybara ^-^"
|
||||
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from slack_sdk.web.async_client import AsyncWebClient
|
|||
|
||||
from nephthys.actions.assign_category_tag import assign_category_tag_callback
|
||||
from nephthys.actions.assign_team_tag import assign_team_tag_callback
|
||||
from nephthys.actions.create_category_tag import create_category_tag_btn_callback
|
||||
from nephthys.actions.create_category_tag import create_category_tag_view_callback
|
||||
from nephthys.actions.create_team_tag import create_team_tag_btn_callback
|
||||
from nephthys.actions.create_team_tag import create_team_tag_view_callback
|
||||
from nephthys.actions.resolve import resolve
|
||||
|
|
@ -106,6 +108,20 @@ async def create_team_tag_view(
|
|||
await create_team_tag_view_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.action("create-category-tag")
|
||||
async def create_category_tag(
|
||||
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
|
||||
):
|
||||
await create_category_tag_btn_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.view("create_category_tag")
|
||||
async def create_category_tag_view(
|
||||
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
|
||||
):
|
||||
await create_category_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)
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@ APP_HOME_VIEWS: list[View] = [
|
|||
View("Dashboard", "dashboard"),
|
||||
View("Assigned Tickets", "assigned-tickets"),
|
||||
View("Team Tags", "team-tags"),
|
||||
View("Category Tags", "category-tags"),
|
||||
View("My Stats", "my-stats"),
|
||||
]
|
||||
|
|
|
|||
53
nephthys/views/home/category_tags.py
Normal file
53
nephthys/views/home/category_tags.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from blockkit import Actions
|
||||
from blockkit import Button
|
||||
from blockkit import Divider
|
||||
from blockkit import Header
|
||||
from blockkit import Home
|
||||
from blockkit import Section
|
||||
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.views.home.components.header import get_header_components
|
||||
from prisma.models import User
|
||||
|
||||
|
||||
async def get_category_tags_view(user: User | None) -> dict:
|
||||
is_admin = bool(user and user.admin)
|
||||
|
||||
header = get_header_components(user, "category-tags")
|
||||
|
||||
if not is_admin:
|
||||
return Home(
|
||||
[
|
||||
*header,
|
||||
Header(":rac_info: Category Tags"),
|
||||
Section(":rac_nooo: only admins can manage category tags."),
|
||||
]
|
||||
).build()
|
||||
|
||||
category_tags = await env.db.categorytag.find_many(order={"id": "asc"})
|
||||
|
||||
tag_blocks = []
|
||||
if not category_tags:
|
||||
tag_blocks.append(Section(":rac_nooo: no category tags yet — add one below!"))
|
||||
else:
|
||||
for tag in category_tags:
|
||||
tag_blocks.append(Section(f"*{tag.name}*"))
|
||||
|
||||
return Home(
|
||||
[
|
||||
*header,
|
||||
Header(":rac_info: Category Tags"),
|
||||
Section(":rac_thumbs: manage category tags used by the AI classifier"),
|
||||
Divider(),
|
||||
*tag_blocks,
|
||||
Actions(
|
||||
elements=[
|
||||
Button(
|
||||
text=":rac_cute: add category",
|
||||
action_id="create-category-tag",
|
||||
style=Button.PRIMARY,
|
||||
)
|
||||
]
|
||||
),
|
||||
]
|
||||
).build()
|
||||
20
nephthys/views/modals/create_category_tag.py
Normal file
20
nephthys/views/modals/create_category_tag.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from blockkit import Input
|
||||
from blockkit import Modal
|
||||
from blockkit import PlainTextInput
|
||||
|
||||
|
||||
def get_create_category_tag_modal():
|
||||
return Modal(
|
||||
title=":rac_info: new category",
|
||||
callback_id="create_category_tag",
|
||||
submit=":rac_question: create",
|
||||
blocks=[
|
||||
Input(
|
||||
label="Category name",
|
||||
block_id="category_tag_name",
|
||||
element=PlainTextInput(action_id="category_tag_name"),
|
||||
),
|
||||
# future: description
|
||||
# future: slug
|
||||
],
|
||||
).build()
|
||||
Loading…
Add table
Reference in a new issue