feat(telegram): inline keyboard buttons (#491)
Co-authored-by: Azade <azade@hey.com>
This commit is contained in:
87
src/config/provider-capabilities.test.ts
Normal file
87
src/config/provider-capabilities.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "./config.js";
|
||||
import { resolveProviderCapabilities } from "./provider-capabilities.js";
|
||||
|
||||
describe("resolveProviderCapabilities", () => {
|
||||
it("returns undefined for missing inputs", () => {
|
||||
expect(resolveProviderCapabilities({})).toBeUndefined();
|
||||
expect(
|
||||
resolveProviderCapabilities({ cfg: {} as ClawdbotConfig }),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
resolveProviderCapabilities({ cfg: {} as ClawdbotConfig, provider: "" }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes and prefers per-account capabilities", () => {
|
||||
const cfg = {
|
||||
telegram: {
|
||||
capabilities: [" inlineButtons ", ""],
|
||||
accounts: {
|
||||
default: {
|
||||
capabilities: [" perAccount ", " "],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Partial<ClawdbotConfig>;
|
||||
|
||||
expect(
|
||||
resolveProviderCapabilities({
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
provider: "telegram",
|
||||
accountId: "default",
|
||||
}),
|
||||
).toEqual(["perAccount"]);
|
||||
});
|
||||
|
||||
it("falls back to provider capabilities when account capabilities are missing", () => {
|
||||
const cfg = {
|
||||
telegram: {
|
||||
capabilities: ["inlineButtons"],
|
||||
accounts: {
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
} satisfies Partial<ClawdbotConfig>;
|
||||
|
||||
expect(
|
||||
resolveProviderCapabilities({
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
provider: "telegram",
|
||||
accountId: "default",
|
||||
}),
|
||||
).toEqual(["inlineButtons"]);
|
||||
});
|
||||
|
||||
it("matches account keys case-insensitively", () => {
|
||||
const cfg = {
|
||||
slack: {
|
||||
accounts: {
|
||||
Family: { capabilities: ["threads"] },
|
||||
},
|
||||
},
|
||||
} satisfies Partial<ClawdbotConfig>;
|
||||
|
||||
expect(
|
||||
resolveProviderCapabilities({
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
provider: "slack",
|
||||
accountId: "family",
|
||||
}),
|
||||
).toEqual(["threads"]);
|
||||
});
|
||||
|
||||
it("supports msteams capabilities", () => {
|
||||
const cfg = {
|
||||
msteams: { capabilities: [" polls ", ""] },
|
||||
} satisfies Partial<ClawdbotConfig>;
|
||||
|
||||
expect(
|
||||
resolveProviderCapabilities({
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
provider: "msteams",
|
||||
}),
|
||||
).toEqual(["polls"]);
|
||||
});
|
||||
});
|
||||
91
src/config/provider-capabilities.ts
Normal file
91
src/config/provider-capabilities.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import type { ClawdbotConfig } from "./config.js";
|
||||
|
||||
function normalizeCapabilities(
|
||||
capabilities: string[] | undefined,
|
||||
): string[] | undefined {
|
||||
if (!capabilities) return undefined;
|
||||
const normalized = capabilities.map((entry) => entry.trim()).filter(Boolean);
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function resolveAccountCapabilities(params: {
|
||||
cfg?: { accounts?: Record<string, { capabilities?: string[] }> } & {
|
||||
capabilities?: string[];
|
||||
};
|
||||
accountId?: string | null;
|
||||
}): string[] | undefined {
|
||||
const cfg = params.cfg;
|
||||
if (!cfg) return undefined;
|
||||
const normalizedAccountId = normalizeAccountId(params.accountId);
|
||||
|
||||
const accounts = cfg.accounts;
|
||||
if (accounts && typeof accounts === "object") {
|
||||
const direct = accounts[normalizedAccountId];
|
||||
if (direct) {
|
||||
return (
|
||||
normalizeCapabilities(direct.capabilities) ??
|
||||
normalizeCapabilities(cfg.capabilities)
|
||||
);
|
||||
}
|
||||
const matchKey = Object.keys(accounts).find(
|
||||
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
|
||||
);
|
||||
const match = matchKey ? accounts[matchKey] : undefined;
|
||||
if (match) {
|
||||
return (
|
||||
normalizeCapabilities(match.capabilities) ??
|
||||
normalizeCapabilities(cfg.capabilities)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeCapabilities(cfg.capabilities);
|
||||
}
|
||||
|
||||
export function resolveProviderCapabilities(params: {
|
||||
cfg?: ClawdbotConfig;
|
||||
provider?: string | null;
|
||||
accountId?: string | null;
|
||||
}): string[] | undefined {
|
||||
const cfg = params.cfg;
|
||||
const provider = params.provider?.trim().toLowerCase();
|
||||
if (!cfg || !provider) return undefined;
|
||||
|
||||
switch (provider) {
|
||||
case "whatsapp":
|
||||
return resolveAccountCapabilities({
|
||||
cfg: cfg.whatsapp,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
case "telegram":
|
||||
return resolveAccountCapabilities({
|
||||
cfg: cfg.telegram,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
case "discord":
|
||||
return resolveAccountCapabilities({
|
||||
cfg: cfg.discord,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
case "slack":
|
||||
return resolveAccountCapabilities({
|
||||
cfg: cfg.slack,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
case "signal":
|
||||
return resolveAccountCapabilities({
|
||||
cfg: cfg.signal,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
case "imessage":
|
||||
return resolveAccountCapabilities({
|
||||
cfg: cfg.imessage,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
case "msteams":
|
||||
return normalizeCapabilities(cfg.msteams?.capabilities);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -106,11 +106,14 @@ export type IdentityConfig = {
|
||||
export type WhatsAppActionConfig = {
|
||||
reactions?: boolean;
|
||||
sendMessage?: boolean;
|
||||
polls?: boolean;
|
||||
};
|
||||
|
||||
export type WhatsAppConfig = {
|
||||
/** Optional per-account WhatsApp configuration (multi-account). */
|
||||
accounts?: Record<string, WhatsAppAccountConfig>;
|
||||
/** Optional provider capability tags used for agent/runtime guidance. */
|
||||
capabilities?: string[];
|
||||
/**
|
||||
* Inbound message prefix (WhatsApp only).
|
||||
* Default: `[{agents.list[].identity.name}]` (or `[clawdbot]`) when allowFrom is empty, else `""`.
|
||||
@@ -155,6 +158,8 @@ export type WhatsAppConfig = {
|
||||
export type WhatsAppAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
/** Optional provider capability tags used for agent/runtime guidance. */
|
||||
capabilities?: string[];
|
||||
/** If false, do not start this WhatsApp account provider. Default: true. */
|
||||
enabled?: boolean;
|
||||
/** Inbound message prefix override for this account (WhatsApp only). */
|
||||
@@ -300,6 +305,8 @@ export type TelegramActionConfig = {
|
||||
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[];
|
||||
/**
|
||||
* Controls how Telegram direct chats (DMs) are handled:
|
||||
* - "pairing" (default): unknown senders get a pairing code; owner must approve
|
||||
@@ -441,6 +448,8 @@ export type DiscordActionConfig = {
|
||||
export type DiscordAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
/** Optional provider capability tags used for agent/runtime guidance. */
|
||||
capabilities?: string[];
|
||||
/** If false, do not start this Discord account. Default: true. */
|
||||
enabled?: boolean;
|
||||
token?: string;
|
||||
@@ -538,6 +547,8 @@ export type SlackSlashCommandConfig = {
|
||||
export type SlackAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
/** Optional provider capability tags used for agent/runtime guidance. */
|
||||
capabilities?: string[];
|
||||
/** If false, do not start this Slack account. Default: true. */
|
||||
enabled?: boolean;
|
||||
botToken?: string;
|
||||
@@ -576,6 +587,8 @@ export type SlackConfig = {
|
||||
export type SignalAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
/** Optional provider capability tags used for agent/runtime guidance. */
|
||||
capabilities?: string[];
|
||||
/** If false, do not start this Signal account. Default: true. */
|
||||
enabled?: boolean;
|
||||
/** Optional explicit E.164 account for signal-cli. */
|
||||
@@ -650,6 +663,8 @@ export type MSTeamsTeamConfig = {
|
||||
export type MSTeamsConfig = {
|
||||
/** If false, do not start the MS Teams provider. Default: true. */
|
||||
enabled?: boolean;
|
||||
/** Optional provider capability tags used for agent/runtime guidance. */
|
||||
capabilities?: string[];
|
||||
/** Azure Bot App ID (from Azure Bot registration). */
|
||||
appId?: string;
|
||||
/** Azure Bot App Password / Client Secret. */
|
||||
@@ -682,6 +697,8 @@ export type MSTeamsConfig = {
|
||||
export type IMessageAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
/** Optional provider capability tags used for agent/runtime guidance. */
|
||||
capabilities?: string[];
|
||||
/** If false, do not start this iMessage account. Default: true. */
|
||||
enabled?: boolean;
|
||||
/** imsg CLI binary path (default: imsg). */
|
||||
|
||||
@@ -187,6 +187,7 @@ const TelegramGroupSchema = z.object({
|
||||
|
||||
const TelegramAccountSchemaBase = z.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
||||
botToken: z.string().optional(),
|
||||
@@ -279,6 +280,7 @@ const DiscordGuildSchema = z.object({
|
||||
|
||||
const DiscordAccountSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
token: z.string().optional(),
|
||||
groupPolicy: GroupPolicySchema.optional().default("open"),
|
||||
@@ -348,6 +350,7 @@ const SlackChannelSchema = z.object({
|
||||
|
||||
const SlackAccountSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
botToken: z.string().optional(),
|
||||
appToken: z.string().optional(),
|
||||
@@ -390,6 +393,7 @@ const SlackConfigSchema = SlackAccountSchema.extend({
|
||||
|
||||
const SignalAccountSchemaBase = z.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
account: z.string().optional(),
|
||||
httpUrl: z.string().optional(),
|
||||
@@ -438,6 +442,7 @@ const SignalConfigSchema = SignalAccountSchemaBase.extend({
|
||||
|
||||
const IMessageAccountSchemaBase = z.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
cliPath: z.string().optional(),
|
||||
dbPath: z.string().optional(),
|
||||
@@ -506,6 +511,7 @@ const MSTeamsTeamSchema = z.object({
|
||||
const MSTeamsConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
appId: z.string().optional(),
|
||||
appPassword: z.string().optional(),
|
||||
tenantId: z.string().optional(),
|
||||
@@ -1228,6 +1234,7 @@ export const ClawdbotSchema = z.object({
|
||||
z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
messagePrefix: z.string().optional(),
|
||||
/** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */
|
||||
@@ -1268,6 +1275,7 @@ export const ClawdbotSchema = z.object({
|
||||
.optional(),
|
||||
)
|
||||
.optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
||||
messagePrefix: z.string().optional(),
|
||||
selfChatMode: z.boolean().optional(),
|
||||
@@ -1281,6 +1289,8 @@ export const ClawdbotSchema = z.object({
|
||||
actions: z
|
||||
.object({
|
||||
reactions: z.boolean().optional(),
|
||||
sendMessage: z.boolean().optional(),
|
||||
polls: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
groups: z
|
||||
|
||||
Reference in New Issue
Block a user