Compare commits
4 Commits
76279b703a
...
2152544d09
| Author | SHA1 | Date | |
|---|---|---|---|
| 2152544d09 | |||
| c79463fc2a | |||
| e8e114e4ad | |||
| 452f005aea |
@@ -11,6 +11,7 @@ const { mongoose } = require('./db-connection');
|
||||
// Handlers
|
||||
const { handleButton, handleTicketModal } = require('./handlers/buttons');
|
||||
const { handleCommand, handleContextMenu, handleAutocomplete } = require('./handlers/commands');
|
||||
const { requireStaffRole } = require('./handlers/commands/helpers');
|
||||
const { handleDiscordReply } = require('./handlers/messages');
|
||||
|
||||
// Services & jobs
|
||||
@@ -110,6 +111,9 @@ client.on('interactionCreate', async interaction => {
|
||||
}
|
||||
|
||||
if (interaction.isModalSubmit() && interaction.customId.startsWith('signature_modal_')) {
|
||||
// Staff-only: /signature shows this modal, which is gated; double-gate the
|
||||
// submit path in case an attacker crafts the submission directly.
|
||||
if (await requireStaffRole(interaction)) return;
|
||||
// Handle signature modal submit
|
||||
try {
|
||||
const valediction = interaction.fields.getTextInputValue('valediction');
|
||||
|
||||
@@ -34,6 +34,7 @@ const { addMemberToStaffThread, createStaffThread } = require('../services/staff
|
||||
const { pinMessage } = require('../services/pinMessage');
|
||||
const { logError, logTicketEvent } = require('../services/debugLog');
|
||||
const { findTicketForChannel, runDeferred } = require('./sharedHelpers');
|
||||
const { requireStaffRole } = require('./commands/helpers');
|
||||
|
||||
const Ticket = mongoose.model('Ticket');
|
||||
const Transcript = mongoose.model('Transcript');
|
||||
@@ -706,12 +707,15 @@ async function postTicketWelcomeEmbeds(channel, interaction, email, game, descri
|
||||
// Dispatch tables
|
||||
// ============================================================
|
||||
|
||||
/** Buttons that don't depend on a ticket-bound channel. */
|
||||
/**
|
||||
* Public-facing buttons that don't require a staff role: the panel buttons
|
||||
* that any member uses to open a ticket. Customer-facing entry points stay
|
||||
* here. cancel_delete_tag is staff-only and gated separately in handleButton.
|
||||
*/
|
||||
const FREE_BUTTON_HANDLERS = {
|
||||
open_ticket: handleOpenTicketModal,
|
||||
open_ticket_thread: handleOpenTicketModal,
|
||||
open_ticket_channel: handleOpenTicketModal,
|
||||
cancel_delete_tag: handleTagDeleteCancel
|
||||
open_ticket_channel: handleOpenTicketModal
|
||||
};
|
||||
|
||||
/** Buttons that fire inside a ticket channel. The dispatcher does the lookup. */
|
||||
@@ -732,16 +736,35 @@ async function handleButton(interaction) {
|
||||
const { customId } = interaction;
|
||||
|
||||
// Tag-delete confirm has a dynamic id (`confirm_delete_tag::<name>`).
|
||||
// Mutates the Tag collection — staff only.
|
||||
if (customId.startsWith('confirm_delete_tag::')) {
|
||||
if (await requireStaffRole(interaction)) return;
|
||||
return handleTagDeleteConfirm(interaction);
|
||||
}
|
||||
|
||||
// Tag-delete cancel: paired with the staff-only delete flow; gate to keep
|
||||
// the button surface consistent (non-staff can't reach the dialog anyway).
|
||||
if (customId === 'cancel_delete_tag') {
|
||||
if (await requireStaffRole(interaction)) return;
|
||||
return handleTagDeleteCancel(interaction);
|
||||
}
|
||||
|
||||
// FREE_BUTTON_HANDLERS are the public-facing panel buttons (open_ticket*).
|
||||
// Customers/members must be able to click these to open a ticket.
|
||||
const freeHandler = FREE_BUTTON_HANDLERS[customId];
|
||||
if (freeHandler) return freeHandler(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;
|
||||
|
||||
const ticket = await findTicketForChannel(
|
||||
interaction,
|
||||
'This channel is not linked to a ticket, or the ticket could not be found.'
|
||||
|
||||
@@ -274,11 +274,11 @@ const CONTEXT_MENU_HANDLERS = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Slash-command dispatcher. /help is open to everyone; everything else
|
||||
* requires the staff role.
|
||||
* Slash-command dispatcher. Every command is staff-only — including /help,
|
||||
* which previously bypassed the role check.
|
||||
*/
|
||||
async function handleCommand(interaction) {
|
||||
if (interaction.commandName !== 'help' && (await requireStaffRole(interaction))) return;
|
||||
if (await requireStaffRole(interaction)) return;
|
||||
const handler = COMMAND_HANDLERS[interaction.commandName];
|
||||
if (handler) await handler(interaction);
|
||||
}
|
||||
|
||||
@@ -25,10 +25,13 @@ async function executeRename(channel, entry) {
|
||||
// (403), or no token configured — fall back to the primary Discord.js client.
|
||||
// Non-fallback errors rethrow so enqueueRename's catch can classify/log.
|
||||
if (err && err.fallback === true && channel && typeof channel.setName === 'function') {
|
||||
logWarn(
|
||||
'renameQueue',
|
||||
`secondary-bot ${err.status ?? 'unavailable'}; falling back to primary channel=${channel.id}`
|
||||
).catch(() => {});
|
||||
// Local log only; discord.js's REST client transparently handles 429s
|
||||
// on the primary fallback, so this used to post a paired warning to
|
||||
// the debug channel for every secondary-bot quota event with no
|
||||
// operator action required. Keep the visibility in container logs.
|
||||
console.warn(
|
||||
`[renameQueue] secondary-bot ${err.status ?? 'unavailable'}; falling back to primary channel=${channel.id}`
|
||||
);
|
||||
await channel.setName(currentName);
|
||||
} else {
|
||||
throw err;
|
||||
@@ -80,6 +83,12 @@ function enqueueRename(channel, newName) {
|
||||
|
||||
// Shares renameChains so a move+rename pair on the same channel executes in
|
||||
// call order. No coalescing: every move is a distinct chain link.
|
||||
//
|
||||
// lockPermissions: false preserves the channel's existing permission overwrites
|
||||
// across the parent change. With the default (true), Discord re-syncs the
|
||||
// channel's overwrites to match the new category and wipes per-user grants —
|
||||
// in practice that kicked the ticket creator and any /add'd users off the
|
||||
// channel on every escalate / de-escalate / /move.
|
||||
function enqueueMove(channel, categoryId) {
|
||||
let entry = renameChains.get(channel.id);
|
||||
if (!entry) {
|
||||
@@ -87,7 +96,7 @@ function enqueueMove(channel, categoryId) {
|
||||
renameChains.set(channel.id, entry);
|
||||
}
|
||||
|
||||
const next = entry.chain.catch(() => {}).then(() => channel.setParent(categoryId, { lockPermissions: true }));
|
||||
const next = entry.chain.catch(() => {}).then(() => channel.setParent(categoryId, { lockPermissions: false }));
|
||||
entry.chain = next;
|
||||
|
||||
next.catch((err) => {
|
||||
|
||||
@@ -41,7 +41,10 @@ async function renameChannel(channelId, newName) {
|
||||
if (res.status === 429) {
|
||||
const retryAfterSec = (body && typeof body === 'object' && body.retry_after) || null;
|
||||
const retryAfterMs = retryAfterSec != null ? Math.ceil(Number(retryAfterSec) * 1000) : null;
|
||||
logWarn('renamer', `429 rename channel=${channelId} retry_after=${retryAfterSec}`).catch(() => {});
|
||||
// Local log only; the channelQueue fallback path handles recovery
|
||||
// transparently via discord.js's built-in 429 retry. Posting these to
|
||||
// the debug channel was non-actionable noise.
|
||||
console.warn(`[renamer] 429 rename channel=${channelId} retry_after=${retryAfterSec}`);
|
||||
|
||||
// Respect retry_after up to 2000ms; otherwise fail over immediately.
|
||||
if (retryAfterMs != null && retryAfterMs > 0 && retryAfterMs <= 2000) {
|
||||
|
||||
Reference in New Issue
Block a user