@yaebal/rich 0.0.1 → 0.0.3

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.
Files changed (95) hide show
  1. package/README.md +109 -35
  2. package/lib/blocks.d.ts +93 -32
  3. package/lib/blocks.d.ts.map +1 -1
  4. package/lib/blocks.js +227 -71
  5. package/lib/blocks.js.map +1 -1
  6. package/lib/blocks.test.d.ts +2 -0
  7. package/lib/blocks.test.d.ts.map +1 -0
  8. package/lib/blocks.test.js +142 -0
  9. package/lib/blocks.test.js.map +1 -0
  10. package/lib/document.d.ts +28 -0
  11. package/lib/document.d.ts.map +1 -0
  12. package/lib/document.js +46 -0
  13. package/lib/document.js.map +1 -0
  14. package/lib/draft.d.ts +46 -9
  15. package/lib/draft.d.ts.map +1 -1
  16. package/lib/draft.js +115 -19
  17. package/lib/draft.js.map +1 -1
  18. package/lib/draft.test.d.ts +2 -0
  19. package/lib/draft.test.d.ts.map +1 -0
  20. package/lib/draft.test.js +122 -0
  21. package/lib/draft.test.js.map +1 -0
  22. package/lib/escape.d.ts +8 -0
  23. package/lib/escape.d.ts.map +1 -1
  24. package/lib/escape.js +17 -0
  25. package/lib/escape.js.map +1 -1
  26. package/lib/escape.test.d.ts +2 -0
  27. package/lib/escape.test.d.ts.map +1 -0
  28. package/lib/escape.test.js +28 -0
  29. package/lib/escape.test.js.map +1 -0
  30. package/lib/guards.test.d.ts +2 -0
  31. package/lib/guards.test.d.ts.map +1 -0
  32. package/lib/guards.test.js +85 -0
  33. package/lib/guards.test.js.map +1 -0
  34. package/lib/index.d.ts +18 -9
  35. package/lib/index.d.ts.map +1 -1
  36. package/lib/index.js +17 -8
  37. package/lib/index.js.map +1 -1
  38. package/lib/index.test.js +73 -103
  39. package/lib/index.test.js.map +1 -1
  40. package/lib/inline.d.ts +48 -59
  41. package/lib/inline.d.ts.map +1 -1
  42. package/lib/inline.js +87 -82
  43. package/lib/inline.js.map +1 -1
  44. package/lib/inline.test.d.ts +2 -0
  45. package/lib/inline.test.d.ts.map +1 -0
  46. package/lib/inline.test.js +84 -0
  47. package/lib/inline.test.js.map +1 -0
  48. package/lib/node.d.ts +25 -0
  49. package/lib/node.d.ts.map +1 -0
  50. package/lib/node.js +21 -0
  51. package/lib/node.js.map +1 -0
  52. package/lib/plaintext.test.d.ts +2 -0
  53. package/lib/plaintext.test.d.ts.map +1 -0
  54. package/lib/plaintext.test.js +100 -0
  55. package/lib/plaintext.test.js.map +1 -0
  56. package/lib/render.d.ts +17 -0
  57. package/lib/render.d.ts.map +1 -0
  58. package/lib/render.js +30 -0
  59. package/lib/render.js.map +1 -0
  60. package/lib/send.d.ts +6 -3
  61. package/lib/send.d.ts.map +1 -1
  62. package/lib/send.js +12 -3
  63. package/lib/send.js.map +1 -1
  64. package/lib/template.d.ts +46 -0
  65. package/lib/template.d.ts.map +1 -0
  66. package/lib/template.js +82 -0
  67. package/lib/template.js.map +1 -0
  68. package/lib/template.test.d.ts +2 -0
  69. package/lib/template.test.d.ts.map +1 -0
  70. package/lib/template.test.js +78 -0
  71. package/lib/template.test.js.map +1 -0
  72. package/package.json +5 -4
  73. package/src/blocks.test.ts +233 -0
  74. package/src/blocks.ts +304 -86
  75. package/src/document.ts +51 -0
  76. package/src/draft.test.ts +167 -0
  77. package/src/draft.ts +148 -21
  78. package/src/escape.test.ts +38 -0
  79. package/src/escape.ts +20 -0
  80. package/src/guards.test.ts +138 -0
  81. package/src/index.test.ts +79 -140
  82. package/src/index.ts +26 -11
  83. package/src/inline.test.ts +125 -0
  84. package/src/inline.ts +131 -97
  85. package/src/node.ts +43 -0
  86. package/src/plaintext.test.ts +141 -0
  87. package/src/render.ts +54 -0
  88. package/src/send.ts +17 -10
  89. package/src/template.test.ts +100 -0
  90. package/src/template.ts +115 -0
  91. package/lib/message.d.ts +0 -26
  92. package/lib/message.d.ts.map +0 -1
  93. package/lib/message.js +0 -31
  94. package/lib/message.js.map +0 -1
  95. package/src/message.ts +0 -45
@@ -0,0 +1,46 @@
1
+ /**
2
+ * the emitted rich message — a rendered dialect string plus the
3
+ * `InputRichMessage` flags, settable fluently (`.rtl()`, `.noEntityDetection()`).
4
+ *
5
+ * `sendRichMessage`/`RichMessageDraft` accept it directly, and `toJSON()`
6
+ * delegates to `toInputRichMessage()`, so even a raw `api.call` payload holding
7
+ * a `RichDocument` serializes into the correct `rich_message` shape.
8
+ */
9
+ export class RichDocument {
10
+ dialect;
11
+ content;
12
+ #isRtl;
13
+ #skipEntityDetection;
14
+ constructor(dialect, content) {
15
+ this.dialect = dialect;
16
+ this.content = content;
17
+ }
18
+ /** `InputRichMessage.is_rtl` — show the message right-to-left. */
19
+ rtl(value = true) {
20
+ this.#isRtl = value;
21
+ return this;
22
+ }
23
+ /**
24
+ * `InputRichMessage.skip_entity_detection` — turn off auto-detection of urls,
25
+ * emails, `@mentions`, `#hashtags`, `$cashtags`, `/bot_commands`, and phone
26
+ * numbers in plain text.
27
+ */
28
+ noEntityDetection(value = true) {
29
+ this.#skipEntityDetection = value;
30
+ return this;
31
+ }
32
+ /** the `rich_message` method argument: `{ [dialect]: content, is_rtl?, skip_entity_detection? }`. */
33
+ toInputRichMessage() {
34
+ return {
35
+ [this.dialect]: this.content,
36
+ ...(this.#isRtl !== undefined ? { is_rtl: this.#isRtl } : {}),
37
+ ...(this.#skipEntityDetection !== undefined
38
+ ? { skip_entity_detection: this.#skipEntityDetection }
39
+ : {}),
40
+ };
41
+ }
42
+ toJSON() {
43
+ return this.toInputRichMessage();
44
+ }
45
+ }
46
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.js","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,OAAO,YAAY;IAKd;IACA;IALV,MAAM,CAAW;IACjB,oBAAoB,CAAW;IAE/B,YACU,OAAgB,EAChB,OAAe;QADf,YAAO,GAAP,OAAO,CAAS;QAChB,YAAO,GAAP,OAAO,CAAQ;IACtB,CAAC;IAEJ,kEAAkE;IAClE,GAAG,CAAC,KAAK,GAAG,IAAI;QACf,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;;OAIG;IACH,iBAAiB,CAAC,KAAK,GAAG,IAAI;QAC7B,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;QAClC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,qGAAqG;IACrG,kBAAkB;QACjB,OAAO;YACN,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO;YAC5B,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,GAAG,CAAC,IAAI,CAAC,oBAAoB,KAAK,SAAS;gBAC1C,CAAC,CAAC,EAAE,qBAAqB,EAAE,IAAI,CAAC,oBAAoB,EAAE;gBACtD,CAAC,CAAC,EAAE,CAAC;SACN,CAAC;IACH,CAAC;IAED,MAAM;QACL,OAAO,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAClC,CAAC;CACD"}
package/lib/draft.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Api, Message } from "@yaebal/core";
2
2
  import type { InputRichMessage } from "@yaebal/types";
3
+ import { RichDocument } from "./document.js";
3
4
  export interface RichMessageDraftOptions {
4
5
  /**
5
6
  * how often to re-push the last draft to keep it alive, in ms. telegram drops
@@ -10,7 +11,17 @@ export interface RichMessageDraftOptions {
10
11
  keepAliveMs?: number;
11
12
  /** called when a background keep-alive push fails (e.g. network blip). */
12
13
  onError?: (error: unknown) => void;
14
+ /**
15
+ * the forum topic to stream/send into. sendRichMessageDraft has no
16
+ * business_connection_id param (drafts aren't supported in business chats), so only
17
+ * the thread id carries through the keep-alive pushes; `send()` additionally accepts
18
+ * `businessConnectionId` for the final, non-ephemeral sendRichMessage call.
19
+ */
20
+ messageThreadId?: number;
21
+ /** routed into the final `send()` only — see {@link messageThreadId}. */
22
+ businessConnectionId?: string;
13
23
  }
24
+ type DraftSource = RichDocument | InputRichMessage | string;
14
25
  /**
15
26
  * a `sendRichMessageDraft` streaming session — the operationally hard part of the
16
27
  * rich-message api. telegram's draft is ephemeral: it vanishes 30s after the last
@@ -20,25 +31,51 @@ export interface RichMessageDraftOptions {
20
31
  *
21
32
  * - re-pushes the latest draft on a timer so a slow generator (e.g. an LLM
22
33
  * stream) doesn't lose the draft between tokens;
23
- * - refuses to push after `commit()`/`cancel()`, so a stray late token can't
34
+ * - refuses to push after `send()`/`cancel()`, so a stray late call can't
24
35
  * resurrect a closed draft;
25
- * - requires an explicit `commit()` — there is no implicit "last push wins".
36
+ * - requires an explicit `send()` — there is no implicit "last push wins".
37
+ *
38
+ * two ways to grow the draft: `rewrite()` replaces the whole thing (what you want
39
+ * for a token stream, where each chunk is a longer version of the *same* paragraph),
40
+ * `write()` appends to it via plain string concatenation (what you want to tack on
41
+ * a block — a footer, a divider — after content that's already there, without
42
+ * re-supplying it). a push boundary that lands mid-tag with `write()` is on the
43
+ * caller, same tradeoff raw concatenation always has.
44
+ *
45
+ * `send()` finalizes: with no argument it auto-assembles from the accumulated
46
+ * `rewrite()`/`write()` calls, so you don't have to re-render the final content —
47
+ * pass an explicit override when you want the persisted message to differ from the
48
+ * last draft snapshot.
26
49
  *
27
50
  * @example
28
51
  * const draft = ctx.richMessageDraft(1);
29
- * await draft.push(thinking("…"));
30
- * for await (const chunk of stream) await draft.push(document([paragraph(soFar)]));
31
- * await draft.commit(document([paragraph(finalAnswer)]));
52
+ * await draft.rewrite(document([thinking("…")]));
53
+ * for await (const chunk of stream) {
54
+ * soFar += chunk;
55
+ * await draft.rewrite(document([paragraph(soFar)]));
56
+ * }
57
+ * await draft.send();
32
58
  */
33
59
  export declare class RichMessageDraft {
34
60
  #private;
35
61
  constructor(api: Api, chatId: number, draftId: number, options?: RichMessageDraftOptions);
36
62
  get closed(): boolean;
37
- /** push a new partial draft; telegram animates the transition for a shared `draft_id`. */
38
- push(input: InputRichMessage | string): Promise<void>;
39
- /** finalize: persist the real message and stop the keep-alive. always call this or `cancel()`. */
40
- commit(input: InputRichMessage | string, extra?: Record<string, unknown>): Promise<Message>;
63
+ /** replace the whole draft with new content. */
64
+ rewrite(input: DraftSource): Promise<void>;
65
+ /**
66
+ * append to the current draft (plain string concatenation of the resolved text).
67
+ * throws if `input`'s dialect doesn't match the draft's, or if nothing has been
68
+ * written yet — call `rewrite()` first.
69
+ */
70
+ write(input: DraftSource): Promise<void>;
71
+ /**
72
+ * finalize: persist the real message and stop the keep-alive. with no `override`,
73
+ * auto-assembles from the accumulated `rewrite()`/`write()` calls. always call this
74
+ * or `cancel()`.
75
+ */
76
+ send(override?: DraftSource, extra?: Record<string, unknown>): Promise<Message>;
41
77
  /** abandon the draft without persisting anything — it expires within 30s on its own. */
42
78
  cancel(): void;
43
79
  }
80
+ export {};
44
81
  //# sourceMappingURL=draft.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,WAAW,uBAAuB;IACvC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,gBAAgB;;gBAWhB,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,uBAA4B;IAQ5F,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,0FAA0F;IACpF,IAAI,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B3D,kGAAkG;IAC5F,MAAM,CACX,KAAK,EAAE,gBAAgB,GAAG,MAAM,EAChC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACjC,OAAO,CAAC,OAAO,CAAC;IAYnB,wFAAwF;IACxF,MAAM,IAAI,IAAI;CAUd"}
1
+ {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAG7C,MAAM,WAAW,uBAAuB;IACvC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AASD,KAAK,WAAW,GAAG,YAAY,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAqC5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,gBAAgB;;gBAahB,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,uBAA4B;IAU5F,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,gDAAgD;IAC1C,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhD;;;;OAIG;IACG,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAoD9C;;;;OAIG;IACG,IAAI,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAuBzF,wFAAwF;IACxF,MAAM,IAAI,IAAI;CAgBd"}
package/lib/draft.js CHANGED
@@ -1,3 +1,36 @@
1
+ import { RichDocument } from "./document.js";
2
+ function resolve(input, fallbackDialect) {
3
+ if (typeof input === "string")
4
+ return { dialect: fallbackDialect, text: input };
5
+ if (input instanceof RichDocument)
6
+ return resolve(input.toInputRichMessage(), fallbackDialect);
7
+ if (input.html !== undefined) {
8
+ return {
9
+ dialect: "html",
10
+ text: input.html,
11
+ isRtl: input.is_rtl,
12
+ skipEntityDetection: input.skip_entity_detection,
13
+ };
14
+ }
15
+ if (input.markdown !== undefined) {
16
+ return {
17
+ dialect: "markdown",
18
+ text: input.markdown,
19
+ isRtl: input.is_rtl,
20
+ skipEntityDetection: input.skip_entity_detection,
21
+ };
22
+ }
23
+ throw new Error("RichMessageDraft: input must have an `html` or `markdown` field");
24
+ }
25
+ function toInputRichMessage(state) {
26
+ return {
27
+ [state.dialect]: state.text,
28
+ ...(state.isRtl !== undefined ? { is_rtl: state.isRtl } : {}),
29
+ ...(state.skipEntityDetection !== undefined
30
+ ? { skip_entity_detection: state.skipEntityDetection }
31
+ : {}),
32
+ };
33
+ }
1
34
  /**
2
35
  * a `sendRichMessageDraft` streaming session — the operationally hard part of the
3
36
  * rich-message api. telegram's draft is ephemeral: it vanishes 30s after the last
@@ -7,15 +40,30 @@
7
40
  *
8
41
  * - re-pushes the latest draft on a timer so a slow generator (e.g. an LLM
9
42
  * stream) doesn't lose the draft between tokens;
10
- * - refuses to push after `commit()`/`cancel()`, so a stray late token can't
43
+ * - refuses to push after `send()`/`cancel()`, so a stray late call can't
11
44
  * resurrect a closed draft;
12
- * - requires an explicit `commit()` — there is no implicit "last push wins".
45
+ * - requires an explicit `send()` — there is no implicit "last push wins".
46
+ *
47
+ * two ways to grow the draft: `rewrite()` replaces the whole thing (what you want
48
+ * for a token stream, where each chunk is a longer version of the *same* paragraph),
49
+ * `write()` appends to it via plain string concatenation (what you want to tack on
50
+ * a block — a footer, a divider — after content that's already there, without
51
+ * re-supplying it). a push boundary that lands mid-tag with `write()` is on the
52
+ * caller, same tradeoff raw concatenation always has.
53
+ *
54
+ * `send()` finalizes: with no argument it auto-assembles from the accumulated
55
+ * `rewrite()`/`write()` calls, so you don't have to re-render the final content —
56
+ * pass an explicit override when you want the persisted message to differ from the
57
+ * last draft snapshot.
13
58
  *
14
59
  * @example
15
60
  * const draft = ctx.richMessageDraft(1);
16
- * await draft.push(thinking("…"));
17
- * for await (const chunk of stream) await draft.push(document([paragraph(soFar)]));
18
- * await draft.commit(document([paragraph(finalAnswer)]));
61
+ * await draft.rewrite(document([thinking("…")]));
62
+ * for await (const chunk of stream) {
63
+ * soFar += chunk;
64
+ * await draft.rewrite(document([paragraph(soFar)]));
65
+ * }
66
+ * await draft.send();
19
67
  */
20
68
  export class RichMessageDraft {
21
69
  #api;
@@ -23,8 +71,10 @@ export class RichMessageDraft {
23
71
  #draftId;
24
72
  #keepAliveMs;
25
73
  #onError;
74
+ #messageThreadId;
75
+ #businessConnectionId;
26
76
  #timer;
27
- #latest;
77
+ #state;
28
78
  #closed = false;
29
79
  constructor(api, chatId, draftId, options = {}) {
30
80
  this.#api = api;
@@ -32,42 +82,83 @@ export class RichMessageDraft {
32
82
  this.#draftId = draftId;
33
83
  this.#keepAliveMs = options.keepAliveMs ?? 20_000;
34
84
  this.#onError = options.onError;
85
+ this.#messageThreadId = options.messageThreadId;
86
+ this.#businessConnectionId = options.businessConnectionId;
35
87
  }
36
88
  get closed() {
37
89
  return this.#closed;
38
90
  }
39
- /** push a new partial draft; telegram animates the transition for a shared `draft_id`. */
40
- async push(input) {
41
- if (this.#closed)
42
- throw new Error("RichMessageDraft: push() after commit()/cancel()");
43
- const resolved = typeof input === "string" ? { html: input } : input;
44
- this.#latest = resolved;
45
- await this.#push(resolved);
91
+ /** replace the whole draft with new content. */
92
+ async rewrite(input) {
93
+ this.#assertOpen();
94
+ this.#state = resolve(input, this.#state?.dialect ?? "html");
95
+ await this.#pushCurrent();
96
+ this.#arm();
97
+ }
98
+ /**
99
+ * append to the current draft (plain string concatenation of the resolved text).
100
+ * throws if `input`'s dialect doesn't match the draft's, or if nothing has been
101
+ * written yet — call `rewrite()` first.
102
+ */
103
+ async write(input) {
104
+ this.#assertOpen();
105
+ if (!this.#state) {
106
+ throw new Error("RichMessageDraft: write() before the first rewrite()");
107
+ }
108
+ const next = resolve(input, this.#state.dialect);
109
+ if (next.dialect !== this.#state.dialect) {
110
+ throw new Error(`RichMessageDraft: write() dialect "${next.dialect}" doesn't match the draft's "${this.#state.dialect}"`);
111
+ }
112
+ this.#state = {
113
+ dialect: this.#state.dialect,
114
+ text: this.#state.text + next.text,
115
+ isRtl: next.isRtl ?? this.#state.isRtl,
116
+ skipEntityDetection: next.skipEntityDetection ?? this.#state.skipEntityDetection,
117
+ };
118
+ await this.#pushCurrent();
46
119
  this.#arm();
47
120
  }
121
+ #pushCurrent() {
122
+ const state = this.#state;
123
+ if (!state)
124
+ throw new Error("unreachable: #pushCurrent() with no state");
125
+ return this.#push(toInputRichMessage(state));
126
+ }
48
127
  #push(input) {
49
128
  return this.#api.call("sendRichMessageDraft", {
50
129
  chat_id: this.#chatId,
51
130
  draft_id: this.#draftId,
52
131
  rich_message: input,
132
+ ...(this.#messageThreadId === undefined ? {} : { message_thread_id: this.#messageThreadId }),
53
133
  });
54
134
  }
55
135
  #arm() {
56
136
  clearInterval(this.#timer);
57
137
  this.#timer = setInterval(() => {
58
- if (!this.#latest)
138
+ if (!this.#state)
59
139
  return;
60
- this.#push(this.#latest).catch((error) => this.#onError?.(error));
140
+ this.#push(toInputRichMessage(this.#state)).catch((error) => this.#onError?.(error));
61
141
  }, this.#keepAliveMs);
62
142
  this.#timer.unref?.();
63
143
  }
64
- /** finalize: persist the real message and stop the keep-alive. always call this or `cancel()`. */
65
- async commit(input, extra = {}) {
144
+ /**
145
+ * finalize: persist the real message and stop the keep-alive. with no `override`,
146
+ * auto-assembles from the accumulated `rewrite()`/`write()` calls. always call this
147
+ * or `cancel()`.
148
+ */
149
+ async send(override, extra = {}) {
66
150
  this.#stop();
67
- const resolved = typeof input === "string" ? { html: input } : input;
151
+ const state = override !== undefined ? resolve(override, this.#state?.dialect ?? "html") : this.#state;
152
+ if (!state) {
153
+ throw new Error("RichMessageDraft: send() with nothing written — call rewrite()/write() first or pass an override");
154
+ }
68
155
  return this.#api.call("sendRichMessage", {
69
156
  chat_id: this.#chatId,
70
- rich_message: resolved,
157
+ rich_message: toInputRichMessage(state),
158
+ ...(this.#messageThreadId === undefined ? {} : { message_thread_id: this.#messageThreadId }),
159
+ ...(this.#businessConnectionId === undefined
160
+ ? {}
161
+ : { business_connection_id: this.#businessConnectionId }),
71
162
  ...extra,
72
163
  });
73
164
  }
@@ -81,5 +172,10 @@ export class RichMessageDraft {
81
172
  this.#closed = true;
82
173
  clearInterval(this.#timer);
83
174
  }
175
+ #assertOpen() {
176
+ if (this.#closed) {
177
+ throw new Error("RichMessageDraft: rewrite()/write() after send()/cancel()");
178
+ }
179
+ }
84
180
  }
85
181
  //# sourceMappingURL=draft.js.map
package/lib/draft.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,gBAAgB;IACnB,IAAI,CAAM;IACV,OAAO,CAAS;IAChB,QAAQ,CAAS;IACjB,YAAY,CAAS;IACrB,QAAQ,CAA4B;IAE7C,MAAM,CAAkC;IACxC,OAAO,CAA+B;IACtC,OAAO,GAAG,KAAK,CAAC;IAEhB,YAAY,GAAQ,EAAE,MAAc,EAAE,OAAe,EAAE,UAAmC,EAAE;QAC3F,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,WAAW,IAAI,MAAM,CAAC;QAClD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IACjC,CAAC;IAED,IAAI,MAAM;QACT,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;IAED,0FAA0F;IAC1F,KAAK,CAAC,IAAI,CAAC,KAAgC;QAC1C,IAAI,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAEtF,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QAErE,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC;QACxB,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,EAAE,CAAC;IACb,CAAC;IAED,KAAK,CAAC,KAAuB;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAU,sBAAsB,EAAE;YACtD,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,YAAY,EAAE,KAAK;SACnB,CAAC,CAAC;IACJ,CAAC;IAED,IAAI;QACH,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAE3B,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;YAC9B,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,OAAO;YAE1B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5E,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAEtB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;IACvB,CAAC;IAED,kGAAkG;IAClG,KAAK,CAAC,MAAM,CACX,KAAgC,EAChC,QAAiC,EAAE;QAEnC,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QAErE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAU,iBAAiB,EAAE;YACjD,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,YAAY,EAAE,QAAQ;YACtB,GAAG,KAAK;SACR,CAAC,CAAC;IACJ,CAAC;IAED,wFAAwF;IACxF,MAAM;QACL,IAAI,CAAC,KAAK,EAAE,CAAC;IACd,CAAC;IAED,KAAK;QACJ,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QAEzB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;CACD"}
1
+ {"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAiC7C,SAAS,OAAO,CAAC,KAAkB,EAAE,eAAwB;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAChF,IAAI,KAAK,YAAY,YAAY;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,EAAE,eAAe,CAAC,CAAC;IAE/F,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO;YACN,OAAO,EAAE,MAAM;YACf,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK,EAAE,KAAK,CAAC,MAAM;YACnB,mBAAmB,EAAE,KAAK,CAAC,qBAAqB;SAChD,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,OAAO;YACN,OAAO,EAAE,UAAU;YACnB,IAAI,EAAE,KAAK,CAAC,QAAQ;YACpB,KAAK,EAAE,KAAK,CAAC,MAAM;YACnB,mBAAmB,EAAE,KAAK,CAAC,qBAAqB;SAChD,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;AACpF,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAiB;IAC5C,OAAO;QACN,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI;QAC3B,GAAG,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,GAAG,CAAC,KAAK,CAAC,mBAAmB,KAAK,SAAS;YAC1C,CAAC,CAAC,EAAE,qBAAqB,EAAE,KAAK,CAAC,mBAAmB,EAAE;YACtD,CAAC,CAAC,EAAE,CAAC;KACN,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,OAAO,gBAAgB;IACnB,IAAI,CAAM;IACV,OAAO,CAAS;IAChB,QAAQ,CAAS;IACjB,YAAY,CAAS;IACrB,QAAQ,CAA4B;IACpC,gBAAgB,CAAU;IAC1B,qBAAqB,CAAU;IAExC,MAAM,CAAkC;IACxC,MAAM,CAAyB;IAC/B,OAAO,GAAG,KAAK,CAAC;IAEhB,YAAY,GAAQ,EAAE,MAAc,EAAE,OAAe,EAAE,UAAmC,EAAE;QAC3F,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,WAAW,IAAI,MAAM,CAAC;QAClD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;QAChD,IAAI,CAAC,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAC3D,CAAC;IAED,IAAI,MAAM;QACT,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;IAED,gDAAgD;IAChD,KAAK,CAAC,OAAO,CAAC,KAAkB;QAC/B,IAAI,CAAC,WAAW,EAAE,CAAC;QAEnB,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI,MAAM,CAAC,CAAC;QAC7D,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;IACb,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK,CAAC,KAAkB;QAC7B,IAAI,CAAC,WAAW,EAAE,CAAC;QAEnB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;QACzE,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACjD,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CACd,sCAAsC,IAAI,CAAC,OAAO,gCAAgC,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,CACxG,CAAC;QACH,CAAC;QAED,IAAI,CAAC,MAAM,GAAG;YACb,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO;YAC5B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI;YAClC,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK;YACtC,mBAAmB,EAAE,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,MAAM,CAAC,mBAAmB;SAChF,CAAC;QACF,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;IACb,CAAC;IAED,YAAY;QACX,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAEzE,OAAO,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,KAAuB;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAU,sBAAsB,EAAE;YACtD,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,YAAY,EAAE,KAAK;YACnB,GAAG,CAAC,IAAI,CAAC,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,IAAI,CAAC,gBAAgB,EAAE,CAAC;SAC5F,CAAC,CAAC;IACJ,CAAC;IAED,IAAI;QACH,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAE3B,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;YAC9B,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,OAAO;YAEzB,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/F,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAEtB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;IACvB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAI,CAAC,QAAsB,EAAE,QAAiC,EAAE;QACrE,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,MAAM,KAAK,GACV,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;QAE1F,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CACd,kGAAkG,CAClG,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAU,iBAAiB,EAAE;YACjD,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,YAAY,EAAE,kBAAkB,CAAC,KAAK,CAAC;YACvC,GAAG,CAAC,IAAI,CAAC,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC5F,GAAG,CAAC,IAAI,CAAC,qBAAqB,KAAK,SAAS;gBAC3C,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC1D,GAAG,KAAK;SACR,CAAC,CAAC;IACJ,CAAC;IAED,wFAAwF;IACxF,MAAM;QACL,IAAI,CAAC,KAAK,EAAE,CAAC;IACd,CAAC;IAED,KAAK;QACJ,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QAEzB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;IAED,WAAW;QACV,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;QAC9E,CAAC;IACF,CAAC;CACD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=draft.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft.test.d.ts","sourceRoot":"","sources":["../src/draft.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,122 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mockApi } from "@yaebal/test";
4
+ import { RichMessageDraft } from "./draft.js";
5
+ test("rewrite() pushes a full sendRichMessageDraft snapshot and re-arms the keep-alive timer", async () => {
6
+ const { api, calls } = mockApi();
7
+ const draft = new RichMessageDraft(api, 1, 7, { keepAliveMs: 60_000 });
8
+ await draft.rewrite("<tg-thinking>…</tg-thinking>");
9
+ assert.deepEqual(calls.map((c) => ({ method: c.method, params: c.params })), [
10
+ {
11
+ method: "sendRichMessageDraft",
12
+ params: { chat_id: 1, draft_id: 7, rich_message: { html: "<tg-thinking>…</tg-thinking>" } },
13
+ },
14
+ ]);
15
+ });
16
+ test("rewrite() replaces the whole draft — a second call drops the first's content", async () => {
17
+ const { api, calls } = mockApi();
18
+ const draft = new RichMessageDraft(api, 1, 1, { keepAliveMs: 60_000 });
19
+ await draft.rewrite("<p>one</p>");
20
+ await draft.rewrite("<p>two</p>");
21
+ assert.equal(calls.length, 2);
22
+ assert.deepEqual(calls[1]?.params?.rich_message, { html: "<p>two</p>" });
23
+ });
24
+ test("write() appends via string concatenation onto the current draft", async () => {
25
+ const { api, calls } = mockApi();
26
+ const draft = new RichMessageDraft(api, 1, 1, { keepAliveMs: 60_000 });
27
+ await draft.rewrite({ html: "<p>hello</p>" });
28
+ await draft.write({ html: "<hr/>" });
29
+ assert.deepEqual(calls[1]?.params?.rich_message, { html: "<p>hello</p><hr/>" });
30
+ });
31
+ test("write() before the first rewrite() throws", async () => {
32
+ const { api } = mockApi();
33
+ const draft = new RichMessageDraft(api, 1, 1);
34
+ await assert.rejects(() => draft.write("x"), /write\(\) before the first rewrite\(\)/);
35
+ });
36
+ test("write() with a dialect that doesn't match the draft's throws", async () => {
37
+ const { api } = mockApi();
38
+ const draft = new RichMessageDraft(api, 1, 1, { keepAliveMs: 60_000 });
39
+ await draft.rewrite({ html: "<p>hi</p>" });
40
+ await assert.rejects(() => draft.write({ markdown: "hi" }), /dialect "markdown" doesn't match the draft's "html"/);
41
+ });
42
+ test("send() with no override auto-assembles from the accumulated rewrite()/write() calls", async () => {
43
+ const { api, calls } = mockApi();
44
+ const draft = new RichMessageDraft(api, 1, 1, { keepAliveMs: 60_000 });
45
+ await draft.rewrite({ html: "<p>hello</p>" });
46
+ await draft.write({ html: "<hr/>" });
47
+ await draft.send();
48
+ assert.equal(calls[2]?.method, "sendRichMessage");
49
+ assert.deepEqual(calls[2]?.params, { chat_id: 1, rich_message: { html: "<p>hello</p><hr/>" } });
50
+ assert.equal(draft.closed, true);
51
+ });
52
+ test("send(override) persists the override instead of the accumulated draft", async () => {
53
+ const { api, calls } = mockApi();
54
+ const draft = new RichMessageDraft(api, 1, 1, { keepAliveMs: 60_000 });
55
+ await draft.rewrite({ html: "<p>draft text</p>" });
56
+ await draft.send({ html: "<p>final text</p>" }, { reply_markup: { x: 1 } });
57
+ assert.equal(calls[1]?.method, "sendRichMessage");
58
+ assert.deepEqual(calls[1]?.params, {
59
+ chat_id: 1,
60
+ rich_message: { html: "<p>final text</p>" },
61
+ reply_markup: { x: 1 },
62
+ });
63
+ });
64
+ test("send() with nothing written and no override throws", async () => {
65
+ const { api } = mockApi();
66
+ const draft = new RichMessageDraft(api, 1, 1);
67
+ await assert.rejects(() => draft.send(), /send\(\) with nothing written/);
68
+ });
69
+ test("cancel() closes without persisting anything", async () => {
70
+ const { api, calls } = mockApi();
71
+ const draft = new RichMessageDraft(api, 1, 2, { keepAliveMs: 60_000 });
72
+ await draft.rewrite("draft text");
73
+ draft.cancel();
74
+ assert.equal(draft.closed, true);
75
+ assert.equal(calls.some((c) => c.method === "sendRichMessage"), false);
76
+ });
77
+ test("rewrite()/write() after send()/cancel() throws", async () => {
78
+ const { api } = mockApi();
79
+ const draft = new RichMessageDraft(api, 1, 1, { keepAliveMs: 60_000 });
80
+ await draft.rewrite("x");
81
+ await draft.send();
82
+ await assert.rejects(() => draft.rewrite("late"), /after send\(\)\/cancel\(\)/);
83
+ await assert.rejects(() => draft.write("late"), /after send\(\)\/cancel\(\)/);
84
+ });
85
+ test("messageThreadId routes both the keep-alive pushes and the final send", async () => {
86
+ const { api, calls } = mockApi();
87
+ const draft = new RichMessageDraft(api, 1, 1, { keepAliveMs: 60_000, messageThreadId: 42 });
88
+ await draft.rewrite("<p>hi</p>");
89
+ await draft.send();
90
+ assert.equal(calls[0]?.method, "sendRichMessageDraft");
91
+ assert.deepEqual(calls[0]?.params, {
92
+ chat_id: 1,
93
+ draft_id: 1,
94
+ rich_message: { html: "<p>hi</p>" },
95
+ message_thread_id: 42,
96
+ });
97
+ assert.equal(calls[1]?.method, "sendRichMessage");
98
+ assert.deepEqual(calls[1]?.params, {
99
+ chat_id: 1,
100
+ rich_message: { html: "<p>hi</p>" },
101
+ message_thread_id: 42,
102
+ });
103
+ });
104
+ test("businessConnectionId routes only the final send (sendRichMessageDraft has no such param)", async () => {
105
+ const { api, calls } = mockApi();
106
+ const draft = new RichMessageDraft(api, 1, 1, {
107
+ keepAliveMs: 60_000,
108
+ businessConnectionId: "bc1",
109
+ });
110
+ await draft.rewrite("<p>hi</p>");
111
+ await draft.send();
112
+ assert.equal(calls[0]?.params?.business_connection_id, undefined);
113
+ assert.equal(calls[1]?.params?.business_connection_id, "bc1");
114
+ });
115
+ test("preserves is_rtl/skip_entity_detection across write() unless the new push overrides them", async () => {
116
+ const { api, calls } = mockApi();
117
+ const draft = new RichMessageDraft(api, 1, 1, { keepAliveMs: 60_000 });
118
+ await draft.rewrite({ html: "<p>a</p>", is_rtl: true });
119
+ await draft.write({ html: "<p>b</p>" });
120
+ assert.deepEqual(calls[1]?.params?.rich_message, { html: "<p>a</p><p>b</p>", is_rtl: true });
121
+ });
122
+ //# sourceMappingURL=draft.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft.test.js","sourceRoot":"","sources":["../src/draft.test.ts"],"names":[],"mappings":"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"}
package/lib/escape.d.ts CHANGED
@@ -2,4 +2,12 @@
2
2
  export declare function escapeText(value: string): string;
3
3
  /** escape a double-quoted attribute value. */
4
4
  export declare function escapeAttr(value: string): string;
5
+ /** escape text so it can never be re-parsed as rich-markdown syntax. */
6
+ export declare function escapeMarkdown(value: string): string;
7
+ /**
8
+ * escape a url used as a markdown link destination. `[text](url)` is terminated by `)`
9
+ * or whitespace, so an attacker-controlled url can't break out of the link — escape the
10
+ * parens/backslash and percent-encode whitespace.
11
+ */
12
+ export declare function escapeMarkdownUrl(value: string): string;
5
13
  //# sourceMappingURL=escape.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,8CAA8C;AAC9C,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD"}
1
+ {"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,8CAA8C;AAC9C,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAQD,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD"}
package/lib/escape.js CHANGED
@@ -6,4 +6,21 @@ export function escapeText(value) {
6
6
  export function escapeAttr(value) {
7
7
  return escapeText(value).replace(/"/g, "&quot;");
8
8
  }
9
+ // rich-markdown specials. `&`/`<`/`>` become numeric entities (rich-markdown renders
10
+ // `\<` with the backslash showing, but does accept entities — the same trick telegram's
11
+ // classic parse_mode markdown uses); everything else backslash-escapes cleanly.
12
+ const MARKDOWN_SPECIALS = /[\\`*_~=|[\]()#!+\-&<>]/g;
13
+ const MARKDOWN_ENTITY = { "&": "&#38;", "<": "&#60;", ">": "&#62;" };
14
+ /** escape text so it can never be re-parsed as rich-markdown syntax. */
15
+ export function escapeMarkdown(value) {
16
+ return value.replace(MARKDOWN_SPECIALS, (ch) => MARKDOWN_ENTITY[ch] ?? `\\${ch}`);
17
+ }
18
+ /**
19
+ * escape a url used as a markdown link destination. `[text](url)` is terminated by `)`
20
+ * or whitespace, so an attacker-controlled url can't break out of the link — escape the
21
+ * parens/backslash and percent-encode whitespace.
22
+ */
23
+ export function escapeMarkdownUrl(value) {
24
+ return value.replace(/[\\()]/g, "\\$&").replace(/\s/g, (ch) => encodeURIComponent(ch));
25
+ }
9
26
  //# sourceMappingURL=escape.js.map
package/lib/escape.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"escape.js","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,UAAU,CAAC,KAAa;IACvC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACjF,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,UAAU,CAAC,KAAa;IACvC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAClD,CAAC"}
1
+ {"version":3,"file":"escape.js","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,UAAU,CAAC,KAAa;IACvC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACjF,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,UAAU,CAAC,KAAa;IACvC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAClD,CAAC;AAED,qFAAqF;AACrF,wFAAwF;AACxF,gFAAgF;AAChF,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;AACrD,MAAM,eAAe,GAA2B,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AAE7F,wEAAwE;AACxE,MAAM,UAAU,cAAc,CAAC,KAAa;IAC3C,OAAO,KAAK,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC,CAAC;AACnF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC;AACxF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=escape.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escape.test.d.ts","sourceRoot":"","sources":["../src/escape.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,28 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { escapeAttr, escapeMarkdown, escapeMarkdownUrl, escapeText } from "./escape.js";
4
+ test("escapeText escapes &, <, > with named entities", () => {
5
+ assert.equal(escapeText("a & b < c > d"), "a &amp; b &lt; c &gt; d");
6
+ });
7
+ test("escapeText leaves other characters (including quotes) untouched", () => {
8
+ assert.equal(escapeText(`"hi" 'there'`), `"hi" 'there'`);
9
+ });
10
+ test("escapeAttr additionally escapes double quotes", () => {
11
+ assert.equal(escapeAttr(`say "hi" & bye`), "say &quot;hi&quot; &amp; bye");
12
+ });
13
+ test("escapeMarkdown backslash-escapes rich-markdown specials", () => {
14
+ assert.equal(escapeMarkdown("a*b_c~d=e|f[g]h(i)j#k!l+m-n`o"), "a\\*b\\_c\\~d\\=e\\|f\\[g\\]h\\(i\\)j\\#k\\!l\\+m\\-n\\`o");
15
+ });
16
+ test("escapeMarkdown numeric-entity-escapes &, <, >", () => {
17
+ assert.equal(escapeMarkdown("a & b < c > d"), "a &#38; b &#60; c &#62; d");
18
+ });
19
+ test("escapeMarkdown leaves plain text untouched", () => {
20
+ assert.equal(escapeMarkdown("hello world 123"), "hello world 123");
21
+ });
22
+ test("escapeMarkdownUrl escapes parens/backslash so a url can't break out of [text](url)", () => {
23
+ assert.equal(escapeMarkdownUrl("https://x.test/a(b)c\\d"), "https://x.test/a\\(b\\)c\\\\d");
24
+ });
25
+ test("escapeMarkdownUrl percent-encodes whitespace", () => {
26
+ assert.equal(escapeMarkdownUrl("https://x.test/a b"), "https://x.test/a%20b");
27
+ });
28
+ //# sourceMappingURL=escape.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escape.test.js","sourceRoot":"","sources":["../src/escape.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAExF,IAAI,CAAC,gDAAgD,EAAE,GAAG,EAAE;IAC3D,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,yBAAyB,CAAC,CAAC;AACtE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,iEAAiE,EAAE,GAAG,EAAE;IAC5E,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC,CAAC;AAC1D,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+CAA+C,EAAE,GAAG,EAAE;IAC1D,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,8BAA8B,CAAC,CAAC;AAC5E,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,yDAAyD,EAAE,GAAG,EAAE;IACpE,MAAM,CAAC,KAAK,CACX,cAAc,CAAC,+BAA+B,CAAC,EAC/C,2DAA2D,CAC3D,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+CAA+C,EAAE,GAAG,EAAE;IAC1D,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE,2BAA2B,CAAC,CAAC;AAC5E,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,4CAA4C,EAAE,GAAG,EAAE;IACvD,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC,CAAC;AACpE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,oFAAoF,EAAE,GAAG,EAAE;IAC/F,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,yBAAyB,CAAC,EAAE,+BAA+B,CAAC,CAAC;AAC7F,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,8CAA8C,EAAE,GAAG,EAAE;IACzD,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,oBAAoB,CAAC,EAAE,sBAAsB,CAAC,CAAC;AAC/E,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=guards.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.test.d.ts","sourceRoot":"","sources":["../src/guards.test.ts"],"names":[],"mappings":""}