buttons: allow non-staff to close tickets (countdown still applies)

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.
This commit is contained in:
2026-05-19 22:15:38 +00:00
parent 837fd10984
commit a565450e2d

View File

@@ -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,