@chatman-media/channel-telegram 1.2.0 → 1.4.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 +33 -0
- package/dist/bot-api/client.d.ts.map +1 -1
- package/dist/bot-api/types.d.ts +1 -0
- package/dist/bot-api/types.d.ts.map +1 -1
- package/dist/index.js +72 -7
- package/package.json +1 -1
- package/src/bot-api/adapter.test.ts +201 -0
- package/src/bot-api/client.test.ts +50 -1
- package/src/bot-api/client.ts +133 -7
- package/src/bot-api/types.ts +1 -0
- package/src/bot-api/update-parser.test.ts +131 -0
- package/src/userbot/adapter.test.ts +24 -0
package/dist/bot-api/client.d.ts
CHANGED
|
@@ -17,7 +17,9 @@ export declare class TelegramClient {
|
|
|
17
17
|
private readonly baseUrl;
|
|
18
18
|
private readonly fetchImpl;
|
|
19
19
|
constructor(opts: TelegramClientOptions);
|
|
20
|
+
private parseResponse;
|
|
20
21
|
private call;
|
|
22
|
+
private callMultipart;
|
|
21
23
|
getMe(): Promise<TgUser>;
|
|
22
24
|
/**
|
|
23
25
|
* Resolve a file_id to a downloadable path. The path returned is
|
|
@@ -66,6 +68,30 @@ export declare class TelegramClient {
|
|
|
66
68
|
videoFileId: string;
|
|
67
69
|
caption?: string;
|
|
68
70
|
}): Promise<TgSendMessageResult>;
|
|
71
|
+
sendVideoNote(input: {
|
|
72
|
+
chatId: number | string;
|
|
73
|
+
videoNoteFileId: string;
|
|
74
|
+
}): Promise<TgSendMessageResult>;
|
|
75
|
+
sendPhotoUpload(input: {
|
|
76
|
+
chatId: number | string;
|
|
77
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
78
|
+
filename: string;
|
|
79
|
+
contentType?: string;
|
|
80
|
+
caption?: string;
|
|
81
|
+
}): Promise<TgSendMessageResult>;
|
|
82
|
+
sendVideoUpload(input: {
|
|
83
|
+
chatId: number | string;
|
|
84
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
85
|
+
filename: string;
|
|
86
|
+
contentType?: string;
|
|
87
|
+
caption?: string;
|
|
88
|
+
}): Promise<TgSendMessageResult>;
|
|
89
|
+
sendVideoNoteUpload(input: {
|
|
90
|
+
chatId: number | string;
|
|
91
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
92
|
+
filename: string;
|
|
93
|
+
contentType?: string;
|
|
94
|
+
}): Promise<TgSendMessageResult>;
|
|
69
95
|
/**
|
|
70
96
|
* Send a video from a local file on disk (not a Telegram `file_id`).
|
|
71
97
|
* Bot API supports this via multipart upload, but our funnel goes through
|
|
@@ -83,6 +109,13 @@ export declare class TelegramClient {
|
|
|
83
109
|
documentFileId: string;
|
|
84
110
|
caption?: string;
|
|
85
111
|
}): Promise<TgSendMessageResult>;
|
|
112
|
+
sendDocumentUpload(input: {
|
|
113
|
+
chatId: number | string;
|
|
114
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
115
|
+
filename: string;
|
|
116
|
+
contentType?: string;
|
|
117
|
+
caption?: string;
|
|
118
|
+
}): Promise<TgSendMessageResult>;
|
|
86
119
|
/**
|
|
87
120
|
* Edit a message we previously sent (commonly the lead card after the
|
|
88
121
|
* operator clicks approve/reject — the card stays in the ops chat with
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/bot-api/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAErF,MAAM,MAAM,SAAS,GAAG,OAAO,KAAK,CAAC;AAErC,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IAEhC,MAAM,EAAE,MAAM;IACd,UAAU,EAAE,MAAM;IAClB,SAAS,EAAE,MAAM,GAAG,SAAS;IAC7B,WAAW,EAAE,MAAM;gBAHnB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,EAAE,MAAM;CAK7B;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/bot-api/client.ts"],"names":[],"mappings":"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"}
|
package/dist/bot-api/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/bot-api/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/bot-api/types.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -32553,6 +32553,13 @@ class TelegramApiError extends Error {
|
|
|
32553
32553
|
this.name = "TelegramApiError";
|
|
32554
32554
|
}
|
|
32555
32555
|
}
|
|
32556
|
+
function toArrayBuffer(bytes) {
|
|
32557
|
+
if (bytes instanceof ArrayBuffer)
|
|
32558
|
+
return bytes;
|
|
32559
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
32560
|
+
copy.set(bytes);
|
|
32561
|
+
return copy.buffer;
|
|
32562
|
+
}
|
|
32556
32563
|
|
|
32557
32564
|
class TelegramClient {
|
|
32558
32565
|
token;
|
|
@@ -32565,13 +32572,7 @@ class TelegramClient {
|
|
|
32565
32572
|
this.baseUrl = opts.baseUrl ?? "https://api.telegram.org";
|
|
32566
32573
|
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
32567
32574
|
}
|
|
32568
|
-
async
|
|
32569
|
-
const url = `${this.baseUrl}/bot${this.token}/${method}`;
|
|
32570
|
-
const res = await this.fetchImpl(url, {
|
|
32571
|
-
method: "POST",
|
|
32572
|
-
headers: { "content-type": "application/json" },
|
|
32573
|
-
body: JSON.stringify(params)
|
|
32574
|
-
});
|
|
32575
|
+
async parseResponse(method, res) {
|
|
32575
32576
|
let body;
|
|
32576
32577
|
try {
|
|
32577
32578
|
body = await res.json();
|
|
@@ -32583,6 +32584,32 @@ class TelegramClient {
|
|
|
32583
32584
|
}
|
|
32584
32585
|
return body.result;
|
|
32585
32586
|
}
|
|
32587
|
+
async call(method, params) {
|
|
32588
|
+
const url = `${this.baseUrl}/bot${this.token}/${method}`;
|
|
32589
|
+
const res = await this.fetchImpl(url, {
|
|
32590
|
+
method: "POST",
|
|
32591
|
+
headers: { "content-type": "application/json" },
|
|
32592
|
+
body: JSON.stringify(params)
|
|
32593
|
+
});
|
|
32594
|
+
return this.parseResponse(method, res);
|
|
32595
|
+
}
|
|
32596
|
+
async callMultipart(method, params, file) {
|
|
32597
|
+
const form = new FormData;
|
|
32598
|
+
for (const [key, value] of Object.entries(params)) {
|
|
32599
|
+
if (value === undefined)
|
|
32600
|
+
continue;
|
|
32601
|
+
form.append(key, String(value));
|
|
32602
|
+
}
|
|
32603
|
+
form.append(file.field, new Blob([toArrayBuffer(file.bytes)], {
|
|
32604
|
+
type: file.contentType ?? "application/octet-stream"
|
|
32605
|
+
}), file.filename);
|
|
32606
|
+
const url = `${this.baseUrl}/bot${this.token}/${method}`;
|
|
32607
|
+
const res = await this.fetchImpl(url, {
|
|
32608
|
+
method: "POST",
|
|
32609
|
+
body: form
|
|
32610
|
+
});
|
|
32611
|
+
return this.parseResponse(method, res);
|
|
32612
|
+
}
|
|
32586
32613
|
getMe() {
|
|
32587
32614
|
return this.call("getMe", {});
|
|
32588
32615
|
}
|
|
@@ -32630,6 +32657,36 @@ class TelegramClient {
|
|
|
32630
32657
|
params.caption = input.caption;
|
|
32631
32658
|
return this.call("sendVideo", params);
|
|
32632
32659
|
}
|
|
32660
|
+
sendVideoNote(input) {
|
|
32661
|
+
return this.call("sendVideoNote", {
|
|
32662
|
+
chat_id: input.chatId,
|
|
32663
|
+
video_note: input.videoNoteFileId
|
|
32664
|
+
});
|
|
32665
|
+
}
|
|
32666
|
+
sendPhotoUpload(input) {
|
|
32667
|
+
return this.callMultipart("sendPhoto", { chat_id: input.chatId, caption: input.caption }, {
|
|
32668
|
+
field: "photo",
|
|
32669
|
+
bytes: input.bytes,
|
|
32670
|
+
filename: input.filename,
|
|
32671
|
+
contentType: input.contentType
|
|
32672
|
+
});
|
|
32673
|
+
}
|
|
32674
|
+
sendVideoUpload(input) {
|
|
32675
|
+
return this.callMultipart("sendVideo", { chat_id: input.chatId, caption: input.caption }, {
|
|
32676
|
+
field: "video",
|
|
32677
|
+
bytes: input.bytes,
|
|
32678
|
+
filename: input.filename,
|
|
32679
|
+
contentType: input.contentType
|
|
32680
|
+
});
|
|
32681
|
+
}
|
|
32682
|
+
sendVideoNoteUpload(input) {
|
|
32683
|
+
return this.callMultipart("sendVideoNote", { chat_id: input.chatId }, {
|
|
32684
|
+
field: "video_note",
|
|
32685
|
+
bytes: input.bytes,
|
|
32686
|
+
filename: input.filename,
|
|
32687
|
+
contentType: input.contentType
|
|
32688
|
+
});
|
|
32689
|
+
}
|
|
32633
32690
|
sendLocalVideo(_input) {
|
|
32634
32691
|
throw new Error("sendLocalVideo: Bot API path not implemented — userbot-only feature");
|
|
32635
32692
|
}
|
|
@@ -32642,6 +32699,14 @@ class TelegramClient {
|
|
|
32642
32699
|
params.caption = input.caption;
|
|
32643
32700
|
return this.call("sendDocument", params);
|
|
32644
32701
|
}
|
|
32702
|
+
sendDocumentUpload(input) {
|
|
32703
|
+
return this.callMultipart("sendDocument", { chat_id: input.chatId, caption: input.caption }, {
|
|
32704
|
+
field: "document",
|
|
32705
|
+
bytes: input.bytes,
|
|
32706
|
+
filename: input.filename,
|
|
32707
|
+
contentType: input.contentType
|
|
32708
|
+
});
|
|
32709
|
+
}
|
|
32645
32710
|
editMessageText(input) {
|
|
32646
32711
|
const params = {
|
|
32647
32712
|
chat_id: input.chatId,
|
package/package.json
CHANGED
|
@@ -81,6 +81,207 @@ describe("TelegramBotAdapter", () => {
|
|
|
81
81
|
expect(last.done).toBe(true);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
it("rawClient отдаёт нижележащий TelegramClient", () => {
|
|
85
|
+
const { fetch } = fakeFetch();
|
|
86
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
87
|
+
expect(adapter.rawClient).toBeDefined();
|
|
88
|
+
expect(typeof adapter.rawClient.getMe).toBe("function");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("send: пустые parts → throws", async () => {
|
|
92
|
+
const { fetch } = fakeFetch();
|
|
93
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
94
|
+
await expect(
|
|
95
|
+
adapter.send({ channelId: "tg1", externalUserId: "1", parts: [] }),
|
|
96
|
+
).rejects.toThrow(/non-empty/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("send: нечисловой externalUserId → throws", async () => {
|
|
100
|
+
const { fetch } = fakeFetch();
|
|
101
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
102
|
+
await expect(
|
|
103
|
+
adapter.send({
|
|
104
|
+
channelId: "tg1",
|
|
105
|
+
externalUserId: "not-a-number",
|
|
106
|
+
parts: [{ kind: "text", text: "x" }],
|
|
107
|
+
}),
|
|
108
|
+
).rejects.toThrow(/invalid externalUserId/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("send photo/video/document parts → sendPhoto/sendVideo/sendDocument с caption", async () => {
|
|
112
|
+
const { fetch, calls } = fakeFetch();
|
|
113
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
114
|
+
await adapter.send({
|
|
115
|
+
channelId: "tg1",
|
|
116
|
+
externalUserId: "5",
|
|
117
|
+
parts: [
|
|
118
|
+
{ kind: "photo", mediaRef: { channelId: "tg1", externalRef: "PH" }, caption: "pic" },
|
|
119
|
+
{ kind: "video", mediaRef: { channelId: "tg1", externalRef: "VID" }, caption: "vid" },
|
|
120
|
+
{ kind: "document", mediaRef: { channelId: "tg1", externalRef: "DOC" }, caption: "doc" },
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
expect(calls.map((c) => c.url.split("/").pop())).toEqual([
|
|
124
|
+
"sendPhoto",
|
|
125
|
+
"sendVideo",
|
|
126
|
+
"sendDocument",
|
|
127
|
+
]);
|
|
128
|
+
expect(calls[0]?.body).toEqual({ chat_id: 5, photo: "PH", caption: "pic" });
|
|
129
|
+
expect(calls[1]?.body).toEqual({ chat_id: 5, video: "VID", caption: "vid" });
|
|
130
|
+
expect(calls[2]?.body).toEqual({ chat_id: 5, document: "DOC", caption: "doc" });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("send media-parts без caption → нет caption в body", async () => {
|
|
134
|
+
const { fetch, calls } = fakeFetch();
|
|
135
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
136
|
+
await adapter.send({
|
|
137
|
+
channelId: "tg1",
|
|
138
|
+
externalUserId: "5",
|
|
139
|
+
parts: [{ kind: "photo", mediaRef: { channelId: "tg1", externalRef: "PH" } }],
|
|
140
|
+
});
|
|
141
|
+
expect(calls[0]?.body).toEqual({ chat_id: 5, photo: "PH" });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("send: parseMode markdown→MarkdownV2, html→HTML", async () => {
|
|
145
|
+
const { fetch, calls } = fakeFetch();
|
|
146
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
147
|
+
await adapter.send({
|
|
148
|
+
channelId: "tg1",
|
|
149
|
+
externalUserId: "5",
|
|
150
|
+
parts: [
|
|
151
|
+
{ kind: "text", text: "md", parseMode: "markdown" },
|
|
152
|
+
{ kind: "text", text: "ht", parseMode: "html" },
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
expect(calls[0]?.body).toMatchObject({ parse_mode: "MarkdownV2" });
|
|
156
|
+
expect(calls[1]?.body).toMatchObject({ parse_mode: "HTML" });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("send: replyToExternalMessageId уходит как reply_to_message_id только на первой части", async () => {
|
|
160
|
+
const { fetch, calls } = fakeFetch();
|
|
161
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
162
|
+
await adapter.send({
|
|
163
|
+
channelId: "tg1",
|
|
164
|
+
externalUserId: "5",
|
|
165
|
+
parts: [
|
|
166
|
+
{ kind: "text", text: "one" },
|
|
167
|
+
{ kind: "text", text: "two" },
|
|
168
|
+
],
|
|
169
|
+
replyToExternalMessageId: "77",
|
|
170
|
+
});
|
|
171
|
+
expect(calls[0]?.body).toMatchObject({ reply_to_message_id: 77 });
|
|
172
|
+
expect(calls[1]?.body).not.toHaveProperty("reply_to_message_id");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("edit: маппит в editMessageText с parse_mode и reply_markup", async () => {
|
|
176
|
+
const { fetch, calls } = fakeFetch();
|
|
177
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
178
|
+
await adapter.edit({
|
|
179
|
+
channelId: "tg1",
|
|
180
|
+
externalUserId: "10",
|
|
181
|
+
externalMessageId: "20",
|
|
182
|
+
text: "edited",
|
|
183
|
+
parseMode: "html",
|
|
184
|
+
replyMarkup: { inlineButtons: [[{ label: "Ok", callbackData: "ok" }]] },
|
|
185
|
+
});
|
|
186
|
+
expect(calls[0]?.url).toBe("https://api.telegram.org/botTKN/editMessageText");
|
|
187
|
+
expect(calls[0]?.body).toEqual({
|
|
188
|
+
chat_id: 10,
|
|
189
|
+
message_id: 20,
|
|
190
|
+
text: "edited",
|
|
191
|
+
parse_mode: "HTML",
|
|
192
|
+
reply_markup: { inline_keyboard: [[{ text: "Ok", callback_data: "ok" }]] },
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("edit: нечисловые id → throws", async () => {
|
|
197
|
+
const { fetch } = fakeFetch();
|
|
198
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
199
|
+
await expect(
|
|
200
|
+
adapter.edit({
|
|
201
|
+
channelId: "tg1",
|
|
202
|
+
externalUserId: "abc",
|
|
203
|
+
externalMessageId: "20",
|
|
204
|
+
text: "x",
|
|
205
|
+
}),
|
|
206
|
+
).rejects.toThrow(/edit requires numeric/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("delete: маппит в deleteMessage; нечисловые id → throws", async () => {
|
|
210
|
+
const { fetch, calls } = fakeFetch();
|
|
211
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
212
|
+
await adapter.delete({ channelId: "tg1", externalUserId: "10", externalMessageId: "20" });
|
|
213
|
+
expect(calls[0]?.url).toBe("https://api.telegram.org/botTKN/deleteMessage");
|
|
214
|
+
expect(calls[0]?.body).toEqual({ chat_id: 10, message_id: 20 });
|
|
215
|
+
await expect(
|
|
216
|
+
adapter.delete({ channelId: "tg1", externalUserId: "10", externalMessageId: "nope" }),
|
|
217
|
+
).rejects.toThrow(/delete requires numeric/);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("downloadMedia: getFile → скачивание байтов по file_path", async () => {
|
|
221
|
+
const urls: string[] = [];
|
|
222
|
+
const fn = async (url: string | URL | Request): Promise<Response> => {
|
|
223
|
+
urls.push(String(url));
|
|
224
|
+
if (String(url).endsWith("/getFile")) {
|
|
225
|
+
return new Response(
|
|
226
|
+
JSON.stringify({
|
|
227
|
+
ok: true,
|
|
228
|
+
result: { file_id: "F", file_unique_id: "U", file_path: "photos/x.jpg" },
|
|
229
|
+
}),
|
|
230
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return new Response("BYTES", { status: 200 });
|
|
234
|
+
};
|
|
235
|
+
const adapter = new TelegramBotAdapter({
|
|
236
|
+
id: "tg1",
|
|
237
|
+
token: "TKN",
|
|
238
|
+
fetch: fn as unknown as typeof fetch,
|
|
239
|
+
});
|
|
240
|
+
const res = await adapter.downloadMedia({ channelId: "tg1", externalRef: "F" });
|
|
241
|
+
expect(await res.text()).toBe("BYTES");
|
|
242
|
+
expect(urls[0]).toBe("https://api.telegram.org/botTKN/getFile");
|
|
243
|
+
expect(urls[1]).toBe("https://api.telegram.org/file/botTKN/photos/x.jpg");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("signalTyping: валидный id → sendChatAction; NaN → no-op", async () => {
|
|
247
|
+
const { fetch, calls } = fakeFetch();
|
|
248
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
249
|
+
await adapter.signalTyping("42");
|
|
250
|
+
expect(calls[0]?.url).toBe("https://api.telegram.org/botTKN/sendChatAction");
|
|
251
|
+
expect(calls[0]?.body).toEqual({ chat_id: 42, action: "typing" });
|
|
252
|
+
await adapter.signalTyping("not-a-number");
|
|
253
|
+
expect(calls).toHaveLength(1); // второй вызов не дошёл до API
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("pushUpdate с нерелевантным update → silent drop", async () => {
|
|
257
|
+
const { fetch } = fakeFetch();
|
|
258
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
259
|
+
adapter.pushUpdate({ update_id: 99 });
|
|
260
|
+
const iter = adapter.receive()[Symbol.asyncIterator]();
|
|
261
|
+
const pending = iter.next();
|
|
262
|
+
adapter.close();
|
|
263
|
+
expect((await pending).done).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("receive: уже aborted signal → done сразу", async () => {
|
|
267
|
+
const { fetch } = fakeFetch();
|
|
268
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
269
|
+
const ctrl = new AbortController();
|
|
270
|
+
ctrl.abort();
|
|
271
|
+
const iter = adapter.receive(ctrl.signal)[Symbol.asyncIterator]();
|
|
272
|
+
expect((await iter.next()).done).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("receive: abort во время ожидания → done и waiter убран", async () => {
|
|
276
|
+
const { fetch } = fakeFetch();
|
|
277
|
+
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
278
|
+
const ctrl = new AbortController();
|
|
279
|
+
const iter = adapter.receive(ctrl.signal)[Symbol.asyncIterator]();
|
|
280
|
+
const pending = iter.next();
|
|
281
|
+
ctrl.abort();
|
|
282
|
+
expect((await pending).done).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
|
|
84
285
|
it("receive() ждёт pushUpdate если очередь пуста (resolver-pattern)", async () => {
|
|
85
286
|
const { fetch } = fakeFetch();
|
|
86
287
|
const adapter = new TelegramBotAdapter({ id: "tg1", token: "TKN", fetch });
|
|
@@ -50,11 +50,60 @@ describe("TelegramClient методы", () => {
|
|
|
50
50
|
const c = client(fn);
|
|
51
51
|
await c.sendPhoto({ chatId: 1, photoFileId: "p", caption: "c" });
|
|
52
52
|
await c.sendVideo({ chatId: 1, videoFileId: "v" });
|
|
53
|
+
await c.sendVideoNote({ chatId: 1, videoNoteFileId: "vn" });
|
|
53
54
|
await c.sendDocument({ chatId: 1, documentFileId: "d", caption: "cap" });
|
|
54
55
|
expect(calls[0]!.url).toContain("/sendPhoto");
|
|
55
56
|
expect(bodyOf(calls[0]!.init).photo).toBe("p");
|
|
56
57
|
expect(calls[1]!.url).toContain("/sendVideo");
|
|
57
|
-
expect(
|
|
58
|
+
expect(calls[2]!.url).toContain("/sendVideoNote");
|
|
59
|
+
expect(bodyOf(calls[2]!.init).video_note).toBe("vn");
|
|
60
|
+
expect(bodyOf(calls[3]!.init).document).toBe("d");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("send media upload methods use multipart form data", async () => {
|
|
64
|
+
const { fn, calls } = mock(() => envelope({ message_id: 1 }));
|
|
65
|
+
const c = client(fn);
|
|
66
|
+
await c.sendPhotoUpload({
|
|
67
|
+
chatId: "ops",
|
|
68
|
+
bytes: new TextEncoder().encode("IMG"),
|
|
69
|
+
filename: "kyc.jpg",
|
|
70
|
+
contentType: "image/jpeg",
|
|
71
|
+
caption: "passport",
|
|
72
|
+
});
|
|
73
|
+
await c.sendVideoUpload({
|
|
74
|
+
chatId: "ops",
|
|
75
|
+
bytes: new TextEncoder().encode("VID"),
|
|
76
|
+
filename: "kyc.mp4",
|
|
77
|
+
contentType: "video/mp4",
|
|
78
|
+
});
|
|
79
|
+
await c.sendVideoNoteUpload({
|
|
80
|
+
chatId: "ops",
|
|
81
|
+
bytes: new TextEncoder().encode("CIRCLE"),
|
|
82
|
+
filename: "circle.mp4",
|
|
83
|
+
contentType: "video/mp4",
|
|
84
|
+
});
|
|
85
|
+
await c.sendDocumentUpload({
|
|
86
|
+
chatId: "ops",
|
|
87
|
+
bytes: new TextEncoder().encode("DOC"),
|
|
88
|
+
filename: "statement.pdf",
|
|
89
|
+
contentType: "application/pdf",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const photoForm = calls[0]!.init!.body as FormData;
|
|
93
|
+
expect(calls[0]!.url).toContain("/sendPhoto");
|
|
94
|
+
expect(photoForm.get("chat_id")).toBe("ops");
|
|
95
|
+
expect(photoForm.get("caption")).toBe("passport");
|
|
96
|
+
const photo = photoForm.get("photo") as File;
|
|
97
|
+
expect(photo.name).toBe("kyc.jpg");
|
|
98
|
+
expect(photo.type).toBe("image/jpeg");
|
|
99
|
+
expect(await photo.text()).toBe("IMG");
|
|
100
|
+
|
|
101
|
+
expect(calls[1]!.url).toContain("/sendVideo");
|
|
102
|
+
expect((calls[1]!.init!.body as FormData).get("video")).toBeInstanceOf(File);
|
|
103
|
+
expect(calls[2]!.url).toContain("/sendVideoNote");
|
|
104
|
+
expect((calls[2]!.init!.body as FormData).get("video_note")).toBeInstanceOf(File);
|
|
105
|
+
expect(calls[3]!.url).toContain("/sendDocument");
|
|
106
|
+
expect((calls[3]!.init!.body as FormData).get("document")).toBeInstanceOf(File);
|
|
58
107
|
});
|
|
59
108
|
|
|
60
109
|
it("sendLocalVideo — не поддержано (throws)", () => {
|
package/src/bot-api/client.ts
CHANGED
|
@@ -27,6 +27,13 @@ interface TgResponse<T> {
|
|
|
27
27
|
description?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function toArrayBuffer(bytes: ArrayBuffer | Uint8Array): ArrayBuffer {
|
|
31
|
+
if (bytes instanceof ArrayBuffer) return bytes;
|
|
32
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
33
|
+
copy.set(bytes);
|
|
34
|
+
return copy.buffer;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
export class TelegramClient {
|
|
31
38
|
private readonly token: string;
|
|
32
39
|
private readonly baseUrl: string;
|
|
@@ -39,13 +46,7 @@ export class TelegramClient {
|
|
|
39
46
|
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
private async
|
|
43
|
-
const url = `${this.baseUrl}/bot${this.token}/${method}`;
|
|
44
|
-
const res = await this.fetchImpl(url, {
|
|
45
|
-
method: "POST",
|
|
46
|
-
headers: { "content-type": "application/json" },
|
|
47
|
-
body: JSON.stringify(params),
|
|
48
|
-
});
|
|
49
|
+
private async parseResponse<T>(method: string, res: Response): Promise<T> {
|
|
49
50
|
let body: TgResponse<T>;
|
|
50
51
|
try {
|
|
51
52
|
body = (await res.json()) as TgResponse<T>;
|
|
@@ -68,6 +69,46 @@ export class TelegramClient {
|
|
|
68
69
|
return body.result;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
private async call<T>(method: string, params: Record<string, unknown>): Promise<T> {
|
|
73
|
+
const url = `${this.baseUrl}/bot${this.token}/${method}`;
|
|
74
|
+
const res = await this.fetchImpl(url, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "content-type": "application/json" },
|
|
77
|
+
body: JSON.stringify(params),
|
|
78
|
+
});
|
|
79
|
+
return this.parseResponse<T>(method, res);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async callMultipart<T>(
|
|
83
|
+
method: string,
|
|
84
|
+
params: Record<string, unknown>,
|
|
85
|
+
file: {
|
|
86
|
+
field: string;
|
|
87
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
88
|
+
filename: string;
|
|
89
|
+
contentType?: string;
|
|
90
|
+
},
|
|
91
|
+
): Promise<T> {
|
|
92
|
+
const form = new FormData();
|
|
93
|
+
for (const [key, value] of Object.entries(params)) {
|
|
94
|
+
if (value === undefined) continue;
|
|
95
|
+
form.append(key, String(value));
|
|
96
|
+
}
|
|
97
|
+
form.append(
|
|
98
|
+
file.field,
|
|
99
|
+
new Blob([toArrayBuffer(file.bytes)], {
|
|
100
|
+
type: file.contentType ?? "application/octet-stream",
|
|
101
|
+
}),
|
|
102
|
+
file.filename,
|
|
103
|
+
);
|
|
104
|
+
const url = `${this.baseUrl}/bot${this.token}/${method}`;
|
|
105
|
+
const res = await this.fetchImpl(url, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
body: form,
|
|
108
|
+
});
|
|
109
|
+
return this.parseResponse<T>(method, res);
|
|
110
|
+
}
|
|
111
|
+
|
|
71
112
|
getMe(): Promise<TgUser> {
|
|
72
113
|
return this.call<TgUser>("getMe", {});
|
|
73
114
|
}
|
|
@@ -157,6 +198,72 @@ export class TelegramClient {
|
|
|
157
198
|
return this.call<TgSendMessageResult>("sendVideo", params);
|
|
158
199
|
}
|
|
159
200
|
|
|
201
|
+
sendVideoNote(input: {
|
|
202
|
+
chatId: number | string;
|
|
203
|
+
videoNoteFileId: string;
|
|
204
|
+
}): Promise<TgSendMessageResult> {
|
|
205
|
+
return this.call<TgSendMessageResult>("sendVideoNote", {
|
|
206
|
+
chat_id: input.chatId,
|
|
207
|
+
video_note: input.videoNoteFileId,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
sendPhotoUpload(input: {
|
|
212
|
+
chatId: number | string;
|
|
213
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
214
|
+
filename: string;
|
|
215
|
+
contentType?: string;
|
|
216
|
+
caption?: string;
|
|
217
|
+
}): Promise<TgSendMessageResult> {
|
|
218
|
+
return this.callMultipart<TgSendMessageResult>(
|
|
219
|
+
"sendPhoto",
|
|
220
|
+
{ chat_id: input.chatId, caption: input.caption },
|
|
221
|
+
{
|
|
222
|
+
field: "photo",
|
|
223
|
+
bytes: input.bytes,
|
|
224
|
+
filename: input.filename,
|
|
225
|
+
contentType: input.contentType,
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
sendVideoUpload(input: {
|
|
231
|
+
chatId: number | string;
|
|
232
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
233
|
+
filename: string;
|
|
234
|
+
contentType?: string;
|
|
235
|
+
caption?: string;
|
|
236
|
+
}): Promise<TgSendMessageResult> {
|
|
237
|
+
return this.callMultipart<TgSendMessageResult>(
|
|
238
|
+
"sendVideo",
|
|
239
|
+
{ chat_id: input.chatId, caption: input.caption },
|
|
240
|
+
{
|
|
241
|
+
field: "video",
|
|
242
|
+
bytes: input.bytes,
|
|
243
|
+
filename: input.filename,
|
|
244
|
+
contentType: input.contentType,
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
sendVideoNoteUpload(input: {
|
|
250
|
+
chatId: number | string;
|
|
251
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
252
|
+
filename: string;
|
|
253
|
+
contentType?: string;
|
|
254
|
+
}): Promise<TgSendMessageResult> {
|
|
255
|
+
return this.callMultipart<TgSendMessageResult>(
|
|
256
|
+
"sendVideoNote",
|
|
257
|
+
{ chat_id: input.chatId },
|
|
258
|
+
{
|
|
259
|
+
field: "video_note",
|
|
260
|
+
bytes: input.bytes,
|
|
261
|
+
filename: input.filename,
|
|
262
|
+
contentType: input.contentType,
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
160
267
|
/**
|
|
161
268
|
* Send a video from a local file on disk (not a Telegram `file_id`).
|
|
162
269
|
* Bot API supports this via multipart upload, but our funnel goes through
|
|
@@ -185,6 +292,25 @@ export class TelegramClient {
|
|
|
185
292
|
return this.call<TgSendMessageResult>("sendDocument", params);
|
|
186
293
|
}
|
|
187
294
|
|
|
295
|
+
sendDocumentUpload(input: {
|
|
296
|
+
chatId: number | string;
|
|
297
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
298
|
+
filename: string;
|
|
299
|
+
contentType?: string;
|
|
300
|
+
caption?: string;
|
|
301
|
+
}): Promise<TgSendMessageResult> {
|
|
302
|
+
return this.callMultipart<TgSendMessageResult>(
|
|
303
|
+
"sendDocument",
|
|
304
|
+
{ chat_id: input.chatId, caption: input.caption },
|
|
305
|
+
{
|
|
306
|
+
field: "document",
|
|
307
|
+
bytes: input.bytes,
|
|
308
|
+
filename: input.filename,
|
|
309
|
+
contentType: input.contentType,
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
188
314
|
/**
|
|
189
315
|
* Edit a message we previously sent (commonly the lead card after the
|
|
190
316
|
* operator clicks approve/reject — the card stays in the ops chat with
|
package/src/bot-api/types.ts
CHANGED
|
@@ -123,6 +123,137 @@ describe("parseUpdate", () => {
|
|
|
123
123
|
expect(inbound?.externalUserId).toBe("500");
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
it("парсит video с caption", () => {
|
|
127
|
+
const update: TgUpdate = {
|
|
128
|
+
update_id: 10,
|
|
129
|
+
message: {
|
|
130
|
+
message_id: 11,
|
|
131
|
+
chat: { id: 600, type: "private" },
|
|
132
|
+
from: { id: 600 },
|
|
133
|
+
date: 1700000000,
|
|
134
|
+
caption: "watch",
|
|
135
|
+
video: {
|
|
136
|
+
file_id: "vid1",
|
|
137
|
+
file_unique_id: "vu1",
|
|
138
|
+
width: 640,
|
|
139
|
+
height: 480,
|
|
140
|
+
duration: 30,
|
|
141
|
+
mime_type: "video/mp4",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
const inbound = parseUpdate(CH, update);
|
|
146
|
+
expect(inbound?.parts).toEqual([
|
|
147
|
+
{
|
|
148
|
+
kind: "video",
|
|
149
|
+
mediaRef: { channelId: CH, externalRef: "vid1" },
|
|
150
|
+
caption: "watch",
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("парсит video без caption — нет caption-поля", () => {
|
|
156
|
+
const update: TgUpdate = {
|
|
157
|
+
update_id: 11,
|
|
158
|
+
message: {
|
|
159
|
+
message_id: 12,
|
|
160
|
+
chat: { id: 600, type: "private" },
|
|
161
|
+
from: { id: 600 },
|
|
162
|
+
date: 1700000000,
|
|
163
|
+
video: { file_id: "vid2", file_unique_id: "vu2", width: 1, height: 1, duration: 2 },
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
expect(parseUpdate(CH, update)?.parts).toEqual([
|
|
167
|
+
{ kind: "video", mediaRef: { channelId: CH, externalRef: "vid2" } },
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("парсит video_note («кружок») с durationSec", () => {
|
|
172
|
+
const update: TgUpdate = {
|
|
173
|
+
update_id: 12,
|
|
174
|
+
message: {
|
|
175
|
+
message_id: 13,
|
|
176
|
+
chat: { id: 700, type: "private" },
|
|
177
|
+
from: { id: 700 },
|
|
178
|
+
date: 1700000000,
|
|
179
|
+
video_note: { file_id: "vn1", file_unique_id: "vnu1", length: 240, duration: 14 },
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
expect(parseUpdate(CH, update)?.parts).toEqual([
|
|
183
|
+
{
|
|
184
|
+
kind: "video_note",
|
|
185
|
+
mediaRef: { channelId: CH, externalRef: "vn1" },
|
|
186
|
+
durationSec: 14,
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("парсит video_note с duration=0 — durationSec опущен", () => {
|
|
192
|
+
const update: TgUpdate = {
|
|
193
|
+
update_id: 13,
|
|
194
|
+
message: {
|
|
195
|
+
message_id: 14,
|
|
196
|
+
chat: { id: 700, type: "private" },
|
|
197
|
+
from: { id: 700 },
|
|
198
|
+
date: 1700000000,
|
|
199
|
+
video_note: { file_id: "vn2", file_unique_id: "vnu2", length: 240, duration: 0 },
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
expect(parseUpdate(CH, update)?.parts).toEqual([
|
|
203
|
+
{ kind: "video_note", mediaRef: { channelId: CH, externalRef: "vn2" } },
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("caption без media и без text → трактуется как text", () => {
|
|
208
|
+
const update: TgUpdate = {
|
|
209
|
+
update_id: 14,
|
|
210
|
+
message: {
|
|
211
|
+
message_id: 15,
|
|
212
|
+
chat: { id: 800, type: "private" },
|
|
213
|
+
from: { id: 800 },
|
|
214
|
+
date: 1700000000,
|
|
215
|
+
caption: "stray caption",
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
expect(parseUpdate(CH, update)?.parts).toEqual([{ kind: "text", text: "stray caption" }]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("edited_message парсится как обычное сообщение", () => {
|
|
222
|
+
const update: TgUpdate = {
|
|
223
|
+
update_id: 15,
|
|
224
|
+
edited_message: {
|
|
225
|
+
message_id: 16,
|
|
226
|
+
chat: { id: 900, type: "private" },
|
|
227
|
+
from: { id: 900 },
|
|
228
|
+
date: 1700000001,
|
|
229
|
+
text: "fixed typo",
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
const inbound = parseUpdate(CH, update);
|
|
233
|
+
expect(inbound?.externalMessageId).toBe("16");
|
|
234
|
+
expect(inbound?.parts).toEqual([{ kind: "text", text: "fixed typo" }]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("callback_query без data → null", () => {
|
|
238
|
+
const update: TgUpdate = {
|
|
239
|
+
update_id: 16,
|
|
240
|
+
callback_query: {
|
|
241
|
+
id: "cb-1",
|
|
242
|
+
from: { id: 1 },
|
|
243
|
+
message: { message_id: 5, chat: { id: 1, type: "private" }, date: 0 },
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
expect(parseUpdate(CH, update)).toBeNull();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("callback_query без message → null", () => {
|
|
250
|
+
const update: TgUpdate = {
|
|
251
|
+
update_id: 17,
|
|
252
|
+
callback_query: { id: "cb-2", from: { id: 1 }, data: "x" },
|
|
253
|
+
};
|
|
254
|
+
expect(parseUpdate(CH, update)).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
126
257
|
it("возвращает null для пустого update без message/callback", () => {
|
|
127
258
|
expect(parseUpdate(CH, { update_id: 6 })).toBeNull();
|
|
128
259
|
});
|
|
@@ -242,6 +242,30 @@ describe("connect() (через clientFactory)", () => {
|
|
|
242
242
|
await expect(a.connect()).rejects.toThrow("connect failed");
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
+
it("дефолтная фабрика (без clientFactory) строит реальный gramjs-клиент: патченный connect → auth-throw", async () => {
|
|
246
|
+
// defaultGramjsClientFactory — production-путь. Сам конструктор gramjs
|
|
247
|
+
// оффлайновый; сетевой connect() патчим на прототипе, чтобы покрыть
|
|
248
|
+
// фабрику без живого MTProto.
|
|
249
|
+
const { TelegramClient: RealGramjs } = await import("telegram");
|
|
250
|
+
const orig = RealGramjs.prototype.connect;
|
|
251
|
+
RealGramjs.prototype.connect = async function patched() {
|
|
252
|
+
throw new Error("AUTH_KEY_DUPLICATED");
|
|
253
|
+
};
|
|
254
|
+
try {
|
|
255
|
+
const a = new TelegramUserbotAdapter({
|
|
256
|
+
id: "ub1",
|
|
257
|
+
apiId: 1,
|
|
258
|
+
apiHash: "h",
|
|
259
|
+
sessionString: "",
|
|
260
|
+
connectionRetries: 1,
|
|
261
|
+
retryDelayMs: 0,
|
|
262
|
+
});
|
|
263
|
+
await expect(a.connect()).rejects.toThrow("auth key revoked");
|
|
264
|
+
} finally {
|
|
265
|
+
RealGramjs.prototype.connect = orig;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
245
269
|
it("идемпотентность: уже connected → фабрика не зовётся", async () => {
|
|
246
270
|
const a = new TelegramUserbotAdapter({
|
|
247
271
|
id: "ub1", apiId: 1, apiHash: "h", sessionString: "",
|