import fs from 'fs'; import { SENDER_ALLOWLIST_PATH } from './config.js'; import { logger } from './logger.js'; export interface ChatAllowlistEntry { allow: '*' | string[]; mode: 'trigger' | 'drop'; } export interface SenderAllowlistConfig { default: ChatAllowlistEntry; chats: Record; logDenied: boolean; } const DEFAULT_CONFIG: SenderAllowlistConfig = { default: { allow: '*', mode: 'trigger' }, chats: {}, logDenied: true, }; function isValidEntry(entry: unknown): entry is ChatAllowlistEntry { if (!entry || typeof entry !== 'object') return false; const e = entry as Record; const validAllow = e.allow === '*' || (Array.isArray(e.allow) && e.allow.every((v) => typeof v === 'string')); const validMode = e.mode === 'trigger' || e.mode === 'drop'; return validAllow && validMode; } export function loadSenderAllowlist( pathOverride?: string, ): SenderAllowlistConfig { const filePath = pathOverride ?? SENDER_ALLOWLIST_PATH; let raw: string; try { raw = fs.readFileSync(filePath, 'utf-8'); } catch (err: unknown) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') return DEFAULT_CONFIG; logger.warn( { err, path: filePath }, 'sender-allowlist: cannot read config', ); return DEFAULT_CONFIG; } let parsed: unknown; try { parsed = JSON.parse(raw); } catch { logger.warn({ path: filePath }, 'sender-allowlist: invalid JSON'); return DEFAULT_CONFIG; } const obj = parsed as Record; if (!isValidEntry(obj.default)) { logger.warn( { path: filePath }, 'sender-allowlist: invalid or missing default entry', ); return DEFAULT_CONFIG; } const chats: Record = {}; if (obj.chats && typeof obj.chats === 'object') { for (const [jid, entry] of Object.entries( obj.chats as Record, )) { if (isValidEntry(entry)) { chats[jid] = entry; } else { logger.warn( { jid, path: filePath }, 'sender-allowlist: skipping invalid chat entry', ); } } } return { default: obj.default as ChatAllowlistEntry, chats, logDenied: obj.logDenied !== false, }; } function getEntry( chatJid: string, cfg: SenderAllowlistConfig, ): ChatAllowlistEntry { return cfg.chats[chatJid] ?? cfg.default; } export function isSenderAllowed( chatJid: string, sender: string, cfg: SenderAllowlistConfig, ): boolean { const entry = getEntry(chatJid, cfg); if (entry.allow === '*') return true; return entry.allow.includes(sender); } export function shouldDropMessage( chatJid: string, cfg: SenderAllowlistConfig, ): boolean { return getEntry(chatJid, cfg).mode === 'drop'; } export function isTriggerAllowed( chatJid: string, sender: string, cfg: SenderAllowlistConfig, ): boolean { const allowed = isSenderAllowed(chatJid, sender, cfg); if (!allowed && cfg.logDenied) { logger.debug( { chatJid, sender }, 'sender-allowlist: trigger denied for sender', ); } return allowed; }