audit week 1: creator ID tracking, channel-queue migration, deprecation cleanup
QUAL-006 store ticket.creatorId on creation; legacy split-pop returned the
message ID for discord-msg-* tickets, breaking transcript DM, close
log, and channel rename for context-menu-created tickets. Adds the
field to the Ticket schema and writes a one-shot backfill script
(scripts/backfill-creatorId.js, dry-run by default).
QUEUE-001 add enqueueOverwrite + enqueueTopic to services/channelQueue.js
(chain on renameChains alongside enqueueMove). Migrate handleAdd /
handleRemove / handleMove / handleTopic so permissionOverwrites,
setParent, and setTopic no longer race pending renames or sends.
handleMove now uses the existing enqueueMove. Initial overwrites in
handleTicketModal stay inline; channel doesn't exist yet so no race.
DISCORD-001 replace ephemeral: true with flags: MessageFlags.Ephemeral across
broccolini-discord.js, handlers/sharedHelpers.js, handlers/buttons.js,
handlers/commands.js. runDeferred opts now take { flags } directly.
SEC-003 /gmailpoll min interval is 30s. Drop the 5s/10s slash-command
choices and clamp Math.max(30000, ms) in handleGmailPoll for
defense in depth.
QUAL-001 upgrade silent .catch(() => {}) on the lastActivity updateOne in
handlers/messages.js to log via logError, so transient Mongo errors
surface in the debug channel instead of disappearing.
QUAL-002 drop await from logError/logWarn calls in services/staffThread.js
and services/pinMessage.js — fire-and-forget per CLAUDE.md hard rule.
QUAL-003 wrap stray setTimeouts (handleConfirmCloseRequest force-close timer,
runFinalClose channel-delete + overflow-cleanup, checkAutoClose
delete-after-email) in trackTimeout via lazy require so they clear
on shutdown.
This commit is contained in:
@@ -113,6 +113,81 @@ function enqueueMove(channel, categoryId) {
|
||||
return next;
|
||||
}
|
||||
|
||||
// Shares renameChains so a permissionOverwrite mutation serializes with pending
|
||||
// renames/moves on the same channel. Mode 'create' calls
|
||||
// `channel.permissionOverwrites.create(id, perms)`; 'delete' calls
|
||||
// `channel.permissionOverwrites.delete(id)`. No coalescing.
|
||||
function enqueueOverwrite(channel, id, perms, mode = 'create') {
|
||||
let entry = renameChains.get(channel.id);
|
||||
if (!entry) {
|
||||
entry = { chain: Promise.resolve(), pendingName: null };
|
||||
renameChains.set(channel.id, entry);
|
||||
}
|
||||
|
||||
const next = entry.chain.catch(() => {}).then(() =>
|
||||
mode === 'delete'
|
||||
? channel.permissionOverwrites.delete(id)
|
||||
: channel.permissionOverwrites.create(id, perms)
|
||||
);
|
||||
entry.chain = next;
|
||||
|
||||
next.catch((err) => {
|
||||
logWarn('overwriteQueue', `Overwrite ${mode} failed for ${channel.name}: ${err && err.message || err}`).catch(() => {});
|
||||
const status = err && err.status;
|
||||
const msg = (err && err.message) || String(err);
|
||||
if (status === 401 || status === 403) {
|
||||
logError(
|
||||
'overwriteQueue:token/permission',
|
||||
new Error(`${status} channel=${channel.id} target=${id} mode=${mode}: ${msg}`)
|
||||
).catch(() => {});
|
||||
} else if (status === 429) {
|
||||
logError(
|
||||
'overwriteQueue:ratelimited',
|
||||
new Error(`429 channel=${channel.id} target=${id} mode=${mode}: ${msg}`)
|
||||
).catch(() => {});
|
||||
}
|
||||
}).finally(() => {
|
||||
if (renameChains.get(channel.id) === entry && entry.chain === next && entry.pendingName == null) {
|
||||
renameChains.delete(channel.id);
|
||||
}
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
// Shares renameChains so setTopic serializes with pending renames/moves.
|
||||
function enqueueTopic(channel, text) {
|
||||
let entry = renameChains.get(channel.id);
|
||||
if (!entry) {
|
||||
entry = { chain: Promise.resolve(), pendingName: null };
|
||||
renameChains.set(channel.id, entry);
|
||||
}
|
||||
|
||||
const next = entry.chain.catch(() => {}).then(() => channel.setTopic(text));
|
||||
entry.chain = next;
|
||||
|
||||
next.catch((err) => {
|
||||
logWarn('topicQueue', `Topic set failed for ${channel.name}: ${err && err.message || err}`).catch(() => {});
|
||||
const status = err && err.status;
|
||||
const msg = (err && err.message) || String(err);
|
||||
if (status === 401 || status === 403) {
|
||||
logError(
|
||||
'topicQueue:token/permission',
|
||||
new Error(`${status} channel=${channel.id}: ${msg}`)
|
||||
).catch(() => {});
|
||||
} else if (status === 429) {
|
||||
logError(
|
||||
'topicQueue:ratelimited',
|
||||
new Error(`429 channel=${channel.id}: ${msg}`)
|
||||
).catch(() => {});
|
||||
}
|
||||
}).finally(() => {
|
||||
if (renameChains.get(channel.id) === entry && entry.chain === next && entry.pendingName == null) {
|
||||
renameChains.delete(channel.id);
|
||||
}
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
// Per-channel promise chain for send ordering and to prevent interleaving.
|
||||
const sendChains = new Map();
|
||||
|
||||
@@ -157,4 +232,4 @@ function enqueueDelete(channel) {
|
||||
return next;
|
||||
}
|
||||
|
||||
module.exports = { enqueueRename, enqueueMove, enqueueSend, enqueueDelete };
|
||||
module.exports = { enqueueRename, enqueueMove, enqueueOverwrite, enqueueTopic, enqueueSend, enqueueDelete };
|
||||
|
||||
@@ -31,9 +31,9 @@ async function pinMessage(message, client) {
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 30003) {
|
||||
await logWarn('pinMessage', `Max pins reached in channel #${message.channel.name} — could not pin message.`, client).catch(() => {});
|
||||
logWarn('pinMessage', `Max pins reached in channel #${message.channel.name} — could not pin message.`, client).catch(() => {});
|
||||
} else {
|
||||
await logWarn('pinMessage', `Failed to pin message in #${message.channel.name}: ${err.message}`, client).catch(() => {});
|
||||
logWarn('pinMessage', `Failed to pin message in #${message.channel.name}: ${err.message}`, client).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ async function createStaffThread(channel, client) {
|
||||
if (err.code === 50024 || err.code === 160004) {
|
||||
logWarn('staffThread', `Cannot create private thread in ${channel.name}: server may lack Community features or required boost level (code ${err.code}).`).catch(() => {});
|
||||
}
|
||||
await logError('staffThread:create', err, null, client).catch(() => {});
|
||||
logError('staffThread:create', err, null, client).catch(() => {});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ async function addRoleMembersToThread(thread, guild, client) {
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
} catch (err) {
|
||||
await logError('staffThread:addMembers', err, null, client).catch(() => {});
|
||||
logError('staffThread:addMembers', err, null, client).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,12 @@ function toDiscordSafeName(str) {
|
||||
*/
|
||||
async function resolveCreatorNickname(guild, ticket) {
|
||||
if (ticket.gmailThreadId.startsWith('discord-')) {
|
||||
const creatorUserId = ticket.gmailThreadId.split('-').pop();
|
||||
// Prefer ticket.creatorId (stored on creation). Legacy fallback parses the
|
||||
// tail segment, which is correct for discord-${ts}-${userId} but returns
|
||||
// the message ID for discord-msg-${ts}-${msgId} — skip the parse for those.
|
||||
const creatorUserId = ticket.creatorId
|
||||
|| (ticket.gmailThreadId.startsWith('discord-msg-') ? null : ticket.gmailThreadId.split('-').pop());
|
||||
if (!creatorUserId) return getSenderLocal(ticket.senderEmail);
|
||||
try {
|
||||
const member = await guild.members.fetch(creatorUserId);
|
||||
return member.displayName;
|
||||
@@ -305,14 +310,16 @@ async function checkAutoClose(client, sendTicketClosedEmail) {
|
||||
|
||||
await sendTicketClosedEmail(ticket, 'Auto-Close System', null);
|
||||
|
||||
setTimeout(() => {
|
||||
// Lazy require — broccolini-discord re-exports trackTimeout; cycle-safe.
|
||||
const { trackTimeout } = require('../broccolini-discord');
|
||||
trackTimeout(setTimeout(() => {
|
||||
enqueueDelete(channel).then(() => {
|
||||
withRetry(() => Ticket.updateOne(
|
||||
{ gmailThreadId: ticket.gmailThreadId },
|
||||
{ $unset: { pendingDelete: '' } }
|
||||
)).catch(() => {});
|
||||
}).catch(() => {});
|
||||
}, 5000);
|
||||
}, 5000));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Auto-close error for ticket ${ticket.gmailThreadId}:`, error);
|
||||
|
||||
Reference in New Issue
Block a user