@openclaw/crabline 0.1.10 → 0.1.12

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 (197) hide show
  1. package/README.md +99 -12
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +264 -50
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/matrix-ids.d.ts +5 -0
  31. package/dist/src/matrix-ids.js +83 -0
  32. package/dist/src/matrix-ids.js.map +1 -0
  33. package/dist/src/openclaw/artifact-generation.d.ts +12 -2
  34. package/dist/src/openclaw/artifact-generation.js +436 -15
  35. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  36. package/dist/src/openclaw/bridges/matrix.js +21 -4
  37. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  38. package/dist/src/openclaw/bridges/mattermost.js +13 -8
  39. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  40. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  41. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  42. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  43. package/dist/src/openclaw/bridges/signal.js +15 -6
  44. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  45. package/dist/src/openclaw/bridges/slack.js +116 -23
  46. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  47. package/dist/src/openclaw/bridges/telegram.js +45 -19
  48. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  49. package/dist/src/openclaw/bridges/whatsapp.js +32 -16
  50. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  51. package/dist/src/openclaw/bridges/zalo.js +8 -3
  52. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  53. package/dist/src/openclaw/private-file.d.ts +40 -3
  54. package/dist/src/openclaw/private-file.js +2070 -95
  55. package/dist/src/openclaw/private-file.js.map +1 -1
  56. package/dist/src/openclaw/shared.d.ts +1 -0
  57. package/dist/src/openclaw/shared.js +48 -13
  58. package/dist/src/openclaw/shared.js.map +1 -1
  59. package/dist/src/openclaw/smoke-lock.d.ts +9 -0
  60. package/dist/src/openclaw/smoke-lock.js +696 -187
  61. package/dist/src/openclaw/smoke-lock.js.map +1 -1
  62. package/dist/src/openclaw.js +352 -29
  63. package/dist/src/openclaw.js.map +1 -1
  64. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  65. package/dist/src/platform/process-owned-lock.js +1698 -0
  66. package/dist/src/platform/process-owned-lock.js.map +1 -0
  67. package/dist/src/platform/recorder-directory.d.ts +1 -0
  68. package/dist/src/platform/recorder-directory.js +14 -0
  69. package/dist/src/platform/recorder-directory.js.map +1 -0
  70. package/dist/src/platform/windows-acl.d.ts +18 -0
  71. package/dist/src/platform/windows-acl.js +1367 -0
  72. package/dist/src/platform/windows-acl.js.map +1 -0
  73. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  74. package/dist/src/platform/windows-lock-root.js +98 -0
  75. package/dist/src/platform/windows-lock-root.js.map +1 -0
  76. package/dist/src/providers/builtin/discord.d.ts +4 -0
  77. package/dist/src/providers/builtin/discord.js +52 -9
  78. package/dist/src/providers/builtin/discord.js.map +1 -1
  79. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  80. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  81. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  82. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  83. package/dist/src/providers/builtin/feishu.js +96 -26
  84. package/dist/src/providers/builtin/feishu.js.map +1 -1
  85. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  86. package/dist/src/providers/builtin/googlechat.js +95 -26
  87. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  88. package/dist/src/providers/builtin/imessage.js +60 -17
  89. package/dist/src/providers/builtin/imessage.js.map +1 -1
  90. package/dist/src/providers/builtin/loopback.js +88 -35
  91. package/dist/src/providers/builtin/loopback.js.map +1 -1
  92. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  93. package/dist/src/providers/builtin/matrix.js +78 -21
  94. package/dist/src/providers/builtin/matrix.js.map +1 -1
  95. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  96. package/dist/src/providers/builtin/mattermost.js +138 -24
  97. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  98. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  99. package/dist/src/providers/builtin/msteams.js +118 -38
  100. package/dist/src/providers/builtin/msteams.js.map +1 -1
  101. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  102. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  103. package/dist/src/providers/builtin/script.js +1057 -146
  104. package/dist/src/providers/builtin/script.js.map +1 -1
  105. package/dist/src/providers/builtin/slack.d.ts +8 -1
  106. package/dist/src/providers/builtin/slack.js +327 -24
  107. package/dist/src/providers/builtin/slack.js.map +1 -1
  108. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  109. package/dist/src/providers/builtin/telegram.js +55 -14
  110. package/dist/src/providers/builtin/telegram.js.map +1 -1
  111. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  112. package/dist/src/providers/builtin/whatsapp.js +157 -29
  113. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  114. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  115. package/dist/src/providers/builtin/zalo.js +31 -7
  116. package/dist/src/providers/builtin/zalo.js.map +1 -1
  117. package/dist/src/providers/catalog.d.ts +1 -1
  118. package/dist/src/providers/catalog.js +1 -1
  119. package/dist/src/providers/catalog.js.map +1 -1
  120. package/dist/src/providers/local-mock.d.ts +4 -0
  121. package/dist/src/providers/local-mock.js +162 -35
  122. package/dist/src/providers/local-mock.js.map +1 -1
  123. package/dist/src/providers/native-ids.d.ts +2 -0
  124. package/dist/src/providers/native-ids.js +3 -0
  125. package/dist/src/providers/native-ids.js.map +1 -1
  126. package/dist/src/providers/recorder.d.ts +14 -3
  127. package/dist/src/providers/recorder.js +532 -42
  128. package/dist/src/providers/recorder.js.map +1 -1
  129. package/dist/src/providers/registry.d.ts +1 -0
  130. package/dist/src/providers/registry.js +65 -20
  131. package/dist/src/providers/registry.js.map +1 -1
  132. package/dist/src/providers/signed-jwt.d.ts +6 -1
  133. package/dist/src/providers/signed-jwt.js +241 -50
  134. package/dist/src/providers/signed-jwt.js.map +1 -1
  135. package/dist/src/providers/slack-ids.d.ts +1 -0
  136. package/dist/src/providers/slack-ids.js +5 -0
  137. package/dist/src/providers/slack-ids.js.map +1 -1
  138. package/dist/src/providers/target-normalizers.d.ts +9 -1
  139. package/dist/src/providers/target-normalizers.js +177 -58
  140. package/dist/src/providers/target-normalizers.js.map +1 -1
  141. package/dist/src/providers/webhook-server.d.ts +3 -0
  142. package/dist/src/providers/webhook-server.js +268 -47
  143. package/dist/src/providers/webhook-server.js.map +1 -1
  144. package/dist/src/servers/http.d.ts +14 -2
  145. package/dist/src/servers/http.js +229 -58
  146. package/dist/src/servers/http.js.map +1 -1
  147. package/dist/src/servers/matrix.d.ts +3 -0
  148. package/dist/src/servers/matrix.js +268 -110
  149. package/dist/src/servers/matrix.js.map +1 -1
  150. package/dist/src/servers/mattermost.d.ts +5 -0
  151. package/dist/src/servers/mattermost.js +389 -91
  152. package/dist/src/servers/mattermost.js.map +1 -1
  153. package/dist/src/servers/recorder.d.ts +11 -0
  154. package/dist/src/servers/recorder.js +1241 -22
  155. package/dist/src/servers/recorder.js.map +1 -1
  156. package/dist/src/servers/signal.d.ts +1 -0
  157. package/dist/src/servers/signal.js +279 -44
  158. package/dist/src/servers/signal.js.map +1 -1
  159. package/dist/src/servers/slack.d.ts +15 -0
  160. package/dist/src/servers/slack.js +259 -51
  161. package/dist/src/servers/slack.js.map +1 -1
  162. package/dist/src/servers/telegram-identity.d.ts +6 -0
  163. package/dist/src/servers/telegram-identity.js +27 -0
  164. package/dist/src/servers/telegram-identity.js.map +1 -0
  165. package/dist/src/servers/telegram.d.ts +17 -6
  166. package/dist/src/servers/telegram.js +1193 -118
  167. package/dist/src/servers/telegram.js.map +1 -1
  168. package/dist/src/servers/webhook-target.d.ts +12 -4
  169. package/dist/src/servers/webhook-target.js +226 -28
  170. package/dist/src/servers/webhook-target.js.map +1 -1
  171. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  172. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  173. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  174. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  175. package/dist/src/servers/whatsapp-jid.js +9 -5
  176. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  177. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  178. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  179. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  180. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  181. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  182. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  183. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  184. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  185. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  186. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  187. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  188. package/dist/src/servers/whatsapp.d.ts +7 -0
  189. package/dist/src/servers/whatsapp.js +149 -24
  190. package/dist/src/servers/whatsapp.js.map +1 -1
  191. package/dist/src/servers/zalo.d.ts +1 -0
  192. package/dist/src/servers/zalo.js +193 -82
  193. package/dist/src/servers/zalo.js.map +1 -1
  194. package/docs/channel-setup.md +185 -43
  195. package/fixtures/examples/crabline.example.yaml +3 -2
  196. package/package.json +18 -10
  197. package/fixtures/examples/openclaw-bridge.yaml +0 -125
@@ -1,17 +1,26 @@
1
1
  import { createServer } from "node:http";
2
- import { randomBytes } from "node:crypto";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { Buffer } from "node:buffer";
3
4
  import path from "node:path";
4
5
  import { CrablineError } from "../core/errors.js";
5
- import { adminAuthError, closeServer, drainRequestBody, formatUrlHost, hasAdminToken, InvalidJsonBodyError, isJsonObject, isLoopbackHost, jsonResponse, queryRecord, readBody, RequestBodyTooLargeError, writeResponse, } from "./http.js";
6
+ import { adminAuthError, closeServer, drainRequestBody, formatUrlHost, hasAdminToken, InvalidJsonBodyError, isJsonObject, isJsonMediaType, isLoopbackHost, jsonResponse, queryRecord, readBody, RequestBodyTooLargeError, writeResponse, } from "./http.js";
6
7
  import { recordServerEvent } from "./recorder.js";
7
8
  import { resolveMaxPendingInboundEvents } from "./pending-events.js";
8
9
  import { postWebhookRequest, validateWebhookTarget, } from "./webhook-target.js";
10
+ import { canonicalizeTelegramUsername, isTelegramUsernameChatId, TELEGRAM_BOT_USERNAME_PATTERN, TELEGRAM_NATIVE_CHAT_ID_MAX, telegramUsernameChatId, } from "./telegram-identity.js";
9
11
  const TELEGRAM_MAX_REQUEST_BODY_BYTES = 50 * 1024 * 1024;
12
+ const TELEGRAM_MAX_MULTIPART_HEADER_BYTES = 16 * 1024;
13
+ const TELEGRAM_MAX_MULTIPART_PARTS = 128;
14
+ const TELEGRAM_MAX_MULTIPART_TEXT_BYTES = 1024 * 1024;
10
15
  const TELEGRAM_WEBHOOK_MAX_BACKOFF_EXPONENT = 5;
11
16
  const TELEGRAM_WEBHOOK_RETRY_BASE_MS = 100;
12
17
  const TELEGRAM_WEBHOOK_DELIVERY_TIMEOUT_MS = 3_000;
13
18
  const MAX_ACTIVE_TELEGRAM_WEBHOOK_VALIDATIONS = 8;
14
- const TELEGRAM_CHAT_USERNAME_PATTERN = /^@[A-Za-z][A-Za-z0-9_]{3,31}$/u;
19
+ const TELEGRAM_WEBHOOK_SECRET_PATTERN = /^[A-Za-z0-9_-]{1,256}(?![\s\S])/u;
20
+ const TELEGRAM_MAX_TEXT_LENGTH = 4096;
21
+ const TELEGRAM_MAX_CAPTION_LENGTH = 1024;
22
+ const TELEGRAM_HTML_TAG_PATTERN = /<\/?(?:a|b|blockquote|code|del|em|i|ins|pre|s|span|strike|strong|tg-emoji|tg-spoiler|tg-time|u)(?:\s+[^<>]*)?>/giu;
23
+ const TELEGRAM_HTML_ENTITY_PATTERN = /&(?:#\d+|#x[\da-f]+|amp|gt|lt|quot);/giu;
15
24
  const TELEGRAM_SEND_METHODS = new Set([
16
25
  "sendanimation",
17
26
  "sendaudio",
@@ -58,6 +67,27 @@ const TELEGRAM_UNSUPPORTED_UPDATE_FIELDS = [
58
67
  "removed_chat_boost",
59
68
  "shipping_query",
60
69
  ];
70
+ const TELEGRAM_NEW_MESSAGE_UPDATE_FIELDS = ["business_message", "channel_post", "message"];
71
+ const TELEGRAM_MESSAGE_REFERENCE_FIELDS = [
72
+ "edited_business_message",
73
+ "edited_channel_post",
74
+ "edited_message",
75
+ ];
76
+ const TELEGRAM_MESSAGE_UPDATE_FIELDS = [
77
+ ...TELEGRAM_NEW_MESSAGE_UPDATE_FIELDS,
78
+ ...TELEGRAM_MESSAGE_REFERENCE_FIELDS,
79
+ ];
80
+ const TELEGRAM_CHAT_UPDATE_FIELDS = [
81
+ "chat_boost",
82
+ "chat_join_request",
83
+ "chat_member",
84
+ "deleted_business_messages",
85
+ "message_reaction",
86
+ "message_reaction_count",
87
+ "my_chat_member",
88
+ "removed_chat_boost",
89
+ ];
90
+ const TELEGRAM_MULTIPART_FILE = Symbol("telegramMultipartFile");
61
91
  function telegramOk(result) {
62
92
  return jsonResponse({ ok: true, result });
63
93
  }
@@ -65,13 +95,17 @@ function telegramError(description, status = 400) {
65
95
  return jsonResponse({ description, error_code: status, ok: false }, status);
66
96
  }
67
97
  async function parseRequestBody(request) {
98
+ const contentType = request.headers["content-type"] ?? "";
99
+ const contentTypes = Array.isArray(contentType) ? contentType : [contentType];
100
+ const multipartType = contentTypes.find((entry) => entry.toLowerCase().includes("multipart/form-data"));
101
+ if (multipartType) {
102
+ return await parseMultipartFormDataRequest(request, multipartType);
103
+ }
68
104
  const body = await readBody(request, TELEGRAM_MAX_REQUEST_BODY_BYTES);
69
105
  if (body.length === 0) {
70
106
  return {};
71
107
  }
72
- const contentType = request.headers["content-type"] ?? "";
73
- const contentTypes = Array.isArray(contentType) ? contentType : [contentType];
74
- if (contentTypes.some((entry) => entry.toLowerCase().includes("json"))) {
108
+ if (contentTypes.some(isJsonMediaType)) {
75
109
  try {
76
110
  return JSON.parse(body.toString("utf8"));
77
111
  }
@@ -79,30 +113,435 @@ async function parseRequestBody(request) {
79
113
  throw new InvalidJsonBodyError(error);
80
114
  }
81
115
  }
82
- const multipartType = contentTypes.find((entry) => entry.toLowerCase().includes("multipart/form-data"));
83
- if (multipartType) {
84
- return await parseMultipartFormDataBody(body, multipartType);
85
- }
86
116
  const params = new URLSearchParams(body.toString("utf8"));
87
117
  return Object.fromEntries(params.entries());
88
118
  }
89
- async function parseMultipartFormDataBody(body, contentType) {
90
- let form;
119
+ function invalidTelegramMultipartBody(message) {
120
+ return new InvalidJsonBodyError(new Error(message));
121
+ }
122
+ function parseTelegramMimeParameters(value, options = {}) {
123
+ const separator = value.indexOf(";");
124
+ const type = value
125
+ .slice(0, separator < 0 ? value.length : separator)
126
+ .trim()
127
+ .toLowerCase();
128
+ const parameters = new Map();
129
+ let index = separator < 0 ? value.length : separator + 1;
130
+ while (index < value.length) {
131
+ while (index < value.length && /[\t ;]/u.test(value[index] ?? "")) {
132
+ index += 1;
133
+ }
134
+ if (index >= value.length) {
135
+ break;
136
+ }
137
+ const nameStart = index;
138
+ while (index < value.length && /[!#$%&'*+\-.^_`|~0-9A-Za-z]/u.test(value[index] ?? "")) {
139
+ index += 1;
140
+ }
141
+ const name = value.slice(nameStart, index).toLowerCase();
142
+ while (index < value.length && /[\t ]/u.test(value[index] ?? "")) {
143
+ index += 1;
144
+ }
145
+ if (!name || value[index] !== "=") {
146
+ throw invalidTelegramMultipartBody("Multipart header contains a malformed parameter.");
147
+ }
148
+ index += 1;
149
+ while (index < value.length && /[\t ]/u.test(value[index] ?? "")) {
150
+ index += 1;
151
+ }
152
+ let parameterValue = "";
153
+ let quoted = false;
154
+ if (value[index] === '"') {
155
+ quoted = true;
156
+ index += 1;
157
+ let closed = false;
158
+ while (index < value.length) {
159
+ const character = value[index];
160
+ index += 1;
161
+ if (character === "\\" && options.decodeQuotedPairs) {
162
+ if (index >= value.length) {
163
+ break;
164
+ }
165
+ const quotedCharacter = value[index];
166
+ if (quotedCharacter === "\r" || quotedCharacter === "\n") {
167
+ break;
168
+ }
169
+ parameterValue += quotedCharacter;
170
+ index += 1;
171
+ }
172
+ else if (character === '"') {
173
+ closed = true;
174
+ break;
175
+ }
176
+ else if (character === "\r" || character === "\n") {
177
+ break;
178
+ }
179
+ else {
180
+ parameterValue += character;
181
+ }
182
+ }
183
+ if (!closed) {
184
+ throw invalidTelegramMultipartBody("Multipart header contains an unterminated quote.");
185
+ }
186
+ while (index < value.length && /[\t ]/u.test(value[index] ?? "")) {
187
+ index += 1;
188
+ }
189
+ if (index < value.length && value[index] !== ";") {
190
+ throw invalidTelegramMultipartBody("Multipart header contains trailing parameter data.");
191
+ }
192
+ }
193
+ else {
194
+ const parameterStart = index;
195
+ while (index < value.length && value[index] !== ";") {
196
+ if (value[index] === "\r" || value[index] === "\n") {
197
+ throw invalidTelegramMultipartBody("Multipart header contains a line break.");
198
+ }
199
+ index += 1;
200
+ }
201
+ parameterValue = value.slice(parameterStart, index).trim();
202
+ }
203
+ parameters.set(name, { quoted, value: parameterValue });
204
+ if (value[index] === ";") {
205
+ index += 1;
206
+ }
207
+ }
208
+ return { parameters, type };
209
+ }
210
+ function parseTelegramMultipartBoundary(contentType) {
211
+ const parsed = parseTelegramMimeParameters(contentType, { decodeQuotedPairs: true });
212
+ const boundary = parsed.parameters.get("boundary")?.value;
213
+ if (parsed.type !== "multipart/form-data" ||
214
+ !boundary ||
215
+ boundary.length > 70 ||
216
+ boundary.endsWith(" ") ||
217
+ !/^[0-9A-Za-z'()+_,./:=? -]+$/u.test(boundary)) {
218
+ throw invalidTelegramMultipartBody("Multipart boundary is missing or invalid.");
219
+ }
220
+ return Buffer.from(`--${boundary}`, "ascii");
221
+ }
222
+ function decodeTelegramMultipartParameter(parameter) {
223
+ return parameter.quoted
224
+ ? parameter.value.replace(/%0A/giu, "\n").replace(/%0D/giu, "\r").replace(/%22/giu, '"')
225
+ : parameter.value;
226
+ }
227
+ function decodeTelegramMultipartExtendedParameter(parameter) {
228
+ const extended = /^([^']*)'[^']*'(.*)$/u.exec(parameter.value);
229
+ if (!extended) {
230
+ return decodeTelegramMultipartParameter(parameter);
231
+ }
232
+ const charset = extended[1]?.toLowerCase();
233
+ if (charset !== "utf-8" && charset !== "us-ascii") {
234
+ return decodeTelegramMultipartParameter(parameter);
235
+ }
91
236
  try {
92
- form = await new Request("http://localhost", {
93
- body,
94
- headers: { "content-type": contentType },
95
- method: "POST",
96
- }).formData();
237
+ return decodeURIComponent(extended[2] ?? "");
97
238
  }
98
- catch (error) {
99
- throw new InvalidJsonBodyError(error);
239
+ catch {
240
+ return extended[2] ?? "";
241
+ }
242
+ }
243
+ function parseTelegramMultipartDisposition(headerBlock) {
244
+ let contentDisposition;
245
+ for (const line of headerBlock.toString("utf8").split("\r\n")) {
246
+ const separator = line.indexOf(":");
247
+ if (separator <= 0) {
248
+ throw invalidTelegramMultipartBody("Multipart part contains a malformed header.");
249
+ }
250
+ if (line.slice(0, separator).trim().toLowerCase() === "content-disposition") {
251
+ contentDisposition = line.slice(separator + 1).trim();
252
+ }
253
+ }
254
+ if (!contentDisposition) {
255
+ throw invalidTelegramMultipartBody("Multipart part is missing a form-data disposition.");
256
+ }
257
+ const parsed = parseTelegramMimeParameters(contentDisposition, { decodeQuotedPairs: true });
258
+ if (parsed.type !== "form-data") {
259
+ throw invalidTelegramMultipartBody("Multipart part is missing a form-data disposition.");
260
+ }
261
+ const rawName = parsed.parameters.get("name");
262
+ if (rawName === undefined) {
263
+ throw invalidTelegramMultipartBody("Multipart part is missing a field name.");
264
+ }
265
+ const rawFileName = parsed.parameters.get("filename");
266
+ const rawExtendedFileName = parsed.parameters.get("filename*");
267
+ return {
268
+ ...(rawExtendedFileName !== undefined
269
+ ? { fileName: decodeTelegramMultipartExtendedParameter(rawExtendedFileName) }
270
+ : rawFileName !== undefined
271
+ ? { fileName: decodeTelegramMultipartParameter(rawFileName) }
272
+ : {}),
273
+ name: decodeTelegramMultipartParameter(rawName),
274
+ };
275
+ }
276
+ class TelegramMultipartReader {
277
+ request;
278
+ maxBytes;
279
+ #iterator;
280
+ #buffer = Buffer.alloc(0);
281
+ #ended = false;
282
+ #totalBytes = 0;
283
+ constructor(request, maxBytes) {
284
+ this.request = request;
285
+ this.maxBytes = maxBytes;
286
+ this.#iterator = request[Symbol.asyncIterator]();
287
+ }
288
+ async readUntil(delimiter, maxBytes) {
289
+ const chunks = [];
290
+ let length = 0;
291
+ while (true) {
292
+ const delimiterIndex = this.#buffer.indexOf(delimiter);
293
+ if (delimiterIndex >= 0) {
294
+ length += delimiterIndex;
295
+ if (length > maxBytes) {
296
+ throw new RequestBodyTooLargeError(maxBytes);
297
+ }
298
+ chunks.push(this.#buffer.subarray(0, delimiterIndex));
299
+ this.#consume(delimiterIndex + delimiter.length);
300
+ return Buffer.concat(chunks, length);
301
+ }
302
+ const emitLength = Math.max(0, this.#buffer.length - delimiter.length + 1);
303
+ if (emitLength > 0) {
304
+ length += emitLength;
305
+ if (length > maxBytes) {
306
+ throw new RequestBodyTooLargeError(maxBytes);
307
+ }
308
+ chunks.push(this.#buffer.subarray(0, emitLength));
309
+ this.#consume(emitLength);
310
+ }
311
+ if (!(await this.#readMore())) {
312
+ throw invalidTelegramMultipartBody("Multipart delimiter is incomplete.");
313
+ }
314
+ }
315
+ }
316
+ async readInitialBoundary(boundary, maxPreambleBytes) {
317
+ const lineBoundary = Buffer.concat([Buffer.from("\r\n"), boundary]);
318
+ let preambleBytes = 0;
319
+ let checkBodyStart = true;
320
+ while (true) {
321
+ if (checkBodyStart) {
322
+ await this.#ensure(boundary.length + 2);
323
+ checkBodyStart = false;
324
+ if (this.#buffer.subarray(0, boundary.length).equals(boundary)) {
325
+ const suffix = await this.#readBoundarySuffix(boundary.length);
326
+ if (suffix) {
327
+ this.#consume(suffix.consumedBytes);
328
+ return suffix.kind;
329
+ }
330
+ }
331
+ }
332
+ const boundaryIndex = this.#buffer.indexOf(lineBoundary);
333
+ if (boundaryIndex >= 0) {
334
+ const suffixOffset = boundaryIndex + lineBoundary.length;
335
+ if (this.#buffer.length < suffixOffset + 2) {
336
+ if (boundaryIndex > 0) {
337
+ preambleBytes += boundaryIndex;
338
+ if (preambleBytes > maxPreambleBytes) {
339
+ throw new RequestBodyTooLargeError(maxPreambleBytes);
340
+ }
341
+ this.#consume(boundaryIndex);
342
+ }
343
+ if (!(await this.#readMore())) {
344
+ throw invalidTelegramMultipartBody("Multipart boundary is incomplete.");
345
+ }
346
+ continue;
347
+ }
348
+ const suffix = await this.#readBoundarySuffix(suffixOffset);
349
+ if (suffix) {
350
+ preambleBytes += boundaryIndex;
351
+ if (preambleBytes > maxPreambleBytes) {
352
+ throw new RequestBodyTooLargeError(maxPreambleBytes);
353
+ }
354
+ this.#consume(suffix.consumedBytes);
355
+ return suffix.kind;
356
+ }
357
+ const emitLength = suffixOffset;
358
+ preambleBytes += emitLength;
359
+ if (preambleBytes > maxPreambleBytes) {
360
+ throw new RequestBodyTooLargeError(maxPreambleBytes);
361
+ }
362
+ this.#consume(emitLength);
363
+ continue;
364
+ }
365
+ const emitLength = Math.max(0, this.#buffer.length - lineBoundary.length + 1);
366
+ if (emitLength > 0) {
367
+ preambleBytes += emitLength;
368
+ if (preambleBytes > maxPreambleBytes) {
369
+ throw new RequestBodyTooLargeError(maxPreambleBytes);
370
+ }
371
+ this.#consume(emitLength);
372
+ }
373
+ if (!(await this.#readMore())) {
374
+ throw invalidTelegramMultipartBody("Multipart body is missing its initial boundary.");
375
+ }
376
+ }
377
+ }
378
+ async readPartBody(boundaryMarker, onChunk) {
379
+ while (true) {
380
+ const boundaryIndex = this.#buffer.indexOf(boundaryMarker);
381
+ if (boundaryIndex >= 0) {
382
+ const suffixOffset = boundaryIndex + boundaryMarker.length;
383
+ if (this.#buffer.length < suffixOffset + 2) {
384
+ if (boundaryIndex > 0) {
385
+ onChunk(this.#buffer.subarray(0, boundaryIndex));
386
+ this.#consume(boundaryIndex);
387
+ }
388
+ if (!(await this.#readMore())) {
389
+ throw invalidTelegramMultipartBody("Multipart boundary is incomplete.");
390
+ }
391
+ continue;
392
+ }
393
+ const suffix = await this.#readBoundarySuffix(suffixOffset);
394
+ if (suffix) {
395
+ if (boundaryIndex > 0) {
396
+ onChunk(this.#buffer.subarray(0, boundaryIndex));
397
+ }
398
+ this.#consume(suffix.consumedBytes);
399
+ return suffix.kind;
400
+ }
401
+ const emitLength = suffixOffset;
402
+ onChunk(this.#buffer.subarray(0, emitLength));
403
+ this.#consume(emitLength);
404
+ continue;
405
+ }
406
+ const emitLength = Math.max(0, this.#buffer.length - boundaryMarker.length + 1);
407
+ if (emitLength > 0) {
408
+ onChunk(this.#buffer.subarray(0, emitLength));
409
+ this.#consume(emitLength);
410
+ }
411
+ if (!(await this.#readMore())) {
412
+ throw invalidTelegramMultipartBody("Multipart body is missing its closing boundary.");
413
+ }
414
+ }
415
+ }
416
+ async consumeRemainder() {
417
+ while (await this.#readMore()) {
418
+ this.#buffer = Buffer.alloc(0);
419
+ }
420
+ }
421
+ #consume(length) {
422
+ this.#buffer = this.#buffer.subarray(length);
423
+ }
424
+ async #ensure(length) {
425
+ while (this.#buffer.length < length) {
426
+ if (!(await this.#readMore())) {
427
+ throw invalidTelegramMultipartBody("Multipart body ended unexpectedly.");
428
+ }
429
+ }
430
+ }
431
+ async #readBoundarySuffix(suffixOffset) {
432
+ await this.#ensure(suffixOffset + 2);
433
+ const suffix = this.#buffer.subarray(suffixOffset, suffixOffset + 2);
434
+ const final = suffix.equals(Buffer.from("--"));
435
+ let lineEnd = suffixOffset + (final ? 2 : 0);
436
+ while (true) {
437
+ while (lineEnd < this.#buffer.length &&
438
+ (this.#buffer[lineEnd] === 0x20 || this.#buffer[lineEnd] === 0x09)) {
439
+ lineEnd += 1;
440
+ if (lineEnd - suffixOffset > TELEGRAM_MAX_MULTIPART_HEADER_BYTES) {
441
+ return undefined;
442
+ }
443
+ }
444
+ if (lineEnd >= this.#buffer.length) {
445
+ if (await this.#readMore()) {
446
+ continue;
447
+ }
448
+ return final ? { consumedBytes: lineEnd, kind: "final" } : undefined;
449
+ }
450
+ if (this.#buffer[lineEnd] !== 0x0d) {
451
+ return undefined;
452
+ }
453
+ if (lineEnd + 1 >= this.#buffer.length) {
454
+ if (await this.#readMore()) {
455
+ continue;
456
+ }
457
+ return undefined;
458
+ }
459
+ return this.#buffer[lineEnd + 1] === 0x0a
460
+ ? { consumedBytes: lineEnd + 2, kind: final ? "final" : "next" }
461
+ : undefined;
462
+ }
463
+ }
464
+ async #readMore() {
465
+ if (this.#ended) {
466
+ return false;
467
+ }
468
+ const next = await this.#iterator.next();
469
+ if (next.done) {
470
+ this.#ended = true;
471
+ return false;
472
+ }
473
+ const chunk = Buffer.isBuffer(next.value) ? next.value : Buffer.from(next.value);
474
+ this.#totalBytes += chunk.length;
475
+ if (this.#totalBytes > this.maxBytes) {
476
+ throw new RequestBodyTooLargeError(this.maxBytes);
477
+ }
478
+ this.#buffer =
479
+ this.#buffer.length === 0
480
+ ? chunk
481
+ : Buffer.concat([this.#buffer, chunk], this.#buffer.length + chunk.length);
482
+ return true;
483
+ }
484
+ }
485
+ function enforceTelegramMultipartContentLength(request) {
486
+ const contentLengthHeader = request.headers["content-length"];
487
+ const contentLength = Array.isArray(contentLengthHeader)
488
+ ? contentLengthHeader[0]
489
+ : contentLengthHeader;
490
+ if (contentLength && /^\d+$/u.test(contentLength)) {
491
+ const bytes = Number(contentLength);
492
+ if (!Number.isSafeInteger(bytes) || bytes > TELEGRAM_MAX_REQUEST_BODY_BYTES) {
493
+ drainRequestBody(request);
494
+ throw new RequestBodyTooLargeError(TELEGRAM_MAX_REQUEST_BODY_BYTES);
495
+ }
496
+ }
497
+ }
498
+ async function parseMultipartFormDataRequest(request, contentType) {
499
+ enforceTelegramMultipartContentLength(request);
500
+ try {
501
+ const boundary = parseTelegramMultipartBoundary(contentType);
502
+ const bodyBoundary = Buffer.concat([Buffer.from("\r\n"), boundary]);
503
+ const reader = new TelegramMultipartReader(request, TELEGRAM_MAX_REQUEST_BODY_BYTES);
504
+ const fields = Object.create(null);
505
+ let retainedTextBytes = 0;
506
+ let partCount = 0;
507
+ let boundaryKind = await reader.readInitialBoundary(boundary, TELEGRAM_MAX_MULTIPART_HEADER_BYTES);
508
+ while (boundaryKind !== "final") {
509
+ partCount += 1;
510
+ if (partCount > TELEGRAM_MAX_MULTIPART_PARTS) {
511
+ throw new RequestBodyTooLargeError(TELEGRAM_MAX_MULTIPART_PARTS);
512
+ }
513
+ const disposition = parseTelegramMultipartDisposition(await reader.readUntil(Buffer.from("\r\n\r\n"), TELEGRAM_MAX_MULTIPART_HEADER_BYTES));
514
+ const digest = disposition.fileName === undefined ? undefined : createHash("sha256");
515
+ const textChunks = [];
516
+ let textLength = 0;
517
+ boundaryKind = await reader.readPartBody(bodyBoundary, (chunk) => {
518
+ if (digest) {
519
+ digest.update(chunk);
520
+ return;
521
+ }
522
+ retainedTextBytes += chunk.length;
523
+ textLength += chunk.length;
524
+ if (retainedTextBytes > TELEGRAM_MAX_MULTIPART_TEXT_BYTES) {
525
+ throw new RequestBodyTooLargeError(TELEGRAM_MAX_MULTIPART_TEXT_BYTES);
526
+ }
527
+ textChunks.push(Buffer.from(chunk));
528
+ });
529
+ fields[disposition.name] =
530
+ digest && disposition.fileName !== undefined
531
+ ? {
532
+ [TELEGRAM_MULTIPART_FILE]: true,
533
+ contentDigest: digest.digest("base64url"),
534
+ fileName: disposition.fileName,
535
+ }
536
+ : Buffer.concat(textChunks, textLength).toString("utf8");
537
+ }
538
+ await reader.consumeRemainder();
539
+ return fields;
100
540
  }
101
- const fields = {};
102
- for (const [name, value] of form) {
103
- fields[name] = typeof value === "string" ? value : value.name;
541
+ catch (error) {
542
+ drainRequestBody(request);
543
+ throw error;
104
544
  }
105
- return fields;
106
545
  }
107
546
  function requireTelegramBotPath(pathname) {
108
547
  const match = /^\/bot([^/]+)\/([A-Za-z][A-Za-z0-9_]*)$/u.exec(pathname);
@@ -135,18 +574,48 @@ function telegramChatId(value) {
135
574
  return undefined;
136
575
  }
137
576
  if (/^-?\d+$/u.test(stringValue)) {
138
- const integerValue = toIntegerValue(stringValue);
139
- return integerValue === 0 ? undefined : integerValue;
577
+ return telegramNumericChatId(stringValue);
578
+ }
579
+ return telegramUsernameChatId(stringValue);
580
+ }
581
+ function telegramNumericChatId(value) {
582
+ const stringValue = toStringValue(value);
583
+ if (!stringValue || !/^-?\d+$/u.test(stringValue)) {
584
+ return undefined;
140
585
  }
141
- return TELEGRAM_CHAT_USERNAME_PATTERN.test(stringValue) ? stringValue : undefined;
586
+ const integerValue = BigInt(stringValue);
587
+ if (integerValue === 0n ||
588
+ integerValue < -TELEGRAM_NATIVE_CHAT_ID_MAX ||
589
+ integerValue > TELEGRAM_NATIVE_CHAT_ID_MAX) {
590
+ return undefined;
591
+ }
592
+ return Number(integerValue);
593
+ }
594
+ function telegramStoredChatId(value) {
595
+ const nativeChatId = telegramNumericChatId(value);
596
+ if (nativeChatId !== undefined) {
597
+ return nativeChatId;
598
+ }
599
+ const chatId = toIntegerValue(value);
600
+ return chatId !== undefined && isTelegramUsernameChatId(chatId) ? chatId : undefined;
142
601
  }
143
602
  async function appendEvent(state, event) {
144
603
  await recordServerEvent({ event, onEvent: state.onEvent, recorderPath: state.recorderPath });
145
604
  }
605
+ function isTelegramMultipartFile(value) {
606
+ return (typeof value === "object" &&
607
+ value !== null &&
608
+ TELEGRAM_MULTIPART_FILE in value &&
609
+ value[TELEGRAM_MULTIPART_FILE] === true);
610
+ }
146
611
  function redactTelegramSecrets(body) {
147
612
  return Object.fromEntries(Object.entries(body).map(([key, value]) => [
148
613
  key,
149
- key === "secret_token" ? "<redacted>" : value,
614
+ key === "secret_token"
615
+ ? "<redacted>"
616
+ : isTelegramMultipartFile(value)
617
+ ? value.fileName
618
+ : value,
150
619
  ]));
151
620
  }
152
621
  function createBotUser(state) {
@@ -154,73 +623,242 @@ function createBotUser(state) {
154
623
  can_join_groups: true,
155
624
  can_read_all_group_messages: true,
156
625
  first_name: "Crabline",
157
- has_topics_enabled: true,
626
+ ...(state.botHasTopicsEnabled ? { has_topics_enabled: true } : {}),
158
627
  id: state.botId,
159
628
  is_bot: true,
160
629
  supports_inline_queries: false,
161
630
  username: state.botUsername,
162
631
  };
163
632
  }
633
+ function inferTelegramChatType(chatId) {
634
+ return chatId >= 0
635
+ ? "private"
636
+ : String(chatId).startsWith("-100") || isTelegramUsernameChatId(chatId)
637
+ ? "supergroup"
638
+ : "group";
639
+ }
164
640
  function createChat(chatId) {
165
- const type = typeof chatId === "string"
166
- ? "supergroup"
167
- : chatId >= 0
168
- ? "private"
169
- : String(chatId).startsWith("-100")
170
- ? "supergroup"
171
- : "group";
172
641
  return {
173
642
  id: chatId,
643
+ type: inferTelegramChatType(chatId),
644
+ };
645
+ }
646
+ function telegramDestinationUsername(value) {
647
+ const username = toStringValue(value);
648
+ return username ? canonicalizeTelegramUsername(username) : undefined;
649
+ }
650
+ function telegramStoredUsername(value) {
651
+ const username = toStringValue(value)?.trim();
652
+ if (!username) {
653
+ return undefined;
654
+ }
655
+ return canonicalizeTelegramUsername(username.startsWith("@") ? username : `@${username}`)?.slice(1);
656
+ }
657
+ function telegramChatType(value) {
658
+ return value === "channel" || value === "group" || value === "private" || value === "supergroup"
659
+ ? value
660
+ : undefined;
661
+ }
662
+ function registerTelegramChat(state, chat) {
663
+ const previous = state.chatsById.get(chat.id);
664
+ const previousUsername = telegramStoredUsername(previous?.username);
665
+ if (previousUsername && state.chatsByUsername.get(previousUsername)?.id === chat.id) {
666
+ state.chatsByUsername.delete(previousUsername);
667
+ }
668
+ const username = telegramStoredUsername(chat.username);
669
+ const stored = {
670
+ id: chat.id,
671
+ ...(chat.type === "supergroup" && chat.is_forum === true ? { is_forum: true } : {}),
672
+ ...(chat.title ? { title: chat.title } : {}),
673
+ type: chat.type,
674
+ ...(username ? { username } : {}),
675
+ };
676
+ state.chatsById.set(chat.id, stored);
677
+ if (username) {
678
+ state.chatsByUsername.set(username, stored);
679
+ }
680
+ }
681
+ function resolveTelegramChat(state, value) {
682
+ const stringValue = toStringValue(value);
683
+ if (!stringValue) {
684
+ return undefined;
685
+ }
686
+ if (/^-?\d+$/u.test(stringValue)) {
687
+ const chatId = telegramChatId(stringValue);
688
+ if (chatId === undefined) {
689
+ return undefined;
690
+ }
691
+ return state.chatsById.get(chatId) ?? createChat(chatId);
692
+ }
693
+ const username = telegramDestinationUsername(stringValue);
694
+ if (!username) {
695
+ return undefined;
696
+ }
697
+ const storedUsername = username.slice(1);
698
+ const registered = state.chatsByUsername.get(storedUsername);
699
+ if (registered) {
700
+ return registered;
701
+ }
702
+ const chatId = telegramUsernameChatId(username);
703
+ return chatId === undefined
704
+ ? undefined
705
+ : {
706
+ id: chatId,
707
+ type: inferTelegramChatType(chatId),
708
+ username: storedUsername,
709
+ };
710
+ }
711
+ function telegramChatFromRecord(value) {
712
+ if (!isJsonObject(value)) {
713
+ return undefined;
714
+ }
715
+ const id = telegramStoredChatId(value.id);
716
+ const type = telegramChatType(value.type);
717
+ if (id === undefined || !type) {
718
+ return undefined;
719
+ }
720
+ const username = telegramStoredUsername(value.username);
721
+ const title = toStringValue(value.title);
722
+ const isForum = value.is_forum === true;
723
+ if (isForum && type !== "supergroup") {
724
+ return undefined;
725
+ }
726
+ return {
727
+ id,
728
+ ...(isForum ? { is_forum: true } : {}),
729
+ ...(title ? { title } : {}),
174
730
  type,
731
+ ...(username ? { username } : {}),
175
732
  };
176
733
  }
177
- function createOutboundMessage(state, body) {
178
- const chatId = telegramChatId(body.chat_id);
179
- const text = toStringValue(body.text);
180
- if (chatId === undefined || !text) {
734
+ function registerTelegramMessageChat(state, message) {
735
+ const chat = telegramChatFromRecord(message.chat);
736
+ if (!chat) {
737
+ return;
738
+ }
739
+ registerTelegramChat(state, chat);
740
+ }
741
+ function registerTelegramUpdateChats(state, body) {
742
+ for (const field of TELEGRAM_MESSAGE_UPDATE_FIELDS) {
743
+ const message = body[field];
744
+ if (!isJsonObject(message)) {
745
+ continue;
746
+ }
747
+ registerTelegramMessageChat(state, message);
748
+ }
749
+ const callbackQuery = isJsonObject(body.callback_query) ? body.callback_query : undefined;
750
+ const callbackMessage = callbackQuery && isJsonObject(callbackQuery.message) ? callbackQuery.message : undefined;
751
+ if (callbackMessage) {
752
+ registerTelegramMessageChat(state, callbackMessage);
753
+ }
754
+ for (const field of TELEGRAM_CHAT_UPDATE_FIELDS) {
755
+ const update = body[field];
756
+ if (!isJsonObject(update)) {
757
+ continue;
758
+ }
759
+ const chat = telegramChatFromRecord(update.chat);
760
+ if (chat) {
761
+ registerTelegramChat(state, chat);
762
+ }
763
+ }
764
+ }
765
+ function telegramChatKey(chatId) {
766
+ return `number:${chatId}`;
767
+ }
768
+ function nextTelegramMessageId(state, chatId) {
769
+ return state.nextMessageIds.get(telegramChatKey(chatId)) ?? 1;
770
+ }
771
+ function takeNextTelegramMessageId(state, chatId) {
772
+ const messageId = nextTelegramMessageId(state, chatId);
773
+ if (messageId >= Number.MAX_SAFE_INTEGER) {
774
+ return undefined;
775
+ }
776
+ state.nextMessageIds.set(telegramChatKey(chatId), messageId + 1);
777
+ return messageId;
778
+ }
779
+ function createOutboundMessage(state, body, text) {
780
+ const chat = resolveTelegramChat(state, body.chat_id);
781
+ if (!chat) {
181
782
  return undefined;
182
783
  }
183
- const parsedThreadId = toIntegerValue(body.message_thread_id);
784
+ const parsedThreadId = telegramMessageThreadId(body.message_thread_id);
184
785
  if (body.message_thread_id !== undefined && parsedThreadId === undefined) {
185
786
  return undefined;
186
787
  }
187
- const threadId = parsedThreadId !== undefined && parsedThreadId > 0 ? parsedThreadId : undefined;
788
+ if (parsedThreadId !== undefined && !canTargetTelegramTopic(state, chat)) {
789
+ return undefined;
790
+ }
791
+ const messageId = takeNextTelegramMessageId(state, chat.id);
792
+ if (messageId === undefined) {
793
+ return undefined;
794
+ }
188
795
  return {
189
- chat: createChat(chatId),
796
+ chat,
190
797
  date: Math.floor(Date.now() / 1000),
191
798
  from: createBotUser(state),
192
- message_id: state.nextMessageId++,
193
- ...(threadId !== undefined ? { message_thread_id: threadId } : {}),
799
+ message_id: messageId,
800
+ ...(parsedThreadId !== undefined ? { message_thread_id: parsedThreadId } : {}),
194
801
  text,
195
802
  };
196
803
  }
197
- function createOutboundMediaMessage(state, body, mediaKind) {
198
- const chatId = telegramChatId(body.chat_id);
199
- const fileName = toStringValue(body[mediaKind]);
200
- if (chatId === undefined || !fileName) {
804
+ function createOutboundMediaMessage(state, body, mediaKind, caption) {
805
+ const chat = resolveTelegramChat(state, body.chat_id);
806
+ const mediaInput = body[mediaKind];
807
+ const multipartFile = isTelegramMultipartFile(mediaInput) ? mediaInput : undefined;
808
+ const fileName = multipartFile?.fileName ?? toStringValue(mediaInput);
809
+ if (!chat || !fileName) {
201
810
  return undefined;
202
811
  }
203
- const parsedThreadId = toIntegerValue(body.message_thread_id);
812
+ const parsedThreadId = telegramMessageThreadId(body.message_thread_id);
204
813
  if (body.message_thread_id !== undefined && parsedThreadId === undefined) {
205
814
  return undefined;
206
815
  }
207
- const threadId = parsedThreadId !== undefined && parsedThreadId > 0 ? parsedThreadId : undefined;
208
- const caption = toStringValue(body.caption);
816
+ if (parsedThreadId !== undefined && !canTargetTelegramTopic(state, chat)) {
817
+ return undefined;
818
+ }
819
+ const messageId = takeNextTelegramMessageId(state, chat.id);
820
+ if (messageId === undefined) {
821
+ return undefined;
822
+ }
209
823
  const duration = Math.max(0, toIntegerValue(body.duration) ?? 0);
210
824
  const height = Math.max(1, toIntegerValue(body.height) ?? 1);
211
825
  const width = Math.max(1, toIntegerValue(body.width) ?? 1);
826
+ const chatIdentity = Buffer.from(telegramChatKey(chat.id)).toString("base64url");
827
+ let fileIdentity;
828
+ if (multipartFile) {
829
+ fileIdentity = multipartFile.contentDigest.slice(0, 32);
830
+ }
831
+ else {
832
+ fileIdentity =
833
+ new RegExp(`^crabline-${mediaKind}-([A-Za-z0-9_-]{32})-`, "u").exec(fileName)?.[1] ??
834
+ createHash("sha256")
835
+ .update(mediaKind)
836
+ .update("\0")
837
+ .update(fileName)
838
+ .digest("base64url")
839
+ .slice(0, 32);
840
+ }
841
+ const fileId = `crabline-${mediaKind}-${fileIdentity}-${chatIdentity}-${messageId}`;
842
+ const fileUniqueId = `crabline-${mediaKind}-unique-${fileIdentity}`;
212
843
  const media = {
213
- file_id: `crabline-${mediaKind}-${state.nextMessageId}`,
844
+ file_id: fileId,
214
845
  file_name: fileName,
215
- file_unique_id: `crabline-${mediaKind}-unique-${state.nextMessageId}`,
846
+ file_unique_id: fileUniqueId,
216
847
  };
217
848
  return {
218
- chat: createChat(chatId),
849
+ chat,
219
850
  date: Math.floor(Date.now() / 1000),
220
851
  from: createBotUser(state),
221
852
  ...(caption ? { caption } : {}),
222
853
  [mediaKind]: mediaKind === "photo"
223
- ? [{ ...media, height: 1, width: 1 }]
854
+ ? [
855
+ {
856
+ file_id: media.file_id,
857
+ file_unique_id: media.file_unique_id,
858
+ height: 1,
859
+ width: 1,
860
+ },
861
+ ]
224
862
  : {
225
863
  ...media,
226
864
  ...(mediaKind === "audio"
@@ -230,35 +868,39 @@ function createOutboundMediaMessage(state, body, mediaKind) {
230
868
  : {}),
231
869
  mime_type: "application/octet-stream",
232
870
  },
233
- message_id: state.nextMessageId++,
234
- ...(threadId !== undefined ? { message_thread_id: threadId } : {}),
871
+ message_id: messageId,
872
+ ...(parsedThreadId !== undefined ? { message_thread_id: parsedThreadId } : {}),
235
873
  };
236
874
  }
237
- function createEditedMessage(state, body) {
238
- const chatId = telegramChatId(body.chat_id);
239
- const text = toStringValue(body.text);
875
+ function createEditedMessage(state, body, text) {
876
+ const chat = resolveTelegramChat(state, body.chat_id);
240
877
  const messageId = toIntegerValue(body.message_id);
241
- if (chatId === undefined || !text || messageId === undefined) {
878
+ if (!chat || messageId === undefined || messageId < 1) {
242
879
  return undefined;
243
880
  }
244
- const parsedThreadId = toIntegerValue(body.message_thread_id);
881
+ const parsedThreadId = telegramMessageThreadId(body.message_thread_id);
245
882
  if (body.message_thread_id !== undefined && parsedThreadId === undefined) {
246
883
  return undefined;
247
884
  }
248
- const threadId = parsedThreadId !== undefined && parsedThreadId > 0 ? parsedThreadId : undefined;
885
+ if (parsedThreadId !== undefined && !canTargetTelegramTopic(state, chat)) {
886
+ return undefined;
887
+ }
249
888
  return {
250
- chat: createChat(chatId),
889
+ chat,
251
890
  date: Math.floor(Date.now() / 1000),
252
891
  from: createBotUser(state),
253
892
  message_id: messageId,
254
- ...(threadId !== undefined ? { message_thread_id: threadId } : {}),
893
+ ...(parsedThreadId !== undefined ? { message_thread_id: parsedThreadId } : {}),
255
894
  text,
256
895
  };
257
896
  }
258
897
  function createInboundUpdate(state, body) {
259
- const chatId = telegramChatId(body.chatId ?? body.chat_id);
260
- const text = toStringValue(body.text);
261
- if (chatId === undefined || !text) {
898
+ const chat = resolveTelegramChat(state, body.chatId ?? body.chat_id);
899
+ const text = body.text;
900
+ if (!chat ||
901
+ typeof text !== "string" ||
902
+ text.length < 1 ||
903
+ text.length > TELEGRAM_MAX_TEXT_LENGTH) {
262
904
  return undefined;
263
905
  }
264
906
  const fromIdValue = body.fromId ?? body.from_id;
@@ -266,24 +908,43 @@ function createInboundUpdate(state, body) {
266
908
  const messageIdValue = body.messageId ?? body.message_id;
267
909
  const updateIdValue = body.updateId ?? body.update_id;
268
910
  const fromId = toIntegerValue(fromIdValue);
269
- const parsedThreadId = toIntegerValue(threadIdValue);
270
- const threadId = parsedThreadId !== undefined && parsedThreadId > 0 ? parsedThreadId : undefined;
271
- const fromUsername = toStringValue(body.fromUsername ?? body.from_username);
911
+ const threadId = telegramMessageThreadId(threadIdValue);
912
+ const fromUsernameValue = body.fromUsername ?? body.from_username;
913
+ const fromUsername = telegramStoredUsername(fromUsernameValue);
272
914
  const messageId = toIntegerValue(messageIdValue);
273
915
  const updateId = toIntegerValue(updateIdValue);
274
- if ((fromIdValue !== undefined && fromId === undefined) ||
275
- (threadIdValue !== undefined && parsedThreadId === undefined) ||
276
- (messageIdValue !== undefined && messageId === undefined) ||
277
- (updateIdValue !== undefined && updateId === undefined)) {
916
+ const explicitChatType = telegramChatType(body.chatType ?? body.chat_type);
917
+ const isForumValue = body.isForum ?? body.is_forum;
918
+ if (((body.chatType !== undefined || body.chat_type !== undefined) && !explicitChatType) ||
919
+ (isForumValue !== undefined && typeof isForumValue !== "boolean") ||
920
+ (fromIdValue !== undefined && (fromId === undefined || fromId < 1)) ||
921
+ (fromUsernameValue !== undefined && !fromUsername) ||
922
+ (threadIdValue !== undefined && threadId === undefined) ||
923
+ (messageIdValue !== undefined && (messageId === undefined || messageId < 1)) ||
924
+ (updateIdValue !== undefined && (updateId === undefined || updateId < 1))) {
278
925
  return undefined;
279
926
  }
280
927
  const entities = parseTelegramEntities(body.entities, text);
281
928
  if (body.entities !== undefined && !entities) {
282
929
  return undefined;
283
930
  }
931
+ const inboundChat = {
932
+ ...chat,
933
+ ...(explicitChatType ? { type: explicitChatType } : {}),
934
+ ...(isForumValue === true ? { is_forum: true } : {}),
935
+ };
936
+ if (isForumValue === false) {
937
+ delete inboundChat.is_forum;
938
+ }
939
+ if (inboundChat.is_forum === true && inboundChat.type !== "supergroup") {
940
+ return undefined;
941
+ }
942
+ if (threadId !== undefined && !canTargetTelegramTopic(state, inboundChat)) {
943
+ return undefined;
944
+ }
284
945
  return {
285
946
  message: {
286
- chat: createChat(chatId),
947
+ chat: inboundChat,
287
948
  date: Math.floor(Date.now() / 1000),
288
949
  from: {
289
950
  first_name: toStringValue(body.fromName ?? body.from_name) ?? "QA User",
@@ -291,7 +952,7 @@ function createInboundUpdate(state, body) {
291
952
  is_bot: false,
292
953
  ...(fromUsername ? { username: fromUsername } : {}),
293
954
  },
294
- message_id: messageId ?? state.nextMessageId,
955
+ message_id: messageId ?? nextTelegramMessageId(state, chat.id),
295
956
  ...(entities && entities.length > 0 ? { entities } : {}),
296
957
  ...(threadId !== undefined ? { message_thread_id: threadId } : {}),
297
958
  text,
@@ -313,22 +974,297 @@ function parseTelegramEntities(value, text) {
313
974
  }
314
975
  const length = toIntegerValue(entry.length);
315
976
  const offset = toIntegerValue(entry.offset);
316
- const type = toStringValue(entry.type);
977
+ const type = typeof entry.type === "string" && entry.type.length > 0 ? entry.type : undefined;
978
+ const customEmojiId = entry.custom_emoji_id;
979
+ const language = entry.language;
980
+ const url = entry.url;
981
+ const user = entry.user;
317
982
  if (length === undefined ||
318
983
  length < 1 ||
319
984
  offset === undefined ||
320
985
  offset < 0 ||
321
986
  offset + length > text.length ||
322
- !type) {
987
+ !isTelegramUtf16Boundary(text, offset) ||
988
+ !isTelegramUtf16Boundary(text, offset + length) ||
989
+ !type ||
990
+ (customEmojiId !== undefined &&
991
+ (typeof customEmojiId !== "string" || customEmojiId.length === 0)) ||
992
+ (language !== undefined && (typeof language !== "string" || language.length === 0)) ||
993
+ (url !== undefined && (typeof url !== "string" || url.length === 0)) ||
994
+ (user !== undefined && !isJsonObject(user))) {
323
995
  return undefined;
324
996
  }
325
- entities.push({ length, offset, type });
997
+ entities.push({
998
+ ...(customEmojiId !== undefined ? { custom_emoji_id: customEmojiId } : {}),
999
+ ...(language !== undefined ? { language } : {}),
1000
+ length,
1001
+ offset,
1002
+ type,
1003
+ ...(url !== undefined ? { url } : {}),
1004
+ ...(user !== undefined ? { user } : {}),
1005
+ });
326
1006
  }
327
1007
  return entities;
328
1008
  }
1009
+ function isTelegramUtf16Boundary(text, index) {
1010
+ if (index <= 0 || index >= text.length) {
1011
+ return true;
1012
+ }
1013
+ const previous = text.charCodeAt(index - 1);
1014
+ const next = text.charCodeAt(index);
1015
+ return !(previous >= 0xd800 && previous <= 0xdbff && next >= 0xdc00 && next <= 0xdfff);
1016
+ }
329
1017
  function hasTelegramMedia(value) {
330
1018
  return TELEGRAM_MEDIA_FIELDS.some((field) => value[field] !== undefined);
331
1019
  }
1020
+ function telegramMessageThreadId(value) {
1021
+ const threadId = toIntegerValue(value);
1022
+ return threadId !== undefined && threadId > 0 ? threadId : undefined;
1023
+ }
1024
+ function canTargetTelegramTopic(state, chat) {
1025
+ if (chat.type === "supergroup") {
1026
+ return chat.is_forum === true;
1027
+ }
1028
+ if (chat.type === "private") {
1029
+ return state.botHasTopicsEnabled;
1030
+ }
1031
+ return false;
1032
+ }
1033
+ function telegramMessageTargetsValidTopic(state, message) {
1034
+ if (message.message_thread_id === undefined) {
1035
+ return true;
1036
+ }
1037
+ const chat = telegramChatFromRecord(message.chat);
1038
+ if (!chat) {
1039
+ return false;
1040
+ }
1041
+ return canTargetTelegramTopic(state, chat);
1042
+ }
1043
+ function hasValidTelegramTopicDestinations(state, body) {
1044
+ const topLevelThreadId = body.messageThreadId ?? body.message_thread_id;
1045
+ if (topLevelThreadId !== undefined) {
1046
+ const chat = resolveTelegramChat(state, body.chatId ?? body.chat_id);
1047
+ const explicitType = telegramChatType(body.chatType ?? body.chat_type);
1048
+ if (!chat || ((body.chatType !== undefined || body.chat_type !== undefined) && !explicitType)) {
1049
+ return false;
1050
+ }
1051
+ const topicChat = {
1052
+ ...chat,
1053
+ ...(explicitType ? { type: explicitType } : {}),
1054
+ ...((body.isForum ?? body.is_forum) === true ? { is_forum: true } : {}),
1055
+ };
1056
+ if ((body.isForum ?? body.is_forum) === false) {
1057
+ delete topicChat.is_forum;
1058
+ }
1059
+ if ((body.isForum !== undefined || body.is_forum !== undefined) &&
1060
+ typeof (body.isForum ?? body.is_forum) !== "boolean") {
1061
+ return false;
1062
+ }
1063
+ if (!canTargetTelegramTopic(state, topicChat)) {
1064
+ return false;
1065
+ }
1066
+ }
1067
+ for (const field of TELEGRAM_MESSAGE_UPDATE_FIELDS) {
1068
+ const message = body[field];
1069
+ if (isJsonObject(message) && !telegramMessageTargetsValidTopic(state, message)) {
1070
+ return false;
1071
+ }
1072
+ }
1073
+ const callbackQuery = isJsonObject(body.callback_query) ? body.callback_query : undefined;
1074
+ const callbackMessage = callbackQuery && isJsonObject(callbackQuery.message) ? callbackQuery.message : undefined;
1075
+ return !callbackMessage || telegramMessageTargetsValidTopic(state, callbackMessage);
1076
+ }
1077
+ function hasValidTelegramMessageThreadIds(body) {
1078
+ const values = [
1079
+ body.messageThreadId,
1080
+ body.message_thread_id,
1081
+ ...TELEGRAM_MESSAGE_UPDATE_FIELDS.flatMap((field) => {
1082
+ const message = body[field];
1083
+ return isJsonObject(message) ? [message.message_thread_id] : [];
1084
+ }),
1085
+ ];
1086
+ const callbackQuery = isJsonObject(body.callback_query) ? body.callback_query : undefined;
1087
+ const callbackMessage = callbackQuery && isJsonObject(callbackQuery.message) ? callbackQuery.message : undefined;
1088
+ if (callbackMessage) {
1089
+ values.push(callbackMessage.message_thread_id);
1090
+ }
1091
+ return values.every((value) => value === undefined || telegramMessageThreadId(value) !== undefined);
1092
+ }
1093
+ function explicitTelegramId(body, names, additionalValues = [], options = {}) {
1094
+ const values = [
1095
+ ...names.flatMap((name) => (body[name] === undefined ? [] : [body[name]])),
1096
+ ...additionalValues.filter((value) => value !== undefined),
1097
+ ];
1098
+ if (values.length === 0) {
1099
+ return { present: false };
1100
+ }
1101
+ const parsed = values.map(toIntegerValue);
1102
+ const minimum = options.allowZero ? 0 : 1;
1103
+ if (parsed.some((value) => value === undefined || value < minimum || value >= Number.MAX_SAFE_INTEGER) ||
1104
+ new Set(parsed).size !== 1) {
1105
+ return undefined;
1106
+ }
1107
+ return { present: true, value: parsed[0] };
1108
+ }
1109
+ function explicitTelegramMessageIdValues(body) {
1110
+ return TELEGRAM_NEW_MESSAGE_UPDATE_FIELDS.flatMap((field) => {
1111
+ const message = body[field];
1112
+ return isJsonObject(message) ? [message.message_id] : [];
1113
+ });
1114
+ }
1115
+ function referencedTelegramMessageIdValues(body) {
1116
+ const values = TELEGRAM_MESSAGE_REFERENCE_FIELDS.flatMap((field) => {
1117
+ const message = body[field];
1118
+ return isJsonObject(message) ? [message.message_id] : [];
1119
+ });
1120
+ const callbackQuery = isJsonObject(body.callback_query) ? body.callback_query : undefined;
1121
+ const callbackMessage = callbackQuery && isJsonObject(callbackQuery.message) ? callbackQuery.message : undefined;
1122
+ return callbackMessage ? [...values, callbackMessage.message_id] : values;
1123
+ }
1124
+ function explicitTelegramMessageChatId(body) {
1125
+ const chatIds = [];
1126
+ if (body.messageId !== undefined || body.message_id !== undefined) {
1127
+ const chatId = telegramChatId(body.chatId ?? body.chat_id);
1128
+ if (chatId === undefined) {
1129
+ return false;
1130
+ }
1131
+ chatIds.push(chatId);
1132
+ }
1133
+ for (const field of TELEGRAM_MESSAGE_UPDATE_FIELDS) {
1134
+ const message = body[field];
1135
+ if (!isJsonObject(message) || message.message_id === undefined) {
1136
+ continue;
1137
+ }
1138
+ const chat = isJsonObject(message.chat) ? message.chat : undefined;
1139
+ const chatId = telegramStoredChatId(chat?.id);
1140
+ if (chatId === undefined) {
1141
+ return false;
1142
+ }
1143
+ chatIds.push(chatId);
1144
+ }
1145
+ const callbackQuery = isJsonObject(body.callback_query) ? body.callback_query : undefined;
1146
+ const callbackMessage = callbackQuery && isJsonObject(callbackQuery.message) ? callbackQuery.message : undefined;
1147
+ if (callbackMessage?.message_id !== undefined) {
1148
+ const chat = isJsonObject(callbackMessage.chat) ? callbackMessage.chat : undefined;
1149
+ const chatId = telegramStoredChatId(chat?.id);
1150
+ if (chatId === undefined) {
1151
+ return false;
1152
+ }
1153
+ chatIds.push(chatId);
1154
+ }
1155
+ if (chatIds.length === 0) {
1156
+ return undefined;
1157
+ }
1158
+ const firstChatId = chatIds[0];
1159
+ return chatIds.every((chatId) => telegramChatKey(chatId) === telegramChatKey(firstChatId))
1160
+ ? firstChatId
1161
+ : false;
1162
+ }
1163
+ function readTelegramTextField(value, field, options) {
1164
+ if (value === undefined) {
1165
+ return options.required ? telegramError(`Bad Request: ${field} is required`) : undefined;
1166
+ }
1167
+ if (typeof value !== "string") {
1168
+ return telegramError(`Bad Request: ${field} must be a string`);
1169
+ }
1170
+ const parsedLength = telegramTextLength(value, options.parseMode);
1171
+ if (options.required && parsedLength === 0) {
1172
+ return telegramError(`Bad Request: ${field} must not be empty`);
1173
+ }
1174
+ const maxLength = field === "text" ? TELEGRAM_MAX_TEXT_LENGTH : TELEGRAM_MAX_CAPTION_LENGTH;
1175
+ if (parsedLength > maxLength) {
1176
+ return telegramError(`Bad Request: ${field} is too long`);
1177
+ }
1178
+ return value;
1179
+ }
1180
+ function telegramTextLength(value, parseMode) {
1181
+ const normalizedParseMode = typeof parseMode === "string" ? parseMode.toLowerCase() : undefined;
1182
+ if (normalizedParseMode === "html") {
1183
+ return telegramHtmlText(value).length;
1184
+ }
1185
+ if (normalizedParseMode === "markdown" || normalizedParseMode === "markdownv2") {
1186
+ return telegramMarkdownText(value, normalizedParseMode === "markdownv2").length;
1187
+ }
1188
+ return value.length;
1189
+ }
1190
+ function telegramHtmlText(value) {
1191
+ return value
1192
+ .replace(TELEGRAM_HTML_TAG_PATTERN, "")
1193
+ .replace(TELEGRAM_HTML_ENTITY_PATTERN, (entity) => {
1194
+ const normalized = entity.toLowerCase();
1195
+ if (normalized === "&amp;") {
1196
+ return "&";
1197
+ }
1198
+ if (normalized === "&gt;") {
1199
+ return ">";
1200
+ }
1201
+ if (normalized === "&lt;") {
1202
+ return "<";
1203
+ }
1204
+ if (normalized === "&quot;") {
1205
+ return '"';
1206
+ }
1207
+ const radix = normalized.startsWith("&#x") ? 16 : 10;
1208
+ const digits = normalized.slice(radix === 16 ? 3 : 2, -1);
1209
+ const codePoint = Number.parseInt(digits, radix);
1210
+ return Number.isSafeInteger(codePoint) && codePoint <= 0x10ffff
1211
+ ? String.fromCodePoint(codePoint)
1212
+ : entity;
1213
+ });
1214
+ }
1215
+ function protectTelegramMarkdownEscapes(value, versionTwo) {
1216
+ let protectedText = "";
1217
+ for (let index = 0; index < value.length; index += 1) {
1218
+ const character = value[index];
1219
+ const escapedCharacter = value[index + 1];
1220
+ if (character === "\\" &&
1221
+ escapedCharacter !== undefined &&
1222
+ (versionTwo
1223
+ ? escapedCharacter.charCodeAt(0) >= 1 && escapedCharacter.charCodeAt(0) <= 0x7e
1224
+ : "*_[]`".includes(escapedCharacter))) {
1225
+ protectedText += "\0";
1226
+ index += 1;
1227
+ continue;
1228
+ }
1229
+ protectedText += character;
1230
+ }
1231
+ return protectedText;
1232
+ }
1233
+ function telegramMarkdownLiteralMask(value) {
1234
+ return "\0".repeat(value.length);
1235
+ }
1236
+ function stripTelegramMarkdownExpandableMarker(line) {
1237
+ if (!line.endsWith("||")) {
1238
+ return line;
1239
+ }
1240
+ let markers = 0;
1241
+ for (let index = line.indexOf("||"); index >= 0; index = line.indexOf("||", index + 2)) {
1242
+ markers += 1;
1243
+ }
1244
+ return markers % 2 === 1 ? line.slice(0, -2) : line;
1245
+ }
1246
+ function telegramMarkdownText(value, versionTwo) {
1247
+ let parsed = protectTelegramMarkdownEscapes(value, versionTwo)
1248
+ .replace(/```(?:[^\n]*\n)?([\s\S]*?)```/gu, (_match, content) => telegramMarkdownLiteralMask(content))
1249
+ .replace(/`([^`\n]*)`/gu, (_match, content) => telegramMarkdownLiteralMask(content))
1250
+ .replace(/!\[([^\]\n]+)\]\(tg:\/\/(?:emoji\?id=\d+|time\?unix=-?\d+(?:&format=[rwdDtT]+)?)\)/gu, "$1")
1251
+ .replace(/\[([^\]\n]+)\]\((?:\\.|[^)\n])*\)/gu, "$1");
1252
+ if (versionTwo) {
1253
+ parsed = parsed
1254
+ .replace(/^(?:\*\*)?>[^\n]*$/gmu, stripTelegramMarkdownExpandableMarker)
1255
+ .replace(/^\*\*(?=>)/gmu, "")
1256
+ .replace(/^>/gmu, "")
1257
+ .replace(/\|\|([\s\S]*?)\|\|/gu, "$1")
1258
+ .replace(/__([\s\S]*?)__/gu, "$1")
1259
+ .replace(/~([\s\S]*?)~/gu, "$1");
1260
+ }
1261
+ let previous;
1262
+ do {
1263
+ previous = parsed;
1264
+ parsed = parsed.replace(/\*([\s\S]*?)\*/gu, "$1").replace(/_([\s\S]*?)_/gu, "$1");
1265
+ } while (parsed !== previous);
1266
+ return parsed;
1267
+ }
332
1268
  function hasValidExplicitTelegramIdentities(body) {
333
1269
  return ([body.chatId, body.chat_id].every((value) => value === undefined || telegramChatId(value) !== undefined) &&
334
1270
  [
@@ -336,8 +1272,6 @@ function hasValidExplicitTelegramIdentities(body) {
336
1272
  body.from_id,
337
1273
  body.messageId,
338
1274
  body.message_id,
339
- body.messageThreadId,
340
- body.message_thread_id,
341
1275
  body.updateId,
342
1276
  body.update_id,
343
1277
  ].every((value) => value === undefined || toIntegerValue(value) !== undefined));
@@ -348,14 +1282,17 @@ function isValidIgnoredTelegramUpdate(body) {
348
1282
  if (isJsonObject(body.message)) {
349
1283
  const message = body.message;
350
1284
  const chat = isJsonObject(message.chat) ? message.chat : undefined;
1285
+ const messageId = toIntegerValue(message.message_id);
351
1286
  if (updateId === undefined ||
1287
+ updateId < 1 ||
352
1288
  !chat ||
353
- telegramChatId(chat.id) === undefined ||
354
- toIntegerValue(message.message_id) === undefined ||
1289
+ telegramStoredChatId(chat.id) === undefined ||
1290
+ messageId === undefined ||
1291
+ messageId < 0 ||
355
1292
  (message.from !== undefined &&
356
- (!isJsonObject(message.from) || toIntegerValue(message.from.id) === undefined)) ||
1293
+ (!isJsonObject(message.from) || (toIntegerValue(message.from.id) ?? 0) < 1)) ||
357
1294
  (message.message_thread_id !== undefined &&
358
- toIntegerValue(message.message_thread_id) === undefined)) {
1295
+ telegramMessageThreadId(message.message_thread_id) === undefined)) {
359
1296
  return false;
360
1297
  }
361
1298
  return toStringValue(message.text) === undefined;
@@ -378,13 +1315,55 @@ async function handleTelegramAdminInbound(params) {
378
1315
  });
379
1316
  await previousAdmission;
380
1317
  try {
381
- const nextMessageId = params.state.nextMessageId;
382
1318
  const nextUpdateId = params.state.nextUpdateId;
1319
+ const explicitMessageId = explicitTelegramId(params.body, ["messageId", "message_id"], explicitTelegramMessageIdValues(params.body), { allowZero: true });
1320
+ const explicitMessageChatId = explicitTelegramMessageChatId(params.body);
1321
+ const explicitUpdateId = explicitTelegramId(params.body, ["updateId", "update_id"]);
1322
+ const referencedMessageId = explicitTelegramId({}, [], referencedTelegramMessageIdValues(params.body));
1323
+ const topLevelChatId = resolveTelegramChat(params.state, params.body.chatId ?? params.body.chat_id)?.id;
1324
+ const messageChatId = explicitMessageChatId === false ? undefined : (explicitMessageChatId ?? topLevelChatId);
1325
+ const messageChatKey = messageChatId === undefined ? undefined : telegramChatKey(messageChatId);
1326
+ const previousNextMessageId = messageChatKey === undefined ? undefined : params.state.nextMessageIds.get(messageChatKey);
1327
+ const nextMessageId = messageChatId === undefined ? undefined : nextTelegramMessageId(params.state, messageChatId);
1328
+ if (!hasValidTelegramMessageThreadIds(params.body) ||
1329
+ !hasValidTelegramTopicDestinations(params.state, params.body) ||
1330
+ explicitMessageId === undefined ||
1331
+ explicitMessageChatId === false ||
1332
+ explicitUpdateId === undefined ||
1333
+ referencedMessageId === undefined ||
1334
+ (explicitMessageId.present &&
1335
+ explicitMessageId.value > 0 &&
1336
+ (nextMessageId === undefined || explicitMessageId.value < nextMessageId)) ||
1337
+ (explicitUpdateId.present && explicitUpdateId.value < nextUpdateId)) {
1338
+ return telegramError("Bad Request: explicit IDs must be valid and monotonic");
1339
+ }
383
1340
  const update = createInboundUpdate(params.state, params.body);
384
- params.state.nextMessageId = nextMessageId;
385
1341
  params.state.nextUpdateId = nextUpdateId;
1342
+ if (update &&
1343
+ ((!explicitMessageId.present &&
1344
+ (nextMessageId ?? Number.MAX_SAFE_INTEGER) >= Number.MAX_SAFE_INTEGER) ||
1345
+ (!explicitUpdateId.present && nextUpdateId >= Number.MAX_SAFE_INTEGER))) {
1346
+ return telegramError("Bad Request: generated ID space is exhausted");
1347
+ }
386
1348
  if (!update) {
387
1349
  if (isValidIgnoredTelegramUpdate(params.body)) {
1350
+ if (messageChatKey !== undefined) {
1351
+ const observedNextMessageIds = [
1352
+ ...(explicitMessageId.present && explicitMessageId.value > 0
1353
+ ? [explicitMessageId.value + 1]
1354
+ : []),
1355
+ ...(referencedMessageId.present && referencedMessageId.value > 0
1356
+ ? [referencedMessageId.value + 1]
1357
+ : []),
1358
+ ];
1359
+ if (observedNextMessageIds.length > 0) {
1360
+ params.state.nextMessageIds.set(messageChatKey, Math.max(params.state.nextMessageIds.get(messageChatKey) ?? 1, ...observedNextMessageIds));
1361
+ }
1362
+ }
1363
+ if (explicitUpdateId.present) {
1364
+ params.state.nextUpdateId = explicitUpdateId.value + 1;
1365
+ }
1366
+ registerTelegramUpdateChats(params.state, params.body);
388
1367
  return jsonResponse({ ok: true });
389
1368
  }
390
1369
  return telegramError("Bad Request: chatId and text are required");
@@ -392,9 +1371,12 @@ async function handleTelegramAdminInbound(params) {
392
1371
  if (params.state.updates.length >= params.state.maxPendingInboundEvents) {
393
1372
  return telegramError(`Too Many Requests: pending inbound queue is full (${params.state.maxPendingInboundEvents} updates)`, 429);
394
1373
  }
395
- params.state.nextMessageId = Math.max(params.state.nextMessageId, update.message.message_id + 1);
1374
+ const updateChatKey = telegramChatKey(update.message.chat.id);
1375
+ params.state.nextMessageIds.set(updateChatKey, Math.max(params.state.nextMessageIds.get(updateChatKey) ?? 1, update.message.message_id + 1, ...(referencedMessageId.present && referencedMessageId.value > 0
1376
+ ? [referencedMessageId.value + 1]
1377
+ : [])));
396
1378
  params.state.nextUpdateId = Math.max(params.state.nextUpdateId, update.update_id + 1);
397
- const reservedNextMessageId = params.state.nextMessageId;
1379
+ const reservedNextMessageId = params.state.nextMessageIds.get(updateChatKey);
398
1380
  const reservedNextUpdateId = params.state.nextUpdateId;
399
1381
  try {
400
1382
  await appendEvent(params.state, {
@@ -407,14 +1389,20 @@ async function handleTelegramAdminInbound(params) {
407
1389
  });
408
1390
  }
409
1391
  catch (error) {
410
- if (params.state.nextMessageId === reservedNextMessageId) {
411
- params.state.nextMessageId = nextMessageId;
1392
+ if (params.state.nextMessageIds.get(updateChatKey) === reservedNextMessageId) {
1393
+ if (previousNextMessageId === undefined) {
1394
+ params.state.nextMessageIds.delete(updateChatKey);
1395
+ }
1396
+ else {
1397
+ params.state.nextMessageIds.set(updateChatKey, previousNextMessageId);
1398
+ }
412
1399
  }
413
1400
  if (params.state.nextUpdateId === reservedNextUpdateId) {
414
1401
  params.state.nextUpdateId = nextUpdateId;
415
1402
  }
416
1403
  throw error;
417
1404
  }
1405
+ registerTelegramChat(params.state, update.message.chat);
418
1406
  params.state.updates.push(update);
419
1407
  params.state.updates.sort((left, right) => left.update_id - right.update_id);
420
1408
  if (params.state.webhook) {
@@ -690,14 +1678,29 @@ async function handleTelegramApi(params) {
690
1678
  return telegramOk(createBotUser(params.state));
691
1679
  case "setmycommands":
692
1680
  case "deletemycommands":
693
- case "sendchataction":
694
1681
  case "answercallbackquery":
695
1682
  case "deletemessage":
696
1683
  case "pinchatmessage":
697
1684
  case "unpinchatmessage":
698
1685
  case "setmessagereaction":
699
- case "editforumtopic":
700
1686
  return telegramOk(true);
1687
+ case "sendchataction": {
1688
+ if (params.body.message_thread_id === undefined) {
1689
+ return telegramOk(true);
1690
+ }
1691
+ const chat = resolveTelegramChat(params.state, params.body.chat_id);
1692
+ const messageThreadId = telegramMessageThreadId(params.body.message_thread_id);
1693
+ return chat && messageThreadId !== undefined && canTargetTelegramTopic(params.state, chat)
1694
+ ? telegramOk(true)
1695
+ : telegramError("Bad Request: chat is not a forum or message_thread_id is invalid");
1696
+ }
1697
+ case "editforumtopic": {
1698
+ const chat = resolveTelegramChat(params.state, params.body.chat_id);
1699
+ const messageThreadId = telegramMessageThreadId(params.body.message_thread_id);
1700
+ return chat && messageThreadId !== undefined && canTargetTelegramTopic(params.state, chat)
1701
+ ? telegramOk(true)
1702
+ : telegramError("Bad Request: chat is not a forum or message_thread_id is invalid");
1703
+ }
701
1704
  case "setwebhook": {
702
1705
  if (params.body.url === "") {
703
1706
  await replaceTelegramWebhook(params.state, undefined, toBooleanValue(params.body.drop_pending_updates));
@@ -717,6 +1720,15 @@ async function handleTelegramApi(params) {
717
1720
  if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
718
1721
  return telegramError("Bad Request: bad webhook URL");
719
1722
  }
1723
+ const secretToken = params.body.secret_token === undefined
1724
+ ? undefined
1725
+ : typeof params.body.secret_token === "string"
1726
+ ? params.body.secret_token
1727
+ : null;
1728
+ if (secretToken === null ||
1729
+ (secretToken !== undefined && !TELEGRAM_WEBHOOK_SECRET_PATTERN.test(secretToken))) {
1730
+ return telegramError("Bad Request: invalid secret token");
1731
+ }
720
1732
  if (params.state.activeWebhookValidations.size >= MAX_ACTIVE_TELEGRAM_WEBHOOK_VALIDATIONS) {
721
1733
  return telegramError("Too Many Requests: too many webhook validations", 429);
722
1734
  }
@@ -728,10 +1740,6 @@ async function handleTelegramApi(params) {
728
1740
  if (target instanceof Response) {
729
1741
  return target;
730
1742
  }
731
- const secretToken = toStringValue(params.body.secret_token);
732
- if (secretToken && !/^[A-Za-z0-9_-]{1,256}$/u.test(secretToken)) {
733
- return telegramError("Bad Request: invalid secret token");
734
- }
735
1743
  await replaceTelegramWebhook(params.state, {
736
1744
  ...(secretToken ? { secretToken } : {}),
737
1745
  url: parsedUrl.href,
@@ -753,20 +1761,50 @@ async function handleTelegramApi(params) {
753
1761
  pending_update_count: params.state.updates.length,
754
1762
  url: params.state.webhook?.url ?? "",
755
1763
  });
756
- case "createforumtopic":
1764
+ case "createforumtopic": {
1765
+ const chat = resolveTelegramChat(params.state, params.body.chat_id);
1766
+ if (!chat) {
1767
+ return telegramError("Bad Request: chat_id is required");
1768
+ }
1769
+ if (!canTargetTelegramTopic(params.state, chat)) {
1770
+ return telegramError("Bad Request: chat is not a forum");
1771
+ }
1772
+ const messageThreadId = takeNextTelegramMessageId(params.state, chat.id);
1773
+ if (messageThreadId === undefined) {
1774
+ return telegramError("Bad Request: generated ID space is exhausted");
1775
+ }
757
1776
  return telegramOk({
758
1777
  icon_color: 0x6fb9f0,
759
- message_thread_id: params.state.nextMessageId++,
1778
+ message_thread_id: messageThreadId,
760
1779
  name: toStringValue(params.body.name) ?? "Crabline Topic",
761
1780
  });
1781
+ }
762
1782
  case "editmessagetext": {
763
- const message = createEditedMessage(params.state, params.body);
1783
+ const text = readTelegramTextField(params.body.text, "text", {
1784
+ parseMode: params.body.parse_mode,
1785
+ required: true,
1786
+ });
1787
+ if (text instanceof Response) {
1788
+ return text;
1789
+ }
1790
+ const message = createEditedMessage(params.state, params.body, text);
764
1791
  return message
765
1792
  ? telegramOk(message)
766
1793
  : telegramError("Bad Request: chat_id, message_id, and text are required");
767
1794
  }
768
1795
  case "sendmessage": {
769
- const message = createOutboundMessage(params.state, params.body);
1796
+ const text = readTelegramTextField(params.body.text, "text", {
1797
+ parseMode: params.body.parse_mode,
1798
+ required: true,
1799
+ });
1800
+ if (text instanceof Response) {
1801
+ return text;
1802
+ }
1803
+ const chat = resolveTelegramChat(params.state, params.body.chat_id);
1804
+ if (chat && nextTelegramMessageId(params.state, chat.id) >= Number.MAX_SAFE_INTEGER) {
1805
+ return telegramError("Bad Request: generated ID space is exhausted");
1806
+ }
1807
+ const message = createOutboundMessage(params.state, params.body, text);
770
1808
  return message
771
1809
  ? telegramOk(message)
772
1810
  : telegramError("Bad Request: chat_id and text are required");
@@ -777,7 +1815,18 @@ async function handleTelegramApi(params) {
777
1815
  case "sendphoto":
778
1816
  case "sendvideo": {
779
1817
  const mediaKind = method.slice("send".length);
780
- const message = createOutboundMediaMessage(params.state, params.body, mediaKind);
1818
+ const caption = readTelegramTextField(params.body.caption, "caption", {
1819
+ parseMode: params.body.parse_mode,
1820
+ required: false,
1821
+ });
1822
+ if (caption instanceof Response) {
1823
+ return caption;
1824
+ }
1825
+ const chat = resolveTelegramChat(params.state, params.body.chat_id);
1826
+ if (chat && nextTelegramMessageId(params.state, chat.id) >= Number.MAX_SAFE_INTEGER) {
1827
+ return telegramError("Bad Request: generated ID space is exhausted");
1828
+ }
1829
+ const message = createOutboundMediaMessage(params.state, params.body, mediaKind, caption);
781
1830
  return message
782
1831
  ? telegramOk(message)
783
1832
  : telegramError(`Bad Request: chat_id and ${mediaKind} are required`);
@@ -958,35 +2007,70 @@ async function handleRequest(params) {
958
2007
  }
959
2008
  return response;
960
2009
  }
961
- await appendEvent(params.state, event);
962
- return await handleTelegramApi({
2010
+ const response = await handleTelegramApi({
963
2011
  body,
964
2012
  method: botPath.method,
965
2013
  request: params.request,
966
2014
  state: params.state,
967
2015
  });
2016
+ event.accepted = response.ok;
2017
+ await appendEvent(params.state, event);
2018
+ return response;
2019
+ }
2020
+ async function serveRequest(params) {
2021
+ let fetchResponse;
2022
+ try {
2023
+ fetchResponse = await handleRequest({ request: params.request, state: params.state });
2024
+ }
2025
+ catch (error) {
2026
+ fetchResponse =
2027
+ error instanceof InvalidJsonBodyError
2028
+ ? telegramError("Bad Request: can't parse JSON object")
2029
+ : error instanceof RequestBodyTooLargeError
2030
+ ? telegramError("Request Entity Too Large", 413)
2031
+ : jsonResponse({ error: "internal server error", ok: false }, 500);
2032
+ }
2033
+ try {
2034
+ await writeResponse(params.response, fetchResponse);
2035
+ }
2036
+ catch {
2037
+ // A disconnected client cannot receive an error fallback. End delivery here
2038
+ // so the Node request callback never leaks an unhandled rejection.
2039
+ params.response.destroy();
2040
+ }
968
2041
  }
969
2042
  export async function startTelegramServer(params = {}) {
970
2043
  const host = params.host ?? "127.0.0.1";
2044
+ if (params.botHasTopicsEnabled !== undefined && typeof params.botHasTopicsEnabled !== "boolean") {
2045
+ throw new Error("botHasTopicsEnabled must be a boolean.");
2046
+ }
971
2047
  const botId = params.botId ?? 424242;
972
2048
  if (!Number.isSafeInteger(botId) || botId < 1) {
973
2049
  throw new Error("botId must be a positive safe integer.");
974
2050
  }
2051
+ const botUsernameValue = params.botUsername ?? "crabline_bot";
2052
+ const botUsername = botUsernameValue.trim().replace(/^@/u, "").toLowerCase();
2053
+ if (!TELEGRAM_BOT_USERNAME_PATTERN.test(`@${botUsername}`)) {
2054
+ throw new Error("botUsername must be a valid 5-32 character Telegram username.");
2055
+ }
975
2056
  const state = {
976
2057
  activeUpdatePoll: undefined,
977
2058
  activeWebhookDeliveries: new Set(),
978
2059
  activeWebhookValidations: new Set(),
979
2060
  adminToken: params.adminToken ?? randomBytes(24).toString("hex"),
980
2061
  allowLoopbackHttpWebhook: isLoopbackHost(host),
2062
+ botHasTopicsEnabled: params.botHasTopicsEnabled ?? true,
981
2063
  botId,
982
2064
  botToken: params.botToken ??
983
2065
  (isLoopbackHost(host)
984
2066
  ? "424242:crabline-telegram-token"
985
2067
  : `${botId}:${randomBytes(26).toString("base64url")}`),
986
- botUsername: params.botUsername ?? "crabline_bot",
2068
+ botUsername,
2069
+ chatsById: new Map(),
2070
+ chatsByUsername: new Map(),
987
2071
  inboundAdmission: Promise.resolve(),
988
2072
  maxPendingInboundEvents: resolveMaxPendingInboundEvents(params.maxPendingInboundEvents),
989
- nextMessageId: 1,
2073
+ nextMessageIds: new Map(),
990
2074
  nextUpdateId: 1,
991
2075
  onEvent: params.onEvent,
992
2076
  recorderPath: params.recorderPath ?? path.resolve(".crabline", "servers", "telegram.jsonl"),
@@ -1001,17 +2085,8 @@ export async function startTelegramServer(params = {}) {
1001
2085
  webhookRetryUpdateId: undefined,
1002
2086
  };
1003
2087
  const port = params.port ?? 0;
1004
- const server = createServer(async (request, response) => {
1005
- try {
1006
- await writeResponse(response, await handleRequest({ request, state }));
1007
- }
1008
- catch (error) {
1009
- await writeResponse(response, error instanceof InvalidJsonBodyError
1010
- ? telegramError("Bad Request: can't parse JSON object")
1011
- : error instanceof RequestBodyTooLargeError
1012
- ? telegramError("Request Entity Too Large", 413)
1013
- : jsonResponse({ error: "internal server error", ok: false }, 500));
1014
- }
2088
+ const server = createServer((request, response) => {
2089
+ void serveRequest({ request, response, state });
1015
2090
  });
1016
2091
  await new Promise((resolve, reject) => {
1017
2092
  server.once("error", reject);