From 9b7f536bf498d8453cbc55589cf254a653876fe7 Mon Sep 17 00:00:00 2001 From: End Date: Wed, 11 Mar 2026 23:25:19 -0700 Subject: [PATCH 1/4] stale tickets config --- docs/deployment.md | 11 ++ nephthys/__main__.py | 17 ++- nephthys/actions/settings.py | 125 ++++++++++++++++++ nephthys/events/app_home_opened.py | 3 + nephthys/tasks/close_stale.py | 25 +++- nephthys/utils/env.py | 15 +++ nephthys/utils/slack.py | 24 ++++ nephthys/views/home/__init__.py | 1 + nephthys/views/home/settings.py | 72 ++++++++++ nephthys/views/modals/configure_stale_days.py | 30 +++++ prisma/schema.prisma | 24 ++-- 11 files changed, 328 insertions(+), 19 deletions(-) create mode 100644 nephthys/actions/settings.py create mode 100644 nephthys/views/home/settings.py create mode 100644 nephthys/views/modals/configure_stale_days.py diff --git a/docs/deployment.md b/docs/deployment.md index 6b09b6a..ac81122 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -102,6 +102,17 @@ Note: These steps have to be done by a Workspace Admin (otherwise it will be una 4. Don't forget to click **Save All Environment Variables** +## Configuring Bot Settings + +After deployment, you can configure bot settings (like stale ticket auto-close) from the Slack App Home: + +1. Open the bot's App Home in Slack +2. Navigate to the **Settings** tab +3. Configure the stale ticket auto-close feature: + - Click **Configure** to set the number of days before tickets are auto-closed + - Click **Enable/Disable** to turn the feature on or off + - Settings are stored in the database and take effect immediately + ## Some final pre-requisites 1. Add the bot to the BTS channel, help channel, tickets channel, and heartbeat channel diff --git a/nephthys/__main__.py b/nephthys/__main__.py index 268b533..28efbd5 100644 --- a/nephthys/__main__.py +++ b/nephthys/__main__.py @@ -54,13 +54,16 @@ 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(), - # ) + from nephthys.tasks.close_stale import close_stale_tickets + from datetime import datetime + + 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()) diff --git a/nephthys/actions/settings.py b/nephthys/actions/settings.py new file mode 100644 index 0000000..565a638 --- /dev/null +++ b/nephthys/actions/settings.py @@ -0,0 +1,125 @@ +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.configure_stale_days import get_configure_stale_days_modal + + +async def configure_stale_days_btn_callback( + ack: AsyncAck, body: dict, client: AsyncWebClient +): + """Opens the configure stale days modal when the configure button is clicked.""" + 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 configure stale days modal by non-admin user <@{user_id}>" + ) + return + + # Fetch current value + stale_days_setting = await env.db.settings.find_unique( + where={"key": "stale_ticket_days"} + ) + current_value = stale_days_setting.value if stale_days_setting else None + + view = get_configure_stale_days_modal(current_value) + await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id) + + +async def configure_stale_days_view_callback( + ack: AsyncAck, body: dict, client: AsyncWebClient +): + """Handles the submission of the configure stale days modal.""" + user_id = body["user"]["id"] + + raw_days = body["view"]["state"]["values"]["stale_days"]["stale_days"]["value"] + days = raw_days.strip() if raw_days else None + + errors = {} + + # Validate that it's a positive integer if provided + if days: + try: + days_int = int(days) + if days_int <= 0: + errors["stale_days"] = "Must be a positive number." + except ValueError: + errors["stale_days"] = "Must be a valid number." + + if errors: + await ack(response_action="errors", errors=errors) + 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 configure stale days by non-admin user <@{user_id}>" + ) + return + + if days: + await env.db.settings.upsert( + where={"key": "stale_ticket_days"}, + data={ + "create": {"key": "stale_ticket_days", "value": days}, + "update": {"value": days}, + }, + ) + logging.info(f"Stale ticket days updated to {days} by <@{user_id}>") + await send_heartbeat( + f"Stale ticket days updated to {days} days by <@{user_id}>" + ) + else: + await env.db.settings.delete_many(where={"key": "stale_ticket_days"}) + logging.info(f"Stale ticket auto-close disabled by <@{user_id}>") + await send_heartbeat(f"Stale ticket auto-close disabled by <@{user_id}>") + + await ack() + + from nephthys.events.app_home_opened import open_app_home + + await open_app_home("settings", client, user_id) + + +async def toggle_stale_feature_callback( + ack: AsyncAck, body: dict, client: AsyncWebClient +): + """Handles toggling the stale feature on/off. + + Disable: deletes the setting. + Enable: opens the configure modal so the user must pick a value. + """ + user_id = body["user"]["id"] + action = body["actions"][0]["value"] + + 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 toggle stale feature by non-admin user <@{user_id}>" + ) + return + + if action == "disable": + await ack() + await env.db.settings.delete_many(where={"key": "stale_ticket_days"}) + logging.info(f"Stale ticket auto-close disabled by <@{user_id}>") + await send_heartbeat(f"Stale ticket auto-close disabled by <@{user_id}>") + + from nephthys.events.app_home_opened import open_app_home + + await open_app_home("settings", client, user_id) + else: + # Open the configure modal to set time + await ack() + trigger_id = body["trigger_id"] + view = get_configure_stale_days_modal() + await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id) diff --git a/nephthys/events/app_home_opened.py b/nephthys/events/app_home_opened.py index 60738c2..3156181 100644 --- a/nephthys/events/app_home_opened.py +++ b/nephthys/events/app_home_opened.py @@ -14,6 +14,7 @@ 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 +from nephthys.views.home.settings import get_settings_view from nephthys.views.home.stats import get_stats_view from nephthys.views.home.team_tags import get_team_tags_view @@ -59,6 +60,8 @@ async def open_app_home(home_type: str, client: AsyncWebClient, user_id: str): view = await get_team_tags_view(user) case "category-tags": view = await get_category_tags_view(user) + case "settings": + view = await get_settings_view(user) case "my-stats": view = await get_stats_view(user) case _: diff --git a/nephthys/tasks/close_stale.py b/nephthys/tasks/close_stale.py index 1d1369f..d9beea6 100644 --- a/nephthys/tasks/close_stale.py +++ b/nephthys/tasks/close_stale.py @@ -13,6 +13,13 @@ async def get_is_stale(ts: str, max_retries: int = 3) -> bool: + stale_ticket_days = await env.get_stale_ticket_days() + if not stale_ticket_days: + logging.error( + "get_is_stale called but stale_ticket_days not configured in database" + ) + return False + for attempt in range(max_retries): try: replies = await env.slack_client.conversations_replies( @@ -28,7 +35,7 @@ async def get_is_stale(ts: str, max_retries: int = 3) -> bool: return ( datetime.now(tz=timezone.utc) - datetime.fromtimestamp(float(last_reply["ts"]), tz=timezone.utc) - ) > timedelta(days=3) + ) > timedelta(days=stale_ticket_days) except SlackApiError as e: if e.response["error"] == "ratelimited": retry_after = int(e.response.headers.get("Retry-After", 1)) @@ -82,12 +89,22 @@ async def get_is_stale(ts: str, max_retries: int = 3) -> bool: async def close_stale_tickets(): """ - Closes tickets that have been open for more than 3 days. + Closes tickets that have been open for more than the configured number of days. + The number of days is configured in the database settings (key: stale_ticket_days). This task is intended to be run periodically. """ - logging.info("Closing stale tickets...") - await send_heartbeat("Closing stale tickets...") + stale_ticket_days = await env.get_stale_ticket_days() + if not stale_ticket_days: + logging.info( + "Stale ticket auto-close is disabled (no stale_ticket_days setting)" + ) + return + + logging.info(f"Closing stale tickets (threshold: {stale_ticket_days} days)...") + await send_heartbeat( + f"Closing stale tickets (threshold: {stale_ticket_days} days)..." + ) try: tickets = await env.db.ticket.find_many( diff --git a/nephthys/utils/env.py b/nephthys/utils/env.py index c98b50c..ba9f312 100644 --- a/nephthys/utils/env.py +++ b/nephthys/utils/env.py @@ -121,5 +121,20 @@ async def workspace_admin_available(self) -> bool: self._workspace_admin_available = user_info["is_admin"] return user_info["is_admin"] + async def get_stale_ticket_days(self) -> int | None: + """Get the number of days before a ticket is considered stale from database settings.""" + stale_days_setting = await self.db.settings.find_unique( + where={"key": "stale_ticket_days"} + ) + if stale_days_setting and stale_days_setting.value: + try: + return int(stale_days_setting.value) + except ValueError: + logging.warning( + f"Invalid stale_ticket_days value in database: {stale_days_setting.value}" + ) + return None + return None + env = Environment() diff --git a/nephthys/utils/slack.py b/nephthys/utils/slack.py index edfc29b..95ef730 100644 --- a/nephthys/utils/slack.py +++ b/nephthys/utils/slack.py @@ -13,6 +13,9 @@ 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 +from nephthys.actions.settings import configure_stale_days_btn_callback +from nephthys.actions.settings import configure_stale_days_view_callback +from nephthys.actions.settings import toggle_stale_feature_callback from nephthys.actions.tag_subscribe import tag_subscribe_callback from nephthys.commands.dm_magic_link import dm_magic_link_cmd_callback from nephthys.events.app_home_opened import on_app_home_opened @@ -146,3 +149,24 @@ async def dm_magic_link( command, ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient ): await dm_magic_link_cmd_callback(command, ack, body, client) + + +@app.action("configure-stale-days") +async def configure_stale_days( + ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient +): + await configure_stale_days_btn_callback(ack, body, client) + + +@app.view("configure_stale_days") +async def configure_stale_days_view( + ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient +): + await configure_stale_days_view_callback(ack, body, client) + + +@app.action("toggle-stale-feature") +async def toggle_stale_feature( + ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient +): + await toggle_stale_feature_callback(ack, body, client) diff --git a/nephthys/views/home/__init__.py b/nephthys/views/home/__init__.py index 67188fd..fb5bffd 100644 --- a/nephthys/views/home/__init__.py +++ b/nephthys/views/home/__init__.py @@ -14,5 +14,6 @@ class View: View("Assigned Tickets", "assigned-tickets"), View("Team Tags", "team-tags"), View("Category Tags", "category-tags"), + View("Settings", "settings"), View("My Stats", "my-stats"), ] diff --git a/nephthys/views/home/settings.py b/nephthys/views/home/settings.py new file mode 100644 index 0000000..6a82808 --- /dev/null +++ b/nephthys/views/home/settings.py @@ -0,0 +1,72 @@ +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_settings_view(user: User | None) -> dict: + is_admin = bool(user and user.admin) + + header = get_header_components(user, "settings") + + if not is_admin: + return Home( + [ + *header, + Header(":rac_info: Settings"), + Section(":rac_nooo: only admins can manage settings."), + ] + ).build() + + stale_days_setting = await env.db.settings.find_unique( + where={"key": "stale_ticket_days"} + ) + current_stale_days = stale_days_setting.value if stale_days_setting else "Not set" + + stale_enabled = stale_days_setting and stale_days_setting.value + + return Home( + [ + *header, + Header(":rac_wrench: Bot Settings"), + Section(":rac_thumbs: configure bot behavior and features"), + Divider(), + Section( + text=f"*Stale Ticket Auto-Close*\n" + f"Automatically close tickets after a period of inactivity.\n" + f"Current setting: *{current_stale_days}* {'day' if current_stale_days == '1' else 'days'} " + f"{'(Enabled)' if stale_enabled else '(Disabled)'}" + ), + Actions( + elements=[ + Button( + text=":pencil2: Configure", + action_id="configure-stale-days" + if stale_enabled + else "toggle-stale-feature", + value="enable" if not stale_enabled else None, + style=Button.PRIMARY, + ), + ] + + ( + [ + Button( + text=":x: Disable", + action_id="toggle-stale-feature", + value="disable", + style=Button.DANGER, + ), + ] + if stale_enabled + else [] + ) + ), + Divider(), + ] + ).build() diff --git a/nephthys/views/modals/configure_stale_days.py b/nephthys/views/modals/configure_stale_days.py new file mode 100644 index 0000000..5f73758 --- /dev/null +++ b/nephthys/views/modals/configure_stale_days.py @@ -0,0 +1,30 @@ +from blockkit import Input +from blockkit import Modal +from blockkit import PlainTextInput + + +def get_configure_stale_days_modal(current_value: str | None = None): + """ + Returns a modal for configuring the number of days before tickets are auto-closed as stale. + + Args: + current_value: Current number of days (optional) + """ + return Modal( + title=":wrench: Stale Tickets", + callback_id="configure_stale_days", + submit=":white_check_mark: Save", + blocks=[ + Input( + label="Days before auto-close", + block_id="stale_days", + element=PlainTextInput( + action_id="stale_days", + placeholder="e.g. 7", + **({"initial_value": current_value} if current_value else {}), + ), + hint="Set the number of days of inactivity before a ticket is automatically closed. Leave empty to disable.", + optional=True, + ), + ], + ).build() diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 59af15f..affebcc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -32,9 +32,9 @@ model User { assignedTickets Ticket[] @relation("AssignedTickets") reopenedTickets Ticket[] @relation("ReopenedTickets") - tagSubscriptions UserTagSubscription[] - createdCategoryTags CategoryTag[] @relation("CreatedCategoryTags") - helper Boolean @default(false) + tagSubscriptions UserTagSubscription[] + createdCategoryTags CategoryTag[] @relation("CreatedCategoryTags") + helper Boolean @default(false) createdAt DateTime @default(now()) } @@ -75,7 +75,7 @@ model Ticket { questionTag QuestionTag? @relation("QuestionTagTickets", fields: [questionTagId], references: [id]) questionTagId Int? - categoryTag CategoryTag? @relation("CategoryTagTickets", fields: [categoryTagId], references: [id]) + categoryTag CategoryTag? @relation("CategoryTagTickets", fields: [categoryTagId], references: [id]) categoryTagId Int? createdAt DateTime @default(now()) @@ -134,12 +134,20 @@ model BotMessage { } model CategoryTag { - id Int @id @unique @default(autoincrement()) - name String @unique - tickets Ticket[] @relation("CategoryTagTickets") + id Int @id @unique @default(autoincrement()) + name String @unique + tickets Ticket[] @relation("CategoryTagTickets") - createdBy User? @relation("CreatedCategoryTags", fields: [createdById], references: [id]) + createdBy User? @relation("CreatedCategoryTags", fields: [createdById], references: [id]) createdById Int? createdAt DateTime @default(now()) } + +model Settings { + id Int @id @unique @default(autoincrement()) + key String @unique + value String? + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} From 47d13c49edf1ad9f575f94a7de2914e06e0b0ba0 Mon Sep 17 00:00:00 2001 From: End Date: Fri, 13 Mar 2026 17:09:04 -0700 Subject: [PATCH 2/4] Update nephthys/tasks/close_stale.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- nephthys/tasks/close_stale.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nephthys/tasks/close_stale.py b/nephthys/tasks/close_stale.py index d9beea6..c9616c9 100644 --- a/nephthys/tasks/close_stale.py +++ b/nephthys/tasks/close_stale.py @@ -89,7 +89,8 @@ async def get_is_stale(ts: str, max_retries: int = 3) -> bool: async def close_stale_tickets(): """ - Closes tickets that have been open for more than the configured number of days. + Closes tickets that have been inactive for more than the configured number of days, + based on the timestamp of the last message in the ticket's Slack thread. The number of days is configured in the database settings (key: stale_ticket_days). This task is intended to be run periodically. """ From b34bc61f730e9706df6ac01094fee7963b5d8a9e Mon Sep 17 00:00:00 2001 From: End Date: Fri, 13 Mar 2026 17:09:34 -0700 Subject: [PATCH 3/4] Update docs/deployment.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/deployment.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index ac81122..175ea65 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -109,8 +109,8 @@ After deployment, you can configure bot settings (like stale ticket auto-close) 1. Open the bot's App Home in Slack 2. Navigate to the **Settings** tab 3. Configure the stale ticket auto-close feature: - - Click **Configure** to set the number of days before tickets are auto-closed - - Click **Enable/Disable** to turn the feature on or off + - Click **Configure** to open the settings, set the number of days before tickets are auto-closed, and enable or disable the feature + - When the feature is enabled, a **Disable** button will appear that you can use to turn it off - Settings are stored in the database and take effect immediately ## Some final pre-requisites From 79e065bd2a42aea1374d67b82d01129dbdd1ebaf Mon Sep 17 00:00:00 2001 From: End Date: Sat, 28 Mar 2026 07:07:04 -0700 Subject: [PATCH 4/4] feedback --- docs/deployment.md | 16 +-- nephthys/actions/settings.py | 125 ------------------ nephthys/events/app_home_opened.py | 3 - nephthys/tasks/close_stale.py | 22 +-- nephthys/utils/env.py | 34 ++--- nephthys/utils/slack.py | 24 ---- nephthys/views/home/__init__.py | 1 - nephthys/views/home/settings.py | 72 ---------- nephthys/views/modals/configure_stale_days.py | 30 ----- prisma/schema.prisma | 8 +- 10 files changed, 32 insertions(+), 303 deletions(-) delete mode 100644 nephthys/actions/settings.py delete mode 100644 nephthys/views/home/settings.py delete mode 100644 nephthys/views/modals/configure_stale_days.py diff --git a/docs/deployment.md b/docs/deployment.md index 175ea65..b2d391d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -98,21 +98,15 @@ Note: These steps have to be done by a Workspace Admin (otherwise it will be una LOG_LEVEL_STDERR="WARNING" # Override the log level for OpenTelemetry output LOG_LEVEL_OTEL="WARNING" + + # Optional: Enable stale ticket auto-close + # Tickets inactive for this many days will be automatically closed + # Leave unset to disable + STALE_TICKET_DAYS="7" ``` 4. Don't forget to click **Save All Environment Variables** -## Configuring Bot Settings - -After deployment, you can configure bot settings (like stale ticket auto-close) from the Slack App Home: - -1. Open the bot's App Home in Slack -2. Navigate to the **Settings** tab -3. Configure the stale ticket auto-close feature: - - Click **Configure** to open the settings, set the number of days before tickets are auto-closed, and enable or disable the feature - - When the feature is enabled, a **Disable** button will appear that you can use to turn it off - - Settings are stored in the database and take effect immediately - ## Some final pre-requisites 1. Add the bot to the BTS channel, help channel, tickets channel, and heartbeat channel diff --git a/nephthys/actions/settings.py b/nephthys/actions/settings.py deleted file mode 100644 index 565a638..0000000 --- a/nephthys/actions/settings.py +++ /dev/null @@ -1,125 +0,0 @@ -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.configure_stale_days import get_configure_stale_days_modal - - -async def configure_stale_days_btn_callback( - ack: AsyncAck, body: dict, client: AsyncWebClient -): - """Opens the configure stale days modal when the configure button is clicked.""" - 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 configure stale days modal by non-admin user <@{user_id}>" - ) - return - - # Fetch current value - stale_days_setting = await env.db.settings.find_unique( - where={"key": "stale_ticket_days"} - ) - current_value = stale_days_setting.value if stale_days_setting else None - - view = get_configure_stale_days_modal(current_value) - await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id) - - -async def configure_stale_days_view_callback( - ack: AsyncAck, body: dict, client: AsyncWebClient -): - """Handles the submission of the configure stale days modal.""" - user_id = body["user"]["id"] - - raw_days = body["view"]["state"]["values"]["stale_days"]["stale_days"]["value"] - days = raw_days.strip() if raw_days else None - - errors = {} - - # Validate that it's a positive integer if provided - if days: - try: - days_int = int(days) - if days_int <= 0: - errors["stale_days"] = "Must be a positive number." - except ValueError: - errors["stale_days"] = "Must be a valid number." - - if errors: - await ack(response_action="errors", errors=errors) - 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 configure stale days by non-admin user <@{user_id}>" - ) - return - - if days: - await env.db.settings.upsert( - where={"key": "stale_ticket_days"}, - data={ - "create": {"key": "stale_ticket_days", "value": days}, - "update": {"value": days}, - }, - ) - logging.info(f"Stale ticket days updated to {days} by <@{user_id}>") - await send_heartbeat( - f"Stale ticket days updated to {days} days by <@{user_id}>" - ) - else: - await env.db.settings.delete_many(where={"key": "stale_ticket_days"}) - logging.info(f"Stale ticket auto-close disabled by <@{user_id}>") - await send_heartbeat(f"Stale ticket auto-close disabled by <@{user_id}>") - - await ack() - - from nephthys.events.app_home_opened import open_app_home - - await open_app_home("settings", client, user_id) - - -async def toggle_stale_feature_callback( - ack: AsyncAck, body: dict, client: AsyncWebClient -): - """Handles toggling the stale feature on/off. - - Disable: deletes the setting. - Enable: opens the configure modal so the user must pick a value. - """ - user_id = body["user"]["id"] - action = body["actions"][0]["value"] - - 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 toggle stale feature by non-admin user <@{user_id}>" - ) - return - - if action == "disable": - await ack() - await env.db.settings.delete_many(where={"key": "stale_ticket_days"}) - logging.info(f"Stale ticket auto-close disabled by <@{user_id}>") - await send_heartbeat(f"Stale ticket auto-close disabled by <@{user_id}>") - - from nephthys.events.app_home_opened import open_app_home - - await open_app_home("settings", client, user_id) - else: - # Open the configure modal to set time - await ack() - trigger_id = body["trigger_id"] - view = get_configure_stale_days_modal() - await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id) diff --git a/nephthys/events/app_home_opened.py b/nephthys/events/app_home_opened.py index 3156181..60738c2 100644 --- a/nephthys/events/app_home_opened.py +++ b/nephthys/events/app_home_opened.py @@ -14,7 +14,6 @@ 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 -from nephthys.views.home.settings import get_settings_view from nephthys.views.home.stats import get_stats_view from nephthys.views.home.team_tags import get_team_tags_view @@ -60,8 +59,6 @@ async def open_app_home(home_type: str, client: AsyncWebClient, user_id: str): view = await get_team_tags_view(user) case "category-tags": view = await get_category_tags_view(user) - case "settings": - view = await get_settings_view(user) case "my-stats": view = await get_stats_view(user) case _: diff --git a/nephthys/tasks/close_stale.py b/nephthys/tasks/close_stale.py index c9616c9..a74154f 100644 --- a/nephthys/tasks/close_stale.py +++ b/nephthys/tasks/close_stale.py @@ -12,14 +12,7 @@ from prisma.enums import TicketStatus -async def get_is_stale(ts: str, max_retries: int = 3) -> bool: - stale_ticket_days = await env.get_stale_ticket_days() - if not stale_ticket_days: - logging.error( - "get_is_stale called but stale_ticket_days not configured in database" - ) - return False - +async def get_is_stale(ts: str, stale_ticket_days: int, max_retries: int = 3) -> bool: for attempt in range(max_retries): try: replies = await env.slack_client.conversations_replies( @@ -91,15 +84,14 @@ async def close_stale_tickets(): """ Closes tickets that have been inactive for more than the configured number of days, based on the timestamp of the last message in the ticket's Slack thread. - The number of days is configured in the database settings (key: stale_ticket_days). - This task is intended to be run periodically. + + Configure via the STALE_TICKET_DAYS environment variable. + This task is intended to be run periodically (e.g., hourly). """ - stale_ticket_days = await env.get_stale_ticket_days() + stale_ticket_days = env.stale_ticket_days if not stale_ticket_days: - logging.info( - "Stale ticket auto-close is disabled (no stale_ticket_days setting)" - ) + logging.info("Stale ticket auto-close is disabled (STALE_TICKET_DAYS not set)") return logging.info(f"Closing stale tickets (threshold: {stale_ticket_days} days)...") @@ -125,7 +117,7 @@ async def close_stale_tickets(): for ticket in batch: await asyncio.sleep(1.2) # Rate limiting delay - if await get_is_stale(ticket.msgTs): + if await get_is_stale(ticket.msgTs, stale_ticket_days): stale += 1 resolver_user = ( ticket.assignedTo if ticket.assignedTo else ticket.openedBy diff --git a/nephthys/utils/env.py b/nephthys/utils/env.py index ba9f312..0c6de39 100644 --- a/nephthys/utils/env.py +++ b/nephthys/utils/env.py @@ -63,6 +63,25 @@ def __init__(self): self.slack_heartbeat_channel = os.environ.get("SLACK_HEARTBEAT_CHANNEL") + # Stale ticket auto-close: number of days of inactivity before closing + # Set to a positive integer to enable, leave unset to disable + stale_days_str = os.environ.get("STALE_TICKET_DAYS") + if stale_days_str: + try: + stale_days = int(stale_days_str) + self.stale_ticket_days = stale_days if stale_days > 0 else None + if stale_days <= 0: + logging.warning( + f"STALE_TICKET_DAYS must be positive, got {stale_days}. Disabling." + ) + except ValueError: + logging.warning( + f"Invalid STALE_TICKET_DAYS value: {stale_days_str}. Disabling." + ) + self.stale_ticket_days = None + else: + self.stale_ticket_days = None + unset = [key for key, value in self.__dict__.items() if value == "unset"] if unset: @@ -121,20 +140,5 @@ async def workspace_admin_available(self) -> bool: self._workspace_admin_available = user_info["is_admin"] return user_info["is_admin"] - async def get_stale_ticket_days(self) -> int | None: - """Get the number of days before a ticket is considered stale from database settings.""" - stale_days_setting = await self.db.settings.find_unique( - where={"key": "stale_ticket_days"} - ) - if stale_days_setting and stale_days_setting.value: - try: - return int(stale_days_setting.value) - except ValueError: - logging.warning( - f"Invalid stale_ticket_days value in database: {stale_days_setting.value}" - ) - return None - return None - env = Environment() diff --git a/nephthys/utils/slack.py b/nephthys/utils/slack.py index 95ef730..edfc29b 100644 --- a/nephthys/utils/slack.py +++ b/nephthys/utils/slack.py @@ -13,9 +13,6 @@ 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 -from nephthys.actions.settings import configure_stale_days_btn_callback -from nephthys.actions.settings import configure_stale_days_view_callback -from nephthys.actions.settings import toggle_stale_feature_callback from nephthys.actions.tag_subscribe import tag_subscribe_callback from nephthys.commands.dm_magic_link import dm_magic_link_cmd_callback from nephthys.events.app_home_opened import on_app_home_opened @@ -149,24 +146,3 @@ async def dm_magic_link( command, ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient ): await dm_magic_link_cmd_callback(command, ack, body, client) - - -@app.action("configure-stale-days") -async def configure_stale_days( - ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient -): - await configure_stale_days_btn_callback(ack, body, client) - - -@app.view("configure_stale_days") -async def configure_stale_days_view( - ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient -): - await configure_stale_days_view_callback(ack, body, client) - - -@app.action("toggle-stale-feature") -async def toggle_stale_feature( - ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient -): - await toggle_stale_feature_callback(ack, body, client) diff --git a/nephthys/views/home/__init__.py b/nephthys/views/home/__init__.py index fb5bffd..67188fd 100644 --- a/nephthys/views/home/__init__.py +++ b/nephthys/views/home/__init__.py @@ -14,6 +14,5 @@ class View: View("Assigned Tickets", "assigned-tickets"), View("Team Tags", "team-tags"), View("Category Tags", "category-tags"), - View("Settings", "settings"), View("My Stats", "my-stats"), ] diff --git a/nephthys/views/home/settings.py b/nephthys/views/home/settings.py deleted file mode 100644 index 6a82808..0000000 --- a/nephthys/views/home/settings.py +++ /dev/null @@ -1,72 +0,0 @@ -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_settings_view(user: User | None) -> dict: - is_admin = bool(user and user.admin) - - header = get_header_components(user, "settings") - - if not is_admin: - return Home( - [ - *header, - Header(":rac_info: Settings"), - Section(":rac_nooo: only admins can manage settings."), - ] - ).build() - - stale_days_setting = await env.db.settings.find_unique( - where={"key": "stale_ticket_days"} - ) - current_stale_days = stale_days_setting.value if stale_days_setting else "Not set" - - stale_enabled = stale_days_setting and stale_days_setting.value - - return Home( - [ - *header, - Header(":rac_wrench: Bot Settings"), - Section(":rac_thumbs: configure bot behavior and features"), - Divider(), - Section( - text=f"*Stale Ticket Auto-Close*\n" - f"Automatically close tickets after a period of inactivity.\n" - f"Current setting: *{current_stale_days}* {'day' if current_stale_days == '1' else 'days'} " - f"{'(Enabled)' if stale_enabled else '(Disabled)'}" - ), - Actions( - elements=[ - Button( - text=":pencil2: Configure", - action_id="configure-stale-days" - if stale_enabled - else "toggle-stale-feature", - value="enable" if not stale_enabled else None, - style=Button.PRIMARY, - ), - ] - + ( - [ - Button( - text=":x: Disable", - action_id="toggle-stale-feature", - value="disable", - style=Button.DANGER, - ), - ] - if stale_enabled - else [] - ) - ), - Divider(), - ] - ).build() diff --git a/nephthys/views/modals/configure_stale_days.py b/nephthys/views/modals/configure_stale_days.py deleted file mode 100644 index 5f73758..0000000 --- a/nephthys/views/modals/configure_stale_days.py +++ /dev/null @@ -1,30 +0,0 @@ -from blockkit import Input -from blockkit import Modal -from blockkit import PlainTextInput - - -def get_configure_stale_days_modal(current_value: str | None = None): - """ - Returns a modal for configuring the number of days before tickets are auto-closed as stale. - - Args: - current_value: Current number of days (optional) - """ - return Modal( - title=":wrench: Stale Tickets", - callback_id="configure_stale_days", - submit=":white_check_mark: Save", - blocks=[ - Input( - label="Days before auto-close", - block_id="stale_days", - element=PlainTextInput( - action_id="stale_days", - placeholder="e.g. 7", - **({"initial_value": current_value} if current_value else {}), - ), - hint="Set the number of days of inactivity before a ticket is automatically closed. Leave empty to disable.", - optional=True, - ), - ], - ).build() diff --git a/prisma/schema.prisma b/prisma/schema.prisma index affebcc..bdf920f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -144,10 +144,4 @@ model CategoryTag { createdAt DateTime @default(now()) } -model Settings { - id Int @id @unique @default(autoincrement()) - key String @unique - value String? - updatedAt DateTime @updatedAt - createdAt DateTime @default(now()) -} +