@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.
@@ -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":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/bot-api/client.ts"],"names":[],"mappings":"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"}
@@ -90,6 +90,7 @@ export interface TgMessage {
90
90
  * cards (matched by the parent's `message_id` against
91
91
  * `leads.ops_message_id`). */
92
92
  reply_to_message?: TgMessage;
93
+ reply_markup?: TgReplyMarkup;
93
94
  }
94
95
  export interface TgCallbackQuery {
95
96
  id: string;
@@ -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 call(method, params) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatman-media/channel-telegram",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Telegram-каналы (BotAPI + MTProto userbot) как реализация ChannelAdapter из @chatman-media/channel-core.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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(bodyOf(calls[2]!.init).document).toBe("d");
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)", () => {
@@ -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 call<T>(method: string, params: Record<string, unknown>): Promise<T> {
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
@@ -100,6 +100,7 @@ export interface TgMessage {
100
100
  * cards (matched by the parent's `message_id` against
101
101
  * `leads.ops_message_id`). */
102
102
  reply_to_message?: TgMessage;
103
+ reply_markup?: TgReplyMarkup;
103
104
  }
104
105
 
105
106
  export interface TgCallbackQuery {
@@ -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: "",