024f1082ef
Replace per-case trackers with a unified key-based tracker and tighten detection logic. Introduces buildMessageTrackingKeys/collectTrackableText to canonicalize text, extract invite codes, include image fingerprints and dedupe keys; replaces trackCrossChannelMessage/trackImageOnlySpam with trackCrossChannelKeys that tracks multiple normalized keys. Simplifies detectSpam to use trackable content and invite-aware link handling, updates message preview/attachment embedding, and adjusts command/help text to reflect the new "same content across channels" rule. Overall reduces duplicate-prior merging and streamlines cross-channel spam handling.
85 lines
2.5 KiB
JavaScript
85 lines
2.5 KiB
JavaScript
const WINDOW_MS = 150_000; // 2.5 minutes
|
|
const recentPosts = new Map();
|
|
|
|
function normalizeKey(text) {
|
|
return text.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 200);
|
|
}
|
|
|
|
function dedupePriorMessages(priorMessages) {
|
|
const seen = new Set();
|
|
return priorMessages.filter(({ channelId, messageId }) => {
|
|
const entry = `${channelId}:${messageId}`;
|
|
if (seen.has(entry)) return false;
|
|
seen.add(entry);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function trackCrossChannelPost(guildId, userId, channelId, messageId, keySuffix) {
|
|
const normalized = normalizeKey(keySuffix);
|
|
if (!normalized) {
|
|
return { duplicateAcrossChannels: false, priorMessages: [] };
|
|
}
|
|
|
|
const key = `${guildId}:${userId}:${normalized}`;
|
|
const now = Date.now();
|
|
const existing = recentPosts.get(key);
|
|
|
|
if (existing && now - existing.firstSeen < WINDOW_MS) {
|
|
const priorMessages = [...existing.messages.entries()].map(([chId, msgId]) => ({
|
|
channelId: chId,
|
|
messageId: msgId,
|
|
}));
|
|
|
|
existing.channels.add(channelId);
|
|
existing.messages.set(channelId, messageId);
|
|
|
|
const duplicateAcrossChannels = existing.channels.size >= 2;
|
|
return { duplicateAcrossChannels, priorMessages: duplicateAcrossChannels ? priorMessages : [] };
|
|
}
|
|
|
|
recentPosts.set(key, {
|
|
channels: new Set([channelId]),
|
|
messages: new Map([[channelId, messageId]]),
|
|
firstSeen: now,
|
|
});
|
|
|
|
pruneExpiredPosts(now);
|
|
|
|
return { duplicateAcrossChannels: false, priorMessages: [] };
|
|
}
|
|
|
|
function pruneExpiredPosts(now) {
|
|
if (recentPosts.size <= 5000) return;
|
|
|
|
for (const [entryKey, entry] of recentPosts) {
|
|
if (now - entry.firstSeen > WINDOW_MS) {
|
|
recentPosts.delete(entryKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function trackCrossChannelMessage(guildId, userId, channelId, messageId, text) {
|
|
return trackCrossChannelPost(guildId, userId, channelId, messageId, normalizeKey(text));
|
|
}
|
|
|
|
export function trackCrossChannelKeys(guildId, userId, channelId, messageId, keys) {
|
|
const uniqueKeys = [...new Set(keys.map((key) => normalizeKey(key)).filter(Boolean))];
|
|
if (uniqueKeys.length === 0) {
|
|
return { duplicateAcrossChannels: false, priorMessages: [] };
|
|
}
|
|
|
|
const trackers = uniqueKeys.map((key) =>
|
|
trackCrossChannelPost(guildId, userId, channelId, messageId, key)
|
|
);
|
|
|
|
const duplicateAcrossChannels = trackers.some((tracker) => tracker.duplicateAcrossChannels);
|
|
const priorMessages = duplicateAcrossChannels
|
|
? dedupePriorMessages(trackers.flatMap((tracker) => tracker.priorMessages))
|
|
: [];
|
|
|
|
return { duplicateAcrossChannels, priorMessages };
|
|
}
|
|
|
|
export { dedupePriorMessages };
|