changes
This commit is contained in:
@@ -25,7 +25,24 @@
|
|||||||
"Bash(node --check handlers/messages.js)",
|
"Bash(node --check handlers/messages.js)",
|
||||||
"Bash(node *)",
|
"Bash(node *)",
|
||||||
"Bash(npm info *)",
|
"Bash(npm info *)",
|
||||||
"Bash(npm ls *)"
|
"Bash(npm ls *)",
|
||||||
|
"Bash(curl -sk http://127.0.0.1:12752/css/main.css)",
|
||||||
|
"Bash(curl -sk http://127.0.0.1:12752/js/app.js)",
|
||||||
|
"Bash(curl -sk -o /dev/null -w \"HTTP %{http_code}\\\\n\" http://127.0.0.1:12752/css/main.css)",
|
||||||
|
"Bash(curl -sk -o /dev/null -w \"HTTP %{http_code}\\\\n\" http://127.0.0.1:12752/js/app.js)",
|
||||||
|
"Bash(docker compose *)",
|
||||||
|
"Bash(curl -sI http://100.114.205.53:12752/healthz)",
|
||||||
|
"Bash(curl -s http://100.114.205.53:12752/healthz)",
|
||||||
|
"mcp__plugin_context7_context7__query-docs",
|
||||||
|
"Bash(curl -sI http://100.114.205.53:12752/api/config)",
|
||||||
|
"Bash(curl -sI http://100.114.205.53:12752/)",
|
||||||
|
"Bash(curl -sI http://100.114.205.53:12752/login)",
|
||||||
|
"Bash(curl -sI http://100.114.205.53:12752/some/nonexistent/path)",
|
||||||
|
"Bash(curl -sI http://100.114.205.53:12752/js/app.js)",
|
||||||
|
"Bash(curl -sI http://100.114.205.53:12752/js/util.js)",
|
||||||
|
"Bash(curl -sI http://100.114.205.53:12752/js/notifications.js)",
|
||||||
|
"Bash(docker exec *)",
|
||||||
|
"Bash(curl -sI http://100.114.205.53:12752/api/notifications/alerts)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +1,53 @@
|
|||||||
/**
|
/**
|
||||||
* Per-channel rename rate limiting with queue.
|
* Per-channel rename serialization with coalescing.
|
||||||
* Discord allows 2 channel renames per 10 minutes per channel.
|
* Renames route through utils/renamer.js (secondary bot token, RENAMER_BOT),
|
||||||
* We use a 9-minute window for safety margin.
|
* which has its own Discord-side rate bucket — no in-process throttle needed.
|
||||||
|
* We serialize per channel so concurrent PATCHes don't land out of order, and
|
||||||
|
* coalesce rapid successive calls so only the latest name is written.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const RENAME_WINDOW_MS = 9 * 60 * 1000;
|
|
||||||
const RENAME_LIMIT = 2;
|
|
||||||
const { logWarn } = require('../services/debugLog');
|
const { logWarn } = require('../services/debugLog');
|
||||||
|
const { renameChannel } = require('../utils/renamer');
|
||||||
|
|
||||||
// Per-channel state: { count, windowStart, queue: [{newName, started}], processing }
|
// Per-channel: { chain: Promise, pendingName: string | null }.
|
||||||
const renameState = new Map();
|
// enqueueRename updates pendingName synchronously (latest wins) and chains an
|
||||||
|
// executeRename link. executeRename reads the latest pendingName at start.
|
||||||
|
const renameChains = new Map();
|
||||||
|
|
||||||
function getOrInitState(channelId) {
|
async function executeRename(channel, entry) {
|
||||||
let state = renameState.get(channelId);
|
const currentName = entry.pendingName;
|
||||||
if (!state) {
|
if (currentName == null) return;
|
||||||
state = { count: 0, windowStart: 0, queue: [], processing: false };
|
try {
|
||||||
renameState.set(channelId, state);
|
await renameChannel(channel.id, currentName);
|
||||||
}
|
} finally {
|
||||||
return state;
|
// Clear only if no newer call arrived during the PATCH. If pendingName
|
||||||
}
|
// has changed, leave it — the link queued by that newer call picks it up.
|
||||||
|
if (entry.pendingName === currentName) {
|
||||||
async function executeRename(channel, newName) {
|
entry.pendingName = null;
|
||||||
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
|
|
||||||
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) {
|
function enqueueRename(channel, newName) {
|
||||||
const state = getOrInitState(channel.id);
|
let entry = renameChains.get(channel.id);
|
||||||
const now = Date.now();
|
if (!entry) {
|
||||||
|
entry = { chain: Promise.resolve(), pendingName: newName };
|
||||||
// Window expired — reset
|
renameChains.set(channel.id, entry);
|
||||||
if (now - state.windowStart >= RENAME_WINDOW_MS) {
|
} else {
|
||||||
state.count = 1;
|
entry.pendingName = newName;
|
||||||
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
|
const next = entry.chain.catch(() => {}).then(() => executeRename(channel, entry));
|
||||||
if (state.count < RENAME_LIMIT) {
|
entry.chain = next;
|
||||||
state.count++;
|
|
||||||
executeRename(channel, newName).catch((err) => {
|
|
||||||
logWarn('renameQueue', `Immediate rename failed for ${channel.name}: ${err.message || err}`).catch(() => {});
|
|
||||||
});
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// At limit — replace pending rename with latest
|
next.catch((err) => {
|
||||||
const isNew = state.queue.length === 0;
|
logWarn('renameQueue', `Rename failed for ${channel.name}: ${err && err.message || err}`).catch(() => {});
|
||||||
state.queue[0] = { newName, started: false };
|
}).finally(() => {
|
||||||
const queuedItem = state.queue[0];
|
if (renameChains.get(channel.id) === entry && entry.chain === next && entry.pendingName == null) {
|
||||||
|
renameChains.delete(channel.id);
|
||||||
if (isNew) {
|
}
|
||||||
setTimeout(() => {
|
});
|
||||||
if (queuedItem.started) return;
|
return next;
|
||||||
const estMinutes = Math.ceil(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) {
|
function enqueueMove(channel, categoryId) {
|
||||||
|
|||||||
62
utils/renamer.js
Normal file
62
utils/renamer.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Secondary-token channel rename helper.
|
||||||
|
*
|
||||||
|
* Routes channel/thread renames through a second bot token (RENAMER_BOT)
|
||||||
|
* so renames don't consume the primary bot's per-channel 2/10min budget.
|
||||||
|
*
|
||||||
|
* The secondary bot must be invited to the guild with Manage Channels
|
||||||
|
* and Manage Threads.
|
||||||
|
*
|
||||||
|
* Not called directly from feature code — invoked by services/channelQueue.js
|
||||||
|
* so all channel ops continue to flow through the queue.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { logWarn } = require('../services/debugLog');
|
||||||
|
|
||||||
|
const DISCORD_API = 'https://discord.com/api/v10';
|
||||||
|
|
||||||
|
async function renameChannel(channelId, newName) {
|
||||||
|
const token = (process.env.RENAMER_BOT || '').trim();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('RENAMER_BOT is not set; cannot rename via secondary token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${DISCORD_API}/channels/${channelId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bot ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: newName })
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = text ? JSON.parse(text) : null;
|
||||||
|
} catch (_) {
|
||||||
|
body = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 429) {
|
||||||
|
const retryAfter = (body && typeof body === 'object' && body.retry_after) || null;
|
||||||
|
logWarn('renamer', `429 rename channel=${channelId} retry_after=${retryAfter}`).catch(() => {});
|
||||||
|
const err = new Error(`rename 429: retry_after=${retryAfter}`);
|
||||||
|
err.status = 429;
|
||||||
|
err.retryAfter = retryAfter;
|
||||||
|
err.body = body;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
||||||
|
const err = new Error(`rename failed: status=${res.status} body=${bodyStr}`);
|
||||||
|
err.status = res.status;
|
||||||
|
err.body = body;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { renameChannel };
|
||||||
Reference in New Issue
Block a user