@openclaw/crabline 0.1.8 → 0.1.10
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 +52 -22
- package/assets/crabline-banner.svg +20 -0
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +25 -1
- package/dist/src/cli/program.js +579 -82
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +59 -6
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +116 -92
- package/dist/src/config/schema.js +179 -73
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.js +9 -2
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/exit-codes.js.map +1 -1
- package/dist/src/core/matcher.js +7 -6
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/message-template.js.map +1 -1
- package/dist/src/core/nonces.d.ts +1 -0
- package/dist/src/core/nonces.js +4 -2
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +1 -0
- package/dist/src/core/reporters.js +42 -4
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -0
- package/dist/src/core/run.js +415 -107
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/core/safe-regex.d.ts +3 -0
- package/dist/src/core/safe-regex.js +21 -0
- package/dist/src/core/safe-regex.js.map +1 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/openclaw/artifact-generation.d.ts +38 -0
- package/dist/src/openclaw/artifact-generation.js +314 -0
- package/dist/src/openclaw/artifact-generation.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +97 -12
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +32 -10
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/signal.js +55 -18
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +85 -7
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +124 -30
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +78 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +14 -5
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.d.ts +5 -0
- package/dist/src/openclaw/outbound-contract.js +40 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -0
- package/dist/src/openclaw/private-file.d.ts +26 -0
- package/dist/src/openclaw/private-file.js +411 -0
- package/dist/src/openclaw/private-file.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +23 -2
- package/dist/src/openclaw/shared.js +115 -18
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +63 -0
- package/dist/src/openclaw/smoke-lock.js +949 -0
- package/dist/src/openclaw/smoke-lock.js.map +1 -0
- package/dist/src/openclaw.d.ts +16 -8
- package/dist/src/openclaw.js +195 -52
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/providers/builtin/discord.d.ts +10 -1
- package/dist/src/providers/builtin/discord.js +144 -18
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
- package/dist/src/providers/builtin/feishu.d.ts +19 -2
- package/dist/src/providers/builtin/feishu.js +307 -25
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +20 -1
- package/dist/src/providers/builtin/googlechat.js +178 -19
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.d.ts +4 -0
- package/dist/src/providers/builtin/imessage.js +26 -11
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +92 -30
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +11 -1
- package/dist/src/providers/builtin/matrix.js +53 -29
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +10 -0
- package/dist/src/providers/builtin/mattermost.js +38 -15
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +17 -1
- package/dist/src/providers/builtin/msteams.js +113 -14
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
- package/dist/src/providers/builtin/native-local-mock.js +12 -35
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.d.ts +11 -15
- package/dist/src/providers/builtin/script.js +566 -104
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +6 -0
- package/dist/src/providers/builtin/slack.js +99 -40
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +6 -0
- package/dist/src/providers/builtin/telegram.js +64 -43
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
- package/dist/src/providers/builtin/whatsapp.js +567 -30
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.js +30 -19
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +3 -3
- package/dist/src/providers/catalog.js +3 -3
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +11 -2
- package/dist/src/providers/local-mock.js +322 -96
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +1 -0
- package/dist/src/providers/native-ids.js +6 -1
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +30 -2
- package/dist/src/providers/recorder.js +539 -70
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +25 -1
- package/dist/src/providers/registry.js +280 -40
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +29 -0
- package/dist/src/providers/signed-jwt.js +202 -0
- package/dist/src/providers/signed-jwt.js.map +1 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +26 -0
- package/dist/src/providers/target-normalizers.js +276 -0
- package/dist/src/providers/target-normalizers.js.map +1 -0
- package/dist/src/providers/types.d.ts +5 -0
- package/dist/src/providers/types.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +2 -0
- package/dist/src/providers/webhook-server.js +72 -25
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +23 -3
- package/dist/src/servers/http.js +213 -20
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.js +20 -21
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +2 -0
- package/dist/src/servers/matrix.js +466 -52
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +7 -0
- package/dist/src/servers/mattermost.js +307 -59
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/pending-events.d.ts +2 -0
- package/dist/src/servers/pending-events.js +11 -0
- package/dist/src/servers/pending-events.js.map +1 -0
- package/dist/src/servers/recorder.d.ts +12 -0
- package/dist/src/servers/recorder.js +51 -0
- package/dist/src/servers/recorder.js.map +1 -0
- package/dist/src/servers/signal.d.ts +4 -0
- package/dist/src/servers/signal.js +445 -57
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +7 -0
- package/dist/src/servers/slack.js +516 -50
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram.d.ts +113 -0
- package/dist/src/servers/telegram.js +814 -155
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +40 -0
- package/dist/src/servers/webhook-target.js +349 -0
- package/dist/src/servers/webhook-target.js.map +1 -0
- package/dist/src/servers/websocket.d.ts +2 -0
- package/dist/src/servers/websocket.js +29 -0
- package/dist/src/servers/websocket.js.map +1 -0
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +5 -0
- package/dist/src/servers/whatsapp-jid.js +32 -0
- package/dist/src/servers/whatsapp-jid.js.map +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
- package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +14 -7
- package/dist/src/servers/whatsapp.js +272 -110
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +16 -0
- package/dist/src/servers/zalo.js +524 -91
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +197 -19
- package/fixtures/examples/crabline.example.yaml +10 -0
- package/package.json +20 -15
|
@@ -1,91 +1,109 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { CrablineError } from "../core/errors.js";
|
|
6
|
-
import { adminAuthError, hasAdminToken } from "./http.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
5
|
+
import { adminAuthError, closeServer, drainRequestBody, formatUrlHost, hasAdminToken, InvalidJsonBodyError, isJsonObject, isLoopbackHost, jsonResponse, queryRecord, readBody, RequestBodyTooLargeError, writeResponse, } from "./http.js";
|
|
6
|
+
import { recordServerEvent } from "./recorder.js";
|
|
7
|
+
import { resolveMaxPendingInboundEvents } from "./pending-events.js";
|
|
8
|
+
import { postWebhookRequest, validateWebhookTarget, } from "./webhook-target.js";
|
|
9
|
+
const TELEGRAM_MAX_REQUEST_BODY_BYTES = 50 * 1024 * 1024;
|
|
10
|
+
const TELEGRAM_WEBHOOK_MAX_BACKOFF_EXPONENT = 5;
|
|
11
|
+
const TELEGRAM_WEBHOOK_RETRY_BASE_MS = 100;
|
|
12
|
+
const TELEGRAM_WEBHOOK_DELIVERY_TIMEOUT_MS = 3_000;
|
|
13
|
+
const MAX_ACTIVE_TELEGRAM_WEBHOOK_VALIDATIONS = 8;
|
|
14
|
+
const TELEGRAM_CHAT_USERNAME_PATTERN = /^@[A-Za-z][A-Za-z0-9_]{3,31}$/u;
|
|
15
|
+
const TELEGRAM_SEND_METHODS = new Set([
|
|
16
|
+
"sendanimation",
|
|
17
|
+
"sendaudio",
|
|
18
|
+
"senddocument",
|
|
19
|
+
"sendmessage",
|
|
20
|
+
"sendphoto",
|
|
21
|
+
"sendvideo",
|
|
22
|
+
]);
|
|
23
|
+
const TELEGRAM_MEDIA_FIELDS = [
|
|
24
|
+
"animation",
|
|
25
|
+
"audio",
|
|
26
|
+
"document",
|
|
27
|
+
"paid_media",
|
|
28
|
+
"photo",
|
|
29
|
+
"sticker",
|
|
30
|
+
"story",
|
|
31
|
+
"video",
|
|
32
|
+
"video_note",
|
|
33
|
+
"voice",
|
|
34
|
+
];
|
|
35
|
+
const TELEGRAM_UNSUPPORTED_UPDATE_FIELDS = [
|
|
36
|
+
"business_connection",
|
|
37
|
+
"business_message",
|
|
38
|
+
"callback_query",
|
|
39
|
+
"channel_post",
|
|
40
|
+
"chat_boost",
|
|
41
|
+
"chat_join_request",
|
|
42
|
+
"chat_member",
|
|
43
|
+
"chosen_inline_result",
|
|
44
|
+
"deleted_business_messages",
|
|
45
|
+
"edited_business_message",
|
|
46
|
+
"edited_channel_post",
|
|
47
|
+
"edited_message",
|
|
48
|
+
"guest_message",
|
|
49
|
+
"inline_query",
|
|
50
|
+
"managed_bot",
|
|
51
|
+
"message_reaction",
|
|
52
|
+
"message_reaction_count",
|
|
53
|
+
"my_chat_member",
|
|
54
|
+
"poll",
|
|
55
|
+
"poll_answer",
|
|
56
|
+
"pre_checkout_query",
|
|
57
|
+
"purchased_paid_media",
|
|
58
|
+
"removed_chat_boost",
|
|
59
|
+
"shipping_query",
|
|
60
|
+
];
|
|
10
61
|
function telegramOk(result) {
|
|
11
62
|
return jsonResponse({ ok: true, result });
|
|
12
63
|
}
|
|
13
64
|
function telegramError(description, status = 400) {
|
|
14
65
|
return jsonResponse({ description, error_code: status, ok: false }, status);
|
|
15
66
|
}
|
|
16
|
-
async function readBody(request) {
|
|
17
|
-
const chunks = [];
|
|
18
|
-
for await (const chunk of request) {
|
|
19
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
20
|
-
}
|
|
21
|
-
return Buffer.concat(chunks);
|
|
22
|
-
}
|
|
23
67
|
async function parseRequestBody(request) {
|
|
24
|
-
const body = await readBody(request);
|
|
68
|
+
const body = await readBody(request, TELEGRAM_MAX_REQUEST_BODY_BYTES);
|
|
25
69
|
if (body.length === 0) {
|
|
26
70
|
return {};
|
|
27
71
|
}
|
|
28
72
|
const contentType = request.headers["content-type"] ?? "";
|
|
29
73
|
const contentTypes = Array.isArray(contentType) ? contentType : [contentType];
|
|
30
|
-
if (contentTypes.some((entry) => entry.includes("json"))) {
|
|
31
|
-
|
|
74
|
+
if (contentTypes.some((entry) => entry.toLowerCase().includes("json"))) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(body.toString("utf8"));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw new InvalidJsonBodyError(error);
|
|
80
|
+
}
|
|
32
81
|
}
|
|
33
|
-
const multipartType = contentTypes.find((entry) => entry.includes("multipart/form-data"));
|
|
82
|
+
const multipartType = contentTypes.find((entry) => entry.toLowerCase().includes("multipart/form-data"));
|
|
34
83
|
if (multipartType) {
|
|
35
|
-
return parseMultipartFormDataBody(body, multipartType);
|
|
84
|
+
return await parseMultipartFormDataBody(body, multipartType);
|
|
36
85
|
}
|
|
37
86
|
const params = new URLSearchParams(body.toString("utf8"));
|
|
38
87
|
return Object.fromEntries(params.entries());
|
|
39
88
|
}
|
|
40
|
-
function parseMultipartFormDataBody(body, contentType) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
89
|
+
async function parseMultipartFormDataBody(body, contentType) {
|
|
90
|
+
let form;
|
|
91
|
+
try {
|
|
92
|
+
form = await new Request("http://localhost", {
|
|
93
|
+
body,
|
|
94
|
+
headers: { "content-type": contentType },
|
|
95
|
+
method: "POST",
|
|
96
|
+
}).formData();
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new InvalidJsonBodyError(error);
|
|
45
100
|
}
|
|
46
101
|
const fields = {};
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
const part = rawPart.replace(/^\r?\n/u, "").replace(/\r?\n$/u, "");
|
|
50
|
-
if (!part || part === "--") {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
const separatorIndex = part.indexOf("\r\n\r\n");
|
|
54
|
-
if (separatorIndex < 0) {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const rawHeaders = part.slice(0, separatorIndex);
|
|
58
|
-
const rawContent = part.slice(separatorIndex + 4).replace(/\r?\n--$/u, "");
|
|
59
|
-
const disposition = rawHeaders
|
|
60
|
-
.split(/\r?\n/u)
|
|
61
|
-
.find((header) => header.toLowerCase().startsWith("content-disposition:"));
|
|
62
|
-
const name = /(?:^|;\s*)name="([^"]+)"/iu.exec(disposition ?? "")?.[1];
|
|
63
|
-
if (!name) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
const filename = /(?:^|;\s*)filename="([^"]*)"/iu.exec(disposition ?? "")?.[1];
|
|
67
|
-
fields[name] = filename && filename.length > 0 ? filename : rawContent;
|
|
102
|
+
for (const [name, value] of form) {
|
|
103
|
+
fields[name] = typeof value === "string" ? value : value.name;
|
|
68
104
|
}
|
|
69
105
|
return fields;
|
|
70
106
|
}
|
|
71
|
-
function closeServer(server) {
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
server.close((error) => {
|
|
74
|
-
if (error) {
|
|
75
|
-
reject(error);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
resolve();
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
async function writeResponse(response, fetchResponse) {
|
|
83
|
-
response.statusCode = fetchResponse.status;
|
|
84
|
-
for (const [name, value] of fetchResponse.headers) {
|
|
85
|
-
response.setHeader(name, value);
|
|
86
|
-
}
|
|
87
|
-
response.end(Buffer.from(await fetchResponse.arrayBuffer()));
|
|
88
|
-
}
|
|
89
107
|
function requireTelegramBotPath(pathname) {
|
|
90
108
|
const match = /^\/bot([^/]+)\/([A-Za-z][A-Za-z0-9_]*)$/u.exec(pathname);
|
|
91
109
|
if (!match?.[1] || !match[2]) {
|
|
@@ -105,18 +123,31 @@ function toIntegerValue(value) {
|
|
|
105
123
|
if (!stringValue || !/^-?\d+$/u.test(stringValue)) {
|
|
106
124
|
return undefined;
|
|
107
125
|
}
|
|
108
|
-
|
|
126
|
+
const integerValue = Number(stringValue);
|
|
127
|
+
return Number.isSafeInteger(integerValue) ? integerValue : undefined;
|
|
128
|
+
}
|
|
129
|
+
function toBooleanValue(value) {
|
|
130
|
+
return value === true || (typeof value === "string" && value.toLowerCase() === "true");
|
|
109
131
|
}
|
|
110
132
|
function telegramChatId(value) {
|
|
111
133
|
const stringValue = toStringValue(value);
|
|
112
134
|
if (!stringValue) {
|
|
113
135
|
return undefined;
|
|
114
136
|
}
|
|
115
|
-
|
|
137
|
+
if (/^-?\d+$/u.test(stringValue)) {
|
|
138
|
+
const integerValue = toIntegerValue(stringValue);
|
|
139
|
+
return integerValue === 0 ? undefined : integerValue;
|
|
140
|
+
}
|
|
141
|
+
return TELEGRAM_CHAT_USERNAME_PATTERN.test(stringValue) ? stringValue : undefined;
|
|
116
142
|
}
|
|
117
143
|
async function appendEvent(state, event) {
|
|
118
|
-
await
|
|
119
|
-
|
|
144
|
+
await recordServerEvent({ event, onEvent: state.onEvent, recorderPath: state.recorderPath });
|
|
145
|
+
}
|
|
146
|
+
function redactTelegramSecrets(body) {
|
|
147
|
+
return Object.fromEntries(Object.entries(body).map(([key, value]) => [
|
|
148
|
+
key,
|
|
149
|
+
key === "secret_token" ? "<redacted>" : value,
|
|
150
|
+
]));
|
|
120
151
|
}
|
|
121
152
|
function createBotUser(state) {
|
|
122
153
|
return {
|
|
@@ -131,11 +162,13 @@ function createBotUser(state) {
|
|
|
131
162
|
};
|
|
132
163
|
}
|
|
133
164
|
function createChat(chatId) {
|
|
134
|
-
const type = typeof chatId
|
|
135
|
-
? "
|
|
136
|
-
:
|
|
137
|
-
? "
|
|
138
|
-
: "
|
|
165
|
+
const type = typeof chatId === "string"
|
|
166
|
+
? "supergroup"
|
|
167
|
+
: chatId >= 0
|
|
168
|
+
? "private"
|
|
169
|
+
: String(chatId).startsWith("-100")
|
|
170
|
+
? "supergroup"
|
|
171
|
+
: "group";
|
|
139
172
|
return {
|
|
140
173
|
id: chatId,
|
|
141
174
|
type,
|
|
@@ -147,7 +180,11 @@ function createOutboundMessage(state, body) {
|
|
|
147
180
|
if (chatId === undefined || !text) {
|
|
148
181
|
return undefined;
|
|
149
182
|
}
|
|
150
|
-
const
|
|
183
|
+
const parsedThreadId = toIntegerValue(body.message_thread_id);
|
|
184
|
+
if (body.message_thread_id !== undefined && parsedThreadId === undefined) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const threadId = parsedThreadId !== undefined && parsedThreadId > 0 ? parsedThreadId : undefined;
|
|
151
188
|
return {
|
|
152
189
|
chat: createChat(chatId),
|
|
153
190
|
date: Math.floor(Date.now() / 1000),
|
|
@@ -159,15 +196,22 @@ function createOutboundMessage(state, body) {
|
|
|
159
196
|
}
|
|
160
197
|
function createOutboundMediaMessage(state, body, mediaKind) {
|
|
161
198
|
const chatId = telegramChatId(body.chat_id);
|
|
162
|
-
|
|
199
|
+
const fileName = toStringValue(body[mediaKind]);
|
|
200
|
+
if (chatId === undefined || !fileName) {
|
|
163
201
|
return undefined;
|
|
164
202
|
}
|
|
165
|
-
const
|
|
203
|
+
const parsedThreadId = toIntegerValue(body.message_thread_id);
|
|
204
|
+
if (body.message_thread_id !== undefined && parsedThreadId === undefined) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
const threadId = parsedThreadId !== undefined && parsedThreadId > 0 ? parsedThreadId : undefined;
|
|
166
208
|
const caption = toStringValue(body.caption);
|
|
167
|
-
const
|
|
209
|
+
const duration = Math.max(0, toIntegerValue(body.duration) ?? 0);
|
|
210
|
+
const height = Math.max(1, toIntegerValue(body.height) ?? 1);
|
|
211
|
+
const width = Math.max(1, toIntegerValue(body.width) ?? 1);
|
|
168
212
|
const media = {
|
|
169
213
|
file_id: `crabline-${mediaKind}-${state.nextMessageId}`,
|
|
170
|
-
|
|
214
|
+
file_name: fileName,
|
|
171
215
|
file_unique_id: `crabline-${mediaKind}-unique-${state.nextMessageId}`,
|
|
172
216
|
};
|
|
173
217
|
return {
|
|
@@ -177,7 +221,15 @@ function createOutboundMediaMessage(state, body, mediaKind) {
|
|
|
177
221
|
...(caption ? { caption } : {}),
|
|
178
222
|
[mediaKind]: mediaKind === "photo"
|
|
179
223
|
? [{ ...media, height: 1, width: 1 }]
|
|
180
|
-
: {
|
|
224
|
+
: {
|
|
225
|
+
...media,
|
|
226
|
+
...(mediaKind === "audio"
|
|
227
|
+
? { duration }
|
|
228
|
+
: mediaKind === "animation" || mediaKind === "video"
|
|
229
|
+
? { duration, height, width }
|
|
230
|
+
: {}),
|
|
231
|
+
mime_type: "application/octet-stream",
|
|
232
|
+
},
|
|
181
233
|
message_id: state.nextMessageId++,
|
|
182
234
|
...(threadId !== undefined ? { message_thread_id: threadId } : {}),
|
|
183
235
|
};
|
|
@@ -189,7 +241,11 @@ function createEditedMessage(state, body) {
|
|
|
189
241
|
if (chatId === undefined || !text || messageId === undefined) {
|
|
190
242
|
return undefined;
|
|
191
243
|
}
|
|
192
|
-
const
|
|
244
|
+
const parsedThreadId = toIntegerValue(body.message_thread_id);
|
|
245
|
+
if (body.message_thread_id !== undefined && parsedThreadId === undefined) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
const threadId = parsedThreadId !== undefined && parsedThreadId > 0 ? parsedThreadId : undefined;
|
|
193
249
|
return {
|
|
194
250
|
chat: createChat(chatId),
|
|
195
251
|
date: Math.floor(Date.now() / 1000),
|
|
@@ -205,23 +261,25 @@ function createInboundUpdate(state, body) {
|
|
|
205
261
|
if (chatId === undefined || !text) {
|
|
206
262
|
return undefined;
|
|
207
263
|
}
|
|
208
|
-
const
|
|
209
|
-
const
|
|
264
|
+
const fromIdValue = body.fromId ?? body.from_id;
|
|
265
|
+
const threadIdValue = body.messageThreadId ?? body.message_thread_id;
|
|
266
|
+
const messageIdValue = body.messageId ?? body.message_id;
|
|
267
|
+
const updateIdValue = body.updateId ?? body.update_id;
|
|
268
|
+
const fromId = toIntegerValue(fromIdValue);
|
|
269
|
+
const parsedThreadId = toIntegerValue(threadIdValue);
|
|
270
|
+
const threadId = parsedThreadId !== undefined && parsedThreadId > 0 ? parsedThreadId : undefined;
|
|
210
271
|
const fromUsername = toStringValue(body.fromUsername ?? body.from_username);
|
|
211
|
-
const messageId = toIntegerValue(
|
|
212
|
-
const updateId = toIntegerValue(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
if (updateId !== undefined) {
|
|
224
|
-
state.nextUpdateId = Math.max(state.nextUpdateId, updateId + 1);
|
|
272
|
+
const messageId = toIntegerValue(messageIdValue);
|
|
273
|
+
const updateId = toIntegerValue(updateIdValue);
|
|
274
|
+
if ((fromIdValue !== undefined && fromId === undefined) ||
|
|
275
|
+
(threadIdValue !== undefined && parsedThreadId === undefined) ||
|
|
276
|
+
(messageIdValue !== undefined && messageId === undefined) ||
|
|
277
|
+
(updateIdValue !== undefined && updateId === undefined)) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
const entities = parseTelegramEntities(body.entities, text);
|
|
281
|
+
if (body.entities !== undefined && !entities) {
|
|
282
|
+
return undefined;
|
|
225
283
|
}
|
|
226
284
|
return {
|
|
227
285
|
message: {
|
|
@@ -229,75 +287,616 @@ function createInboundUpdate(state, body) {
|
|
|
229
287
|
date: Math.floor(Date.now() / 1000),
|
|
230
288
|
from: {
|
|
231
289
|
first_name: toStringValue(body.fromName ?? body.from_name) ?? "QA User",
|
|
232
|
-
id: fromId,
|
|
290
|
+
id: fromId ?? 100001,
|
|
233
291
|
is_bot: false,
|
|
234
292
|
...(fromUsername ? { username: fromUsername } : {}),
|
|
235
293
|
},
|
|
236
|
-
message_id: messageId ?? state.nextMessageId
|
|
294
|
+
message_id: messageId ?? state.nextMessageId,
|
|
237
295
|
...(entities && entities.length > 0 ? { entities } : {}),
|
|
238
296
|
...(threadId !== undefined ? { message_thread_id: threadId } : {}),
|
|
239
297
|
text,
|
|
240
298
|
},
|
|
241
|
-
update_id: updateId ?? state.nextUpdateId
|
|
299
|
+
update_id: updateId ?? state.nextUpdateId,
|
|
242
300
|
};
|
|
243
301
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
302
|
+
function parseTelegramEntities(value, text) {
|
|
303
|
+
if (value === undefined) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
if (!Array.isArray(value)) {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
const entities = [];
|
|
310
|
+
for (const entry of value) {
|
|
311
|
+
if (!isJsonObject(entry)) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const length = toIntegerValue(entry.length);
|
|
315
|
+
const offset = toIntegerValue(entry.offset);
|
|
316
|
+
const type = toStringValue(entry.type);
|
|
317
|
+
if (length === undefined ||
|
|
318
|
+
length < 1 ||
|
|
319
|
+
offset === undefined ||
|
|
320
|
+
offset < 0 ||
|
|
321
|
+
offset + length > text.length ||
|
|
322
|
+
!type) {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
entities.push({ length, offset, type });
|
|
326
|
+
}
|
|
327
|
+
return entities;
|
|
328
|
+
}
|
|
329
|
+
function hasTelegramMedia(value) {
|
|
330
|
+
return TELEGRAM_MEDIA_FIELDS.some((field) => value[field] !== undefined);
|
|
331
|
+
}
|
|
332
|
+
function hasValidExplicitTelegramIdentities(body) {
|
|
333
|
+
return ([body.chatId, body.chat_id].every((value) => value === undefined || telegramChatId(value) !== undefined) &&
|
|
334
|
+
[
|
|
335
|
+
body.fromId,
|
|
336
|
+
body.from_id,
|
|
337
|
+
body.messageId,
|
|
338
|
+
body.message_id,
|
|
339
|
+
body.messageThreadId,
|
|
340
|
+
body.message_thread_id,
|
|
341
|
+
body.updateId,
|
|
342
|
+
body.update_id,
|
|
343
|
+
].every((value) => value === undefined || toIntegerValue(value) !== undefined));
|
|
344
|
+
}
|
|
345
|
+
function isValidIgnoredTelegramUpdate(body) {
|
|
346
|
+
const updateIdValue = body.updateId ?? body.update_id;
|
|
347
|
+
const updateId = toIntegerValue(updateIdValue);
|
|
348
|
+
if (isJsonObject(body.message)) {
|
|
349
|
+
const message = body.message;
|
|
350
|
+
const chat = isJsonObject(message.chat) ? message.chat : undefined;
|
|
351
|
+
if (updateId === undefined ||
|
|
352
|
+
!chat ||
|
|
353
|
+
telegramChatId(chat.id) === undefined ||
|
|
354
|
+
toIntegerValue(message.message_id) === undefined ||
|
|
355
|
+
(message.from !== undefined &&
|
|
356
|
+
(!isJsonObject(message.from) || toIntegerValue(message.from.id) === undefined)) ||
|
|
357
|
+
(message.message_thread_id !== undefined &&
|
|
358
|
+
toIntegerValue(message.message_thread_id) === undefined)) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
return toStringValue(message.text) === undefined;
|
|
362
|
+
}
|
|
363
|
+
const chatId = telegramChatId(body.chatId ?? body.chat_id);
|
|
364
|
+
if (chatId !== undefined &&
|
|
365
|
+
hasValidExplicitTelegramIdentities(body) &&
|
|
366
|
+
hasTelegramMedia(body) &&
|
|
367
|
+
toStringValue(body.text) === undefined) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
return (updateId !== undefined &&
|
|
371
|
+
TELEGRAM_UNSUPPORTED_UPDATE_FIELDS.some((field) => isJsonObject(body[field])));
|
|
372
|
+
}
|
|
373
|
+
async function handleTelegramAdminInbound(params) {
|
|
374
|
+
let releaseAdmission;
|
|
375
|
+
const previousAdmission = params.state.inboundAdmission;
|
|
376
|
+
params.state.inboundAdmission = new Promise((resolve) => {
|
|
377
|
+
releaseAdmission = resolve;
|
|
378
|
+
});
|
|
379
|
+
await previousAdmission;
|
|
380
|
+
try {
|
|
381
|
+
const nextMessageId = params.state.nextMessageId;
|
|
382
|
+
const nextUpdateId = params.state.nextUpdateId;
|
|
383
|
+
const update = createInboundUpdate(params.state, params.body);
|
|
384
|
+
params.state.nextMessageId = nextMessageId;
|
|
385
|
+
params.state.nextUpdateId = nextUpdateId;
|
|
386
|
+
if (!update) {
|
|
387
|
+
if (isValidIgnoredTelegramUpdate(params.body)) {
|
|
388
|
+
return jsonResponse({ ok: true });
|
|
389
|
+
}
|
|
390
|
+
return telegramError("Bad Request: chatId and text are required");
|
|
391
|
+
}
|
|
392
|
+
if (params.state.updates.length >= params.state.maxPendingInboundEvents) {
|
|
393
|
+
return telegramError(`Too Many Requests: pending inbound queue is full (${params.state.maxPendingInboundEvents} updates)`, 429);
|
|
394
|
+
}
|
|
395
|
+
params.state.nextMessageId = Math.max(params.state.nextMessageId, update.message.message_id + 1);
|
|
396
|
+
params.state.nextUpdateId = Math.max(params.state.nextUpdateId, update.update_id + 1);
|
|
397
|
+
const reservedNextMessageId = params.state.nextMessageId;
|
|
398
|
+
const reservedNextUpdateId = params.state.nextUpdateId;
|
|
399
|
+
try {
|
|
400
|
+
await appendEvent(params.state, {
|
|
401
|
+
at: new Date().toISOString(),
|
|
402
|
+
body: params.body,
|
|
403
|
+
method: params.request.method ?? "POST",
|
|
404
|
+
path: params.url.pathname,
|
|
405
|
+
query: queryRecord(params.url),
|
|
406
|
+
type: "admin",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (params.state.nextMessageId === reservedNextMessageId) {
|
|
411
|
+
params.state.nextMessageId = nextMessageId;
|
|
412
|
+
}
|
|
413
|
+
if (params.state.nextUpdateId === reservedNextUpdateId) {
|
|
414
|
+
params.state.nextUpdateId = nextUpdateId;
|
|
415
|
+
}
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
params.state.updates.push(update);
|
|
419
|
+
params.state.updates.sort((left, right) => left.update_id - right.update_id);
|
|
420
|
+
if (params.state.webhook) {
|
|
421
|
+
scheduleTelegramWebhookDelivery(params.state, 0);
|
|
422
|
+
return jsonResponse({ ok: true, update });
|
|
423
|
+
}
|
|
424
|
+
finishTelegramUpdatePoll(params.state, "update");
|
|
425
|
+
return jsonResponse({ ok: true, update });
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
releaseAdmission();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function flushTelegramWebhookUpdates(state, onAttempt) {
|
|
432
|
+
if (!state.webhook) {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
while (state.updates.length > 0 && state.webhook) {
|
|
436
|
+
syncTelegramWebhookRetryHead(state);
|
|
437
|
+
const webhook = state.webhook;
|
|
438
|
+
const update = state.updates[0];
|
|
439
|
+
onAttempt(update.update_id);
|
|
440
|
+
try {
|
|
441
|
+
const delivery = await postTelegramWebhook(state, webhook, update);
|
|
442
|
+
if ("error" in delivery) {
|
|
443
|
+
if (state.webhook === webhook) {
|
|
444
|
+
webhook.lastErrorDate = Math.floor(Date.now() / 1_000);
|
|
445
|
+
webhook.lastErrorMessage = delivery.error;
|
|
446
|
+
}
|
|
447
|
+
return telegramError("Bad Gateway: webhook delivery failed", 502);
|
|
448
|
+
}
|
|
449
|
+
if (delivery.status < 200 || delivery.status >= 300) {
|
|
450
|
+
if (state.webhook === webhook) {
|
|
451
|
+
webhook.lastErrorDate = Math.floor(Date.now() / 1_000);
|
|
452
|
+
webhook.lastErrorMessage = `Wrong response from the webhook: ${delivery.status}`;
|
|
453
|
+
}
|
|
454
|
+
return telegramError("Bad Gateway: webhook delivery failed", 502);
|
|
455
|
+
}
|
|
456
|
+
if (state.webhook !== webhook) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
const deliveredIndex = state.updates.indexOf(update);
|
|
460
|
+
if (deliveredIndex >= 0) {
|
|
461
|
+
state.updates.splice(deliveredIndex, 1);
|
|
462
|
+
}
|
|
463
|
+
if (state.webhook === webhook) {
|
|
464
|
+
state.webhookRetryAttempts = 0;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
if (state.webhook === webhook) {
|
|
469
|
+
webhook.lastErrorDate = Math.floor(Date.now() / 1_000);
|
|
470
|
+
webhook.lastErrorMessage = "Webhook delivery failed";
|
|
471
|
+
}
|
|
472
|
+
return telegramError("Bad Gateway: webhook delivery failed", 502);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
async function validateTelegramWebhookUrl(state, url, deadlineAt, signal) {
|
|
478
|
+
const remainingMs = deadlineAt - Date.now();
|
|
479
|
+
if (remainingMs <= 0) {
|
|
480
|
+
return telegramError("Bad Request: webhook host could not be resolved");
|
|
481
|
+
}
|
|
482
|
+
const deadline = new AbortController();
|
|
483
|
+
const timer = setTimeout(() => deadline.abort(new DOMException("Webhook delivery timed out", "TimeoutError")), remainingMs);
|
|
484
|
+
timer.unref();
|
|
485
|
+
let target;
|
|
486
|
+
try {
|
|
487
|
+
target = await validateWebhookTarget({
|
|
488
|
+
allowLoopbackHttp: state.allowLoopbackHttpWebhook,
|
|
489
|
+
restrictPrivateAddresses: state.restrictWebhookTargets,
|
|
490
|
+
signal: AbortSignal.any([signal, deadline.signal]),
|
|
491
|
+
url,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return telegramError("Bad Request: webhook host could not be resolved");
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
clearTimeout(timer);
|
|
499
|
+
}
|
|
500
|
+
if (!("error" in target)) {
|
|
501
|
+
return target;
|
|
502
|
+
}
|
|
503
|
+
return target.error === "unresolvable"
|
|
504
|
+
? telegramError("Bad Request: webhook host could not be resolved")
|
|
505
|
+
: target.error === "private-address"
|
|
506
|
+
? telegramError("Bad Request: webhook URL must not target a private or link-local address")
|
|
507
|
+
: telegramError("Bad Request: webhook URL must use HTTPS");
|
|
508
|
+
}
|
|
509
|
+
/** @internal */
|
|
510
|
+
export async function withTelegramWebhookDeadline(promise, deadlineAt, signal) {
|
|
511
|
+
const remainingMs = deadlineAt - Date.now();
|
|
512
|
+
if (remainingMs <= 0) {
|
|
513
|
+
throw new DOMException("Webhook delivery timed out", "TimeoutError");
|
|
514
|
+
}
|
|
515
|
+
return await new Promise((resolve, reject) => {
|
|
516
|
+
let settled = false;
|
|
517
|
+
const finish = (callback) => {
|
|
518
|
+
if (settled) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
settled = true;
|
|
522
|
+
clearTimeout(timer);
|
|
523
|
+
signal.removeEventListener("abort", onAbort);
|
|
524
|
+
callback();
|
|
525
|
+
};
|
|
526
|
+
const onAbort = () => finish(() => reject(signal.reason instanceof Error
|
|
527
|
+
? signal.reason
|
|
528
|
+
: new DOMException("Webhook delivery aborted", "AbortError")));
|
|
529
|
+
const timer = setTimeout(() => finish(() => reject(new DOMException("Webhook delivery timed out", "TimeoutError"))), remainingMs);
|
|
530
|
+
timer.unref();
|
|
531
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
532
|
+
if (signal.aborted) {
|
|
533
|
+
onAbort();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
async function postTelegramWebhook(state, webhook, update) {
|
|
540
|
+
const url = new URL(webhook.url);
|
|
541
|
+
const deadlineAt = Date.now() + TELEGRAM_WEBHOOK_DELIVERY_TIMEOUT_MS;
|
|
542
|
+
const controller = new AbortController();
|
|
543
|
+
state.activeWebhookDeliveries.add(controller);
|
|
544
|
+
try {
|
|
545
|
+
const target = await validateTelegramWebhookUrl(state, url, deadlineAt, controller.signal);
|
|
546
|
+
if (target instanceof Response) {
|
|
547
|
+
return { error: "Webhook target is no longer allowed" };
|
|
548
|
+
}
|
|
549
|
+
const addresses = target.addresses && target.addresses.length > 0 ? target.addresses : [undefined];
|
|
550
|
+
let lastError;
|
|
551
|
+
for (const [index, address] of addresses.entries()) {
|
|
552
|
+
if (state.closing) {
|
|
553
|
+
throw new Error("Telegram server is shutting down.");
|
|
554
|
+
}
|
|
555
|
+
const remainingMs = deadlineAt - Date.now();
|
|
556
|
+
if (remainingMs <= 0) {
|
|
557
|
+
throw new DOMException("Webhook delivery timed out", "TimeoutError");
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
const attemptsRemaining = addresses.length - index;
|
|
561
|
+
const status = await postWebhookRequest({
|
|
562
|
+
address,
|
|
563
|
+
body: JSON.stringify(update),
|
|
564
|
+
headerEntries: webhook.secretToken
|
|
565
|
+
? [["x-telegram-bot-api-secret-token", webhook.secretToken]]
|
|
566
|
+
: undefined,
|
|
567
|
+
signal: controller.signal,
|
|
568
|
+
timeoutMs: Math.max(1, Math.floor(remainingMs / attemptsRemaining)),
|
|
569
|
+
url,
|
|
570
|
+
});
|
|
571
|
+
return { status };
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
lastError = error;
|
|
575
|
+
if (state.closing || controller.signal.aborted) {
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
throw lastError;
|
|
581
|
+
}
|
|
582
|
+
finally {
|
|
583
|
+
state.activeWebhookDeliveries.delete(controller);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function replaceTelegramWebhook(state, webhook, dropPendingUpdates) {
|
|
587
|
+
let releaseAdmission;
|
|
588
|
+
const previousAdmission = state.webhookAdmission;
|
|
589
|
+
state.webhookAdmission = new Promise((resolve) => {
|
|
590
|
+
releaseAdmission = resolve;
|
|
591
|
+
});
|
|
592
|
+
await previousAdmission;
|
|
593
|
+
try {
|
|
594
|
+
state.webhook = undefined;
|
|
595
|
+
clearTelegramWebhookRetry(state, true);
|
|
596
|
+
for (const controller of state.activeWebhookDeliveries) {
|
|
597
|
+
controller.abort();
|
|
598
|
+
}
|
|
599
|
+
await state.webhookDelivery;
|
|
600
|
+
if (dropPendingUpdates) {
|
|
601
|
+
state.updates.length = 0;
|
|
602
|
+
}
|
|
603
|
+
state.webhook = webhook;
|
|
604
|
+
if (webhook) {
|
|
605
|
+
finishTelegramUpdatePoll(state, "conflict");
|
|
606
|
+
scheduleTelegramWebhookDelivery(state, 0);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
finally {
|
|
610
|
+
releaseAdmission();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function clearTelegramWebhookRetry(state, resetAttempts = false) {
|
|
614
|
+
if (state.webhookRetryTimer) {
|
|
615
|
+
clearTimeout(state.webhookRetryTimer);
|
|
616
|
+
state.webhookRetryTimer = undefined;
|
|
617
|
+
}
|
|
618
|
+
if (resetAttempts) {
|
|
619
|
+
state.webhookRetryAttempts = 0;
|
|
620
|
+
state.webhookRetryUpdateId = undefined;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function syncTelegramWebhookRetryHead(state) {
|
|
624
|
+
const updateId = state.updates[0]?.update_id;
|
|
625
|
+
if (updateId === state.webhookRetryUpdateId) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
clearTelegramWebhookRetry(state);
|
|
629
|
+
state.webhookRetryAttempts = 0;
|
|
630
|
+
state.webhookRetryUpdateId = updateId;
|
|
631
|
+
}
|
|
632
|
+
function scheduleTelegramWebhookDelivery(state, delayMs) {
|
|
633
|
+
if (state.closing ||
|
|
634
|
+
!state.webhook ||
|
|
635
|
+
state.updates.length === 0 ||
|
|
636
|
+
state.webhookDelivery ||
|
|
637
|
+
state.webhookRetryTimer) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
state.webhookRetryTimer = setTimeout(() => {
|
|
641
|
+
state.webhookRetryTimer = undefined;
|
|
642
|
+
void deliverTelegramWebhookUpdates(state, true);
|
|
643
|
+
}, delayMs);
|
|
644
|
+
state.webhookRetryTimer.unref();
|
|
645
|
+
}
|
|
646
|
+
async function deliverTelegramWebhookUpdates(state, scheduledRetry = false) {
|
|
647
|
+
if (state.webhookDelivery) {
|
|
648
|
+
return await state.webhookDelivery;
|
|
649
|
+
}
|
|
650
|
+
syncTelegramWebhookRetryHead(state);
|
|
651
|
+
if (!scheduledRetry && state.webhookRetryTimer) {
|
|
652
|
+
return telegramError("Bad Gateway: webhook delivery failed", 502);
|
|
653
|
+
}
|
|
654
|
+
let attemptedUpdateId;
|
|
655
|
+
const delivery = flushTelegramWebhookUpdates(state, (updateId) => {
|
|
656
|
+
attemptedUpdateId = updateId;
|
|
657
|
+
});
|
|
658
|
+
state.webhookDelivery = delivery;
|
|
659
|
+
let result;
|
|
660
|
+
try {
|
|
661
|
+
result = await delivery;
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
finally {
|
|
665
|
+
if (state.webhookDelivery === delivery) {
|
|
666
|
+
state.webhookDelivery = undefined;
|
|
667
|
+
}
|
|
668
|
+
if (state.webhook && state.updates.length > 0) {
|
|
669
|
+
if (state.updates[0]?.update_id !== attemptedUpdateId) {
|
|
670
|
+
syncTelegramWebhookRetryHead(state);
|
|
671
|
+
scheduleTelegramWebhookDelivery(state, 0);
|
|
672
|
+
}
|
|
673
|
+
else if (result) {
|
|
674
|
+
const delayMs = TELEGRAM_WEBHOOK_RETRY_BASE_MS *
|
|
675
|
+
2 ** Math.min(state.webhookRetryAttempts, TELEGRAM_WEBHOOK_MAX_BACKOFF_EXPONENT);
|
|
676
|
+
state.webhookRetryAttempts = Math.min(state.webhookRetryAttempts + 1, TELEGRAM_WEBHOOK_MAX_BACKOFF_EXPONENT);
|
|
677
|
+
scheduleTelegramWebhookDelivery(state, delayMs);
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
state.webhookRetryAttempts = 0;
|
|
681
|
+
scheduleTelegramWebhookDelivery(state, 0);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
246
685
|
}
|
|
247
686
|
async function handleTelegramApi(params) {
|
|
248
|
-
|
|
249
|
-
|
|
687
|
+
const method = params.method.toLowerCase();
|
|
688
|
+
switch (method) {
|
|
689
|
+
case "getme":
|
|
250
690
|
return telegramOk(createBotUser(params.state));
|
|
251
|
-
case "
|
|
252
|
-
case "
|
|
253
|
-
case "
|
|
254
|
-
case "
|
|
255
|
-
case "
|
|
256
|
-
case "
|
|
257
|
-
case "
|
|
258
|
-
case "
|
|
259
|
-
case "
|
|
260
|
-
|
|
261
|
-
case "
|
|
691
|
+
case "setmycommands":
|
|
692
|
+
case "deletemycommands":
|
|
693
|
+
case "sendchataction":
|
|
694
|
+
case "answercallbackquery":
|
|
695
|
+
case "deletemessage":
|
|
696
|
+
case "pinchatmessage":
|
|
697
|
+
case "unpinchatmessage":
|
|
698
|
+
case "setmessagereaction":
|
|
699
|
+
case "editforumtopic":
|
|
700
|
+
return telegramOk(true);
|
|
701
|
+
case "setwebhook": {
|
|
702
|
+
if (params.body.url === "") {
|
|
703
|
+
await replaceTelegramWebhook(params.state, undefined, toBooleanValue(params.body.drop_pending_updates));
|
|
704
|
+
return telegramOk(true);
|
|
705
|
+
}
|
|
706
|
+
const webhookUrl = toStringValue(params.body.url);
|
|
707
|
+
if (!webhookUrl) {
|
|
708
|
+
return telegramError("Bad Request: url is required");
|
|
709
|
+
}
|
|
710
|
+
let parsedUrl;
|
|
711
|
+
try {
|
|
712
|
+
parsedUrl = new URL(webhookUrl);
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
return telegramError("Bad Request: bad webhook URL");
|
|
716
|
+
}
|
|
717
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
718
|
+
return telegramError("Bad Request: bad webhook URL");
|
|
719
|
+
}
|
|
720
|
+
if (params.state.activeWebhookValidations.size >= MAX_ACTIVE_TELEGRAM_WEBHOOK_VALIDATIONS) {
|
|
721
|
+
return telegramError("Too Many Requests: too many webhook validations", 429);
|
|
722
|
+
}
|
|
723
|
+
const validation = new AbortController();
|
|
724
|
+
params.state.activeWebhookValidations.add(validation);
|
|
725
|
+
const target = await validateTelegramWebhookUrl(params.state, parsedUrl, Date.now() + TELEGRAM_WEBHOOK_DELIVERY_TIMEOUT_MS, validation.signal).finally(() => {
|
|
726
|
+
params.state.activeWebhookValidations.delete(validation);
|
|
727
|
+
});
|
|
728
|
+
if (target instanceof Response) {
|
|
729
|
+
return target;
|
|
730
|
+
}
|
|
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
|
+
await replaceTelegramWebhook(params.state, {
|
|
736
|
+
...(secretToken ? { secretToken } : {}),
|
|
737
|
+
url: parsedUrl.href,
|
|
738
|
+
}, toBooleanValue(params.body.drop_pending_updates));
|
|
739
|
+
return telegramOk(true);
|
|
740
|
+
}
|
|
741
|
+
case "deletewebhook":
|
|
742
|
+
await replaceTelegramWebhook(params.state, undefined, toBooleanValue(params.body.drop_pending_updates));
|
|
262
743
|
return telegramOk(true);
|
|
263
|
-
case "
|
|
744
|
+
case "getwebhookinfo":
|
|
745
|
+
return telegramOk({
|
|
746
|
+
has_custom_certificate: false,
|
|
747
|
+
...(params.state.webhook?.lastErrorDate
|
|
748
|
+
? { last_error_date: params.state.webhook.lastErrorDate }
|
|
749
|
+
: {}),
|
|
750
|
+
...(params.state.webhook?.lastErrorMessage
|
|
751
|
+
? { last_error_message: params.state.webhook.lastErrorMessage }
|
|
752
|
+
: {}),
|
|
753
|
+
pending_update_count: params.state.updates.length,
|
|
754
|
+
url: params.state.webhook?.url ?? "",
|
|
755
|
+
});
|
|
756
|
+
case "createforumtopic":
|
|
264
757
|
return telegramOk({
|
|
265
758
|
icon_color: 0x6fb9f0,
|
|
266
759
|
message_thread_id: params.state.nextMessageId++,
|
|
267
760
|
name: toStringValue(params.body.name) ?? "Crabline Topic",
|
|
268
761
|
});
|
|
269
|
-
case "
|
|
762
|
+
case "editmessagetext": {
|
|
270
763
|
const message = createEditedMessage(params.state, params.body);
|
|
271
764
|
return message
|
|
272
765
|
? telegramOk(message)
|
|
273
766
|
: telegramError("Bad Request: chat_id, message_id, and text are required");
|
|
274
767
|
}
|
|
275
|
-
case "
|
|
768
|
+
case "sendmessage": {
|
|
276
769
|
const message = createOutboundMessage(params.state, params.body);
|
|
277
770
|
return message
|
|
278
771
|
? telegramOk(message)
|
|
279
772
|
: telegramError("Bad Request: chat_id and text are required");
|
|
280
773
|
}
|
|
281
|
-
case "
|
|
282
|
-
case "
|
|
283
|
-
case "
|
|
284
|
-
case "
|
|
285
|
-
|
|
774
|
+
case "sendanimation":
|
|
775
|
+
case "sendaudio":
|
|
776
|
+
case "senddocument":
|
|
777
|
+
case "sendphoto":
|
|
778
|
+
case "sendvideo": {
|
|
779
|
+
const mediaKind = method.slice("send".length);
|
|
286
780
|
const message = createOutboundMediaMessage(params.state, params.body, mediaKind);
|
|
287
|
-
return message
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const offset = toIntegerValue(params.body.offset);
|
|
291
|
-
const limit = toIntegerValue(params.body.limit) ?? 100;
|
|
292
|
-
const updates = offset === undefined
|
|
293
|
-
? params.state.updates
|
|
294
|
-
: params.state.updates.filter((update) => update.update_id >= offset);
|
|
295
|
-
return telegramOk(updates.slice(0, limit));
|
|
781
|
+
return message
|
|
782
|
+
? telegramOk(message)
|
|
783
|
+
: telegramError(`Bad Request: chat_id and ${mediaKind} are required`);
|
|
296
784
|
}
|
|
785
|
+
case "getupdates":
|
|
786
|
+
if (params.state.webhook) {
|
|
787
|
+
return telegramError("Conflict: can't use getUpdates method while webhook is active", 409);
|
|
788
|
+
}
|
|
789
|
+
return await handleTelegramGetUpdates(params);
|
|
297
790
|
default:
|
|
298
791
|
return telegramError(`Not Found: unsupported method ${params.method}`, 404);
|
|
299
792
|
}
|
|
300
793
|
}
|
|
794
|
+
/** @internal */
|
|
795
|
+
export async function handleTelegramGetUpdates(params) {
|
|
796
|
+
const offset = toIntegerValue(params.body.offset);
|
|
797
|
+
const limit = toIntegerValue(params.body.limit) ?? 100;
|
|
798
|
+
const timeout = toIntegerValue(params.body.timeout) ?? 0;
|
|
799
|
+
if (params.body.offset !== undefined && offset === undefined) {
|
|
800
|
+
return telegramError("Bad Request: offset must be a safe integer");
|
|
801
|
+
}
|
|
802
|
+
if (params.body.limit !== undefined && toIntegerValue(params.body.limit) === undefined) {
|
|
803
|
+
return telegramError("Bad Request: limit must be a safe integer");
|
|
804
|
+
}
|
|
805
|
+
if (params.body.timeout !== undefined && toIntegerValue(params.body.timeout) === undefined) {
|
|
806
|
+
return telegramError("Bad Request: timeout must be a safe integer");
|
|
807
|
+
}
|
|
808
|
+
if (limit < 1 || limit > 100) {
|
|
809
|
+
return telegramError("Bad Request: limit must be between 1 and 100");
|
|
810
|
+
}
|
|
811
|
+
if (timeout < 0) {
|
|
812
|
+
return telegramError("Bad Request: timeout must be non-negative");
|
|
813
|
+
}
|
|
814
|
+
finishTelegramUpdatePoll(params.state, "conflict");
|
|
815
|
+
let updates = takeTelegramUpdates(params.state, offset, limit);
|
|
816
|
+
const deadline = Date.now() + Math.min(timeout * 1_000, 2_147_483_647);
|
|
817
|
+
if (updates.length === 0 && timeout > 0) {
|
|
818
|
+
while (updates.length === 0) {
|
|
819
|
+
const remainingMs = deadline - Date.now();
|
|
820
|
+
if (remainingMs <= 0) {
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
const pollResult = await waitForTelegramUpdate(params.state, params.request, remainingMs, () => hasTelegramUpdates(params.state, offset));
|
|
824
|
+
if (pollResult === "conflict") {
|
|
825
|
+
return telegramError("Conflict: terminated by other getUpdates request; make sure that only one bot instance is running", 409);
|
|
826
|
+
}
|
|
827
|
+
if (pollResult !== "update") {
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
updates = takeTelegramUpdates(params.state, offset, limit);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return telegramOk(updates);
|
|
834
|
+
}
|
|
835
|
+
function takeTelegramUpdates(state, offset, limit) {
|
|
836
|
+
if (offset !== undefined && offset < 0) {
|
|
837
|
+
const retainedCount = Math.min(Math.abs(offset), state.updates.length);
|
|
838
|
+
state.updates.splice(0, state.updates.length - retainedCount);
|
|
839
|
+
}
|
|
840
|
+
else if (offset !== undefined) {
|
|
841
|
+
const firstUnconfirmed = state.updates.findIndex((update) => update.update_id >= offset);
|
|
842
|
+
if (firstUnconfirmed === -1) {
|
|
843
|
+
state.updates.length = 0;
|
|
844
|
+
}
|
|
845
|
+
else if (firstUnconfirmed > 0) {
|
|
846
|
+
state.updates.splice(0, firstUnconfirmed);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return state.updates.slice(0, limit);
|
|
850
|
+
}
|
|
851
|
+
function hasTelegramUpdates(state, offset) {
|
|
852
|
+
if (offset === undefined || offset < 0) {
|
|
853
|
+
return state.updates.length > 0;
|
|
854
|
+
}
|
|
855
|
+
return state.updates.some((update) => update.update_id >= offset);
|
|
856
|
+
}
|
|
857
|
+
function finishTelegramUpdatePoll(state, result) {
|
|
858
|
+
state.activeUpdatePoll?.finish(result);
|
|
859
|
+
}
|
|
860
|
+
async function waitForTelegramUpdate(state, request, timeoutMs, hasUpdate) {
|
|
861
|
+
if (state.closing || request.socket.destroyed) {
|
|
862
|
+
return "shutdown";
|
|
863
|
+
}
|
|
864
|
+
return await new Promise((resolve) => {
|
|
865
|
+
let settled = false;
|
|
866
|
+
const poll = {
|
|
867
|
+
finish(result) {
|
|
868
|
+
if (settled) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
settled = true;
|
|
872
|
+
clearTimeout(timer);
|
|
873
|
+
request.socket.off("close", onSocketClose);
|
|
874
|
+
if (state.activeUpdatePoll === poll) {
|
|
875
|
+
state.activeUpdatePoll = undefined;
|
|
876
|
+
}
|
|
877
|
+
resolve(result);
|
|
878
|
+
},
|
|
879
|
+
};
|
|
880
|
+
const onSocketClose = () => poll.finish("shutdown");
|
|
881
|
+
const timer = setTimeout(() => poll.finish("timeout"), timeoutMs);
|
|
882
|
+
timer.unref();
|
|
883
|
+
const previousPoll = state.activeUpdatePoll;
|
|
884
|
+
state.activeUpdatePoll = poll;
|
|
885
|
+
previousPoll?.finish("conflict");
|
|
886
|
+
request.socket.once("close", onSocketClose);
|
|
887
|
+
if (state.closing || request.socket.destroyed) {
|
|
888
|
+
poll.finish("shutdown");
|
|
889
|
+
}
|
|
890
|
+
else if (hasUpdate()) {
|
|
891
|
+
poll.finish("update");
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
function telegramMethodNotAllowed() {
|
|
896
|
+
const response = telegramError("Method Not Allowed", 405);
|
|
897
|
+
response.headers.set("allow", "GET, POST");
|
|
898
|
+
return response;
|
|
899
|
+
}
|
|
301
900
|
async function handleRequest(params) {
|
|
302
901
|
const url = new URL(params.request.url ?? "/", "http://127.0.0.1");
|
|
303
902
|
if (url.pathname === "/crabline/telegram/inbound") {
|
|
@@ -305,65 +904,113 @@ async function handleRequest(params) {
|
|
|
305
904
|
return new Response("not found", { status: 404 });
|
|
306
905
|
}
|
|
307
906
|
if (!hasAdminToken(params.request, params.state.adminToken)) {
|
|
907
|
+
drainRequestBody(params.request);
|
|
308
908
|
return adminAuthError();
|
|
309
909
|
}
|
|
310
910
|
const body = await parseRequestBody(params.request);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
911
|
+
if (!isJsonObject(body)) {
|
|
912
|
+
return telegramError("Bad Request: request body must be a JSON object");
|
|
913
|
+
}
|
|
914
|
+
return await handleTelegramAdminInbound({
|
|
314
915
|
body,
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
type: "admin",
|
|
916
|
+
request: params.request,
|
|
917
|
+
state: params.state,
|
|
918
|
+
url,
|
|
319
919
|
});
|
|
320
|
-
if (!update) {
|
|
321
|
-
return telegramError("Bad Request: chatId and text are required");
|
|
322
|
-
}
|
|
323
|
-
params.state.updates.push(update);
|
|
324
|
-
return jsonResponse({ ok: true, update });
|
|
325
920
|
}
|
|
326
921
|
const botPath = requireTelegramBotPath(url.pathname);
|
|
327
922
|
if (!botPath || botPath.token !== params.state.botToken) {
|
|
923
|
+
drainRequestBody(params.request);
|
|
328
924
|
return new Response("not found", { status: 404 });
|
|
329
925
|
}
|
|
330
|
-
const
|
|
331
|
-
|
|
926
|
+
const requestMethod = params.request.method ?? "GET";
|
|
927
|
+
if (requestMethod !== "GET" && requestMethod !== "POST") {
|
|
928
|
+
drainRequestBody(params.request);
|
|
929
|
+
return telegramMethodNotAllowed();
|
|
930
|
+
}
|
|
931
|
+
const body = requestMethod === "GET" ? queryRecord(url) : await parseRequestBody(params.request);
|
|
932
|
+
if (!isJsonObject(body)) {
|
|
933
|
+
return telegramError("Bad Request: can't parse JSON object");
|
|
934
|
+
}
|
|
935
|
+
const event = {
|
|
332
936
|
at: new Date().toISOString(),
|
|
333
|
-
body,
|
|
334
|
-
method:
|
|
937
|
+
body: redactTelegramSecrets(body),
|
|
938
|
+
method: requestMethod,
|
|
335
939
|
path: `/bot<redacted>/${botPath.method}`,
|
|
336
|
-
query: queryRecord(url),
|
|
940
|
+
query: redactTelegramSecrets(queryRecord(url)),
|
|
337
941
|
type: "api",
|
|
338
|
-
}
|
|
339
|
-
|
|
942
|
+
};
|
|
943
|
+
if (TELEGRAM_SEND_METHODS.has(botPath.method.toLowerCase())) {
|
|
944
|
+
const response = await handleTelegramApi({
|
|
945
|
+
body,
|
|
946
|
+
method: botPath.method,
|
|
947
|
+
request: params.request,
|
|
948
|
+
state: params.state,
|
|
949
|
+
});
|
|
950
|
+
event.accepted = response.ok;
|
|
951
|
+
try {
|
|
952
|
+
await appendEvent(params.state, event);
|
|
953
|
+
}
|
|
954
|
+
catch (error) {
|
|
955
|
+
if (!event.accepted) {
|
|
956
|
+
throw error;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return response;
|
|
960
|
+
}
|
|
961
|
+
await appendEvent(params.state, event);
|
|
962
|
+
return await handleTelegramApi({
|
|
340
963
|
body,
|
|
341
964
|
method: botPath.method,
|
|
965
|
+
request: params.request,
|
|
342
966
|
state: params.state,
|
|
343
967
|
});
|
|
344
968
|
}
|
|
345
969
|
export async function startTelegramServer(params = {}) {
|
|
970
|
+
const host = params.host ?? "127.0.0.1";
|
|
971
|
+
const botId = params.botId ?? 424242;
|
|
972
|
+
if (!Number.isSafeInteger(botId) || botId < 1) {
|
|
973
|
+
throw new Error("botId must be a positive safe integer.");
|
|
974
|
+
}
|
|
346
975
|
const state = {
|
|
976
|
+
activeUpdatePoll: undefined,
|
|
977
|
+
activeWebhookDeliveries: new Set(),
|
|
978
|
+
activeWebhookValidations: new Set(),
|
|
347
979
|
adminToken: params.adminToken ?? randomBytes(24).toString("hex"),
|
|
348
|
-
|
|
349
|
-
|
|
980
|
+
allowLoopbackHttpWebhook: isLoopbackHost(host),
|
|
981
|
+
botId,
|
|
982
|
+
botToken: params.botToken ??
|
|
983
|
+
(isLoopbackHost(host)
|
|
984
|
+
? "424242:crabline-telegram-token"
|
|
985
|
+
: `${botId}:${randomBytes(26).toString("base64url")}`),
|
|
350
986
|
botUsername: params.botUsername ?? "crabline_bot",
|
|
987
|
+
inboundAdmission: Promise.resolve(),
|
|
988
|
+
maxPendingInboundEvents: resolveMaxPendingInboundEvents(params.maxPendingInboundEvents),
|
|
351
989
|
nextMessageId: 1,
|
|
352
990
|
nextUpdateId: 1,
|
|
991
|
+
onEvent: params.onEvent,
|
|
353
992
|
recorderPath: params.recorderPath ?? path.resolve(".crabline", "servers", "telegram.jsonl"),
|
|
993
|
+
restrictWebhookTargets: true,
|
|
994
|
+
closing: false,
|
|
354
995
|
updates: [],
|
|
996
|
+
webhook: undefined,
|
|
997
|
+
webhookAdmission: Promise.resolve(),
|
|
998
|
+
webhookDelivery: undefined,
|
|
999
|
+
webhookRetryAttempts: 0,
|
|
1000
|
+
webhookRetryTimer: undefined,
|
|
1001
|
+
webhookRetryUpdateId: undefined,
|
|
355
1002
|
};
|
|
356
|
-
const host = params.host ?? "127.0.0.1";
|
|
357
1003
|
const port = params.port ?? 0;
|
|
358
1004
|
const server = createServer(async (request, response) => {
|
|
359
1005
|
try {
|
|
360
1006
|
await writeResponse(response, await handleRequest({ request, state }));
|
|
361
1007
|
}
|
|
362
1008
|
catch (error) {
|
|
363
|
-
await writeResponse(response,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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));
|
|
367
1014
|
}
|
|
368
1015
|
});
|
|
369
1016
|
await new Promise((resolve, reject) => {
|
|
@@ -379,9 +1026,21 @@ export async function startTelegramServer(params = {}) {
|
|
|
379
1026
|
kind: "connectivity",
|
|
380
1027
|
});
|
|
381
1028
|
}
|
|
382
|
-
const baseUrl = `http://${host}:${address.port}`;
|
|
1029
|
+
const baseUrl = `http://${formatUrlHost(host)}:${address.port}`;
|
|
383
1030
|
return {
|
|
384
1031
|
async close() {
|
|
1032
|
+
state.closing = true;
|
|
1033
|
+
finishTelegramUpdatePoll(state, "shutdown");
|
|
1034
|
+
clearTelegramWebhookRetry(state);
|
|
1035
|
+
for (const controller of state.activeWebhookDeliveries) {
|
|
1036
|
+
controller.abort();
|
|
1037
|
+
}
|
|
1038
|
+
for (const controller of state.activeWebhookValidations) {
|
|
1039
|
+
controller.abort();
|
|
1040
|
+
}
|
|
1041
|
+
await state.webhookDelivery;
|
|
1042
|
+
await state.webhookAdmission;
|
|
1043
|
+
await state.inboundAdmission;
|
|
385
1044
|
await closeServer(server);
|
|
386
1045
|
},
|
|
387
1046
|
manifest: {
|