Add clustered bot visitor filter

This commit is contained in:
rajchales-monito
2026-07-30 13:54:34 +02:00
parent d815fa6d0e
commit a0bef3baa3
3 changed files with 107 additions and 18 deletions
+62 -6
View File
@@ -363,7 +363,10 @@ function defaultSettings() {
},
adminPresence: {
activeSeconds: 30,
offlineSeconds: 120
offlineSeconds: 120,
clusterBotFilterEnabled: true,
clusterBotCount: 20,
clusterBotWindowSeconds: 60
}
};
}
@@ -1482,7 +1485,13 @@ function mergeSiteSettings(currentSettings, incomingSettings) {
function normalizeAdminPresenceSettings(value = {}) {
const activeSeconds = clampNumber(value.activeSeconds, 10, 3600, 30);
const offlineSeconds = Math.max(clampNumber(value.offlineSeconds, 20, 7200, 120), activeSeconds + 10);
return { activeSeconds, offlineSeconds };
return {
activeSeconds,
offlineSeconds,
clusterBotFilterEnabled: value.clusterBotFilterEnabled !== false,
clusterBotCount: clampNumber(value.clusterBotCount, 3, 500, 20),
clusterBotWindowSeconds: clampNumber(value.clusterBotWindowSeconds, 10, 3600, 60)
};
}
function adminPresenceSettings() {
@@ -1829,10 +1838,11 @@ function currentVisitors() {
ORDER BY datetime(vs.first_seen_at) ASC, vs.id ASC
LIMIT 100
`).all();
return rows.map((row) => formatVisitorSession(row, presence));
const clusters = suspiciousVisitorClusters(rows, presence);
return rows.map((row) => formatVisitorSession(row, presence, clusters));
}
function formatVisitorSession(row, thresholds = adminPresenceSettings()) {
function formatVisitorSession(row, thresholds = adminPresenceSettings(), clusters = new Map()) {
const history = parseJson(row.browsing_history_json, []);
const device = parseJson(row.device_json, {});
const pageCountValue = new Set(history.map((item) => item.url).filter(Boolean)).size;
@@ -1845,6 +1855,8 @@ function formatVisitorSession(row, thresholds = adminPresenceSettings()) {
sessionSeconds: sessionSecondsValue,
referrer: row.referrer
});
const clusterReason = clusters.get(visitorClusterKey(row, device));
const finalBot = clusterReason && conversationCountValue === 0 ? { isBot: true, reason: clusterReason } : bot;
const countryCode = row.country_code || countryFromTimezone(row.timezone) || countryFromLanguage(row.language);
const presence = visitorPresence(row, thresholds);
return {
@@ -1862,8 +1874,8 @@ function formatVisitorSession(row, thresholds = adminPresenceSettings()) {
visitCount: Math.max(1, history.length),
conversationCount: conversationCountValue,
lastConversationId: row.last_conversation_id,
isBot: bot.isBot,
botReason: bot.reason,
isBot: finalBot.isBot,
botReason: finalBot.reason,
device,
language: row.language,
timezone: row.timezone,
@@ -1884,6 +1896,50 @@ function visitorPresence(row, thresholds = adminPresenceSettings()) {
return Number.isFinite(lastActivity) && Date.now() - lastActivity <= thresholds.activeSeconds * 1000 ? "active" : "idle";
}
function suspiciousVisitorClusters(rows, settings) {
if (!settings.clusterBotFilterEnabled) return new Map();
const groups = new Map();
for (const row of rows) {
if (Number(row.visitor_conversation_count || 0) > 0) continue;
if (String(row.referrer || "").trim()) continue;
const device = parseJson(row.device_json, {});
const key = visitorClusterKey(row, device);
if (!key) continue;
const firstSeen = parseTimestampMs(row.first_seen_at);
if (!Number.isFinite(firstSeen)) continue;
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(firstSeen);
}
const suspicious = new Map();
const windowMs = settings.clusterBotWindowSeconds * 1000;
for (const [key, times] of groups.entries()) {
times.sort((a, b) => a - b);
let start = 0;
for (let end = 0; end < times.length; end += 1) {
while (times[end] - times[start] > windowMs) start += 1;
if (end - start + 1 >= settings.clusterBotCount) {
suspicious.set(key, `cluster ${end - start + 1}/${settings.clusterBotWindowSeconds}s`);
break;
}
}
}
return suspicious;
}
function visitorClusterKey(row, device = {}) {
const ip = String(row.ip || "").trim();
if (!ip) return "";
const country = String(row.country_code || countryFromTimezone(row.timezone) || countryFromLanguage(row.language) || "").toUpperCase();
const platform = String(device.platform || "").trim();
const ua = String(device.userAgent || "").trim();
const browser = ua.includes("Edg/") ? "Edge"
: ua.includes("Chrome/") ? "Chrome"
: ua.includes("Firefox/") ? "Firefox"
: ua.includes("Safari/") ? "Safari"
: ua.slice(0, 80);
return [ip, country, platform, browser].join("|");
}
function conversationDetails(conversation) {
const messages = db.prepare("SELECT * FROM messages WHERE conversation_id = ? ORDER BY id ASC").all(conversation.id);
const attachments = db.prepare("SELECT * FROM attachments WHERE conversation_id = ? ORDER BY id ASC").all(conversation.id);