mirror of
https://github.com/System-End/nephthys.git
synced 2026-04-19 16:28:16 +00:00
feedback
This commit is contained in:
parent
b34bc61f73
commit
79e065bd2a
10 changed files with 32 additions and 303 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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 _:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue