@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.
- package/README.md +99 -12
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +264 -50
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +12 -2
- package/dist/src/openclaw/artifact-generation.js +436 -15
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/matrix.js +21 -4
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +13 -8
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +15 -6
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +116 -23
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +45 -19
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +32 -16
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +8 -3
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +40 -3
- package/dist/src/openclaw/private-file.js +2070 -95
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/shared.d.ts +1 -0
- package/dist/src/openclaw/shared.js +48 -13
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +9 -0
- package/dist/src/openclaw/smoke-lock.js +696 -187
- package/dist/src/openclaw/smoke-lock.js.map +1 -1
- package/dist/src/openclaw.js +352 -29
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +532 -42
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1241 -22
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +259 -51
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1193 -118
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -0
- package/dist/src/servers/whatsapp.js +149 -24
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +185 -43
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- 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
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
body,
|
|
94
|
-
headers: { "content-type": contentType },
|
|
95
|
-
method: "POST",
|
|
96
|
-
}).formData();
|
|
237
|
+
return decodeURIComponent(extended[2] ?? "");
|
|
97
238
|
}
|
|
98
|
-
catch
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
796
|
+
chat,
|
|
190
797
|
date: Math.floor(Date.now() / 1000),
|
|
191
798
|
from: createBotUser(state),
|
|
192
|
-
message_id:
|
|
193
|
-
...(
|
|
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
|
|
199
|
-
const
|
|
200
|
-
|
|
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 =
|
|
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
|
-
|
|
208
|
-
|
|
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:
|
|
844
|
+
file_id: fileId,
|
|
214
845
|
file_name: fileName,
|
|
215
|
-
file_unique_id:
|
|
846
|
+
file_unique_id: fileUniqueId,
|
|
216
847
|
};
|
|
217
848
|
return {
|
|
218
|
-
chat
|
|
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
|
-
? [
|
|
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:
|
|
234
|
-
...(
|
|
871
|
+
message_id: messageId,
|
|
872
|
+
...(parsedThreadId !== undefined ? { message_thread_id: parsedThreadId } : {}),
|
|
235
873
|
};
|
|
236
874
|
}
|
|
237
|
-
function createEditedMessage(state, body) {
|
|
238
|
-
const
|
|
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 (
|
|
878
|
+
if (!chat || messageId === undefined || messageId < 1) {
|
|
242
879
|
return undefined;
|
|
243
880
|
}
|
|
244
|
-
const parsedThreadId =
|
|
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
|
-
|
|
885
|
+
if (parsedThreadId !== undefined && !canTargetTelegramTopic(state, chat)) {
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
249
888
|
return {
|
|
250
|
-
chat
|
|
889
|
+
chat,
|
|
251
890
|
date: Math.floor(Date.now() / 1000),
|
|
252
891
|
from: createBotUser(state),
|
|
253
892
|
message_id: messageId,
|
|
254
|
-
...(
|
|
893
|
+
...(parsedThreadId !== undefined ? { message_thread_id: parsedThreadId } : {}),
|
|
255
894
|
text,
|
|
256
895
|
};
|
|
257
896
|
}
|
|
258
897
|
function createInboundUpdate(state, body) {
|
|
259
|
-
const
|
|
260
|
-
const text =
|
|
261
|
-
if (
|
|
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
|
|
270
|
-
const
|
|
271
|
-
const fromUsername =
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
(
|
|
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:
|
|
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.
|
|
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 =
|
|
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
|
-
!
|
|
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({
|
|
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 === "&") {
|
|
1196
|
+
return "&";
|
|
1197
|
+
}
|
|
1198
|
+
if (normalized === ">") {
|
|
1199
|
+
return ">";
|
|
1200
|
+
}
|
|
1201
|
+
if (normalized === "<") {
|
|
1202
|
+
return "<";
|
|
1203
|
+
}
|
|
1204
|
+
if (normalized === """) {
|
|
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
|
-
|
|
354
|
-
|
|
1289
|
+
telegramStoredChatId(chat.id) === undefined ||
|
|
1290
|
+
messageId === undefined ||
|
|
1291
|
+
messageId < 0 ||
|
|
355
1292
|
(message.from !== undefined &&
|
|
356
|
-
(!isJsonObject(message.from) || toIntegerValue(message.from.id)
|
|
1293
|
+
(!isJsonObject(message.from) || (toIntegerValue(message.from.id) ?? 0) < 1)) ||
|
|
357
1294
|
(message.message_thread_id !== undefined &&
|
|
358
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
411
|
-
|
|
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:
|
|
1778
|
+
message_thread_id: messageThreadId,
|
|
760
1779
|
name: toStringValue(params.body.name) ?? "Crabline Topic",
|
|
761
1780
|
});
|
|
1781
|
+
}
|
|
762
1782
|
case "editmessagetext": {
|
|
763
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2068
|
+
botUsername,
|
|
2069
|
+
chatsById: new Map(),
|
|
2070
|
+
chatsByUsername: new Map(),
|
|
987
2071
|
inboundAdmission: Promise.resolve(),
|
|
988
2072
|
maxPendingInboundEvents: resolveMaxPendingInboundEvents(params.maxPendingInboundEvents),
|
|
989
|
-
|
|
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(
|
|
1005
|
-
|
|
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);
|