add sender allowlist for per-chat access control (#705)
* feat: add sender allowlist for per-chat access control * style: fix prettier formatting
This commit is contained in:
@@ -188,6 +188,37 @@ Groups can have extra directories mounted. Add `containerConfig` to their entry:
|
|||||||
|
|
||||||
The directory will appear at `/workspace/extra/webapp` in that group's container.
|
The directory will appear at `/workspace/extra/webapp` in that group's container.
|
||||||
|
|
||||||
|
#### Sender Allowlist
|
||||||
|
|
||||||
|
After registering a group, explain the sender allowlist feature to the user:
|
||||||
|
|
||||||
|
> This group can be configured with a sender allowlist to control who can interact with me. There are two modes:
|
||||||
|
>
|
||||||
|
> - **Trigger mode** (default): Everyone's messages are stored for context, but only allowed senders can trigger me with @{AssistantName}.
|
||||||
|
> - **Drop mode**: Messages from non-allowed senders are not stored at all.
|
||||||
|
>
|
||||||
|
> For closed groups with trusted members, I recommend setting up an allow-only list so only specific people can trigger me. Want me to configure that?
|
||||||
|
|
||||||
|
If the user wants to set up an allowlist, edit `~/.config/nanoclaw/sender-allowlist.json` on the host:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"default": { "allow": "*", "mode": "trigger" },
|
||||||
|
"chats": {
|
||||||
|
"<chat-jid>": {
|
||||||
|
"allow": ["sender-id-1", "sender-id-2"],
|
||||||
|
"mode": "trigger"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logDenied": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Your own messages (`is_from_me`) explicitly bypass the allowlist in trigger checks. Bot messages are filtered out by the database query before trigger evaluation, so they never reach the allowlist.
|
||||||
|
- If the config file doesn't exist or is invalid, all senders are allowed (fail-open)
|
||||||
|
- The config file is on the host at `~/.config/nanoclaw/sender-allowlist.json`, not inside the container
|
||||||
|
|
||||||
### Removing a Group
|
### Removing a Group
|
||||||
|
|
||||||
1. Read `/workspace/project/data/registered_groups.json`
|
1. Read `/workspace/project/data/registered_groups.json`
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export const MOUNT_ALLOWLIST_PATH = path.join(
|
|||||||
'nanoclaw',
|
'nanoclaw',
|
||||||
'mount-allowlist.json',
|
'mount-allowlist.json',
|
||||||
);
|
);
|
||||||
|
export const SENDER_ALLOWLIST_PATH = path.join(
|
||||||
|
HOME_DIR,
|
||||||
|
'.config',
|
||||||
|
'nanoclaw',
|
||||||
|
'sender-allowlist.json',
|
||||||
|
);
|
||||||
export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store');
|
export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store');
|
||||||
export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups');
|
export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups');
|
||||||
export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data');
|
export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data');
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ export function getNewMessages(
|
|||||||
// Filter bot messages using both the is_bot_message flag AND the content
|
// Filter bot messages using both the is_bot_message flag AND the content
|
||||||
// prefix as a backstop for messages written before the migration ran.
|
// prefix as a backstop for messages written before the migration ran.
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT id, chat_jid, sender, sender_name, content, timestamp
|
SELECT id, chat_jid, sender, sender_name, content, timestamp, is_from_me
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE timestamp > ? AND chat_jid IN (${placeholders})
|
WHERE timestamp > ? AND chat_jid IN (${placeholders})
|
||||||
AND is_bot_message = 0 AND content NOT LIKE ?
|
AND is_bot_message = 0 AND content NOT LIKE ?
|
||||||
@@ -341,7 +341,7 @@ export function getMessagesSince(
|
|||||||
// Filter bot messages using both the is_bot_message flag AND the content
|
// Filter bot messages using both the is_bot_message flag AND the content
|
||||||
// prefix as a backstop for messages written before the migration ran.
|
// prefix as a backstop for messages written before the migration ran.
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT id, chat_jid, sender, sender_name, content, timestamp
|
SELECT id, chat_jid, sender, sender_name, content, timestamp, is_from_me
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE chat_jid = ? AND timestamp > ?
|
WHERE chat_jid = ? AND timestamp > ?
|
||||||
AND is_bot_message = 0 AND content NOT LIKE ?
|
AND is_bot_message = 0 AND content NOT LIKE ?
|
||||||
|
|||||||
45
src/index.ts
45
src/index.ts
@@ -41,6 +41,12 @@ import { GroupQueue } from './group-queue.js';
|
|||||||
import { resolveGroupFolderPath } from './group-folder.js';
|
import { resolveGroupFolderPath } from './group-folder.js';
|
||||||
import { startIpcWatcher } from './ipc.js';
|
import { startIpcWatcher } from './ipc.js';
|
||||||
import { findChannel, formatMessages, formatOutbound } from './router.js';
|
import { findChannel, formatMessages, formatOutbound } from './router.js';
|
||||||
|
import {
|
||||||
|
isSenderAllowed,
|
||||||
|
isTriggerAllowed,
|
||||||
|
loadSenderAllowlist,
|
||||||
|
shouldDropMessage,
|
||||||
|
} from './sender-allowlist.js';
|
||||||
import { startSchedulerLoop } from './task-scheduler.js';
|
import { startSchedulerLoop } from './task-scheduler.js';
|
||||||
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
@@ -155,8 +161,11 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
|||||||
|
|
||||||
// For non-main groups, check if trigger is required and present
|
// For non-main groups, check if trigger is required and present
|
||||||
if (!isMainGroup && group.requiresTrigger !== false) {
|
if (!isMainGroup && group.requiresTrigger !== false) {
|
||||||
const hasTrigger = missedMessages.some((m) =>
|
const allowlistCfg = loadSenderAllowlist();
|
||||||
TRIGGER_PATTERN.test(m.content.trim()),
|
const hasTrigger = missedMessages.some(
|
||||||
|
(m) =>
|
||||||
|
TRIGGER_PATTERN.test(m.content.trim()) &&
|
||||||
|
(m.is_from_me || isTriggerAllowed(chatJid, m.sender, allowlistCfg)),
|
||||||
);
|
);
|
||||||
if (!hasTrigger) return true;
|
if (!hasTrigger) return true;
|
||||||
}
|
}
|
||||||
@@ -380,8 +389,12 @@ async function startMessageLoop(): Promise<void> {
|
|||||||
// Non-trigger messages accumulate in DB and get pulled as
|
// Non-trigger messages accumulate in DB and get pulled as
|
||||||
// context when a trigger eventually arrives.
|
// context when a trigger eventually arrives.
|
||||||
if (needsTrigger) {
|
if (needsTrigger) {
|
||||||
const hasTrigger = groupMessages.some((m) =>
|
const allowlistCfg = loadSenderAllowlist();
|
||||||
TRIGGER_PATTERN.test(m.content.trim()),
|
const hasTrigger = groupMessages.some(
|
||||||
|
(m) =>
|
||||||
|
TRIGGER_PATTERN.test(m.content.trim()) &&
|
||||||
|
(m.is_from_me ||
|
||||||
|
isTriggerAllowed(chatJid, m.sender, allowlistCfg)),
|
||||||
);
|
);
|
||||||
if (!hasTrigger) continue;
|
if (!hasTrigger) continue;
|
||||||
}
|
}
|
||||||
@@ -465,7 +478,29 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
// Channel callbacks (shared by all channels)
|
// Channel callbacks (shared by all channels)
|
||||||
const channelOpts = {
|
const channelOpts = {
|
||||||
onMessage: (_chatJid: string, msg: NewMessage) => storeMessage(msg),
|
onMessage: (_chatJid: string, msg: NewMessage) => {
|
||||||
|
// Sender allowlist drop mode: discard messages from denied senders before storing
|
||||||
|
if (
|
||||||
|
!msg.is_from_me &&
|
||||||
|
!msg.is_bot_message &&
|
||||||
|
registeredGroups[_chatJid]
|
||||||
|
) {
|
||||||
|
const cfg = loadSenderAllowlist();
|
||||||
|
if (
|
||||||
|
shouldDropMessage(_chatJid, cfg) &&
|
||||||
|
!isSenderAllowed(_chatJid, msg.sender, cfg)
|
||||||
|
) {
|
||||||
|
if (cfg.logDenied) {
|
||||||
|
logger.debug(
|
||||||
|
{ chatJid: _chatJid, sender: msg.sender },
|
||||||
|
'sender-allowlist: dropping message (drop mode)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storeMessage(msg);
|
||||||
|
},
|
||||||
onChatMetadata: (
|
onChatMetadata: (
|
||||||
chatJid: string,
|
chatJid: string,
|
||||||
timestamp: string,
|
timestamp: string,
|
||||||
|
|||||||
216
src/sender-allowlist.test.ts
Normal file
216
src/sender-allowlist.test.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
isSenderAllowed,
|
||||||
|
isTriggerAllowed,
|
||||||
|
loadSenderAllowlist,
|
||||||
|
SenderAllowlistConfig,
|
||||||
|
shouldDropMessage,
|
||||||
|
} from './sender-allowlist.js';
|
||||||
|
|
||||||
|
let tmpDir: string;
|
||||||
|
|
||||||
|
function cfgPath(name = 'sender-allowlist.json'): string {
|
||||||
|
return path.join(tmpDir, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeConfig(config: unknown, name?: string): string {
|
||||||
|
const p = cfgPath(name);
|
||||||
|
fs.writeFileSync(p, JSON.stringify(config));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'allowlist-test-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadSenderAllowlist', () => {
|
||||||
|
it('returns allow-all defaults when file is missing', () => {
|
||||||
|
const cfg = loadSenderAllowlist(cfgPath());
|
||||||
|
expect(cfg.default.allow).toBe('*');
|
||||||
|
expect(cfg.default.mode).toBe('trigger');
|
||||||
|
expect(cfg.logDenied).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads allow=* config', () => {
|
||||||
|
const p = writeConfig({
|
||||||
|
default: { allow: '*', mode: 'trigger' },
|
||||||
|
chats: {},
|
||||||
|
logDenied: false,
|
||||||
|
});
|
||||||
|
const cfg = loadSenderAllowlist(p);
|
||||||
|
expect(cfg.default.allow).toBe('*');
|
||||||
|
expect(cfg.logDenied).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads allow=[] (deny all)', () => {
|
||||||
|
const p = writeConfig({
|
||||||
|
default: { allow: [], mode: 'trigger' },
|
||||||
|
chats: {},
|
||||||
|
});
|
||||||
|
const cfg = loadSenderAllowlist(p);
|
||||||
|
expect(cfg.default.allow).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads allow=[list]', () => {
|
||||||
|
const p = writeConfig({
|
||||||
|
default: { allow: ['alice', 'bob'], mode: 'drop' },
|
||||||
|
chats: {},
|
||||||
|
});
|
||||||
|
const cfg = loadSenderAllowlist(p);
|
||||||
|
expect(cfg.default.allow).toEqual(['alice', 'bob']);
|
||||||
|
expect(cfg.default.mode).toBe('drop');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('per-chat override beats default', () => {
|
||||||
|
const p = writeConfig({
|
||||||
|
default: { allow: '*', mode: 'trigger' },
|
||||||
|
chats: { 'group-a': { allow: ['alice'], mode: 'drop' } },
|
||||||
|
});
|
||||||
|
const cfg = loadSenderAllowlist(p);
|
||||||
|
expect(cfg.chats['group-a'].allow).toEqual(['alice']);
|
||||||
|
expect(cfg.chats['group-a'].mode).toBe('drop');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns allow-all on invalid JSON', () => {
|
||||||
|
const p = cfgPath();
|
||||||
|
fs.writeFileSync(p, '{ not valid json }}}');
|
||||||
|
const cfg = loadSenderAllowlist(p);
|
||||||
|
expect(cfg.default.allow).toBe('*');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns allow-all on invalid schema', () => {
|
||||||
|
const p = writeConfig({ default: { oops: true } });
|
||||||
|
const cfg = loadSenderAllowlist(p);
|
||||||
|
expect(cfg.default.allow).toBe('*');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects non-string allow array items', () => {
|
||||||
|
const p = writeConfig({
|
||||||
|
default: { allow: [123, null, true], mode: 'trigger' },
|
||||||
|
chats: {},
|
||||||
|
});
|
||||||
|
const cfg = loadSenderAllowlist(p);
|
||||||
|
expect(cfg.default.allow).toBe('*'); // falls back to default
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips invalid per-chat entries', () => {
|
||||||
|
const p = writeConfig({
|
||||||
|
default: { allow: '*', mode: 'trigger' },
|
||||||
|
chats: {
|
||||||
|
good: { allow: ['alice'], mode: 'trigger' },
|
||||||
|
bad: { allow: 123 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const cfg = loadSenderAllowlist(p);
|
||||||
|
expect(cfg.chats['good']).toBeDefined();
|
||||||
|
expect(cfg.chats['bad']).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSenderAllowed', () => {
|
||||||
|
it('allow=* allows any sender', () => {
|
||||||
|
const cfg: SenderAllowlistConfig = {
|
||||||
|
default: { allow: '*', mode: 'trigger' },
|
||||||
|
chats: {},
|
||||||
|
logDenied: true,
|
||||||
|
};
|
||||||
|
expect(isSenderAllowed('g1', 'anyone', cfg)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow=[] denies any sender', () => {
|
||||||
|
const cfg: SenderAllowlistConfig = {
|
||||||
|
default: { allow: [], mode: 'trigger' },
|
||||||
|
chats: {},
|
||||||
|
logDenied: true,
|
||||||
|
};
|
||||||
|
expect(isSenderAllowed('g1', 'anyone', cfg)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow=[list] allows exact match only', () => {
|
||||||
|
const cfg: SenderAllowlistConfig = {
|
||||||
|
default: { allow: ['alice', 'bob'], mode: 'trigger' },
|
||||||
|
chats: {},
|
||||||
|
logDenied: true,
|
||||||
|
};
|
||||||
|
expect(isSenderAllowed('g1', 'alice', cfg)).toBe(true);
|
||||||
|
expect(isSenderAllowed('g1', 'eve', cfg)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses per-chat entry over default', () => {
|
||||||
|
const cfg: SenderAllowlistConfig = {
|
||||||
|
default: { allow: '*', mode: 'trigger' },
|
||||||
|
chats: { g1: { allow: ['alice'], mode: 'trigger' } },
|
||||||
|
logDenied: true,
|
||||||
|
};
|
||||||
|
expect(isSenderAllowed('g1', 'bob', cfg)).toBe(false);
|
||||||
|
expect(isSenderAllowed('g2', 'bob', cfg)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldDropMessage', () => {
|
||||||
|
it('returns false for trigger mode', () => {
|
||||||
|
const cfg: SenderAllowlistConfig = {
|
||||||
|
default: { allow: '*', mode: 'trigger' },
|
||||||
|
chats: {},
|
||||||
|
logDenied: true,
|
||||||
|
};
|
||||||
|
expect(shouldDropMessage('g1', cfg)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for drop mode', () => {
|
||||||
|
const cfg: SenderAllowlistConfig = {
|
||||||
|
default: { allow: '*', mode: 'drop' },
|
||||||
|
chats: {},
|
||||||
|
logDenied: true,
|
||||||
|
};
|
||||||
|
expect(shouldDropMessage('g1', cfg)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('per-chat mode override', () => {
|
||||||
|
const cfg: SenderAllowlistConfig = {
|
||||||
|
default: { allow: '*', mode: 'trigger' },
|
||||||
|
chats: { g1: { allow: '*', mode: 'drop' } },
|
||||||
|
logDenied: true,
|
||||||
|
};
|
||||||
|
expect(shouldDropMessage('g1', cfg)).toBe(true);
|
||||||
|
expect(shouldDropMessage('g2', cfg)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isTriggerAllowed', () => {
|
||||||
|
it('allows trigger for allowed sender', () => {
|
||||||
|
const cfg: SenderAllowlistConfig = {
|
||||||
|
default: { allow: ['alice'], mode: 'trigger' },
|
||||||
|
chats: {},
|
||||||
|
logDenied: false,
|
||||||
|
};
|
||||||
|
expect(isTriggerAllowed('g1', 'alice', cfg)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies trigger for disallowed sender', () => {
|
||||||
|
const cfg: SenderAllowlistConfig = {
|
||||||
|
default: { allow: ['alice'], mode: 'trigger' },
|
||||||
|
chats: {},
|
||||||
|
logDenied: false,
|
||||||
|
};
|
||||||
|
expect(isTriggerAllowed('g1', 'eve', cfg)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs when logDenied is true', () => {
|
||||||
|
const cfg: SenderAllowlistConfig = {
|
||||||
|
default: { allow: ['alice'], mode: 'trigger' },
|
||||||
|
chats: {},
|
||||||
|
logDenied: true,
|
||||||
|
};
|
||||||
|
isTriggerAllowed('g1', 'eve', cfg);
|
||||||
|
// Logger.debug is called — we just verify no crash; logger is a real pino instance
|
||||||
|
});
|
||||||
|
});
|
||||||
128
src/sender-allowlist.ts
Normal file
128
src/sender-allowlist.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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<string, ChatAllowlistEntry>;
|
||||||
|
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<string, unknown>;
|
||||||
|
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<string, unknown>;
|
||||||
|
|
||||||
|
if (!isValidEntry(obj.default)) {
|
||||||
|
logger.warn(
|
||||||
|
{ path: filePath },
|
||||||
|
'sender-allowlist: invalid or missing default entry',
|
||||||
|
);
|
||||||
|
return DEFAULT_CONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chats: Record<string, ChatAllowlistEntry> = {};
|
||||||
|
if (obj.chats && typeof obj.chats === 'object') {
|
||||||
|
for (const [jid, entry] of Object.entries(
|
||||||
|
obj.chats as Record<string, unknown>,
|
||||||
|
)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user