feat(whatsapp,telegram): add groupPolicy config option (#216)

Co-authored-by: Marcus Neves <conhecendo.contato@gmail.com>
Co-authored-by: Shadow <hi@shadowing.dev>
This commit is contained in:
Marcus Neves
2026-01-06 01:41:19 -03:00
committed by GitHub
parent f6d9d3ce67
commit 9ab0b88ac6
10 changed files with 917 additions and 21 deletions

View File

@@ -643,4 +643,494 @@ describe("createTelegramBot", () => {
});
expect(sendPhotoSpy).not.toHaveBeenCalled();
});
// groupPolicy tests
it("blocks all group messages when groupPolicy is 'disabled'", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "disabled",
allowFrom: ["123456789"],
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 123456789, username: "testuser" },
text: "@clawdbot_bot hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
// Should NOT call getReplyFromConfig because groupPolicy is disabled
expect(replySpy).not.toHaveBeenCalled();
});
it("blocks group messages from senders not in allowFrom when groupPolicy is 'allowlist'", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "allowlist",
allowFrom: ["123456789"], // Does not include sender 999999
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 999999, username: "notallowed" }, // Not in allowFrom
text: "@clawdbot_bot hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).not.toHaveBeenCalled();
});
it("allows group messages from senders in allowFrom (by ID) when groupPolicy is 'allowlist'", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "allowlist",
allowFrom: ["123456789"],
groups: { "*": { requireMention: false } }, // Skip mention check
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 123456789, username: "testuser" }, // In allowFrom
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("allows group messages from senders in allowFrom (by username) when groupPolicy is 'allowlist'", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "allowlist",
allowFrom: ["@testuser"], // By username
groups: { "*": { requireMention: false } },
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 12345, username: "testuser" }, // Username matches @testuser
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("allows group messages from telegram:-prefixed allowFrom entries when groupPolicy is 'allowlist'", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "allowlist",
allowFrom: ["telegram:77112533"],
groups: { "*": { requireMention: false } },
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 77112533, username: "mneves" },
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("allows group messages from tg:-prefixed allowFrom entries case-insensitively when groupPolicy is 'allowlist'", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "allowlist",
allowFrom: ["TG:77112533"],
groups: { "*": { requireMention: false } },
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 77112533, username: "mneves" },
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("allows all group messages when groupPolicy is 'open' (default)", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
// groupPolicy not set, should default to "open"
groups: { "*": { requireMention: false } },
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 999999, username: "random" }, // Random sender
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("matches usernames case-insensitively when groupPolicy is 'allowlist'", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "allowlist",
allowFrom: ["@TestUser"], // Uppercase in config
groups: { "*": { requireMention: false } },
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 12345, username: "testuser" }, // Lowercase in message
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("allows direct messages regardless of groupPolicy", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "disabled", // Even with disabled, DMs should work
allowFrom: ["123456789"],
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: 123456789, type: "private" }, // Direct message
from: { id: 123456789, username: "testuser" },
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("allows direct messages with tg/Telegram-prefixed allowFrom entries", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
allowFrom: [" TG:123456789 "],
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: 123456789, type: "private" }, // Direct message
from: { id: 123456789, username: "testuser" },
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("allows direct messages with telegram:-prefixed allowFrom entries", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
allowFrom: ["telegram:123456789"],
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: 123456789, type: "private" },
from: { id: 123456789, username: "testuser" },
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("allows group messages with wildcard in allowFrom when groupPolicy is 'allowlist'", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "allowlist",
allowFrom: ["*"], // Wildcard allows everyone
groups: { "*": { requireMention: false } },
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 999999, username: "random" }, // Random sender, but wildcard allows
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("blocks group messages with no sender ID when groupPolicy is 'allowlist'", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "allowlist",
allowFrom: ["123456789"],
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
// No `from` field (e.g., channel post or anonymous admin)
text: "hello",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).not.toHaveBeenCalled();
});
it("matches telegram:-prefixed allowFrom entries in group allowlist", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "allowlist",
allowFrom: ["telegram:123456789"], // Prefixed format
groups: { "*": { requireMention: false } },
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 123456789, username: "testuser" }, // Matches after stripping prefix
text: "hello from prefixed user",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
// Should call reply because sender ID matches after stripping telegram: prefix
expect(replySpy).toHaveBeenCalled();
});
it("matches tg:-prefixed allowFrom entries case-insensitively in group allowlist", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groupPolicy: "allowlist",
allowFrom: ["TG:123456789"], // Prefixed format (case-insensitive)
groups: { "*": { requireMention: false } },
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: -100123456789, type: "group", title: "Test Group" },
from: { id: 123456789, username: "testuser" }, // Matches after stripping tg: prefix
text: "hello from prefixed user",
date: 1736380800,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
// Should call reply because sender ID matches after stripping tg: prefix
expect(replySpy).toHaveBeenCalled();
});
});