Files
Discord_Bots/Anti-Spam/src/rapidSpamTracker.js
T
andrew ef4d2aeb40 Add image-only spam detection and tracking
Add support for detecting and tracking image-only spam across channels. spamDetector.js: introduce image helpers (isImageAttachment, getImageAttachments), buildImageFingerprint, isImageOnlyMessage, and extend detectSpam to report image-only reasons and a duplicateImageAcrossChannels flag. rapidSpamTracker.js: refactor cross-channel tracking with trackCrossChannelPost, pruneExpiredPosts, dedupePriorMessages, and add trackImageOnlySpam which uses optional image fingerprints; export dedupePriorMessages. index.js: wire image-only tracking into message handling (build fingerprint, trackImageOnlySpam, pass duplicateImageAcrossChannels to detectSpam), merge and dedupe prior message lists, and include attachments in mod log previews. commands.js: update detection rules text to mention image spam. These changes enable identifying image-only spam (including fingerprinting) and avoid duplicate prior-message entries when creating moderation logs.
2026-06-17 17:35:07 +12:00

84 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 trackImageOnlySpam(guildId, userId, channelId, messageId, fingerprint) {
const trackers = [trackCrossChannelPost(guildId, userId, channelId, messageId, "imgonly")];
if (fingerprint) {
trackers.push(
trackCrossChannelPost(guildId, userId, channelId, messageId, `imgfp:${fingerprint}`)
);
}
const duplicateAcrossChannels = trackers.some((tracker) => tracker.duplicateAcrossChannels);
const priorMessages = duplicateAcrossChannels
? dedupePriorMessages(trackers.flatMap((tracker) => tracker.priorMessages))
: [];
return { duplicateAcrossChannels, priorMessages };
}
export { dedupePriorMessages };