diff --git a/.github/labeler.yml b/.github/labeler.yml index 5c19fa418..73bc4fb75 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -68,6 +68,11 @@ - "src/telegram/**" - "extensions/telegram/**" - "docs/channels/telegram.md" +"channel: telegram-user": + - changed-files: + - any-glob-to-any-file: + - "extensions/telegram-user/**" + - "docs/channels/telegram-user.md" "channel: tlon": - changed-files: - any-glob-to-any-file: diff --git a/docs/channels/telegram-user.md b/docs/channels/telegram-user.md index 36d59ae37..ff058ff43 100644 --- a/docs/channels/telegram-user.md +++ b/docs/channels/telegram-user.md @@ -3,7 +3,7 @@ summary: "Connect a Telegram user account via MTProto (DMs + groups)" --- # Telegram User -Telegram User connects Clawdbot to a **personal Telegram account** using MTProto. +Telegram User connects Moltbot 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 @@ -16,7 +16,7 @@ Use this when you need user-level DMs or want to message from your own account i If the plugin is not bundled, install it: ```bash -clawdbot plugins install @clawdbot/telegram-user +moltbot plugins install @moltbot/telegram-user ``` ## Configure @@ -27,25 +27,25 @@ Option A: env vars (default account only) ```bash export TELEGRAM_USER_API_ID="123456" export TELEGRAM_USER_API_HASH="your_api_hash" -clawdbot channels add --channel telegram-user --use-env +moltbot channels add --channel telegram-user --use-env ``` Option B: config ```bash -clawdbot channels add --channel telegram-user --api-id 123456 --api-hash your_api_hash +moltbot channels add --channel telegram-user --api-id 123456 --api-hash your_api_hash ``` ## Login (QR or phone code) QR login (default): ```bash -clawdbot channels login --channel telegram-user +moltbot channels login --channel telegram-user ``` Phone login: ```bash export TELEGRAM_USER_PHONE="+15551234567" -clawdbot channels login --channel telegram-user +moltbot channels login --channel telegram-user ``` Optional env helpers: @@ -57,7 +57,7 @@ Optional env helpers: By default, DMs are protected with pairing. Approve requests with: ```bash -clawdbot pairing approve telegram-user +moltbot pairing approve telegram-user ``` See [Pairing](/start/pairing) for details. diff --git a/extensions/telegram-user/index.ts b/extensions/telegram-user/index.ts index db524a6ed..6bb1142df 100644 --- a/extensions/telegram-user/index.ts +++ b/extensions/telegram-user/index.ts @@ -1,4 +1,4 @@ -import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk"; +import type { MoltbotPluginApi } from "clawdbot/plugin-sdk"; import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; import { telegramUserPlugin } from "./src/channel.js"; @@ -9,7 +9,7 @@ const plugin = { name: "Telegram User", description: "Telegram MTProto user channel plugin", configSchema: emptyPluginConfigSchema(), - register(api: ClawdbotPluginApi) { + register(api: MoltbotPluginApi) { setTelegramUserRuntime(api.runtime); api.registerChannel({ plugin: telegramUserPlugin }); }, diff --git a/extensions/telegram-user/package.json b/extensions/telegram-user/package.json index 5aae90caa..c312860cb 100644 --- a/extensions/telegram-user/package.json +++ b/extensions/telegram-user/package.json @@ -1,9 +1,9 @@ { - "name": "@clawdbot/telegram-user", + "name": "@moltbot/telegram-user", "version": "2026.1.22", "type": "module", - "description": "Clawdbot Telegram user (MTProto) channel plugin", - "clawdbot": { + "description": "Moltbot Telegram user (MTProto) channel plugin", + "moltbot": { "extensions": [ "./index.ts" ], @@ -19,7 +19,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/telegram-user", + "npmSpec": "@moltbot/telegram-user", "localPath": "extensions/telegram-user", "defaultChoice": "npm" } @@ -33,9 +33,9 @@ "zod": "^4.3.6" }, "devDependencies": { - "clawdbot": "workspace:*" + "moltbot": "workspace:*" }, "peerDependencies": { - "clawdbot": ">=2026.1.25" + "moltbot": ">=2026.1.25" } } diff --git a/extensions/telegram-user/src/active-client.ts b/extensions/telegram-user/src/active-client.ts index 4eb60171f..9d28de517 100644 --- a/extensions/telegram-user/src/active-client.ts +++ b/extensions/telegram-user/src/active-client.ts @@ -1,11 +1,25 @@ import type { TelegramClient } from "@mtcute/node"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; -let activeClient: TelegramClient | null = null; +const activeClients = new Map(); -export function setActiveTelegramUserClient(next: TelegramClient | null) { - activeClient = next; +function resolveAccountKey(accountId?: string | null): string { + return normalizeAccountId(accountId ?? DEFAULT_ACCOUNT_ID); } -export function getActiveTelegramUserClient(): TelegramClient | null { - return activeClient; +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; } diff --git a/extensions/telegram-user/src/channel.test.ts b/extensions/telegram-user/src/channel.test.ts index 777a817c8..00c7fd3b4 100644 --- a/extensions/telegram-user/src/channel.test.ts +++ b/extensions/telegram-user/src/channel.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { ClawdbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk"; +import type { MoltbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk"; const sendMediaTelegramUser = vi.fn< typeof import("./send.js").sendMediaTelegramUser @@ -37,7 +37,7 @@ describe("telegram-user channel plugin", () => { mediaMaxMb: 7, }, }, - } satisfies Partial as unknown as ClawdbotConfig; + } satisfies Partial as unknown as MoltbotConfig; const mod = await import("./channel.js"); await mod.telegramUserPlugin.outbound?.sendMedia?.({ @@ -60,7 +60,7 @@ describe("telegram-user channel plugin", () => { channels: { "telegram-user": {}, }, - } satisfies Partial as unknown as ClawdbotConfig; + } satisfies Partial as unknown as MoltbotConfig; const mod = await import("./channel.js"); await mod.telegramUserPlugin.outbound?.sendMedia?.({ @@ -88,7 +88,7 @@ describe("telegram-user channel plugin", () => { }, }, }, - } satisfies Partial as unknown as ClawdbotConfig; + } satisfies Partial as unknown as MoltbotConfig; const mod = await import("./channel.js"); const runtime = { diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 45d59fb89..372906a4d 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -7,12 +7,13 @@ import { deleteAccountFromConfigSection, formatPairingApproveHint, normalizeAccountId, + PAIRING_APPROVED_MESSAGE, resolveChannelMediaMaxBytes, setAccountEnabledInConfigSection, type ChannelGroupContext, type ChannelPlugin, type ChannelSetupInput, - type ClawdbotConfig, + type MoltbotConfig, type GroupToolPolicyConfig, } from "clawdbot/plugin-sdk"; @@ -108,7 +109,7 @@ export const telegramUserPlugin: ChannelPlugin = { normalizeAllowEntry: (entry) => entry.replace(/^(telegram-user|telegram|tg):/i, "").toLowerCase(), notifyApproval: async ({ id }) => { - await sendMessageTelegramUser(String(id), "Clawdbot: access approved.", {}); + await sendMessageTelegramUser(String(id), PAIRING_APPROVED_MESSAGE, {}); }, }, capabilities: { @@ -337,7 +338,7 @@ export const telegramUserPlugin: ChannelPlugin = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ - cfg: cfg as ClawdbotConfig, + cfg: cfg as MoltbotConfig, channelKey: "telegram-user", accountId, name, @@ -355,7 +356,7 @@ export const telegramUserPlugin: ChannelPlugin = { applyAccountConfig: ({ cfg, accountId, input }) => { const setupInput = input as TelegramUserSetupInput; const namedConfig = applyAccountNameToChannelSection({ - cfg: cfg as ClawdbotConfig, + cfg: cfg as MoltbotConfig, channelKey: "telegram-user", accountId, name: setupInput.name, @@ -432,13 +433,13 @@ export const telegramUserPlugin: ChannelPlugin = { throw err; } }, - stopAccount: async () => { + stopAccount: async ({ accountId }) => { const { getActiveTelegramUserClient, setActiveTelegramUserClient } = await import("./active-client.js"); - const active = getActiveTelegramUserClient(); + const active = getActiveTelegramUserClient(accountId); if (active) { await active.destroy().catch(() => undefined); - setActiveTelegramUserClient(null); + setActiveTelegramUserClient(accountId, null); } }, logoutAccount: async ({ accountId, cfg, runtime }) => { @@ -453,7 +454,7 @@ export const telegramUserPlugin: ChannelPlugin = { } } - const nextCfg = { ...cfg } as ClawdbotConfig; + const nextCfg = { ...cfg } as MoltbotConfig; const nextSection = cfg.channels?.["telegram-user"] ? { ...cfg.channels["telegram-user"] } : undefined; diff --git a/extensions/telegram-user/src/client.ts b/extensions/telegram-user/src/client.ts index 156083c49..708541e1b 100644 --- a/extensions/telegram-user/src/client.ts +++ b/extensions/telegram-user/src/client.ts @@ -17,10 +17,10 @@ export async function createTelegramUserClient(params: { // the "import" condition (ESM), eliminating the warning. const { BaseTelegramClient, TelegramClient, NodePlatform } = await loadMtcuteNode(); - class ClawdbotTelegramUserPlatform extends NodePlatform { + class MoltbotTelegramUserPlatform extends NodePlatform { // mtcute's default NodePlatform.beforeExit installs SIGINT/SIGTERM handlers that re-send the - // signal, which can race with Clawdbot's graceful shutdown and close sqlite while writes are - // pending. We only hook into process exit events (no signal handlers) and rely on Clawdbot to + // signal, which can race with Moltbot's graceful shutdown and close sqlite while writes are + // pending. We only hook into process exit events (no signal handlers) and rely on Moltbot to // stop cleanly. override beforeExit(fn: () => void): () => void { const onBeforeExit = () => fn(); @@ -38,7 +38,7 @@ export async function createTelegramUserClient(params: { apiId: params.apiId, apiHash: params.apiHash, storage: params.storagePath, - platform: new ClawdbotTelegramUserPlatform(), + platform: new MoltbotTelegramUserPlatform(), }); return new TelegramClient({ client }); } diff --git a/extensions/telegram-user/src/directory-config.ts b/extensions/telegram-user/src/directory-config.ts index e28755e63..e252df6f1 100644 --- a/extensions/telegram-user/src/directory-config.ts +++ b/extensions/telegram-user/src/directory-config.ts @@ -1,10 +1,10 @@ -import type { ChannelDirectoryEntry, ClawdbotConfig } from "clawdbot/plugin-sdk"; +import type { ChannelDirectoryEntry, MoltbotConfig } from "clawdbot/plugin-sdk"; import { resolveTelegramUserAccount } from "./accounts.js"; import type { CoreConfig } from "./types.js"; export type TelegramUserDirectoryConfigParams = { - cfg: ClawdbotConfig; + cfg: MoltbotConfig; accountId?: string | null; query?: string | null; limit?: number | null; @@ -65,4 +65,3 @@ export async function listTelegramUserDirectoryGroupsFromConfig( .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) .map((id) => ({ kind: "group", id }) as const); } - diff --git a/extensions/telegram-user/src/monitor/handler.test.ts b/extensions/telegram-user/src/monitor/handler.test.ts new file mode 100644 index 000000000..41898dc4f --- /dev/null +++ b/extensions/telegram-user/src/monitor/handler.test.ts @@ -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(); + }); +}); diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 224f3eb73..5bb3c2ae3 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -115,6 +115,20 @@ function buildTelegramUserSelfMentionRegexes(params: { .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; @@ -348,9 +362,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const chatId = msg.chat.type === "chat" ? msg.chat.id : undefined; const isForum = msg.chat.type === "chat" && msg.chat.isForum === true; const threadId = - isGroup && msg.isTopicMessage - ? msg.replyToMessage?.threadId ?? undefined - : undefined; + isGroup && isForum ? msg.replyToMessage?.threadId ?? undefined : undefined; const { groupConfig, topicConfig } = isGroup && chatId != null ? resolveTelegramUserGroupConfig(accountConfig, chatId, threadId) @@ -452,7 +464,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const media = allMedia[0] ?? null; const rawBody = [text, locationText, pollText].filter(Boolean).join("\n").trim(); if (!rawBody && !media) return; - const timestampMs = msg.date ? msg.date * 1000 : undefined; + const timestampMs = resolveTelegramUserTimestampMs(msg.date); const replyInfo = msg.replyToMessage ?? null; const replyToId = replyInfo?.id != null ? String(replyInfo.id) : undefined; const replyToSender = replyInfo?.sender diff --git a/extensions/telegram-user/src/monitor/index.ts b/extensions/telegram-user/src/monitor/index.ts index 1110c56b9..e7c0e46e5 100644 --- a/extensions/telegram-user/src/monitor/index.ts +++ b/extensions/telegram-user/src/monitor/index.ts @@ -60,15 +60,15 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts const storagePath = resolveTelegramUserSessionPath(account.accountId); if (!fs.existsSync(storagePath)) { throw new Error( - "Telegram user session missing. Run `clawdbot channels login --channel telegram-user` first.", + "Telegram user session missing. Run `moltbot channels login --channel telegram-user` first.", ); } const client = await createTelegramUserClient({ apiId, apiHash, storagePath }); - setActiveTelegramUserClient(client); + setActiveTelegramUserClient(account.accountId, client); const stop = async () => { shuttingDown = true; - setActiveTelegramUserClient(null); + setActiveTelegramUserClient(account.accountId, null); await client.destroy().catch(() => undefined); }; diff --git a/extensions/telegram-user/src/onboarding.ts b/extensions/telegram-user/src/onboarding.ts index 6ac988a8d..0c9f0933a 100644 --- a/extensions/telegram-user/src/onboarding.ts +++ b/extensions/telegram-user/src/onboarding.ts @@ -6,7 +6,7 @@ import { normalizeAccountId, type ChannelOnboardingAdapter, type ChannelOnboardingDmPolicy, - type ClawdbotConfig, + type MoltbotConfig, type DmPolicy, type WizardPrompter, } from "clawdbot/plugin-sdk"; @@ -24,10 +24,10 @@ const channel = "telegram-user" as const; type TelegramUserChannelConfig = NonNullable["telegram-user"]; function setTelegramUserDmPolicy( - cfg: ClawdbotConfig, + cfg: MoltbotConfig, policy: DmPolicy, accountId?: string, -): ClawdbotConfig { +): MoltbotConfig { const resolvedAccountId = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID; const current = cfg.channels?.["telegram-user"] as TelegramUserChannelConfig | undefined; const allowFrom = @@ -38,13 +38,13 @@ function setTelegramUserDmPolicy( if (resolvedAccountId === DEFAULT_ACCOUNT_ID) { return { ...cfg, - channels: { - ...cfg.channels, - "telegram-user": { - ...(current ?? {}), - dmPolicy: policy, - ...(allowFrom ? { allowFrom } : {}), - }, + channels: { + ...cfg.channels, + "telegram-user": { + ...(current ?? {}), + dmPolicy: policy, + ...(allowFrom ? { allowFrom } : {}), + }, }, }; } @@ -73,7 +73,7 @@ async function noteTelegramUserAuthHelp(prompter: WizardPrompter): Promise [ "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 `clawdbot channels login --channel telegram-user`.", + "Login happens via `moltbot channels login --channel telegram-user`.", `Docs: ${formatDocsLink("/channels/telegram-user", "channels/telegram-user")}`, ].join("\n"), "Telegram user setup", @@ -94,10 +94,10 @@ function parseAllowFromInput(raw: string): string[] { } async function promptTelegramUserAllowFrom(params: { - cfg: ClawdbotConfig; + cfg: MoltbotConfig; prompter: WizardPrompter; accountId?: string; -}): Promise { +}): Promise { const accountId = normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID; const resolved = resolveTelegramUserAccount({ cfg: params.cfg as CoreConfig, @@ -197,7 +197,7 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { let accountId = override ? normalizeAccountId(override) : defaultAccountId; if (shouldPromptAccountIds && !override) { accountId = await promptAccountId({ - cfg: cfg as ClawdbotConfig, + cfg: cfg as MoltbotConfig, prompter, label: "Telegram User", currentId: accountId ?? defaultAccountId, @@ -344,7 +344,7 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { } catch (err) { runtime.error(`Telegram user login failed: ${String(err)}`); await prompter.note( - `Run \`clawdbot channels login --channel telegram-user\` later to link.`, + `Run \`moltbot channels login --channel telegram-user\` later to link.`, "Telegram user login", ); } @@ -353,7 +353,7 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { await prompter.note( [ "Next: link the account via QR or phone code.", - "Run: clawdbot channels login --channel telegram-user", + "Run: moltbot channels login --channel telegram-user", ].join("\n"), "Telegram user login", ); diff --git a/extensions/telegram-user/src/send.ts b/extensions/telegram-user/src/send.ts index 5d55b0354..13e6558eb 100644 --- a/extensions/telegram-user/src/send.ts +++ b/extensions/telegram-user/src/send.ts @@ -6,6 +6,7 @@ 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 = { @@ -145,6 +146,8 @@ async function resolveClient(params: { 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) { @@ -153,7 +156,7 @@ async function resolveClient(params: { const storagePath = resolveTelegramUserSessionPath(account.accountId); if (!fs.existsSync(storagePath)) { throw new Error( - "Telegram user session missing. Run `clawdbot channels login --channel telegram-user` first.", + "Telegram user session missing. Run `moltbot channels login --channel telegram-user` first.", ); } const client = await createTelegramUserClient({ apiId, apiHash, storagePath });