feat: scope telegram inline buttons
This commit is contained in:
@@ -8,6 +8,7 @@ import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { getMachineDisplayName } from "../../infra/machine-name.js";
|
||||
import { resolveTelegramInlineButtonsScope } from "../../telegram/inline-buttons.js";
|
||||
import { type enqueueCommand, enqueueCommandInLane } from "../../process/command-queue.js";
|
||||
import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||
import { isSubagentSessionKey } from "../../routing/session-key.js";
|
||||
@@ -210,13 +211,29 @@ export async function compactEmbeddedPiSession(params: {
|
||||
const runtimeChannel = normalizeMessageChannel(
|
||||
params.messageChannel ?? params.messageProvider,
|
||||
);
|
||||
const runtimeCapabilities = runtimeChannel
|
||||
let runtimeCapabilities = runtimeChannel
|
||||
? (resolveChannelCapabilities({
|
||||
cfg: params.config,
|
||||
channel: runtimeChannel,
|
||||
accountId: params.agentAccountId,
|
||||
}) ?? [])
|
||||
: undefined;
|
||||
if (runtimeChannel === "telegram" && params.config) {
|
||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
if (inlineButtonsScope !== "off") {
|
||||
if (!runtimeCapabilities) runtimeCapabilities = [];
|
||||
if (
|
||||
!runtimeCapabilities.some(
|
||||
(cap) => String(cap).trim().toLowerCase() === "inlinebuttons",
|
||||
)
|
||||
) {
|
||||
runtimeCapabilities.push("inlineButtons");
|
||||
}
|
||||
}
|
||||
}
|
||||
const runtimeInfo = {
|
||||
host: machineName,
|
||||
os: `${os.type()} ${os.release()}`,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createAgentSession, SessionManager, SettingsManager } from "@mariozechn
|
||||
import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js";
|
||||
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
|
||||
import { getMachineDisplayName } from "../../../infra/machine-name.js";
|
||||
import { resolveTelegramInlineButtonsScope } from "../../../telegram/inline-buttons.js";
|
||||
import { resolveTelegramReactionLevel } from "../../../telegram/reaction-level.js";
|
||||
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
|
||||
import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
||||
@@ -157,13 +158,27 @@ export async function runEmbeddedAttempt(
|
||||
|
||||
const machineName = await getMachineDisplayName();
|
||||
const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider);
|
||||
const runtimeCapabilities = runtimeChannel
|
||||
let runtimeCapabilities = runtimeChannel
|
||||
? (resolveChannelCapabilities({
|
||||
cfg: params.config,
|
||||
channel: runtimeChannel,
|
||||
accountId: params.agentAccountId,
|
||||
}) ?? [])
|
||||
: undefined;
|
||||
if (runtimeChannel === "telegram" && params.config) {
|
||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
if (inlineButtonsScope !== "off") {
|
||||
if (!runtimeCapabilities) runtimeCapabilities = [];
|
||||
if (
|
||||
!runtimeCapabilities.some((cap) => String(cap).trim().toLowerCase() === "inlinebuttons")
|
||||
) {
|
||||
runtimeCapabilities.push("inlineButtons");
|
||||
}
|
||||
}
|
||||
}
|
||||
const reactionGuidance =
|
||||
runtimeChannel === "telegram" && params.config
|
||||
? (() => {
|
||||
|
||||
@@ -98,7 +98,7 @@ function buildMessagingSection(params: {
|
||||
params.inlineButtonsEnabled
|
||||
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
|
||||
: params.runtimeChannel
|
||||
? `- Inline buttons not enabled for ${params.runtimeChannel}. If you need them, ask to add "inlineButtons" to ${params.runtimeChannel}.capabilities or ${params.runtimeChannel}.accounts.<id>.capabilities.`
|
||||
? `- Inline buttons not enabled for ${params.runtimeChannel}. If you need them, ask to set ${params.runtimeChannel}.capabilities.inlineButtons ("dm"|"group"|"all"|"allowlist").`
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -328,10 +328,28 @@ describe("handleTelegramAction", () => {
|
||||
).rejects.toThrow(/Telegram bot token missing/);
|
||||
});
|
||||
|
||||
it("requires inlineButtons capability when buttons are provided", async () => {
|
||||
it("allows inline buttons by default (allowlist)", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "@testchannel",
|
||||
content: "Choose",
|
||||
buttons: [[{ text: "Ok", callback_data: "cmd:ok" }]],
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(sendMessageTelegram).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks inline buttons when scope is off", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: { botToken: "tok", capabilities: { inlineButtons: "off" } },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
{
|
||||
@@ -342,13 +360,32 @@ describe("handleTelegramAction", () => {
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
).rejects.toThrow(/inlineButtons/i);
|
||||
).rejects.toThrow(/inline buttons are disabled/i);
|
||||
});
|
||||
|
||||
it("blocks inline buttons in groups when scope is dm", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: { botToken: "tok", capabilities: { inlineButtons: "dm" } },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "-100123456",
|
||||
content: "Choose",
|
||||
buttons: [[{ text: "Ok", callback_data: "cmd:ok" }]],
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
).rejects.toThrow(/inline buttons are limited to DMs/i);
|
||||
});
|
||||
|
||||
it("sends messages with inline keyboard buttons when enabled", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: { botToken: "tok", capabilities: ["inlineButtons"] },
|
||||
telegram: { botToken: "tok", capabilities: { inlineButtons: "all" } },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js";
|
||||
import {
|
||||
@@ -8,6 +7,10 @@ import {
|
||||
sendMessageTelegram,
|
||||
} from "../../telegram/send.js";
|
||||
import { resolveTelegramToken } from "../../telegram/token.js";
|
||||
import {
|
||||
resolveTelegramInlineButtonsScope,
|
||||
resolveTelegramTargetChatType,
|
||||
} from "../../telegram/inline-buttons.js";
|
||||
import {
|
||||
createActionGate,
|
||||
jsonResult,
|
||||
@@ -22,19 +25,6 @@ type TelegramButton = {
|
||||
callback_data: string;
|
||||
};
|
||||
|
||||
function hasInlineButtonsCapability(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | undefined;
|
||||
}): boolean {
|
||||
const caps =
|
||||
resolveChannelCapabilities({
|
||||
cfg: params.cfg,
|
||||
channel: "telegram",
|
||||
accountId: params.accountId,
|
||||
}) ?? [];
|
||||
return caps.some((cap) => cap.toLowerCase() === "inlinebuttons");
|
||||
}
|
||||
|
||||
export function readTelegramButtons(
|
||||
params: Record<string, unknown>,
|
||||
): TelegramButton[][] | undefined {
|
||||
@@ -138,10 +128,32 @@ export async function handleTelegramAction(
|
||||
allowEmpty: true,
|
||||
}) ?? "";
|
||||
const buttons = readTelegramButtons(params);
|
||||
if (buttons && !hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined })) {
|
||||
throw new Error(
|
||||
'Telegram inline buttons requested but not enabled. Add "inlineButtons" to channels.telegram.capabilities (or channels.telegram.accounts.<id>.capabilities).',
|
||||
);
|
||||
if (buttons) {
|
||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
if (inlineButtonsScope === "off") {
|
||||
throw new Error(
|
||||
'Telegram inline buttons are disabled. Set channels.telegram.capabilities.inlineButtons to "dm", "group", "all", or "allowlist".',
|
||||
);
|
||||
}
|
||||
if (inlineButtonsScope === "dm" || inlineButtonsScope === "group") {
|
||||
const targetType = resolveTelegramTargetChatType(to);
|
||||
if (targetType === "unknown") {
|
||||
throw new Error(
|
||||
`Telegram inline buttons require a numeric chat id when inlineButtons="${inlineButtonsScope}".`,
|
||||
);
|
||||
}
|
||||
if (inlineButtonsScope === "dm" && targetType !== "direct") {
|
||||
throw new Error('Telegram inline buttons are limited to DMs when inlineButtons="dm".');
|
||||
}
|
||||
if (inlineButtonsScope === "group" && targetType !== "group") {
|
||||
throw new Error(
|
||||
'Telegram inline buttons are limited to groups when inlineButtons="group".',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Optional threading parameters for forum topics and reply chains
|
||||
const replyToMessageId = readNumberParam(params, "replyToMessageId", {
|
||||
|
||||
@@ -4,32 +4,12 @@ import {
|
||||
readStringParam,
|
||||
} from "../../../agents/tools/common.js";
|
||||
import { handleTelegramAction } from "../../../agents/tools/telegram-actions.js";
|
||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||
import { listEnabledTelegramAccounts } from "../../../telegram/accounts.js";
|
||||
import { isTelegramInlineButtonsEnabled } from "../../../telegram/inline-buttons.js";
|
||||
import type { ChannelMessageActionAdapter, ChannelMessageActionName } from "../types.js";
|
||||
|
||||
const providerId = "telegram";
|
||||
|
||||
function hasTelegramInlineButtons(cfg: ClawdbotConfig): boolean {
|
||||
const caps = new Set<string>();
|
||||
for (const entry of cfg.channels?.telegram?.capabilities ?? []) {
|
||||
const trimmed = String(entry).trim();
|
||||
if (trimmed) caps.add(trimmed.toLowerCase());
|
||||
}
|
||||
const accounts = cfg.channels?.telegram?.accounts;
|
||||
if (accounts && typeof accounts === "object") {
|
||||
for (const account of Object.values(accounts)) {
|
||||
const accountCaps = (account as { capabilities?: unknown })?.capabilities;
|
||||
if (!Array.isArray(accountCaps)) continue;
|
||||
for (const entry of accountCaps) {
|
||||
const trimmed = String(entry).trim();
|
||||
if (trimmed) caps.add(trimmed.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
return caps.has("inlinebuttons");
|
||||
}
|
||||
|
||||
export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
const accounts = listEnabledTelegramAccounts(cfg).filter(
|
||||
@@ -42,7 +22,15 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
if (gate("deleteMessage")) actions.add("delete");
|
||||
return Array.from(actions);
|
||||
},
|
||||
supportsButtons: ({ cfg }) => hasTelegramInlineButtons(cfg),
|
||||
supportsButtons: ({ cfg }) => {
|
||||
const accounts = listEnabledTelegramAccounts(cfg).filter(
|
||||
(account) => account.tokenSource !== "none",
|
||||
);
|
||||
if (accounts.length === 0) return false;
|
||||
return accounts.some((account) =>
|
||||
isTelegramInlineButtonsEnabled({ cfg, accountId: account.accountId }),
|
||||
);
|
||||
},
|
||||
extractToolSend: ({ args }) => {
|
||||
const action = typeof args.action === "string" ? args.action.trim() : "";
|
||||
if (action !== "sendMessage") return null;
|
||||
|
||||
@@ -194,6 +194,7 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.telegram.retry.maxDelayMs": "Telegram Retry Max Delay (ms)",
|
||||
"channels.telegram.retry.jitter": "Telegram Retry Jitter",
|
||||
"channels.telegram.timeoutSeconds": "Telegram API Timeout (seconds)",
|
||||
"channels.telegram.capabilities.inlineButtons": "Telegram Inline Buttons",
|
||||
"channels.whatsapp.dmPolicy": "WhatsApp DM Policy",
|
||||
"channels.whatsapp.selfChatMode": "WhatsApp Self-Phone Mode",
|
||||
"channels.whatsapp.debounceMs": "WhatsApp Message Debounce (ms)",
|
||||
|
||||
@@ -14,6 +14,14 @@ export type TelegramActionConfig = {
|
||||
deleteMessage?: boolean;
|
||||
};
|
||||
|
||||
export type TelegramInlineButtonsScope = "off" | "dm" | "group" | "all" | "allowlist";
|
||||
|
||||
export type TelegramCapabilitiesConfig =
|
||||
| string[]
|
||||
| {
|
||||
inlineButtons?: TelegramInlineButtonsScope;
|
||||
};
|
||||
|
||||
/** Custom command definition for Telegram bot menu. */
|
||||
export type TelegramCustomCommand = {
|
||||
/** Command name (without leading /). */
|
||||
@@ -26,7 +34,7 @@ export type TelegramAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
/** Optional provider capability tags used for agent/runtime guidance. */
|
||||
capabilities?: string[];
|
||||
capabilities?: TelegramCapabilitiesConfig;
|
||||
/** Override native command registration for Telegram (bool or "auto"). */
|
||||
commands?: ProviderCommandsConfig;
|
||||
/** Custom commands to register in Telegram's command menu (merged with native). */
|
||||
|
||||
@@ -19,6 +19,15 @@ import {
|
||||
resolveTelegramCustomCommands,
|
||||
} from "./telegram-custom-commands.js";
|
||||
|
||||
const TelegramInlineButtonsScopeSchema = z.enum(["off", "dm", "group", "all", "allowlist"]);
|
||||
|
||||
const TelegramCapabilitiesSchema = z.union([
|
||||
z.array(z.string()),
|
||||
z.object({
|
||||
inlineButtons: TelegramInlineButtonsScopeSchema.optional(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const TelegramTopicSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
@@ -62,7 +71,7 @@ const validateTelegramCustomCommands = (
|
||||
|
||||
export const TelegramAccountSchemaBase = z.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
capabilities: TelegramCapabilitiesSchema.optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
commands: ProviderCommandsSchema,
|
||||
customCommands: z.array(TelegramCustomCommandSchema).optional(),
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { TelegramMessage } from "./bot/types.js";
|
||||
import { firstDefined, isSenderAllowed, normalizeAllowFrom } from "./bot-access.js";
|
||||
import { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js";
|
||||
import { migrateTelegramGroupConfig } from "./group-migration.js";
|
||||
import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
|
||||
import { readTelegramAllowFromStore } from "./pairing-store.js";
|
||||
import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
|
||||
|
||||
@@ -183,6 +184,131 @@ export const registerTelegramHandlers = ({
|
||||
const callbackMessage = callback.message;
|
||||
if (!data || !callbackMessage) return;
|
||||
|
||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||
cfg,
|
||||
accountId,
|
||||
});
|
||||
if (inlineButtonsScope === "off") return;
|
||||
|
||||
const chatId = callbackMessage.chat.id;
|
||||
const isGroup =
|
||||
callbackMessage.chat.type === "group" || callbackMessage.chat.type === "supergroup";
|
||||
if (inlineButtonsScope === "dm" && isGroup) return;
|
||||
if (inlineButtonsScope === "group" && !isGroup) return;
|
||||
|
||||
const messageThreadId = (callbackMessage as { message_thread_id?: number }).message_thread_id;
|
||||
const isForum = (callbackMessage.chat as { is_forum?: boolean }).is_forum === true;
|
||||
const resolvedThreadId = resolveTelegramForumThreadId({
|
||||
isForum,
|
||||
messageThreadId,
|
||||
});
|
||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
|
||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
||||
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
|
||||
const effectiveGroupAllow = normalizeAllowFrom([
|
||||
...(groupAllowOverride ?? groupAllowFrom ?? []),
|
||||
...storeAllowFrom,
|
||||
]);
|
||||
const effectiveDmAllow = normalizeAllowFrom([
|
||||
...(telegramCfg.allowFrom ?? []),
|
||||
...storeAllowFrom,
|
||||
]);
|
||||
const dmPolicy = telegramCfg.dmPolicy ?? "pairing";
|
||||
const senderId = callback.from?.id ? String(callback.from.id) : "";
|
||||
const senderUsername = callback.from?.username ?? "";
|
||||
|
||||
if (isGroup) {
|
||||
if (groupConfig?.enabled === false) {
|
||||
logVerbose(`Blocked telegram group ${chatId} (group disabled)`);
|
||||
return;
|
||||
}
|
||||
if (topicConfig?.enabled === false) {
|
||||
logVerbose(
|
||||
`Blocked telegram topic ${chatId} (${resolvedThreadId ?? "unknown"}) (topic disabled)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (typeof groupAllowOverride !== "undefined") {
|
||||
const allowed =
|
||||
senderId &&
|
||||
isSenderAllowed({
|
||||
allow: effectiveGroupAllow,
|
||||
senderId,
|
||||
senderUsername,
|
||||
});
|
||||
if (!allowed) {
|
||||
logVerbose(
|
||||
`Blocked telegram group sender ${senderId || "unknown"} (group allowFrom override)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const groupPolicy = telegramCfg.groupPolicy ?? "open";
|
||||
if (groupPolicy === "disabled") {
|
||||
logVerbose(`Blocked telegram group message (groupPolicy: disabled)`);
|
||||
return;
|
||||
}
|
||||
if (groupPolicy === "allowlist") {
|
||||
if (!senderId) {
|
||||
logVerbose(`Blocked telegram group message (no sender ID, groupPolicy: allowlist)`);
|
||||
return;
|
||||
}
|
||||
if (!effectiveGroupAllow.hasEntries) {
|
||||
logVerbose(
|
||||
"Blocked telegram group message (groupPolicy: allowlist, no group allowlist entries)",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!isSenderAllowed({
|
||||
allow: effectiveGroupAllow,
|
||||
senderId,
|
||||
senderUsername,
|
||||
})
|
||||
) {
|
||||
logVerbose(
|
||||
`Blocked telegram group message from ${senderId} (groupPolicy: allowlist)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const groupAllowlist = resolveGroupPolicy(chatId);
|
||||
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
|
||||
logger.info(
|
||||
{ chatId, title: callbackMessage.chat.title, reason: "not-allowed" },
|
||||
"skipping group message",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (inlineButtonsScope === "allowlist") {
|
||||
if (!isGroup) {
|
||||
if (dmPolicy === "disabled") return;
|
||||
if (dmPolicy !== "open") {
|
||||
const allowed =
|
||||
effectiveDmAllow.hasWildcard ||
|
||||
(effectiveDmAllow.hasEntries &&
|
||||
isSenderAllowed({
|
||||
allow: effectiveDmAllow,
|
||||
senderId,
|
||||
senderUsername,
|
||||
}));
|
||||
if (!allowed) return;
|
||||
}
|
||||
} else {
|
||||
const allowed =
|
||||
effectiveGroupAllow.hasWildcard ||
|
||||
(effectiveGroupAllow.hasEntries &&
|
||||
isSenderAllowed({
|
||||
allow: effectiveGroupAllow,
|
||||
senderId,
|
||||
senderUsername,
|
||||
}));
|
||||
if (!allowed) return;
|
||||
}
|
||||
}
|
||||
|
||||
const syntheticMessage: TelegramMessage = {
|
||||
...callbackMessage,
|
||||
from: callback.from,
|
||||
@@ -191,7 +317,6 @@ export const registerTelegramHandlers = ({
|
||||
caption_entities: undefined,
|
||||
entities: undefined,
|
||||
};
|
||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
||||
const getFile = typeof ctx.getFile === "function" ? ctx.getFile.bind(ctx) : async () => ({});
|
||||
await processMessage({ message: syntheticMessage, me: ctx.me, getFile }, [], storeAllowFrom, {
|
||||
forceWasMentioned: true,
|
||||
|
||||
@@ -382,6 +382,47 @@ describe("createTelegramBot", () => {
|
||||
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-1");
|
||||
});
|
||||
|
||||
it("blocks callback_query when inline buttons are allowlist-only and sender not authorized", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
createTelegramBot({
|
||||
token: "tok",
|
||||
config: {
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "pairing",
|
||||
capabilities: { inlineButtons: "allowlist" },
|
||||
allowFrom: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const callbackHandler = onSpy.mock.calls.find((call) => call[0] === "callback_query")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(callbackHandler).toBeDefined();
|
||||
|
||||
await callbackHandler({
|
||||
callbackQuery: {
|
||||
id: "cbq-2",
|
||||
data: "cmd:option_b",
|
||||
from: { id: 9, first_name: "Ada", username: "ada_bot" },
|
||||
message: {
|
||||
chat: { id: 1234, type: "private" },
|
||||
date: 1736380800,
|
||||
message_id: 11,
|
||||
},
|
||||
},
|
||||
me: { username: "clawdbot_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-2");
|
||||
});
|
||||
|
||||
it("wraps inbound message with Telegram envelope", async () => {
|
||||
const originalTz = process.env.TZ;
|
||||
process.env.TZ = "Europe/Vienna";
|
||||
|
||||
73
src/telegram/inline-buttons.ts
Normal file
73
src/telegram/inline-buttons.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { TelegramInlineButtonsScope } from "../config/types.telegram.js";
|
||||
import { listTelegramAccountIds, resolveTelegramAccount } from "./accounts.js";
|
||||
|
||||
const DEFAULT_INLINE_BUTTONS_SCOPE: TelegramInlineButtonsScope = "allowlist";
|
||||
|
||||
function normalizeInlineButtonsScope(value: unknown): TelegramInlineButtonsScope | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
if (
|
||||
trimmed === "off" ||
|
||||
trimmed === "dm" ||
|
||||
trimmed === "group" ||
|
||||
trimmed === "all" ||
|
||||
trimmed === "allowlist"
|
||||
) {
|
||||
return trimmed as TelegramInlineButtonsScope;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveInlineButtonsScopeFromCapabilities(
|
||||
capabilities: unknown,
|
||||
): TelegramInlineButtonsScope {
|
||||
if (!capabilities) return DEFAULT_INLINE_BUTTONS_SCOPE;
|
||||
if (Array.isArray(capabilities)) {
|
||||
const enabled = capabilities.some(
|
||||
(entry) => String(entry).trim().toLowerCase() === "inlinebuttons",
|
||||
);
|
||||
return enabled ? "all" : "off";
|
||||
}
|
||||
if (typeof capabilities === "object") {
|
||||
const inlineButtons = (capabilities as { inlineButtons?: unknown }).inlineButtons;
|
||||
return normalizeInlineButtonsScope(inlineButtons) ?? DEFAULT_INLINE_BUTTONS_SCOPE;
|
||||
}
|
||||
return DEFAULT_INLINE_BUTTONS_SCOPE;
|
||||
}
|
||||
|
||||
export function resolveTelegramInlineButtonsScope(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
}): TelegramInlineButtonsScope {
|
||||
const account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
return resolveInlineButtonsScopeFromCapabilities(account.config.capabilities);
|
||||
}
|
||||
|
||||
export function isTelegramInlineButtonsEnabled(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
}): boolean {
|
||||
if (params.accountId) {
|
||||
return resolveTelegramInlineButtonsScope(params) !== "off";
|
||||
}
|
||||
const accountIds = listTelegramAccountIds(params.cfg);
|
||||
if (accountIds.length === 0) {
|
||||
return resolveTelegramInlineButtonsScope(params) !== "off";
|
||||
}
|
||||
return accountIds.some(
|
||||
(accountId) =>
|
||||
resolveTelegramInlineButtonsScope({ cfg: params.cfg, accountId }) !== "off",
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveTelegramTargetChatType(
|
||||
target: string,
|
||||
): "direct" | "group" | "unknown" {
|
||||
const trimmed = target.trim();
|
||||
if (!trimmed) return "unknown";
|
||||
if (/^-?\d+$/.test(trimmed)) {
|
||||
return trimmed.startsWith("-") ? "group" : "direct";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
Reference in New Issue
Block a user