feat: add channel match metadata logs
Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
@@ -175,6 +175,7 @@ Notes:
|
||||
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages.
|
||||
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||
- If `channels` is present, any channel not listed is denied by default.
|
||||
- Threads inherit parent channel config (allowlist, `requireMention`, skills, prompts, etc.) unless you add the thread id explicitly.
|
||||
- Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered).
|
||||
- Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
|
||||
|
||||
|
||||
@@ -216,6 +216,7 @@ Telegram forum topics include a `message_thread_id` per message. Clawdbot:
|
||||
- General topic (thread id `1`) is special: message sends omit `message_thread_id` (Telegram rejects it), but typing indicators still include it.
|
||||
- Exposes `MessageThreadId` + `IsForum` in template context for routing/templating.
|
||||
- Topic-specific configuration is available under `channels.telegram.groups.<chatId>.topics.<threadId>` (skills, allowlists, auto-reply, system prompts, disable).
|
||||
- Topic configs inherit group settings (requireMention, allowlists, skills, prompts, enabled) unless overridden per topic.
|
||||
|
||||
Private chats can include `message_thread_id` in some edge cases. Clawdbot keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present.
|
||||
|
||||
|
||||
@@ -89,6 +89,18 @@ describe("msteams policy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("inherits team mention settings when channel config is missing", () => {
|
||||
const policy = resolveMSTeamsReplyPolicy({
|
||||
isDirectMessage: false,
|
||||
globalConfig: { requireMention: true },
|
||||
teamConfig: { requireMention: false },
|
||||
});
|
||||
expect(policy).toEqual({
|
||||
requireMention: false,
|
||||
replyStyle: "top-level",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses explicit replyStyle even when requireMention defaults would differ", () => {
|
||||
const policy = resolveMSTeamsReplyPolicy({
|
||||
isDirectMessage: false,
|
||||
|
||||
2
src/channels/plugins/channel-config.ts
Normal file
2
src/channels/plugins/channel-config.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type { ChannelEntryMatch } from "../channel-config.js";
|
||||
export { buildChannelKeyCandidates, resolveChannelEntryMatch } from "../channel-config.js";
|
||||
@@ -84,4 +84,9 @@ export {
|
||||
listWhatsAppDirectoryGroupsFromConfig,
|
||||
listWhatsAppDirectoryPeersFromConfig,
|
||||
} from "./directory-config.js";
|
||||
export {
|
||||
buildChannelKeyCandidates,
|
||||
resolveChannelEntryMatch,
|
||||
type ChannelEntryMatch,
|
||||
} from "./channel-config.js";
|
||||
export type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
|
||||
@@ -249,6 +249,34 @@ describe("discord mention gating", () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("inherits parent channel mention rules for threads", () => {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
"parent-1": { allow: true, requireMention: false },
|
||||
},
|
||||
};
|
||||
const channelConfig = resolveDiscordChannelConfigWithFallback({
|
||||
guildInfo,
|
||||
channelId: "thread-1",
|
||||
channelName: "topic",
|
||||
channelSlug: "topic",
|
||||
parentId: "parent-1",
|
||||
parentName: "Parent",
|
||||
parentSlug: "parent",
|
||||
scope: "thread",
|
||||
});
|
||||
expect(channelConfig?.matchSource).toBe("parent");
|
||||
expect(
|
||||
resolveDiscordShouldRequireMention({
|
||||
isGuildMessage: true,
|
||||
isThread: true,
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("discord groupPolicy gating", () => {
|
||||
|
||||
@@ -252,8 +252,11 @@ export async function preflightDiscordMessage(
|
||||
scope: threadChannel ? "thread" : "channel",
|
||||
})
|
||||
: null;
|
||||
const channelMatchMeta = `matchKey=${channelConfig?.matchKey ?? "none"} matchSource=${
|
||||
channelConfig?.matchSource ?? "none"
|
||||
}`;
|
||||
if (isGuildMessage && channelConfig?.enabled === false) {
|
||||
logVerbose(`Blocked discord channel ${message.channelId} (channel disabled)`);
|
||||
logVerbose(`Blocked discord channel ${message.channelId} (channel disabled, ${channelMatchMeta})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -280,21 +283,28 @@ export async function preflightDiscordMessage(
|
||||
})
|
||||
) {
|
||||
if (params.groupPolicy === "disabled") {
|
||||
logVerbose("discord: drop guild message (groupPolicy: disabled)");
|
||||
logVerbose(`discord: drop guild message (groupPolicy: disabled, ${channelMatchMeta})`);
|
||||
} else if (!channelAllowlistConfigured) {
|
||||
logVerbose("discord: drop guild message (groupPolicy: allowlist, no channel allowlist)");
|
||||
logVerbose(
|
||||
`discord: drop guild message (groupPolicy: allowlist, no channel allowlist, ${channelMatchMeta})`,
|
||||
);
|
||||
} else {
|
||||
logVerbose(
|
||||
`Blocked discord channel ${message.channelId} not in guild channel allowlist (groupPolicy: allowlist)`,
|
||||
`Blocked discord channel ${message.channelId} not in guild channel allowlist (groupPolicy: allowlist, ${channelMatchMeta})`,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isGuildMessage && channelConfig?.allowed === false) {
|
||||
logVerbose(`Blocked discord channel ${message.channelId} not in guild channel allowlist`);
|
||||
logVerbose(
|
||||
`Blocked discord channel ${message.channelId} not in guild channel allowlist (${channelMatchMeta})`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (isGuildMessage) {
|
||||
logVerbose(`discord: allow channel ${message.channelId} (${channelMatchMeta})`);
|
||||
}
|
||||
|
||||
const textForHistory = resolveDiscordMessageText(message, {
|
||||
includeForwarded: true,
|
||||
|
||||
@@ -28,4 +28,18 @@ describe("resolveSlackChannelConfig", () => {
|
||||
});
|
||||
expect(res).toMatchObject({ requireMention: true });
|
||||
});
|
||||
|
||||
it("uses wildcard entries when no direct channel config exists", () => {
|
||||
const res = resolveSlackChannelConfig({
|
||||
channelId: "C1",
|
||||
channels: { "*": { allow: true, requireMention: false } },
|
||||
defaultRequireMention: true,
|
||||
});
|
||||
expect(res).toMatchObject({
|
||||
allowed: true,
|
||||
requireMention: false,
|
||||
matchKey: "*",
|
||||
matchSource: "wildcard",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -310,6 +310,9 @@ export function createSlackMonitorContext(params: {
|
||||
channels: params.channelsConfig,
|
||||
defaultRequireMention,
|
||||
});
|
||||
const channelMatchMeta = `matchKey=${channelConfig?.matchKey ?? "none"} matchSource=${
|
||||
channelConfig?.matchSource ?? "none"
|
||||
}`;
|
||||
const channelAllowed = channelConfig?.allowed !== false;
|
||||
const channelAllowlistConfigured =
|
||||
Boolean(params.channelsConfig) && Object.keys(params.channelsConfig ?? {}).length > 0;
|
||||
@@ -320,9 +323,16 @@ export function createSlackMonitorContext(params: {
|
||||
channelAllowed,
|
||||
})
|
||||
) {
|
||||
logVerbose(
|
||||
`slack: drop channel ${p.channelId} (groupPolicy=${params.groupPolicy}, ${channelMatchMeta})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!channelAllowed) return false;
|
||||
if (!channelAllowed) {
|
||||
logVerbose(`slack: drop channel ${p.channelId} (${channelMatchMeta})`);
|
||||
return false;
|
||||
}
|
||||
logVerbose(`slack: allow channel ${p.channelId} (${channelMatchMeta})`);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1159,6 +1159,50 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("inherits group allowlist + requireMention in topics", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
telegram: {
|
||||
groupPolicy: "allowlist",
|
||||
groups: {
|
||||
"-1001234567890": {
|
||||
requireMention: false,
|
||||
allowFrom: ["123456789"],
|
||||
topics: {
|
||||
"99": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: {
|
||||
id: -1001234567890,
|
||||
type: "supergroup",
|
||||
title: "Forum Group",
|
||||
is_forum: true,
|
||||
},
|
||||
from: { id: 123456789, username: "testuser" },
|
||||
text: "hello",
|
||||
date: 1736380800,
|
||||
message_thread_id: 99,
|
||||
},
|
||||
me: { username: "clawdbot_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("honors groups default when no explicit group override exists", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
Reference in New Issue
Block a user