Merge c463b13ef7 into da71eaebd2
This commit is contained in:
commit
f27359d4e7
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@ -68,6 +68,11 @@
|
|||||||
- "src/telegram/**"
|
- "src/telegram/**"
|
||||||
- "extensions/telegram/**"
|
- "extensions/telegram/**"
|
||||||
- "docs/channels/telegram.md"
|
- "docs/channels/telegram.md"
|
||||||
|
"channel: telegram-user":
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- "extensions/telegram-user/**"
|
||||||
|
- "docs/channels/telegram-user.md"
|
||||||
"channel: tlon":
|
"channel: tlon":
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
|
|||||||
@ -13,6 +13,7 @@ Text is supported everywhere; media and reactions vary by channel.
|
|||||||
|
|
||||||
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
|
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
|
||||||
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
|
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
|
||||||
|
- [Telegram User](/channels/telegram-user) — MTProto user account with DM + group support (plugin, installed separately).
|
||||||
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
|
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
|
||||||
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
|
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
|
||||||
- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.
|
- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.
|
||||||
|
|||||||
68
docs/channels/telegram-user.md
Normal file
68
docs/channels/telegram-user.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
summary: "Connect a Telegram user account via MTProto (DMs + groups)"
|
||||||
|
---
|
||||||
|
# Telegram User
|
||||||
|
|
||||||
|
Telegram User connects OpenClaw to a **personal Telegram account** using MTProto.
|
||||||
|
Use this when you need user-level DMs or want to message from your own account in groups.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Telegram API ID + API hash from [my.telegram.org](https://my.telegram.org).
|
||||||
|
- The `telegram-user` plugin installed.
|
||||||
|
|
||||||
|
## Install the plugin
|
||||||
|
|
||||||
|
If the plugin is not bundled, install it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw plugins install @openclaw/telegram-user
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configure
|
||||||
|
|
||||||
|
You can store credentials in config or use env vars.
|
||||||
|
|
||||||
|
Option A: env vars (default account only)
|
||||||
|
```bash
|
||||||
|
export TELEGRAM_USER_API_ID="123456"
|
||||||
|
export TELEGRAM_USER_API_HASH="your_api_hash"
|
||||||
|
openclaw channels add --channel telegram-user --use-env
|
||||||
|
```
|
||||||
|
|
||||||
|
Option B: config
|
||||||
|
```bash
|
||||||
|
openclaw channels add --channel telegram-user --api-id 123456 --api-hash your_api_hash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Login (QR or phone code)
|
||||||
|
|
||||||
|
QR login (default):
|
||||||
|
```bash
|
||||||
|
openclaw channels login --channel telegram-user
|
||||||
|
```
|
||||||
|
|
||||||
|
Phone login:
|
||||||
|
```bash
|
||||||
|
export TELEGRAM_USER_PHONE="+15551234567"
|
||||||
|
openclaw channels login --channel telegram-user
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional env helpers:
|
||||||
|
- `TELEGRAM_USER_CODE` (one-time code)
|
||||||
|
- `TELEGRAM_USER_PASSWORD` (2FA password)
|
||||||
|
|
||||||
|
## Security (DM policy)
|
||||||
|
|
||||||
|
By default, DMs are protected with pairing. Approve requests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw pairing approve telegram-user <code>
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Pairing](/start/pairing) for details.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Broadcast channels are not supported.
|
||||||
|
- Calls are not supported.
|
||||||
1
extensions/telegram-user/.gitignore
vendored
Normal file
1
extensions/telegram-user/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
18
extensions/telegram-user/index.ts
Normal file
18
extensions/telegram-user/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
import { telegramUserPlugin } from "./src/channel.js";
|
||||||
|
import { setTelegramUserRuntime } from "./src/runtime.js";
|
||||||
|
|
||||||
|
const plugin = {
|
||||||
|
id: "telegram-user",
|
||||||
|
name: "Telegram User",
|
||||||
|
description: "Telegram MTProto user channel plugin",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
|
register(api: OpenClawPluginApi) {
|
||||||
|
setTelegramUserRuntime(api.runtime);
|
||||||
|
api.registerChannel({ plugin: telegramUserPlugin });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
||||||
11
extensions/telegram-user/openclaw.plugin.json
Normal file
11
extensions/telegram-user/openclaw.plugin.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "telegram-user",
|
||||||
|
"channels": [
|
||||||
|
"telegram-user"
|
||||||
|
],
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
extensions/telegram-user/package.json
Normal file
41
extensions/telegram-user/package.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "@openclaw/telegram-user",
|
||||||
|
"version": "2026.1.29",
|
||||||
|
"type": "module",
|
||||||
|
"description": "OpenClaw Telegram user (MTProto) channel plugin",
|
||||||
|
"openclaw": {
|
||||||
|
"extensions": [
|
||||||
|
"./index.ts"
|
||||||
|
],
|
||||||
|
"channel": {
|
||||||
|
"id": "telegram-user",
|
||||||
|
"label": "Telegram User",
|
||||||
|
"selectionLabel": "Telegram User (MTProto)",
|
||||||
|
"detailLabel": "Telegram User",
|
||||||
|
"docsPath": "/channels/telegram-user",
|
||||||
|
"docsLabel": "telegram-user",
|
||||||
|
"blurb": "login as a Telegram user via QR or phone code; supports DMs + groups.",
|
||||||
|
"order": 12,
|
||||||
|
"quickstartAllowFrom": true
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"npmSpec": "@openclaw/telegram-user",
|
||||||
|
"localPath": "extensions/telegram-user",
|
||||||
|
"defaultChoice": "npm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mtcute/core": "^0.27.6",
|
||||||
|
"@mtcute/dispatcher": "^0.27.6",
|
||||||
|
"@mtcute/node": "^0.27.6",
|
||||||
|
"@clack/prompts": "^0.8.2",
|
||||||
|
"qrcode-terminal": "^0.12.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"openclaw": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"openclaw": ">=2026.1.29"
|
||||||
|
}
|
||||||
|
}
|
||||||
95
extensions/telegram-user/src/accounts.ts
Normal file
95
extensions/telegram-user/src/accounts.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import type { CoreConfig, TelegramUserAccountConfig } from "./types.js";
|
||||||
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
export type TelegramUserCredentials = {
|
||||||
|
apiId?: number;
|
||||||
|
apiHash?: string;
|
||||||
|
apiIdSource: "env" | "config" | "none";
|
||||||
|
apiHashSource: "env" | "config" | "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResolvedTelegramUserAccount = {
|
||||||
|
accountId: string;
|
||||||
|
enabled: boolean;
|
||||||
|
name?: string;
|
||||||
|
credentials: TelegramUserCredentials;
|
||||||
|
config: TelegramUserAccountConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveAccountConfig(
|
||||||
|
cfg: CoreConfig,
|
||||||
|
accountId: string,
|
||||||
|
): TelegramUserAccountConfig | undefined {
|
||||||
|
const accounts = cfg.channels?.["telegram-user"]?.accounts;
|
||||||
|
if (!accounts || typeof accounts !== "object") return undefined;
|
||||||
|
const direct = accounts[accountId] as TelegramUserAccountConfig | undefined;
|
||||||
|
if (direct) return direct;
|
||||||
|
const normalized = normalizeAccountId(accountId);
|
||||||
|
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
|
||||||
|
return matchKey ? (accounts[matchKey] as TelegramUserAccountConfig | undefined) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeTelegramUserAccountConfig(
|
||||||
|
cfg: CoreConfig,
|
||||||
|
accountId: string,
|
||||||
|
): TelegramUserAccountConfig {
|
||||||
|
const { accounts: _ignored, ...base } = (cfg.channels?.["telegram-user"] ??
|
||||||
|
{}) as TelegramUserAccountConfig & { accounts?: unknown };
|
||||||
|
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||||
|
return { ...base, ...account };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCredentials(cfg: CoreConfig, accountId: string): TelegramUserCredentials {
|
||||||
|
const merged = mergeTelegramUserAccountConfig(cfg, accountId);
|
||||||
|
const envApiId =
|
||||||
|
accountId === DEFAULT_ACCOUNT_ID
|
||||||
|
? Number.parseInt(process.env.TELEGRAM_USER_API_ID ?? "", 10)
|
||||||
|
: Number.NaN;
|
||||||
|
const envApiHash =
|
||||||
|
accountId === DEFAULT_ACCOUNT_ID ? process.env.TELEGRAM_USER_API_HASH?.trim() : undefined;
|
||||||
|
const apiId =
|
||||||
|
Number.isFinite(envApiId) && envApiId > 0 ? envApiId : merged.apiId ?? undefined;
|
||||||
|
const apiHash = envApiHash || merged.apiHash?.trim();
|
||||||
|
return {
|
||||||
|
apiId,
|
||||||
|
apiHash,
|
||||||
|
apiIdSource:
|
||||||
|
Number.isFinite(envApiId) && envApiId > 0
|
||||||
|
? "env"
|
||||||
|
: merged.apiId
|
||||||
|
? "config"
|
||||||
|
: "none",
|
||||||
|
apiHashSource: envApiHash ? "env" : merged.apiHash ? "config" : "none",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listTelegramUserAccountIds(cfg: CoreConfig): string[] {
|
||||||
|
const accounts = cfg.channels?.["telegram-user"]?.accounts;
|
||||||
|
const ids = accounts ? Object.keys(accounts).filter(Boolean) : [];
|
||||||
|
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
||||||
|
if (!ids.includes(DEFAULT_ACCOUNT_ID)) ids.push(DEFAULT_ACCOUNT_ID);
|
||||||
|
return ids.sort((a, b) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDefaultTelegramUserAccountId(cfg: CoreConfig): string {
|
||||||
|
const ids = listTelegramUserAccountIds(cfg);
|
||||||
|
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
||||||
|
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTelegramUserAccount(params: {
|
||||||
|
cfg: CoreConfig;
|
||||||
|
accountId?: string | null;
|
||||||
|
}): ResolvedTelegramUserAccount {
|
||||||
|
const normalized = normalizeAccountId(params.accountId);
|
||||||
|
const merged = mergeTelegramUserAccountConfig(params.cfg, normalized);
|
||||||
|
const baseEnabled = params.cfg.channels?.["telegram-user"]?.enabled !== false;
|
||||||
|
const enabled = baseEnabled && merged.enabled !== false;
|
||||||
|
return {
|
||||||
|
accountId: normalized,
|
||||||
|
enabled,
|
||||||
|
name: merged.name?.trim() || undefined,
|
||||||
|
credentials: resolveCredentials(params.cfg, normalized),
|
||||||
|
config: merged,
|
||||||
|
};
|
||||||
|
}
|
||||||
25
extensions/telegram-user/src/active-client.ts
Normal file
25
extensions/telegram-user/src/active-client.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import type { TelegramClient } from "@mtcute/node";
|
||||||
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
const activeClients = new Map<string, TelegramClient>();
|
||||||
|
|
||||||
|
function resolveAccountKey(accountId?: string | null): string {
|
||||||
|
return normalizeAccountId(accountId ?? DEFAULT_ACCOUNT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActiveTelegramUserClient(
|
||||||
|
accountId: string | null | undefined,
|
||||||
|
next: TelegramClient | null,
|
||||||
|
) {
|
||||||
|
const key = resolveAccountKey(accountId);
|
||||||
|
if (next) {
|
||||||
|
activeClients.set(key, next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeClients.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveTelegramUserClient(accountId?: string | null): TelegramClient | null {
|
||||||
|
const key = resolveAccountKey(accountId);
|
||||||
|
return activeClients.get(key) ?? null;
|
||||||
|
}
|
||||||
116
extensions/telegram-user/src/channel.test.ts
Normal file
116
extensions/telegram-user/src/channel.test.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
const sendMediaTelegramUser = vi.fn<
|
||||||
|
typeof import("./send.js").sendMediaTelegramUser
|
||||||
|
>();
|
||||||
|
|
||||||
|
vi.mock("./send.js", () => {
|
||||||
|
return {
|
||||||
|
looksLikeTelegramUserTargetId: () => true,
|
||||||
|
normalizeTelegramUserMessagingTarget: (raw: string) => raw,
|
||||||
|
sendMessageTelegramUser: vi.fn(async () => ({ messageId: "m1", chatId: "c1" })),
|
||||||
|
sendPollTelegramUser: vi.fn(async () => ({ messageId: "m2", chatId: "c2" })),
|
||||||
|
sendMediaTelegramUser,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("telegram-user channel plugin", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sendMediaTelegramUser.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("declares thread/reaction capabilities consistent with handler behavior", async () => {
|
||||||
|
const mod = await import("./channel.js");
|
||||||
|
expect(mod.telegramUserPlugin.capabilities?.reactions).toBe(true);
|
||||||
|
expect(mod.telegramUserPlugin.capabilities?.threads).toBe(true);
|
||||||
|
expect(mod.telegramUserPlugin.capabilities?.chatTypes).toContain("thread");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enforces mediaMaxMb in outbound sendMedia", async () => {
|
||||||
|
sendMediaTelegramUser.mockResolvedValue({ messageId: "m3", chatId: "c3" });
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
channels: {
|
||||||
|
"telegram-user": {
|
||||||
|
mediaMaxMb: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Partial<OpenClawConfig> as unknown as OpenClawConfig;
|
||||||
|
|
||||||
|
const mod = await import("./channel.js");
|
||||||
|
await mod.telegramUserPlugin.outbound?.sendMedia?.({
|
||||||
|
cfg,
|
||||||
|
to: "telegram-user:123",
|
||||||
|
text: "hello",
|
||||||
|
mediaUrl: "file:///tmp/example.jpg",
|
||||||
|
accountId: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMediaTelegramUser).toHaveBeenCalledTimes(1);
|
||||||
|
const [, , opts] = sendMediaTelegramUser.mock.calls[0] ?? [];
|
||||||
|
expect(opts?.maxBytes).toBe(7 * 1024 * 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits maxBytes when mediaMaxMb is not configured", async () => {
|
||||||
|
sendMediaTelegramUser.mockResolvedValue({ messageId: "m4", chatId: "c4" });
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
channels: {
|
||||||
|
"telegram-user": {},
|
||||||
|
},
|
||||||
|
} satisfies Partial<OpenClawConfig> as unknown as OpenClawConfig;
|
||||||
|
|
||||||
|
const mod = await import("./channel.js");
|
||||||
|
await mod.telegramUserPlugin.outbound?.sendMedia?.({
|
||||||
|
cfg,
|
||||||
|
to: "telegram-user:123",
|
||||||
|
text: "hello",
|
||||||
|
mediaUrl: "file:///tmp/example.jpg",
|
||||||
|
accountId: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMediaTelegramUser).toHaveBeenCalledTimes(1);
|
||||||
|
const [, , opts] = sendMediaTelegramUser.mock.calls[0] ?? [];
|
||||||
|
expect(opts).not.toHaveProperty("maxBytes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists peers and groups from config like the telegram plugin directory", async () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: {
|
||||||
|
"telegram-user": {
|
||||||
|
allowFrom: ["123", "@alice", "telegram-user:456", "user:@bob", "*"],
|
||||||
|
groupAllowFrom: ["tg:carol", 789],
|
||||||
|
groups: {
|
||||||
|
"-1001": {},
|
||||||
|
"*": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Partial<OpenClawConfig> as unknown as OpenClawConfig;
|
||||||
|
|
||||||
|
const mod = await import("./channel.js");
|
||||||
|
const runtime = {
|
||||||
|
log: () => {},
|
||||||
|
warn: () => {},
|
||||||
|
error: () => {},
|
||||||
|
exit: (): never => {
|
||||||
|
throw new Error("exit called");
|
||||||
|
},
|
||||||
|
} satisfies RuntimeEnv;
|
||||||
|
const peers = await mod.telegramUserPlugin.directory?.listPeers?.({
|
||||||
|
cfg,
|
||||||
|
runtime,
|
||||||
|
});
|
||||||
|
const groups = await mod.telegramUserPlugin.directory?.listGroups?.({
|
||||||
|
cfg,
|
||||||
|
runtime,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(peers?.map((p) => p.id).sort()).toEqual(
|
||||||
|
["123", "456", "@alice", "@bob", "@carol", "789"].sort(),
|
||||||
|
);
|
||||||
|
expect(groups?.map((g) => g.id)).toEqual(["-1001"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
545
extensions/telegram-user/src/channel.ts
Normal file
545
extensions/telegram-user/src/channel.ts
Normal file
@ -0,0 +1,545 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyAccountNameToChannelSection,
|
||||||
|
buildChannelConfigSchema,
|
||||||
|
DEFAULT_ACCOUNT_ID,
|
||||||
|
deleteAccountFromConfigSection,
|
||||||
|
formatPairingApproveHint,
|
||||||
|
normalizeAccountId,
|
||||||
|
PAIRING_APPROVED_MESSAGE,
|
||||||
|
resolveChannelMediaMaxBytes,
|
||||||
|
setAccountEnabledInConfigSection,
|
||||||
|
type ChannelGroupContext,
|
||||||
|
type ChannelPlugin,
|
||||||
|
type ChannelSetupInput,
|
||||||
|
type OpenClawConfig,
|
||||||
|
type GroupToolPolicyConfig,
|
||||||
|
} from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
import {
|
||||||
|
listTelegramUserAccountIds,
|
||||||
|
resolveDefaultTelegramUserAccountId,
|
||||||
|
resolveTelegramUserAccount,
|
||||||
|
type ResolvedTelegramUserAccount,
|
||||||
|
} from "./accounts.js";
|
||||||
|
import { TelegramUserConfigSchema } from "./config-schema.js";
|
||||||
|
import {
|
||||||
|
listTelegramUserDirectoryGroupsFromConfig,
|
||||||
|
listTelegramUserDirectoryPeersFromConfig,
|
||||||
|
} from "./directory-config.js";
|
||||||
|
import { loginTelegramUser } from "./login.js";
|
||||||
|
import { monitorTelegramUserProvider } from "./monitor/index.js";
|
||||||
|
import {
|
||||||
|
looksLikeTelegramUserTargetId,
|
||||||
|
normalizeTelegramUserMessagingTarget,
|
||||||
|
sendMediaTelegramUser,
|
||||||
|
sendMessageTelegramUser,
|
||||||
|
sendPollTelegramUser,
|
||||||
|
} from "./send.js";
|
||||||
|
import { resolveTelegramUserSessionPath } from "./session.js";
|
||||||
|
import { getTelegramUserRuntime } from "./runtime.js";
|
||||||
|
import { telegramUserOnboardingAdapter } from "./onboarding.js";
|
||||||
|
import type { CoreConfig } from "./types.js";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
id: "telegram-user",
|
||||||
|
label: "Telegram User",
|
||||||
|
selectionLabel: "Telegram User (MTProto)",
|
||||||
|
detailLabel: "Telegram User",
|
||||||
|
docsPath: "/channels/telegram-user",
|
||||||
|
docsLabel: "telegram-user",
|
||||||
|
blurb: "login as a Telegram user via QR or phone code; supports DMs + groups.",
|
||||||
|
order: 12,
|
||||||
|
quickstartAllowFrom: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
type TelegramUserSetupInput = ChannelSetupInput & {
|
||||||
|
apiId?: number;
|
||||||
|
apiHash?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseReplyToId(replyToId?: string | null): number | undefined {
|
||||||
|
if (!replyToId) return undefined;
|
||||||
|
const parsed = Number.parseInt(replyToId, 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTelegramUserGroupKey(raw?: string | null): string | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
const withoutPrefix = trimmed.replace(/^telegram-user:group:/i, "");
|
||||||
|
const [base] = withoutPrefix.split(/:topic:/i);
|
||||||
|
const normalized = base?.trim();
|
||||||
|
return normalized ? normalized : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTelegramUserGroupToolPolicy(
|
||||||
|
params: ChannelGroupContext,
|
||||||
|
): GroupToolPolicyConfig | undefined {
|
||||||
|
const account = resolveTelegramUserAccount({
|
||||||
|
cfg: params.cfg as CoreConfig,
|
||||||
|
accountId: params.accountId,
|
||||||
|
});
|
||||||
|
const groups = account.config.groups ?? {};
|
||||||
|
const groupId = normalizeTelegramUserGroupKey(params.groupId);
|
||||||
|
const groupChannel = normalizeTelegramUserGroupKey(params.groupChannel);
|
||||||
|
const candidates = [groupId, groupChannel, "*"].filter(
|
||||||
|
(value): value is string => Boolean(value),
|
||||||
|
);
|
||||||
|
for (const key of candidates) {
|
||||||
|
const entry = groups[key];
|
||||||
|
if (entry?.tools) return entry.tools;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSessionLinked = async (accountId: string): Promise<boolean> => {
|
||||||
|
const sessionPath = resolveTelegramUserSessionPath(accountId);
|
||||||
|
return fs.existsSync(sessionPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const telegramUserPlugin: ChannelPlugin<ResolvedTelegramUserAccount> = {
|
||||||
|
id: "telegram-user",
|
||||||
|
meta,
|
||||||
|
onboarding: telegramUserOnboardingAdapter,
|
||||||
|
pairing: {
|
||||||
|
idLabel: "telegramUserId",
|
||||||
|
normalizeAllowEntry: (entry) =>
|
||||||
|
entry.replace(/^(telegram-user|telegram|tg):/i, "").toLowerCase(),
|
||||||
|
notifyApproval: async ({ id }) => {
|
||||||
|
await sendMessageTelegramUser(String(id), PAIRING_APPROVED_MESSAGE, {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
chatTypes: ["direct", "group", "thread"],
|
||||||
|
polls: true,
|
||||||
|
reactions: true,
|
||||||
|
threads: true,
|
||||||
|
media: true,
|
||||||
|
nativeCommands: false,
|
||||||
|
blockStreaming: true,
|
||||||
|
},
|
||||||
|
messaging: {
|
||||||
|
normalizeTarget: normalizeTelegramUserMessagingTarget,
|
||||||
|
targetResolver: {
|
||||||
|
looksLikeId: looksLikeTelegramUserTargetId,
|
||||||
|
hint: "<userId or @username>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
directory: {
|
||||||
|
self: async () => null,
|
||||||
|
listPeers: async (params) => listTelegramUserDirectoryPeersFromConfig(params),
|
||||||
|
listGroups: async (params) => listTelegramUserDirectoryGroupsFromConfig(params),
|
||||||
|
},
|
||||||
|
reload: { configPrefixes: ["channels.telegram-user"] },
|
||||||
|
configSchema: buildChannelConfigSchema(TelegramUserConfigSchema),
|
||||||
|
config: {
|
||||||
|
listAccountIds: (cfg) => listTelegramUserAccountIds(cfg as CoreConfig),
|
||||||
|
resolveAccount: (cfg, accountId) =>
|
||||||
|
resolveTelegramUserAccount({ cfg: cfg as CoreConfig, accountId }),
|
||||||
|
defaultAccountId: (cfg) => resolveDefaultTelegramUserAccountId(cfg as CoreConfig),
|
||||||
|
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||||
|
setAccountEnabledInConfigSection({
|
||||||
|
cfg,
|
||||||
|
sectionKey: "telegram-user",
|
||||||
|
accountId,
|
||||||
|
enabled,
|
||||||
|
allowTopLevel: true,
|
||||||
|
}),
|
||||||
|
deleteAccount: ({ cfg, accountId }) =>
|
||||||
|
deleteAccountFromConfigSection({
|
||||||
|
cfg,
|
||||||
|
sectionKey: "telegram-user",
|
||||||
|
accountId,
|
||||||
|
clearBaseFields: ["apiId", "apiHash", "name"],
|
||||||
|
}),
|
||||||
|
isConfigured: (account) =>
|
||||||
|
Boolean(account.credentials.apiId && account.credentials.apiHash),
|
||||||
|
describeAccount: (account) => ({
|
||||||
|
accountId: account.accountId,
|
||||||
|
name: account.name,
|
||||||
|
enabled: account.enabled,
|
||||||
|
configured: Boolean(account.credentials.apiId && account.credentials.apiHash),
|
||||||
|
}),
|
||||||
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||||
|
(resolveTelegramUserAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map(
|
||||||
|
(entry) => String(entry),
|
||||||
|
),
|
||||||
|
formatAllowFrom: ({ allowFrom }) =>
|
||||||
|
allowFrom
|
||||||
|
.map((entry) => String(entry).trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((entry) => entry.replace(/^(telegram-user|telegram|tg):/i, ""))
|
||||||
|
.map((entry) => entry.toLowerCase()),
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||||
|
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
||||||
|
const useAccountPath = Boolean(cfg.channels?.["telegram-user"]?.accounts?.[resolvedAccountId]);
|
||||||
|
const basePath = useAccountPath
|
||||||
|
? `channels.telegram-user.accounts.${resolvedAccountId}.`
|
||||||
|
: "channels.telegram-user.";
|
||||||
|
return {
|
||||||
|
policy: account.config.dmPolicy ?? "pairing",
|
||||||
|
allowFrom: account.config.allowFrom ?? [],
|
||||||
|
policyPath: `${basePath}dmPolicy`,
|
||||||
|
allowFromPath: basePath,
|
||||||
|
approveHint: formatPairingApproveHint("telegram-user"),
|
||||||
|
normalizeEntry: (raw) =>
|
||||||
|
raw.replace(/^(telegram-user|telegram|tg):/i, "").toLowerCase(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
collectWarnings: ({ account, cfg }) => {
|
||||||
|
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||||
|
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||||
|
if (groupPolicy !== "open") return [];
|
||||||
|
const groupAllowlistConfigured =
|
||||||
|
account.config.groups && Object.keys(account.config.groups).length > 0;
|
||||||
|
if (groupAllowlistConfigured) {
|
||||||
|
return [
|
||||||
|
`- Telegram user groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set channels.telegram-user.groupPolicy="allowlist" + channels.telegram-user.groupAllowFrom to restrict senders.`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
`- Telegram user groups: groupPolicy="open" with no channels.telegram-user.groups allowlist; any group can add + ping (mention-gated). Set channels.telegram-user.groupPolicy="allowlist" + channels.telegram-user.groupAllowFrom or configure channels.telegram-user.groups.`,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
resolveRequireMention: ({ cfg, groupId, accountId }) =>
|
||||||
|
getTelegramUserRuntime().channel.groups.resolveRequireMention({
|
||||||
|
cfg,
|
||||||
|
channel: "telegram-user",
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
}),
|
||||||
|
resolveToolPolicy: resolveTelegramUserGroupToolPolicy,
|
||||||
|
},
|
||||||
|
threading: {
|
||||||
|
resolveReplyToMode: ({ cfg }) => cfg.channels?.["telegram-user"]?.replyToMode ?? "first",
|
||||||
|
buildToolContext: ({ context, hasRepliedRef }) => {
|
||||||
|
const threadId = context.MessageThreadId ?? context.ReplyToId;
|
||||||
|
return {
|
||||||
|
currentChannelId: context.To?.trim() || undefined,
|
||||||
|
currentThreadTs: threadId != null ? String(threadId) : undefined,
|
||||||
|
hasRepliedRef,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
listActions: ({ cfg }) => {
|
||||||
|
if (!cfg.channels?.["telegram-user"]) return [];
|
||||||
|
return ["poll"];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
agentPrompt: {
|
||||||
|
messageToolHints: () => [
|
||||||
|
"Telegram user polls only work in groups/channels (DM polls return MEDIA_INVALID). Use the group id for polls.",
|
||||||
|
"When ChatType is group, use currentChannelId as the target for message/poll actions.",
|
||||||
|
"To send files, use `message` action=send with `filePath` (local path) or `media` (URL); put any caption in `message`.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
outbound: {
|
||||||
|
deliveryMode: "direct",
|
||||||
|
chunker: (text, limit) =>
|
||||||
|
getTelegramUserRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||||
|
chunkerMode: "markdown",
|
||||||
|
textChunkLimit: 4000,
|
||||||
|
pollMaxOptions: 10,
|
||||||
|
sendText: async ({ to, text, accountId, threadId, replyToId }) => {
|
||||||
|
const parsedReplyToId = parseReplyToId(replyToId);
|
||||||
|
const result = await sendMessageTelegramUser(to, text, {
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
threadId,
|
||||||
|
...(parsedReplyToId ? { replyToId: parsedReplyToId } : {}),
|
||||||
|
});
|
||||||
|
return { channel: "telegram-user", ...result };
|
||||||
|
},
|
||||||
|
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, threadId, replyToId }) => {
|
||||||
|
const parsedReplyToId = parseReplyToId(replyToId);
|
||||||
|
const maxBytes = resolveChannelMediaMaxBytes({
|
||||||
|
cfg,
|
||||||
|
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||||
|
resolveTelegramUserAccount({
|
||||||
|
cfg: cfg as CoreConfig,
|
||||||
|
accountId,
|
||||||
|
}).config.mediaMaxMb,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
const result = await sendMediaTelegramUser(to, text, {
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
mediaUrl,
|
||||||
|
threadId,
|
||||||
|
...(parsedReplyToId ? { replyToId: parsedReplyToId } : {}),
|
||||||
|
...(maxBytes ? { maxBytes } : {}),
|
||||||
|
});
|
||||||
|
return { channel: "telegram-user", ...result };
|
||||||
|
},
|
||||||
|
sendPoll: async ({ to, poll, accountId, threadId, replyToId }) => {
|
||||||
|
const parsedReplyToId = parseReplyToId(replyToId);
|
||||||
|
const result = await sendPollTelegramUser(to, poll, {
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
threadId,
|
||||||
|
...(parsedReplyToId ? { replyToId: parsedReplyToId } : {}),
|
||||||
|
});
|
||||||
|
return { channel: "telegram-user", ...result };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
login: async ({ cfg, accountId, runtime }) => {
|
||||||
|
const account = resolveTelegramUserAccount({
|
||||||
|
cfg: cfg as CoreConfig,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
const apiId = account.credentials.apiId;
|
||||||
|
const apiHash = account.credentials.apiHash;
|
||||||
|
if (!apiId || !apiHash) {
|
||||||
|
throw new Error("Telegram user apiId/apiHash required. Set in config or env.");
|
||||||
|
}
|
||||||
|
const storagePath = resolveTelegramUserSessionPath(account.accountId);
|
||||||
|
await loginTelegramUser({
|
||||||
|
apiId,
|
||||||
|
apiHash,
|
||||||
|
storagePath,
|
||||||
|
runtime,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
defaultRuntime: {
|
||||||
|
accountId: DEFAULT_ACCOUNT_ID,
|
||||||
|
running: false,
|
||||||
|
lastStartAt: null,
|
||||||
|
lastStopAt: null,
|
||||||
|
lastError: null,
|
||||||
|
lastInboundAt: null,
|
||||||
|
lastOutboundAt: null,
|
||||||
|
},
|
||||||
|
buildAccountSnapshot: async ({ account, runtime }) => ({
|
||||||
|
accountId: account.accountId,
|
||||||
|
name: account.name,
|
||||||
|
enabled: account.enabled,
|
||||||
|
configured: Boolean(account.credentials.apiId && account.credentials.apiHash),
|
||||||
|
linked: await isSessionLinked(account.accountId),
|
||||||
|
running: runtime?.running ?? false,
|
||||||
|
lastStartAt: runtime?.lastStartAt ?? null,
|
||||||
|
lastStopAt: runtime?.lastStopAt ?? null,
|
||||||
|
lastError: runtime?.lastError ?? null,
|
||||||
|
lastInboundAt: runtime?.lastInboundAt ?? null,
|
||||||
|
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
||||||
|
dmPolicy: account.config.dmPolicy ?? "pairing",
|
||||||
|
allowFrom: (account.config.allowFrom ?? []).map((entry) => String(entry)),
|
||||||
|
}),
|
||||||
|
resolveAccountState: ({ configured }) => (configured ? "configured" : "not configured"),
|
||||||
|
},
|
||||||
|
setup: {
|
||||||
|
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||||
|
applyAccountName: ({ cfg, accountId, name }) =>
|
||||||
|
applyAccountNameToChannelSection({
|
||||||
|
cfg: cfg as OpenClawConfig,
|
||||||
|
channelKey: "telegram-user",
|
||||||
|
accountId,
|
||||||
|
name,
|
||||||
|
}),
|
||||||
|
validateInput: ({ accountId, input }) => {
|
||||||
|
const setupInput = input as TelegramUserSetupInput;
|
||||||
|
if (setupInput.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
||||||
|
return "TELEGRAM_USER_API_ID/TELEGRAM_USER_API_HASH can only be used for the default account.";
|
||||||
|
}
|
||||||
|
if (!setupInput.useEnv && (!setupInput.apiId || !setupInput.apiHash)) {
|
||||||
|
return "Telegram user requires apiId/apiHash (or --use-env).";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||||
|
const setupInput = input as TelegramUserSetupInput;
|
||||||
|
const namedConfig = applyAccountNameToChannelSection({
|
||||||
|
cfg: cfg as OpenClawConfig,
|
||||||
|
channelKey: "telegram-user",
|
||||||
|
accountId,
|
||||||
|
name: setupInput.name,
|
||||||
|
});
|
||||||
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
return {
|
||||||
|
...namedConfig,
|
||||||
|
channels: {
|
||||||
|
...namedConfig.channels,
|
||||||
|
"telegram-user": {
|
||||||
|
...namedConfig.channels?.["telegram-user"],
|
||||||
|
enabled: true,
|
||||||
|
...(setupInput.useEnv
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
apiId: setupInput.apiId,
|
||||||
|
apiHash: setupInput.apiHash,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...namedConfig,
|
||||||
|
channels: {
|
||||||
|
...namedConfig.channels,
|
||||||
|
"telegram-user": {
|
||||||
|
...namedConfig.channels?.["telegram-user"],
|
||||||
|
enabled: true,
|
||||||
|
accounts: {
|
||||||
|
...namedConfig.channels?.["telegram-user"]?.accounts,
|
||||||
|
[accountId]: {
|
||||||
|
...namedConfig.channels?.["telegram-user"]?.accounts?.[accountId],
|
||||||
|
enabled: true,
|
||||||
|
...(setupInput.useEnv
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
apiId: setupInput.apiId,
|
||||||
|
apiHash: setupInput.apiHash,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
startAccount: async (ctx) => {
|
||||||
|
ctx.setStatus({
|
||||||
|
accountId: ctx.accountId,
|
||||||
|
running: true,
|
||||||
|
lastStartAt: Date.now(),
|
||||||
|
lastError: null,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await monitorTelegramUserProvider({
|
||||||
|
runtime: ctx.runtime,
|
||||||
|
abortSignal: ctx.abortSignal,
|
||||||
|
accountId: ctx.accountId,
|
||||||
|
});
|
||||||
|
ctx.setStatus({
|
||||||
|
accountId: ctx.accountId,
|
||||||
|
running: false,
|
||||||
|
lastStopAt: Date.now(),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
ctx.setStatus({
|
||||||
|
accountId: ctx.accountId,
|
||||||
|
running: false,
|
||||||
|
lastStopAt: Date.now(),
|
||||||
|
lastError: String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stopAccount: async ({ accountId }) => {
|
||||||
|
const { getActiveTelegramUserClient, setActiveTelegramUserClient } =
|
||||||
|
await import("./active-client.js");
|
||||||
|
const active = getActiveTelegramUserClient(accountId);
|
||||||
|
if (active) {
|
||||||
|
await active.destroy().catch(() => undefined);
|
||||||
|
setActiveTelegramUserClient(accountId, null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logoutAccount: async ({ accountId, cfg, runtime }) => {
|
||||||
|
const sessionPath = resolveTelegramUserSessionPath(accountId);
|
||||||
|
let cleared = false;
|
||||||
|
if (fs.existsSync(sessionPath)) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(sessionPath, { force: true });
|
||||||
|
cleared = true;
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error?.(`Failed to remove Telegram user session: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextCfg = { ...cfg } as OpenClawConfig;
|
||||||
|
const nextSection = cfg.channels?.["telegram-user"]
|
||||||
|
? { ...cfg.channels["telegram-user"] }
|
||||||
|
: undefined;
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
if (nextSection) {
|
||||||
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
if ("apiId" in nextSection) {
|
||||||
|
if (nextSection.apiId) cleared = true;
|
||||||
|
delete nextSection.apiId;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if ("apiHash" in nextSection) {
|
||||||
|
if (nextSection.apiHash) cleared = true;
|
||||||
|
delete nextSection.apiHash;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts =
|
||||||
|
nextSection.accounts && typeof nextSection.accounts === "object"
|
||||||
|
? { ...nextSection.accounts }
|
||||||
|
: undefined;
|
||||||
|
if (accounts && accountId in accounts) {
|
||||||
|
const entry = accounts[accountId];
|
||||||
|
if (entry && typeof entry === "object") {
|
||||||
|
const nextEntry = { ...entry } as Record<string, unknown>;
|
||||||
|
if ("apiId" in nextEntry) {
|
||||||
|
const apiId = nextEntry.apiId;
|
||||||
|
if (typeof apiId === "number" && Number.isFinite(apiId)) {
|
||||||
|
cleared = true;
|
||||||
|
}
|
||||||
|
delete nextEntry.apiId;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if ("apiHash" in nextEntry) {
|
||||||
|
const apiHash = nextEntry.apiHash;
|
||||||
|
if (typeof apiHash === "string" ? apiHash.trim() : apiHash) {
|
||||||
|
cleared = true;
|
||||||
|
}
|
||||||
|
delete nextEntry.apiHash;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (Object.keys(nextEntry).length === 0) {
|
||||||
|
delete accounts[accountId];
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
accounts[accountId] = nextEntry as typeof entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (accounts) {
|
||||||
|
if (Object.keys(accounts).length === 0) {
|
||||||
|
delete nextSection.accounts;
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
nextSection.accounts = accounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
if (nextSection && Object.keys(nextSection).length > 0) {
|
||||||
|
nextCfg.channels = { ...nextCfg.channels, "telegram-user": nextSection };
|
||||||
|
} else {
|
||||||
|
const nextChannels = { ...nextCfg.channels };
|
||||||
|
delete nextChannels["telegram-user"];
|
||||||
|
if (Object.keys(nextChannels).length > 0) {
|
||||||
|
nextCfg.channels = nextChannels;
|
||||||
|
} else {
|
||||||
|
delete nextCfg.channels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await getTelegramUserRuntime().config.writeConfigFile(nextCfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envApiId = process.env.TELEGRAM_USER_API_ID?.trim();
|
||||||
|
const envApiHash = process.env.TELEGRAM_USER_API_HASH?.trim();
|
||||||
|
const loggedOut = !fs.existsSync(sessionPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cleared,
|
||||||
|
loggedOut,
|
||||||
|
envCredentials: Boolean(envApiId && envApiHash),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
44
extensions/telegram-user/src/client.ts
Normal file
44
extensions/telegram-user/src/client.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
type MtcuteNode = typeof import("@mtcute/node");
|
||||||
|
|
||||||
|
let mtcuteNodePromise: Promise<MtcuteNode> | null = null;
|
||||||
|
|
||||||
|
async function loadMtcuteNode(): Promise<MtcuteNode> {
|
||||||
|
mtcuteNodePromise ??= import("@mtcute/node");
|
||||||
|
return mtcuteNodePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTelegramUserClient(params: {
|
||||||
|
apiId: number;
|
||||||
|
apiHash: string;
|
||||||
|
storagePath: string;
|
||||||
|
}): Promise<import("@mtcute/node").TelegramClient> {
|
||||||
|
// When loaded via jiti (plugin loader), dependencies often resolve through the "require" export
|
||||||
|
// condition. mtcute prints a deprecation warning from its CommonJS bundle. Dynamic import forces
|
||||||
|
// the "import" condition (ESM), eliminating the warning.
|
||||||
|
const { BaseTelegramClient, TelegramClient, NodePlatform } = await loadMtcuteNode();
|
||||||
|
|
||||||
|
class OpenClawTelegramUserPlatform extends NodePlatform {
|
||||||
|
// mtcute's default NodePlatform.beforeExit installs SIGINT/SIGTERM handlers that re-send the
|
||||||
|
// signal, which can race with OpenClaw's graceful shutdown and close sqlite while writes are
|
||||||
|
// pending. We only hook into process exit events (no signal handlers) and rely on OpenClaw to
|
||||||
|
// stop cleanly.
|
||||||
|
override beforeExit(fn: () => void): () => void {
|
||||||
|
const onBeforeExit = () => fn();
|
||||||
|
const onExit = () => fn();
|
||||||
|
process.once("beforeExit", onBeforeExit);
|
||||||
|
process.once("exit", onExit);
|
||||||
|
return () => {
|
||||||
|
process.off("beforeExit", onBeforeExit);
|
||||||
|
process.off("exit", onExit);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new BaseTelegramClient({
|
||||||
|
apiId: params.apiId,
|
||||||
|
apiHash: params.apiHash,
|
||||||
|
storage: params.storagePath,
|
||||||
|
platform: new OpenClawTelegramUserPlatform(),
|
||||||
|
});
|
||||||
|
return new TelegramClient({ client });
|
||||||
|
}
|
||||||
73
extensions/telegram-user/src/config-schema.ts
Normal file
73
extensions/telegram-user/src/config-schema.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DmPolicySchema,
|
||||||
|
GroupPolicySchema,
|
||||||
|
ToolPolicySchema,
|
||||||
|
requireOpenAllowFrom,
|
||||||
|
} from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||||
|
|
||||||
|
const TelegramUserTopicSchema = z
|
||||||
|
.object({
|
||||||
|
requireMention: z.boolean().optional(),
|
||||||
|
skills: z.array(z.string()).optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
allowFrom: z.array(allowFromEntry).optional(),
|
||||||
|
systemPrompt: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const TelegramUserGroupSchema = z
|
||||||
|
.object({
|
||||||
|
requireMention: z.boolean().optional(),
|
||||||
|
skills: z.array(z.string()).optional(),
|
||||||
|
tools: ToolPolicySchema,
|
||||||
|
topics: z.record(z.string(), TelegramUserTopicSchema.optional()).optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
allowFrom: z.array(allowFromEntry).optional(),
|
||||||
|
systemPrompt: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const TelegramUserAccountSchemaBase = z
|
||||||
|
.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
apiId: z.number().int().positive().optional(),
|
||||||
|
apiHash: z.string().optional(),
|
||||||
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
||||||
|
allowFrom: z.array(allowFromEntry).optional(),
|
||||||
|
replyToMode: z.enum(["off", "first", "all"]).optional(),
|
||||||
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
mediaMaxMb: z.number().positive().optional(),
|
||||||
|
groupAllowFrom: z.array(allowFromEntry).optional(),
|
||||||
|
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
||||||
|
groups: z.record(z.string(), TelegramUserGroupSchema.optional()).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const TelegramUserAccountSchema = TelegramUserAccountSchemaBase.superRefine((value, ctx) => {
|
||||||
|
requireOpenAllowFrom({
|
||||||
|
policy: value.dmPolicy,
|
||||||
|
allowFrom: value.allowFrom,
|
||||||
|
ctx,
|
||||||
|
path: ["allowFrom"],
|
||||||
|
message:
|
||||||
|
'channels.telegram-user.dmPolicy="open" requires channels.telegram-user.allowFrom to include "*"',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TelegramUserConfigSchema = TelegramUserAccountSchemaBase.extend({
|
||||||
|
accounts: z.record(z.string(), TelegramUserAccountSchema.optional()).optional(),
|
||||||
|
}).superRefine((value, ctx) => {
|
||||||
|
requireOpenAllowFrom({
|
||||||
|
policy: value.dmPolicy,
|
||||||
|
allowFrom: value.allowFrom,
|
||||||
|
ctx,
|
||||||
|
path: ["allowFrom"],
|
||||||
|
message:
|
||||||
|
'channels.telegram-user.dmPolicy="open" requires channels.telegram-user.allowFrom to include "*"',
|
||||||
|
});
|
||||||
|
});
|
||||||
67
extensions/telegram-user/src/directory-config.ts
Normal file
67
extensions/telegram-user/src/directory-config.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import type { ChannelDirectoryEntry, OpenClawConfig } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
import { resolveTelegramUserAccount } from "./accounts.js";
|
||||||
|
import type { CoreConfig } from "./types.js";
|
||||||
|
|
||||||
|
export type TelegramUserDirectoryConfigParams = {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
accountId?: string | null;
|
||||||
|
query?: string | null;
|
||||||
|
limit?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizePeerEntry(raw: string): string | null {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
const cleaned = trimmed
|
||||||
|
.replace(/^(telegram-user|telegram|tg):/i, "")
|
||||||
|
.replace(/^user:/i, "")
|
||||||
|
.trim();
|
||||||
|
if (!cleaned) return null;
|
||||||
|
if (/^-?\d+$/.test(cleaned)) return cleaned;
|
||||||
|
const withoutAt = cleaned.replace(/^@/, "");
|
||||||
|
if (!withoutAt) return null;
|
||||||
|
return `@${withoutAt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listTelegramUserDirectoryPeersFromConfig(
|
||||||
|
params: TelegramUserDirectoryConfigParams,
|
||||||
|
): Promise<ChannelDirectoryEntry[]> {
|
||||||
|
const account = resolveTelegramUserAccount({
|
||||||
|
cfg: params.cfg as CoreConfig,
|
||||||
|
accountId: params.accountId,
|
||||||
|
});
|
||||||
|
const q = params.query?.trim().toLowerCase() || "";
|
||||||
|
const raw = [
|
||||||
|
...(account.config.allowFrom ?? []).map((entry) => String(entry)),
|
||||||
|
...(account.config.groupAllowFrom ?? []).map((entry) => String(entry)),
|
||||||
|
];
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
raw
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter((entry) => Boolean(entry) && entry !== "*"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map((entry) => normalizePeerEntry(entry))
|
||||||
|
.filter((id): id is string => Boolean(id))
|
||||||
|
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||||
|
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||||
|
.map((id) => ({ kind: "user", id }) as const);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listTelegramUserDirectoryGroupsFromConfig(
|
||||||
|
params: TelegramUserDirectoryConfigParams,
|
||||||
|
): Promise<ChannelDirectoryEntry[]> {
|
||||||
|
const account = resolveTelegramUserAccount({
|
||||||
|
cfg: params.cfg as CoreConfig,
|
||||||
|
accountId: params.accountId,
|
||||||
|
});
|
||||||
|
const q = params.query?.trim().toLowerCase() || "";
|
||||||
|
return Object.keys(account.config.groups ?? {})
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter((id) => Boolean(id) && id !== "*")
|
||||||
|
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||||
|
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||||
|
.map((id) => ({ kind: "group", id }) as const);
|
||||||
|
}
|
||||||
107
extensions/telegram-user/src/login.ts
Normal file
107
extensions/telegram-user/src/login.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import qrcode from "qrcode-terminal";
|
||||||
|
import { createInterface } from "node:readline/promises";
|
||||||
|
import { stdin as input, stdout as output } from "node:process";
|
||||||
|
import { isCancel, select } from "@clack/prompts";
|
||||||
|
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
import { createTelegramUserClient } from "./client.js";
|
||||||
|
import { ensureTelegramUserSessionDir } from "./session.js";
|
||||||
|
|
||||||
|
async function promptText(message: string): Promise<string> {
|
||||||
|
const rl = createInterface({ input, output });
|
||||||
|
try {
|
||||||
|
const value = await rl.question(message);
|
||||||
|
return value.trim();
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptLoginMode(): Promise<"qr" | "phone"> {
|
||||||
|
if (!input.isTTY || !output.isTTY) return "qr";
|
||||||
|
const response = await select({
|
||||||
|
message: "Telegram login method",
|
||||||
|
options: [
|
||||||
|
{ value: "qr", label: "QR code (scan with Telegram)" },
|
||||||
|
{ value: "phone", label: "Phone code (SMS/Telegram)" },
|
||||||
|
],
|
||||||
|
initialValue: "qr",
|
||||||
|
});
|
||||||
|
if (isCancel(response)) return "qr";
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginTelegramUser(params: {
|
||||||
|
apiId: number;
|
||||||
|
apiHash: string;
|
||||||
|
storagePath: string;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
}) {
|
||||||
|
const { apiId, apiHash, storagePath, runtime } = params;
|
||||||
|
ensureTelegramUserSessionDir({ sessionPath: storagePath });
|
||||||
|
const client = await createTelegramUserClient({ apiId, apiHash, storagePath });
|
||||||
|
let lastUrl = "";
|
||||||
|
|
||||||
|
const passwordEnv = process.env.TELEGRAM_USER_PASSWORD?.trim() || undefined;
|
||||||
|
let phoneEnv = process.env.TELEGRAM_USER_PHONE?.trim() || undefined;
|
||||||
|
const codeEnv = process.env.TELEGRAM_USER_CODE?.trim() || undefined;
|
||||||
|
|
||||||
|
const passwordPrompt = passwordEnv
|
||||||
|
? passwordEnv
|
||||||
|
: async () => await promptText("2FA password: ");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!phoneEnv) {
|
||||||
|
const mode = await promptLoginMode();
|
||||||
|
if (mode === "phone") {
|
||||||
|
phoneEnv = await promptText("Telegram phone number (E.164): ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const user = await client.start(
|
||||||
|
phoneEnv
|
||||||
|
? {
|
||||||
|
phone: phoneEnv,
|
||||||
|
code: codeEnv ? codeEnv : async () => await promptText("Telegram code: "),
|
||||||
|
password: passwordPrompt,
|
||||||
|
codeSentCallback: (code) => {
|
||||||
|
runtime.log(
|
||||||
|
`Telegram code sent via ${code.type}. Check your device and enter it here.`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
invalidCodeCallback: async (type) => {
|
||||||
|
if (type === "password" && passwordEnv) {
|
||||||
|
runtime.error?.(
|
||||||
|
"Telegram 2FA password rejected. Update TELEGRAM_USER_PASSWORD and rerun.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === "code" && codeEnv) {
|
||||||
|
runtime.error?.(
|
||||||
|
"Telegram code rejected. Update TELEGRAM_USER_CODE and rerun.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
qrCodeHandler: (url, expires) => {
|
||||||
|
if (url === lastUrl) return;
|
||||||
|
lastUrl = url;
|
||||||
|
runtime.log(`Scan this QR in Telegram (expires ${expires.toLocaleTimeString()}):`);
|
||||||
|
qrcode.generate(url, { small: true });
|
||||||
|
},
|
||||||
|
password: passwordPrompt,
|
||||||
|
invalidCodeCallback: async (type) => {
|
||||||
|
if (type === "password") {
|
||||||
|
runtime.error?.(
|
||||||
|
passwordEnv
|
||||||
|
? "Telegram 2FA password rejected. Update TELEGRAM_USER_PASSWORD and rerun."
|
||||||
|
: "Telegram 2FA password rejected. Try again.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
runtime.log(`Telegram user logged in as ${user.displayName}.`);
|
||||||
|
} finally {
|
||||||
|
await client.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
extensions/telegram-user/src/monitor/handler.test.ts
Normal file
23
extensions/telegram-user/src/monitor/handler.test.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { resolveTelegramUserTimestampMs } from "./handler.js";
|
||||||
|
|
||||||
|
describe("resolveTelegramUserTimestampMs", () => {
|
||||||
|
it("uses Date values directly", () => {
|
||||||
|
const date = new Date("2025-01-02T03:04:05Z");
|
||||||
|
expect(resolveTelegramUserTimestampMs(date)).toBe(date.getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts seconds to milliseconds", () => {
|
||||||
|
expect(resolveTelegramUserTimestampMs(1_710_000_000)).toBe(1_710_000_000 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through millisecond values", () => {
|
||||||
|
expect(resolveTelegramUserTimestampMs(1_710_000_000_000)).toBe(1_710_000_000_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for invalid dates", () => {
|
||||||
|
const invalid = new Date("invalid");
|
||||||
|
expect(resolveTelegramUserTimestampMs(invalid)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
811
extensions/telegram-user/src/monitor/handler.ts
Normal file
811
extensions/telegram-user/src/monitor/handler.ts
Normal file
@ -0,0 +1,811 @@
|
|||||||
|
import type { TelegramClient } from "@mtcute/node";
|
||||||
|
import type { MessageContext } from "@mtcute/dispatcher";
|
||||||
|
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatLocationText,
|
||||||
|
resolveAckReaction,
|
||||||
|
resolveMentionGatingWithBypass,
|
||||||
|
toLocationContext,
|
||||||
|
type NormalizedLocation,
|
||||||
|
} from "openclaw/plugin-sdk";
|
||||||
|
import { getTelegramUserRuntime } from "../runtime.js";
|
||||||
|
import type { CoreConfig, TelegramUserAccountConfig } from "../types.js";
|
||||||
|
import { sendMediaTelegramUser, sendMessageTelegramUser } from "../send.js";
|
||||||
|
|
||||||
|
const DEFAULT_TEXT_LIMIT = 4000;
|
||||||
|
const DEFAULT_MEDIA_MAX_MB = 5;
|
||||||
|
|
||||||
|
type TelegramUserHandlerParams = {
|
||||||
|
client: TelegramClient;
|
||||||
|
cfg: CoreConfig;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
accountId: string;
|
||||||
|
accountConfig: TelegramUserAccountConfig;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
|
self?: { id: number; username?: string | null; name?: string | null };
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeAllowEntry(raw: string): string {
|
||||||
|
const trimmed = raw.trim().toLowerCase();
|
||||||
|
return trimmed
|
||||||
|
.replace(/^(telegram-user|telegram|tg):/i, "")
|
||||||
|
.replace(/^user:/i, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAllowlist(entries: Array<string | number> | undefined) {
|
||||||
|
const normalized = (entries ?? [])
|
||||||
|
.map((entry) => normalizeAllowEntry(String(entry)))
|
||||||
|
.filter(Boolean);
|
||||||
|
const hasWildcard = normalized.includes("*");
|
||||||
|
const usernames = new Set<string>();
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const entry of normalized) {
|
||||||
|
if (entry === "*") continue;
|
||||||
|
if (/^-?\d+$/.test(entry)) {
|
||||||
|
ids.add(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const username = entry.startsWith("@") ? entry.slice(1) : entry;
|
||||||
|
if (username) usernames.add(username);
|
||||||
|
}
|
||||||
|
return { hasWildcard, usernames, ids, hasEntries: normalized.length > 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSenderAllowed(params: {
|
||||||
|
allowFrom: Array<string | number> | undefined;
|
||||||
|
senderId: string;
|
||||||
|
senderUsername?: string | null;
|
||||||
|
}): boolean {
|
||||||
|
const parsed = parseAllowlist(params.allowFrom);
|
||||||
|
if (parsed.hasWildcard) return true;
|
||||||
|
if (parsed.ids.has(params.senderId)) return true;
|
||||||
|
const username = params.senderUsername?.trim().toLowerCase();
|
||||||
|
if (!username) return false;
|
||||||
|
return parsed.usernames.has(username.replace(/^@/, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTelegramUserPeer(target: string): number | string {
|
||||||
|
if (/^-?\d+$/.test(target)) {
|
||||||
|
const parsed = Number.parseInt(target, 10);
|
||||||
|
if (Number.isFinite(parsed)) return parsed;
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDestroyedClientError(err: unknown): boolean {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
return /client is destroyed/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClientDestroyed(client: TelegramClient): boolean {
|
||||||
|
const candidate = client as TelegramClient & { destroyed?: boolean };
|
||||||
|
return candidate.destroyed === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(text: string): string {
|
||||||
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTelegramUserSelfMentionRegexes(params: {
|
||||||
|
username?: string | null;
|
||||||
|
name?: string | null;
|
||||||
|
}): RegExp[] {
|
||||||
|
const patterns: string[] = [];
|
||||||
|
const username = params.username?.trim().replace(/^@/, "");
|
||||||
|
if (username) {
|
||||||
|
patterns.push(String.raw`\b@?${escapeRegExp(username)}\b`);
|
||||||
|
}
|
||||||
|
const name = params.name?.trim();
|
||||||
|
if (name) {
|
||||||
|
const parts = name.split(/\s+/).filter(Boolean).map(escapeRegExp);
|
||||||
|
if (parts.length > 0) {
|
||||||
|
patterns.push(String.raw`\b@?${parts.join(String.raw`\s+`)}\b`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patterns
|
||||||
|
.map((pattern) => {
|
||||||
|
try {
|
||||||
|
return new RegExp(pattern, "i");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((entry): entry is RegExp => Boolean(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTelegramUserTimestampMs(
|
||||||
|
value: Date | number | null | undefined,
|
||||||
|
): number | undefined {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
if (value instanceof Date) {
|
||||||
|
const ms = value.getTime();
|
||||||
|
return Number.isFinite(ms) ? ms : undefined;
|
||||||
|
}
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return value < 1_000_000_000_000 ? Math.round(value * 1000) : Math.round(value);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeSendTyping(params: {
|
||||||
|
client: TelegramClient;
|
||||||
|
target: number | string;
|
||||||
|
status: Parameters<TelegramClient["sendTyping"]>[1];
|
||||||
|
typingParams?: Parameters<TelegramClient["sendTyping"]>[2];
|
||||||
|
runtime: TelegramUserHandlerParams["runtime"];
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
|
logLabel: string;
|
||||||
|
}) {
|
||||||
|
if (params.abortSignal?.aborted) return;
|
||||||
|
if (isClientDestroyed(params.client)) return;
|
||||||
|
try {
|
||||||
|
await params.client.sendTyping(params.target, params.status, params.typingParams);
|
||||||
|
} catch (err) {
|
||||||
|
if (isDestroyedClientError(err)) return;
|
||||||
|
params.runtime.error?.(`telegram-user ${params.logLabel} failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstDefined<T>(...values: Array<T | undefined>): T | undefined {
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value !== "undefined") return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTelegramUserGroupPeerId(chatId: number | string, threadId?: number) {
|
||||||
|
return threadId != null ? `${chatId}:topic:${threadId}` : String(chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTelegramUserGroupFrom(chatId: number | string, threadId?: number) {
|
||||||
|
return `telegram-user:group:${buildTelegramUserGroupPeerId(chatId, threadId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTelegramUserGroupLabel(
|
||||||
|
title: string | undefined,
|
||||||
|
chatId: number | string,
|
||||||
|
threadId?: number,
|
||||||
|
) {
|
||||||
|
const topicSuffix = threadId != null ? ` topic:${threadId}` : "";
|
||||||
|
if (title) return `${title} id:${chatId}${topicSuffix}`;
|
||||||
|
return `group:${chatId}${topicSuffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTelegramUserGroupConfig(
|
||||||
|
accountConfig: TelegramUserAccountConfig,
|
||||||
|
chatId: number | string,
|
||||||
|
threadId?: number,
|
||||||
|
) {
|
||||||
|
const groups = accountConfig.groups ?? {};
|
||||||
|
const chatKey = String(chatId);
|
||||||
|
const groupConfig = groups[chatKey] ?? groups["*"];
|
||||||
|
if (!threadId) return { groupConfig, topicConfig: undefined };
|
||||||
|
const topicKey = String(threadId);
|
||||||
|
const topicConfig =
|
||||||
|
groupConfig?.topics?.[topicKey] ?? groups["*"]?.topics?.[topicKey];
|
||||||
|
return { groupConfig, topicConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTelegramUserLocation(
|
||||||
|
media: MessageContext["media"],
|
||||||
|
): NormalizedLocation | null {
|
||||||
|
if (!media) return null;
|
||||||
|
const typed = media as { type?: string };
|
||||||
|
if (typed.type === "venue") {
|
||||||
|
const venue = media as {
|
||||||
|
location: { latitude: number; longitude: number; radius?: number };
|
||||||
|
title: string;
|
||||||
|
address: string;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
latitude: venue.location.latitude,
|
||||||
|
longitude: venue.location.longitude,
|
||||||
|
accuracy: venue.location.radius,
|
||||||
|
name: venue.title,
|
||||||
|
address: venue.address,
|
||||||
|
source: "place",
|
||||||
|
isLive: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typed.type === "location" || typed.type === "live_location") {
|
||||||
|
const location = media as {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
radius?: number;
|
||||||
|
};
|
||||||
|
const isLive = typed.type === "live_location";
|
||||||
|
return {
|
||||||
|
latitude: location.latitude,
|
||||||
|
longitude: location.longitude,
|
||||||
|
accuracy: location.radius,
|
||||||
|
source: isLive ? "live" : "pin",
|
||||||
|
isLive,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTelegramUserPoll(media: MessageContext["media"]): string | null {
|
||||||
|
if (!media) return null;
|
||||||
|
const typed = media as { type?: string };
|
||||||
|
if (typed.type !== "poll") return null;
|
||||||
|
const poll = media as {
|
||||||
|
question: string;
|
||||||
|
answers: Array<{ text: string }>;
|
||||||
|
isMultiple?: boolean;
|
||||||
|
isQuiz?: boolean;
|
||||||
|
};
|
||||||
|
const mode = poll.isQuiz ? "quiz" : poll.isMultiple ? "multi" : null;
|
||||||
|
const header = `📊 Poll${mode ? ` (${mode})` : ""}: ${poll.question}`;
|
||||||
|
const options = poll.answers.map((ans, idx) => `${idx + 1}) ${ans.text}`);
|
||||||
|
return [header, ...options].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeReplySender(sender: unknown): string | undefined {
|
||||||
|
const typed = sender as {
|
||||||
|
type?: string;
|
||||||
|
displayName?: string;
|
||||||
|
title?: string;
|
||||||
|
id?: number;
|
||||||
|
};
|
||||||
|
if (!typed || typeof typed !== "object") return undefined;
|
||||||
|
if (typed.type === "anonymous" && typed.displayName) return typed.displayName;
|
||||||
|
if (typed.type === "user" && typed.displayName) return typed.displayName;
|
||||||
|
if (typed.type === "chat") {
|
||||||
|
if (typed.title) return typed.title;
|
||||||
|
if (typed.id != null) return `chat:${typed.id}`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveMediaAttachment(params: {
|
||||||
|
client: TelegramClient;
|
||||||
|
mediaMaxMb: number;
|
||||||
|
media: MessageContext["media"];
|
||||||
|
}) {
|
||||||
|
if (!params.media) return null;
|
||||||
|
const typed = params.media as { type?: string };
|
||||||
|
if (
|
||||||
|
typed.type === "location" ||
|
||||||
|
typed.type === "live_location" ||
|
||||||
|
typed.type === "venue" ||
|
||||||
|
typed.type === "poll"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const core = getTelegramUserRuntime();
|
||||||
|
const maxBytes = Math.max(1, params.mediaMaxMb) * 1024 * 1024;
|
||||||
|
if ("fileSize" in params.media && typeof params.media.fileSize === "number") {
|
||||||
|
if (params.media.fileSize > maxBytes) {
|
||||||
|
throw new Error(`Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const buffer = Buffer.from(await params.client.downloadAsBuffer(params.media));
|
||||||
|
const fileName =
|
||||||
|
params.media && "fileName" in params.media && typeof params.media.fileName === "string"
|
||||||
|
? params.media.fileName
|
||||||
|
: undefined;
|
||||||
|
const contentType =
|
||||||
|
params.media && "mimeType" in params.media && typeof params.media.mimeType === "string"
|
||||||
|
? params.media.mimeType
|
||||||
|
: await core.media.detectMime({ buffer, filePath: fileName });
|
||||||
|
const saved = await core.channel.media.saveMediaBuffer(
|
||||||
|
buffer,
|
||||||
|
contentType,
|
||||||
|
"telegram-user",
|
||||||
|
maxBytes,
|
||||||
|
fileName,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
path: saved.path,
|
||||||
|
contentType: saved.contentType ?? contentType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveMediaAttachments(params: {
|
||||||
|
client: TelegramClient;
|
||||||
|
mediaMaxMb: number;
|
||||||
|
messages: MessageContext[];
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
}): Promise<Array<{ path: string; contentType?: string }>> {
|
||||||
|
const results: Array<{ path: string; contentType?: string }> = [];
|
||||||
|
for (const message of params.messages) {
|
||||||
|
if (!message.media) continue;
|
||||||
|
const resolved = await resolveMediaAttachment({
|
||||||
|
client: params.client,
|
||||||
|
mediaMaxMb: params.mediaMaxMb,
|
||||||
|
media: message.media,
|
||||||
|
}).catch((err) => {
|
||||||
|
params.runtime.error?.(`telegram-user media download failed: ${String(err)}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (resolved) results.push(resolved);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTelegramUserMessageHandler(params: TelegramUserHandlerParams) {
|
||||||
|
const { client, cfg, runtime, accountId, accountConfig, self, abortSignal } = params;
|
||||||
|
const core = getTelegramUserRuntime();
|
||||||
|
const textLimit = accountConfig.textChunkLimit ?? DEFAULT_TEXT_LIMIT;
|
||||||
|
const mediaMaxMb = accountConfig.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
|
||||||
|
const dmPolicy = accountConfig.dmPolicy ?? "pairing";
|
||||||
|
const allowFrom = accountConfig.allowFrom ?? [];
|
||||||
|
const groupAllowFrom = accountConfig.groupAllowFrom ?? allowFrom;
|
||||||
|
|
||||||
|
return async (msg: MessageContext) => {
|
||||||
|
if (abortSignal?.aborted) return;
|
||||||
|
if (isClientDestroyed(client)) return;
|
||||||
|
try {
|
||||||
|
if (msg.isOutgoing || msg.isService) return;
|
||||||
|
const messageGroup = msg.isMessageGroup ? msg.messages : [msg];
|
||||||
|
const isDirect = msg.chat.type === "user";
|
||||||
|
const isGroup =
|
||||||
|
msg.chat.type === "chat" && msg.chat.chatType !== "channel";
|
||||||
|
if (!isDirect && !isGroup) return;
|
||||||
|
|
||||||
|
const sender = await msg.getCompleteSender().catch(() => msg.sender);
|
||||||
|
if (sender.type !== "user") return;
|
||||||
|
if ("isSelf" in sender && sender.isSelf) return;
|
||||||
|
if (self?.id != null && sender.id === self.id) return;
|
||||||
|
|
||||||
|
const senderId = String(sender.id);
|
||||||
|
const senderPeer = resolveTelegramUserPeer(senderId);
|
||||||
|
const senderUsername = "username" in sender ? sender.username : null;
|
||||||
|
const senderName = "displayName" in sender ? sender.displayName : senderId;
|
||||||
|
const storeAllowFrom = await core.channel.pairing
|
||||||
|
.readAllowFromStore("telegram-user")
|
||||||
|
.catch(() => []);
|
||||||
|
const combinedAllowFrom = [...allowFrom, ...storeAllowFrom];
|
||||||
|
const chatId = msg.chat.type === "chat" ? msg.chat.id : undefined;
|
||||||
|
const isForum = msg.chat.type === "chat" && msg.chat.isForum === true;
|
||||||
|
const isTopicMessage = msg.isTopicMessage === true;
|
||||||
|
const threadId =
|
||||||
|
isGroup && isForum && isTopicMessage ? msg.replyToMessage?.threadId ?? undefined : undefined;
|
||||||
|
const { groupConfig, topicConfig } =
|
||||||
|
isGroup && chatId != null
|
||||||
|
? resolveTelegramUserGroupConfig(accountConfig, chatId, threadId)
|
||||||
|
: { groupConfig: undefined, topicConfig: undefined };
|
||||||
|
|
||||||
|
const groupAllowOverride = firstDefined(
|
||||||
|
topicConfig?.allowFrom,
|
||||||
|
groupConfig?.allowFrom,
|
||||||
|
);
|
||||||
|
const groupAllowEntries = [
|
||||||
|
...((groupAllowOverride ?? groupAllowFrom) as Array<string | number>),
|
||||||
|
...storeAllowFrom,
|
||||||
|
];
|
||||||
|
const effectiveGroupAllow = parseAllowlist(groupAllowEntries);
|
||||||
|
const effectiveDmAllow = parseAllowlist(combinedAllowFrom);
|
||||||
|
|
||||||
|
if (isDirect) {
|
||||||
|
if (dmPolicy === "disabled") return;
|
||||||
|
if (
|
||||||
|
dmPolicy !== "open" &&
|
||||||
|
!isSenderAllowed({
|
||||||
|
allowFrom: combinedAllowFrom,
|
||||||
|
senderId,
|
||||||
|
senderUsername,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
if (dmPolicy === "pairing") {
|
||||||
|
const pairing = await core.channel.pairing.upsertPairingRequest({
|
||||||
|
channel: "telegram-user",
|
||||||
|
id: senderId,
|
||||||
|
meta: {
|
||||||
|
username: senderUsername ?? undefined,
|
||||||
|
name: senderName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const reply = core.channel.pairing.buildPairingReply({
|
||||||
|
channel: "telegram-user",
|
||||||
|
idLine: `Telegram user id: ${senderId}`,
|
||||||
|
code: pairing.code,
|
||||||
|
});
|
||||||
|
await sendMessageTelegramUser(`telegram-user:${senderId}`, reply, {
|
||||||
|
client,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (isGroup) {
|
||||||
|
if (groupConfig?.enabled === false) return;
|
||||||
|
if (topicConfig?.enabled === false) return;
|
||||||
|
if (typeof groupAllowOverride !== "undefined") {
|
||||||
|
const allowed = isSenderAllowed({
|
||||||
|
allowFrom: groupAllowEntries,
|
||||||
|
senderId,
|
||||||
|
senderUsername,
|
||||||
|
});
|
||||||
|
if (!allowed) return;
|
||||||
|
}
|
||||||
|
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||||
|
const groupPolicy =
|
||||||
|
accountConfig.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||||
|
if (groupPolicy === "disabled") return;
|
||||||
|
if (groupPolicy === "allowlist") {
|
||||||
|
if (!senderId) return;
|
||||||
|
if (!effectiveGroupAllow.hasEntries) return;
|
||||||
|
if (
|
||||||
|
!isSenderAllowed({
|
||||||
|
allowFrom: groupAllowEntries,
|
||||||
|
senderId,
|
||||||
|
senderUsername,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chatId != null) {
|
||||||
|
const groupAllowlist = core.channel.groups.resolveGroupPolicy({
|
||||||
|
cfg,
|
||||||
|
channel: "telegram-user",
|
||||||
|
groupId: String(chatId),
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryMessage =
|
||||||
|
messageGroup.find((entry) => entry.text?.trim()) ?? msg;
|
||||||
|
const text = primaryMessage.text?.trim() ?? "";
|
||||||
|
const locationData = extractTelegramUserLocation(primaryMessage.media);
|
||||||
|
const locationText = locationData ? formatLocationText(locationData) : undefined;
|
||||||
|
const pollText = formatTelegramUserPoll(primaryMessage.media);
|
||||||
|
const allMedia = await resolveMediaAttachments({
|
||||||
|
client,
|
||||||
|
mediaMaxMb,
|
||||||
|
messages: messageGroup,
|
||||||
|
runtime,
|
||||||
|
});
|
||||||
|
const media = allMedia[0] ?? null;
|
||||||
|
const rawBody = [text, locationText, pollText].filter(Boolean).join("\n").trim();
|
||||||
|
if (!rawBody && !media) return;
|
||||||
|
const timestampMs = resolveTelegramUserTimestampMs(msg.date);
|
||||||
|
const replyInfo = msg.replyToMessage ?? null;
|
||||||
|
const replyToId = replyInfo?.id != null ? String(replyInfo.id) : undefined;
|
||||||
|
const replyToSender = replyInfo?.sender
|
||||||
|
? describeReplySender(replyInfo.sender)
|
||||||
|
: undefined;
|
||||||
|
let replyToBody: string | undefined;
|
||||||
|
if (replyToId) {
|
||||||
|
const replyMessage = await msg.getReplyTo().catch(() => null);
|
||||||
|
replyToBody = replyMessage?.text?.trim() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
core.channel.activity.record({
|
||||||
|
channel: "telegram-user",
|
||||||
|
accountId,
|
||||||
|
direction: "inbound",
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupPeerId =
|
||||||
|
isGroup && chatId != null
|
||||||
|
? buildTelegramUserGroupPeerId(chatId, threadId)
|
||||||
|
: null;
|
||||||
|
const route = core.channel.routing.resolveAgentRoute({
|
||||||
|
cfg,
|
||||||
|
channel: "telegram-user",
|
||||||
|
accountId,
|
||||||
|
peer: {
|
||||||
|
kind: isGroup ? "group" : "dm",
|
||||||
|
id: isGroup && groupPeerId ? groupPeerId : senderId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mentionRegexes = [
|
||||||
|
...core.channel.mentions.buildMentionRegexes(cfg, route.agentId),
|
||||||
|
...buildTelegramUserSelfMentionRegexes({ username: self?.username, name: self?.name }),
|
||||||
|
];
|
||||||
|
const entities = msg.entities ?? [];
|
||||||
|
const hasAnyMention = entities.some(
|
||||||
|
(ent) => ent.kind === "mention" || ent.kind === "text_mention",
|
||||||
|
);
|
||||||
|
const hasControlCommandInMessage = core.channel.text.hasControlCommand(text, cfg, {
|
||||||
|
botUsername: self?.username?.trim().toLowerCase(),
|
||||||
|
});
|
||||||
|
const allowForCommands = isGroup ? effectiveGroupAllow : effectiveDmAllow;
|
||||||
|
const senderAllowedForCommands = isSenderAllowed({
|
||||||
|
allowFrom: isGroup ? groupAllowEntries : combinedAllowFrom,
|
||||||
|
senderId,
|
||||||
|
senderUsername,
|
||||||
|
});
|
||||||
|
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||||
|
const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
||||||
|
useAccessGroups,
|
||||||
|
authorizers: [{ configured: allowForCommands.hasEntries, allowed: senderAllowedForCommands }],
|
||||||
|
});
|
||||||
|
if (isGroup && hasControlCommandInMessage && !commandAuthorized) return;
|
||||||
|
|
||||||
|
const computedWasMentioned =
|
||||||
|
msg.isMention || core.channel.mentions.matchesMentionPatterns(text, mentionRegexes);
|
||||||
|
const baseRequireMention = isGroup
|
||||||
|
? core.channel.groups.resolveRequireMention({
|
||||||
|
cfg,
|
||||||
|
channel: "telegram-user",
|
||||||
|
groupId: chatId != null ? String(chatId) : undefined,
|
||||||
|
accountId,
|
||||||
|
})
|
||||||
|
: false;
|
||||||
|
const requireMention = firstDefined(
|
||||||
|
topicConfig?.requireMention,
|
||||||
|
groupConfig?.requireMention,
|
||||||
|
baseRequireMention,
|
||||||
|
);
|
||||||
|
const replySenderId =
|
||||||
|
msg.replyToMessage?.sender?.type === "user"
|
||||||
|
? msg.replyToMessage.sender.id
|
||||||
|
: undefined;
|
||||||
|
const implicitMention =
|
||||||
|
isGroup && Boolean(requireMention) && self?.id != null && replySenderId === self.id;
|
||||||
|
const canDetectMention =
|
||||||
|
Boolean(self?.username) || mentionRegexes.length > 0 || msg.isMention;
|
||||||
|
const mentionGate = resolveMentionGatingWithBypass({
|
||||||
|
isGroup,
|
||||||
|
requireMention: Boolean(requireMention),
|
||||||
|
canDetectMention,
|
||||||
|
wasMentioned: computedWasMentioned,
|
||||||
|
implicitMention,
|
||||||
|
hasAnyMention,
|
||||||
|
allowTextCommands: true,
|
||||||
|
hasControlCommand: hasControlCommandInMessage,
|
||||||
|
commandAuthorized,
|
||||||
|
});
|
||||||
|
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
||||||
|
if (isGroup && requireMention && canDetectMention && mentionGate.shouldSkip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||||
|
const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false;
|
||||||
|
const ackReaction = resolveAckReaction(cfg, route.agentId);
|
||||||
|
const shouldAckReaction = () => {
|
||||||
|
if (!ackReaction) return false;
|
||||||
|
if (ackReactionScope === "all") return true;
|
||||||
|
if (ackReactionScope === "direct") return !isGroup;
|
||||||
|
if (ackReactionScope === "group-all") return isGroup;
|
||||||
|
if (ackReactionScope === "group-mentions") {
|
||||||
|
return isGroup && Boolean(requireMention) && canDetectMention && effectiveWasMentioned;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const ackReactionPromise = shouldAckReaction()
|
||||||
|
? client
|
||||||
|
.sendReaction({
|
||||||
|
chatId: isGroup && chatId != null ? chatId : senderPeer,
|
||||||
|
message: msg.id,
|
||||||
|
emoji: ackReaction,
|
||||||
|
})
|
||||||
|
.then(() => true)
|
||||||
|
.catch((err) => {
|
||||||
|
runtime.error?.(`telegram-user ack reaction failed: ${String(err)}`);
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
||||||
|
agentId: route.agentId,
|
||||||
|
});
|
||||||
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
||||||
|
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
||||||
|
storePath,
|
||||||
|
sessionKey: route.sessionKey,
|
||||||
|
});
|
||||||
|
const groupTitle = msg.chat.type === "chat" ? msg.chat.title : undefined;
|
||||||
|
const conversationLabel = isGroup && chatId != null
|
||||||
|
? buildTelegramUserGroupLabel(groupTitle, chatId, threadId)
|
||||||
|
: senderName;
|
||||||
|
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 mediaSuffix =
|
||||||
|
!rawBody && allMedia.length > 1 ? ` (${allMedia.length} items)` : "";
|
||||||
|
const body = core.channel.reply.formatAgentEnvelope({
|
||||||
|
channel: "Telegram User",
|
||||||
|
from: senderName,
|
||||||
|
timestamp: timestampMs,
|
||||||
|
previousTimestamp,
|
||||||
|
envelope: envelopeOptions,
|
||||||
|
body: rawBody || `(media${mediaSuffix})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
||||||
|
Body: body,
|
||||||
|
RawBody: text,
|
||||||
|
CommandBody: text,
|
||||||
|
From: isGroup && chatId != null ? buildTelegramUserGroupFrom(chatId, threadId) : `telegram-user:${senderId}`,
|
||||||
|
To: isGroup && chatId != null ? buildTelegramUserGroupFrom(chatId, threadId) : `telegram-user:${senderId}`,
|
||||||
|
SessionKey: route.sessionKey,
|
||||||
|
AccountId: route.accountId,
|
||||||
|
ChatType: isGroup ? "group" : "direct",
|
||||||
|
ConversationLabel: conversationLabel,
|
||||||
|
GroupSubject: isGroup ? groupTitle ?? undefined : undefined,
|
||||||
|
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
|
||||||
|
SenderName: senderName,
|
||||||
|
SenderId: senderId,
|
||||||
|
SenderUsername: senderUsername ?? undefined,
|
||||||
|
Provider: "telegram-user" as const,
|
||||||
|
Surface: "telegram-user" as const,
|
||||||
|
MessageSid: String(msg.id),
|
||||||
|
ReplyToId: replyToId ?? String(msg.id),
|
||||||
|
ReplyToBody: replyToBody,
|
||||||
|
ReplyToSender: replyToSender,
|
||||||
|
Timestamp: timestampMs,
|
||||||
|
MediaPath: media?.path,
|
||||||
|
MediaType: media?.contentType,
|
||||||
|
MediaUrl: media?.path,
|
||||||
|
MediaPaths: allMedia.length > 0 ? allMedia.map((item) => item.path) : undefined,
|
||||||
|
MediaUrls: allMedia.length > 0 ? allMedia.map((item) => item.path) : undefined,
|
||||||
|
MediaTypes:
|
||||||
|
allMedia.length > 0
|
||||||
|
? (allMedia
|
||||||
|
.map((item) => item.contentType)
|
||||||
|
.filter(Boolean) as string[])
|
||||||
|
: undefined,
|
||||||
|
CommandAuthorized: commandAuthorized,
|
||||||
|
CommandSource: "text" as const,
|
||||||
|
OriginatingChannel: "telegram-user" as const,
|
||||||
|
OriginatingTo:
|
||||||
|
isGroup && chatId != null
|
||||||
|
? buildTelegramUserGroupFrom(chatId, threadId)
|
||||||
|
: `telegram-user:${senderId}`,
|
||||||
|
WasMentioned: isGroup ? effectiveWasMentioned : undefined,
|
||||||
|
MessageThreadId: threadId,
|
||||||
|
IsForum: isForum,
|
||||||
|
...(locationData ? toLocationContext(locationData) : undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
void core.channel.session
|
||||||
|
.recordSessionMetaFromInbound({
|
||||||
|
storePath,
|
||||||
|
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
||||||
|
ctx: ctxPayload,
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
runtime.error?.(`telegram-user failed to update session meta: ${String(err)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isGroup) {
|
||||||
|
await core.channel.session.updateLastRoute({
|
||||||
|
storePath,
|
||||||
|
sessionKey: route.mainSessionKey,
|
||||||
|
channel: "telegram-user",
|
||||||
|
to: `telegram-user:${senderId}`,
|
||||||
|
accountId: route.accountId,
|
||||||
|
ctx: ctxPayload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasReplied = false;
|
||||||
|
const replyTarget =
|
||||||
|
isGroup && chatId != null ? `telegram-user:${chatId}` : `telegram-user:${senderId}`;
|
||||||
|
const typingTarget = isGroup && chatId != null ? chatId : senderPeer;
|
||||||
|
const typingParams = isGroup && threadId != null ? { threadId } : undefined;
|
||||||
|
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||||
|
core.channel.reply.createReplyDispatcherWithTyping({
|
||||||
|
responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||||
|
.responsePrefix,
|
||||||
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
||||||
|
deliver: async (payload) => {
|
||||||
|
if (abortSignal?.aborted) return;
|
||||||
|
if (isClientDestroyed(client)) return;
|
||||||
|
const replyToId = hasReplied ? undefined : msg.id;
|
||||||
|
const replyText = payload.text ?? "";
|
||||||
|
const mediaUrl = payload.mediaUrl;
|
||||||
|
if (mediaUrl) {
|
||||||
|
if (abortSignal?.aborted) return;
|
||||||
|
if (isClientDestroyed(client)) return;
|
||||||
|
if (payload.audioAsVoice) {
|
||||||
|
await safeSendTyping({
|
||||||
|
client,
|
||||||
|
target: typingTarget,
|
||||||
|
status: "record_voice",
|
||||||
|
typingParams,
|
||||||
|
runtime,
|
||||||
|
abortSignal,
|
||||||
|
logLabel: "voice typing",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await sendMediaTelegramUser(replyTarget, replyText, {
|
||||||
|
client,
|
||||||
|
accountId,
|
||||||
|
replyToId,
|
||||||
|
threadId,
|
||||||
|
mediaUrl,
|
||||||
|
audioAsVoice: payload.audioAsVoice === true,
|
||||||
|
maxBytes: mediaMaxMb * 1024 * 1024,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (isDestroyedClientError(err)) return;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
hasReplied = true;
|
||||||
|
core.channel.activity.record({
|
||||||
|
channel: "telegram-user",
|
||||||
|
accountId,
|
||||||
|
direction: "outbound",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (replyText) {
|
||||||
|
for (const chunk of core.channel.text.chunkMarkdownText(replyText, textLimit)) {
|
||||||
|
if (abortSignal?.aborted) return;
|
||||||
|
if (isClientDestroyed(client)) return;
|
||||||
|
const trimmed = chunk.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
try {
|
||||||
|
await sendMessageTelegramUser(replyTarget, trimmed, {
|
||||||
|
client,
|
||||||
|
accountId,
|
||||||
|
replyToId,
|
||||||
|
threadId,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (isDestroyedClientError(err)) return;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
hasReplied = true;
|
||||||
|
core.channel.activity.record({
|
||||||
|
channel: "telegram-user",
|
||||||
|
accountId,
|
||||||
|
direction: "outbound",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onReplyStart: async () => {
|
||||||
|
await safeSendTyping({
|
||||||
|
client,
|
||||||
|
target: typingTarget,
|
||||||
|
status: "typing",
|
||||||
|
typingParams,
|
||||||
|
runtime,
|
||||||
|
abortSignal,
|
||||||
|
logLabel: "typing",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
runtime.error?.(`telegram-user reply failed: ${String(err)}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await core.channel.reply.dispatchReplyFromConfig({
|
||||||
|
ctx: ctxPayload,
|
||||||
|
cfg,
|
||||||
|
dispatcher,
|
||||||
|
replyOptions: {
|
||||||
|
...replyOptions,
|
||||||
|
skillFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
markDispatchIdle();
|
||||||
|
|
||||||
|
if (removeAckAfterReply && ackReactionPromise) {
|
||||||
|
const didAck = await ackReactionPromise;
|
||||||
|
if (didAck) {
|
||||||
|
await client
|
||||||
|
.sendReaction({
|
||||||
|
chatId: isGroup && chatId != null ? chatId : senderPeer,
|
||||||
|
message: msg.id,
|
||||||
|
emoji: null,
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
runtime.error?.(`telegram-user ack reaction cleanup failed: ${String(err)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error?.(`telegram-user handler failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
162
extensions/telegram-user/src/monitor/index.ts
Normal file
162
extensions/telegram-user/src/monitor/index.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
import { createTelegramUserClient } from "../client.js";
|
||||||
|
import { resolveTelegramUserAccount } from "../accounts.js";
|
||||||
|
import { resolveTelegramUserSessionPath } from "../session.js";
|
||||||
|
import { getTelegramUserRuntime } from "../runtime.js";
|
||||||
|
import { setActiveTelegramUserClient } from "../active-client.js";
|
||||||
|
import { createTelegramUserMessageHandler } from "./handler.js";
|
||||||
|
import type { CoreConfig } from "../types.js";
|
||||||
|
|
||||||
|
type MtcuteDispatcher = typeof import("@mtcute/dispatcher");
|
||||||
|
|
||||||
|
let mtcuteDispatcherPromise: Promise<MtcuteDispatcher> | null = null;
|
||||||
|
|
||||||
|
async function loadMtcuteDispatcher(): Promise<MtcuteDispatcher> {
|
||||||
|
mtcuteDispatcherPromise ??= import("@mtcute/dispatcher");
|
||||||
|
return mtcuteDispatcherPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDestroyedClientError(err: unknown): boolean {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
return /client is destroyed/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MonitorTelegramUserOpts = {
|
||||||
|
runtime?: RuntimeEnv;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
|
accountId?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts = {}) {
|
||||||
|
const core = getTelegramUserRuntime();
|
||||||
|
const cfg = core.config.loadConfig() as CoreConfig;
|
||||||
|
const account = resolveTelegramUserAccount({
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
if (!account.enabled) return;
|
||||||
|
|
||||||
|
let shuttingDown = false;
|
||||||
|
|
||||||
|
const apiId = account.credentials.apiId;
|
||||||
|
const apiHash = account.credentials.apiHash;
|
||||||
|
if (!apiId || !apiHash) {
|
||||||
|
throw new Error("Telegram user credentials missing (apiId/apiHash required).");
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtime: RuntimeEnv =
|
||||||
|
opts.runtime ??
|
||||||
|
({
|
||||||
|
log: (message: string) => core.logging.getChildLogger({ module: "telegram-user" }).info(message),
|
||||||
|
error: (message: string) =>
|
||||||
|
core.logging.getChildLogger({ module: "telegram-user" }).error(message),
|
||||||
|
exit: (code: number): never => {
|
||||||
|
throw new Error(`exit ${code}`);
|
||||||
|
},
|
||||||
|
} satisfies RuntimeEnv);
|
||||||
|
|
||||||
|
const storagePath = resolveTelegramUserSessionPath(account.accountId);
|
||||||
|
if (!fs.existsSync(storagePath)) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram user session missing. Run `openclaw channels login --channel telegram-user` first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const client = await createTelegramUserClient({ apiId, apiHash, storagePath });
|
||||||
|
let stopped = false;
|
||||||
|
|
||||||
|
const stop = async () => {
|
||||||
|
if (stopped) return;
|
||||||
|
stopped = true;
|
||||||
|
shuttingDown = true;
|
||||||
|
setActiveTelegramUserClient(account.accountId, null);
|
||||||
|
await client.destroy().catch(() => undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
opts.abortSignal?.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() => {
|
||||||
|
shuttingDown = true;
|
||||||
|
void stop();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.start();
|
||||||
|
setActiveTelegramUserClient(account.accountId, client);
|
||||||
|
|
||||||
|
const { Dispatcher, filters } = await loadMtcuteDispatcher();
|
||||||
|
const dispatcher = Dispatcher.for(client);
|
||||||
|
const self = await client.getMe().catch(() => undefined);
|
||||||
|
const selfName =
|
||||||
|
self && typeof (self as unknown as { displayName?: unknown }).displayName === "string"
|
||||||
|
? (self as unknown as { displayName: string }).displayName
|
||||||
|
: self && typeof (self as unknown as { firstName?: unknown }).firstName === "string"
|
||||||
|
? [
|
||||||
|
(self as unknown as { firstName?: string }).firstName,
|
||||||
|
typeof (self as unknown as { lastName?: unknown }).lastName === "string"
|
||||||
|
? (self as unknown as { lastName: string }).lastName
|
||||||
|
: undefined,
|
||||||
|
]
|
||||||
|
.filter((entry): entry is string => Boolean(entry && entry.trim()))
|
||||||
|
.join(" ")
|
||||||
|
: undefined;
|
||||||
|
const handleMessage = createTelegramUserMessageHandler({
|
||||||
|
client,
|
||||||
|
cfg,
|
||||||
|
runtime,
|
||||||
|
accountId: account.accountId,
|
||||||
|
accountConfig: account.config,
|
||||||
|
abortSignal: opts.abortSignal,
|
||||||
|
self: self
|
||||||
|
? {
|
||||||
|
id: self.id,
|
||||||
|
username: "username" in self ? self.username : undefined,
|
||||||
|
name: selfName,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatcher.onNewMessage(
|
||||||
|
filters.or(
|
||||||
|
filters.chat("user"),
|
||||||
|
filters.chat("group"),
|
||||||
|
filters.chat("supergroup"),
|
||||||
|
filters.chat("gigagroup"),
|
||||||
|
),
|
||||||
|
handleMessage,
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
let settled = false;
|
||||||
|
const settleResolve = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const settleReject = (err: unknown) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.onError.add((err) => {
|
||||||
|
if (shuttingDown || opts.abortSignal?.aborted || isDestroyedClientError(err)) {
|
||||||
|
settleResolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runtime.error?.(`telegram-user client error: ${String(err)}`);
|
||||||
|
settleReject(err);
|
||||||
|
});
|
||||||
|
if (opts.abortSignal?.aborted) {
|
||||||
|
settleResolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
opts.abortSignal?.addEventListener("abort", () => settleResolve(), { once: true });
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
375
extensions/telegram-user/src/onboarding.ts
Normal file
375
extensions/telegram-user/src/onboarding.ts
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
import {
|
||||||
|
addWildcardAllowFrom,
|
||||||
|
formatDocsLink,
|
||||||
|
promptAccountId,
|
||||||
|
DEFAULT_ACCOUNT_ID,
|
||||||
|
normalizeAccountId,
|
||||||
|
type ChannelOnboardingAdapter,
|
||||||
|
type ChannelOnboardingDmPolicy,
|
||||||
|
type OpenClawConfig,
|
||||||
|
type DmPolicy,
|
||||||
|
type WizardPrompter,
|
||||||
|
} from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
import {
|
||||||
|
listTelegramUserAccountIds,
|
||||||
|
resolveDefaultTelegramUserAccountId,
|
||||||
|
resolveTelegramUserAccount,
|
||||||
|
} from "./accounts.js";
|
||||||
|
import { loginTelegramUser } from "./login.js";
|
||||||
|
import { resolveTelegramUserSessionPath } from "./session.js";
|
||||||
|
import type { CoreConfig } from "./types.js";
|
||||||
|
|
||||||
|
const channel = "telegram-user" as const;
|
||||||
|
type TelegramUserChannelConfig = NonNullable<CoreConfig["channels"]>["telegram-user"];
|
||||||
|
|
||||||
|
function setTelegramUserDmPolicy(
|
||||||
|
cfg: OpenClawConfig,
|
||||||
|
policy: DmPolicy,
|
||||||
|
accountId?: string,
|
||||||
|
): OpenClawConfig {
|
||||||
|
const resolvedAccountId = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID;
|
||||||
|
const current = cfg.channels?.["telegram-user"] as TelegramUserChannelConfig | undefined;
|
||||||
|
const allowFrom =
|
||||||
|
policy === "open"
|
||||||
|
? addWildcardAllowFrom(current?.allowFrom)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
"telegram-user": {
|
||||||
|
...(current ?? {}),
|
||||||
|
dmPolicy: policy,
|
||||||
|
...(allowFrom ? { allowFrom } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
"telegram-user": {
|
||||||
|
...(current ?? {}),
|
||||||
|
accounts: {
|
||||||
|
...(current?.accounts ?? {}),
|
||||||
|
[resolvedAccountId]: {
|
||||||
|
...(current?.accounts?.[resolvedAccountId] ?? {}),
|
||||||
|
dmPolicy: policy,
|
||||||
|
...(allowFrom ? { allowFrom } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function noteTelegramUserAuthHelp(prompter: WizardPrompter): Promise<void> {
|
||||||
|
await prompter.note(
|
||||||
|
[
|
||||||
|
"Telegram User (MTProto) needs an API ID + API hash from my.telegram.org.",
|
||||||
|
"You can store them in config or set TELEGRAM_USER_API_ID/TELEGRAM_USER_API_HASH.",
|
||||||
|
"Login happens via `openclaw channels login --channel telegram-user`.",
|
||||||
|
`Docs: ${formatDocsLink("/channels/telegram-user", "channels/telegram-user")}`,
|
||||||
|
].join("\n"),
|
||||||
|
"Telegram user setup",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAllowFromInput(raw: string): string[] {
|
||||||
|
return raw
|
||||||
|
.split(/[\n,;]+/g)
|
||||||
|
.map((entry) =>
|
||||||
|
entry
|
||||||
|
.trim()
|
||||||
|
.replace(/^(telegram-user|telegram|tg):/i, "")
|
||||||
|
.replace(/^user:/i, "")
|
||||||
|
.trim(),
|
||||||
|
)
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptTelegramUserAllowFrom(params: {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
accountId?: string;
|
||||||
|
}): Promise<OpenClawConfig> {
|
||||||
|
const accountId = normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID;
|
||||||
|
const resolved = resolveTelegramUserAccount({
|
||||||
|
cfg: params.cfg as CoreConfig,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
||||||
|
|
||||||
|
const entry = await params.prompter.text({
|
||||||
|
message: "Telegram user allowFrom (user id or @username)",
|
||||||
|
placeholder: "@username",
|
||||||
|
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
||||||
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = parseAllowFromInput(String(entry));
|
||||||
|
const merged = [
|
||||||
|
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
||||||
|
...parsed,
|
||||||
|
];
|
||||||
|
const unique = [...new Set(merged)];
|
||||||
|
const current = params.cfg.channels?.["telegram-user"] as TelegramUserChannelConfig | undefined;
|
||||||
|
|
||||||
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
return {
|
||||||
|
...params.cfg,
|
||||||
|
channels: {
|
||||||
|
...params.cfg.channels,
|
||||||
|
"telegram-user": {
|
||||||
|
...(current ?? {}),
|
||||||
|
enabled: true,
|
||||||
|
dmPolicy: "allowlist",
|
||||||
|
allowFrom: unique,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...params.cfg,
|
||||||
|
channels: {
|
||||||
|
...params.cfg.channels,
|
||||||
|
"telegram-user": {
|
||||||
|
...(current ?? {}),
|
||||||
|
enabled: true,
|
||||||
|
accounts: {
|
||||||
|
...(current?.accounts ?? {}),
|
||||||
|
[accountId]: {
|
||||||
|
...(current?.accounts?.[accountId] ?? {}),
|
||||||
|
enabled: true,
|
||||||
|
dmPolicy: "allowlist",
|
||||||
|
allowFrom: unique,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||||
|
label: "Telegram User",
|
||||||
|
channel,
|
||||||
|
policyKey: "channels.telegram-user.dmPolicy",
|
||||||
|
allowFromKey: "channels.telegram-user.allowFrom",
|
||||||
|
getCurrent: (cfg) =>
|
||||||
|
(cfg as CoreConfig).channels?.["telegram-user"]?.dmPolicy ?? "pairing",
|
||||||
|
setPolicy: (cfg, policy) => setTelegramUserDmPolicy(cfg, policy),
|
||||||
|
promptAllowFrom: async ({ cfg, prompter, accountId }) =>
|
||||||
|
await promptTelegramUserAllowFrom({ cfg, prompter, accountId }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
|
channel,
|
||||||
|
getStatus: async ({ cfg }) => {
|
||||||
|
const configured = listTelegramUserAccountIds(cfg as CoreConfig).some((accountId) => {
|
||||||
|
const resolved = resolveTelegramUserAccount({ cfg: cfg as CoreConfig, accountId });
|
||||||
|
return Boolean(resolved.credentials.apiId && resolved.credentials.apiHash);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
channel,
|
||||||
|
configured,
|
||||||
|
statusLines: [
|
||||||
|
`Telegram User: ${configured ? "configured" : "needs API ID + API hash"}`,
|
||||||
|
],
|
||||||
|
selectionHint: configured ? "configured" : "needs credentials",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
configure: async ({
|
||||||
|
cfg,
|
||||||
|
runtime,
|
||||||
|
prompter,
|
||||||
|
accountOverrides,
|
||||||
|
shouldPromptAccountIds,
|
||||||
|
forceAllowFrom,
|
||||||
|
}) => {
|
||||||
|
const override = accountOverrides["telegram-user"]?.trim();
|
||||||
|
const defaultAccountId = resolveDefaultTelegramUserAccountId(cfg as CoreConfig);
|
||||||
|
let accountId = override ? normalizeAccountId(override) : defaultAccountId;
|
||||||
|
if (shouldPromptAccountIds && !override) {
|
||||||
|
accountId = await promptAccountId({
|
||||||
|
cfg: cfg as OpenClawConfig,
|
||||||
|
prompter,
|
||||||
|
label: "Telegram User",
|
||||||
|
currentId: accountId ?? defaultAccountId,
|
||||||
|
listAccountIds: (next) => listTelegramUserAccountIds(next as CoreConfig),
|
||||||
|
defaultAccountId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const resolvedAccountId = normalizeAccountId(accountId) ?? defaultAccountId;
|
||||||
|
|
||||||
|
let next = cfg as CoreConfig;
|
||||||
|
const resolved = resolveTelegramUserAccount({
|
||||||
|
cfg: next,
|
||||||
|
accountId: resolvedAccountId,
|
||||||
|
});
|
||||||
|
const configured = Boolean(resolved.credentials.apiId && resolved.credentials.apiHash);
|
||||||
|
|
||||||
|
if (!configured) {
|
||||||
|
await noteTelegramUserAuthHelp(prompter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envApiId = process.env.TELEGRAM_USER_API_ID?.trim();
|
||||||
|
const envApiHash = process.env.TELEGRAM_USER_API_HASH?.trim();
|
||||||
|
const canUseEnv =
|
||||||
|
resolvedAccountId === DEFAULT_ACCOUNT_ID && Boolean(envApiId && envApiHash);
|
||||||
|
const hasConfig = Boolean(resolved.config.apiId && resolved.config.apiHash);
|
||||||
|
|
||||||
|
let useEnv = false;
|
||||||
|
if (canUseEnv && !hasConfig) {
|
||||||
|
useEnv = await prompter.confirm({
|
||||||
|
message: "Telegram user env vars detected. Use env values?",
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let apiId = resolved.config.apiId;
|
||||||
|
let apiHash = resolved.config.apiHash;
|
||||||
|
if (!useEnv && (!apiId || !apiHash)) {
|
||||||
|
if (configured) {
|
||||||
|
const keep = await prompter.confirm({
|
||||||
|
message: "Telegram user credentials already configured. Keep them?",
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
if (!keep) {
|
||||||
|
apiId = undefined;
|
||||||
|
apiHash = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!apiId || !apiHash) {
|
||||||
|
const apiIdRaw = String(
|
||||||
|
await prompter.text({
|
||||||
|
message: "Telegram API ID",
|
||||||
|
initialValue: apiId ? String(apiId) : envApiId,
|
||||||
|
validate: (value) =>
|
||||||
|
Number.isFinite(Number.parseInt(String(value ?? ""), 10))
|
||||||
|
? undefined
|
||||||
|
: "Enter a numeric API ID",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
apiId = Number.parseInt(apiIdRaw, 10);
|
||||||
|
apiHash = String(
|
||||||
|
await prompter.text({
|
||||||
|
message: "Telegram API hash",
|
||||||
|
initialValue: apiHash ?? envApiHash,
|
||||||
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||||
|
}),
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = next.channels?.["telegram-user"] as TelegramUserChannelConfig | undefined;
|
||||||
|
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
next = {
|
||||||
|
...next,
|
||||||
|
channels: {
|
||||||
|
...next.channels,
|
||||||
|
"telegram-user": {
|
||||||
|
...(current ?? {}),
|
||||||
|
enabled: true,
|
||||||
|
...(useEnv
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
apiId,
|
||||||
|
apiHash,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
next = {
|
||||||
|
...next,
|
||||||
|
channels: {
|
||||||
|
...next.channels,
|
||||||
|
"telegram-user": {
|
||||||
|
...(current ?? {}),
|
||||||
|
enabled: true,
|
||||||
|
accounts: {
|
||||||
|
...(current?.accounts ?? {}),
|
||||||
|
[resolvedAccountId]: {
|
||||||
|
...(current?.accounts?.[resolvedAccountId] ?? {}),
|
||||||
|
enabled: true,
|
||||||
|
...(useEnv
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
apiId,
|
||||||
|
apiHash,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceAllowFrom) {
|
||||||
|
next = await promptTelegramUserAllowFrom({
|
||||||
|
cfg: next,
|
||||||
|
prompter,
|
||||||
|
accountId: resolvedAccountId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const wantsLogin = await prompter.confirm({
|
||||||
|
message: "Link Telegram user now (QR or phone code)?",
|
||||||
|
initialValue: !configured,
|
||||||
|
});
|
||||||
|
if (wantsLogin) {
|
||||||
|
const refreshed = resolveTelegramUserAccount({
|
||||||
|
cfg: next,
|
||||||
|
accountId: resolvedAccountId,
|
||||||
|
});
|
||||||
|
if (!refreshed.credentials.apiId || !refreshed.credentials.apiHash) {
|
||||||
|
await prompter.note(
|
||||||
|
"Telegram API ID/hash missing. Add credentials first, then retry login.",
|
||||||
|
"Telegram user login",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await loginTelegramUser({
|
||||||
|
apiId: refreshed.credentials.apiId,
|
||||||
|
apiHash: refreshed.credentials.apiHash,
|
||||||
|
storagePath: resolveTelegramUserSessionPath(resolvedAccountId),
|
||||||
|
runtime,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error(`Telegram user login failed: ${String(err)}`);
|
||||||
|
await prompter.note(
|
||||||
|
`Run \`openclaw channels login --channel telegram-user\` later to link.`,
|
||||||
|
"Telegram user login",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await prompter.note(
|
||||||
|
[
|
||||||
|
"Next: link the account via QR or phone code.",
|
||||||
|
"Run: openclaw channels login --channel telegram-user",
|
||||||
|
].join("\n"),
|
||||||
|
"Telegram user login",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cfg: next, accountId: resolvedAccountId };
|
||||||
|
},
|
||||||
|
dmPolicy,
|
||||||
|
disable: (cfg) => ({
|
||||||
|
...(cfg as CoreConfig),
|
||||||
|
channels: {
|
||||||
|
...(cfg as CoreConfig).channels,
|
||||||
|
"telegram-user": {
|
||||||
|
...(cfg as CoreConfig).channels?.["telegram-user"],
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
14
extensions/telegram-user/src/runtime.ts
Normal file
14
extensions/telegram-user/src/runtime.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
let runtime: PluginRuntime | null = null;
|
||||||
|
|
||||||
|
export function setTelegramUserRuntime(next: PluginRuntime) {
|
||||||
|
runtime = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTelegramUserRuntime(): PluginRuntime {
|
||||||
|
if (!runtime) {
|
||||||
|
throw new Error("Telegram user runtime not initialized");
|
||||||
|
}
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
110
extensions/telegram-user/src/send.test.ts
Normal file
110
extensions/telegram-user/src/send.test.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const loadWebMedia = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("./runtime.js", () => {
|
||||||
|
return {
|
||||||
|
getTelegramUserRuntime: () => ({
|
||||||
|
config: { loadConfig: () => ({}) },
|
||||||
|
media: {
|
||||||
|
loadWebMedia: (...args: unknown[]) => loadWebMedia(...args),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputMediaAuto = vi.fn((file: unknown, params: unknown) => ({
|
||||||
|
type: "auto",
|
||||||
|
file,
|
||||||
|
...(params && typeof params === "object" ? params : {}),
|
||||||
|
}));
|
||||||
|
const inputMediaVoice = vi.fn((file: unknown, params: unknown) => ({
|
||||||
|
type: "voice",
|
||||||
|
file,
|
||||||
|
...(params && typeof params === "object" ? params : {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@mtcute/core", () => {
|
||||||
|
return {
|
||||||
|
InputMedia: {
|
||||||
|
auto: (...args: unknown[]) => inputMediaAuto(...args),
|
||||||
|
voice: (...args: unknown[]) => inputMediaVoice(...args),
|
||||||
|
poll: () => ({ type: "poll" }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("telegram-user send", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
loadWebMedia.mockReset();
|
||||||
|
inputMediaAuto.mockClear();
|
||||||
|
inputMediaVoice.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends audio media as voice note when audioAsVoice is set", async () => {
|
||||||
|
loadWebMedia.mockResolvedValue({
|
||||||
|
buffer: Buffer.from("voice"),
|
||||||
|
contentType: "audio/ogg",
|
||||||
|
fileName: "note.ogg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMedia = vi.fn(async () => ({ id: 123 }));
|
||||||
|
const { sendMediaTelegramUser } = await import("./send.js");
|
||||||
|
await sendMediaTelegramUser("telegram-user:123", "hi", {
|
||||||
|
client: { sendMedia } as unknown as import("@mtcute/node").TelegramClient,
|
||||||
|
mediaUrl: "https://example.com/note.ogg",
|
||||||
|
audioAsVoice: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(inputMediaVoice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendMedia).toHaveBeenCalledTimes(1);
|
||||||
|
const [, media] = sendMedia.mock.calls[0] ?? [];
|
||||||
|
expect(media).toMatchObject({ type: "voice" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to normal media when audioAsVoice is set but media is not voice-compatible", async () => {
|
||||||
|
loadWebMedia.mockResolvedValue({
|
||||||
|
buffer: Buffer.from("img"),
|
||||||
|
contentType: "image/png",
|
||||||
|
fileName: "image.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMedia = vi.fn(async () => ({ id: 123 }));
|
||||||
|
const { sendMediaTelegramUser } = await import("./send.js");
|
||||||
|
await sendMediaTelegramUser("telegram-user:123", "hi", {
|
||||||
|
client: { sendMedia } as unknown as import("@mtcute/node").TelegramClient,
|
||||||
|
mediaUrl: "https://example.com/image.png",
|
||||||
|
audioAsVoice: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(inputMediaVoice).toHaveBeenCalledTimes(0);
|
||||||
|
expect(inputMediaAuto).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to auto when voice messages are forbidden", async () => {
|
||||||
|
loadWebMedia.mockResolvedValue({
|
||||||
|
buffer: Buffer.from("voice"),
|
||||||
|
contentType: "audio/ogg",
|
||||||
|
fileName: "note.ogg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMedia = vi.fn(async (_to: unknown, media: unknown) => {
|
||||||
|
if (media && typeof media === "object" && (media as { type?: string }).type === "voice") {
|
||||||
|
throw new Error("VOICE_MESSAGES_FORBIDDEN");
|
||||||
|
}
|
||||||
|
return { id: 123 };
|
||||||
|
});
|
||||||
|
|
||||||
|
const { sendMediaTelegramUser } = await import("./send.js");
|
||||||
|
await sendMediaTelegramUser("telegram-user:123", "hi", {
|
||||||
|
client: { sendMedia } as unknown as import("@mtcute/node").TelegramClient,
|
||||||
|
mediaUrl: "https://example.com/note.ogg",
|
||||||
|
audioAsVoice: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(inputMediaVoice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(inputMediaAuto).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendMedia).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
301
extensions/telegram-user/src/send.ts
Normal file
301
extensions/telegram-user/src/send.ts
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import type { TelegramClient } from "@mtcute/node";
|
||||||
|
import type { PollInput } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
import { getTelegramUserRuntime } from "./runtime.js";
|
||||||
|
import { resolveTelegramUserAccount } from "./accounts.js";
|
||||||
|
import { createTelegramUserClient } from "./client.js";
|
||||||
|
import { resolveTelegramUserSessionPath } from "./session.js";
|
||||||
|
import { getActiveTelegramUserClient } from "./active-client.js";
|
||||||
|
import type { CoreConfig } from "./types.js";
|
||||||
|
|
||||||
|
export type TelegramUserSendResult = {
|
||||||
|
messageId: string;
|
||||||
|
chatId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MtcuteCore = typeof import("@mtcute/core");
|
||||||
|
|
||||||
|
let mtcuteCorePromise: Promise<MtcuteCore> | null = null;
|
||||||
|
|
||||||
|
async function loadMtcuteCore(): Promise<MtcuteCore> {
|
||||||
|
mtcuteCorePromise ??= import("@mtcute/core");
|
||||||
|
return mtcuteCorePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NormalizedPollInput = {
|
||||||
|
question: string;
|
||||||
|
options: string[];
|
||||||
|
maxSelections: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isDestroyedClientError(err: unknown): boolean {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
return /client is destroyed/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TelegramUserSendOpts = {
|
||||||
|
client?: TelegramClient;
|
||||||
|
accountId?: string;
|
||||||
|
replyToId?: number;
|
||||||
|
threadId?: string | number | null;
|
||||||
|
mediaUrl?: string;
|
||||||
|
audioAsVoice?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeTarget(raw: string): string {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) throw new Error("Recipient is required for Telegram User sends");
|
||||||
|
const withoutProvider = trimmed.replace(/^(telegram-user|telegram|tg):/i, "").trim();
|
||||||
|
const withoutPrefix = withoutProvider.replace(/^(user|group|channel|chat):/i, "").trim();
|
||||||
|
if (!withoutPrefix) throw new Error("Recipient is required for Telegram User sends");
|
||||||
|
return withoutPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseThreadId(value: string | number | null | undefined): number | undefined {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
||||||
|
}
|
||||||
|
const trimmed = typeof value === "string" ? value.trim() : "";
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
const parsed = Number.parseInt(trimmed, 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTargetAndThread(raw: string, threadId?: string | number | null) {
|
||||||
|
const normalized = normalizeTarget(raw);
|
||||||
|
const [base, topicRaw] = normalized.split(/:topic:/i);
|
||||||
|
const parsedThreadId = parseThreadId(threadId ?? topicRaw);
|
||||||
|
const target = (base ?? normalized).trim();
|
||||||
|
if (!target) throw new Error("Recipient is required for Telegram User sends");
|
||||||
|
return { target, threadId: parsedThreadId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVoiceMessagesForbidden(err: unknown): boolean {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
return /VOICE_MESSAGES_FORBIDDEN/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSendAsVoice(params: {
|
||||||
|
wantsVoice: boolean;
|
||||||
|
contentType?: string | null;
|
||||||
|
fileName?: string | null;
|
||||||
|
}): boolean {
|
||||||
|
if (!params.wantsVoice) return false;
|
||||||
|
const contentType = params.contentType?.toLowerCase() ?? "";
|
||||||
|
const fileName = params.fileName?.toLowerCase() ?? "";
|
||||||
|
if (/(^|\/)(ogg|opus)(;|$)/.test(contentType)) return true;
|
||||||
|
if (/\.(ogg|opus|oga)$/.test(fileName)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTelegramUserMessagingTarget(raw: string): string {
|
||||||
|
return normalizeTarget(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function looksLikeTelegramUserTargetId(value: string): boolean {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return false;
|
||||||
|
if (/^telegram-user:/i.test(trimmed)) return true;
|
||||||
|
if (/^(user|group|channel|chat):/i.test(trimmed)) return true;
|
||||||
|
if (/^-?\d+:topic:\d+$/i.test(trimmed)) return true;
|
||||||
|
return /^-?\d+$/.test(trimmed) || /^@?[a-z0-9_]{5,}$/i.test(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTelegramUserPeer(target: string): number | string {
|
||||||
|
if (/^-?\d+$/.test(target)) {
|
||||||
|
const parsed = Number.parseInt(target, 10);
|
||||||
|
if (Number.isFinite(parsed)) return parsed;
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePollInput(input: PollInput): NormalizedPollInput {
|
||||||
|
const question = input.question.trim();
|
||||||
|
if (!question) {
|
||||||
|
throw new Error("Poll question is required");
|
||||||
|
}
|
||||||
|
const options = (input.options ?? []).map((option) => option.trim()).filter(Boolean);
|
||||||
|
if (options.length < 2) {
|
||||||
|
throw new Error("Poll requires at least 2 options");
|
||||||
|
}
|
||||||
|
if (options.length > 10) {
|
||||||
|
throw new Error("Poll supports at most 10 options");
|
||||||
|
}
|
||||||
|
const maxSelectionsRaw = input.maxSelections;
|
||||||
|
const maxSelections =
|
||||||
|
typeof maxSelectionsRaw === "number" && Number.isFinite(maxSelectionsRaw)
|
||||||
|
? Math.floor(maxSelectionsRaw)
|
||||||
|
: 1;
|
||||||
|
if (maxSelections < 1) {
|
||||||
|
throw new Error("maxSelections must be at least 1");
|
||||||
|
}
|
||||||
|
if (maxSelections > options.length) {
|
||||||
|
throw new Error("maxSelections cannot exceed option count");
|
||||||
|
}
|
||||||
|
return { question, options, maxSelections };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveClient(params: {
|
||||||
|
client?: TelegramClient;
|
||||||
|
cfg: CoreConfig;
|
||||||
|
accountId?: string;
|
||||||
|
}): Promise<{ client: TelegramClient; stopOnDone: boolean }> {
|
||||||
|
if (params.client) return { client: params.client, stopOnDone: false };
|
||||||
|
const account = resolveTelegramUserAccount({
|
||||||
|
cfg: params.cfg,
|
||||||
|
accountId: params.accountId,
|
||||||
|
});
|
||||||
|
const active = getActiveTelegramUserClient(account.accountId);
|
||||||
|
if (active) return { client: active, stopOnDone: false };
|
||||||
|
const apiId = account.credentials.apiId;
|
||||||
|
const apiHash = account.credentials.apiHash;
|
||||||
|
if (!apiId || !apiHash) {
|
||||||
|
throw new Error("Telegram user credentials missing (apiId/apiHash required).");
|
||||||
|
}
|
||||||
|
const storagePath = resolveTelegramUserSessionPath(account.accountId);
|
||||||
|
if (!fs.existsSync(storagePath)) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram user session missing. Run `openclaw channels login --channel telegram-user` first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const client = await createTelegramUserClient({ apiId, apiHash, storagePath });
|
||||||
|
await client.start();
|
||||||
|
return { client, stopOnDone: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMessageTelegramUser(
|
||||||
|
to: string,
|
||||||
|
text: string,
|
||||||
|
opts: TelegramUserSendOpts = {},
|
||||||
|
): Promise<TelegramUserSendResult> {
|
||||||
|
const cfg = getTelegramUserRuntime().config.loadConfig() as CoreConfig;
|
||||||
|
const { client, stopOnDone } = await resolveClient({
|
||||||
|
client: opts.client,
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const resolved = resolveTargetAndThread(to, opts.threadId);
|
||||||
|
const target = resolveTelegramUserPeer(resolved.target);
|
||||||
|
let message: Awaited<ReturnType<TelegramClient["sendText"]>> | null = null;
|
||||||
|
try {
|
||||||
|
message = await client.sendText(target, text, {
|
||||||
|
...(opts.replyToId ? { replyTo: opts.replyToId } : {}),
|
||||||
|
...(resolved.threadId ? { threadId: resolved.threadId } : {}),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!isDestroyedClientError(err)) throw err;
|
||||||
|
}
|
||||||
|
if (!message) {
|
||||||
|
return { messageId: "", chatId: String(target) };
|
||||||
|
}
|
||||||
|
return { messageId: String(message.id), chatId: String(target) };
|
||||||
|
} finally {
|
||||||
|
if (stopOnDone) {
|
||||||
|
await client.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMediaTelegramUser(
|
||||||
|
to: string,
|
||||||
|
text: string,
|
||||||
|
opts: TelegramUserSendOpts & { mediaUrl: string; maxBytes?: number },
|
||||||
|
): Promise<TelegramUserSendResult> {
|
||||||
|
const cfg = getTelegramUserRuntime().config.loadConfig() as CoreConfig;
|
||||||
|
const { client, stopOnDone } = await resolveClient({
|
||||||
|
client: opts.client,
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { InputMedia } = await loadMtcuteCore();
|
||||||
|
const resolved = resolveTargetAndThread(to, opts.threadId);
|
||||||
|
const target = resolveTelegramUserPeer(resolved.target);
|
||||||
|
const media = await getTelegramUserRuntime().media.loadWebMedia(opts.mediaUrl, opts.maxBytes);
|
||||||
|
const wantsVoice = shouldSendAsVoice({
|
||||||
|
wantsVoice: opts.audioAsVoice === true,
|
||||||
|
contentType: media.contentType,
|
||||||
|
fileName: media.fileName,
|
||||||
|
});
|
||||||
|
const buildAuto = () =>
|
||||||
|
InputMedia.auto(media.buffer, {
|
||||||
|
fileName: media.fileName ?? undefined,
|
||||||
|
fileMime: media.contentType,
|
||||||
|
caption: text,
|
||||||
|
});
|
||||||
|
const buildVoice = () =>
|
||||||
|
InputMedia.voice(media.buffer, {
|
||||||
|
fileName: media.fileName ?? undefined,
|
||||||
|
fileMime: media.contentType,
|
||||||
|
caption: text,
|
||||||
|
});
|
||||||
|
const input = wantsVoice ? buildVoice() : buildAuto();
|
||||||
|
let message: Awaited<ReturnType<TelegramClient["sendMedia"]>> | null = null;
|
||||||
|
try {
|
||||||
|
message = await client.sendMedia(target, input, {
|
||||||
|
...(opts.replyToId ? { replyTo: opts.replyToId } : {}),
|
||||||
|
...(resolved.threadId ? { threadId: resolved.threadId } : {}),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (wantsVoice && isVoiceMessagesForbidden(err)) {
|
||||||
|
message = await client.sendMedia(target, buildAuto(), {
|
||||||
|
...(opts.replyToId ? { replyTo: opts.replyToId } : {}),
|
||||||
|
...(resolved.threadId ? { threadId: resolved.threadId } : {}),
|
||||||
|
});
|
||||||
|
} else if (!isDestroyedClientError(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!message) {
|
||||||
|
return { messageId: "", chatId: String(target) };
|
||||||
|
}
|
||||||
|
return { messageId: String(message.id), chatId: String(target) };
|
||||||
|
} finally {
|
||||||
|
if (stopOnDone) {
|
||||||
|
await client.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendPollTelegramUser(
|
||||||
|
to: string,
|
||||||
|
poll: PollInput,
|
||||||
|
opts: TelegramUserSendOpts = {},
|
||||||
|
): Promise<TelegramUserSendResult> {
|
||||||
|
const cfg = getTelegramUserRuntime().config.loadConfig() as CoreConfig;
|
||||||
|
const { client, stopOnDone } = await resolveClient({
|
||||||
|
client: opts.client,
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { InputMedia } = await loadMtcuteCore();
|
||||||
|
const resolved = resolveTargetAndThread(to, opts.threadId);
|
||||||
|
const target = resolveTelegramUserPeer(resolved.target);
|
||||||
|
const normalized = normalizePollInput(poll);
|
||||||
|
const input = InputMedia.poll({
|
||||||
|
question: normalized.question,
|
||||||
|
answers: normalized.options,
|
||||||
|
multiple: normalized.maxSelections > 1,
|
||||||
|
});
|
||||||
|
let message: Awaited<ReturnType<TelegramClient["sendMedia"]>> | null = null;
|
||||||
|
try {
|
||||||
|
message = await client.sendMedia(target, input, {
|
||||||
|
...(opts.replyToId ? { replyTo: opts.replyToId } : {}),
|
||||||
|
...(resolved.threadId ? { threadId: resolved.threadId } : {}),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!isDestroyedClientError(err)) throw err;
|
||||||
|
}
|
||||||
|
if (!message) {
|
||||||
|
return { messageId: "", chatId: String(target) };
|
||||||
|
}
|
||||||
|
return { messageId: String(message.id), chatId: String(target) };
|
||||||
|
} finally {
|
||||||
|
if (stopOnDone) {
|
||||||
|
await client.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
extensions/telegram-user/src/session.ts
Normal file
20
extensions/telegram-user/src/session.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { normalizeAccountId } from "openclaw/plugin-sdk";
|
||||||
|
import { getTelegramUserRuntime } from "./runtime.js";
|
||||||
|
|
||||||
|
export function resolveTelegramUserSessionPath(accountId?: string | null): string {
|
||||||
|
const normalized = normalizeAccountId(accountId);
|
||||||
|
const stateDir = getTelegramUserRuntime().state.resolveStateDir();
|
||||||
|
return path.join(stateDir, "telegram-user", `session-${normalized}.sqlite`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureTelegramUserSessionDir(params?: {
|
||||||
|
accountId?: string | null;
|
||||||
|
sessionPath?: string;
|
||||||
|
}): void {
|
||||||
|
const sessionPath =
|
||||||
|
params?.sessionPath ?? resolveTelegramUserSessionPath(params?.accountId);
|
||||||
|
fs.mkdirSync(path.dirname(sessionPath), { recursive: true });
|
||||||
|
}
|
||||||
67
extensions/telegram-user/src/types.ts
Normal file
67
extensions/telegram-user/src/types.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import type { DmPolicy, GroupPolicy, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
export type TelegramUserTopicConfig = {
|
||||||
|
requireMention?: boolean;
|
||||||
|
skills?: string[];
|
||||||
|
enabled?: boolean;
|
||||||
|
allowFrom?: Array<string | number>;
|
||||||
|
systemPrompt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TelegramUserGroupConfig = {
|
||||||
|
requireMention?: boolean;
|
||||||
|
skills?: string[];
|
||||||
|
tools?: GroupToolPolicyConfig;
|
||||||
|
topics?: Record<string, TelegramUserTopicConfig>;
|
||||||
|
enabled?: boolean;
|
||||||
|
allowFrom?: Array<string | number>;
|
||||||
|
systemPrompt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TelegramUserAccountConfig = {
|
||||||
|
/** Optional display name for this account (used in CLI/UI lists). */
|
||||||
|
name?: string;
|
||||||
|
/** If false, do not start this Telegram user account. Default: true. */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** Telegram API ID from my.telegram.org. */
|
||||||
|
apiId?: number;
|
||||||
|
/** Telegram API hash from my.telegram.org. */
|
||||||
|
apiHash?: string;
|
||||||
|
/** Direct message access policy (default: pairing). */
|
||||||
|
dmPolicy?: DmPolicy;
|
||||||
|
/** Allowlist for DM senders (user ids or usernames, or "*"). */
|
||||||
|
allowFrom?: Array<string | number>;
|
||||||
|
/** Control reply threading when reply tags are present (off|first|all). */
|
||||||
|
replyToMode?: "off" | "first" | "all";
|
||||||
|
/** Outbound text chunk size (chars). Default: 4000. */
|
||||||
|
textChunkLimit?: number;
|
||||||
|
/** Max outbound media size in MB. */
|
||||||
|
mediaMaxMb?: number;
|
||||||
|
/** Optional allowlist for Telegram group senders (user ids or usernames). */
|
||||||
|
groupAllowFrom?: Array<string | number>;
|
||||||
|
/** Controls how group messages are handled (open | disabled | allowlist). */
|
||||||
|
groupPolicy?: GroupPolicy;
|
||||||
|
/** Group-specific overrides (keyed by chat id). */
|
||||||
|
groups?: Record<string, TelegramUserGroupConfig>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TelegramUserConfig = TelegramUserAccountConfig & {
|
||||||
|
accounts?: Record<string, TelegramUserAccountConfig>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreConfig = {
|
||||||
|
channels?: {
|
||||||
|
defaults?: {
|
||||||
|
groupPolicy?: GroupPolicy;
|
||||||
|
};
|
||||||
|
"telegram-user"?: TelegramUserConfig;
|
||||||
|
};
|
||||||
|
commands?: {
|
||||||
|
useAccessGroups?: boolean;
|
||||||
|
};
|
||||||
|
messages?: {
|
||||||
|
ackReactionScope?: "off" | "group-mentions" | "group-all" | "direct" | "all";
|
||||||
|
removeAckAfterReply?: boolean;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
353
pnpm-lock.yaml
generated
353
pnpm-lock.yaml
generated
@ -415,6 +415,31 @@ importers:
|
|||||||
|
|
||||||
extensions/telegram: {}
|
extensions/telegram: {}
|
||||||
|
|
||||||
|
extensions/telegram-user:
|
||||||
|
dependencies:
|
||||||
|
'@clack/prompts':
|
||||||
|
specifier: ^0.8.2
|
||||||
|
version: 0.8.2
|
||||||
|
'@mtcute/core':
|
||||||
|
specifier: ^0.27.6
|
||||||
|
version: 0.27.6
|
||||||
|
'@mtcute/dispatcher':
|
||||||
|
specifier: ^0.27.6
|
||||||
|
version: 0.27.6
|
||||||
|
'@mtcute/node':
|
||||||
|
specifier: ^0.27.6
|
||||||
|
version: 0.27.6(ws@8.19.0)
|
||||||
|
qrcode-terminal:
|
||||||
|
specifier: ^0.12.0
|
||||||
|
version: 0.12.0
|
||||||
|
zod:
|
||||||
|
specifier: ^4.3.6
|
||||||
|
version: 4.3.6
|
||||||
|
devDependencies:
|
||||||
|
openclaw:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../..
|
||||||
|
|
||||||
extensions/tlon:
|
extensions/tlon:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@urbit/aura':
|
'@urbit/aura':
|
||||||
@ -832,12 +857,18 @@ packages:
|
|||||||
'@cacheable/utils@2.3.3':
|
'@cacheable/utils@2.3.3':
|
||||||
resolution: {integrity: sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==}
|
resolution: {integrity: sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==}
|
||||||
|
|
||||||
|
'@clack/core@0.3.5':
|
||||||
|
resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==}
|
||||||
|
|
||||||
'@clack/core@0.5.0':
|
'@clack/core@0.5.0':
|
||||||
resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==}
|
resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==}
|
||||||
|
|
||||||
'@clack/prompts@0.11.0':
|
'@clack/prompts@0.11.0':
|
||||||
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
|
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
|
||||||
|
|
||||||
|
'@clack/prompts@0.8.2':
|
||||||
|
resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==}
|
||||||
|
|
||||||
'@cloudflare/workers-types@4.20260120.0':
|
'@cloudflare/workers-types@4.20260120.0':
|
||||||
resolution: {integrity: sha512-B8pueG+a5S+mdK3z8oKu1ShcxloZ7qWb68IEyLLaepvdryIbNC7JVPcY0bWsjS56UQVKc5fnyRge3yZIwc9bxw==}
|
resolution: {integrity: sha512-B8pueG+a5S+mdK3z8oKu1ShcxloZ7qWb68IEyLLaepvdryIbNC7JVPcY0bWsjS56UQVKc5fnyRge3yZIwc9bxw==}
|
||||||
|
|
||||||
@ -1046,6 +1077,26 @@ packages:
|
|||||||
'@eshaz/web-worker@1.2.2':
|
'@eshaz/web-worker@1.2.2':
|
||||||
resolution: {integrity: sha512-WxXiHFmD9u/owrzempiDlBB1ZYqiLnm9s6aPc8AlFQalq2tKmqdmMr9GXOupDgzXtqnBipj8Un0gkIm7Sjf8mw==}
|
resolution: {integrity: sha512-WxXiHFmD9u/owrzempiDlBB1ZYqiLnm9s6aPc8AlFQalq2tKmqdmMr9GXOupDgzXtqnBipj8Un0gkIm7Sjf8mw==}
|
||||||
|
|
||||||
|
'@fuman/io@0.0.17':
|
||||||
|
resolution: {integrity: sha512-VmMnfHtXzBfEddEfptn/oYshUzWqW2XUkdVnwKuHWphEQTQZrOWxC7G12FI9U2EhEYt4nRdrUTYk65U8GVJWYw==}
|
||||||
|
|
||||||
|
'@fuman/net@0.0.17':
|
||||||
|
resolution: {integrity: sha512-x/kK3kWQ+gy5rfsoS6QVCsodh9n/XJeM3c6m1YHPUiQ0gWWQd4CC1bcQ/rh2UHh9DQyJJeWjCQXWH2xmsVCcFQ==}
|
||||||
|
|
||||||
|
'@fuman/node@0.0.17':
|
||||||
|
resolution: {integrity: sha512-XXRlJthuCnJBnIrg/tZcqCfv/cPuXuNOVUN521oJgKrW8FyFmt+lAt2MlYw3TROumGNRMtvn3ySjdQRpBT2sLw==}
|
||||||
|
peerDependencies:
|
||||||
|
ws: ^8.18.1
|
||||||
|
peerDependenciesMeta:
|
||||||
|
ws:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@fuman/utils@0.0.15':
|
||||||
|
resolution: {integrity: sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA==}
|
||||||
|
|
||||||
|
'@fuman/utils@0.0.17':
|
||||||
|
resolution: {integrity: sha512-hy1Xu1146nOspVam8FC6p4yakb1FV1V3KrS85RzcHiK7AccFKR43Fgtv8exC8Ybsw6MtMU+MRNyaPqVhA+7TsA==}
|
||||||
|
|
||||||
'@glideapps/ts-necessities@2.2.3':
|
'@glideapps/ts-necessities@2.2.3':
|
||||||
resolution: {integrity: sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==}
|
resolution: {integrity: sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==}
|
||||||
|
|
||||||
@ -1493,6 +1544,33 @@ packages:
|
|||||||
resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==}
|
resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
'@mtcute/core@0.27.6':
|
||||||
|
resolution: {integrity: sha512-OqjQ2hchF15yJAjcAgBuWx4RCnvMED0P3kiUfU3EsDMMIJlh8TgOgm0QspIda/Uz7icZ5+pi0rHyCYblw2MMKw==}
|
||||||
|
|
||||||
|
'@mtcute/dispatcher@0.27.6':
|
||||||
|
resolution: {integrity: sha512-5ZmI5cmyeWVYY5BtPlGYB0b4oxmF86xiP/c1Wo3VQ0SElYuknf+xZDlFxt6AMlHk9d/v2rxEd+dBXB5kRczpUA==}
|
||||||
|
|
||||||
|
'@mtcute/file-id@0.27.6':
|
||||||
|
resolution: {integrity: sha512-ZSPxbGjS6YdcZv4xW0zHJ/iR28nEBisG3G6gDTwVS4gU51SJ4vlGcwGjF1uLyEeuGGbPFemQHLCP6CMSzqMRvA==}
|
||||||
|
|
||||||
|
'@mtcute/html-parser@0.27.6':
|
||||||
|
resolution: {integrity: sha512-zxTuT0nv0CBR4qy7KyKB9vGQ++DxeiofKJEwHFSj5oG/7qUARm21G5GZaVlel/v7oRzx6V3u9mKDzdlbv8BcxA==}
|
||||||
|
|
||||||
|
'@mtcute/markdown-parser@0.27.6':
|
||||||
|
resolution: {integrity: sha512-YB4HXeDGQi+ilbOp1qDJ/iP3VfBFrsR+gEyQcaQo/PAR4NLtD+rZ5veWM/OSVjbawYl2OpFpfXzQdINAAlaEJg==}
|
||||||
|
|
||||||
|
'@mtcute/node@0.27.6':
|
||||||
|
resolution: {integrity: sha512-fDnufwcRJyqMr7rpCIiSW6GIRR1j+tgM8Og1Rx38U16Ftmu3gB7Xt5K7lHJJWQHDhv59w8AiU+NksiPdTXbxxQ==}
|
||||||
|
|
||||||
|
'@mtcute/tl-runtime@0.24.3':
|
||||||
|
resolution: {integrity: sha512-61J3cgYgNOQT532GdIiuezRrSC7v6cc9MfvWv9GO27bRGf7JUKWVbFt4U0KzQ9Tp0J1uMOUfi1EbQKkYKacIKQ==}
|
||||||
|
|
||||||
|
'@mtcute/tl@221.0.0':
|
||||||
|
resolution: {integrity: sha512-Wp01L9nznTMLl2s9rbKnzQ8pij72eF4HK2XIziOQoJXiObPKZxQdxvMj+C6l0ArxMFmpT0H0/3EL5RB7O6VPwg==}
|
||||||
|
|
||||||
|
'@mtcute/wasm@0.27.0':
|
||||||
|
resolution: {integrity: sha512-1v4eO1N1BVRQ8L+cyUsMAeLXs5suTGXyVv/tftkbd/mGGHxc+fvOWItp3Fmq+GIwN7m4VX7kztuMMLhHxv2i2Q==}
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.88':
|
'@napi-rs/canvas-android-arm64@0.1.88':
|
||||||
resolution: {integrity: sha512-KEaClPnZuVxJ8smUWjV1wWFkByBO/D+vy4lN+Dm5DFH514oqwukxKGeck9xcKJhaWJGjfruGmYGiwRe//+/zQQ==}
|
resolution: {integrity: sha512-KEaClPnZuVxJ8smUWjV1wWFkByBO/D+vy4lN+Dm5DFH514oqwukxKGeck9xcKJhaWJGjfruGmYGiwRe//+/zQQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@ -2701,6 +2779,9 @@ packages:
|
|||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/events@3.0.0':
|
||||||
|
resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==}
|
||||||
|
|
||||||
'@types/express-serve-static-core@4.19.8':
|
'@types/express-serve-static-core@4.19.8':
|
||||||
resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==}
|
resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==}
|
||||||
|
|
||||||
@ -3100,6 +3181,10 @@ packages:
|
|||||||
before-after-hook@4.0.0:
|
before-after-hook@4.0.0:
|
||||||
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
|
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
|
||||||
|
|
||||||
|
better-sqlite3@12.6.2:
|
||||||
|
resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==}
|
||||||
|
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
|
||||||
|
|
||||||
bignumber.js@9.3.1:
|
bignumber.js@9.3.1:
|
||||||
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||||
|
|
||||||
@ -3107,6 +3192,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
bindings@1.5.0:
|
||||||
|
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||||
|
|
||||||
|
bl@4.1.0:
|
||||||
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
bluebird@3.7.2:
|
bluebird@3.7.2:
|
||||||
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
||||||
|
|
||||||
@ -3150,6 +3241,9 @@ packages:
|
|||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
|
buffer@5.7.1:
|
||||||
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
@ -3201,6 +3295,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||||
engines: {node: '>= 20.19.0'}
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
|
chownr@1.1.4:
|
||||||
|
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||||
|
|
||||||
chownr@3.0.0:
|
chownr@3.0.0:
|
||||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -3374,6 +3471,10 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decompress-response@6.0.0:
|
||||||
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
deep-extend@0.6.0:
|
deep-extend@0.6.0:
|
||||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
@ -3463,6 +3564,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
end-of-stream@1.4.5:
|
||||||
|
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||||
|
|
||||||
entities@4.5.0:
|
entities@4.5.0:
|
||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
@ -3531,6 +3635,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
|
|
||||||
|
expand-template@2.0.3:
|
||||||
|
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
expect-type@1.3.0:
|
expect-type@1.3.0:
|
||||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@ -3590,6 +3698,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==}
|
resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
file-uri-to-path@1.0.0:
|
||||||
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
|
|
||||||
filename-reserved-regex@3.0.0:
|
filename-reserved-regex@3.0.0:
|
||||||
resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==}
|
resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@ -3661,6 +3772,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
fs-constants@1.0.0:
|
||||||
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
|
|
||||||
fs-extra@11.3.3:
|
fs-extra@11.3.3:
|
||||||
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
|
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
|
||||||
engines: {node: '>=14.14'}
|
engines: {node: '>=14.14'}
|
||||||
@ -3713,6 +3827,9 @@ packages:
|
|||||||
getpass@0.1.7:
|
getpass@0.1.7:
|
||||||
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
|
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
|
||||||
|
|
||||||
|
github-from-package@0.0.0:
|
||||||
|
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -4201,6 +4318,9 @@ packages:
|
|||||||
long@4.0.0:
|
long@4.0.0:
|
||||||
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
|
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
|
||||||
|
|
||||||
|
long@5.2.3:
|
||||||
|
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
|
||||||
|
|
||||||
long@5.3.2:
|
long@5.3.2:
|
||||||
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
||||||
|
|
||||||
@ -4323,6 +4443,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
|
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
mimic-response@3.1.0:
|
||||||
|
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
minimalistic-assert@1.0.1:
|
minimalistic-assert@1.0.1:
|
||||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||||
|
|
||||||
@ -4348,6 +4472,9 @@ packages:
|
|||||||
mitt@3.0.1:
|
mitt@3.0.1:
|
||||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||||
|
|
||||||
|
mkdirp-classic@0.5.3:
|
||||||
|
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||||
|
|
||||||
mkdirp@3.0.1:
|
mkdirp@3.0.1:
|
||||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -4390,6 +4517,9 @@ packages:
|
|||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
napi-build-utils@2.0.0:
|
||||||
|
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||||
|
|
||||||
negotiator@0.6.3:
|
negotiator@0.6.3:
|
||||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -4398,6 +4528,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
node-abi@3.87.0:
|
||||||
|
resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
node-addon-api@8.5.0:
|
node-addon-api@8.5.0:
|
||||||
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||||
engines: {node: ^18 || ^20 || >= 21}
|
engines: {node: ^18 || ^20 || >= 21}
|
||||||
@ -4723,6 +4857,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
|
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
prebuild-install@7.1.3:
|
||||||
|
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
pretty-bytes@6.1.1:
|
pretty-bytes@6.1.1:
|
||||||
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
||||||
engines: {node: ^14.13.1 || >=16.0.0}
|
engines: {node: ^14.13.1 || >=16.0.0}
|
||||||
@ -4787,6 +4926,9 @@ packages:
|
|||||||
psl@1.15.0:
|
psl@1.15.0:
|
||||||
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
|
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
|
||||||
|
|
||||||
|
pump@3.0.3:
|
||||||
|
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||||
|
|
||||||
punycode.js@2.3.1:
|
punycode.js@2.3.1:
|
||||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -5030,6 +5172,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
signal-polyfill: ^0.2.0
|
signal-polyfill: ^0.2.0
|
||||||
|
|
||||||
|
simple-concat@1.0.1:
|
||||||
|
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||||
|
|
||||||
|
simple-get@4.0.1:
|
||||||
|
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||||
|
|
||||||
simple-git@3.30.0:
|
simple-git@3.30.0:
|
||||||
resolution: {integrity: sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==}
|
resolution: {integrity: sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==}
|
||||||
|
|
||||||
@ -5191,6 +5339,13 @@ packages:
|
|||||||
tailwindcss@4.1.17:
|
tailwindcss@4.1.17:
|
||||||
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
|
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
|
||||||
|
|
||||||
|
tar-fs@2.1.4:
|
||||||
|
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
||||||
|
|
||||||
|
tar-stream@2.2.0:
|
||||||
|
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
tar@7.5.4:
|
tar@7.5.4:
|
||||||
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
|
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -6472,6 +6627,11 @@ snapshots:
|
|||||||
hashery: 1.4.0
|
hashery: 1.4.0
|
||||||
keyv: 5.6.0
|
keyv: 5.6.0
|
||||||
|
|
||||||
|
'@clack/core@0.3.5':
|
||||||
|
dependencies:
|
||||||
|
picocolors: 1.1.1
|
||||||
|
sisteransi: 1.0.5
|
||||||
|
|
||||||
'@clack/core@0.5.0':
|
'@clack/core@0.5.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
@ -6483,6 +6643,12 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
sisteransi: 1.0.5
|
sisteransi: 1.0.5
|
||||||
|
|
||||||
|
'@clack/prompts@0.8.2':
|
||||||
|
dependencies:
|
||||||
|
'@clack/core': 0.3.5
|
||||||
|
picocolors: 1.1.1
|
||||||
|
sisteransi: 1.0.5
|
||||||
|
|
||||||
'@cloudflare/workers-types@4.20260120.0':
|
'@cloudflare/workers-types@4.20260120.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -6647,6 +6813,27 @@ snapshots:
|
|||||||
'@eshaz/web-worker@1.2.2':
|
'@eshaz/web-worker@1.2.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@fuman/io@0.0.17':
|
||||||
|
dependencies:
|
||||||
|
'@fuman/utils': 0.0.17
|
||||||
|
|
||||||
|
'@fuman/net@0.0.17':
|
||||||
|
dependencies:
|
||||||
|
'@fuman/io': 0.0.17
|
||||||
|
'@fuman/utils': 0.0.17
|
||||||
|
|
||||||
|
'@fuman/node@0.0.17(ws@8.19.0)':
|
||||||
|
dependencies:
|
||||||
|
'@fuman/io': 0.0.17
|
||||||
|
'@fuman/net': 0.0.17
|
||||||
|
'@fuman/utils': 0.0.17
|
||||||
|
optionalDependencies:
|
||||||
|
ws: 8.19.0
|
||||||
|
|
||||||
|
'@fuman/utils@0.0.15': {}
|
||||||
|
|
||||||
|
'@fuman/utils@0.0.17': {}
|
||||||
|
|
||||||
'@glideapps/ts-necessities@2.2.3': {}
|
'@glideapps/ts-necessities@2.2.3': {}
|
||||||
|
|
||||||
'@google/genai@1.34.0':
|
'@google/genai@1.34.0':
|
||||||
@ -7113,6 +7300,63 @@ snapshots:
|
|||||||
|
|
||||||
'@mozilla/readability@0.6.0': {}
|
'@mozilla/readability@0.6.0': {}
|
||||||
|
|
||||||
|
'@mtcute/core@0.27.6':
|
||||||
|
dependencies:
|
||||||
|
'@fuman/io': 0.0.17
|
||||||
|
'@fuman/net': 0.0.17
|
||||||
|
'@fuman/utils': 0.0.17
|
||||||
|
'@mtcute/file-id': 0.27.6
|
||||||
|
'@mtcute/tl': 221.0.0
|
||||||
|
'@mtcute/tl-runtime': 0.24.3
|
||||||
|
'@types/events': 3.0.0
|
||||||
|
long: 5.2.3
|
||||||
|
|
||||||
|
'@mtcute/dispatcher@0.27.6':
|
||||||
|
dependencies:
|
||||||
|
'@fuman/utils': 0.0.17
|
||||||
|
'@mtcute/core': 0.27.6
|
||||||
|
|
||||||
|
'@mtcute/file-id@0.27.6':
|
||||||
|
dependencies:
|
||||||
|
'@fuman/utils': 0.0.17
|
||||||
|
'@mtcute/tl-runtime': 0.24.3
|
||||||
|
long: 5.2.3
|
||||||
|
|
||||||
|
'@mtcute/html-parser@0.27.6':
|
||||||
|
dependencies:
|
||||||
|
'@mtcute/core': 0.27.6
|
||||||
|
htmlparser2: 10.1.0
|
||||||
|
long: 5.2.3
|
||||||
|
|
||||||
|
'@mtcute/markdown-parser@0.27.6':
|
||||||
|
dependencies:
|
||||||
|
'@mtcute/core': 0.27.6
|
||||||
|
long: 5.2.3
|
||||||
|
|
||||||
|
'@mtcute/node@0.27.6(ws@8.19.0)':
|
||||||
|
dependencies:
|
||||||
|
'@fuman/net': 0.0.17
|
||||||
|
'@fuman/node': 0.0.17(ws@8.19.0)
|
||||||
|
'@fuman/utils': 0.0.17
|
||||||
|
'@mtcute/core': 0.27.6
|
||||||
|
'@mtcute/html-parser': 0.27.6
|
||||||
|
'@mtcute/markdown-parser': 0.27.6
|
||||||
|
'@mtcute/wasm': 0.27.0
|
||||||
|
better-sqlite3: 12.6.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- ws
|
||||||
|
|
||||||
|
'@mtcute/tl-runtime@0.24.3':
|
||||||
|
dependencies:
|
||||||
|
'@fuman/utils': 0.0.15
|
||||||
|
long: 5.2.3
|
||||||
|
|
||||||
|
'@mtcute/tl@221.0.0':
|
||||||
|
dependencies:
|
||||||
|
long: 5.2.3
|
||||||
|
|
||||||
|
'@mtcute/wasm@0.27.0': {}
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.88':
|
'@napi-rs/canvas-android-arm64@0.1.88':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -8464,6 +8708,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/events@3.0.0': {}
|
||||||
|
|
||||||
'@types/express-serve-static-core@4.19.8':
|
'@types/express-serve-static-core@4.19.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.0.10
|
'@types/node': 25.0.10
|
||||||
@ -8955,10 +9201,25 @@ snapshots:
|
|||||||
before-after-hook@4.0.0:
|
before-after-hook@4.0.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
better-sqlite3@12.6.2:
|
||||||
|
dependencies:
|
||||||
|
bindings: 1.5.0
|
||||||
|
prebuild-install: 7.1.3
|
||||||
|
|
||||||
bignumber.js@9.3.1: {}
|
bignumber.js@9.3.1: {}
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
|
bindings@1.5.0:
|
||||||
|
dependencies:
|
||||||
|
file-uri-to-path: 1.0.0
|
||||||
|
|
||||||
|
bl@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
buffer: 5.7.1
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
bluebird@3.7.2: {}
|
bluebird@3.7.2: {}
|
||||||
|
|
||||||
body-parser@1.20.4:
|
body-parser@1.20.4:
|
||||||
@ -9018,6 +9279,11 @@ snapshots:
|
|||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
|
buffer@5.7.1:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
@ -9082,6 +9348,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 5.0.0
|
readdirp: 5.0.0
|
||||||
|
|
||||||
|
chownr@1.1.4: {}
|
||||||
|
|
||||||
chownr@3.0.0: {}
|
chownr@3.0.0: {}
|
||||||
|
|
||||||
chromium-bidi@13.0.1(devtools-protocol@0.0.1561482):
|
chromium-bidi@13.0.1(devtools-protocol@0.0.1561482):
|
||||||
@ -9252,8 +9520,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
deep-extend@0.6.0:
|
decompress-response@6.0.0:
|
||||||
optional: true
|
dependencies:
|
||||||
|
mimic-response: 3.1.0
|
||||||
|
|
||||||
|
deep-extend@0.6.0: {}
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
@ -9330,6 +9601,10 @@ snapshots:
|
|||||||
|
|
||||||
encodeurl@2.0.0: {}
|
encodeurl@2.0.0: {}
|
||||||
|
|
||||||
|
end-of-stream@1.4.5:
|
||||||
|
dependencies:
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
entities@4.5.0: {}
|
entities@4.5.0: {}
|
||||||
|
|
||||||
entities@7.0.1: {}
|
entities@7.0.1: {}
|
||||||
@ -9403,6 +9678,8 @@ snapshots:
|
|||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
|
expand-template@2.0.3: {}
|
||||||
|
|
||||||
expect-type@1.3.0: {}
|
expect-type@1.3.0: {}
|
||||||
|
|
||||||
express@4.22.1:
|
express@4.22.1:
|
||||||
@ -9521,6 +9798,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
file-uri-to-path@1.0.0: {}
|
||||||
|
|
||||||
filename-reserved-regex@3.0.0:
|
filename-reserved-regex@3.0.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -9606,6 +9885,8 @@ snapshots:
|
|||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
|
fs-constants@1.0.0: {}
|
||||||
|
|
||||||
fs-extra@11.3.3:
|
fs-extra@11.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@ -9680,6 +9961,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
assert-plus: 1.0.0
|
assert-plus: 1.0.0
|
||||||
|
|
||||||
|
github-from-package@0.0.0: {}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@ -9860,8 +10143,7 @@ snapshots:
|
|||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
ini@1.3.8:
|
ini@1.3.8: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
@ -10204,6 +10486,8 @@ snapshots:
|
|||||||
|
|
||||||
long@4.0.0: {}
|
long@4.0.0: {}
|
||||||
|
|
||||||
|
long@5.2.3: {}
|
||||||
|
|
||||||
long@5.3.2: {}
|
long@5.3.2: {}
|
||||||
|
|
||||||
lowdb@1.0.0:
|
lowdb@1.0.0:
|
||||||
@ -10308,6 +10592,8 @@ snapshots:
|
|||||||
mimic-function@5.0.1:
|
mimic-function@5.0.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
mimic-response@3.1.0: {}
|
||||||
|
|
||||||
minimalistic-assert@1.0.1: {}
|
minimalistic-assert@1.0.1: {}
|
||||||
|
|
||||||
minimatch@10.1.1:
|
minimatch@10.1.1:
|
||||||
@ -10318,8 +10604,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.2
|
brace-expansion: 2.0.2
|
||||||
|
|
||||||
minimist@1.2.8:
|
minimist@1.2.8: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
@ -10329,6 +10614,8 @@ snapshots:
|
|||||||
|
|
||||||
mitt@3.0.1: {}
|
mitt@3.0.1: {}
|
||||||
|
|
||||||
|
mkdirp-classic@0.5.3: {}
|
||||||
|
|
||||||
mkdirp@3.0.1: {}
|
mkdirp@3.0.1: {}
|
||||||
|
|
||||||
module-details-from-path@1.0.4: {}
|
module-details-from-path@1.0.4: {}
|
||||||
@ -10380,10 +10667,16 @@ snapshots:
|
|||||||
nanoid@5.1.6:
|
nanoid@5.1.6:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
napi-build-utils@2.0.0: {}
|
||||||
|
|
||||||
negotiator@0.6.3: {}
|
negotiator@0.6.3: {}
|
||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
|
node-abi@3.87.0:
|
||||||
|
dependencies:
|
||||||
|
semver: 7.7.3
|
||||||
|
|
||||||
node-addon-api@8.5.0:
|
node-addon-api@8.5.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -10749,6 +11042,21 @@ snapshots:
|
|||||||
|
|
||||||
postgres@3.4.8: {}
|
postgres@3.4.8: {}
|
||||||
|
|
||||||
|
prebuild-install@7.1.3:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
expand-template: 2.0.3
|
||||||
|
github-from-package: 0.0.0
|
||||||
|
minimist: 1.2.8
|
||||||
|
mkdirp-classic: 0.5.3
|
||||||
|
napi-build-utils: 2.0.0
|
||||||
|
node-abi: 3.87.0
|
||||||
|
pump: 3.0.3
|
||||||
|
rc: 1.2.8
|
||||||
|
simple-get: 4.0.1
|
||||||
|
tar-fs: 2.1.4
|
||||||
|
tunnel-agent: 0.6.0
|
||||||
|
|
||||||
pretty-bytes@6.1.1:
|
pretty-bytes@6.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -10834,6 +11142,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
pump@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
punycode.js@2.3.1: {}
|
punycode.js@2.3.1: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
@ -10900,7 +11213,6 @@ snapshots:
|
|||||||
ini: 1.3.8
|
ini: 1.3.8
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
strip-json-comments: 2.0.1
|
strip-json-comments: 2.0.1
|
||||||
optional: true
|
|
||||||
|
|
||||||
readable-stream@2.3.8:
|
readable-stream@2.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -10917,7 +11229,6 @@ snapshots:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
string_decoder: 1.3.0
|
string_decoder: 1.3.0
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
optional: true
|
|
||||||
|
|
||||||
readable-stream@4.5.2:
|
readable-stream@4.5.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -11225,6 +11536,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
signal-polyfill: 0.2.2
|
signal-polyfill: 0.2.2
|
||||||
|
|
||||||
|
simple-concat@1.0.1: {}
|
||||||
|
|
||||||
|
simple-get@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
decompress-response: 6.0.0
|
||||||
|
once: 1.4.0
|
||||||
|
simple-concat: 1.0.1
|
||||||
|
|
||||||
simple-git@3.30.0:
|
simple-git@3.30.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@kwsites/file-exists': 1.1.1
|
'@kwsites/file-exists': 1.1.1
|
||||||
@ -11365,8 +11684,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 6.2.2
|
ansi-regex: 6.2.2
|
||||||
|
|
||||||
strip-json-comments@2.0.1:
|
strip-json-comments@2.0.1: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
strnum@2.1.2: {}
|
strnum@2.1.2: {}
|
||||||
|
|
||||||
@ -11393,6 +11711,21 @@ snapshots:
|
|||||||
|
|
||||||
tailwindcss@4.1.17: {}
|
tailwindcss@4.1.17: {}
|
||||||
|
|
||||||
|
tar-fs@2.1.4:
|
||||||
|
dependencies:
|
||||||
|
chownr: 1.1.4
|
||||||
|
mkdirp-classic: 0.5.3
|
||||||
|
pump: 3.0.3
|
||||||
|
tar-stream: 2.2.0
|
||||||
|
|
||||||
|
tar-stream@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
bl: 4.1.0
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
fs-constants: 1.0.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
tar@7.5.4:
|
tar@7.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/fs-minipass': 4.0.1
|
'@isaacs/fs-minipass': 4.0.1
|
||||||
|
|||||||
@ -36,6 +36,8 @@ export type ChannelSetupInput = {
|
|||||||
audienceType?: string;
|
audienceType?: string;
|
||||||
audience?: string;
|
audience?: string;
|
||||||
useEnv?: boolean;
|
useEnv?: boolean;
|
||||||
|
apiId?: number;
|
||||||
|
apiHash?: string;
|
||||||
homeserver?: string;
|
homeserver?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
|||||||
@ -39,6 +39,8 @@ const optionNamesAdd = [
|
|||||||
"audienceType",
|
"audienceType",
|
||||||
"audience",
|
"audience",
|
||||||
"useEnv",
|
"useEnv",
|
||||||
|
"apiId",
|
||||||
|
"apiHash",
|
||||||
"homeserver",
|
"homeserver",
|
||||||
"userId",
|
"userId",
|
||||||
"accessToken",
|
"accessToken",
|
||||||
@ -175,6 +177,8 @@ export function registerChannelsCli(program: Command) {
|
|||||||
.option("--webhook-url <url>", "Google Chat webhook URL")
|
.option("--webhook-url <url>", "Google Chat webhook URL")
|
||||||
.option("--audience-type <type>", "Google Chat audience type (app-url|project-number)")
|
.option("--audience-type <type>", "Google Chat audience type (app-url|project-number)")
|
||||||
.option("--audience <value>", "Google Chat audience value (app URL or project number)")
|
.option("--audience <value>", "Google Chat audience value (app URL or project number)")
|
||||||
|
.option("--api-id <id>", "Telegram user API id (my.telegram.org)")
|
||||||
|
.option("--api-hash <hash>", "Telegram user API hash (my.telegram.org)")
|
||||||
.option("--homeserver <url>", "Matrix homeserver URL")
|
.option("--homeserver <url>", "Matrix homeserver URL")
|
||||||
.option("--user-id <id>", "Matrix user ID")
|
.option("--user-id <id>", "Matrix user ID")
|
||||||
.option("--access-token <token>", "Matrix access token")
|
.option("--access-token <token>", "Matrix access token")
|
||||||
|
|||||||
@ -40,6 +40,8 @@ export function applyChannelAccountConfig(params: {
|
|||||||
audienceType?: string;
|
audienceType?: string;
|
||||||
audience?: string;
|
audience?: string;
|
||||||
useEnv?: boolean;
|
useEnv?: boolean;
|
||||||
|
apiId?: number;
|
||||||
|
apiHash?: string;
|
||||||
homeserver?: string;
|
homeserver?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
@ -77,6 +79,8 @@ export function applyChannelAccountConfig(params: {
|
|||||||
audienceType: params.audienceType,
|
audienceType: params.audienceType,
|
||||||
audience: params.audience,
|
audience: params.audience,
|
||||||
useEnv: params.useEnv,
|
useEnv: params.useEnv,
|
||||||
|
apiId: params.apiId,
|
||||||
|
apiHash: params.apiHash,
|
||||||
homeserver: params.homeserver,
|
homeserver: params.homeserver,
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
accessToken: params.accessToken,
|
accessToken: params.accessToken,
|
||||||
|
|||||||
@ -37,6 +37,8 @@ export type ChannelsAddOptions = {
|
|||||||
audienceType?: string;
|
audienceType?: string;
|
||||||
audience?: string;
|
audience?: string;
|
||||||
useEnv?: boolean;
|
useEnv?: boolean;
|
||||||
|
apiId?: string | number;
|
||||||
|
apiHash?: string;
|
||||||
homeserver?: string;
|
homeserver?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
@ -181,7 +183,12 @@ export async function channelsAddCommand(
|
|||||||
: undefined;
|
: undefined;
|
||||||
const groupChannels = parseList(opts.groupChannels);
|
const groupChannels = parseList(opts.groupChannels);
|
||||||
const dmAllowlist = parseList(opts.dmAllowlist);
|
const dmAllowlist = parseList(opts.dmAllowlist);
|
||||||
|
const apiId =
|
||||||
|
typeof opts.apiId === "number"
|
||||||
|
? opts.apiId
|
||||||
|
: typeof opts.apiId === "string" && opts.apiId.trim()
|
||||||
|
? Number.parseInt(opts.apiId, 10)
|
||||||
|
: undefined;
|
||||||
const validationError = plugin.setup.validateInput?.({
|
const validationError = plugin.setup.validateInput?.({
|
||||||
cfg: nextConfig,
|
cfg: nextConfig,
|
||||||
accountId,
|
accountId,
|
||||||
@ -204,6 +211,8 @@ export async function channelsAddCommand(
|
|||||||
webhookUrl: opts.webhookUrl,
|
webhookUrl: opts.webhookUrl,
|
||||||
audienceType: opts.audienceType,
|
audienceType: opts.audienceType,
|
||||||
audience: opts.audience,
|
audience: opts.audience,
|
||||||
|
apiId,
|
||||||
|
apiHash: opts.apiHash,
|
||||||
homeserver: opts.homeserver,
|
homeserver: opts.homeserver,
|
||||||
userId: opts.userId,
|
userId: opts.userId,
|
||||||
accessToken: opts.accessToken,
|
accessToken: opts.accessToken,
|
||||||
@ -247,6 +256,8 @@ export async function channelsAddCommand(
|
|||||||
webhookUrl: opts.webhookUrl,
|
webhookUrl: opts.webhookUrl,
|
||||||
audienceType: opts.audienceType,
|
audienceType: opts.audienceType,
|
||||||
audience: opts.audience,
|
audience: opts.audience,
|
||||||
|
apiId,
|
||||||
|
apiHash: opts.apiHash,
|
||||||
homeserver: opts.homeserver,
|
homeserver: opts.homeserver,
|
||||||
userId: opts.userId,
|
userId: opts.userId,
|
||||||
accessToken: opts.accessToken,
|
accessToken: opts.accessToken,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user