fix(telegram): normalize bot endpoint api roots

This commit is contained in:
Peter Steinberger
2026-04-28 06:32:51 +01:00
parent 27e313053c
commit 59a4d7fb06
17 changed files with 366 additions and 19 deletions

View File

@@ -16,6 +16,10 @@ Docs: https://docs.openclaw.ai
- Memory/Dreaming: retry Dream Diary once with the session default when a configured dreaming model is unavailable, while leaving subagent trust and allowlist errors visible instead of silently masking configuration problems. Refs #67409 and #69209. Thanks @Ghiggins18 and @everySympathy.
- Feishu/inbound files: recover CJK filenames from plain `Content-Disposition: filename=` download headers when Feishu exposes UTF-8 bytes through Latin-1 header decoding, while leaving valid Latin-1 and JSON-derived names unchanged. (#48578, #50435, #59431) Thanks @alex-xuweilong, @lishuaigit, and @DoChaoing.
### Fixes
- Channels/Telegram: normalize accidental full `/bot<TOKEN>` Telegram `apiRoot` values at runtime and teach `openclaw doctor --fix` to remove the suffix, so startup control calls no longer 404 when direct Bot API curl commands work. Fixes #55387. Thanks @brendanmatthewjones-cmyk, @techfindubai-ux, and @Sivlerback-Chris.
## 2026.4.27
### Changes

View File

@@ -1,4 +1,4 @@
b80df5537b3569826a23b8176910476493ae569b65f9b4c2fa9e0ad415eb4a2b config-baseline.json
9caccd04afca25d18cfcc4a66bdc30c995f5ec51eaa764c076ce58c9af11a7bf config-baseline.json
8530c8fd54e04a2ab7f6704195f9959311e289ae122ebd8e27af236de435fef9 config-baseline.core.json
c4f07c228d4f07e7afafa5b600b4a80f5b26aaed7267c7287a64d04a527be8e8 config-baseline.channel.json
a9f058ee9616e189dab7fc223e1207a49ae52b8490b8028935c9d0a2b16f81b2 config-baseline.channel.json
1f5592bfd141ba1e982ce31763a253c10afb080ab4ea2b6538299b114e29cee1 config-baseline.plugin.json

View File

@@ -364,6 +364,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
Common setup failures:
- `setMyCommands failed` with `BOT_COMMANDS_TOO_MUCH` means the Telegram menu still overflowed after trimming; reduce plugin/skill/custom commands or disable `channels.telegram.commands.native`.
- `deleteWebhook`, `deleteMyCommands`, or `setMyCommands` failing with `404: Not Found` while direct Bot API curl commands work can mean `channels.telegram.apiRoot` was set to the full `/bot<TOKEN>` endpoint. `apiRoot` must be only the Bot API root, and `openclaw doctor --fix` removes an accidental trailing `/bot<TOKEN>`.
- `setMyCommands failed` with network/fetch errors usually means outbound DNS/HTTPS to `api.telegram.org` is blocked.
### Device pairing commands (`device-pair` plugin)
@@ -925,6 +926,7 @@ Primary reference: [Configuration reference - Telegram](/gateway/config-channels
- streaming: `streaming` (preview), `streaming.preview.toolProgress`, `blockStreaming`
- formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix`
- media/network: `mediaMaxMb`, `timeoutSeconds`, `pollingStallThresholdMs`, `retry`, `network.autoSelectFamily`, `network.dangerouslyAllowPrivateNetwork`, `proxy`
- custom API root: `apiRoot` (Bot API root only; do not include `/bot<TOKEN>`)
- webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost`
- actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker`
- reactions: `reactionNotifications`, `reactionLevel`

View File

@@ -195,6 +195,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
autoSelectFamily: true,
dnsResultOrder: "ipv4first",
},
apiRoot: "https://api.telegram.org",
proxy: "socks5://localhost:9050",
webhookUrl: "https://example.com/telegram-webhook",
webhookSecret: "secret",
@@ -205,6 +206,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
```
- Bot token: `channels.telegram.botToken` or `channels.telegram.tokenFile` (regular file only; symlinks rejected), with `TELEGRAM_BOT_TOKEN` as fallback for the default account.
- `apiRoot` is the Telegram Bot API root only. Use `https://api.telegram.org` or your self-hosted/proxy root, not `https://api.telegram.org/bot<TOKEN>`; `openclaw doctor --fix` removes an accidental trailing `/bot<TOKEN>` suffix.
- Optional `channels.telegram.defaultAccount` overrides default account selection when it matches a configured account id.
- In multi-account setups (2+ account ids), set an explicit default (`channels.telegram.defaultAccount` or `channels.telegram.accounts.default`) to avoid fallback routing; `openclaw doctor` warns when this is missing or invalid.
- `configWrites: false` blocks Telegram-initiated config writes (supergroup ID migrations, `/config set|unset`).

View File

@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import {
DEFAULT_TELEGRAM_API_ROOT,
hasTelegramBotEndpointApiRoot,
normalizeTelegramApiRoot,
} from "./api-root.js";
describe("telegram api root", () => {
it("defaults to the public Telegram Bot API root", () => {
expect(normalizeTelegramApiRoot()).toBe(DEFAULT_TELEGRAM_API_ROOT);
expect(normalizeTelegramApiRoot(" ")).toBe(DEFAULT_TELEGRAM_API_ROOT);
});
it("keeps custom Bot API roots without a bot-token endpoint", () => {
expect(normalizeTelegramApiRoot("https://telegram.internal:8443/custom-bot-api/")).toBe(
"https://telegram.internal:8443/custom-bot-api",
);
expect(hasTelegramBotEndpointApiRoot("https://telegram.internal:8443/custom-bot-api/")).toBe(
false,
);
});
it("strips a full bot endpoint from apiRoot", () => {
const root = "https://api.telegram.org/bot123456:ABC_def-ghi/";
expect(hasTelegramBotEndpointApiRoot(root)).toBe(true);
expect(normalizeTelegramApiRoot(root)).toBe("https://api.telegram.org");
});
it("strips only terminal bot-token endpoint segments", () => {
expect(normalizeTelegramApiRoot("https://proxy.example.com/custom/bot123456:ABC_def")).toBe(
"https://proxy.example.com/custom",
);
expect(normalizeTelegramApiRoot("https://proxy.example.com/bot123456")).toBe(
"https://proxy.example.com/bot123456",
);
});
});

View File

@@ -0,0 +1,49 @@
export const DEFAULT_TELEGRAM_API_ROOT = "https://api.telegram.org";
const TELEGRAM_BOT_ENDPOINT_SEGMENT_RE = /^bot\d+:[^/]+$/u;
function isTelegramBotEndpointSegment(segment: string): boolean {
try {
return TELEGRAM_BOT_ENDPOINT_SEGMENT_RE.test(decodeURIComponent(segment));
} catch {
return TELEGRAM_BOT_ENDPOINT_SEGMENT_RE.test(segment);
}
}
export function normalizeTelegramApiRoot(apiRoot?: string): string {
const trimmed = apiRoot?.trim();
if (!trimmed) {
return DEFAULT_TELEGRAM_API_ROOT;
}
let normalized = trimmed.replace(/\/+$/u, "");
try {
const url = new URL(normalized);
const segments = url.pathname.split("/").filter(Boolean);
if (segments.length > 0 && isTelegramBotEndpointSegment(segments[segments.length - 1] ?? "")) {
segments.pop();
url.pathname = segments.length > 0 ? `/${segments.join("/")}` : "/";
url.search = "";
url.hash = "";
normalized = url.toString().replace(/\/+$/u, "");
}
} catch {
// Config validation catches invalid URLs; keep legacy runtime behavior for
// callers that reached this helper with unchecked input.
}
return normalized;
}
export function hasTelegramBotEndpointApiRoot(apiRoot: unknown): boolean {
if (typeof apiRoot !== "string" || !apiRoot.trim()) {
return false;
}
try {
const url = new URL(apiRoot.trim());
const segments = url.pathname.split("/").filter(Boolean);
const last = segments[segments.length - 1];
return Boolean(last && isTelegramBotEndpointSegment(last));
} catch {
return false;
}
}

View File

@@ -24,6 +24,7 @@ import {
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import { resolveTelegramAccount } from "./accounts.js";
import { normalizeTelegramApiRoot } from "./api-root.js";
import type { TelegramBotDeps } from "./bot-deps.js";
import { registerTelegramHandlers } from "./bot-handlers.runtime.js";
import { createTelegramMessageProcessor } from "./bot-message.js";
@@ -296,12 +297,13 @@ export function createTelegramBotCore(
? Math.max(1, Math.floor(telegramCfg.timeoutSeconds))
: undefined;
const apiRoot = normalizeOptionalString(telegramCfg.apiRoot);
const normalizedApiRoot = apiRoot ? normalizeTelegramApiRoot(apiRoot) : undefined;
const client: ApiClientOptions | undefined =
finalFetch || timeoutSeconds || apiRoot
finalFetch || timeoutSeconds || normalizedApiRoot
? {
...(finalFetch ? { fetch: asTelegramClientFetch(finalFetch) } : {}),
...(timeoutSeconds ? { timeoutSeconds } : {}),
...(apiRoot ? { apiRoot } : {}),
...(normalizedApiRoot ? { apiRoot: normalizedApiRoot } : {}),
}
: undefined;

View File

@@ -247,6 +247,28 @@ describe("createTelegramBot", () => {
}),
);
});
it("normalizes full Telegram bot endpoint apiRoot before passing it to grammY", () => {
loadConfig.mockReturnValue({
channels: {
telegram: {
dmPolicy: "open",
allowFrom: ["*"],
apiRoot: "https://api.telegram.org/bot123456:ABC/",
},
},
});
createTelegramBot({ token: "tok" });
expect(botCtorSpy).toHaveBeenCalledWith(
"tok",
expect.objectContaining({
client: expect.objectContaining({ apiRoot: "https://api.telegram.org" }),
}),
);
});
it("sequentializes updates by chat and thread", () => {
createTelegramBot({ token: "tok" });
expect(sequentializeSpy).toHaveBeenCalledTimes(1);

View File

@@ -103,7 +103,7 @@ export const telegramChannelConfigUiHints = {
},
apiRoot: {
label: "Telegram API Root URL",
help: "Custom Telegram Bot API root URL. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked.",
help: "Custom Telegram Bot API root URL. Use the API root only (for example https://api.telegram.org), not a full /bot<TOKEN> endpoint. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked.",
},
trustedLocalFileRoots: {
label: "Telegram Trusted Local File Roots",

View File

@@ -2,9 +2,12 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
collectTelegramInvalidAllowFromWarnings,
collectTelegramApiRootWarnings,
collectTelegramEmptyAllowlistExtraWarnings,
collectTelegramGroupPolicyWarnings,
maybeRepairTelegramApiRoots,
maybeRepairTelegramAllowFromUsernames,
scanTelegramBotEndpointApiRoots,
scanTelegramInvalidAllowFromEntries,
telegramDoctor,
} from "./doctor.js";
@@ -288,4 +291,68 @@ describe("telegram doctor", () => {
expect(warnings[0]).toContain("invalid sender entries");
expect(warnings[1]).toContain("openclaw doctor --fix");
});
it("warns and repairs Telegram apiRoot values that include the bot endpoint", () => {
const cfg = {
channels: {
telegram: {
apiRoot: "https://api.telegram.org/bot123456:ABC",
accounts: {
work: {
apiRoot: "https://proxy.example.test/custom/bot234567:DEF/",
},
},
},
},
} as unknown as OpenClawConfig;
const hits = scanTelegramBotEndpointApiRoots(cfg);
expect(hits.map((hit) => hit.path)).toEqual([
"channels.telegram.apiRoot",
"channels.telegram.accounts.work.apiRoot",
]);
expect(
collectTelegramApiRootWarnings({ hits, doctorFixCommand: "openclaw doctor --fix" }),
).toContain(
"- channels.telegram.apiRoot points at a full Telegram bot endpoint; apiRoot must be the Bot API root only. This can make startup calls like deleteWebhook, deleteMyCommands, and setMyCommands fail with 404 even when direct curl commands work.",
);
const repaired = maybeRepairTelegramApiRoots(cfg);
expect(repaired.config.channels?.telegram?.apiRoot).toBe("https://api.telegram.org");
expect(repaired.config.channels?.telegram?.accounts?.work?.apiRoot).toBe(
"https://proxy.example.test/custom",
);
expect(repaired.changes).toEqual([
"- channels.telegram.apiRoot: removed trailing /bot<TOKEN> from Telegram apiRoot.",
"- channels.telegram.accounts.work.apiRoot: removed trailing /bot<TOKEN> from Telegram apiRoot.",
]);
});
it("wires apiRoot preview warnings and repair through the doctor adapter", async () => {
const cfg = {
channels: {
telegram: {
apiRoot: "https://api.telegram.org/bot123456:ABC",
},
},
} as unknown as OpenClawConfig;
expect(
await telegramDoctor.collectPreviewWarnings?.({
cfg,
doctorFixCommand: "openclaw doctor --fix",
}),
).toContain(
"- channels.telegram.apiRoot points at a full Telegram bot endpoint; apiRoot must be the Bot API root only. This can make startup calls like deleteWebhook, deleteMyCommands, and setMyCommands fail with 404 even when direct curl commands work.",
);
const repaired = await telegramDoctor.repairConfig?.({
cfg,
doctorFixCommand: "openclaw doctor --fix",
});
expect(repaired?.config.channels?.telegram?.apiRoot).toBe("https://api.telegram.org");
expect(repaired?.changes).toEqual([
"- channels.telegram.apiRoot: removed trailing /bot<TOKEN> from Telegram apiRoot.",
]);
});
});

View File

@@ -9,12 +9,19 @@ import { inspectTelegramAccount } from "./account-inspect.js";
import { listTelegramAccountIds, resolveTelegramAccount } from "./accounts.js";
import { isNumericTelegramSenderUserId, normalizeTelegramAllowFromEntry } from "./allow-from.js";
import { lookupTelegramChatId } from "./api-fetch.js";
import { hasTelegramBotEndpointApiRoot, normalizeTelegramApiRoot } from "./api-root.js";
import {
legacyConfigRules as TELEGRAM_LEGACY_CONFIG_RULES,
normalizeCompatibilityConfig as normalizeTelegramCompatibilityConfig,
} from "./doctor-contract.js";
type TelegramAllowFromInvalidHit = { path: string; entry: string };
type TelegramApiRootBotEndpointHit = {
path: string;
pathSegments: string[];
value: string;
normalized: string;
};
type DoctorAllowFromList = Array<string | number>;
type DoctorAccountRecord = Record<string, unknown>;
@@ -40,13 +47,21 @@ function hasAllowFromEntries(values?: DoctorAllowFromList): boolean {
function collectTelegramAccountScopes(
cfg: OpenClawConfig,
): Array<{ prefix: string; account: Record<string, unknown> }> {
const scopes: Array<{ prefix: string; account: Record<string, unknown> }> = [];
): Array<{ prefix: string; pathSegments: string[]; account: Record<string, unknown> }> {
const scopes: Array<{
prefix: string;
pathSegments: string[];
account: Record<string, unknown>;
}> = [];
const telegram = asObjectRecord((cfg.channels as Record<string, unknown> | undefined)?.telegram);
if (!telegram) {
return scopes;
}
scopes.push({ prefix: "channels.telegram", account: telegram });
scopes.push({
prefix: "channels.telegram",
pathSegments: ["channels", "telegram"],
account: telegram,
});
const accounts = asObjectRecord(telegram.accounts);
if (!accounts) {
return scopes;
@@ -54,7 +69,11 @@ function collectTelegramAccountScopes(
for (const key of Object.keys(accounts)) {
const account = asObjectRecord(accounts[key]);
if (account) {
scopes.push({ prefix: `channels.telegram.accounts.${key}`, account });
scopes.push({
prefix: `channels.telegram.accounts.${key}`,
pathSegments: ["channels", "telegram", "accounts", key],
account,
});
}
}
return scopes;
@@ -140,6 +159,83 @@ export function collectTelegramInvalidAllowFromWarnings(params: {
];
}
export function scanTelegramBotEndpointApiRoots(
cfg: OpenClawConfig,
): TelegramApiRootBotEndpointHit[] {
const hits: TelegramApiRootBotEndpointHit[] = [];
for (const scope of collectTelegramAccountScopes(cfg)) {
const value = scope.account.apiRoot;
if (typeof value !== "string" || !hasTelegramBotEndpointApiRoot(value)) {
continue;
}
hits.push({
path: `${scope.prefix}.apiRoot`,
pathSegments: [...scope.pathSegments, "apiRoot"],
value,
normalized: normalizeTelegramApiRoot(value),
});
}
return hits;
}
export function collectTelegramApiRootWarnings(params: {
hits: TelegramApiRootBotEndpointHit[];
doctorFixCommand: string;
}): string[] {
if (params.hits.length === 0) {
return [];
}
const samplePath = sanitizeForLog(params.hits[0]?.path ?? "channels.telegram.apiRoot");
return [
`- ${samplePath} points at a full Telegram bot endpoint; apiRoot must be the Bot API root only. This can make startup calls like deleteWebhook, deleteMyCommands, and setMyCommands fail with 404 even when direct curl commands work.`,
`- Run "${params.doctorFixCommand}" to remove the trailing /bot<TOKEN> path from Telegram apiRoot.`,
];
}
export function maybeRepairTelegramApiRoots(cfg: OpenClawConfig): {
config: OpenClawConfig;
changes: string[];
} {
const hits = scanTelegramBotEndpointApiRoots(cfg);
if (hits.length === 0) {
return { config: cfg, changes: [] };
}
const next = structuredClone(cfg);
const apply = (path: string[], normalized: string) => {
let target: Record<string, unknown> | null = next as Record<string, unknown>;
for (const segment of path.slice(0, -1)) {
target = asObjectRecord(target?.[segment]);
if (!target) {
return;
}
}
target[path[path.length - 1] ?? "apiRoot"] = normalized;
};
for (const hit of hits) {
apply(hit.pathSegments, hit.normalized);
}
return {
config: next,
changes: hits.map(
(hit) => `- ${sanitizeForLog(hit.path)}: removed trailing /bot<TOKEN> from Telegram apiRoot.`,
),
};
}
async function repairTelegramConfig(params: { cfg: OpenClawConfig }): Promise<{
config: OpenClawConfig;
changes: string[];
}> {
const apiRootRepair = maybeRepairTelegramApiRoots(params.cfg);
const allowFromRepair = await maybeRepairTelegramAllowFromUsernames(apiRootRepair.config);
return {
config: allowFromRepair.config,
changes: [...apiRootRepair.changes, ...allowFromRepair.changes],
};
}
export async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig): Promise<{
config: OpenClawConfig;
changes: string[];
@@ -376,12 +472,17 @@ export function collectTelegramEmptyAllowlistExtraWarnings(
export const telegramDoctor: ChannelDoctorAdapter = {
legacyConfigRules: TELEGRAM_LEGACY_CONFIG_RULES,
normalizeCompatibilityConfig: normalizeTelegramCompatibilityConfig,
collectPreviewWarnings: ({ cfg, doctorFixCommand }) =>
collectTelegramInvalidAllowFromWarnings({
collectPreviewWarnings: ({ cfg, doctorFixCommand }) => [
...collectTelegramInvalidAllowFromWarnings({
hits: scanTelegramInvalidAllowFromEntries(cfg),
doctorFixCommand,
}),
repairConfig: async ({ cfg }) => await maybeRepairTelegramAllowFromUsernames(cfg),
...collectTelegramApiRootWarnings({
hits: scanTelegramBotEndpointApiRoots(cfg),
doctorFixCommand,
}),
],
repairConfig: async ({ cfg }) => await repairTelegramConfig({ cfg }),
collectEmptyAllowlistExtraWarnings: collectTelegramEmptyAllowlistExtraWarnings,
shouldSkipDefaultEmptyGroupAllowlistWarning: (params) => params.channelName === "telegram",
};

View File

@@ -98,6 +98,7 @@ vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
}));
let resolveTelegramFetch: typeof import("./fetch.js").resolveTelegramFetch;
let resolveTelegramApiBase: typeof import("./fetch.js").resolveTelegramApiBase;
let resolveTelegramTransport: typeof import("./fetch.js").resolveTelegramTransport;
type TelegramDispatcherPolicy = NonNullable<
@@ -105,7 +106,8 @@ type TelegramDispatcherPolicy = NonNullable<
>[number]["dispatcherPolicy"];
beforeAll(async () => {
({ resolveTelegramFetch, resolveTelegramTransport } = await import("./fetch.js"));
({ resolveTelegramApiBase, resolveTelegramFetch, resolveTelegramTransport } =
await import("./fetch.js"));
});
beforeEach(() => {
@@ -308,6 +310,12 @@ afterEach(() => {
});
describe("resolveTelegramFetch", () => {
it("normalizes a full bot endpoint apiRoot before callers append bot paths", () => {
expect(resolveTelegramApiBase("https://api.telegram.org/bot123456:ABC/")).toBe(
"https://api.telegram.org",
);
});
it("wraps proxy fetches and leaves retry policy to caller-provided fetch", async () => {
const proxyFetch = vi.fn(async () => ({ ok: true }) as Response) as unknown as typeof fetch;

View File

@@ -16,6 +16,7 @@ import { resolveRequestUrl } from "openclaw/plugin-sdk/request-url";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { Agent, EnvHttpProxyAgent, ProxyAgent, fetch as undiciFetch } from "undici";
import { normalizeTelegramApiRoot } from "./api-root.js";
import {
resolveTelegramAutoSelectFamilyDecision,
resolveTelegramDnsResultOrderDecision,
@@ -730,6 +731,5 @@ export function resolveTelegramFetch(
* Returns a trimmed URL without trailing slash, or the standard default.
*/
export function resolveTelegramApiBase(apiRoot?: string): string {
const trimmed = apiRoot?.trim();
return trimmed ? trimmed.replace(/\/+$/, "") : `https://${TELEGRAM_API_HOSTNAME}`;
return normalizeTelegramApiRoot(apiRoot);
}

View File

@@ -491,6 +491,31 @@ describe("sendMessageTelegram", () => {
}
});
it("normalizes full Telegram bot endpoint apiRoot before send clients reach grammY", async () => {
const cfg = {
channels: {
telegram: {
accounts: {
foo: {
apiRoot: "https://api.telegram.org/bot123456:ABC/",
},
},
},
},
};
loadConfig.mockReturnValue(cfg);
botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } });
await sendMessageTelegram("123", "hi", { cfg, token: "tok", accountId: "foo" });
expect(botCtorSpy).toHaveBeenCalledWith(
"tok",
expect.objectContaining({
client: expect.objectContaining({ apiRoot: "https://api.telegram.org" }),
}),
);
});
it("falls back to plain text when Telegram rejects HTML and preserves send params", async () => {
const parseErr = new Error(
"400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 9",

View File

@@ -10,6 +10,7 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { normalizeOptionalString, redactSensitiveText } from "openclaw/plugin-sdk/text-runtime";
import { type ResolvedTelegramAccount, resolveTelegramAccount } from "./accounts.js";
import { withTelegramApiErrorLogging } from "./api-logging.js";
import { normalizeTelegramApiRoot } from "./api-root.js";
import { buildTypingThreadParams } from "./bot/helpers.js";
import type { TelegramInlineButtons } from "./button-types.js";
import { splitTelegramCaption } from "./caption.js";
@@ -262,15 +263,16 @@ function resolveTelegramClientOptions(
const proxyUrl = normalizeOptionalString(account.config.proxy);
const proxyFetch = proxyUrl ? makeProxyFetch(proxyUrl) : undefined;
const apiRoot = normalizeOptionalString(account.config.apiRoot);
const normalizedApiRoot = apiRoot ? normalizeTelegramApiRoot(apiRoot) : undefined;
const fetchImpl = resolveTelegramFetch(proxyFetch, {
network: account.config.network,
});
const clientOptions =
fetchImpl || timeoutSeconds || apiRoot
fetchImpl || timeoutSeconds || normalizedApiRoot
? {
...(fetchImpl ? { fetch: asTelegramClientFetch(fetchImpl) } : {}),
...(timeoutSeconds ? { timeoutSeconds } : {}),
...(apiRoot ? { apiRoot } : {}),
...(normalizedApiRoot ? { apiRoot: normalizedApiRoot } : {}),
}
: undefined;
if (cacheKey) {

View File

@@ -7371,6 +7371,25 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
{
type: "boolean",
},
{
type: "object",
properties: {
mode: {
type: "string",
enum: ["partial", "quiet", "off"],
},
preview: {
type: "object",
properties: {
toolProgress: {
type: "boolean",
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
],
},
replyToMode: {
@@ -10010,6 +10029,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
enum: ["off", "partial"],
},
c2cStreamApi: {
type: "boolean",
},
},
required: ["mode"],
additionalProperties: {},
@@ -10250,6 +10272,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
enum: ["off", "partial"],
},
c2cStreamApi: {
type: "boolean",
},
},
required: ["mode"],
additionalProperties: {},
@@ -15152,7 +15177,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
},
apiRoot: {
label: "Telegram API Root URL",
help: "Custom Telegram Bot API root URL. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked.",
help: "Custom Telegram Bot API root URL. Use the API root only (for example https://api.telegram.org), not a full /bot<TOKEN> endpoint. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked.",
},
trustedLocalFileRoots: {
label: "Telegram Trusted Local File Roots",

View File

@@ -212,7 +212,7 @@ export type TelegramAccountConfig = {
* Telegram expects unicode emoji (e.g., "👀") rather than shortcodes.
*/
ackReaction?: string;
/** Custom Telegram Bot API root URL (e.g. "https://my-proxy.example.com" or a local Bot API server). */
/** Custom Telegram Bot API root URL (e.g. "https://my-proxy.example.com" or a local Bot API server), not a /bot<TOKEN> endpoint. */
apiRoot?: string;
/** Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. */
trustedLocalFileRoots?: string[];