feat: add channel/topic overrides for skills + auto-reply
This commit is contained in:
@@ -26,6 +26,56 @@ function normalizeSlackSlug(raw?: string | null) {
|
|||||||
return cleaned.replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
return cleaned.replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseTelegramGroupId(value?: string | null) {
|
||||||
|
const raw = value?.trim() ?? "";
|
||||||
|
if (!raw) return { chatId: undefined, topicId: undefined };
|
||||||
|
const parts = raw.split(":").filter(Boolean);
|
||||||
|
if (
|
||||||
|
parts.length >= 3 &&
|
||||||
|
parts[1] === "topic" &&
|
||||||
|
/^-?\d+$/.test(parts[0]) &&
|
||||||
|
/^\d+$/.test(parts[2])
|
||||||
|
) {
|
||||||
|
return { chatId: parts[0], topicId: parts[2] };
|
||||||
|
}
|
||||||
|
if (parts.length >= 2 && /^-?\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
|
||||||
|
return { chatId: parts[0], topicId: parts[1] };
|
||||||
|
}
|
||||||
|
return { chatId: raw, topicId: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOwn(obj: unknown, key: string): boolean {
|
||||||
|
return Boolean(obj && typeof obj === "object" && Object.hasOwn(obj, key));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTelegramAutoReply(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
chatId?: string;
|
||||||
|
topicId?: string;
|
||||||
|
}): boolean | undefined {
|
||||||
|
const { cfg, chatId, topicId } = params;
|
||||||
|
if (!chatId) return undefined;
|
||||||
|
const groupConfig = cfg.telegram?.groups?.[chatId];
|
||||||
|
const groupDefault = cfg.telegram?.groups?.["*"];
|
||||||
|
const topicConfig =
|
||||||
|
topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined;
|
||||||
|
const defaultTopicConfig =
|
||||||
|
topicId && groupDefault?.topics ? groupDefault.topics[topicId] : undefined;
|
||||||
|
if (hasOwn(topicConfig, "autoReply")) {
|
||||||
|
return (topicConfig as { autoReply?: boolean }).autoReply;
|
||||||
|
}
|
||||||
|
if (hasOwn(defaultTopicConfig, "autoReply")) {
|
||||||
|
return (defaultTopicConfig as { autoReply?: boolean }).autoReply;
|
||||||
|
}
|
||||||
|
if (hasOwn(groupConfig, "autoReply")) {
|
||||||
|
return (groupConfig as { autoReply?: boolean }).autoReply;
|
||||||
|
}
|
||||||
|
if (hasOwn(groupDefault, "autoReply")) {
|
||||||
|
return (groupDefault as { autoReply?: boolean }).autoReply;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveDiscordGuildEntry(
|
function resolveDiscordGuildEntry(
|
||||||
guilds: NonNullable<ClawdbotConfig["discord"]>["guilds"],
|
guilds: NonNullable<ClawdbotConfig["discord"]>["guilds"],
|
||||||
groupSpace?: string,
|
groupSpace?: string,
|
||||||
@@ -55,11 +105,17 @@ export function resolveGroupRequireMention(params: {
|
|||||||
const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, "");
|
const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, "");
|
||||||
const groupRoom = ctx.GroupRoom?.trim() ?? ctx.GroupSubject?.trim();
|
const groupRoom = ctx.GroupRoom?.trim() ?? ctx.GroupSubject?.trim();
|
||||||
const groupSpace = ctx.GroupSpace?.trim();
|
const groupSpace = ctx.GroupSpace?.trim();
|
||||||
if (
|
if (provider === "telegram") {
|
||||||
provider === "telegram" ||
|
const { chatId, topicId } = parseTelegramGroupId(groupId);
|
||||||
provider === "whatsapp" ||
|
const autoReply = resolveTelegramAutoReply({ cfg, chatId, topicId });
|
||||||
provider === "imessage"
|
if (typeof autoReply === "boolean") return !autoReply;
|
||||||
) {
|
return resolveProviderGroupRequireMention({
|
||||||
|
cfg,
|
||||||
|
provider,
|
||||||
|
groupId: chatId ?? groupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (provider === "whatsapp" || provider === "imessage") {
|
||||||
return resolveProviderGroupRequireMention({
|
return resolveProviderGroupRequireMention({
|
||||||
cfg,
|
cfg,
|
||||||
provider,
|
provider,
|
||||||
@@ -82,6 +138,9 @@ export function resolveGroupRequireMention(params: {
|
|||||||
(groupRoom
|
(groupRoom
|
||||||
? channelEntries[normalizeDiscordSlug(groupRoom)]
|
? channelEntries[normalizeDiscordSlug(groupRoom)]
|
||||||
: undefined);
|
: undefined);
|
||||||
|
if (entry && typeof entry.autoReply === "boolean") {
|
||||||
|
return !entry.autoReply;
|
||||||
|
}
|
||||||
if (entry && typeof entry.requireMention === "boolean") {
|
if (entry && typeof entry.requireMention === "boolean") {
|
||||||
return entry.requireMention;
|
return entry.requireMention;
|
||||||
}
|
}
|
||||||
@@ -104,7 +163,7 @@ export function resolveGroupRequireMention(params: {
|
|||||||
channelName ?? "",
|
channelName ?? "",
|
||||||
normalizedName,
|
normalizedName,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
let matched: { requireMention?: boolean } | undefined;
|
let matched: { requireMention?: boolean; autoReply?: boolean } | undefined;
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (candidate && channels[candidate]) {
|
if (candidate && channels[candidate]) {
|
||||||
matched = channels[candidate];
|
matched = channels[candidate];
|
||||||
@@ -113,6 +172,9 @@ export function resolveGroupRequireMention(params: {
|
|||||||
}
|
}
|
||||||
const fallback = channels["*"];
|
const fallback = channels["*"];
|
||||||
const resolved = matched ?? fallback;
|
const resolved = matched ?? fallback;
|
||||||
|
if (typeof resolved?.autoReply === "boolean") {
|
||||||
|
return !resolved.autoReply;
|
||||||
|
}
|
||||||
if (typeof resolved?.requireMention === "boolean") {
|
if (typeof resolved?.requireMention === "boolean") {
|
||||||
return resolved.requireMention;
|
return resolved.requireMention;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,6 +236,35 @@ export type TelegramActionConfig = {
|
|||||||
reactions?: boolean;
|
reactions?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TelegramTopicConfig = {
|
||||||
|
/** If specified, only load these skills for this topic. Omit = all skills; empty = no skills. */
|
||||||
|
skills?: string[];
|
||||||
|
/** If false, disable the bot for this topic. */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** If true, reply to every message (no mention required). */
|
||||||
|
autoReply?: boolean;
|
||||||
|
/** Optional allowlist for topic senders (ids or usernames). */
|
||||||
|
allowFrom?: Array<string | number>;
|
||||||
|
/** Optional system prompt snippet for this topic. */
|
||||||
|
systemPrompt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TelegramGroupConfig = {
|
||||||
|
requireMention?: boolean;
|
||||||
|
/** If specified, only load these skills for this group (when no topic). Omit = all skills; empty = no skills. */
|
||||||
|
skills?: string[];
|
||||||
|
/** Per-topic configuration (key is message_thread_id as string) */
|
||||||
|
topics?: Record<string, TelegramTopicConfig>;
|
||||||
|
/** If false, disable the bot for this group (and its topics). */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** If true, reply to every message (no mention required). */
|
||||||
|
autoReply?: boolean;
|
||||||
|
/** Optional allowlist for group senders (ids or usernames). */
|
||||||
|
allowFrom?: Array<string | number>;
|
||||||
|
/** Optional system prompt snippet for this group. */
|
||||||
|
systemPrompt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TelegramConfig = {
|
export type TelegramConfig = {
|
||||||
/**
|
/**
|
||||||
* Controls how Telegram direct chats (DMs) are handled:
|
* Controls how Telegram direct chats (DMs) are handled:
|
||||||
@@ -252,12 +281,7 @@ export type TelegramConfig = {
|
|||||||
tokenFile?: string;
|
tokenFile?: string;
|
||||||
/** Control reply threading when reply tags are present (off|first|all). */
|
/** Control reply threading when reply tags are present (off|first|all). */
|
||||||
replyToMode?: ReplyToMode;
|
replyToMode?: ReplyToMode;
|
||||||
groups?: Record<
|
groups?: Record<string, TelegramGroupConfig>;
|
||||||
string,
|
|
||||||
{
|
|
||||||
requireMention?: boolean;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
allowFrom?: Array<string | number>;
|
allowFrom?: Array<string | number>;
|
||||||
/** Optional allowlist for Telegram group senders (user ids or usernames). */
|
/** Optional allowlist for Telegram group senders (user ids or usernames). */
|
||||||
groupAllowFrom?: Array<string | number>;
|
groupAllowFrom?: Array<string | number>;
|
||||||
@@ -297,6 +321,16 @@ export type DiscordDmConfig = {
|
|||||||
export type DiscordGuildChannelConfig = {
|
export type DiscordGuildChannelConfig = {
|
||||||
allow?: boolean;
|
allow?: boolean;
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
|
/** If specified, only load these skills for this channel. Omit = all skills; empty = no skills. */
|
||||||
|
skills?: string[];
|
||||||
|
/** If false, disable the bot for this channel. */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** If true, reply to every message (no mention required). */
|
||||||
|
autoReply?: boolean;
|
||||||
|
/** Optional allowlist for channel senders (ids or names). */
|
||||||
|
users?: Array<string | number>;
|
||||||
|
/** Optional system prompt snippet for this channel. */
|
||||||
|
systemPrompt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiscordReactionNotificationMode =
|
export type DiscordReactionNotificationMode =
|
||||||
@@ -372,8 +406,20 @@ export type SlackDmConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SlackChannelConfig = {
|
export type SlackChannelConfig = {
|
||||||
|
/** If false, disable the bot in this channel. (Alias for allow: false.) */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** Legacy channel allow toggle; prefer enabled. */
|
||||||
allow?: boolean;
|
allow?: boolean;
|
||||||
|
/** Require mentioning the bot to trigger replies. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
|
/** Reply to all messages without needing a mention. */
|
||||||
|
autoReply?: boolean;
|
||||||
|
/** Allowlist of users that can invoke the bot in this channel. */
|
||||||
|
users?: Array<string | number>;
|
||||||
|
/** Optional skill filter for this channel. */
|
||||||
|
skills?: string[];
|
||||||
|
/** Optional system prompt for this channel. */
|
||||||
|
systemPrompt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SlackReactionNotificationMode = "off" | "own" | "all" | "allowlist";
|
export type SlackReactionNotificationMode = "off" | "own" | "all" | "allowlist";
|
||||||
|
|||||||
@@ -785,6 +785,27 @@ export const ClawdbotSchema = z.object({
|
|||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
|
skills: z.array(z.string()).optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
autoReply: z.boolean().optional(),
|
||||||
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
|
systemPrompt: z.string().optional(),
|
||||||
|
topics: z
|
||||||
|
.record(
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
skills: z.array(z.string()).optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
autoReply: z.boolean().optional(),
|
||||||
|
allowFrom: z
|
||||||
|
.array(z.union([z.string(), z.number()]))
|
||||||
|
.optional(),
|
||||||
|
systemPrompt: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
)
|
)
|
||||||
@@ -890,6 +911,13 @@ export const ClawdbotSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
allow: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
|
skills: z.array(z.string()).optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
autoReply: z.boolean().optional(),
|
||||||
|
users: z
|
||||||
|
.array(z.union([z.string(), z.number()]))
|
||||||
|
.optional(),
|
||||||
|
systemPrompt: z.string().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
)
|
)
|
||||||
@@ -959,8 +987,13 @@ export const ClawdbotSchema = z.object({
|
|||||||
z.string(),
|
z.string(),
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
allow: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
|
autoReply: z.boolean().optional(),
|
||||||
|
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
|
skills: z.array(z.string()).optional(),
|
||||||
|
systemPrompt: z.string().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -96,7 +96,15 @@ describe("discord guild/channel resolution", () => {
|
|||||||
const guildInfo: DiscordGuildEntryResolved = {
|
const guildInfo: DiscordGuildEntryResolved = {
|
||||||
channels: {
|
channels: {
|
||||||
general: { allow: true },
|
general: { allow: true },
|
||||||
help: { allow: true, requireMention: true },
|
help: {
|
||||||
|
allow: true,
|
||||||
|
requireMention: true,
|
||||||
|
skills: ["search"],
|
||||||
|
enabled: false,
|
||||||
|
autoReply: true,
|
||||||
|
users: ["123"],
|
||||||
|
systemPrompt: "Use short answers.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const channel = resolveDiscordChannelConfig({
|
const channel = resolveDiscordChannelConfig({
|
||||||
@@ -116,6 +124,11 @@ describe("discord guild/channel resolution", () => {
|
|||||||
});
|
});
|
||||||
expect(help?.allowed).toBe(true);
|
expect(help?.allowed).toBe(true);
|
||||||
expect(help?.requireMention).toBe(true);
|
expect(help?.requireMention).toBe(true);
|
||||||
|
expect(help?.skills).toEqual(["search"]);
|
||||||
|
expect(help?.enabled).toBe(false);
|
||||||
|
expect(help?.autoReply).toBe(true);
|
||||||
|
expect(help?.users).toEqual(["123"]);
|
||||||
|
expect(help?.systemPrompt).toBe("Use short answers.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("denies channel when config present but no match", () => {
|
it("denies channel when config present but no match", () => {
|
||||||
|
|||||||
@@ -94,12 +94,28 @@ export type DiscordGuildEntryResolved = {
|
|||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
reactionNotifications?: "off" | "own" | "all" | "allowlist";
|
reactionNotifications?: "off" | "own" | "all" | "allowlist";
|
||||||
users?: Array<string | number>;
|
users?: Array<string | number>;
|
||||||
channels?: Record<string, { allow?: boolean; requireMention?: boolean }>;
|
channels?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
allow?: boolean;
|
||||||
|
requireMention?: boolean;
|
||||||
|
skills?: string[];
|
||||||
|
enabled?: boolean;
|
||||||
|
autoReply?: boolean;
|
||||||
|
users?: Array<string | number>;
|
||||||
|
systemPrompt?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiscordChannelConfigResolved = {
|
export type DiscordChannelConfigResolved = {
|
||||||
allowed: boolean;
|
allowed: boolean;
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
|
skills?: string[];
|
||||||
|
enabled?: boolean;
|
||||||
|
autoReply?: boolean;
|
||||||
|
users?: Array<string | number>;
|
||||||
|
systemPrompt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiscordMessageEvent = Parameters<
|
export type DiscordMessageEvent = Parameters<
|
||||||
@@ -518,6 +534,12 @@ export function createDiscordMessageHandler(params: {
|
|||||||
channelSlug,
|
channelSlug,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
if (isGuildMessage && channelConfig?.enabled === false) {
|
||||||
|
logVerbose(
|
||||||
|
`Blocked discord channel ${message.channelId} (channel disabled)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const groupDmAllowed =
|
const groupDmAllowed =
|
||||||
isGroupDm &&
|
isGroupDm &&
|
||||||
@@ -579,8 +601,14 @@ export function createDiscordMessageHandler(params: {
|
|||||||
guildHistories.set(message.channelId, history);
|
guildHistories.set(message.channelId, history);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedRequireMention =
|
const baseRequireMention =
|
||||||
channelConfig?.requireMention ?? guildInfo?.requireMention ?? true;
|
channelConfig?.requireMention ?? guildInfo?.requireMention ?? true;
|
||||||
|
const shouldRequireMention =
|
||||||
|
channelConfig?.autoReply === true
|
||||||
|
? false
|
||||||
|
: channelConfig?.autoReply === false
|
||||||
|
? true
|
||||||
|
: baseRequireMention;
|
||||||
const hasAnyMention = Boolean(
|
const hasAnyMention = Boolean(
|
||||||
!isDirectMessage &&
|
!isDirectMessage &&
|
||||||
(message.mentionedEveryone ||
|
(message.mentionedEveryone ||
|
||||||
@@ -602,13 +630,13 @@ export function createDiscordMessageHandler(params: {
|
|||||||
const shouldBypassMention =
|
const shouldBypassMention =
|
||||||
allowTextCommands &&
|
allowTextCommands &&
|
||||||
isGuildMessage &&
|
isGuildMessage &&
|
||||||
resolvedRequireMention &&
|
shouldRequireMention &&
|
||||||
!wasMentioned &&
|
!wasMentioned &&
|
||||||
!hasAnyMention &&
|
!hasAnyMention &&
|
||||||
commandAuthorized &&
|
commandAuthorized &&
|
||||||
hasControlCommand(baseText);
|
hasControlCommand(baseText);
|
||||||
const canDetectMention = Boolean(botId) || mentionRegexes.length > 0;
|
const canDetectMention = Boolean(botId) || mentionRegexes.length > 0;
|
||||||
if (isGuildMessage && resolvedRequireMention) {
|
if (isGuildMessage && shouldRequireMention) {
|
||||||
if (botId && !wasMentioned && !shouldBypassMention) {
|
if (botId && !wasMentioned && !shouldBypassMention) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`discord: drop guild message (mention required, botId=${botId})`,
|
`discord: drop guild message (mention required, botId=${botId})`,
|
||||||
@@ -625,22 +653,17 @@ export function createDiscordMessageHandler(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isGuildMessage) {
|
if (isGuildMessage) {
|
||||||
const userAllow = guildInfo?.users;
|
const channelUsers = channelConfig?.users ?? guildInfo?.users;
|
||||||
if (Array.isArray(userAllow) && userAllow.length > 0) {
|
if (Array.isArray(channelUsers) && channelUsers.length > 0) {
|
||||||
const users = normalizeDiscordAllowList(userAllow, [
|
const userOk = resolveDiscordUserAllowed({
|
||||||
"discord:",
|
allowList: channelUsers,
|
||||||
"user:",
|
userId: author.id,
|
||||||
]);
|
userName: author.username,
|
||||||
const userOk =
|
userTag: formatDiscordUserTag(author),
|
||||||
!users ||
|
});
|
||||||
allowListMatches(users, {
|
|
||||||
id: author.id,
|
|
||||||
name: author.username,
|
|
||||||
tag: formatDiscordUserTag(author),
|
|
||||||
});
|
|
||||||
if (!userOk) {
|
if (!userOk) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Blocked discord guild sender ${author.id} (not in guild users allowlist)`,
|
`Blocked discord guild sender ${author.id} (not in channel users allowlist)`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -676,7 +699,7 @@ export function createDiscordMessageHandler(params: {
|
|||||||
if (ackReactionScope === "group-all") return isGroupChat;
|
if (ackReactionScope === "group-all") return isGroupChat;
|
||||||
if (ackReactionScope === "group-mentions") {
|
if (ackReactionScope === "group-mentions") {
|
||||||
if (!isGuildMessage) return false;
|
if (!isGuildMessage) return false;
|
||||||
if (!resolvedRequireMention) return false;
|
if (!shouldRequireMention) return false;
|
||||||
if (!canDetectMention) return false;
|
if (!canDetectMention) return false;
|
||||||
return wasMentioned || shouldBypassMention;
|
return wasMentioned || shouldBypassMention;
|
||||||
}
|
}
|
||||||
@@ -702,6 +725,15 @@ export function createDiscordMessageHandler(params: {
|
|||||||
const groupRoom =
|
const groupRoom =
|
||||||
isGuildMessage && channelSlug ? `#${channelSlug}` : undefined;
|
isGuildMessage && channelSlug ? `#${channelSlug}` : undefined;
|
||||||
const groupSubject = isDirectMessage ? undefined : groupRoom;
|
const groupSubject = isDirectMessage ? undefined : groupRoom;
|
||||||
|
const channelDescription = channelInfo?.topic?.trim();
|
||||||
|
const systemPromptParts = [
|
||||||
|
channelDescription ? `Channel topic: ${channelDescription}` : null,
|
||||||
|
channelConfig?.systemPrompt?.trim() || null,
|
||||||
|
].filter((entry): entry is string => Boolean(entry));
|
||||||
|
const groupSystemPrompt =
|
||||||
|
systemPromptParts.length > 0
|
||||||
|
? systemPromptParts.join("\n\n")
|
||||||
|
: undefined;
|
||||||
let combinedBody = formatAgentEnvelope({
|
let combinedBody = formatAgentEnvelope({
|
||||||
provider: "Discord",
|
provider: "Discord",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
@@ -755,6 +787,7 @@ export function createDiscordMessageHandler(params: {
|
|||||||
SenderTag: formatDiscordUserTag(author),
|
SenderTag: formatDiscordUserTag(author),
|
||||||
GroupSubject: groupSubject,
|
GroupSubject: groupSubject,
|
||||||
GroupRoom: groupRoom,
|
GroupRoom: groupRoom,
|
||||||
|
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
|
||||||
GroupSpace: isGuildMessage
|
GroupSpace: isGuildMessage
|
||||||
? (guildInfo?.id ?? guildSlug) || undefined
|
? (guildInfo?.id ?? guildSlug) || undefined
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -825,7 +858,7 @@ export function createDiscordMessageHandler(params: {
|
|||||||
ctx: ctxPayload,
|
ctx: ctxPayload,
|
||||||
cfg,
|
cfg,
|
||||||
dispatcher,
|
dispatcher,
|
||||||
replyOptions,
|
replyOptions: { ...replyOptions, skillFilter: channelConfig?.skills },
|
||||||
});
|
});
|
||||||
markDispatchIdle();
|
markDispatchIdle();
|
||||||
if (!queuedFinal) {
|
if (!queuedFinal) {
|
||||||
@@ -1053,13 +1086,27 @@ function createDiscordNativeCommand(params: {
|
|||||||
guild: interaction.guild ?? undefined,
|
guild: interaction.guild ?? undefined,
|
||||||
guildEntries: cfg.discord?.guilds,
|
guildEntries: cfg.discord?.guilds,
|
||||||
});
|
});
|
||||||
if (useAccessGroups && interaction.guild) {
|
const channelConfig = interaction.guild
|
||||||
const channelConfig = resolveDiscordChannelConfig({
|
? resolveDiscordChannelConfig({
|
||||||
guildInfo,
|
guildInfo,
|
||||||
channelId: channel?.id ?? "",
|
channelId: channel?.id ?? "",
|
||||||
channelName,
|
channelName,
|
||||||
channelSlug,
|
channelSlug,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
if (channelConfig?.enabled === false) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: "This channel is disabled.",
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (interaction.guild && channelConfig?.allowed === false) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: "This channel is not allowed.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (useAccessGroups && interaction.guild) {
|
||||||
const channelAllowlistConfigured =
|
const channelAllowlistConfigured =
|
||||||
Boolean(guildInfo?.channels) &&
|
Boolean(guildInfo?.channels) &&
|
||||||
Object.keys(guildInfo?.channels ?? {}).length > 0;
|
Object.keys(guildInfo?.channels ?? {}).length > 0;
|
||||||
@@ -1138,23 +1185,21 @@ function createDiscordNativeCommand(params: {
|
|||||||
commandAuthorized = true;
|
commandAuthorized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (guildInfo?.users && !isDirectMessage) {
|
if (!isDirectMessage) {
|
||||||
const allowList = normalizeDiscordAllowList(guildInfo.users, [
|
const channelUsers = channelConfig?.users ?? guildInfo?.users;
|
||||||
"discord:",
|
if (Array.isArray(channelUsers) && channelUsers.length > 0) {
|
||||||
"user:",
|
const userOk = resolveDiscordUserAllowed({
|
||||||
]);
|
allowList: channelUsers,
|
||||||
if (
|
userId: user.id,
|
||||||
allowList &&
|
userName: user.username,
|
||||||
!allowListMatches(allowList, {
|
userTag: formatDiscordUserTag(user),
|
||||||
id: user.id,
|
|
||||||
name: user.username,
|
|
||||||
tag: formatDiscordUserTag(user),
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
await interaction.reply({
|
|
||||||
content: "You are not authorized to use this command.",
|
|
||||||
});
|
});
|
||||||
return;
|
if (!userOk) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: "You are not authorized to use this command.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isGroupDm && cfg.discord?.dm?.groupEnabled === false) {
|
if (isGroupDm && cfg.discord?.dm?.groupEnabled === false) {
|
||||||
@@ -1183,6 +1228,24 @@ function createDiscordNativeCommand(params: {
|
|||||||
AccountId: route.accountId,
|
AccountId: route.accountId,
|
||||||
ChatType: isDirectMessage ? "direct" : "group",
|
ChatType: isDirectMessage ? "direct" : "group",
|
||||||
GroupSubject: isGuild ? interaction.guild?.name : undefined,
|
GroupSubject: isGuild ? interaction.guild?.name : undefined,
|
||||||
|
GroupSystemPrompt: isGuild
|
||||||
|
? (() => {
|
||||||
|
const channelTopic =
|
||||||
|
channel && "topic" in channel
|
||||||
|
? (channel.topic ?? undefined)
|
||||||
|
: undefined;
|
||||||
|
const channelDescription = channelTopic?.trim();
|
||||||
|
const systemPromptParts = [
|
||||||
|
channelDescription
|
||||||
|
? `Channel topic: ${channelDescription}`
|
||||||
|
: null,
|
||||||
|
channelConfig?.systemPrompt?.trim() || null,
|
||||||
|
].filter((entry): entry is string => Boolean(entry));
|
||||||
|
return systemPromptParts.length > 0
|
||||||
|
? systemPromptParts.join("\n\n")
|
||||||
|
: undefined;
|
||||||
|
})()
|
||||||
|
: undefined,
|
||||||
SenderName: user.globalName ?? user.username,
|
SenderName: user.globalName ?? user.username,
|
||||||
SenderId: user.id,
|
SenderId: user.id,
|
||||||
SenderUsername: user.username,
|
SenderUsername: user.username,
|
||||||
@@ -1213,7 +1276,11 @@ function createDiscordNativeCommand(params: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const replyResult = await getReplyFromConfig(ctxPayload, undefined, cfg);
|
const replyResult = await getReplyFromConfig(
|
||||||
|
ctxPayload,
|
||||||
|
{ skillFilter: channelConfig?.skills },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
const replies = replyResult
|
const replies = replyResult
|
||||||
? Array.isArray(replyResult)
|
? Array.isArray(replyResult)
|
||||||
? replyResult
|
? replyResult
|
||||||
@@ -1339,12 +1406,13 @@ async function deliverDiscordReply(params: {
|
|||||||
async function resolveDiscordChannelInfo(
|
async function resolveDiscordChannelInfo(
|
||||||
client: Client,
|
client: Client,
|
||||||
channelId: string,
|
channelId: string,
|
||||||
): Promise<{ type: ChannelType; name?: string } | null> {
|
): Promise<{ type: ChannelType; name?: string; topic?: string } | null> {
|
||||||
try {
|
try {
|
||||||
const channel = await client.fetchChannel(channelId);
|
const channel = await client.fetchChannel(channelId);
|
||||||
if (!channel) return null;
|
if (!channel) return null;
|
||||||
const name = "name" in channel ? (channel.name ?? undefined) : undefined;
|
const name = "name" in channel ? (channel.name ?? undefined) : undefined;
|
||||||
return { type: channel.type, name };
|
const topic = "topic" in channel ? (channel.topic ?? undefined) : undefined;
|
||||||
|
return { type: channel.type, name, topic };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logVerbose(`discord: failed to fetch channel ${channelId}: ${String(err)}`);
|
logVerbose(`discord: failed to fetch channel ${channelId}: ${String(err)}`);
|
||||||
return null;
|
return null;
|
||||||
@@ -1671,6 +1739,24 @@ export function allowListMatches(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveDiscordUserAllowed(params: {
|
||||||
|
allowList?: Array<string | number>;
|
||||||
|
userId: string;
|
||||||
|
userName?: string;
|
||||||
|
userTag?: string;
|
||||||
|
}) {
|
||||||
|
const allowList = normalizeDiscordAllowList(params.allowList, [
|
||||||
|
"discord:",
|
||||||
|
"user:",
|
||||||
|
]);
|
||||||
|
if (!allowList) return true;
|
||||||
|
return allowListMatches(allowList, {
|
||||||
|
id: params.userId,
|
||||||
|
name: params.userName,
|
||||||
|
tag: params.userTag,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveDiscordCommandAuthorized(params: {
|
export function resolveDiscordCommandAuthorized(params: {
|
||||||
isDirectMessage: boolean;
|
isDirectMessage: boolean;
|
||||||
allowFrom?: Array<string | number>;
|
allowFrom?: Array<string | number>;
|
||||||
@@ -1722,12 +1808,22 @@ export function resolveDiscordChannelConfig(params: {
|
|||||||
return {
|
return {
|
||||||
allowed: byId.allow !== false,
|
allowed: byId.allow !== false,
|
||||||
requireMention: byId.requireMention,
|
requireMention: byId.requireMention,
|
||||||
|
skills: byId.skills,
|
||||||
|
enabled: byId.enabled,
|
||||||
|
autoReply: byId.autoReply,
|
||||||
|
users: byId.users,
|
||||||
|
systemPrompt: byId.systemPrompt,
|
||||||
};
|
};
|
||||||
if (channelSlug && channels[channelSlug]) {
|
if (channelSlug && channels[channelSlug]) {
|
||||||
const entry = channels[channelSlug];
|
const entry = channels[channelSlug];
|
||||||
return {
|
return {
|
||||||
allowed: entry.allow !== false,
|
allowed: entry.allow !== false,
|
||||||
requireMention: entry.requireMention,
|
requireMention: entry.requireMention,
|
||||||
|
skills: entry.skills,
|
||||||
|
enabled: entry.enabled,
|
||||||
|
autoReply: entry.autoReply,
|
||||||
|
users: entry.users,
|
||||||
|
systemPrompt: entry.systemPrompt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (channelName && channels[channelName]) {
|
if (channelName && channels[channelName]) {
|
||||||
@@ -1735,6 +1831,11 @@ export function resolveDiscordChannelConfig(params: {
|
|||||||
return {
|
return {
|
||||||
allowed: entry.allow !== false,
|
allowed: entry.allow !== false,
|
||||||
requireMention: entry.requireMention,
|
requireMention: entry.requireMention,
|
||||||
|
skills: entry.skills,
|
||||||
|
enabled: entry.enabled,
|
||||||
|
autoReply: entry.autoReply,
|
||||||
|
users: entry.users,
|
||||||
|
systemPrompt: entry.systemPrompt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { allowed: false };
|
return { allowed: false };
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ type SlackThreadBroadcastEvent = {
|
|||||||
type SlackChannelConfigResolved = {
|
type SlackChannelConfigResolved = {
|
||||||
allowed: boolean;
|
allowed: boolean;
|
||||||
requireMention: boolean;
|
requireMention: boolean;
|
||||||
|
autoReply?: boolean;
|
||||||
|
users?: Array<string | number>;
|
||||||
|
skills?: string[];
|
||||||
|
systemPrompt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeSlackSlug(raw?: string) {
|
function normalizeSlackSlug(raw?: string) {
|
||||||
@@ -177,6 +181,13 @@ function normalizeAllowListLower(list?: Array<string | number>) {
|
|||||||
return normalizeAllowList(list).map((entry) => entry.toLowerCase());
|
return normalizeAllowList(list).map((entry) => entry.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function firstDefined<T>(...values: Array<T | undefined>) {
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value !== "undefined") return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function allowListMatches(params: {
|
function allowListMatches(params: {
|
||||||
allowList: string[];
|
allowList: string[];
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -199,6 +210,20 @@ function allowListMatches(params: {
|
|||||||
return candidates.some((value) => allowList.includes(value));
|
return candidates.some((value) => allowList.includes(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveSlackUserAllowed(params: {
|
||||||
|
allowList?: Array<string | number>;
|
||||||
|
userId?: string;
|
||||||
|
userName?: string;
|
||||||
|
}) {
|
||||||
|
const allowList = normalizeAllowListLower(params.allowList);
|
||||||
|
if (allowList.length === 0) return true;
|
||||||
|
return allowListMatches({
|
||||||
|
allowList,
|
||||||
|
id: params.userId,
|
||||||
|
name: params.userName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function resolveSlackSlashCommandConfig(
|
function resolveSlackSlashCommandConfig(
|
||||||
raw?: SlackSlashCommandConfig,
|
raw?: SlackSlashCommandConfig,
|
||||||
): Required<SlackSlashCommandConfig> {
|
): Required<SlackSlashCommandConfig> {
|
||||||
@@ -253,7 +278,18 @@ function resolveSlackChannelLabel(params: {
|
|||||||
function resolveSlackChannelConfig(params: {
|
function resolveSlackChannelConfig(params: {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
channelName?: string;
|
channelName?: string;
|
||||||
channels?: Record<string, { allow?: boolean; requireMention?: boolean }>;
|
channels?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
enabled?: boolean;
|
||||||
|
allow?: boolean;
|
||||||
|
requireMention?: boolean;
|
||||||
|
autoReply?: boolean;
|
||||||
|
users?: Array<string | number>;
|
||||||
|
skills?: string[];
|
||||||
|
systemPrompt?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
}): SlackChannelConfigResolved | null {
|
}): SlackChannelConfigResolved | null {
|
||||||
const { channelId, channelName, channels } = params;
|
const { channelId, channelName, channels } = params;
|
||||||
const entries = channels ?? {};
|
const entries = channels ?? {};
|
||||||
@@ -267,7 +303,17 @@ function resolveSlackChannelConfig(params: {
|
|||||||
normalizedName,
|
normalizedName,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
let matched: { allow?: boolean; requireMention?: boolean } | undefined;
|
let matched:
|
||||||
|
| {
|
||||||
|
enabled?: boolean;
|
||||||
|
allow?: boolean;
|
||||||
|
requireMention?: boolean;
|
||||||
|
autoReply?: boolean;
|
||||||
|
users?: Array<string | number>;
|
||||||
|
skills?: string[];
|
||||||
|
systemPrompt?: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (candidate && entries[candidate]) {
|
if (candidate && entries[candidate]) {
|
||||||
matched = entries[candidate];
|
matched = entries[candidate];
|
||||||
@@ -284,10 +330,25 @@ function resolveSlackChannelConfig(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolved = matched ?? fallback ?? {};
|
const resolved = matched ?? fallback ?? {};
|
||||||
const allowed = resolved.allow ?? true;
|
const allowed =
|
||||||
|
firstDefined(
|
||||||
|
resolved.enabled,
|
||||||
|
resolved.allow,
|
||||||
|
fallback?.enabled,
|
||||||
|
fallback?.allow,
|
||||||
|
true,
|
||||||
|
) ?? true;
|
||||||
const requireMention =
|
const requireMention =
|
||||||
resolved.requireMention ?? fallback?.requireMention ?? true;
|
firstDefined(resolved.requireMention, fallback?.requireMention, true) ??
|
||||||
return { allowed, requireMention };
|
true;
|
||||||
|
const autoReply = firstDefined(resolved.autoReply, fallback?.autoReply);
|
||||||
|
const users = firstDefined(resolved.users, fallback?.users);
|
||||||
|
const skills = firstDefined(resolved.skills, fallback?.skills);
|
||||||
|
const systemPrompt = firstDefined(
|
||||||
|
resolved.systemPrompt,
|
||||||
|
fallback?.systemPrompt,
|
||||||
|
);
|
||||||
|
return { allowed, requireMention, autoReply, users, skills, systemPrompt };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveSlackMedia(params: {
|
async function resolveSlackMedia(params: {
|
||||||
@@ -410,7 +471,12 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
const logger = getChildLogger({ module: "slack-auto-reply" });
|
const logger = getChildLogger({ module: "slack-auto-reply" });
|
||||||
const channelCache = new Map<
|
const channelCache = new Map<
|
||||||
string,
|
string,
|
||||||
{ name?: string; type?: SlackMessageEvent["channel_type"] }
|
{
|
||||||
|
name?: string;
|
||||||
|
type?: SlackMessageEvent["channel_type"];
|
||||||
|
topic?: string;
|
||||||
|
purpose?: string;
|
||||||
|
}
|
||||||
>();
|
>();
|
||||||
const userCache = new Map<string, { name?: string }>();
|
const userCache = new Map<string, { name?: string }>();
|
||||||
const seenMessages = new Map<string, number>();
|
const seenMessages = new Map<string, number>();
|
||||||
@@ -469,7 +535,15 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
: channel?.is_group
|
: channel?.is_group
|
||||||
? "group"
|
? "group"
|
||||||
: undefined;
|
: undefined;
|
||||||
const entry = { name, type };
|
const topic =
|
||||||
|
channel && "topic" in channel
|
||||||
|
? (channel.topic?.value ?? undefined)
|
||||||
|
: undefined;
|
||||||
|
const purpose =
|
||||||
|
channel && "purpose" in channel
|
||||||
|
? (channel.purpose?.value ?? undefined)
|
||||||
|
: undefined;
|
||||||
|
const entry = { name, type, topic, purpose };
|
||||||
channelCache.set(channelId, entry);
|
channelCache.set(channelId, entry);
|
||||||
return entry;
|
return entry;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -606,6 +680,8 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
let channelInfo: {
|
let channelInfo: {
|
||||||
name?: string;
|
name?: string;
|
||||||
type?: SlackMessageEvent["channel_type"];
|
type?: SlackMessageEvent["channel_type"];
|
||||||
|
topic?: string;
|
||||||
|
purpose?: string;
|
||||||
} = {};
|
} = {};
|
||||||
let channelType = message.channel_type;
|
let channelType = message.channel_type;
|
||||||
if (!channelType || channelType !== "im") {
|
if (!channelType || channelType !== "im") {
|
||||||
@@ -706,23 +782,44 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
matchesMentionPatterns(message.text ?? "", mentionRegexes)));
|
matchesMentionPatterns(message.text ?? "", mentionRegexes)));
|
||||||
const sender = await resolveUserName(message.user);
|
const sender = await resolveUserName(message.user);
|
||||||
const senderName = sender?.name ?? message.user;
|
const senderName = sender?.name ?? message.user;
|
||||||
|
const channelUserAuthorized = isRoom
|
||||||
|
? resolveSlackUserAllowed({
|
||||||
|
allowList: channelConfig?.users,
|
||||||
|
userId: message.user,
|
||||||
|
userName: senderName,
|
||||||
|
})
|
||||||
|
: true;
|
||||||
|
if (isRoom && !channelUserAuthorized) {
|
||||||
|
logVerbose(
|
||||||
|
`Blocked unauthorized slack sender ${message.user} (not in channel users)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const allowList = effectiveAllowFromLower;
|
const allowList = effectiveAllowFromLower;
|
||||||
const commandAuthorized =
|
const commandAuthorized =
|
||||||
allowList.length === 0 ||
|
(allowList.length === 0 ||
|
||||||
allowListMatches({
|
allowListMatches({
|
||||||
allowList,
|
allowList,
|
||||||
id: message.user,
|
id: message.user,
|
||||||
name: senderName,
|
name: senderName,
|
||||||
});
|
})) &&
|
||||||
|
channelUserAuthorized;
|
||||||
const hasAnyMention = /<@[^>]+>/.test(message.text ?? "");
|
const hasAnyMention = /<@[^>]+>/.test(message.text ?? "");
|
||||||
const allowTextCommands = shouldHandleTextCommands({
|
const allowTextCommands = shouldHandleTextCommands({
|
||||||
cfg,
|
cfg,
|
||||||
surface: "slack",
|
surface: "slack",
|
||||||
});
|
});
|
||||||
|
const shouldRequireMention = isRoom
|
||||||
|
? channelConfig?.autoReply === true
|
||||||
|
? false
|
||||||
|
: channelConfig?.autoReply === false
|
||||||
|
? true
|
||||||
|
: (channelConfig?.requireMention ?? true)
|
||||||
|
: false;
|
||||||
const shouldBypassMention =
|
const shouldBypassMention =
|
||||||
allowTextCommands &&
|
allowTextCommands &&
|
||||||
isRoom &&
|
isRoom &&
|
||||||
channelConfig?.requireMention &&
|
shouldRequireMention &&
|
||||||
!wasMentioned &&
|
!wasMentioned &&
|
||||||
!hasAnyMention &&
|
!hasAnyMention &&
|
||||||
commandAuthorized &&
|
commandAuthorized &&
|
||||||
@@ -730,7 +827,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
const canDetectMention = Boolean(botUserId) || mentionRegexes.length > 0;
|
const canDetectMention = Boolean(botUserId) || mentionRegexes.length > 0;
|
||||||
if (
|
if (
|
||||||
isRoom &&
|
isRoom &&
|
||||||
channelConfig?.requireMention &&
|
shouldRequireMention &&
|
||||||
canDetectMention &&
|
canDetectMention &&
|
||||||
!wasMentioned &&
|
!wasMentioned &&
|
||||||
!shouldBypassMention
|
!shouldBypassMention
|
||||||
@@ -757,7 +854,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
if (ackReactionScope === "group-all") return isGroupChat;
|
if (ackReactionScope === "group-all") return isGroupChat;
|
||||||
if (ackReactionScope === "group-mentions") {
|
if (ackReactionScope === "group-mentions") {
|
||||||
if (!isRoom) return false;
|
if (!isRoom) return false;
|
||||||
if (!channelConfig?.requireMention) return false;
|
if (!shouldRequireMention) return false;
|
||||||
if (!canDetectMention) return false;
|
if (!canDetectMention) return false;
|
||||||
return wasMentioned || shouldBypassMention;
|
return wasMentioned || shouldBypassMention;
|
||||||
}
|
}
|
||||||
@@ -812,6 +909,17 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
const slackTo = isDirectMessage
|
const slackTo = isDirectMessage
|
||||||
? `user:${message.user}`
|
? `user:${message.user}`
|
||||||
: `channel:${message.channel}`;
|
: `channel:${message.channel}`;
|
||||||
|
const channelDescription = [channelInfo?.topic, channelInfo?.purpose]
|
||||||
|
.map((entry) => entry?.trim())
|
||||||
|
.filter((entry): entry is string => Boolean(entry))
|
||||||
|
.filter((entry, index, list) => list.indexOf(entry) === index)
|
||||||
|
.join("\n");
|
||||||
|
const systemPromptParts = [
|
||||||
|
channelDescription ? `Channel description: ${channelDescription}` : null,
|
||||||
|
channelConfig?.systemPrompt?.trim() || null,
|
||||||
|
].filter((entry): entry is string => Boolean(entry));
|
||||||
|
const groupSystemPrompt =
|
||||||
|
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||||
const ctxPayload = {
|
const ctxPayload = {
|
||||||
Body: body,
|
Body: body,
|
||||||
From: slackFrom,
|
From: slackFrom,
|
||||||
@@ -820,6 +928,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
AccountId: route.accountId,
|
AccountId: route.accountId,
|
||||||
ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
|
ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
|
||||||
GroupSubject: isRoomish ? roomLabel : undefined,
|
GroupSubject: isRoomish ? roomLabel : undefined,
|
||||||
|
GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
|
||||||
SenderName: senderName,
|
SenderName: senderName,
|
||||||
SenderId: message.user,
|
SenderId: message.user,
|
||||||
Provider: "slack" as const,
|
Provider: "slack" as const,
|
||||||
@@ -907,7 +1016,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
ctx: ctxPayload,
|
ctx: ctxPayload,
|
||||||
cfg,
|
cfg,
|
||||||
dispatcher,
|
dispatcher,
|
||||||
replyOptions,
|
replyOptions: { ...replyOptions, skillFilter: channelConfig?.skills },
|
||||||
});
|
});
|
||||||
markDispatchIdle();
|
markDispatchIdle();
|
||||||
if (didSetStatus) {
|
if (didSetStatus) {
|
||||||
@@ -1457,6 +1566,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
normalizeAllowListLower(effectiveAllowFrom);
|
normalizeAllowListLower(effectiveAllowFrom);
|
||||||
|
|
||||||
let commandAuthorized = true;
|
let commandAuthorized = true;
|
||||||
|
let channelConfig: SlackChannelConfigResolved | null = null;
|
||||||
if (isDirectMessage) {
|
if (isDirectMessage) {
|
||||||
if (!dmEnabled || dmPolicy === "disabled") {
|
if (!dmEnabled || dmPolicy === "disabled") {
|
||||||
await respond({
|
await respond({
|
||||||
@@ -1506,7 +1616,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRoom) {
|
if (isRoom) {
|
||||||
const channelConfig = resolveSlackChannelConfig({
|
channelConfig = resolveSlackChannelConfig({
|
||||||
channelId: command.channel_id,
|
channelId: command.channel_id,
|
||||||
channelName: channelInfo?.name,
|
channelName: channelInfo?.name,
|
||||||
channels: channelsConfig,
|
channels: channelsConfig,
|
||||||
@@ -1538,6 +1648,20 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
|
|
||||||
const sender = await resolveUserName(command.user_id);
|
const sender = await resolveUserName(command.user_id);
|
||||||
const senderName = sender?.name ?? command.user_name ?? command.user_id;
|
const senderName = sender?.name ?? command.user_name ?? command.user_id;
|
||||||
|
const channelUserAllowed = isRoom
|
||||||
|
? resolveSlackUserAllowed({
|
||||||
|
allowList: channelConfig?.users,
|
||||||
|
userId: command.user_id,
|
||||||
|
userName: senderName,
|
||||||
|
})
|
||||||
|
: true;
|
||||||
|
if (isRoom && !channelUserAllowed) {
|
||||||
|
await respond({
|
||||||
|
text: "You are not authorized to use this command here.",
|
||||||
|
response_type: "ephemeral",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const channelName = channelInfo?.name;
|
const channelName = channelInfo?.name;
|
||||||
const roomLabel = channelName
|
const roomLabel = channelName
|
||||||
? `#${channelName}`
|
? `#${channelName}`
|
||||||
@@ -1552,6 +1676,21 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
id: isDirectMessage ? command.user_id : command.channel_id,
|
id: isDirectMessage ? command.user_id : command.channel_id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const channelDescription = [channelInfo?.topic, channelInfo?.purpose]
|
||||||
|
.map((entry) => entry?.trim())
|
||||||
|
.filter((entry): entry is string => Boolean(entry))
|
||||||
|
.filter((entry, index, list) => list.indexOf(entry) === index)
|
||||||
|
.join("\n");
|
||||||
|
const systemPromptParts = [
|
||||||
|
channelDescription
|
||||||
|
? `Channel description: ${channelDescription}`
|
||||||
|
: null,
|
||||||
|
channelConfig?.systemPrompt?.trim() || null,
|
||||||
|
].filter((entry): entry is string => Boolean(entry));
|
||||||
|
const groupSystemPrompt =
|
||||||
|
systemPromptParts.length > 0
|
||||||
|
? systemPromptParts.join("\n\n")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const ctxPayload = {
|
const ctxPayload = {
|
||||||
Body: prompt,
|
Body: prompt,
|
||||||
@@ -1563,6 +1702,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
To: `slash:${command.user_id}`,
|
To: `slash:${command.user_id}`,
|
||||||
ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
|
ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
|
||||||
GroupSubject: isRoomish ? roomLabel : undefined,
|
GroupSubject: isRoomish ? roomLabel : undefined,
|
||||||
|
GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
|
||||||
SenderName: senderName,
|
SenderName: senderName,
|
||||||
SenderId: command.user_id,
|
SenderId: command.user_id,
|
||||||
Provider: "slack" as const,
|
Provider: "slack" as const,
|
||||||
@@ -1580,7 +1720,11 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
OriginatingTo: `user:${command.user_id}`,
|
OriginatingTo: `user:${command.user_id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const replyResult = await getReplyFromConfig(ctxPayload, undefined, cfg);
|
const replyResult = await getReplyFromConfig(
|
||||||
|
ctxPayload,
|
||||||
|
{ skillFilter: channelConfig?.skills },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
const replies = replyResult
|
const replies = replyResult
|
||||||
? Array.isArray(replyResult)
|
? Array.isArray(replyResult)
|
||||||
? replyResult
|
? replyResult
|
||||||
|
|||||||
@@ -1407,6 +1407,61 @@ describe("createTelegramBot", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies topic skill filters and system prompts", async () => {
|
||||||
|
onSpy.mockReset();
|
||||||
|
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||||
|
typeof vi.fn
|
||||||
|
>;
|
||||||
|
replySpy.mockReset();
|
||||||
|
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
telegram: {
|
||||||
|
groups: {
|
||||||
|
"-1001234567890": {
|
||||||
|
requireMention: false,
|
||||||
|
systemPrompt: "Group prompt",
|
||||||
|
skills: ["group-skill"],
|
||||||
|
topics: {
|
||||||
|
"99": {
|
||||||
|
skills: [],
|
||||||
|
systemPrompt: "Topic prompt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
createTelegramBot({ token: "tok" });
|
||||||
|
const handler = onSpy.mock.calls[0][1] as (
|
||||||
|
ctx: Record<string, unknown>,
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
message: {
|
||||||
|
chat: {
|
||||||
|
id: -1001234567890,
|
||||||
|
type: "supergroup",
|
||||||
|
title: "Forum Group",
|
||||||
|
is_forum: true,
|
||||||
|
},
|
||||||
|
from: { id: 12345, username: "testuser" },
|
||||||
|
text: "hello",
|
||||||
|
date: 1736380800,
|
||||||
|
message_id: 42,
|
||||||
|
message_thread_id: 99,
|
||||||
|
},
|
||||||
|
me: { username: "clawdbot_bot" },
|
||||||
|
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
|
const payload = replySpy.mock.calls[0][0];
|
||||||
|
expect(payload.GroupSystemPrompt).toBe("Group prompt\n\nTopic prompt");
|
||||||
|
const opts = replySpy.mock.calls[0][1];
|
||||||
|
expect(opts?.skillFilter).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("passes message_thread_id to topic replies", async () => {
|
it("passes message_thread_id to topic replies", async () => {
|
||||||
onSpy.mockReset();
|
onSpy.mockReset();
|
||||||
sendMessageSpy.mockReset();
|
sendMessageSpy.mockReset();
|
||||||
|
|||||||
@@ -153,6 +153,12 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
hasEntries: entries.length > 0,
|
hasEntries: entries.length > 0,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
const firstDefined = <T>(...values: Array<T | undefined>) => {
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value !== "undefined") return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
const isSenderAllowed = (params: {
|
const isSenderAllowed = (params: {
|
||||||
allow: ReturnType<typeof normalizeAllowFrom>;
|
allow: ReturnType<typeof normalizeAllowFrom>;
|
||||||
senderId?: string;
|
senderId?: string;
|
||||||
@@ -210,6 +216,20 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
requireMentionOverride: opts.requireMention,
|
requireMentionOverride: opts.requireMention,
|
||||||
overrideOrder: "after-config",
|
overrideOrder: "after-config",
|
||||||
});
|
});
|
||||||
|
const resolveTelegramGroupConfig = (
|
||||||
|
chatId: string | number,
|
||||||
|
messageThreadId?: number,
|
||||||
|
) => {
|
||||||
|
const groups = cfg.telegram?.groups;
|
||||||
|
if (!groups) return { groupConfig: undefined, topicConfig: undefined };
|
||||||
|
const groupKey = String(chatId);
|
||||||
|
const groupConfig = groups[groupKey] ?? groups["*"];
|
||||||
|
const topicConfig =
|
||||||
|
messageThreadId != null
|
||||||
|
? groupConfig?.topics?.[String(messageThreadId)]
|
||||||
|
: undefined;
|
||||||
|
return { groupConfig, topicConfig };
|
||||||
|
};
|
||||||
|
|
||||||
const processMessage = async (
|
const processMessage = async (
|
||||||
primaryCtx: TelegramContext,
|
primaryCtx: TelegramContext,
|
||||||
@@ -222,14 +242,34 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const messageThreadId = (msg as { message_thread_id?: number })
|
const messageThreadId = (msg as { message_thread_id?: number })
|
||||||
.message_thread_id;
|
.message_thread_id;
|
||||||
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
||||||
|
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(
|
||||||
|
chatId,
|
||||||
|
messageThreadId,
|
||||||
|
);
|
||||||
const effectiveDmAllow = normalizeAllowFrom([
|
const effectiveDmAllow = normalizeAllowFrom([
|
||||||
...(allowFrom ?? []),
|
...(allowFrom ?? []),
|
||||||
...storeAllowFrom,
|
...storeAllowFrom,
|
||||||
]);
|
]);
|
||||||
|
const groupAllowOverride = firstDefined(
|
||||||
|
topicConfig?.allowFrom,
|
||||||
|
groupConfig?.allowFrom,
|
||||||
|
);
|
||||||
const effectiveGroupAllow = normalizeAllowFrom([
|
const effectiveGroupAllow = normalizeAllowFrom([
|
||||||
...(groupAllowFrom ?? []),
|
...(groupAllowOverride ?? groupAllowFrom ?? []),
|
||||||
...storeAllowFrom,
|
...storeAllowFrom,
|
||||||
]);
|
]);
|
||||||
|
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
|
||||||
|
|
||||||
|
if (isGroup && groupConfig?.enabled === false) {
|
||||||
|
logVerbose(`Blocked telegram group ${chatId} (group disabled)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isGroup && topicConfig?.enabled === false) {
|
||||||
|
logVerbose(
|
||||||
|
`Blocked telegram topic ${chatId} (${messageThreadId ?? "unknown"}) (topic disabled)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sendTyping = async () => {
|
const sendTyping = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -316,6 +356,19 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const botUsername = primaryCtx.me?.username?.toLowerCase();
|
const botUsername = primaryCtx.me?.username?.toLowerCase();
|
||||||
const senderId = msg.from?.id ? String(msg.from.id) : "";
|
const senderId = msg.from?.id ? String(msg.from.id) : "";
|
||||||
const senderUsername = msg.from?.username ?? "";
|
const senderUsername = msg.from?.username ?? "";
|
||||||
|
if (isGroup && hasGroupAllowOverride) {
|
||||||
|
const allowed = isSenderAllowed({
|
||||||
|
allow: effectiveGroupAllow,
|
||||||
|
senderId,
|
||||||
|
senderUsername,
|
||||||
|
});
|
||||||
|
if (!allowed) {
|
||||||
|
logVerbose(
|
||||||
|
`Blocked telegram group sender ${senderId || "unknown"} (group allowFrom override)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const commandAuthorized = isSenderAllowed({
|
const commandAuthorized = isSenderAllowed({
|
||||||
allow: isGroup ? effectiveGroupAllow : effectiveDmAllow,
|
allow: isGroup ? effectiveGroupAllow : effectiveDmAllow,
|
||||||
senderId,
|
senderId,
|
||||||
@@ -327,7 +380,17 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const hasAnyMention = (msg.entities ?? msg.caption_entities ?? []).some(
|
const hasAnyMention = (msg.entities ?? msg.caption_entities ?? []).some(
|
||||||
(ent) => ent.type === "mention",
|
(ent) => ent.type === "mention",
|
||||||
);
|
);
|
||||||
const requireMention = resolveGroupRequireMention(chatId);
|
const baseRequireMention = resolveGroupRequireMention(chatId);
|
||||||
|
const autoReplySetting = firstDefined(
|
||||||
|
topicConfig?.autoReply,
|
||||||
|
groupConfig?.autoReply,
|
||||||
|
);
|
||||||
|
const requireMention =
|
||||||
|
autoReplySetting === true
|
||||||
|
? false
|
||||||
|
: autoReplySetting === false
|
||||||
|
? true
|
||||||
|
: baseRequireMention;
|
||||||
const shouldBypassMention =
|
const shouldBypassMention =
|
||||||
isGroup &&
|
isGroup &&
|
||||||
requireMention &&
|
requireMention &&
|
||||||
@@ -423,6 +486,13 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
: buildTelegramDmPeerId(chatId, messageThreadId),
|
: buildTelegramDmPeerId(chatId, messageThreadId),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const skillFilter = firstDefined(topicConfig?.skills, groupConfig?.skills);
|
||||||
|
const systemPromptParts = [
|
||||||
|
groupConfig?.systemPrompt?.trim() || null,
|
||||||
|
topicConfig?.systemPrompt?.trim() || null,
|
||||||
|
].filter((entry): entry is string => Boolean(entry));
|
||||||
|
const groupSystemPrompt =
|
||||||
|
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||||
const ctxPayload = {
|
const ctxPayload = {
|
||||||
Body: body,
|
Body: body,
|
||||||
From: isGroup
|
From: isGroup
|
||||||
@@ -433,6 +503,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
AccountId: route.accountId,
|
AccountId: route.accountId,
|
||||||
ChatType: isGroup ? "group" : "direct",
|
ChatType: isGroup ? "group" : "direct",
|
||||||
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
||||||
|
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
|
||||||
SenderName: buildSenderName(msg),
|
SenderName: buildSenderName(msg),
|
||||||
SenderId: senderId || undefined,
|
SenderId: senderId || undefined,
|
||||||
SenderUsername: senderUsername || undefined,
|
SenderUsername: senderUsername || undefined,
|
||||||
@@ -601,6 +672,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
dispatcher,
|
dispatcher,
|
||||||
replyOptions: {
|
replyOptions: {
|
||||||
...replyOptions,
|
...replyOptions,
|
||||||
|
skillFilter,
|
||||||
onPartialReply: draftStream
|
onPartialReply: draftStream
|
||||||
? (payload) => updateDraftFromPartial(payload.text)
|
? (payload) => updateDraftFromPartial(payload.text)
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -642,6 +714,49 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const messageThreadId = (msg as { message_thread_id?: number })
|
const messageThreadId = (msg as { message_thread_id?: number })
|
||||||
.message_thread_id;
|
.message_thread_id;
|
||||||
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
||||||
|
const storeAllowFrom = await readTelegramAllowFromStore().catch(
|
||||||
|
() => [],
|
||||||
|
);
|
||||||
|
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(
|
||||||
|
chatId,
|
||||||
|
messageThreadId,
|
||||||
|
);
|
||||||
|
const groupAllowOverride = firstDefined(
|
||||||
|
topicConfig?.allowFrom,
|
||||||
|
groupConfig?.allowFrom,
|
||||||
|
);
|
||||||
|
const effectiveGroupAllow = normalizeAllowFrom([
|
||||||
|
...(groupAllowOverride ?? groupAllowFrom ?? []),
|
||||||
|
...storeAllowFrom,
|
||||||
|
]);
|
||||||
|
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
|
||||||
|
|
||||||
|
if (isGroup && groupConfig?.enabled === false) {
|
||||||
|
await bot.api.sendMessage(chatId, "This group is disabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isGroup && topicConfig?.enabled === false) {
|
||||||
|
await bot.api.sendMessage(chatId, "This topic is disabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isGroup && hasGroupAllowOverride) {
|
||||||
|
const senderId = msg.from?.id;
|
||||||
|
const senderUsername = msg.from?.username ?? "";
|
||||||
|
if (
|
||||||
|
senderId == null ||
|
||||||
|
!isSenderAllowed({
|
||||||
|
allow: effectiveGroupAllow,
|
||||||
|
senderId: String(senderId),
|
||||||
|
senderUsername,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
await bot.api.sendMessage(
|
||||||
|
chatId,
|
||||||
|
"You are not authorized to use this command.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isGroup && useAccessGroups) {
|
if (isGroup && useAccessGroups) {
|
||||||
const groupPolicy = cfg.telegram?.groupPolicy ?? "open";
|
const groupPolicy = cfg.telegram?.groupPolicy ?? "open";
|
||||||
@@ -664,7 +779,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const senderUsername = msg.from?.username ?? "";
|
const senderUsername = msg.from?.username ?? "";
|
||||||
if (
|
if (
|
||||||
!isSenderAllowed({
|
!isSenderAllowed({
|
||||||
allow: groupAllow,
|
allow: effectiveGroupAllow,
|
||||||
senderId: String(senderId),
|
senderId: String(senderId),
|
||||||
senderUsername,
|
senderUsername,
|
||||||
})
|
})
|
||||||
@@ -718,6 +833,18 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
: buildTelegramDmPeerId(chatId, messageThreadId),
|
: buildTelegramDmPeerId(chatId, messageThreadId),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const skillFilter = firstDefined(
|
||||||
|
topicConfig?.skills,
|
||||||
|
groupConfig?.skills,
|
||||||
|
);
|
||||||
|
const systemPromptParts = [
|
||||||
|
groupConfig?.systemPrompt?.trim() || null,
|
||||||
|
topicConfig?.systemPrompt?.trim() || null,
|
||||||
|
].filter((entry): entry is string => Boolean(entry));
|
||||||
|
const groupSystemPrompt =
|
||||||
|
systemPromptParts.length > 0
|
||||||
|
? systemPromptParts.join("\n\n")
|
||||||
|
: undefined;
|
||||||
const ctxPayload = {
|
const ctxPayload = {
|
||||||
Body: prompt,
|
Body: prompt,
|
||||||
From: isGroup
|
From: isGroup
|
||||||
@@ -726,6 +853,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
To: `slash:${senderId || chatId}`,
|
To: `slash:${senderId || chatId}`,
|
||||||
ChatType: isGroup ? "group" : "direct",
|
ChatType: isGroup ? "group" : "direct",
|
||||||
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
||||||
|
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
|
||||||
SenderName: buildSenderName(msg),
|
SenderName: buildSenderName(msg),
|
||||||
SenderId: senderId || undefined,
|
SenderId: senderId || undefined,
|
||||||
SenderUsername: senderUsername || undefined,
|
SenderUsername: senderUsername || undefined,
|
||||||
@@ -743,7 +871,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
|
|
||||||
const replyResult = await getReplyFromConfig(
|
const replyResult = await getReplyFromConfig(
|
||||||
ctxPayload,
|
ctxPayload,
|
||||||
undefined,
|
{ skillFilter },
|
||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
const replies = replyResult
|
const replies = replyResult
|
||||||
@@ -777,9 +905,51 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const isGroup =
|
const isGroup =
|
||||||
msg.chat.type === "group" || msg.chat.type === "supergroup";
|
msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||||
|
const messageThreadId = (msg as { message_thread_id?: number })
|
||||||
|
.message_thread_id;
|
||||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
||||||
|
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(
|
||||||
|
chatId,
|
||||||
|
messageThreadId,
|
||||||
|
);
|
||||||
|
const groupAllowOverride = firstDefined(
|
||||||
|
topicConfig?.allowFrom,
|
||||||
|
groupConfig?.allowFrom,
|
||||||
|
);
|
||||||
|
const effectiveGroupAllow = normalizeAllowFrom([
|
||||||
|
...(groupAllowOverride ?? groupAllowFrom ?? []),
|
||||||
|
...storeAllowFrom,
|
||||||
|
]);
|
||||||
|
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
|
||||||
|
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
|
if (groupConfig?.enabled === false) {
|
||||||
|
logVerbose(`Blocked telegram group ${chatId} (group disabled)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (topicConfig?.enabled === false) {
|
||||||
|
logVerbose(
|
||||||
|
`Blocked telegram topic ${chatId} (${messageThreadId ?? "unknown"}) (topic disabled)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasGroupAllowOverride) {
|
||||||
|
const senderId = msg.from?.id;
|
||||||
|
const senderUsername = msg.from?.username ?? "";
|
||||||
|
const allowed =
|
||||||
|
senderId != null &&
|
||||||
|
isSenderAllowed({
|
||||||
|
allow: effectiveGroupAllow,
|
||||||
|
senderId: String(senderId),
|
||||||
|
senderUsername,
|
||||||
|
});
|
||||||
|
if (!allowed) {
|
||||||
|
logVerbose(
|
||||||
|
`Blocked telegram group sender ${senderId ?? "unknown"} (group allowFrom override)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Group policy filtering: controls how group messages are handled
|
// Group policy filtering: controls how group messages are handled
|
||||||
// - "open" (default): groups bypass allowFrom, only mention-gating applies
|
// - "open" (default): groups bypass allowFrom, only mention-gating applies
|
||||||
// - "disabled": block all group messages entirely
|
// - "disabled": block all group messages entirely
|
||||||
@@ -790,10 +960,6 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (groupPolicy === "allowlist") {
|
if (groupPolicy === "allowlist") {
|
||||||
const effectiveGroupAllow = normalizeAllowFrom([
|
|
||||||
...(groupAllowFrom ?? []),
|
|
||||||
...storeAllowFrom,
|
|
||||||
]);
|
|
||||||
// For allowlist mode, the sender (msg.from.id) must be in allowFrom
|
// For allowlist mode, the sender (msg.from.id) must be in allowFrom
|
||||||
const senderId = msg.from?.id;
|
const senderId = msg.from?.id;
|
||||||
if (senderId == null) {
|
if (senderId == null) {
|
||||||
@@ -804,7 +970,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
}
|
}
|
||||||
if (!effectiveGroupAllow.hasEntries) {
|
if (!effectiveGroupAllow.hasEntries) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
"Blocked telegram group message (groupPolicy: allowlist, no groupAllowFrom)",
|
"Blocked telegram group message (groupPolicy: allowlist, no group allowlist entries)",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user