@imessaging/core 0.8.2 → 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 +1 -1
- package/src/index.ts +7 -0
- package/src/markup.ts +50 -1
- package/src/types.ts +62 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -18,6 +18,9 @@ export {
|
|
|
18
18
|
canMarkRead,
|
|
19
19
|
canReact,
|
|
20
20
|
canReceive,
|
|
21
|
+
canObserveOutgoing,
|
|
22
|
+
canReceiveEdits,
|
|
23
|
+
canSaveDraft,
|
|
21
24
|
canShowTyping,
|
|
22
25
|
hasCommandMenu,
|
|
23
26
|
isTelegramRecipient,
|
|
@@ -36,6 +39,10 @@ export type {
|
|
|
36
39
|
export type {
|
|
37
40
|
EditableMessage,
|
|
38
41
|
EditableMessageTransport,
|
|
42
|
+
DraftTransport,
|
|
43
|
+
EditedMessageTransport,
|
|
44
|
+
MessageDraft,
|
|
45
|
+
OutgoingObserverTransport,
|
|
39
46
|
EditResult,
|
|
40
47
|
EmailRecipient,
|
|
41
48
|
HeadhunterRecipient,
|
package/src/markup.ts
CHANGED
|
@@ -13,7 +13,20 @@
|
|
|
13
13
|
/** Как написан исходный текст сообщения. */
|
|
14
14
|
export type TextFormat = "plain" | "markdown" | "html";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Общая модель для всех каналов: Telegram и MAX отдают выделения со смещениями, WhatsApp — звёздочки в тексте,
|
|
18
|
+
* почта — HTML; транспорт переводит своё в эти типы, а для чего пары нет (упоминания, хэштеги) — отбрасывает.
|
|
19
|
+
*/
|
|
20
|
+
export type TextEntityType =
|
|
21
|
+
| "bold"
|
|
22
|
+
| "italic"
|
|
23
|
+
| "underline"
|
|
24
|
+
| "strike"
|
|
25
|
+
| "spoiler"
|
|
26
|
+
| "code"
|
|
27
|
+
| "pre"
|
|
28
|
+
| "link"
|
|
29
|
+
| "quote";
|
|
17
30
|
|
|
18
31
|
export type TextEntity = {
|
|
19
32
|
type: TextEntityType;
|
|
@@ -96,6 +109,14 @@ function parseInline(src: string, out: Builder): void {
|
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
const pair = src.slice(i, i + 2);
|
|
112
|
+
if (pair === "||" && !isSpace(src[i + 2])) {
|
|
113
|
+
const end = findCloser(src, i + 2, "||", (at) => at > i + 2 && !isSpace(src[at - 1]));
|
|
114
|
+
if (end !== -1) {
|
|
115
|
+
wrap(out, "spoiler", () => parseInline(src.slice(i + 2, end), out));
|
|
116
|
+
i = end + 2;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
99
120
|
if (pair === "**" || pair === "__" || pair === "~~") {
|
|
100
121
|
// `__` — жирный только на границе слова: `snake__case` остаётся как есть.
|
|
101
122
|
const bounded = pair === "__";
|
|
@@ -166,8 +187,27 @@ export function parseMarkdown(source: string): FormattedText {
|
|
|
166
187
|
|
|
167
188
|
function parseLines(block: string, out: Builder): void {
|
|
168
189
|
const lines = block.split("\n");
|
|
190
|
+
// Подряд идущие строки `> …` — одна цитата, как её показывает любой мессенджер.
|
|
191
|
+
let quote: { offset: number; at: number } | undefined;
|
|
192
|
+
const closeQuote = (end: number) => {
|
|
193
|
+
if (quote && end > quote.offset) {
|
|
194
|
+
out.entities.splice(quote.at, 0, {
|
|
195
|
+
type: "quote",
|
|
196
|
+
offset: quote.offset,
|
|
197
|
+
length: end - quote.offset,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
quote = undefined;
|
|
201
|
+
};
|
|
169
202
|
lines.forEach((line, n) => {
|
|
203
|
+
const quoted = /^>\s?(.*)$/.exec(line);
|
|
204
|
+
if (!quoted) closeQuote(out.text.length);
|
|
170
205
|
if (n > 0) out.text += "\n";
|
|
206
|
+
if (quoted) {
|
|
207
|
+
quote ??= { offset: out.text.length, at: out.entities.length };
|
|
208
|
+
parseInline(quoted[1]!, out);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
171
211
|
const heading = /^#{1,6}\s+(.*?)\s*#*$/.exec(line);
|
|
172
212
|
if (heading) {
|
|
173
213
|
wrap(out, "bold", () => parseInline(heading[1]!, out));
|
|
@@ -175,6 +215,7 @@ function parseLines(block: string, out: Builder): void {
|
|
|
175
215
|
}
|
|
176
216
|
parseInline(line.replace(/^(\s*)[*•●·]\s+/, "$1- "), out);
|
|
177
217
|
});
|
|
218
|
+
closeQuote(out.text.length);
|
|
178
219
|
}
|
|
179
220
|
|
|
180
221
|
const HTML_TAGS: Record<string, TextEntityType> = {
|
|
@@ -190,6 +231,8 @@ const HTML_TAGS: Record<string, TextEntityType> = {
|
|
|
190
231
|
code: "code",
|
|
191
232
|
pre: "pre",
|
|
192
233
|
a: "link",
|
|
234
|
+
"tg-spoiler": "spoiler",
|
|
235
|
+
blockquote: "quote",
|
|
193
236
|
};
|
|
194
237
|
|
|
195
238
|
function decodeHtml(s: string): string {
|
|
@@ -310,6 +353,10 @@ function openTag(e: TextEntity): string {
|
|
|
310
353
|
return e.language ? `<pre><code class="language-${escapeAttribute(e.language)}">` : "<pre>";
|
|
311
354
|
case "link":
|
|
312
355
|
return `<a href="${escapeAttribute(e.url ?? "")}">`;
|
|
356
|
+
case "spoiler":
|
|
357
|
+
return "<tg-spoiler>";
|
|
358
|
+
case "quote":
|
|
359
|
+
return "<blockquote>";
|
|
313
360
|
}
|
|
314
361
|
}
|
|
315
362
|
|
|
@@ -319,9 +366,11 @@ function closeTag(e: TextEntity): string {
|
|
|
319
366
|
italic: "i",
|
|
320
367
|
underline: "u",
|
|
321
368
|
strike: "s",
|
|
369
|
+
spoiler: "tg-spoiler",
|
|
322
370
|
code: "code",
|
|
323
371
|
pre: "pre",
|
|
324
372
|
link: "a",
|
|
373
|
+
quote: "blockquote",
|
|
325
374
|
}[e.type];
|
|
326
375
|
return e.type === "pre" && e.language ? "</code></pre>" : `</${name}>`;
|
|
327
376
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TextFormat } from "./markup";
|
|
1
|
+
import type { TextEntity, TextFormat } from "./markup";
|
|
2
2
|
|
|
3
3
|
export type UsernameRecipient = {
|
|
4
4
|
type: "username";
|
|
@@ -300,6 +300,16 @@ export type IncomingMessage = {
|
|
|
300
300
|
editedAt?: Date;
|
|
301
301
|
/** В сообщении упомянут аккаунт или это ответ на его сообщение (так Telegram определяет упоминание); нет — канал не сообщает. */
|
|
302
302
|
mentionsMe?: boolean;
|
|
303
|
+
/**
|
|
304
|
+
* Только в onOutgoing/onOutgoingEdit: сообщение отправил или поправил сам транспорт (send/edit), а не человек
|
|
305
|
+
* с другого устройства. Помечается, а не отбрасывается: иначе своя отправка выглядела бы правкой оператора.
|
|
306
|
+
*/
|
|
307
|
+
sentByTransport?: boolean;
|
|
308
|
+
/**
|
|
309
|
+
* Форматирование текста в той же модели, что исходящая разметка, смещения в UTF-16;
|
|
310
|
+
* undefined = канал не сообщает форматирование, [] = сообщил, форматирования нет.
|
|
311
|
+
*/
|
|
312
|
+
entities?: TextEntity[];
|
|
303
313
|
attachments: IncomingAttachment[];
|
|
304
314
|
/**
|
|
305
315
|
* Адреса из сообщения, в порядке появления.
|
|
@@ -348,6 +358,57 @@ export function canReceive(transport: MessageTransport): transport is IncomingMe
|
|
|
348
358
|
return typeof (transport as IncomingMessageTransport).onMessage === "function";
|
|
349
359
|
}
|
|
350
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Канал, умеющий сообщать о правках сообщений.
|
|
363
|
+
*
|
|
364
|
+
* Отдельная подписка, потому что подписчики onMessage не ждут правок и сочтут повторный
|
|
365
|
+
* messageId за новое сообщение.
|
|
366
|
+
*/
|
|
367
|
+
export interface EditedMessageTransport extends MessageTransport {
|
|
368
|
+
onEdit(handler: (message: IncomingMessage) => void | Promise<void>): () => void;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function canReceiveEdits(transport: MessageTransport): transport is EditedMessageTransport {
|
|
372
|
+
return typeof (transport as EditedMessageTransport).onEdit === "function";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Черновик в поле ввода чата: ответ готовит машина, отправляет человек. */
|
|
376
|
+
export type MessageDraft = {
|
|
377
|
+
recipient: MessageRecipient;
|
|
378
|
+
/** Текст черновика; пустой — очистить черновик. */
|
|
379
|
+
text: string;
|
|
380
|
+
/** Как написан `text`; не задан — простой текст, без разбора разметки. */
|
|
381
|
+
format?: TextFormat;
|
|
382
|
+
/** Черновик ответом на это сообщение. */
|
|
383
|
+
replyToMessageId?: string;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
export interface DraftTransport extends MessageTransport {
|
|
387
|
+
saveDraft(draft: MessageDraft): Promise<void>;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function canSaveDraft(transport: MessageTransport): transport is DraftTransport {
|
|
391
|
+
return typeof (transport as DraftTransport).saveDraft === "function";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Свои исходящие учётки — то, что человек отправил или поправил с другого устройства. Отдельной подпиской:
|
|
396
|
+
* onMessage и onEdit своё не отдают и отдавать не должны — иначе шлюз отвечал бы сам себе.
|
|
397
|
+
*/
|
|
398
|
+
export interface OutgoingObserverTransport extends MessageTransport {
|
|
399
|
+
onOutgoing(handler: (message: IncomingMessage) => void | Promise<void>): () => void;
|
|
400
|
+
onOutgoingEdit(handler: (message: IncomingMessage) => void | Promise<void>): () => void;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function canObserveOutgoing(
|
|
404
|
+
transport: MessageTransport,
|
|
405
|
+
): transport is OutgoingObserverTransport {
|
|
406
|
+
const candidate = transport as OutgoingObserverTransport;
|
|
407
|
+
return (
|
|
408
|
+
typeof candidate.onOutgoing === "function" && typeof candidate.onOutgoingEdit === "function"
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
351
412
|
/** Пункт меню команд канала: `/menu` и подпись рядом с ним. */
|
|
352
413
|
export type ChannelCommand = {
|
|
353
414
|
/** Без ведущей косой черты. */
|