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.
This commit is contained in:
@@ -5,8 +5,18 @@ function normalizeKey(text) {
|
||||
return text.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 200);
|
||||
}
|
||||
|
||||
export function trackCrossChannelMessage(guildId, userId, channelId, messageId, text) {
|
||||
const normalized = normalizeKey(text);
|
||||
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: [] };
|
||||
}
|
||||
@@ -34,13 +44,40 @@ export function trackCrossChannelMessage(guildId, userId, channelId, messageId,
|
||||
firstSeen: now,
|
||||
});
|
||||
|
||||
if (recentPosts.size > 5000) {
|
||||
for (const [entryKey, entry] of recentPosts) {
|
||||
if (now - entry.firstSeen > WINDOW_MS) {
|
||||
recentPosts.delete(entryKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user