Skip to content

Commit e5bca08

Browse files
authored
refactor: move Telegram channel implementation to extensions/ (openclaw#45635)
* refactor: move Telegram channel implementation to extensions/telegram/src/ Move all Telegram channel code (123 files + 10 bot/ files + 8 channel plugin files) from src/telegram/ and src/channels/plugins/*/telegram.ts to extensions/telegram/src/. Leave thin re-export shims at original locations so cross-cutting src/ imports continue to resolve. - Fix all relative import paths in moved files (../X/ -> ../../../src/X/) - Fix vi.mock paths in 60 test files - Fix inline typeof import() expressions - Update tsconfig.plugin-sdk.dts.json rootDir to "." for cross-directory DTS - Update write-plugin-sdk-entry-dts.ts for new rootDir structure - Move channel plugin files with correct path remapping * fix: support keyed telegram send deps * fix: sync telegram extension copies with latest main * fix: correct import paths and remove misplaced files in telegram extension * fix: sync outbound-adapter with main (add sendTelegramPayloadMessages) and fix delivery.test import path
1 parent 8746362 commit e5bca08

File tree

230 files changed

+19157
-19204
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

230 files changed

+19157
-19204
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import fs from "node:fs";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { describe, expect, it } from "vitest";
5+
import type { OpenClawConfig } from "../../../src/config/config.js";
6+
import { withEnv } from "../../../src/test-utils/env.js";
7+
import { inspectTelegramAccount } from "./account-inspect.js";
8+
9+
describe("inspectTelegramAccount SecretRef resolution", () => {
10+
it("resolves default env SecretRef templates in read-only status paths", () => {
11+
withEnv({ TG_STATUS_TOKEN: "123:token" }, () => {
12+
const cfg: OpenClawConfig = {
13+
channels: {
14+
telegram: {
15+
botToken: "${TG_STATUS_TOKEN}",
16+
},
17+
},
18+
};
19+
20+
const account = inspectTelegramAccount({ cfg, accountId: "default" });
21+
expect(account.tokenSource).toBe("env");
22+
expect(account.tokenStatus).toBe("available");
23+
expect(account.token).toBe("123:token");
24+
});
25+
});
26+
27+
it("respects env provider allowlists in read-only status paths", () => {
28+
withEnv({ TG_NOT_ALLOWED: "123:token" }, () => {
29+
const cfg: OpenClawConfig = {
30+
secrets: {
31+
defaults: {
32+
env: "secure-env",
33+
},
34+
providers: {
35+
"secure-env": {
36+
source: "env",
37+
allowlist: ["TG_ALLOWED"],
38+
},
39+
},
40+
},
41+
channels: {
42+
telegram: {
43+
botToken: "${TG_NOT_ALLOWED}",
44+
},
45+
},
46+
};
47+
48+
const account = inspectTelegramAccount({ cfg, accountId: "default" });
49+
expect(account.tokenSource).toBe("env");
50+
expect(account.tokenStatus).toBe("configured_unavailable");
51+
expect(account.token).toBe("");
52+
});
53+
});
54+
55+
it("does not read env values for non-env providers", () => {
56+
withEnv({ TG_EXEC_PROVIDER: "123:token" }, () => {
57+
const cfg: OpenClawConfig = {
58+
secrets: {
59+
defaults: {
60+
env: "exec-provider",
61+
},
62+
providers: {
63+
"exec-provider": {
64+
source: "exec",
65+
command: "/usr/bin/env",
66+
},
67+
},
68+
},
69+
channels: {
70+
telegram: {
71+
botToken: "${TG_EXEC_PROVIDER}",
72+
},
73+
},
74+
};
75+
76+
const account = inspectTelegramAccount({ cfg, accountId: "default" });
77+
expect(account.tokenSource).toBe("env");
78+
expect(account.tokenStatus).toBe("configured_unavailable");
79+
expect(account.token).toBe("");
80+
});
81+
});
82+
83+
it.runIf(process.platform !== "win32")(
84+
"treats symlinked token files as configured_unavailable",
85+
() => {
86+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-inspect-"));
87+
const tokenFile = path.join(dir, "token.txt");
88+
const tokenLink = path.join(dir, "token-link.txt");
89+
fs.writeFileSync(tokenFile, "123:token\n", "utf8");
90+
fs.symlinkSync(tokenFile, tokenLink);
91+
92+
const cfg: OpenClawConfig = {
93+
channels: {
94+
telegram: {
95+
tokenFile: tokenLink,
96+
},
97+
},
98+
};
99+
100+
const account = inspectTelegramAccount({ cfg, accountId: "default" });
101+
expect(account.tokenSource).toBe("tokenFile");
102+
expect(account.tokenStatus).toBe("configured_unavailable");
103+
expect(account.token).toBe("");
104+
fs.rmSync(dir, { recursive: true, force: true });
105+
},
106+
);
107+
});
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import type { OpenClawConfig } from "../../../src/config/config.js";
2+
import {
3+
coerceSecretRef,
4+
hasConfiguredSecretInput,
5+
normalizeSecretInputString,
6+
} from "../../../src/config/types.secrets.js";
7+
import type { TelegramAccountConfig } from "../../../src/config/types.telegram.js";
8+
import { tryReadSecretFileSync } from "../../../src/infra/secret-file.js";
9+
import { resolveAccountWithDefaultFallback } from "../../../src/plugin-sdk/account-resolution.js";
10+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
11+
import { resolveDefaultSecretProviderAlias } from "../../../src/secrets/ref-contract.js";
12+
import {
13+
mergeTelegramAccountConfig,
14+
resolveDefaultTelegramAccountId,
15+
resolveTelegramAccountConfig,
16+
} from "./accounts.js";
17+
18+
export type TelegramCredentialStatus = "available" | "configured_unavailable" | "missing";
19+
20+
export type InspectedTelegramAccount = {
21+
accountId: string;
22+
enabled: boolean;
23+
name?: string;
24+
token: string;
25+
tokenSource: "env" | "tokenFile" | "config" | "none";
26+
tokenStatus: TelegramCredentialStatus;
27+
configured: boolean;
28+
config: TelegramAccountConfig;
29+
};
30+
31+
function inspectTokenFile(pathValue: unknown): {
32+
token: string;
33+
tokenSource: "tokenFile" | "none";
34+
tokenStatus: TelegramCredentialStatus;
35+
} | null {
36+
const tokenFile = typeof pathValue === "string" ? pathValue.trim() : "";
37+
if (!tokenFile) {
38+
return null;
39+
}
40+
const token = tryReadSecretFileSync(tokenFile, "Telegram bot token", {
41+
rejectSymlink: true,
42+
});
43+
return {
44+
token: token ?? "",
45+
tokenSource: "tokenFile",
46+
tokenStatus: token ? "available" : "configured_unavailable",
47+
};
48+
}
49+
50+
function canResolveEnvSecretRefInReadOnlyPath(params: {
51+
cfg: OpenClawConfig;
52+
provider: string;
53+
id: string;
54+
}): boolean {
55+
const providerConfig = params.cfg.secrets?.providers?.[params.provider];
56+
if (!providerConfig) {
57+
return params.provider === resolveDefaultSecretProviderAlias(params.cfg, "env");
58+
}
59+
if (providerConfig.source !== "env") {
60+
return false;
61+
}
62+
const allowlist = providerConfig.allowlist;
63+
return !allowlist || allowlist.includes(params.id);
64+
}
65+
66+
function inspectTokenValue(params: { cfg: OpenClawConfig; value: unknown }): {
67+
token: string;
68+
tokenSource: "config" | "env" | "none";
69+
tokenStatus: TelegramCredentialStatus;
70+
} | null {
71+
// Try to resolve env-based SecretRefs from process.env for read-only inspection
72+
const ref = coerceSecretRef(params.value, params.cfg.secrets?.defaults);
73+
if (ref?.source === "env") {
74+
if (
75+
!canResolveEnvSecretRefInReadOnlyPath({
76+
cfg: params.cfg,
77+
provider: ref.provider,
78+
id: ref.id,
79+
})
80+
) {
81+
return {
82+
token: "",
83+
tokenSource: "env",
84+
tokenStatus: "configured_unavailable",
85+
};
86+
}
87+
const envValue = process.env[ref.id];
88+
if (envValue && envValue.trim()) {
89+
return {
90+
token: envValue.trim(),
91+
tokenSource: "env",
92+
tokenStatus: "available",
93+
};
94+
}
95+
return {
96+
token: "",
97+
tokenSource: "env",
98+
tokenStatus: "configured_unavailable",
99+
};
100+
}
101+
const token = normalizeSecretInputString(params.value);
102+
if (token) {
103+
return {
104+
token,
105+
tokenSource: "config",
106+
tokenStatus: "available",
107+
};
108+
}
109+
if (hasConfiguredSecretInput(params.value, params.cfg.secrets?.defaults)) {
110+
return {
111+
token: "",
112+
tokenSource: "config",
113+
tokenStatus: "configured_unavailable",
114+
};
115+
}
116+
return null;
117+
}
118+
119+
function inspectTelegramAccountPrimary(params: {
120+
cfg: OpenClawConfig;
121+
accountId: string;
122+
envToken?: string | null;
123+
}): InspectedTelegramAccount {
124+
const accountId = normalizeAccountId(params.accountId);
125+
const merged = mergeTelegramAccountConfig(params.cfg, accountId);
126+
const enabled = params.cfg.channels?.telegram?.enabled !== false && merged.enabled !== false;
127+
128+
const accountConfig = resolveTelegramAccountConfig(params.cfg, accountId);
129+
const accountTokenFile = inspectTokenFile(accountConfig?.tokenFile);
130+
if (accountTokenFile) {
131+
return {
132+
accountId,
133+
enabled,
134+
name: merged.name?.trim() || undefined,
135+
token: accountTokenFile.token,
136+
tokenSource: accountTokenFile.tokenSource,
137+
tokenStatus: accountTokenFile.tokenStatus,
138+
configured: accountTokenFile.tokenStatus !== "missing",
139+
config: merged,
140+
};
141+
}
142+
143+
const accountToken = inspectTokenValue({ cfg: params.cfg, value: accountConfig?.botToken });
144+
if (accountToken) {
145+
return {
146+
accountId,
147+
enabled,
148+
name: merged.name?.trim() || undefined,
149+
token: accountToken.token,
150+
tokenSource: accountToken.tokenSource,
151+
tokenStatus: accountToken.tokenStatus,
152+
configured: accountToken.tokenStatus !== "missing",
153+
config: merged,
154+
};
155+
}
156+
157+
const channelTokenFile = inspectTokenFile(params.cfg.channels?.telegram?.tokenFile);
158+
if (channelTokenFile) {
159+
return {
160+
accountId,
161+
enabled,
162+
name: merged.name?.trim() || undefined,
163+
token: channelTokenFile.token,
164+
tokenSource: channelTokenFile.tokenSource,
165+
tokenStatus: channelTokenFile.tokenStatus,
166+
configured: channelTokenFile.tokenStatus !== "missing",
167+
config: merged,
168+
};
169+
}
170+
171+
const channelToken = inspectTokenValue({
172+
cfg: params.cfg,
173+
value: params.cfg.channels?.telegram?.botToken,
174+
});
175+
if (channelToken) {
176+
return {
177+
accountId,
178+
enabled,
179+
name: merged.name?.trim() || undefined,
180+
token: channelToken.token,
181+
tokenSource: channelToken.tokenSource,
182+
tokenStatus: channelToken.tokenStatus,
183+
configured: channelToken.tokenStatus !== "missing",
184+
config: merged,
185+
};
186+
}
187+
188+
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
189+
const envToken = allowEnv ? (params.envToken ?? process.env.TELEGRAM_BOT_TOKEN)?.trim() : "";
190+
if (envToken) {
191+
return {
192+
accountId,
193+
enabled,
194+
name: merged.name?.trim() || undefined,
195+
token: envToken,
196+
tokenSource: "env",
197+
tokenStatus: "available",
198+
configured: true,
199+
config: merged,
200+
};
201+
}
202+
203+
return {
204+
accountId,
205+
enabled,
206+
name: merged.name?.trim() || undefined,
207+
token: "",
208+
tokenSource: "none",
209+
tokenStatus: "missing",
210+
configured: false,
211+
config: merged,
212+
};
213+
}
214+
215+
export function inspectTelegramAccount(params: {
216+
cfg: OpenClawConfig;
217+
accountId?: string | null;
218+
envToken?: string | null;
219+
}): InspectedTelegramAccount {
220+
return resolveAccountWithDefaultFallback({
221+
accountId: params.accountId,
222+
normalizeAccountId,
223+
resolvePrimary: (accountId) =>
224+
inspectTelegramAccountPrimary({
225+
cfg: params.cfg,
226+
accountId,
227+
envToken: params.envToken,
228+
}),
229+
hasCredential: (account) => account.tokenSource !== "none",
230+
resolveDefaultAccountId: () => resolveDefaultTelegramAccountId(params.cfg),
231+
});
232+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2-
import type { OpenClawConfig } from "../config/config.js";
3-
import { withEnv } from "../test-utils/env.js";
2+
import type { OpenClawConfig } from "../../../src/config/config.js";
3+
import { withEnv } from "../../../src/test-utils/env.js";
44
import {
55
listTelegramAccountIds,
66
resetMissingDefaultWarnFlag,
@@ -29,7 +29,7 @@ function resolveAccountWithEnv(
2929
return withEnv(env, () => resolveTelegramAccount({ cfg, ...(accountId ? { accountId } : {}) }));
3030
}
3131

32-
vi.mock("../logging/subsystem.js", () => ({
32+
vi.mock("../../../src/logging/subsystem.js", () => ({
3333
createSubsystemLogger: () => {
3434
const logger = {
3535
warn: warnMock,

0 commit comments

Comments
 (0)