@imessaging/telegram-mtproto 0.8.3 → 0.8.4
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/package.json +2 -2
- package/src/incoming.ts +32 -0
- package/src/telegram-mtproto-transport.ts +142 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imessaging/telegram-mtproto",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Telegram MTProto user-account transport for imessaging",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"messaging",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@imessaging/core": "^0.8.
|
|
34
|
+
"@imessaging/core": "^0.8.4",
|
|
35
35
|
"big-integer": "^1.6.52"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
package/src/incoming.ts
CHANGED
|
@@ -378,6 +378,38 @@ export class IncomingDispatcher {
|
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Раздача СВОИХ исходящих — зеркало IncomingDispatcher: отдаёт только `out: true`, чужое пропускает.
|
|
383
|
+
* Своё ли это отправление транспорта, решает вызывающий: он знает, что сам отправлял.
|
|
384
|
+
*/
|
|
385
|
+
export class OutgoingDispatcher {
|
|
386
|
+
private readonly handlers = new Set<(message: IncomingMessage) => void | Promise<void>>();
|
|
387
|
+
|
|
388
|
+
add(handler: (message: IncomingMessage) => void | Promise<void>): () => void {
|
|
389
|
+
this.handlers.add(handler);
|
|
390
|
+
return () => {
|
|
391
|
+
this.handlers.delete(handler);
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
get size(): number {
|
|
396
|
+
return this.handlers.size;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async dispatch(
|
|
400
|
+
message: MtprotoMessageLike,
|
|
401
|
+
transportId: string,
|
|
402
|
+
accountId: string,
|
|
403
|
+
sentByTransport: boolean,
|
|
404
|
+
): Promise<boolean> {
|
|
405
|
+
if (message.out !== true) return false;
|
|
406
|
+
if (this.handlers.size === 0) return false;
|
|
407
|
+
const outgoing = { ...toIncomingMessage(message, transportId, accountId), sentByTransport };
|
|
408
|
+
for (const handler of this.handlers) await handler(outgoing);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
381
413
|
/** Клиент MTProto в той части, которой достаточно для скачивания. Объявлен ради двойника в тесте. */
|
|
382
414
|
export type DownloadingClient = {
|
|
383
415
|
getMessages(peer: string, params: { ids: number[] }): Promise<{ media?: unknown }[]>;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
DraftTransport,
|
|
3
|
+
MessageDraft,
|
|
4
|
+
OutgoingObserverTransport,
|
|
2
5
|
ButtonPress,
|
|
3
6
|
ButtonPressTransport,
|
|
4
7
|
IncomingAttachment,
|
|
@@ -20,7 +23,7 @@ import type {
|
|
|
20
23
|
TransportStatus,
|
|
21
24
|
TypingTransport,
|
|
22
25
|
} from "@imessaging/core";
|
|
23
|
-
import { isTelegramRecipient, MemoryPeerStore
|
|
26
|
+
import { formatText, formattedOf, isTelegramRecipient, MemoryPeerStore } from "@imessaging/core";
|
|
24
27
|
import { toTelegramEntities } from "./entities";
|
|
25
28
|
import { peerToChat, unmarkPeerId } from "./incoming";
|
|
26
29
|
import bigInt from "big-integer";
|
|
@@ -35,6 +38,7 @@ import { StringSession } from "telegram/sessions";
|
|
|
35
38
|
import {
|
|
36
39
|
downloadFromMessage,
|
|
37
40
|
IncomingDispatcher,
|
|
41
|
+
OutgoingDispatcher,
|
|
38
42
|
toIncomingMessage,
|
|
39
43
|
type DownloadingClient,
|
|
40
44
|
type MtprotoMessageLike,
|
|
@@ -88,12 +92,24 @@ function isChannel(chat: Api.TypeChat): chat is Api.Channel {
|
|
|
88
92
|
return chat.className === "Channel";
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
const OWN_MEMORY_MS = 10 * 60 * 1000;
|
|
96
|
+
|
|
97
|
+
/** Номер чата входного пира в том же маркированном виде, что chatId во входящем (peerToChat). */
|
|
98
|
+
function inputPeerChatId(peer: Api.TypeInputPeer): string {
|
|
99
|
+
if (peer instanceof Api.InputPeerChannel) return peerToChat({ channelId: peer.channelId }).chatId;
|
|
100
|
+
if (peer instanceof Api.InputPeerChat) return peerToChat({ chatId: peer.chatId }).chatId;
|
|
101
|
+
if (peer instanceof Api.InputPeerUser) return peerToChat({ userId: peer.userId }).chatId;
|
|
102
|
+
throw new Error(`Unsupported input peer: ${peer.className}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
export class TelegramMtprotoTransport
|
|
92
106
|
implements
|
|
107
|
+
DraftTransport,
|
|
93
108
|
EditableMessageTransport,
|
|
94
109
|
EditedMessageTransport,
|
|
95
110
|
ButtonPressTransport,
|
|
96
111
|
IncomingMessageTransport,
|
|
112
|
+
OutgoingObserverTransport,
|
|
97
113
|
ReactionTransport,
|
|
98
114
|
ReadReceiptTransport,
|
|
99
115
|
TypingTransport
|
|
@@ -107,6 +123,18 @@ export class TelegramMtprotoTransport
|
|
|
107
123
|
private incomingListener: ((event: unknown) => void) | null = null;
|
|
108
124
|
private readonly edits = new IncomingDispatcher();
|
|
109
125
|
private editListener: ((event: unknown) => void) | null = null;
|
|
126
|
+
private readonly outgoing = new OutgoingDispatcher();
|
|
127
|
+
private outgoingListener: ((event: unknown) => void) | null = null;
|
|
128
|
+
private readonly outgoingEdits = new OutgoingDispatcher();
|
|
129
|
+
private outgoingEditListener: ((event: unknown) => void) | null = null;
|
|
130
|
+
/**
|
|
131
|
+
* Что отправил и поправил сам транспорт: «чат:сообщение» → когда. Своё приходит тем же обновлением
|
|
132
|
+
* out=true, что и отправленное человеком с телефона, и без этой памяти их не различить.
|
|
133
|
+
*/
|
|
134
|
+
private readonly ownSent = new Map<string, number>();
|
|
135
|
+
private readonly ownEdited = new Map<string, number>();
|
|
136
|
+
/** Идущие send/edit: обновление о своём сообщении может прийти раньше, чем вызов вернёт его номер. */
|
|
137
|
+
private readonly ownPending = new Set<Promise<unknown>>();
|
|
110
138
|
/** Отметка жизни подписки: срабатывает на любом обновлении, включая служебные. */
|
|
111
139
|
private activityListener: ((event: unknown) => void) | null = null;
|
|
112
140
|
|
|
@@ -192,6 +220,112 @@ export class TelegramMtprotoTransport
|
|
|
192
220
|
};
|
|
193
221
|
}
|
|
194
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Черновик в поле ввода чата — ответ готовит машина, отправляет человек. Разметка — тем же рендером, что у
|
|
225
|
+
* send (`format`); без него текст уходит как есть. Пустой текст очищает черновик.
|
|
226
|
+
*/
|
|
227
|
+
async saveDraft(draft: MessageDraft): Promise<void> {
|
|
228
|
+
this.assertConnected();
|
|
229
|
+
if (!isTelegramRecipient(draft.recipient)) {
|
|
230
|
+
throw new Error(`Telegram MTProto cannot save a draft for ${draft.recipient.type}`);
|
|
231
|
+
}
|
|
232
|
+
const replyTo =
|
|
233
|
+
draft.replyToMessageId === undefined ? undefined : Number(draft.replyToMessageId);
|
|
234
|
+
if (replyTo !== undefined && (!Number.isSafeInteger(replyTo) || replyTo <= 0)) {
|
|
235
|
+
throw new Error(`Telegram message id must be a positive integer: ${draft.replyToMessageId}`);
|
|
236
|
+
}
|
|
237
|
+
const ft = draft.format ? formatText(draft.text, draft.format) : undefined;
|
|
238
|
+
const save = async (peer: Api.TypeInputPeer) =>
|
|
239
|
+
this.client.invoke(
|
|
240
|
+
new Api.messages.SaveDraft({
|
|
241
|
+
peer,
|
|
242
|
+
message: ft ? ft.text : draft.text,
|
|
243
|
+
...(ft && ft.entities.length > 0 ? { entities: toTelegramEntities(ft) } : {}),
|
|
244
|
+
...(replyTo ? { replyTo: new Api.InputReplyToMessage({ replyToMsgId: replyTo }) } : {}),
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
try {
|
|
248
|
+
await save(await this.getInputPeer(draft.recipient));
|
|
249
|
+
} catch (error: unknown) {
|
|
250
|
+
// Как у send: устаревший пир из хранилища — забыть и разрешить заново, один раз.
|
|
251
|
+
if (!this.isInvalidPeerError(error)) throw error;
|
|
252
|
+
await this.peerStore.delete(this.options.accountId, draft.recipient);
|
|
253
|
+
await save(await this.getInputPeer(draft.recipient));
|
|
254
|
+
}
|
|
255
|
+
this.lastActivityAt = new Date();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Свои новые исходящие — отправленное человеком с другого устройства; отправленное через send помечено sentByTransport. */
|
|
259
|
+
onOutgoing(handler: (message: IncomingMessage) => void | Promise<void>): () => void {
|
|
260
|
+
const remove = this.outgoing.add(handler);
|
|
261
|
+
if (this.outgoingListener === null) {
|
|
262
|
+
this.outgoingListener = (event: unknown): void => {
|
|
263
|
+
const message = (event as { message?: MtprotoMessageLike }).message;
|
|
264
|
+
if (!message) return;
|
|
265
|
+
this.lastActivityAt = new Date();
|
|
266
|
+
void this.dispatchOwn(this.outgoing, this.ownSent, message);
|
|
267
|
+
};
|
|
268
|
+
this.client.addEventHandler(this.outgoingListener, new NewMessage({ outgoing: true }));
|
|
269
|
+
}
|
|
270
|
+
return () => {
|
|
271
|
+
remove();
|
|
272
|
+
if (this.outgoing.size > 0 || this.outgoingListener === null) return;
|
|
273
|
+
this.client.removeEventHandler(this.outgoingListener, new NewMessage({ outgoing: true }));
|
|
274
|
+
this.outgoingListener = null;
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Правки своих сообщений; поправленное через edit помечено sentByTransport. */
|
|
279
|
+
onOutgoingEdit(handler: (message: IncomingMessage) => void | Promise<void>): () => void {
|
|
280
|
+
const remove = this.outgoingEdits.add(handler);
|
|
281
|
+
if (this.outgoingEditListener === null) {
|
|
282
|
+
this.outgoingEditListener = (event: unknown): void => {
|
|
283
|
+
const message = (event as { message?: MtprotoMessageLike }).message;
|
|
284
|
+
if (!message) return;
|
|
285
|
+
this.lastActivityAt = new Date();
|
|
286
|
+
void this.dispatchOwn(this.outgoingEdits, this.ownEdited, message);
|
|
287
|
+
};
|
|
288
|
+
this.client.addEventHandler(this.outgoingEditListener, new EditedMessage({ outgoing: true }));
|
|
289
|
+
}
|
|
290
|
+
return () => {
|
|
291
|
+
remove();
|
|
292
|
+
if (this.outgoingEdits.size > 0 || this.outgoingEditListener === null) return;
|
|
293
|
+
this.client.removeEventHandler(
|
|
294
|
+
this.outgoingEditListener,
|
|
295
|
+
new EditedMessage({ outgoing: true }),
|
|
296
|
+
);
|
|
297
|
+
this.outgoingEditListener = null;
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private async dispatchOwn(
|
|
302
|
+
dispatcher: OutgoingDispatcher,
|
|
303
|
+
own: Map<string, number>,
|
|
304
|
+
message: MtprotoMessageLike,
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
if (message.out !== true || !message.peerId) return;
|
|
307
|
+
// Своё ли это, известно только после того, как идущие send/edit вернули номера.
|
|
308
|
+
await Promise.allSettled(this.ownPending);
|
|
309
|
+
const key = `${peerToChat(message.peerId).chatId}:${message.id}`;
|
|
310
|
+
await dispatcher.dispatch(message, this.id, this.options.accountId, own.has(key));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private trackOwn<T>(work: Promise<T>): Promise<T> {
|
|
314
|
+
this.ownPending.add(work);
|
|
315
|
+
void work.then(
|
|
316
|
+
() => this.ownPending.delete(work),
|
|
317
|
+
() => this.ownPending.delete(work),
|
|
318
|
+
);
|
|
319
|
+
return work;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Номер своего сообщения; память на 10 минут — обновление приходит за секунды, дольше держать незачем. */
|
|
323
|
+
private rememberOwn(own: Map<string, number>, peer: Api.TypeInputPeer, messageId: number): void {
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
for (const [key, at] of own) if (now - at > OWN_MEMORY_MS) own.delete(key);
|
|
326
|
+
own.set(`${inputPeerChatId(peer)}:${messageId}`, now);
|
|
327
|
+
}
|
|
328
|
+
|
|
195
329
|
onEdit(handler: (message: IncomingMessage) => void | Promise<void>): () => void {
|
|
196
330
|
const remove = this.edits.add(handler);
|
|
197
331
|
this.startEditListeningIfNeeded();
|
|
@@ -392,7 +526,7 @@ export class TelegramMtprotoTransport
|
|
|
392
526
|
|
|
393
527
|
async send(message: OutboundMessage): Promise<SendResult> {
|
|
394
528
|
this.assertConnected();
|
|
395
|
-
return this.sendWithRetry(message, false);
|
|
529
|
+
return this.trackOwn(this.sendWithRetry(message, false));
|
|
396
530
|
}
|
|
397
531
|
|
|
398
532
|
/**
|
|
@@ -408,7 +542,7 @@ export class TelegramMtprotoTransport
|
|
|
408
542
|
*/
|
|
409
543
|
async edit(message: EditableMessage): Promise<EditResult> {
|
|
410
544
|
this.assertConnected();
|
|
411
|
-
return this.editWithRetry(message, false);
|
|
545
|
+
return this.trackOwn(this.editWithRetry(message, false));
|
|
412
546
|
}
|
|
413
547
|
|
|
414
548
|
async getStatus(): Promise<TransportStatus> {
|
|
@@ -548,13 +682,15 @@ export class TelegramMtprotoTransport
|
|
|
548
682
|
formattingEntities,
|
|
549
683
|
replyTo: message.replyToMessageId ?? message.threadId,
|
|
550
684
|
});
|
|
685
|
+
this.rememberOwn(this.ownSent, peer, sent.id);
|
|
551
686
|
for (const document of message.documents ?? []) {
|
|
552
687
|
const data = Buffer.from(document.data);
|
|
553
|
-
await this.client.sendFile(peer, {
|
|
688
|
+
const file = await this.client.sendFile(peer, {
|
|
554
689
|
file: new CustomFile(document.filename, data.length, "", data),
|
|
555
690
|
caption: document.caption,
|
|
556
691
|
replyTo: message.threadId,
|
|
557
692
|
});
|
|
693
|
+
this.rememberOwn(this.ownSent, peer, file.id);
|
|
558
694
|
}
|
|
559
695
|
this.lastActivityAt = new Date();
|
|
560
696
|
return {
|
|
@@ -705,6 +841,8 @@ export class TelegramMtprotoTransport
|
|
|
705
841
|
let peer: Api.TypeInputPeer | null = null;
|
|
706
842
|
try {
|
|
707
843
|
peer = await this.getInputPeer(message.recipient);
|
|
844
|
+
// До вызова: обновление о правке может прийти раньше ответа на неё.
|
|
845
|
+
this.rememberOwn(this.ownEdited, peer, messageId);
|
|
708
846
|
const ft = formattedOf(message);
|
|
709
847
|
const text = ft ? ft.text : message.text;
|
|
710
848
|
const parseMode = ft ? undefined : message.parseMode?.toLowerCase();
|