simplify: prune dead code, dedup gmail send, drop neutered log stubs

- Remove no-op log stubs (logGmail, logAutomation, logSecurity, logSystem)
  and ~17 callsites; dead counters in tickets.js and gmail-poll.js go too
- Dedup three near-identical Gmail send paths into sendThreadedEmail helper
- Drop dead Mongoose fields: broccoliniTicketId, lastSyncedBroccoliniArticleId,
  renameCount, renameWindowStart, reminderSent, staffChannelId,
  unclaimedRemindersSent, lastMessageAuthorIsStaff
- Drop dead config fields and their .env.example entries
- Inline api/botClient.js (3-line wrapper, 2 callers)
- Trim unused exports across utils.js, tickets.js, configSchema.js, debugLog.js
- Fix handlers/messages.js to use isStaff() — old partial check ignored
  ADDITIONAL_STAFF_ROLES, so those members were treated as customers
- Drop unused deps p-queue + dotenv-expand; move mongodb to devDependencies

Net: -583 LOC source + -57 LOC lockfile. All 23 modules load clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 18:37:14 +00:00
parent d5547e5eea
commit 840b6bfcf8
18 changed files with 165 additions and 805 deletions

View File

@@ -2,11 +2,9 @@
* Ticket database helpers counters, rename, limits, auto-close,
* reminders, auto-unclaim, channel creation.
*/
const { ChannelType, PermissionFlagsBits } = require('discord.js');
const { ChannelType } = require('discord.js');
const { mongoose, withRetry } = require('../db-connection');
const { CONFIG } = require('../config');
const { getPriorityEmoji } = require('../utils');
const { logAutomation } = require('../services/debugLog');
const { enqueueSend, enqueueDelete } = require('./channelQueue');
const Ticket = mongoose.model('Ticket');
@@ -30,9 +28,6 @@ async function getNextTicketNumber(senderEmail) {
// primary bot's 2/10min per-channel budget here; 429s from the secondary
// bot surface via utils/renamer.js instead.
const RENAME_WINDOW_MS = 10 * 60 * 1000; // 10 minutes (unused; kept for back-compat)
const RENAME_LIMIT = 2;
function getSenderLocal(senderEmail) {
return (senderEmail || 'unknown').split('@')[0].toLowerCase();
}
@@ -90,16 +85,6 @@ function makeTicketName(state, ticket, creatorNickname, claimerEmoji) {
}
}
// Retained for external callers (bOSScord, scripts). The gate now lives in
// the secondary bot's rate bucket; this helper no longer touches Mongo.
async function canRename(_ticket) {
return { ok: true, remaining: RENAME_LIMIT, waitMs: 0 };
}
function minutesFromMs(ms) {
return Math.max(1, Math.ceil(ms / 60000));
}
// --- RATE LIMIT (per-user ticket creation) ---
const ticketCreationByUser = new Map(); // userId -> { count, resetAt }
@@ -156,15 +141,6 @@ function escapeCategoryNameForRegex(name) {
return String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* @deprecated Use getOrCreateTicketCategory instead.
* @returns {null}
*/
function pickTicketCategoryId(guild, categoryIds) {
console.warn('[tickets] pickTicketCategoryId is deprecated; use getOrCreateTicketCategory() instead');
return null;
}
function countChannelsInCategory(guild, categoryId) {
return guild.channels.cache.filter(c => c.parentId === categoryId).size;
}
@@ -272,52 +248,6 @@ async function cleanupEmptyOverflowCategory(guild, categoryId, categoryName) {
}
}
async function createTicketChannel(guild, ticketNumber, userId, subject, creatorNickname) {
let parentId;
try {
parentId = await getOrCreateTicketCategory(guild, CONFIG.TICKET_CATEGORY_ID, CONFIG.TICKET_CATEGORY_NAME);
} catch (e) {
console.error('getOrCreateTicketCategory (createTicketChannel):', e);
throw new Error('Ticket category not found or could not be allocated');
}
let channel;
try {
channel = await guild.channels.create({
name: creatorNickname ? toDiscordSafeName(`unclaimed-${creatorNickname}-${ticketNumber}`) : `ticket-${ticketNumber}`,
type: ChannelType.GuildText,
parent: parentId,
permissionOverwrites: [
{
id: guild.id,
deny: [PermissionFlagsBits.ViewChannel]
},
{
id: userId,
allow: [
PermissionFlagsBits.ViewChannel,
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.ReadMessageHistory
]
},
{
id: CONFIG.ROLE_ID_TO_PING,
allow: [
PermissionFlagsBits.ViewChannel,
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.ReadMessageHistory
]
}
]
});
} catch (e) {
console.error('guild.channels.create (createTicketChannel):', e);
throw e;
}
return channel;
}
// --- LIMITS & PERMISSIONS ---
async function checkTicketLimits(senderEmail) {
@@ -334,22 +264,12 @@ async function checkTicketLimits(senderEmail) {
return { ok: true };
}
function hasBlacklistedRole(member) {
if (!CONFIG.BLACKLISTED_ROLES || CONFIG.BLACKLISTED_ROLES.length === 0) {
return false;
}
return member.roles.cache.some(role =>
CONFIG.BLACKLISTED_ROLES.includes(role.id)
);
}
// --- ACTIVITY ---
async function updateTicketActivity(gmailThreadId) {
const now = new Date();
await Ticket.updateOne(
{ gmailThreadId },
{ $set: { lastActivity: now, reminderSent: false } }
{ $set: { lastActivity: new Date() } }
);
}
@@ -366,9 +286,7 @@ async function checkAutoClose(client, sendTicketClosedEmail) {
lastActivity: { $lt: cutoffTime, $ne: null }
}).sort({ createdAt: 1 }).limit(500).lean());
let checked = 0, closed = 0;
for (const ticket of staleTickets) {
checked++;
try {
const guild = client.guilds.cache.first();
if (!guild) continue;
@@ -395,53 +313,11 @@ async function checkAutoClose(client, sendTicketClosedEmail) {
)).catch(() => {});
}).catch(() => {});
}, 5000);
closed++;
}
} catch (error) {
console.error(`Auto-close error for ticket ${ticket.gmailThreadId}:`, error);
}
}
logAutomation('Auto-close run', null, `checked: ${checked}, closed: ${closed}`).catch(() => {});
}
async function checkReminders(client) {
if (!CONFIG.REMINDER_ENABLED) return;
const reminderTime = new Date(Date.now() - (CONFIG.REMINDER_AFTER_HOURS * 60 * 60 * 1000));
const ticketsNeedingReminder = await withRetry(() => Ticket.find({
status: 'open',
lastActivity: { $lt: reminderTime, $ne: null },
reminderSent: false
}).lean());
let checked = 0, reminded = 0;
for (const ticket of ticketsNeedingReminder) {
checked++;
try {
const guild = client.guilds.cache.first();
if (!guild) continue;
const channel = await guild.channels.fetch(ticket.discordThreadId).catch(() => null);
if (channel) {
const ping = ticket.claimedBy
? `<@${ticket.claimedBy}>`
: (CONFIG.ROLE_ID_TO_PING ? `<@&${CONFIG.ROLE_ID_TO_PING}>` : 'everyone');
const message = CONFIG.REMINDER_MESSAGE
.replace(/\{hours\}/g, String(CONFIG.REMINDER_AFTER_HOURS))
.replace(/\{ping\}/g, ping);
await enqueueSend(channel, message);
await withRetry(() => Ticket.updateOne(
{ gmailThreadId: ticket.gmailThreadId },
{ $set: { reminderSent: true } }
));
reminded++;
}
} catch (error) {
console.error(`Reminder error for ticket ${ticket.gmailThreadId}:`, error);
}
}
logAutomation('Reminder run', null, `checked: ${checked}, reminded: ${reminded}`).catch(() => {});
}
async function checkAutoUnclaim(client) {
@@ -454,9 +330,7 @@ async function checkAutoUnclaim(client) {
lastActivity: { $lt: unclaimTime, $ne: null }
}).lean());
let checked = 0, unclaimed = 0;
for (const ticket of staleClaimedTickets) {
checked++;
try {
const guild = client.guilds.cache.first();
if (!guild) continue;
@@ -473,18 +347,16 @@ async function checkAutoUnclaim(client) {
);
console.log(`Auto-unclaimed ticket ${ticket.gmailThreadId}`);
unclaimed++;
}
} catch (error) {
console.error(`Auto-unclaim error for ticket ${ticket.gmailThreadId}:`, error);
}
}
logAutomation('Auto-unclaim run', null, `checked: ${checked}, unclaimed: ${unclaimed}`).catch(() => {});
}
async function reconcileDeletedTicketChannels(client) {
const guild = client.guilds.cache.get(CONFIG.DISCORD_GUILD_ID) || client.guilds.cache.first();
if (!guild) return { checked: 0, reconciled: 0 };
if (!guild) return;
// Bounded per-tick; a larger backlog drains in subsequent hourly runs.
const openTickets = await Ticket.find({
@@ -492,9 +364,7 @@ async function reconcileDeletedTicketChannels(client) {
discordThreadId: { $ne: null }
}).sort({ createdAt: 1 }).limit(500).lean();
let checked = 0, reconciled = 0;
for (const ticket of openTickets) {
checked++;
try {
let channel = guild.channels.cache.get(ticket.discordThreadId);
if (!channel) {
@@ -505,17 +375,11 @@ async function reconcileDeletedTicketChannels(client) {
{ gmailThreadId: ticket.gmailThreadId },
{ $set: { status: 'closed', discordThreadId: null } }
);
logAutomation('Reconcile: channel deleted', ticket.discordThreadId, `ticket #${ticket.ticketNumber}`).catch(() => {});
reconciled++;
}
} catch (err) {
console.error(`reconcileDeletedTicketChannels error for ${ticket.gmailThreadId}:`, err);
}
}
if (reconciled > 0) {
logAutomation('Reconcile run', null, `checked: ${checked}, reconciled: ${reconciled}`).catch(() => {});
}
return { checked, reconciled };
}
/**
@@ -525,8 +389,7 @@ async function reconcileDeletedTicketChannels(client) {
*/
async function resumePendingDeletes(client) {
const pending = await Ticket.find({ pendingDelete: true }).lean().catch(() => []);
if (!pending.length) return 0;
let resumed = 0;
if (!pending.length) return;
for (const ticket of pending) {
try {
const guild = client.guilds.cache.first();
@@ -534,7 +397,6 @@ async function resumePendingDeletes(client) {
const channel = await guild.channels.fetch(ticket.discordThreadId).catch(() => null);
if (channel) {
enqueueDelete(channel).catch(() => {});
resumed++;
}
}
Ticket.updateOne(
@@ -545,33 +407,22 @@ async function resumePendingDeletes(client) {
console.error('resumePendingDeletes error:', e);
}
}
logAutomation('Pending-delete resume', null, `pending: ${pending.length}, resumed: ${resumed}`).catch(() => {});
return resumed;
}
module.exports = {
getNextTicketNumber,
getOrCreateTicketCategory,
cleanupEmptyOverflowCategory,
RENAME_WINDOW_MS,
RENAME_LIMIT,
getSenderLocal,
toDiscordSafeName,
resolveCreatorNickname,
makeTicketName,
canRename,
minutesFromMs,
checkTicketCreationRateLimit,
createTicketChannel,
checkTicketLimits,
hasBlacklistedRole,
updateTicketActivity,
checkAutoClose,
checkReminders,
checkAutoUnclaim,
reconcileDeletedTicketChannels,
resumePendingDeletes,
startTicketsSweeps,
sweepTicketCreationByUser,
_internals: { ticketCreationByUser, TICKET_CREATION_SWEEP_TTL_MS }
startTicketsSweeps
};