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.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 -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 nephthys.utils.logging import send_heartbeat 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 @@ class Environment: 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 @@ class Environment: 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_category_tag import create_category_tag_view_callba 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 @@ APP_HOME_VIEWS: list[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()) -} +