@openclaw/crabline 0.1.11 → 0.1.13
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 +137 -23
- 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 +281 -52
- 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/index.d.ts +12 -10
- package/dist/src/index.js +6 -5
- package/dist/src/index.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 +7 -6
- package/dist/src/openclaw/artifact-generation.js +385 -41
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/discord.d.ts +1 -0
- package/dist/src/openclaw/bridges/discord.js +149 -0
- package/dist/src/openclaw/bridges/discord.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +22 -5
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +14 -9
- 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 +16 -7
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +117 -24
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +46 -20
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +33 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +9 -4
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.js +3 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +38 -3
- package/dist/src/openclaw/private-file.js +2008 -87
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
- package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
- package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +10 -17
- package/dist/src/openclaw/shared.js +53 -19
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw.d.ts +3 -5
- package/dist/src/openclaw.js +245 -60
- 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 +521 -41
- 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/discord.d.ts +40 -0
- package/dist/src/servers/discord.js +842 -0
- package/dist/src/servers/discord.js.map +1 -0
- 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/index.d.ts +10 -4
- package/dist/src/servers/index.js +4 -0
- package/dist/src/servers/index.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 +1072 -69
- 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 +270 -54
- 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 +1218 -107
- 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 -1
- package/dist/src/servers/whatsapp.js +149 -25
- 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 +251 -64
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/dist/src/openclaw/smoke-lock.js +0 -1004
- package/dist/src/openclaw/smoke-lock.js.map +0 -1
- 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;
|
|
585
|
+
}
|
|
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;
|
|
140
591
|
}
|
|
141
|
-
return
|
|
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,346 @@ 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
|
+
// Keep this scan strictly forward-only: this request path accepts client-controlled
|
|
1247
|
+
// Markdown, and backtracking over malformed link destinations amplifies CPU use.
|
|
1248
|
+
function stripTelegramMarkdownLinks(value) {
|
|
1249
|
+
const output = [];
|
|
1250
|
+
let copyStart = 0;
|
|
1251
|
+
let searchStart = 0;
|
|
1252
|
+
while (searchStart < value.length) {
|
|
1253
|
+
const labelStart = value.indexOf("[", searchStart);
|
|
1254
|
+
if (labelStart < 0) {
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
let labelEnd = labelStart + 1;
|
|
1258
|
+
while (labelEnd < value.length && value[labelEnd] !== "]" && value[labelEnd] !== "\n") {
|
|
1259
|
+
labelEnd += 1;
|
|
1260
|
+
}
|
|
1261
|
+
if (labelEnd === labelStart + 1 || value[labelEnd] !== "]" || value[labelEnd + 1] !== "(") {
|
|
1262
|
+
searchStart = labelEnd < value.length ? labelEnd + 1 : value.length;
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
let destinationEnd = labelEnd + 2;
|
|
1266
|
+
let matched = false;
|
|
1267
|
+
while (destinationEnd < value.length) {
|
|
1268
|
+
const character = value[destinationEnd];
|
|
1269
|
+
if (character === "\n") {
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
if (character === ")") {
|
|
1273
|
+
output.push(value.slice(copyStart, labelStart), value.slice(labelStart + 1, labelEnd));
|
|
1274
|
+
copyStart = destinationEnd + 1;
|
|
1275
|
+
searchStart = copyStart;
|
|
1276
|
+
matched = true;
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
if (character === "\\" &&
|
|
1280
|
+
destinationEnd + 1 < value.length &&
|
|
1281
|
+
value[destinationEnd + 1] !== "\n") {
|
|
1282
|
+
destinationEnd += 2;
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
destinationEnd += 1;
|
|
1286
|
+
}
|
|
1287
|
+
if (matched) {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
searchStart = destinationEnd < value.length ? destinationEnd + 1 : value.length;
|
|
1291
|
+
}
|
|
1292
|
+
output.push(value.slice(copyStart));
|
|
1293
|
+
return output.join("");
|
|
1294
|
+
}
|
|
1295
|
+
function telegramMarkdownText(value, versionTwo) {
|
|
1296
|
+
let parsed = protectTelegramMarkdownEscapes(value, versionTwo)
|
|
1297
|
+
.replace(/```(?:[^\n]*\n)?([\s\S]*?)```/gu, (_match, content) => telegramMarkdownLiteralMask(content))
|
|
1298
|
+
.replace(/`([^`\n]*)`/gu, (_match, content) => telegramMarkdownLiteralMask(content))
|
|
1299
|
+
.replace(/!\[([^\]\n]+)\]\(tg:\/\/(?:emoji\?id=\d+|time\?unix=-?\d+(?:&format=[rwdDtT]+)?)\)/gu, "$1");
|
|
1300
|
+
parsed = stripTelegramMarkdownLinks(parsed);
|
|
1301
|
+
if (versionTwo) {
|
|
1302
|
+
parsed = parsed
|
|
1303
|
+
.replace(/^(?:\*\*)?>[^\n]*$/gmu, stripTelegramMarkdownExpandableMarker)
|
|
1304
|
+
.replace(/^\*\*(?=>)/gmu, "")
|
|
1305
|
+
.replace(/^>/gmu, "")
|
|
1306
|
+
.replace(/\|\|([\s\S]*?)\|\|/gu, "$1")
|
|
1307
|
+
.replace(/__([\s\S]*?)__/gu, "$1")
|
|
1308
|
+
.replace(/~([\s\S]*?)~/gu, "$1");
|
|
1309
|
+
}
|
|
1310
|
+
let previous;
|
|
1311
|
+
do {
|
|
1312
|
+
previous = parsed;
|
|
1313
|
+
parsed = parsed.replace(/\*([\s\S]*?)\*/gu, "$1").replace(/_([\s\S]*?)_/gu, "$1");
|
|
1314
|
+
} while (parsed !== previous);
|
|
1315
|
+
return parsed;
|
|
1316
|
+
}
|
|
332
1317
|
function hasValidExplicitTelegramIdentities(body) {
|
|
333
1318
|
return ([body.chatId, body.chat_id].every((value) => value === undefined || telegramChatId(value) !== undefined) &&
|
|
334
1319
|
[
|
|
@@ -336,8 +1321,6 @@ function hasValidExplicitTelegramIdentities(body) {
|
|
|
336
1321
|
body.from_id,
|
|
337
1322
|
body.messageId,
|
|
338
1323
|
body.message_id,
|
|
339
|
-
body.messageThreadId,
|
|
340
|
-
body.message_thread_id,
|
|
341
1324
|
body.updateId,
|
|
342
1325
|
body.update_id,
|
|
343
1326
|
].every((value) => value === undefined || toIntegerValue(value) !== undefined));
|
|
@@ -348,14 +1331,17 @@ function isValidIgnoredTelegramUpdate(body) {
|
|
|
348
1331
|
if (isJsonObject(body.message)) {
|
|
349
1332
|
const message = body.message;
|
|
350
1333
|
const chat = isJsonObject(message.chat) ? message.chat : undefined;
|
|
1334
|
+
const messageId = toIntegerValue(message.message_id);
|
|
351
1335
|
if (updateId === undefined ||
|
|
1336
|
+
updateId < 1 ||
|
|
352
1337
|
!chat ||
|
|
353
|
-
|
|
354
|
-
|
|
1338
|
+
telegramStoredChatId(chat.id) === undefined ||
|
|
1339
|
+
messageId === undefined ||
|
|
1340
|
+
messageId < 0 ||
|
|
355
1341
|
(message.from !== undefined &&
|
|
356
|
-
(!isJsonObject(message.from) || toIntegerValue(message.from.id)
|
|
1342
|
+
(!isJsonObject(message.from) || (toIntegerValue(message.from.id) ?? 0) < 1)) ||
|
|
357
1343
|
(message.message_thread_id !== undefined &&
|
|
358
|
-
|
|
1344
|
+
telegramMessageThreadId(message.message_thread_id) === undefined)) {
|
|
359
1345
|
return false;
|
|
360
1346
|
}
|
|
361
1347
|
return toStringValue(message.text) === undefined;
|
|
@@ -378,13 +1364,55 @@ async function handleTelegramAdminInbound(params) {
|
|
|
378
1364
|
});
|
|
379
1365
|
await previousAdmission;
|
|
380
1366
|
try {
|
|
381
|
-
const nextMessageId = params.state.nextMessageId;
|
|
382
1367
|
const nextUpdateId = params.state.nextUpdateId;
|
|
1368
|
+
const explicitMessageId = explicitTelegramId(params.body, ["messageId", "message_id"], explicitTelegramMessageIdValues(params.body), { allowZero: true });
|
|
1369
|
+
const explicitMessageChatId = explicitTelegramMessageChatId(params.body);
|
|
1370
|
+
const explicitUpdateId = explicitTelegramId(params.body, ["updateId", "update_id"]);
|
|
1371
|
+
const referencedMessageId = explicitTelegramId({}, [], referencedTelegramMessageIdValues(params.body));
|
|
1372
|
+
const topLevelChatId = resolveTelegramChat(params.state, params.body.chatId ?? params.body.chat_id)?.id;
|
|
1373
|
+
const messageChatId = explicitMessageChatId === false ? undefined : (explicitMessageChatId ?? topLevelChatId);
|
|
1374
|
+
const messageChatKey = messageChatId === undefined ? undefined : telegramChatKey(messageChatId);
|
|
1375
|
+
const previousNextMessageId = messageChatKey === undefined ? undefined : params.state.nextMessageIds.get(messageChatKey);
|
|
1376
|
+
const nextMessageId = messageChatId === undefined ? undefined : nextTelegramMessageId(params.state, messageChatId);
|
|
1377
|
+
if (!hasValidTelegramMessageThreadIds(params.body) ||
|
|
1378
|
+
!hasValidTelegramTopicDestinations(params.state, params.body) ||
|
|
1379
|
+
explicitMessageId === undefined ||
|
|
1380
|
+
explicitMessageChatId === false ||
|
|
1381
|
+
explicitUpdateId === undefined ||
|
|
1382
|
+
referencedMessageId === undefined ||
|
|
1383
|
+
(explicitMessageId.present &&
|
|
1384
|
+
explicitMessageId.value > 0 &&
|
|
1385
|
+
(nextMessageId === undefined || explicitMessageId.value < nextMessageId)) ||
|
|
1386
|
+
(explicitUpdateId.present && explicitUpdateId.value < nextUpdateId)) {
|
|
1387
|
+
return telegramError("Bad Request: explicit IDs must be valid and monotonic");
|
|
1388
|
+
}
|
|
383
1389
|
const update = createInboundUpdate(params.state, params.body);
|
|
384
|
-
params.state.nextMessageId = nextMessageId;
|
|
385
1390
|
params.state.nextUpdateId = nextUpdateId;
|
|
1391
|
+
if (update &&
|
|
1392
|
+
((!explicitMessageId.present &&
|
|
1393
|
+
(nextMessageId ?? Number.MAX_SAFE_INTEGER) >= Number.MAX_SAFE_INTEGER) ||
|
|
1394
|
+
(!explicitUpdateId.present && nextUpdateId >= Number.MAX_SAFE_INTEGER))) {
|
|
1395
|
+
return telegramError("Bad Request: generated ID space is exhausted");
|
|
1396
|
+
}
|
|
386
1397
|
if (!update) {
|
|
387
1398
|
if (isValidIgnoredTelegramUpdate(params.body)) {
|
|
1399
|
+
if (messageChatKey !== undefined) {
|
|
1400
|
+
const observedNextMessageIds = [
|
|
1401
|
+
...(explicitMessageId.present && explicitMessageId.value > 0
|
|
1402
|
+
? [explicitMessageId.value + 1]
|
|
1403
|
+
: []),
|
|
1404
|
+
...(referencedMessageId.present && referencedMessageId.value > 0
|
|
1405
|
+
? [referencedMessageId.value + 1]
|
|
1406
|
+
: []),
|
|
1407
|
+
];
|
|
1408
|
+
if (observedNextMessageIds.length > 0) {
|
|
1409
|
+
params.state.nextMessageIds.set(messageChatKey, Math.max(params.state.nextMessageIds.get(messageChatKey) ?? 1, ...observedNextMessageIds));
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (explicitUpdateId.present) {
|
|
1413
|
+
params.state.nextUpdateId = explicitUpdateId.value + 1;
|
|
1414
|
+
}
|
|
1415
|
+
registerTelegramUpdateChats(params.state, params.body);
|
|
388
1416
|
return jsonResponse({ ok: true });
|
|
389
1417
|
}
|
|
390
1418
|
return telegramError("Bad Request: chatId and text are required");
|
|
@@ -392,9 +1420,12 @@ async function handleTelegramAdminInbound(params) {
|
|
|
392
1420
|
if (params.state.updates.length >= params.state.maxPendingInboundEvents) {
|
|
393
1421
|
return telegramError(`Too Many Requests: pending inbound queue is full (${params.state.maxPendingInboundEvents} updates)`, 429);
|
|
394
1422
|
}
|
|
395
|
-
|
|
1423
|
+
const updateChatKey = telegramChatKey(update.message.chat.id);
|
|
1424
|
+
params.state.nextMessageIds.set(updateChatKey, Math.max(params.state.nextMessageIds.get(updateChatKey) ?? 1, update.message.message_id + 1, ...(referencedMessageId.present && referencedMessageId.value > 0
|
|
1425
|
+
? [referencedMessageId.value + 1]
|
|
1426
|
+
: [])));
|
|
396
1427
|
params.state.nextUpdateId = Math.max(params.state.nextUpdateId, update.update_id + 1);
|
|
397
|
-
const reservedNextMessageId = params.state.
|
|
1428
|
+
const reservedNextMessageId = params.state.nextMessageIds.get(updateChatKey);
|
|
398
1429
|
const reservedNextUpdateId = params.state.nextUpdateId;
|
|
399
1430
|
try {
|
|
400
1431
|
await appendEvent(params.state, {
|
|
@@ -407,14 +1438,20 @@ async function handleTelegramAdminInbound(params) {
|
|
|
407
1438
|
});
|
|
408
1439
|
}
|
|
409
1440
|
catch (error) {
|
|
410
|
-
if (params.state.
|
|
411
|
-
|
|
1441
|
+
if (params.state.nextMessageIds.get(updateChatKey) === reservedNextMessageId) {
|
|
1442
|
+
if (previousNextMessageId === undefined) {
|
|
1443
|
+
params.state.nextMessageIds.delete(updateChatKey);
|
|
1444
|
+
}
|
|
1445
|
+
else {
|
|
1446
|
+
params.state.nextMessageIds.set(updateChatKey, previousNextMessageId);
|
|
1447
|
+
}
|
|
412
1448
|
}
|
|
413
1449
|
if (params.state.nextUpdateId === reservedNextUpdateId) {
|
|
414
1450
|
params.state.nextUpdateId = nextUpdateId;
|
|
415
1451
|
}
|
|
416
1452
|
throw error;
|
|
417
1453
|
}
|
|
1454
|
+
registerTelegramChat(params.state, update.message.chat);
|
|
418
1455
|
params.state.updates.push(update);
|
|
419
1456
|
params.state.updates.sort((left, right) => left.update_id - right.update_id);
|
|
420
1457
|
if (params.state.webhook) {
|
|
@@ -690,14 +1727,29 @@ async function handleTelegramApi(params) {
|
|
|
690
1727
|
return telegramOk(createBotUser(params.state));
|
|
691
1728
|
case "setmycommands":
|
|
692
1729
|
case "deletemycommands":
|
|
693
|
-
case "sendchataction":
|
|
694
1730
|
case "answercallbackquery":
|
|
695
1731
|
case "deletemessage":
|
|
696
1732
|
case "pinchatmessage":
|
|
697
1733
|
case "unpinchatmessage":
|
|
698
1734
|
case "setmessagereaction":
|
|
699
|
-
case "editforumtopic":
|
|
700
1735
|
return telegramOk(true);
|
|
1736
|
+
case "sendchataction": {
|
|
1737
|
+
if (params.body.message_thread_id === undefined) {
|
|
1738
|
+
return telegramOk(true);
|
|
1739
|
+
}
|
|
1740
|
+
const chat = resolveTelegramChat(params.state, params.body.chat_id);
|
|
1741
|
+
const messageThreadId = telegramMessageThreadId(params.body.message_thread_id);
|
|
1742
|
+
return chat && messageThreadId !== undefined && canTargetTelegramTopic(params.state, chat)
|
|
1743
|
+
? telegramOk(true)
|
|
1744
|
+
: telegramError("Bad Request: chat is not a forum or message_thread_id is invalid");
|
|
1745
|
+
}
|
|
1746
|
+
case "editforumtopic": {
|
|
1747
|
+
const chat = resolveTelegramChat(params.state, params.body.chat_id);
|
|
1748
|
+
const messageThreadId = telegramMessageThreadId(params.body.message_thread_id);
|
|
1749
|
+
return chat && messageThreadId !== undefined && canTargetTelegramTopic(params.state, chat)
|
|
1750
|
+
? telegramOk(true)
|
|
1751
|
+
: telegramError("Bad Request: chat is not a forum or message_thread_id is invalid");
|
|
1752
|
+
}
|
|
701
1753
|
case "setwebhook": {
|
|
702
1754
|
if (params.body.url === "") {
|
|
703
1755
|
await replaceTelegramWebhook(params.state, undefined, toBooleanValue(params.body.drop_pending_updates));
|
|
@@ -717,6 +1769,15 @@ async function handleTelegramApi(params) {
|
|
|
717
1769
|
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
718
1770
|
return telegramError("Bad Request: bad webhook URL");
|
|
719
1771
|
}
|
|
1772
|
+
const secretToken = params.body.secret_token === undefined
|
|
1773
|
+
? undefined
|
|
1774
|
+
: typeof params.body.secret_token === "string"
|
|
1775
|
+
? params.body.secret_token
|
|
1776
|
+
: null;
|
|
1777
|
+
if (secretToken === null ||
|
|
1778
|
+
(secretToken !== undefined && !TELEGRAM_WEBHOOK_SECRET_PATTERN.test(secretToken))) {
|
|
1779
|
+
return telegramError("Bad Request: invalid secret token");
|
|
1780
|
+
}
|
|
720
1781
|
if (params.state.activeWebhookValidations.size >= MAX_ACTIVE_TELEGRAM_WEBHOOK_VALIDATIONS) {
|
|
721
1782
|
return telegramError("Too Many Requests: too many webhook validations", 429);
|
|
722
1783
|
}
|
|
@@ -728,10 +1789,6 @@ async function handleTelegramApi(params) {
|
|
|
728
1789
|
if (target instanceof Response) {
|
|
729
1790
|
return target;
|
|
730
1791
|
}
|
|
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
1792
|
await replaceTelegramWebhook(params.state, {
|
|
736
1793
|
...(secretToken ? { secretToken } : {}),
|
|
737
1794
|
url: parsedUrl.href,
|
|
@@ -753,20 +1810,50 @@ async function handleTelegramApi(params) {
|
|
|
753
1810
|
pending_update_count: params.state.updates.length,
|
|
754
1811
|
url: params.state.webhook?.url ?? "",
|
|
755
1812
|
});
|
|
756
|
-
case "createforumtopic":
|
|
1813
|
+
case "createforumtopic": {
|
|
1814
|
+
const chat = resolveTelegramChat(params.state, params.body.chat_id);
|
|
1815
|
+
if (!chat) {
|
|
1816
|
+
return telegramError("Bad Request: chat_id is required");
|
|
1817
|
+
}
|
|
1818
|
+
if (!canTargetTelegramTopic(params.state, chat)) {
|
|
1819
|
+
return telegramError("Bad Request: chat is not a forum");
|
|
1820
|
+
}
|
|
1821
|
+
const messageThreadId = takeNextTelegramMessageId(params.state, chat.id);
|
|
1822
|
+
if (messageThreadId === undefined) {
|
|
1823
|
+
return telegramError("Bad Request: generated ID space is exhausted");
|
|
1824
|
+
}
|
|
757
1825
|
return telegramOk({
|
|
758
1826
|
icon_color: 0x6fb9f0,
|
|
759
|
-
message_thread_id:
|
|
1827
|
+
message_thread_id: messageThreadId,
|
|
760
1828
|
name: toStringValue(params.body.name) ?? "Crabline Topic",
|
|
761
1829
|
});
|
|
1830
|
+
}
|
|
762
1831
|
case "editmessagetext": {
|
|
763
|
-
const
|
|
1832
|
+
const text = readTelegramTextField(params.body.text, "text", {
|
|
1833
|
+
parseMode: params.body.parse_mode,
|
|
1834
|
+
required: true,
|
|
1835
|
+
});
|
|
1836
|
+
if (text instanceof Response) {
|
|
1837
|
+
return text;
|
|
1838
|
+
}
|
|
1839
|
+
const message = createEditedMessage(params.state, params.body, text);
|
|
764
1840
|
return message
|
|
765
1841
|
? telegramOk(message)
|
|
766
1842
|
: telegramError("Bad Request: chat_id, message_id, and text are required");
|
|
767
1843
|
}
|
|
768
1844
|
case "sendmessage": {
|
|
769
|
-
const
|
|
1845
|
+
const text = readTelegramTextField(params.body.text, "text", {
|
|
1846
|
+
parseMode: params.body.parse_mode,
|
|
1847
|
+
required: true,
|
|
1848
|
+
});
|
|
1849
|
+
if (text instanceof Response) {
|
|
1850
|
+
return text;
|
|
1851
|
+
}
|
|
1852
|
+
const chat = resolveTelegramChat(params.state, params.body.chat_id);
|
|
1853
|
+
if (chat && nextTelegramMessageId(params.state, chat.id) >= Number.MAX_SAFE_INTEGER) {
|
|
1854
|
+
return telegramError("Bad Request: generated ID space is exhausted");
|
|
1855
|
+
}
|
|
1856
|
+
const message = createOutboundMessage(params.state, params.body, text);
|
|
770
1857
|
return message
|
|
771
1858
|
? telegramOk(message)
|
|
772
1859
|
: telegramError("Bad Request: chat_id and text are required");
|
|
@@ -777,7 +1864,18 @@ async function handleTelegramApi(params) {
|
|
|
777
1864
|
case "sendphoto":
|
|
778
1865
|
case "sendvideo": {
|
|
779
1866
|
const mediaKind = method.slice("send".length);
|
|
780
|
-
const
|
|
1867
|
+
const caption = readTelegramTextField(params.body.caption, "caption", {
|
|
1868
|
+
parseMode: params.body.parse_mode,
|
|
1869
|
+
required: false,
|
|
1870
|
+
});
|
|
1871
|
+
if (caption instanceof Response) {
|
|
1872
|
+
return caption;
|
|
1873
|
+
}
|
|
1874
|
+
const chat = resolveTelegramChat(params.state, params.body.chat_id);
|
|
1875
|
+
if (chat && nextTelegramMessageId(params.state, chat.id) >= Number.MAX_SAFE_INTEGER) {
|
|
1876
|
+
return telegramError("Bad Request: generated ID space is exhausted");
|
|
1877
|
+
}
|
|
1878
|
+
const message = createOutboundMediaMessage(params.state, params.body, mediaKind, caption);
|
|
781
1879
|
return message
|
|
782
1880
|
? telegramOk(message)
|
|
783
1881
|
: telegramError(`Bad Request: chat_id and ${mediaKind} are required`);
|
|
@@ -958,13 +2056,15 @@ async function handleRequest(params) {
|
|
|
958
2056
|
}
|
|
959
2057
|
return response;
|
|
960
2058
|
}
|
|
961
|
-
await
|
|
962
|
-
return await handleTelegramApi({
|
|
2059
|
+
const response = await handleTelegramApi({
|
|
963
2060
|
body,
|
|
964
2061
|
method: botPath.method,
|
|
965
2062
|
request: params.request,
|
|
966
2063
|
state: params.state,
|
|
967
2064
|
});
|
|
2065
|
+
event.accepted = response.ok;
|
|
2066
|
+
await appendEvent(params.state, event);
|
|
2067
|
+
return response;
|
|
968
2068
|
}
|
|
969
2069
|
async function serveRequest(params) {
|
|
970
2070
|
let fetchResponse;
|
|
@@ -990,25 +2090,36 @@ async function serveRequest(params) {
|
|
|
990
2090
|
}
|
|
991
2091
|
export async function startTelegramServer(params = {}) {
|
|
992
2092
|
const host = params.host ?? "127.0.0.1";
|
|
2093
|
+
if (params.botHasTopicsEnabled !== undefined && typeof params.botHasTopicsEnabled !== "boolean") {
|
|
2094
|
+
throw new Error("botHasTopicsEnabled must be a boolean.");
|
|
2095
|
+
}
|
|
993
2096
|
const botId = params.botId ?? 424242;
|
|
994
2097
|
if (!Number.isSafeInteger(botId) || botId < 1) {
|
|
995
2098
|
throw new Error("botId must be a positive safe integer.");
|
|
996
2099
|
}
|
|
2100
|
+
const botUsernameValue = params.botUsername ?? "crabline_bot";
|
|
2101
|
+
const botUsername = botUsernameValue.trim().replace(/^@/u, "").toLowerCase();
|
|
2102
|
+
if (!TELEGRAM_BOT_USERNAME_PATTERN.test(`@${botUsername}`)) {
|
|
2103
|
+
throw new Error("botUsername must be a valid 5-32 character Telegram username.");
|
|
2104
|
+
}
|
|
997
2105
|
const state = {
|
|
998
2106
|
activeUpdatePoll: undefined,
|
|
999
2107
|
activeWebhookDeliveries: new Set(),
|
|
1000
2108
|
activeWebhookValidations: new Set(),
|
|
1001
2109
|
adminToken: params.adminToken ?? randomBytes(24).toString("hex"),
|
|
1002
2110
|
allowLoopbackHttpWebhook: isLoopbackHost(host),
|
|
2111
|
+
botHasTopicsEnabled: params.botHasTopicsEnabled ?? true,
|
|
1003
2112
|
botId,
|
|
1004
2113
|
botToken: params.botToken ??
|
|
1005
2114
|
(isLoopbackHost(host)
|
|
1006
2115
|
? "424242:crabline-telegram-token"
|
|
1007
2116
|
: `${botId}:${randomBytes(26).toString("base64url")}`),
|
|
1008
|
-
botUsername
|
|
2117
|
+
botUsername,
|
|
2118
|
+
chatsById: new Map(),
|
|
2119
|
+
chatsByUsername: new Map(),
|
|
1009
2120
|
inboundAdmission: Promise.resolve(),
|
|
1010
2121
|
maxPendingInboundEvents: resolveMaxPendingInboundEvents(params.maxPendingInboundEvents),
|
|
1011
|
-
|
|
2122
|
+
nextMessageIds: new Map(),
|
|
1012
2123
|
nextUpdateId: 1,
|
|
1013
2124
|
onEvent: params.onEvent,
|
|
1014
2125
|
recorderPath: params.recorderPath ?? path.resolve(".crabline", "servers", "telegram.jsonl"),
|