add stale closing

This commit is contained in:
transcental 2025-07-14 16:31:55 +01:00
parent 56f184718a
commit 36b488d9d3
4 changed files with 99 additions and 2 deletions

View file

@ -1,6 +1,7 @@
import asyncio
import contextlib
import logging
from datetime import datetime
import uvicorn
from aiohttp import ClientSession
@ -8,6 +9,7 @@ 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.update_helpers import update_helpers
from nephthys.utils.delete_thread import process_queue
@ -35,6 +37,13 @@ async def main(_app: Starlette):
scheduler = AsyncIOScheduler(timezone="Europe/London")
scheduler.add_job(send_daily_stats, "cron", hour=0, minute=0)
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())

View file

@ -9,7 +9,7 @@ from nephthys.utils.permissions import can_resolve
from prisma.enums import TicketStatus
async def resolve(ts: str, resolver: str, client: AsyncWebClient):
async def resolve(ts: str, resolver: str, client: AsyncWebClient, stale: bool = False):
resolving_user = await env.db.user.find_unique(where={"slackId": resolver})
if not resolving_user:
await send_heartbeat(
@ -57,7 +57,9 @@ async def resolve(ts: str, resolver: str, client: AsyncWebClient):
await client.chat_postMessage(
channel=env.slack_help_channel,
text=env.transcript.ticket_resolve.format(user_id=resolver),
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,
)

View file

@ -0,0 +1,77 @@
import asyncio
import logging
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from slack_sdk.errors import SlackApiError
from nephthys.actions.resolve import resolve
from nephthys.utils.env import env
from nephthys.utils.logging import send_heartbeat
from prisma.enums import TicketStatus
async def get_is_stale(ts: str) -> bool:
try:
replies = await env.slack_client.conversations_replies(
channel=env.slack_help_channel, ts=ts, limit=1000
)
last_reply = (
replies.get("messages", [])[-1] if replies.get("messages") else None
)
if not last_reply:
logging.error("No replies found - this should never happen")
await send_heartbeat(f"No replies found for ticket {ts}")
return False
return (
datetime.now(tz=timezone.utc)
- datetime.fromtimestamp(float(ts), tz=timezone.utc)
) > timedelta(days=3)
except SlackApiError as e:
if e.response["error"] == "ratelimited":
retry_after = int(e.response.headers.get("Retry-After", 1))
logging.warning(
f"Rate limited while fetching replies for ticket {ts}. Retrying after {retry_after} seconds."
)
await asyncio.sleep(retry_after)
return await get_is_stale(ts)
else:
logging.error(
f"Error fetching replies for ticket {ts}: {e.response['error']}"
)
await send_heartbeat(
f"Error fetching replies for ticket {ts}: {e.response['error']}"
)
return False
async def close_stale_tickets():
"""
Closes tickets that have been open for more than 7 days.
This task is intended to be run periodically.
"""
logging.info("Closing stale tickets...")
await send_heartbeat("Closing stale tickets...")
try:
tickets = await env.db.ticket.find_many(
where={"NOT": [{"status": TicketStatus.CLOSED}]},
include={
"openedBy": True,
},
)
stale_tickets = [
ticket for ticket in tickets if await get_is_stale(ticket.msgTs)
]
for ticket in stale_tickets:
await resolve(ticket.msgTs, ticket.openedBy.slackId, env.slack_client) # type: ignore (this is valid - see include above)
await send_heartbeat(f"Closed {len(stale_tickets)} stale tickets.")
logging.info(f"Closed {len(stale_tickets)} stale tickets.")
except Exception as e:
logging.error(f"Error closing stale tickets: {e}")
await send_heartbeat(f"Error closing stale tickets: {e}")

View file

@ -59,6 +59,11 @@ class Transcript(BaseModel):
default="", description="Message when ticket is resolved"
)
ticket_resolve_stale: str = Field(
default="",
description="Message when ticket is resolved due to being stale",
)
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",
)
@ -116,6 +121,10 @@ if your question has been answered, please hit the button below to mark it as re
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.ticket_resolve_stale:
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.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!"