/** * 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; // Per-channel state: { count, windowStart, queue: [{newName, resolve, reject}], 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) { const { logWarn } = require('../services/debugLog'); logWarn('renameQueue', `Channel ${channel.name} has ${state.queue.length} renames queued`).catch(() => {}); } const item = state.queue.shift(); if (!item) return; state.count = 1; state.windowStart = Date.now(); try { await executeRename(channel, item.newName); item.resolve(); } catch (err) { item.reject(err); } // Continue processing remaining queue items processQueue(channel, state); }, timeUntilExpiry); } } function enqueueRename(channel, newName) { return new Promise((resolve, reject) => { 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).then(resolve).catch(reject); return; } // Within window and under limit if (state.count < RENAME_LIMIT) { state.count++; executeRename(channel, newName).then(resolve).catch(reject); return; } // At limit — queue it state.queue.push({ newName, resolve, reject }); processQueue(channel, state); }); } function enqueueMove(channel, categoryId) { return channel.setParent(categoryId, { lockPermissions: true }); } module.exports = { enqueueRename, enqueueMove };