show running process in process viewer and add limiting to the notification system
This commit is contained in:
@@ -6,6 +6,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func listProcesses() ([]wire.ProcessInfo, error) {
|
func listProcesses() ([]wire.ProcessInfo, error) {
|
||||||
|
selfPID := int32(os.Getpid())
|
||||||
|
|
||||||
cmd := exec.Command("ps", "aux")
|
cmd := exec.Command("ps", "aux")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
@@ -78,6 +80,7 @@ func listProcesses() ([]wire.ProcessInfo, error) {
|
|||||||
Memory: memBytes,
|
Memory: memBytes,
|
||||||
Username: username,
|
Username: username,
|
||||||
Type: procType,
|
Type: procType,
|
||||||
|
Self: int32(pid) == selfPID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -37,6 +38,7 @@ type PROCESS_MEMORY_COUNTERS struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listProcesses() ([]wire.ProcessInfo, error) {
|
func listProcesses() ([]wire.ProcessInfo, error) {
|
||||||
|
selfPID := int32(os.Getpid())
|
||||||
var pids [4096]uint32
|
var pids [4096]uint32
|
||||||
var bytesReturned uint32
|
var bytesReturned uint32
|
||||||
|
|
||||||
@@ -157,6 +159,7 @@ func listProcesses() ([]wire.ProcessInfo, error) {
|
|||||||
Memory: memory,
|
Memory: memory,
|
||||||
Username: username,
|
Username: username,
|
||||||
Type: procType,
|
Type: procType,
|
||||||
|
Self: pid == selfPID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ type ProcessInfo struct {
|
|||||||
Memory uint64 `msgpack:"memory"`
|
Memory uint64 `msgpack:"memory"`
|
||||||
Username string `msgpack:"username,omitempty"`
|
Username string `msgpack:"username,omitempty"`
|
||||||
Type string `msgpack:"type,omitempty"`
|
Type string `msgpack:"type,omitempty"`
|
||||||
|
Self bool `msgpack:"self,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProcessListResult struct {
|
type ProcessListResult struct {
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ const keywordInput = document.getElementById("keyword-input");
|
|||||||
const clipboardEnabledInput = document.getElementById("clipboard-enabled");
|
const clipboardEnabledInput = document.getElementById("clipboard-enabled");
|
||||||
const saveKeywordsBtn = document.getElementById("save-keywords");
|
const saveKeywordsBtn = document.getElementById("save-keywords");
|
||||||
const keywordHint = document.getElementById("keyword-hint");
|
const keywordHint = document.getElementById("keyword-hint");
|
||||||
|
const antiSpamMaxHitsInput = document.getElementById("antispam-max-hits");
|
||||||
|
const antiSpamWindowInput = document.getElementById("antispam-window");
|
||||||
|
const antiSpamCooldownInput = document.getElementById("antispam-cooldown");
|
||||||
|
|
||||||
// Webhook section
|
// Webhook section
|
||||||
const webhookEnabledInput = document.getElementById("webhook-enabled");
|
const webhookEnabledInput = document.getElementById("webhook-enabled");
|
||||||
@@ -476,6 +479,15 @@ async function loadKeywords() {
|
|||||||
if (clipboardEnabledInput) {
|
if (clipboardEnabledInput) {
|
||||||
clipboardEnabledInput.checked = data?.notifications?.clipboardEnabled === true;
|
clipboardEnabledInput.checked = data?.notifications?.clipboardEnabled === true;
|
||||||
}
|
}
|
||||||
|
if (antiSpamMaxHitsInput) {
|
||||||
|
antiSpamMaxHitsInput.value = data?.notifications?.antiSpamMaxHits ?? 15;
|
||||||
|
}
|
||||||
|
if (antiSpamWindowInput) {
|
||||||
|
antiSpamWindowInput.value = Math.round((data?.notifications?.antiSpamWindowMs ?? 600000) / 60000);
|
||||||
|
}
|
||||||
|
if (antiSpamCooldownInput) {
|
||||||
|
antiSpamCooldownInput.value = Math.round((data?.notifications?.antiSpamCooldownMs ?? 600000) / 60000);
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,11 +496,21 @@ function wireKeywordSave() {
|
|||||||
saveKeywordsBtn.addEventListener("click", async () => {
|
saveKeywordsBtn.addEventListener("click", async () => {
|
||||||
const keywords = parseKeywords(keywordInput.value);
|
const keywords = parseKeywords(keywordInput.value);
|
||||||
const clipboardEnabled = clipboardEnabledInput ? clipboardEnabledInput.checked : false;
|
const clipboardEnabled = clipboardEnabledInput ? clipboardEnabledInput.checked : false;
|
||||||
|
const payload = { keywords, clipboardEnabled };
|
||||||
|
if (antiSpamMaxHitsInput) {
|
||||||
|
payload.antiSpamMaxHits = Math.max(1, parseInt(antiSpamMaxHitsInput.value, 10) || 15);
|
||||||
|
}
|
||||||
|
if (antiSpamWindowInput) {
|
||||||
|
payload.antiSpamWindowMs = Math.max(1, parseInt(antiSpamWindowInput.value, 10) || 10) * 60000;
|
||||||
|
}
|
||||||
|
if (antiSpamCooldownInput) {
|
||||||
|
payload.antiSpamCooldownMs = Math.max(1, parseInt(antiSpamCooldownInput.value, 10) || 10) * 60000;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/notifications/config", {
|
const res = await fetch("/api/notifications/config", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ keywords, clipboardEnabled }),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
window.showToast?.("Failed to save keywords", "error", 4000);
|
window.showToast?.("Failed to save keywords", "error", 4000);
|
||||||
@@ -501,6 +523,15 @@ function wireKeywordSave() {
|
|||||||
if (clipboardEnabledInput && typeof data?.notifications?.clipboardEnabled === "boolean") {
|
if (clipboardEnabledInput && typeof data?.notifications?.clipboardEnabled === "boolean") {
|
||||||
clipboardEnabledInput.checked = data.notifications.clipboardEnabled;
|
clipboardEnabledInput.checked = data.notifications.clipboardEnabled;
|
||||||
}
|
}
|
||||||
|
if (antiSpamMaxHitsInput && data?.notifications?.antiSpamMaxHits) {
|
||||||
|
antiSpamMaxHitsInput.value = data.notifications.antiSpamMaxHits;
|
||||||
|
}
|
||||||
|
if (antiSpamWindowInput && data?.notifications?.antiSpamWindowMs) {
|
||||||
|
antiSpamWindowInput.value = Math.round(data.notifications.antiSpamWindowMs / 60000);
|
||||||
|
}
|
||||||
|
if (antiSpamCooldownInput && data?.notifications?.antiSpamCooldownMs) {
|
||||||
|
antiSpamCooldownInput.value = Math.round(data.notifications.antiSpamCooldownMs / 60000);
|
||||||
|
}
|
||||||
window.showToast?.("Keywords updated", "success", 3000);
|
window.showToast?.("Keywords updated", "success", 3000);
|
||||||
} catch {
|
} catch {
|
||||||
window.showToast?.("Failed to save keywords", "error", 4000);
|
window.showToast?.("Failed to save keywords", "error", 4000);
|
||||||
|
|||||||
@@ -255,6 +255,9 @@ function createProcessRow(proc, depth = 0) {
|
|||||||
if (selectedPid === proc.pid) {
|
if (selectedPid === proc.pid) {
|
||||||
row.classList.add("selected");
|
row.classList.add("selected");
|
||||||
}
|
}
|
||||||
|
if (proc.self) {
|
||||||
|
row.classList.add("self-process");
|
||||||
|
}
|
||||||
|
|
||||||
const cpuColor =
|
const cpuColor =
|
||||||
proc.cpu > 50
|
proc.cpu > 50
|
||||||
@@ -276,6 +279,7 @@ function createProcessRow(proc, depth = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let nameColor = "text-slate-200";
|
let nameColor = "text-slate-200";
|
||||||
|
let iconClass = "fa-microchip";
|
||||||
let iconColor = "text-blue-400";
|
let iconColor = "text-blue-400";
|
||||||
if (proc.type === "system") {
|
if (proc.type === "system") {
|
||||||
nameColor = "text-purple-400";
|
nameColor = "text-purple-400";
|
||||||
@@ -287,12 +291,21 @@ function createProcessRow(proc, depth = 0) {
|
|||||||
nameColor = "text-green-300";
|
nameColor = "text-green-300";
|
||||||
iconColor = "text-green-400";
|
iconColor = "text-green-400";
|
||||||
}
|
}
|
||||||
|
if (proc.self) {
|
||||||
|
nameColor = "text-yellow-300 font-semibold";
|
||||||
|
iconClass = "fa-crosshairs";
|
||||||
|
iconColor = "text-yellow-400";
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfBadge = proc.self
|
||||||
|
? ' <span class="ml-1 text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-300 border border-yellow-500/40">agent</span>'
|
||||||
|
: "";
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="col-span-1 text-sm font-mono text-slate-400">${proc.pid}</div>
|
<div class="col-span-1 text-sm font-mono text-slate-400">${proc.pid}</div>
|
||||||
<div class="col-span-4 flex items-center gap-1 truncate">
|
<div class="col-span-4 flex items-center gap-1 truncate">
|
||||||
${indent}${treeIcon}<i class="fa-solid fa-microchip ${iconColor}"></i>
|
${indent}${treeIcon}<i class="fa-solid ${iconClass} ${iconColor}"></i>
|
||||||
<span class="truncate ${nameColor}">${escapeHtml(proc.name)}</span>
|
<span class="truncate ${nameColor}">${escapeHtml(proc.name)}</span>${selfBadge}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2 text-sm ${cpuColor} font-semibold">${proc.cpu.toFixed(1)}%</div>
|
<div class="col-span-2 text-sm ${cpuColor} font-semibold">${proc.cpu.toFixed(1)}%</div>
|
||||||
<div class="col-span-2 text-sm text-slate-400">${memoryStr}</div>
|
<div class="col-span-2 text-sm text-slate-400">${memoryStr}</div>
|
||||||
|
|||||||
@@ -72,6 +72,46 @@
|
|||||||
Monitor clipboard content for keyword matches
|
Monitor clipboard content for keyword matches
|
||||||
</label>
|
</label>
|
||||||
<div class="mt-2 text-xs text-slate-500" id="keyword-hint"></div>
|
<div class="mt-2 text-xs text-slate-500" id="keyword-hint"></div>
|
||||||
|
|
||||||
|
<!-- Anti-Spam Settings -->
|
||||||
|
<div class="mt-4 pt-4 border-t border-slate-800">
|
||||||
|
<h3 class="text-sm font-semibold text-slate-300">Anti-Spam Limits</h3>
|
||||||
|
<p class="text-xs text-slate-500 mt-1">
|
||||||
|
Block a keyword for a client when it fires too many times in a short window.
|
||||||
|
</p>
|
||||||
|
<div class="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-slate-400 mb-1">Max hits before block</label>
|
||||||
|
<input
|
||||||
|
id="antispam-max-hits"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="10000"
|
||||||
|
class="w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 px-3 py-2 text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-slate-400 mb-1">Detection window (minutes)</label>
|
||||||
|
<input
|
||||||
|
id="antispam-window"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="1440"
|
||||||
|
class="w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 px-3 py-2 text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-slate-400 mb-1">Cooldown (minutes)</label>
|
||||||
|
<input
|
||||||
|
id="antispam-cooldown"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="1440"
|
||||||
|
class="w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 px-3 py-2 text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Per-user Webhook Delivery -->
|
<!-- Per-user Webhook Delivery -->
|
||||||
|
|||||||
@@ -26,6 +26,14 @@
|
|||||||
background-color: rgba(59, 130, 246, 0.3);
|
background-color: rgba(59, 130, 246, 0.3);
|
||||||
border-color: rgba(59, 130, 246, 0.6);
|
border-color: rgba(59, 130, 246, 0.6);
|
||||||
}
|
}
|
||||||
|
.process-row.self-process {
|
||||||
|
background-color: rgba(234, 179, 8, 0.12);
|
||||||
|
border-color: rgba(234, 179, 8, 0.5);
|
||||||
|
box-shadow: inset 3px 0 0 0 #facc15;
|
||||||
|
}
|
||||||
|
.process-row.self-process.selected {
|
||||||
|
background-color: rgba(234, 179, 8, 0.28);
|
||||||
|
}
|
||||||
/* Dark scrollbar styling */
|
/* Dark scrollbar styling */
|
||||||
#process-list::-webkit-scrollbar {
|
#process-list::-webkit-scrollbar {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
@@ -189,6 +197,10 @@
|
|||||||
<i class="fa-solid fa-circle text-slate-200"></i>
|
<i class="fa-solid fa-circle text-slate-200"></i>
|
||||||
<span class="text-slate-400">Other</span>
|
<span class="text-slate-400">Other</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<i class="fa-solid fa-crosshairs text-yellow-400"></i>
|
||||||
|
<span class="text-slate-400">Overlord Agent</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Process List -->
|
<!-- Process List -->
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ export interface Config {
|
|||||||
telegramBotToken: string;
|
telegramBotToken: string;
|
||||||
telegramChatId: string;
|
telegramChatId: string;
|
||||||
clipboardEnabled: boolean;
|
clipboardEnabled: boolean;
|
||||||
|
antiSpamMaxHits: number;
|
||||||
|
antiSpamWindowMs: number;
|
||||||
|
antiSpamCooldownMs: number;
|
||||||
};
|
};
|
||||||
security: {
|
security: {
|
||||||
sessionTtlHours: number;
|
sessionTtlHours: number;
|
||||||
@@ -117,6 +120,9 @@ const DEFAULT_CONFIG: Config = {
|
|||||||
telegramBotToken: "",
|
telegramBotToken: "",
|
||||||
telegramChatId: "",
|
telegramChatId: "",
|
||||||
clipboardEnabled: false,
|
clipboardEnabled: false,
|
||||||
|
antiSpamMaxHits: 15,
|
||||||
|
antiSpamWindowMs: 600000,
|
||||||
|
antiSpamCooldownMs: 600000,
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
sessionTtlHours: 168,
|
sessionTtlHours: 168,
|
||||||
@@ -438,6 +444,18 @@ export function loadConfig(): Config {
|
|||||||
String(process.env.OVERLORD_NOTIFICATION_CLIPBOARD_ENABLED || "").toLowerCase() === "true" ||
|
String(process.env.OVERLORD_NOTIFICATION_CLIPBOARD_ENABLED || "").toLowerCase() === "true" ||
|
||||||
fileConfig.notifications?.clipboardEnabled ||
|
fileConfig.notifications?.clipboardEnabled ||
|
||||||
DEFAULT_CONFIG.notifications.clipboardEnabled,
|
DEFAULT_CONFIG.notifications.clipboardEnabled,
|
||||||
|
antiSpamMaxHits:
|
||||||
|
Number(process.env.OVERLORD_NOTIFICATION_ANTISPAM_MAX_HITS) ||
|
||||||
|
fileConfig.notifications?.antiSpamMaxHits ||
|
||||||
|
DEFAULT_CONFIG.notifications.antiSpamMaxHits,
|
||||||
|
antiSpamWindowMs:
|
||||||
|
Number(process.env.OVERLORD_NOTIFICATION_ANTISPAM_WINDOW_MS) ||
|
||||||
|
fileConfig.notifications?.antiSpamWindowMs ||
|
||||||
|
DEFAULT_CONFIG.notifications.antiSpamWindowMs,
|
||||||
|
antiSpamCooldownMs:
|
||||||
|
Number(process.env.OVERLORD_NOTIFICATION_ANTISPAM_COOLDOWN_MS) ||
|
||||||
|
fileConfig.notifications?.antiSpamCooldownMs ||
|
||||||
|
DEFAULT_CONFIG.notifications.antiSpamCooldownMs,
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
sessionTtlHours:
|
sessionTtlHours:
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ export type ProcessInfo = {
|
|||||||
memory: number;
|
memory: number;
|
||||||
username?: string;
|
username?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
self?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProcessListResult = {
|
export type ProcessListResult = {
|
||||||
|
|||||||
@@ -170,6 +170,19 @@ export async function handleNotificationsConfigRoutes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const antiSpamMaxHits =
|
||||||
|
typeof body?.antiSpamMaxHits === "number"
|
||||||
|
? Math.max(1, Math.min(10000, Math.floor(body.antiSpamMaxHits)))
|
||||||
|
: currentConfig.antiSpamMaxHits;
|
||||||
|
const antiSpamWindowMs =
|
||||||
|
typeof body?.antiSpamWindowMs === "number"
|
||||||
|
? Math.max(5000, Math.min(86400000, Math.floor(body.antiSpamWindowMs)))
|
||||||
|
: currentConfig.antiSpamWindowMs;
|
||||||
|
const antiSpamCooldownMs =
|
||||||
|
typeof body?.antiSpamCooldownMs === "number"
|
||||||
|
? Math.max(5000, Math.min(86400000, Math.floor(body.antiSpamCooldownMs)))
|
||||||
|
: currentConfig.antiSpamCooldownMs;
|
||||||
|
|
||||||
const updated = await updateNotificationsConfig({
|
const updated = await updateNotificationsConfig({
|
||||||
keywords,
|
keywords,
|
||||||
webhookEnabled,
|
webhookEnabled,
|
||||||
@@ -178,6 +191,9 @@ export async function handleNotificationsConfigRoutes(
|
|||||||
telegramBotToken,
|
telegramBotToken,
|
||||||
telegramChatId,
|
telegramChatId,
|
||||||
clipboardEnabled,
|
clipboardEnabled,
|
||||||
|
antiSpamMaxHits,
|
||||||
|
antiSpamWindowMs,
|
||||||
|
antiSpamCooldownMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const client of clientManager.getAllClients().values()) {
|
for (const client of clientManager.getAllClients().values()) {
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ type NotificationRateState = {
|
|||||||
lastWarned: number;
|
lastWarned: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AntiSpamState = {
|
||||||
|
hits: number;
|
||||||
|
windowStart: number;
|
||||||
|
blockedUntil: number;
|
||||||
|
};
|
||||||
|
|
||||||
type PendingNotificationScreenshot = {
|
type PendingNotificationScreenshot = {
|
||||||
notificationId: string;
|
notificationId: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
@@ -45,6 +51,9 @@ type NotificationConfigShape = {
|
|||||||
spamWindowMs?: number;
|
spamWindowMs?: number;
|
||||||
spamWarnThreshold?: number;
|
spamWarnThreshold?: number;
|
||||||
historyLimit?: number;
|
historyLimit?: number;
|
||||||
|
antiSpamMaxHits?: number;
|
||||||
|
antiSpamWindowMs?: number;
|
||||||
|
antiSpamCooldownMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateDeps = {
|
type CreateDeps = {
|
||||||
@@ -88,6 +97,8 @@ function safeSendViewer(ws: ServerWebSocket<SocketData>, payload: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createNotificationPluginHandlers(deps: CreateDeps) {
|
export function createNotificationPluginHandlers(deps: CreateDeps) {
|
||||||
|
const antiSpamState = new Map<string, AntiSpamState>();
|
||||||
|
|
||||||
const pluginUIEventBuffer = new Map<string, Array<{ event: string; payload: any; ts: number }>>();
|
const pluginUIEventBuffer = new Map<string, Array<{ event: string; payload: any; ts: number }>>();
|
||||||
const PLUGIN_UI_EVENT_BUFFER_MAX = 200;
|
const PLUGIN_UI_EVENT_BUFFER_MAX = 200;
|
||||||
const PLUGIN_UI_EVENT_TTL_MS = 60_000;
|
const PLUGIN_UI_EVENT_TTL_MS = 60_000;
|
||||||
@@ -183,6 +194,43 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkAntiSpam(key: string, ts: number): boolean {
|
||||||
|
const notificationConfig = deps.getNotificationConfig();
|
||||||
|
const maxHits = Math.max(1, notificationConfig.antiSpamMaxHits || 15);
|
||||||
|
const windowMs = Math.max(5000, notificationConfig.antiSpamWindowMs || 600000);
|
||||||
|
const cooldownMs = Math.max(5000, notificationConfig.antiSpamCooldownMs || 600000);
|
||||||
|
|
||||||
|
const state = antiSpamState.get(key);
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
if (ts < state.blockedUntil) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts - state.windowStart > windowMs) {
|
||||||
|
state.windowStart = ts;
|
||||||
|
state.hits = 1;
|
||||||
|
state.blockedUntil = 0;
|
||||||
|
antiSpamState.set(key, state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.hits += 1;
|
||||||
|
if (state.hits > maxHits) {
|
||||||
|
state.blockedUntil = ts + cooldownMs;
|
||||||
|
logger.warn(`[notify] anti-spam: blocked keyword for ${key} until ${new Date(state.blockedUntil).toISOString()} (${state.hits} hits in ${windowMs}ms)`);
|
||||||
|
antiSpamState.set(key, state);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
antiSpamState.set(key, state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
antiSpamState.set(key, { hits: 1, windowStart: ts, blockedUntil: 0 });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function pruneNotificationRate() {
|
function pruneNotificationRate() {
|
||||||
const notificationConfig = deps.getNotificationConfig();
|
const notificationConfig = deps.getNotificationConfig();
|
||||||
const spamWindow = Math.max(5000, notificationConfig.spamWindowMs || 60000);
|
const spamWindow = Math.max(5000, notificationConfig.spamWindowMs || 60000);
|
||||||
@@ -193,6 +241,15 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
|||||||
deps.notificationRate.delete(key);
|
deps.notificationRate.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const antiSpamWindowMs = Math.max(5000, notificationConfig.antiSpamWindowMs || 600000);
|
||||||
|
const antiSpamCooldownMs = Math.max(5000, notificationConfig.antiSpamCooldownMs || 600000);
|
||||||
|
const antiSpamMaxAge = Math.max(antiSpamWindowMs, antiSpamCooldownMs) * 2;
|
||||||
|
for (const [key, state] of antiSpamState) {
|
||||||
|
if (now - state.windowStart > antiSpamMaxAge && now > state.blockedUntil) {
|
||||||
|
antiSpamState.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(pruneNotificationRate, 60_000);
|
setInterval(pruneNotificationRate, 60_000);
|
||||||
@@ -264,6 +321,10 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
|||||||
if (!shouldAcceptNotification(rateKey, ts)) {
|
if (!shouldAcceptNotification(rateKey, ts)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const antiSpamKey = `${clientId}:${keyword || title}`;
|
||||||
|
if (!checkAntiSpam(antiSpamKey, ts)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const info = clientManager.getClient(clientId);
|
const info = clientManager.getClient(clientId);
|
||||||
const category: "active_window" | "clipboard" =
|
const category: "active_window" | "clipboard" =
|
||||||
payload.category === "clipboard" ? "clipboard" : "active_window";
|
payload.category === "clipboard" ? "clipboard" : "active_window";
|
||||||
@@ -368,6 +429,11 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
|||||||
deps.notificationRate.delete(key);
|
deps.notificationRate.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const key of Array.from(antiSpamState.keys())) {
|
||||||
|
if (key === clientId || key.startsWith(`${clientId}:`)) {
|
||||||
|
antiSpamState.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
const item = { type: "notifications_cleared", clientId, ts: Date.now() };
|
const item = { type: "notifications_cleared", clientId, ts: Date.now() };
|
||||||
for (const session of sessionManager.getAllNotificationSessions().values()) {
|
for (const session of sessionManager.getAllNotificationSessions().values()) {
|
||||||
const sRole = session.userRole ?? session.viewer.data.userRole ?? "";
|
const sRole = session.userRole ?? session.viewer.data.userRole ?? "";
|
||||||
|
|||||||
Reference in New Issue
Block a user