/** * Per-channel rename rate limiting with queue. * Discord allows 2 channel renames per 10 minutes per channel. * We use a 9-minute window for safety margin. */ const RENAME_WINDOW_MS = 9 * 60 * 1000; const RENAME_LIMIT = 2; const { logWarn } = require('../services/debugLog'); // Per-channel state: { count, windowStart, queue: [{newName, started}], processing } const renameState = new Map(); function getOrInitState(channelId) { let state = renameState.get(channelId); if (!state) { state = { count: 0, windowStart: 0, queue: [], processing: false }; renameState.set(channelId, state); } return state; } async function executeRename(channel, newName) { await channel.setName(newName); } function processQueue(channel, state) { if (state.queue.length === 0 || state.processing) return; const now = Date.now(); const timeUntilExpiry = (state.windowStart + RENAME_WINDOW_MS) - now; if (timeUntilExpiry > 0) { state.processing = true; setTimeout(async () => { state.processing = false; // New window if (state.queue.length > 3) { logWarn('renameQueue', `Channel ${channel.name} has ${state.queue.length} renames queued`).catch(() => {}); } const item = state.queue.shift(); if (!item) return; item.started = true; state.count = 1; state.windowStart = Date.now(); try { await executeRename(channel, item.newName); } catch (err) { logWarn('renameQueue', `Queued rename failed for ${channel.name}: ${err.message || err}`).catch(() => {}); } // Continue processing remaining queue items processQueue(channel, state); }, timeUntilExpiry); } } function enqueueRename(channel, newName) { const state = getOrInitState(channel.id); const now = Date.now(); // Window expired — reset if (now - state.windowStart >= RENAME_WINDOW_MS) { state.count = 1; state.windowStart = now; executeRename(channel, newName).catch((err) => { logWarn('renameQueue', `Immediate rename failed for ${channel.name}: ${err.message || err}`).catch(() => {}); }); return Promise.resolve(); } // Within window and under limit if (state.count < RENAME_LIMIT) { state.count++; executeRename(channel, newName).catch((err) => { logWarn('renameQueue', `Immediate rename failed for ${channel.name}: ${err.message || err}`).catch(() => {}); }); return Promise.resolve(); } // At limit — queue it const queueSize = state.queue.length + 1; const queuedItem = { newName, started: false }; state.queue.push(queuedItem); // Only notify if this rename is still waiting after ~2s. setTimeout(() => { if (queuedItem.started) return; const estMinutes = Math.max(1, Math.ceil((queueSize * RENAME_WINDOW_MS) / 60000)); channel.send(`⏳ Channel will be renamed in ~${estMinutes} minute${estMinutes === 1 ? '' : 's'}.`).catch(() => {}); }, 2000); processQueue(channel, state); return Promise.resolve(); } function enqueueMove(channel, categoryId) { return channel.setParent(categoryId, { lockPermissions: true }); } module.exports = { enqueueRename, enqueueMove };