@chatman-media/channel-telegram 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bot-api/client.d.ts +1 -0
- package/dist/bot-api/client.d.ts.map +1 -1
- package/dist/bot-api/client.test.d.ts +2 -0
- package/dist/bot-api/client.test.d.ts.map +1 -0
- package/dist/index.js +21 -8
- package/dist/userbot/adapter.d.ts +14 -0
- package/dist/userbot/adapter.d.ts.map +1 -1
- package/dist/userbot/adapter.test.d.ts +2 -0
- package/dist/userbot/adapter.test.d.ts.map +1 -0
- package/dist/userbot/login.d.ts +4 -0
- package/dist/userbot/login.d.ts.map +1 -1
- package/dist/userbot/login.test.d.ts +2 -0
- package/dist/userbot/login.test.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/bot-api/client.test.ts +129 -0
- package/src/bot-api/client.ts +2 -0
- package/src/userbot/adapter.test.ts +253 -0
- package/src/userbot/adapter.ts +35 -11
- package/src/userbot/login.test.ts +118 -0
- package/src/userbot/login.ts +10 -3
package/dist/bot-api/client.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/bot-api/client.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/bot-api/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../../src/bot-api/client.test.ts"],"names":[],"mappings":""}
|
package/dist/index.js
CHANGED
|
@@ -32668,6 +32668,8 @@ class TelegramClient {
|
|
|
32668
32668
|
params.text = input.text;
|
|
32669
32669
|
if (input.showAlert)
|
|
32670
32670
|
params.show_alert = input.showAlert;
|
|
32671
|
+
if (input.url)
|
|
32672
|
+
params.url = input.url;
|
|
32671
32673
|
return this.call("answerCallbackQuery", params);
|
|
32672
32674
|
}
|
|
32673
32675
|
sendChatAction(input) {
|
|
@@ -32972,6 +32974,16 @@ var import_telegram = __toESM(require_telegram(), 1);
|
|
|
32972
32974
|
var import_events = __toESM(require_events(), 1);
|
|
32973
32975
|
var import_Logger = __toESM(require_Logger(), 1);
|
|
32974
32976
|
var import_sessions = __toESM(require_sessions(), 1);
|
|
32977
|
+
function defaultGramjsClientFactory(o) {
|
|
32978
|
+
const session = new import_sessions.StringSession(o.sessionString);
|
|
32979
|
+
const client = new import_telegram.TelegramClient(session, o.apiId, o.apiHash, {
|
|
32980
|
+
connectionRetries: o.connectionRetries,
|
|
32981
|
+
retryDelay: o.retryDelayMs,
|
|
32982
|
+
timeout: 30
|
|
32983
|
+
});
|
|
32984
|
+
client.setLogLevel(import_Logger.LogLevel.NONE);
|
|
32985
|
+
return client;
|
|
32986
|
+
}
|
|
32975
32987
|
var TG_USERBOT_CAPABILITIES = {
|
|
32976
32988
|
text: true,
|
|
32977
32989
|
photo: true,
|
|
@@ -33004,13 +33016,13 @@ class TelegramUserbotAdapter {
|
|
|
33004
33016
|
async connect() {
|
|
33005
33017
|
if (this.client && this.client.connected)
|
|
33006
33018
|
return;
|
|
33007
|
-
const
|
|
33008
|
-
|
|
33019
|
+
const client = (this.opts.clientFactory ?? defaultGramjsClientFactory)({
|
|
33020
|
+
sessionString: this.opts.sessionString,
|
|
33021
|
+
apiId: this.opts.apiId,
|
|
33022
|
+
apiHash: this.opts.apiHash,
|
|
33009
33023
|
connectionRetries: this.opts.connectionRetries ?? 5,
|
|
33010
|
-
|
|
33011
|
-
timeout: 30
|
|
33024
|
+
retryDelayMs: this.opts.retryDelayMs ?? 3000
|
|
33012
33025
|
});
|
|
33013
|
-
client.setLogLevel(import_Logger.LogLevel.NONE);
|
|
33014
33026
|
const maxAttempts = this.opts.connectionRetries ?? 5;
|
|
33015
33027
|
let lastErr = null;
|
|
33016
33028
|
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
@@ -33344,10 +33356,11 @@ function mapRpcError(err) {
|
|
|
33344
33356
|
}
|
|
33345
33357
|
return new UserbotLoginError("unknown", msg);
|
|
33346
33358
|
}
|
|
33359
|
+
function defaultLoginClientFactory(apiId, apiHash) {
|
|
33360
|
+
return new import_telegram2.TelegramClient(new import_sessions2.StringSession(""), apiId, apiHash, { connectionRetries: 3 });
|
|
33361
|
+
}
|
|
33347
33362
|
async function startUserbotLogin(opts) {
|
|
33348
|
-
const client =
|
|
33349
|
-
connectionRetries: 3
|
|
33350
|
-
});
|
|
33363
|
+
const client = (opts.clientFactory ?? defaultLoginClientFactory)(opts.apiId, opts.apiHash);
|
|
33351
33364
|
await client.connect();
|
|
33352
33365
|
try {
|
|
33353
33366
|
const { phoneCodeHash } = await client.sendCode({ apiId: opts.apiId, apiHash: opts.apiHash }, opts.phone);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChannelAdapter, ChannelCapabilities, DeleteOpts, EditOpts, Inbound, MediaRef, OutboundEnvelope, Sent } from "@chatman-media/channel-core";
|
|
2
|
+
import { TelegramClient as GramjsClient } from "telegram";
|
|
2
3
|
/**
|
|
3
4
|
* Healthcheck-статусы MTProto userbot'а. apps/worker отслеживает их через
|
|
4
5
|
* healthEvents() async iterable и:
|
|
@@ -52,6 +53,19 @@ export interface TelegramUserbotAdapterOptions {
|
|
|
52
53
|
connectionRetries?: number;
|
|
53
54
|
/** ms между connect-попытками, default 5000. */
|
|
54
55
|
retryDelayMs?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Фабрика gramjs-клиента. По умолчанию — реальный конструктор
|
|
58
|
+
* (StringSession + GramjsClient + setLogLevel). Инъектируется в тестах,
|
|
59
|
+
* чтобы покрыть connect() без живого MTProto-соединения.
|
|
60
|
+
*/
|
|
61
|
+
clientFactory?: (opts: GramjsClientFactoryOpts) => GramjsClient;
|
|
62
|
+
}
|
|
63
|
+
export interface GramjsClientFactoryOpts {
|
|
64
|
+
sessionString: string;
|
|
65
|
+
apiId: number;
|
|
66
|
+
apiHash: string;
|
|
67
|
+
connectionRetries: number;
|
|
68
|
+
retryDelayMs: number;
|
|
55
69
|
}
|
|
56
70
|
export declare class TelegramUserbotAdapter implements ChannelAdapter {
|
|
57
71
|
readonly kind: "telegram_userbot";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/userbot/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,mBAAmB,EACnB,UAAU,EACV,QAAQ,EACR,OAAO,EAEP,QAAQ,EACR,gBAAgB,EAChB,IAAI,EACL,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/userbot/adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.d.ts","sourceRoot":"","sources":["../../src/userbot/adapter.test.ts"],"names":[],"mappings":""}
|
package/dist/userbot/login.d.ts
CHANGED
|
@@ -31,11 +31,15 @@ export interface FinishedUserbotLogin {
|
|
|
31
31
|
username: string | null;
|
|
32
32
|
phone: string | null;
|
|
33
33
|
}
|
|
34
|
+
/** Фабрика клиента для startUserbotLogin (по умолчанию — реальный конструктор). */
|
|
35
|
+
export type LoginClientFactory = (apiId: number, apiHash: string) => TelegramClient;
|
|
34
36
|
/** Шаг 1: подключиться и отправить код подтверждения на номер. */
|
|
35
37
|
export declare function startUserbotLogin(opts: {
|
|
36
38
|
apiId: number;
|
|
37
39
|
apiHash: string;
|
|
38
40
|
phone: string;
|
|
41
|
+
/** Инъекция клиента для тестов; по умолчанию — defaultLoginClientFactory. */
|
|
42
|
+
clientFactory?: LoginClientFactory;
|
|
39
43
|
}): Promise<StartedUserbotLogin>;
|
|
40
44
|
/**
|
|
41
45
|
* Шаг 2: отправить код. Если у аккаунта включён 2FA — Telegram вернёт
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/userbot/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,cAAc,EAAE,MAAM,UAAU,CAAC;AAI/C;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,qBAAqB,GAC7B,eAAe,GACf,cAAc,GACd,cAAc,GACd,kBAAkB,GAClB,YAAY,GACZ,SAAS,CAAC;AAEd,qBAAa,iBAAkB,SAAQ,KAAK;IAExC,QAAQ,CAAC,IAAI,EAAE,qBAAqB;IAEpC,6CAA6C;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM;gBAHtB,IAAI,EAAE,qBAAqB,EACpC,OAAO,EAAE,MAAM;IACf,6CAA6C;IACpC,aAAa,CAAC,EAAE,MAAM,YAAA;CAKlC;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,cAAc,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAgCD,kEAAkE;AAClE,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/userbot/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,cAAc,EAAE,MAAM,UAAU,CAAC;AAI/C;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,qBAAqB,GAC7B,eAAe,GACf,cAAc,GACd,cAAc,GACd,kBAAkB,GAClB,YAAY,GACZ,SAAS,CAAC;AAEd,qBAAa,iBAAkB,SAAQ,KAAK;IAExC,QAAQ,CAAC,IAAI,EAAE,qBAAqB;IAEpC,6CAA6C;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM;gBAHtB,IAAI,EAAE,qBAAqB,EACpC,OAAO,EAAE,MAAM;IACf,6CAA6C;IACpC,aAAa,CAAC,EAAE,MAAM,YAAA;CAKlC;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,cAAc,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAgCD,mFAAmF;AACnF,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,cAAc,CAAC;AAMpF,kEAAkE;AAClE,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAa/B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,GAAG,CAAC;IAAE,QAAQ,EAAE,KAAK,CAAA;CAAE,GAAG,oBAAoB,CAAC,CAAC,CAoB7E;AAED,2DAA2D;AAC3D,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAShC;AAED,mEAAmE;AACnE,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAS9F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.test.d.ts","sourceRoot":"","sources":["../../src/userbot/login.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatman-media/channel-telegram",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Telegram-каналы (BotAPI + MTProto userbot) как реализация ChannelAdapter из @chatman-media/channel-core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"author": "Alexander Kireev",
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@chatman-media/channel-core": "1.
|
|
40
|
+
"@chatman-media/channel-core": "1.2.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"telegram": ">=2.26.0"
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"telegram": "^2.26.22"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@biomejs/biome": "^2.4.
|
|
54
|
+
"@biomejs/biome": "^2.4.16",
|
|
55
55
|
"@types/bun": "1.3.14",
|
|
56
56
|
"typescript": "^6.0.3"
|
|
57
57
|
},
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { TelegramApiError, TelegramClient } from "./client.ts";
|
|
3
|
+
import type { FetchLike } from "./client.ts";
|
|
4
|
+
|
|
5
|
+
function envelope(result: unknown): Response {
|
|
6
|
+
return new Response(JSON.stringify({ ok: true, result }), { status: 200 });
|
|
7
|
+
}
|
|
8
|
+
function mock(handler: (url: string, init?: RequestInit) => Response | Promise<Response>) {
|
|
9
|
+
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
|
10
|
+
const fn = (async (url: string, init?: RequestInit) => {
|
|
11
|
+
calls.push({ url: String(url), init });
|
|
12
|
+
return handler(String(url), init);
|
|
13
|
+
}) as unknown as FetchLike;
|
|
14
|
+
return { fn, calls };
|
|
15
|
+
}
|
|
16
|
+
const client = (fetch: FetchLike) => new TelegramClient({ token: "T", fetch });
|
|
17
|
+
const bodyOf = (init?: RequestInit) => JSON.parse(init?.body as string);
|
|
18
|
+
|
|
19
|
+
describe("TelegramClient constructor", () => {
|
|
20
|
+
it("требует token", () => {
|
|
21
|
+
expect(() => new TelegramClient({ token: "" })).toThrow("token is required");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("TelegramClient методы", () => {
|
|
26
|
+
it("getMe", async () => {
|
|
27
|
+
const { fn, calls } = mock(() => envelope({ id: 1, is_bot: true }));
|
|
28
|
+
expect((await client(fn).getMe()).id).toBe(1);
|
|
29
|
+
expect(calls[0]!.url).toBe("https://api.telegram.org/botT/getMe");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("sendMessage — параметры и результат", async () => {
|
|
33
|
+
const { fn, calls } = mock(() => envelope({ message_id: 9 }));
|
|
34
|
+
const r = await client(fn).sendMessage({
|
|
35
|
+
chatId: "100",
|
|
36
|
+
text: "hi",
|
|
37
|
+
parseMode: "HTML",
|
|
38
|
+
replyMarkup: { inline_keyboard: [[{ text: "x", callback_data: "y" }]] },
|
|
39
|
+
disableWebPagePreview: true,
|
|
40
|
+
replyToMessageId: 5,
|
|
41
|
+
});
|
|
42
|
+
expect(r.message_id).toBe(9);
|
|
43
|
+
const b = bodyOf(calls[0]!.init);
|
|
44
|
+
expect(b).toMatchObject({ chat_id: "100", text: "hi", parse_mode: "HTML", disable_web_page_preview: true, reply_to_message_id: 5 });
|
|
45
|
+
expect(b.reply_markup.inline_keyboard).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("sendPhoto / sendVideo / sendDocument", async () => {
|
|
49
|
+
const { fn, calls } = mock(() => envelope({ message_id: 1 }));
|
|
50
|
+
const c = client(fn);
|
|
51
|
+
await c.sendPhoto({ chatId: 1, photoFileId: "p", caption: "c" });
|
|
52
|
+
await c.sendVideo({ chatId: 1, videoFileId: "v" });
|
|
53
|
+
await c.sendDocument({ chatId: 1, documentFileId: "d", caption: "cap" });
|
|
54
|
+
expect(calls[0]!.url).toContain("/sendPhoto");
|
|
55
|
+
expect(bodyOf(calls[0]!.init).photo).toBe("p");
|
|
56
|
+
expect(calls[1]!.url).toContain("/sendVideo");
|
|
57
|
+
expect(bodyOf(calls[2]!.init).document).toBe("d");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("sendLocalVideo — не поддержано (throws)", () => {
|
|
61
|
+
expect(() => client(mock(() => envelope({})).fn).sendLocalVideo({ chatId: 1, localFilePath: "/x" })).toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("editMessageText / deleteMessage", async () => {
|
|
65
|
+
const { fn, calls } = mock((url) => (url.includes("delete") ? envelope(true) : envelope({ message_id: 2 })));
|
|
66
|
+
const c = client(fn);
|
|
67
|
+
await c.editMessageText({ chatId: 1, messageId: 2, text: "new", parseMode: "HTML", replyMarkup: {} });
|
|
68
|
+
expect(await c.deleteMessage({ chatId: 1, messageId: 2 })).toBe(true);
|
|
69
|
+
expect(bodyOf(calls[0]!.init)).toMatchObject({ message_id: 2, text: "new" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("answerCallbackQuery — text + show_alert", async () => {
|
|
73
|
+
const { fn, calls } = mock(() => envelope(true));
|
|
74
|
+
await client(fn).answerCallbackQuery({ callbackQueryId: "cb", text: "ok", showAlert: true });
|
|
75
|
+
expect(bodyOf(calls[0]!.init)).toMatchObject({ callback_query_id: "cb", text: "ok", show_alert: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("answerCallbackQuery — url", async () => {
|
|
79
|
+
const { fn, calls } = mock(() => envelope(true));
|
|
80
|
+
await client(fn).answerCallbackQuery({ callbackQueryId: "cb", url: "https://app.test/conversations/7" });
|
|
81
|
+
expect(bodyOf(calls[0]!.init)).toMatchObject({
|
|
82
|
+
callback_query_id: "cb",
|
|
83
|
+
url: "https://app.test/conversations/7",
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("sendChatAction", async () => {
|
|
88
|
+
const { fn, calls } = mock(() => envelope(true));
|
|
89
|
+
await client(fn).sendChatAction({ chatId: 1, action: "typing" });
|
|
90
|
+
expect(bodyOf(calls[0]!.init).action).toBe("typing");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("setWebhook / deleteWebhook / getWebhookInfo", async () => {
|
|
94
|
+
const { fn, calls } = mock((url) =>
|
|
95
|
+
url.includes("getWebhookInfo") ? envelope({ url: "u", pending_update_count: 0 }) : envelope(true),
|
|
96
|
+
);
|
|
97
|
+
const c = client(fn);
|
|
98
|
+
await c.setWebhook({ url: "https://x", secretToken: "s", allowedUpdates: ["message"], dropPendingUpdates: true });
|
|
99
|
+
await c.deleteWebhook(true);
|
|
100
|
+
expect((await c.getWebhookInfo()).url).toBe("u");
|
|
101
|
+
expect(bodyOf(calls[0]!.init)).toMatchObject({ url: "https://x", secret_token: "s", drop_pending_updates: true });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("getFile + downloadFile (two-step)", async () => {
|
|
105
|
+
const { fn } = mock((url) => (url.includes("/getFile") ? envelope({ file_path: "photos/a.jpg" }) : new Response("BYTES")));
|
|
106
|
+
const res = await client(fn).downloadFile("fid");
|
|
107
|
+
expect(await res.text()).toBe("BYTES");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("downloadFile без file_path → throws", async () => {
|
|
111
|
+
const { fn } = mock(() => envelope({ file_id: "f" }));
|
|
112
|
+
await expect(client(fn).downloadFile("fid")).rejects.toThrow("no file_path");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("TelegramClient.call error-ветки", () => {
|
|
117
|
+
it("HTTP !ok → TelegramApiError", async () => {
|
|
118
|
+
const { fn } = mock(() => new Response(JSON.stringify({ ok: false, error_code: 401, description: "bad" }), { status: 401 }));
|
|
119
|
+
await expect(client(fn).getMe()).rejects.toBeInstanceOf(TelegramApiError);
|
|
120
|
+
});
|
|
121
|
+
it("body.ok=false → TelegramApiError с описанием", async () => {
|
|
122
|
+
const { fn } = mock(() => new Response(JSON.stringify({ ok: false, description: "nope" }), { status: 200 }));
|
|
123
|
+
await expect(client(fn).getMe()).rejects.toThrow("nope");
|
|
124
|
+
});
|
|
125
|
+
it("non-JSON → TelegramApiError", async () => {
|
|
126
|
+
const { fn } = mock(() => new Response("<html>", { status: 200 }));
|
|
127
|
+
await expect(client(fn).getMe()).rejects.toBeInstanceOf(TelegramApiError);
|
|
128
|
+
});
|
|
129
|
+
});
|
package/src/bot-api/client.ts
CHANGED
|
@@ -230,12 +230,14 @@ export class TelegramClient {
|
|
|
230
230
|
callbackQueryId: string;
|
|
231
231
|
text?: string;
|
|
232
232
|
showAlert?: boolean;
|
|
233
|
+
url?: string;
|
|
233
234
|
}): Promise<true> {
|
|
234
235
|
const params: Record<string, unknown> = {
|
|
235
236
|
callback_query_id: input.callbackQueryId,
|
|
236
237
|
};
|
|
237
238
|
if (input.text) params.text = input.text;
|
|
238
239
|
if (input.showAlert) params.show_alert = input.showAlert;
|
|
240
|
+
if (input.url) params.url = input.url;
|
|
239
241
|
return this.call<true>("answerCallbackQuery", params);
|
|
240
242
|
}
|
|
241
243
|
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { Inbound } from "@chatman-media/channel-core";
|
|
2
|
+
import { describe, expect, it } from "bun:test";
|
|
3
|
+
import { TelegramUserbotAdapter } from "./adapter.ts";
|
|
4
|
+
|
|
5
|
+
function make() {
|
|
6
|
+
return new TelegramUserbotAdapter({ id: "ub1", apiId: 1, apiHash: "h", sessionString: "" });
|
|
7
|
+
}
|
|
8
|
+
// Доступ к приватам/инъекция фейкового gramjs-клиента.
|
|
9
|
+
// biome-ignore lint/suspicious/noExplicitAny: тестовый доступ к приватам
|
|
10
|
+
const priv = (a: TelegramUserbotAdapter) => a as any;
|
|
11
|
+
|
|
12
|
+
function event(over: Record<string, unknown> = {}) {
|
|
13
|
+
return {
|
|
14
|
+
message: {
|
|
15
|
+
senderId: { toString: () => "123" },
|
|
16
|
+
peerId: { className: "PeerUser" },
|
|
17
|
+
message: "hi",
|
|
18
|
+
id: 5,
|
|
19
|
+
...over,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("TelegramUserbotAdapter базовое", () => {
|
|
25
|
+
it("kind/id/capabilities", () => {
|
|
26
|
+
const a = make();
|
|
27
|
+
expect(a.kind).toBe("telegram_userbot");
|
|
28
|
+
expect(a.id).toBe("ub1");
|
|
29
|
+
expect(a.capabilities.text).toBe(true);
|
|
30
|
+
expect(a.capabilities.callbackQuery).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("eventToInbound", () => {
|
|
35
|
+
it("текст → text-part", () => {
|
|
36
|
+
const inb = priv(make()).eventToInbound(event());
|
|
37
|
+
expect(inb.externalUserId).toBe("123");
|
|
38
|
+
expect(inb.externalMessageId).toBe("5");
|
|
39
|
+
expect(inb.parts).toEqual([{ kind: "text", text: "hi" }]);
|
|
40
|
+
});
|
|
41
|
+
it("фото → photo-part с caption", () => {
|
|
42
|
+
const inb = priv(make()).eventToInbound(event({ message: "подпись", photo: {}, id: 7 }));
|
|
43
|
+
expect(inb.parts[0]).toMatchObject({ kind: "photo", caption: "подпись" });
|
|
44
|
+
});
|
|
45
|
+
it("video / voice / document", () => {
|
|
46
|
+
const a = priv(make());
|
|
47
|
+
expect(a.eventToInbound(event({ message: "", video: { duration: 3 }, photo: undefined })).parts[0].kind).toBe("video");
|
|
48
|
+
expect(a.eventToInbound(event({ message: "", voice: { duration: 2 } })).parts[0]).toMatchObject({ kind: "voice", durationSec: 2 });
|
|
49
|
+
expect(
|
|
50
|
+
a.eventToInbound(event({ message: "", document: { mimeType: "application/pdf", attributes: [{ fileName: "x.pdf" }] } })).parts[0],
|
|
51
|
+
).toMatchObject({ kind: "document", mimeType: "application/pdf", fileName: "x.pdf" });
|
|
52
|
+
});
|
|
53
|
+
it("не-private peer → null", () => {
|
|
54
|
+
expect(priv(make()).eventToInbound(event({ peerId: { className: "PeerChannel" } }))).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
it("нет senderId → null", () => {
|
|
57
|
+
expect(priv(make()).eventToInbound(event({ senderId: undefined }))).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
it("пустой текст без медиа → null", () => {
|
|
60
|
+
expect(priv(make()).eventToInbound(event({ message: "" }))).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("cacheMessage LRU", () => {
|
|
65
|
+
it("вытесняет старейшие при превышении лимита", () => {
|
|
66
|
+
const a = priv(make());
|
|
67
|
+
for (let i = 0; i < 300; i++) a.cacheMessage("u", String(i), { i });
|
|
68
|
+
expect(a.recentMessages.size).toBe(256);
|
|
69
|
+
expect(a.recentMessages.has("u:0")).toBe(false); // старейшие вытеснены
|
|
70
|
+
expect(a.recentMessages.has("u:299")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("receive() очередь", () => {
|
|
75
|
+
it("отдаёт заэнкью'енные inbound, затем done после close", async () => {
|
|
76
|
+
const a = make();
|
|
77
|
+
priv(a).enqueue({ channelId: "ub1", externalMessageId: "1", externalUserId: "123", parts: [], receivedAt: 0 } as unknown as Inbound);
|
|
78
|
+
const it = a.receive()[Symbol.asyncIterator]();
|
|
79
|
+
expect((await it.next()).value.externalMessageId).toBe("1");
|
|
80
|
+
await a.close();
|
|
81
|
+
expect((await it.next()).done).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("healthEvents()", () => {
|
|
86
|
+
it("эмит → читается из итератора", async () => {
|
|
87
|
+
const a = make();
|
|
88
|
+
priv(a).emitHealth({ status: "connected" });
|
|
89
|
+
const it = a.healthEvents()[Symbol.asyncIterator]();
|
|
90
|
+
expect((await it.next()).value.status).toBe("connected");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("send / delete / downloadMedia / signalTyping", () => {
|
|
95
|
+
const fakeClient = () => ({
|
|
96
|
+
sendMessage: async () => ({ id: 42 }),
|
|
97
|
+
deleteMessages: async () => {},
|
|
98
|
+
downloadMedia: async () => Buffer.from("MEDIA"),
|
|
99
|
+
getInputEntity: async (id: number) => ({ id }),
|
|
100
|
+
invoke: async () => ({}),
|
|
101
|
+
disconnect: async () => {},
|
|
102
|
+
connected: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("send до connect → throws", async () => {
|
|
106
|
+
await expect(make().send({ externalUserId: "1", parts: [{ kind: "text", text: "x" }] } as never)).rejects.toThrow("before connect");
|
|
107
|
+
});
|
|
108
|
+
it("send пустые parts → throws", async () => {
|
|
109
|
+
const a = make();
|
|
110
|
+
priv(a).client = fakeClient();
|
|
111
|
+
await expect(a.send({ externalUserId: "1", parts: [] } as never)).rejects.toThrow("non-empty");
|
|
112
|
+
});
|
|
113
|
+
it("send текст → Sent с id", async () => {
|
|
114
|
+
const a = make();
|
|
115
|
+
priv(a).client = fakeClient();
|
|
116
|
+
const sent = await a.send({ externalUserId: "999", parts: [{ kind: "text", text: "hi" }] } as never);
|
|
117
|
+
expect(sent.externalMessageId).toBe("42");
|
|
118
|
+
expect(sent.channelId).toBe("ub1");
|
|
119
|
+
});
|
|
120
|
+
it("send неподдержанный part.kind → throws", async () => {
|
|
121
|
+
const a = make();
|
|
122
|
+
priv(a).client = fakeClient();
|
|
123
|
+
await expect(a.send({ externalUserId: "1", parts: [{ kind: "photo", mediaRef: { channelId: "ub1", externalRef: "r" } }] } as never)).rejects.toThrow("unsupported");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("delete до connect → throws; NaN id → throws; ok с клиентом", async () => {
|
|
127
|
+
await expect(make().delete({ externalUserId: "1", externalMessageId: "2" } as never)).rejects.toThrow("before connect");
|
|
128
|
+
const a = make();
|
|
129
|
+
priv(a).client = fakeClient();
|
|
130
|
+
await expect(a.delete({ externalUserId: "1", externalMessageId: "nope" } as never)).rejects.toThrow("numeric");
|
|
131
|
+
await a.delete({ externalUserId: "1", externalMessageId: "2" } as never); // не бросает
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("downloadMedia: guards + успех из кэша", async () => {
|
|
135
|
+
await expect(make().downloadMedia({ channelId: "ub1", externalRef: "7" })).rejects.toThrow("before connect");
|
|
136
|
+
const a = make();
|
|
137
|
+
priv(a).client = fakeClient();
|
|
138
|
+
await expect(a.downloadMedia({ channelId: "ub1", externalRef: "7" })).rejects.toThrow("externalUserId");
|
|
139
|
+
await expect(a.downloadMedia({ channelId: "ub1", externalRef: "7" }, { externalUserId: "123" })).rejects.toThrow("evicted");
|
|
140
|
+
priv(a).recentMessages.set("123:7", { msg: true });
|
|
141
|
+
const res = await a.downloadMedia({ channelId: "ub1", externalRef: "7" }, { externalUserId: "123" });
|
|
142
|
+
expect(await res.text()).toBe("MEDIA");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("signalTyping: без клиента no-op; с клиентом invoke", async () => {
|
|
146
|
+
await make().signalTyping("1"); // no-op, не бросает
|
|
147
|
+
const a = make();
|
|
148
|
+
let invoked = false;
|
|
149
|
+
priv(a).client = { ...fakeClient(), invoke: async () => { invoked = true; } };
|
|
150
|
+
await a.signalTyping("123");
|
|
151
|
+
expect(invoked).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("resolvePeer NaN → throws", async () => {
|
|
155
|
+
const a = make();
|
|
156
|
+
priv(a).client = fakeClient();
|
|
157
|
+
await expect(priv(a).resolvePeer("not-a-number")).rejects.toThrow("invalid externalUserId");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("edit всегда throws (capability disabled)", async () => {
|
|
161
|
+
await expect(make().edit({} as never)).rejects.toThrow("disabled");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("registerHandler + receive abort", () => {
|
|
166
|
+
it("registerHandler ставит обработчик, эмитит connected, входящее → inbox+cache", async () => {
|
|
167
|
+
const a = make();
|
|
168
|
+
let handler: ((e: unknown) => Promise<void>) | null = null;
|
|
169
|
+
priv(a).registerHandler({ addEventHandler: (h: (e: unknown) => Promise<void>) => { handler = h; } });
|
|
170
|
+
// эмит "connected"
|
|
171
|
+
const hIt = a.healthEvents()[Symbol.asyncIterator]();
|
|
172
|
+
expect((await hIt.next()).value.status).toBe("connected");
|
|
173
|
+
// прогоняем входящее сообщение через захваченный обработчик
|
|
174
|
+
expect(handler).not.toBeNull();
|
|
175
|
+
await handler!(event());
|
|
176
|
+
expect(priv(a).recentMessages.has("123:5")).toBe(true);
|
|
177
|
+
const rIt = a.receive()[Symbol.asyncIterator]();
|
|
178
|
+
expect((await rIt.next()).value.externalUserId).toBe("123");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("registerHandler пропускает свои исходящие (out)", async () => {
|
|
182
|
+
const a = make();
|
|
183
|
+
let handler: ((e: unknown) => Promise<void>) | null = null;
|
|
184
|
+
priv(a).registerHandler({ addEventHandler: (h: (e: unknown) => Promise<void>) => { handler = h; } });
|
|
185
|
+
await handler!({ message: { out: true, senderId: { toString: () => "1" }, peerId: { className: "PeerUser" }, message: "x", id: 1 } });
|
|
186
|
+
expect(priv(a).inbox.length).toBe(0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("receive(): abort прерывает ожидание (done)", async () => {
|
|
190
|
+
const a = make();
|
|
191
|
+
const ctrl = new AbortController();
|
|
192
|
+
const it = a.receive(ctrl.signal)[Symbol.asyncIterator]();
|
|
193
|
+
const p = it.next(); // нет элементов → ждёт
|
|
194
|
+
ctrl.abort();
|
|
195
|
+
expect((await p).done).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("connect() (через clientFactory)", () => {
|
|
200
|
+
const fakeGramjs = (over: Record<string, unknown> = {}) => ({
|
|
201
|
+
connect: async () => true,
|
|
202
|
+
session: { save: () => "NEWSESS" },
|
|
203
|
+
addEventHandler: () => {},
|
|
204
|
+
disconnect: async () => {},
|
|
205
|
+
connected: false,
|
|
206
|
+
...over,
|
|
207
|
+
});
|
|
208
|
+
const withFactory = (fc: unknown, over: Record<string, unknown> = {}) =>
|
|
209
|
+
new TelegramUserbotAdapter({
|
|
210
|
+
id: "ub1",
|
|
211
|
+
apiId: 1,
|
|
212
|
+
apiHash: "h",
|
|
213
|
+
sessionString: "",
|
|
214
|
+
connectionRetries: 1,
|
|
215
|
+
retryDelayMs: 0,
|
|
216
|
+
clientFactory: (() => fc) as never,
|
|
217
|
+
...over,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("успех: client установлен, emitted connected, onSessionUpdated при смене сессии", async () => {
|
|
221
|
+
const updated: string[] = [];
|
|
222
|
+
const a = withFactory(fakeGramjs(), { onSessionUpdated: (s: string) => updated.push(s) });
|
|
223
|
+
await a.connect();
|
|
224
|
+
expect((await a.healthEvents()[Symbol.asyncIterator]().next()).value.status).toBe("connected");
|
|
225
|
+
expect(updated).toEqual(["NEWSESS"]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("AUTH_KEY_DUPLICATED → health auth_key_duplicated + throw", async () => {
|
|
229
|
+
const a = withFactory(fakeGramjs({ connect: async () => { throw new Error("AUTH_KEY_DUPLICATED"); } }));
|
|
230
|
+
await expect(a.connect()).rejects.toThrow("auth key revoked");
|
|
231
|
+
expect((await a.healthEvents()[Symbol.asyncIterator]().next()).value.status).toBe("auth_key_duplicated");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("connect бросает (non-auth) на всех попытках → connection_failed + throw", async () => {
|
|
235
|
+
const a = withFactory(fakeGramjs({ connect: async () => { throw new Error("network blip"); } }));
|
|
236
|
+
await expect(a.connect()).rejects.toThrow("connect failed");
|
|
237
|
+
expect((await a.healthEvents()[Symbol.asyncIterator]().next()).value.status).toBe("connection_failed");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("connect возвращает false → connection_failed", async () => {
|
|
241
|
+
const a = withFactory(fakeGramjs({ connect: async () => false }));
|
|
242
|
+
await expect(a.connect()).rejects.toThrow("connect failed");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("идемпотентность: уже connected → фабрика не зовётся", async () => {
|
|
246
|
+
const a = new TelegramUserbotAdapter({
|
|
247
|
+
id: "ub1", apiId: 1, apiHash: "h", sessionString: "",
|
|
248
|
+
clientFactory: (() => { throw new Error("должен быть no-op"); }) as never,
|
|
249
|
+
});
|
|
250
|
+
priv(a).client = { connected: true };
|
|
251
|
+
await a.connect(); // не бросает → фабрика не вызвана
|
|
252
|
+
});
|
|
253
|
+
});
|
package/src/userbot/adapter.ts
CHANGED
|
@@ -69,6 +69,36 @@ export interface TelegramUserbotAdapterOptions {
|
|
|
69
69
|
connectionRetries?: number;
|
|
70
70
|
/** ms между connect-попытками, default 5000. */
|
|
71
71
|
retryDelayMs?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Фабрика gramjs-клиента. По умолчанию — реальный конструктор
|
|
74
|
+
* (StringSession + GramjsClient + setLogLevel). Инъектируется в тестах,
|
|
75
|
+
* чтобы покрыть connect() без живого MTProto-соединения.
|
|
76
|
+
*/
|
|
77
|
+
clientFactory?: (opts: GramjsClientFactoryOpts) => GramjsClient;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface GramjsClientFactoryOpts {
|
|
81
|
+
sessionString: string;
|
|
82
|
+
apiId: number;
|
|
83
|
+
apiHash: string;
|
|
84
|
+
connectionRetries: number;
|
|
85
|
+
retryDelayMs: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Реальная фабрика gramjs-клиента (production-путь connect()). */
|
|
89
|
+
function defaultGramjsClientFactory(o: GramjsClientFactoryOpts): GramjsClient {
|
|
90
|
+
const session = new StringSession(o.sessionString);
|
|
91
|
+
const client = new GramjsClient(session, o.apiId, o.apiHash, {
|
|
92
|
+
// count, not flag — 5 = разумный лимит чтобы supervisor мог перерестартить.
|
|
93
|
+
connectionRetries: o.connectionRetries,
|
|
94
|
+
retryDelay: o.retryDelayMs,
|
|
95
|
+
timeout: 30,
|
|
96
|
+
});
|
|
97
|
+
// GramJS на уровне ERROR console.error'ит рутинные ping-timeout'ы своего
|
|
98
|
+
// update-loop'а (он сам их ловит и reconnect'ит). Для long-running SaaS это
|
|
99
|
+
// шум; значимые статусы отдаём через healthEvents().
|
|
100
|
+
client.setLogLevel(LogLevel.NONE);
|
|
101
|
+
return client;
|
|
72
102
|
}
|
|
73
103
|
|
|
74
104
|
const TG_USERBOT_CAPABILITIES: ChannelCapabilities = {
|
|
@@ -117,19 +147,13 @@ export class TelegramUserbotAdapter implements ChannelAdapter {
|
|
|
117
147
|
*/
|
|
118
148
|
async connect(): Promise<void> {
|
|
119
149
|
if (this.client && this.client.connected) return;
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
const client = (this.opts.clientFactory ?? defaultGramjsClientFactory)({
|
|
151
|
+
sessionString: this.opts.sessionString,
|
|
152
|
+
apiId: this.opts.apiId,
|
|
153
|
+
apiHash: this.opts.apiHash,
|
|
124
154
|
connectionRetries: this.opts.connectionRetries ?? 5,
|
|
125
|
-
|
|
126
|
-
timeout: 30,
|
|
155
|
+
retryDelayMs: this.opts.retryDelayMs ?? 3000,
|
|
127
156
|
});
|
|
128
|
-
// GramJS на уровне ERROR console.error'ит рутинные ping-timeout'ы своего
|
|
129
|
-
// update-loop'а (он сам их ловит и делает reconnect — см. updates.js).
|
|
130
|
-
// Для long-running SaaS это шум; значимые статусы (connected /
|
|
131
|
-
// connection_failed / auth_key_duplicated) мы отдаём через healthEvents().
|
|
132
|
-
client.setLogLevel(LogLevel.NONE);
|
|
133
157
|
|
|
134
158
|
const maxAttempts = this.opts.connectionRetries ?? 5;
|
|
135
159
|
let lastErr: string | null = null;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { TelegramClient } from "telegram";
|
|
2
|
+
import { describe, expect, it } from "bun:test";
|
|
3
|
+
import {
|
|
4
|
+
finishUserbotLogin,
|
|
5
|
+
startUserbotLogin,
|
|
6
|
+
submitUserbot2fa,
|
|
7
|
+
submitUserbotCode,
|
|
8
|
+
UserbotLoginError,
|
|
9
|
+
type UserbotLoginErrorCode,
|
|
10
|
+
} from "./login.ts";
|
|
11
|
+
|
|
12
|
+
function fc(over: Record<string, unknown> = {}): TelegramClient {
|
|
13
|
+
return {
|
|
14
|
+
invoke: async () => ({}),
|
|
15
|
+
getMe: async () => ({ id: 777, username: "u", phone: "+1" }),
|
|
16
|
+
session: { save: () => "SESSION" },
|
|
17
|
+
...over,
|
|
18
|
+
} as unknown as TelegramClient;
|
|
19
|
+
}
|
|
20
|
+
const throwing = (msg: string) =>
|
|
21
|
+
fc({
|
|
22
|
+
invoke: async () => {
|
|
23
|
+
throw new Error(msg);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const code = (client: TelegramClient) =>
|
|
27
|
+
submitUserbotCode({ client, phone: "+100", phoneCodeHash: "h", code: "12345" });
|
|
28
|
+
|
|
29
|
+
describe("UserbotLoginError", () => {
|
|
30
|
+
it("несёт code/retryAfterSec/name", () => {
|
|
31
|
+
const e = new UserbotLoginError("flood_wait", "msg", 30);
|
|
32
|
+
expect(e.code).toBe("flood_wait");
|
|
33
|
+
expect(e.retryAfterSec).toBe(30);
|
|
34
|
+
expect(e.name).toBe("UserbotLoginError");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("finishUserbotLogin", () => {
|
|
39
|
+
it("собирает session + идентификаторы", async () => {
|
|
40
|
+
expect(await finishUserbotLogin(fc())).toEqual({
|
|
41
|
+
sessionString: "SESSION",
|
|
42
|
+
userId: "777",
|
|
43
|
+
username: "u",
|
|
44
|
+
phone: "+1",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it("null username/phone когда их нет", async () => {
|
|
48
|
+
const r = await finishUserbotLogin(fc({ getMe: async () => ({ id: 1 }) }));
|
|
49
|
+
expect(r.username).toBeNull();
|
|
50
|
+
expect(r.phone).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("submitUserbotCode", () => {
|
|
55
|
+
it("успех → needs2fa:false + session", async () => {
|
|
56
|
+
const r = await code(fc());
|
|
57
|
+
expect(r).toMatchObject({ needs2fa: false, sessionString: "SESSION", userId: "777" });
|
|
58
|
+
});
|
|
59
|
+
it("SESSION_PASSWORD_NEEDED → needs2fa:true", async () => {
|
|
60
|
+
expect(await code(throwing("SESSION_PASSWORD_NEEDED"))).toEqual({ needs2fa: true });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("mapRpcError (через submitUserbotCode)", () => {
|
|
65
|
+
const cases: Array<[string, UserbotLoginErrorCode, number | undefined]> = [
|
|
66
|
+
["FLOOD_WAIT_30", "flood_wait", 30],
|
|
67
|
+
["PHONE_NUMBER_INVALID", "phone_invalid", undefined],
|
|
68
|
+
["PHONE_CODE_EXPIRED", "code_expired", undefined],
|
|
69
|
+
["PHONE_CODE_INVALID", "code_invalid", undefined],
|
|
70
|
+
["что-то совсем другое", "unknown", undefined],
|
|
71
|
+
];
|
|
72
|
+
for (const [raw, expectedCode, retry] of cases) {
|
|
73
|
+
it(`${raw} → ${expectedCode}`, async () => {
|
|
74
|
+
try {
|
|
75
|
+
await code(throwing(raw));
|
|
76
|
+
throw new Error("ожидалась ошибка");
|
|
77
|
+
} catch (err) {
|
|
78
|
+
expect(err).toBeInstanceOf(UserbotLoginError);
|
|
79
|
+
expect((err as UserbotLoginError).code).toBe(expectedCode);
|
|
80
|
+
if (retry !== undefined) expect((err as UserbotLoginError).retryAfterSec).toBe(retry);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("submitUserbot2fa", () => {
|
|
87
|
+
it("ошибка invoke → password_invalid", async () => {
|
|
88
|
+
await expect(
|
|
89
|
+
submitUserbot2fa({ client: throwing("PASSWORD_HASH_INVALID"), password: "p" }),
|
|
90
|
+
).rejects.toMatchObject({ code: "password_invalid" });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("startUserbotLogin (через clientFactory)", () => {
|
|
95
|
+
it("успех → { client, phoneCodeHash }", async () => {
|
|
96
|
+
const fc = { connect: async () => {}, sendCode: async () => ({ phoneCodeHash: "hash" }), disconnect: async () => {} };
|
|
97
|
+
const r = await startUserbotLogin({ apiId: 1, apiHash: "h", phone: "+100", clientFactory: (() => fc) as never });
|
|
98
|
+
expect(r.phoneCodeHash).toBe("hash");
|
|
99
|
+
expect(r.client).toBe(fc as never);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("ошибка sendCode → disconnect + mapRpcError", async () => {
|
|
103
|
+
let disconnected = false;
|
|
104
|
+
const fc = {
|
|
105
|
+
connect: async () => {},
|
|
106
|
+
sendCode: async () => {
|
|
107
|
+
throw new Error("PHONE_NUMBER_INVALID");
|
|
108
|
+
},
|
|
109
|
+
disconnect: async () => {
|
|
110
|
+
disconnected = true;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
await expect(
|
|
114
|
+
startUserbotLogin({ apiId: 1, apiHash: "h", phone: "+100", clientFactory: (() => fc) as never }),
|
|
115
|
+
).rejects.toMatchObject({ code: "phone_invalid" });
|
|
116
|
+
expect(disconnected).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
package/src/userbot/login.ts
CHANGED
|
@@ -78,15 +78,22 @@ function mapRpcError(err: unknown): UserbotLoginError {
|
|
|
78
78
|
return new UserbotLoginError("unknown", msg);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Фабрика клиента для startUserbotLogin (по умолчанию — реальный конструктор). */
|
|
82
|
+
export type LoginClientFactory = (apiId: number, apiHash: string) => TelegramClient;
|
|
83
|
+
|
|
84
|
+
function defaultLoginClientFactory(apiId: number, apiHash: string): TelegramClient {
|
|
85
|
+
return new TelegramClient(new StringSession(""), apiId, apiHash, { connectionRetries: 3 });
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
/** Шаг 1: подключиться и отправить код подтверждения на номер. */
|
|
82
89
|
export async function startUserbotLogin(opts: {
|
|
83
90
|
apiId: number;
|
|
84
91
|
apiHash: string;
|
|
85
92
|
phone: string;
|
|
93
|
+
/** Инъекция клиента для тестов; по умолчанию — defaultLoginClientFactory. */
|
|
94
|
+
clientFactory?: LoginClientFactory;
|
|
86
95
|
}): Promise<StartedUserbotLogin> {
|
|
87
|
-
const client =
|
|
88
|
-
connectionRetries: 3,
|
|
89
|
-
});
|
|
96
|
+
const client = (opts.clientFactory ?? defaultLoginClientFactory)(opts.apiId, opts.apiHash);
|
|
90
97
|
await client.connect();
|
|
91
98
|
try {
|
|
92
99
|
const { phoneCodeHash } = await client.sendCode(
|