From a565450e2d4d6126511989609682c80b8997efd2 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Tue, 19 May 2026 22:15:38 +0000 Subject: [PATCH] buttons: allow non-staff to close tickets (countdown still applies) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the previous TICKET_BUTTON_HANDLERS gate, ticket creators and /add'd members were locked out of every ticket button — including close_ticket on their own ticket. Add a PUBLIC_TICKET_BUTTONS set so the close flow (close_ticket / confirm_close / confirm_close_with_email / confirm_close_no_email / cancel_close) skips the staff check. Claim, escalate, and de-escalate remain staff-only. The 60s FORCE_CLOSE_TIMER countdown, the transcript archive, and the optional customer-closure email all continue to fire on the existing runFinalClose path — nothing about the close behavior changes, only who is allowed to click the button. cancel_close is intentionally public too: anyone in the channel can abort a pending close, including the original setter, staff, or the creator. The pendingCloses entry stores who set it, but the abort path doesn't gate on that — kept permissive to match the rest of the close flow. --- handlers/buttons.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/handlers/buttons.js b/handlers/buttons.js index 0bee532..c408202 100644 --- a/handlers/buttons.js +++ b/handlers/buttons.js @@ -732,6 +732,20 @@ const TICKET_BUTTON_HANDLERS = { deescalate_ticket: handleDeescalateButton }; +/** + * TICKET_BUTTON_HANDLERS entries that any user with channel access may + * invoke — not just staff. Ticket creators and /add'd users get to close + * their own ticket (with the 60s countdown still in place) and cancel a + * pending close. Claim/escalate/de-escalate stay staff-only. + */ +const PUBLIC_TICKET_BUTTONS = new Set([ + 'close_ticket', + 'confirm_close', + 'confirm_close_with_email', + 'confirm_close_no_email', + 'cancel_close' +]); + async function handleButton(interaction) { const { customId } = interaction; @@ -757,13 +771,12 @@ async function handleButton(interaction) { const ticketHandler = TICKET_BUTTON_HANDLERS[customId]; if (!ticketHandler) return; - // Every TICKET_BUTTON_HANDLERS entry mutates ticket state - // (claim/close/confirm_close*/cancel_close/escalate*/deescalate). The slash - // command dispatcher in handlers/commands/index.js gates these via - // requireStaffRole; the button dispatcher must do the same — non-staff - // members with view access to the ticket channel (creator, /add'd users) - // could otherwise click Claim, Escalate, Close, etc. - if (await requireStaffRole(interaction)) return; + // Claim / escalate / de-escalate mutate staff-owned ticket state and stay + // staff-only. Close-related buttons (close_ticket, confirm_close*, + // cancel_close) are public so a ticket creator can close their own ticket; + // the 60s force-close countdown still applies, and the cancel button is + // intentionally visible to anyone in the channel so any party can abort. + if (!PUBLIC_TICKET_BUTTONS.has(customId) && (await requireStaffRole(interaction))) return; const ticket = await findTicketForChannel( interaction,