@prajwalshete/telegram-plugin 0.0.0-stage → 0.1.0
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/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +109 -2
- package/dist/adapters/clock.d.ts +3 -0
- package/dist/adapters/clock.js +4 -0
- package/dist/adapters/console-log.d.ts +13 -0
- package/dist/adapters/console-log.js +41 -0
- package/dist/adapters/d1-db.d.ts +5 -0
- package/dist/adapters/d1-db.js +37 -0
- package/dist/adapters/ids.d.ts +5 -0
- package/dist/adapters/ids.js +25 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/keyring.d.ts +50 -0
- package/dist/adapters/keyring.js +174 -0
- package/dist/adapters/metrics.d.ts +28 -0
- package/dist/adapters/metrics.js +48 -0
- package/dist/adapters/r2-files.d.ts +20 -0
- package/dist/adapters/r2-files.js +53 -0
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +45 -0
- package/dist/client/broadcasts.d.ts +35 -0
- package/dist/client/commands.d.ts +23 -0
- package/dist/client/endpoint.d.ts +34 -0
- package/dist/client/events.d.ts +33 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.js +3 -0
- package/dist/client/schedules.d.ts +42 -0
- package/dist/client/schema.d.ts +181 -0
- package/dist/client/send.d.ts +15 -0
- package/dist/client/templates.d.ts +47 -0
- package/dist/contracts/bots.d.ts +129 -0
- package/dist/contracts/bots.js +70 -0
- package/dist/contracts/broadcasts.d.ts +116 -0
- package/dist/contracts/broadcasts.js +63 -0
- package/dist/contracts/chats.d.ts +97 -0
- package/dist/contracts/chats.js +66 -0
- package/dist/contracts/commands.d.ts +52 -0
- package/dist/contracts/commands.js +53 -0
- package/dist/contracts/common.d.ts +38 -0
- package/dist/contracts/common.js +41 -0
- package/dist/contracts/deliveries.d.ts +103 -0
- package/dist/contracts/deliveries.js +74 -0
- package/dist/contracts/destinations.d.ts +24 -0
- package/dist/contracts/destinations.js +28 -0
- package/dist/contracts/events.d.ts +165 -0
- package/dist/contracts/events.js +98 -0
- package/dist/contracts/features.d.ts +20 -0
- package/dist/contracts/features.js +21 -0
- package/dist/contracts/health.d.ts +55 -0
- package/dist/contracts/health.js +58 -0
- package/dist/contracts/index.d.ts +22 -0
- package/dist/contracts/index.js +22 -0
- package/dist/contracts/labels.d.ts +38 -0
- package/dist/contracts/labels.js +52 -0
- package/dist/contracts/links.d.ts +51 -0
- package/dist/contracts/links.js +50 -0
- package/dist/contracts/meta.d.ts +132 -0
- package/dist/contracts/meta.js +85 -0
- package/dist/contracts/permissions.d.ts +24 -0
- package/dist/contracts/permissions.js +27 -0
- package/dist/contracts/problems.d.ts +106 -0
- package/dist/contracts/problems.js +101 -0
- package/dist/contracts/recipients.d.ts +36 -0
- package/dist/contracts/recipients.js +22 -0
- package/dist/contracts/schedules.d.ts +276 -0
- package/dist/contracts/schedules.js +143 -0
- package/dist/contracts/send.d.ts +126 -0
- package/dist/contracts/send.js +69 -0
- package/dist/contracts/subscriptions.d.ts +38 -0
- package/dist/contracts/subscriptions.js +31 -0
- package/dist/contracts/templates.d.ts +358 -0
- package/dist/contracts/templates.js +174 -0
- package/dist/contracts/users.d.ts +15 -0
- package/dist/contracts/users.js +19 -0
- package/dist/core/guards.d.ts +22 -0
- package/dist/core/guards.js +47 -0
- package/dist/core/html/escape.d.ts +2 -0
- package/dist/core/html/escape.js +36 -0
- package/dist/core/html/tokenize.js +65 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/pacing.d.ts +1 -0
- package/dist/core/pacing.js +86 -0
- package/dist/core/random.d.ts +2 -0
- package/dist/core/random.js +38 -0
- package/dist/core/recipients.d.ts +1 -0
- package/dist/core/recipients.js +86 -0
- package/dist/core/schedule/describe.d.ts +3 -0
- package/dist/core/schedule/describe.js +74 -0
- package/dist/core/schedule/index.d.ts +4 -0
- package/dist/core/schedule/policy.d.ts +1 -0
- package/dist/core/schedule/policy.js +37 -0
- package/dist/core/schedule/rule.d.ts +25 -0
- package/dist/core/schedule/rule.js +142 -0
- package/dist/core/schedule/tz.d.ts +2 -0
- package/dist/core/schedule/tz.js +59 -0
- package/dist/core/split/plain.d.ts +2 -0
- package/dist/core/split/plain.js +23 -0
- package/dist/core/split/split.d.ts +15 -0
- package/dist/core/split/split.js +158 -0
- package/dist/core/template/ast.d.ts +58 -0
- package/dist/core/template/ast.js +5 -0
- package/dist/core/template/expr.js +108 -0
- package/dist/core/template/filters.d.ts +20 -0
- package/dist/core/template/filters.js +127 -0
- package/dist/core/template/format.d.ts +17 -0
- package/dist/core/template/format.js +88 -0
- package/dist/core/template/index.d.ts +20 -0
- package/dist/core/template/index.js +29 -0
- package/dist/core/template/parse.d.ts +8 -0
- package/dist/core/template/parse.js +369 -0
- package/dist/core/template/render.d.ts +17 -0
- package/dist/core/template/render.js +118 -0
- package/dist/core/template/shape.d.ts +28 -0
- package/dist/core/template/shape.js +151 -0
- package/dist/core/template/tags.js +105 -0
- package/dist/core/template/validate.d.ts +10 -0
- package/dist/core/template/validate.js +144 -0
- package/dist/core/template/variables.d.ts +15 -0
- package/dist/core/template/variables.js +41 -0
- package/dist/do/index.d.ts +3 -0
- package/dist/do/index.js +3 -0
- package/dist/do/telegram-bot-do.d.ts +45 -0
- package/dist/do/telegram-bot-do.js +219 -0
- package/dist/engine/actions/callback.js +256 -0
- package/dist/engine/actions/tokens.d.ts +2 -0
- package/dist/engine/actions/tokens.js +51 -0
- package/dist/engine/bot-api.js +20 -0
- package/dist/engine/bot-features.js +39 -0
- package/dist/engine/chat-state.js +66 -0
- package/dist/engine/content/audience.js +54 -0
- package/dist/engine/content/buttons.d.ts +1 -0
- package/dist/engine/content/buttons.js +113 -0
- package/dist/engine/content/fanout.js +284 -0
- package/dist/engine/content/intersect.d.ts +6 -0
- package/dist/engine/content/intersect.js +26 -0
- package/dist/engine/content/providers.d.ts +6 -0
- package/dist/engine/content/providers.js +142 -0
- package/dist/engine/content/recipients.js +128 -0
- package/dist/engine/content/render.js +138 -0
- package/dist/engine/content/reply.js +156 -0
- package/dist/engine/content/report-files.js +78 -0
- package/dist/engine/content/template-rows.js +74 -0
- package/dist/engine/dialogs/dialog.d.ts +2 -0
- package/dist/engine/dialogs/dialog.js +375 -0
- package/dist/engine/dispatch/builtins.js +198 -0
- package/dist/engine/dispatch/commands.js +194 -0
- package/dist/engine/dispatch/context.d.ts +2 -0
- package/dist/engine/dispatch/context.js +194 -0
- package/dist/engine/dispatch/dispatcher.js +66 -0
- package/dist/engine/dispatch/handlers.d.ts +1 -0
- package/dist/engine/dispatch/handlers.js +103 -0
- package/dist/engine/dispatch/lifecycle.js +85 -0
- package/dist/engine/dispatch/link-chat.js +31 -0
- package/dist/engine/dispatch/linking.js +273 -0
- package/dist/engine/errors.d.ts +60 -0
- package/dist/engine/errors.js +72 -0
- package/dist/engine/events/filters.d.ts +5 -0
- package/dist/engine/events/filters.js +82 -0
- package/dist/engine/events/kick.js +30 -0
- package/dist/engine/events/payload.d.ts +3 -0
- package/dist/engine/events/payload.js +16 -0
- package/dist/engine/events/tick.js +208 -0
- package/dist/engine/i18n.d.ts +12 -0
- package/dist/engine/i18n.js +55 -0
- package/dist/engine/inbound/files.js +121 -0
- package/dist/engine/inbound/handler.js +46 -0
- package/dist/engine/jobs.d.ts +32 -0
- package/dist/engine/jobs.js +85 -0
- package/dist/engine/link-codes.js +46 -0
- package/dist/engine/metrics.js +12 -0
- package/dist/engine/options.d.ts +369 -0
- package/dist/engine/options.js +81 -0
- package/dist/engine/outbox/dead-letter.js +78 -0
- package/dist/engine/outbox/do-registry.d.ts +10 -0
- package/dist/engine/outbox/do-registry.js +10 -0
- package/dist/engine/outbox/do-transport.d.ts +33 -0
- package/dist/engine/outbox/do-transport.js +73 -0
- package/dist/engine/outbox/drain.d.ts +13 -0
- package/dist/engine/outbox/drain.js +381 -0
- package/dist/engine/outbox/enqueue.d.ts +1 -0
- package/dist/engine/outbox/enqueue.js +63 -0
- package/dist/engine/outbox/select.js +24 -0
- package/dist/engine/outbox/send.js +362 -0
- package/dist/engine/outbox/transport.d.ts +35 -0
- package/dist/engine/outbox/transport.js +175 -0
- package/dist/engine/outbox/wake.js +28 -0
- package/dist/engine/registry.d.ts +502 -0
- package/dist/engine/registry.js +258 -0
- package/dist/engine/rows.d.ts +25 -0
- package/dist/engine/rows.js +131 -0
- package/dist/engine/runtime.d.ts +95 -0
- package/dist/engine/runtime.js +149 -0
- package/dist/engine/schedules/fire.js +208 -0
- package/dist/engine/schedules/resume.js +75 -0
- package/dist/engine/schedules/rows.js +69 -0
- package/dist/engine/schedules/tick.js +30 -0
- package/dist/engine/schedules/wake.js +50 -0
- package/dist/engine/state.d.ts +11 -0
- package/dist/engine/state.js +78 -0
- package/dist/locales/en.js +339 -0
- package/dist/locales/en.json +341 -0
- package/dist/locales/mr.js +339 -0
- package/dist/locales/mr.json +341 -0
- package/dist/ports/db.d.ts +22 -0
- package/dist/ports/index.d.ts +4 -0
- package/dist/ports/ports.d.ts +257 -0
- package/dist/ports/sql.d.ts +39 -0
- package/dist/ports/sql.js +76 -0
- package/dist/renderers/csv.d.ts +14 -0
- package/dist/renderers/csv.js +51 -0
- package/dist/renderers/html.d.ts +44 -0
- package/dist/renderers/html.js +81 -0
- package/dist/renderers/index.d.ts +5 -0
- package/dist/renderers/index.js +4 -0
- package/dist/renderers/table.d.ts +30 -0
- package/dist/renderers/table.js +114 -0
- package/dist/routes/admin.js +146 -0
- package/dist/routes/app.d.ts +3111 -0
- package/dist/routes/app.js +9 -0
- package/dist/routes/broadcasts.js +37 -0
- package/dist/routes/commands.js +35 -0
- package/dist/routes/content.js +12 -0
- package/dist/routes/events.js +36 -0
- package/dist/routes/http.d.ts +30 -0
- package/dist/routes/http.js +223 -0
- package/dist/routes/me.js +59 -0
- package/dist/routes/schedules.js +62 -0
- package/dist/routes/send.js +24 -0
- package/dist/routes/templates.js +70 -0
- package/dist/routes/webhook.js +147 -0
- package/dist/schema/columns.js +307 -0
- package/dist/schema/ddl.d.ts +7 -0
- package/dist/schema/ddl.js +45 -0
- package/dist/schema/index.d.ts +4255 -0
- package/dist/schema/index.js +44 -0
- package/dist/schema/reference.sql +359 -0
- package/dist/schema/tables.d.ts +4243 -0
- package/dist/schema/tables.js +327 -0
- package/dist/schema/version.d.ts +5 -0
- package/dist/schema/version.js +7 -0
- package/dist/server/index.d.ts +55 -0
- package/dist/server/index.js +46 -0
- package/dist/server/plugin.d.ts +81 -0
- package/dist/server/plugin.js +247 -0
- package/dist/server/version.d.ts +2 -0
- package/dist/server/version.js +4 -0
- package/dist/services/ai-tool.d.ts +26 -0
- package/dist/services/ai-tool.js +148 -0
- package/dist/services/bots.js +300 -0
- package/dist/services/broadcasts.js +390 -0
- package/dist/services/chats.js +83 -0
- package/dist/services/cleanup.js +83 -0
- package/dist/services/commands.js +140 -0
- package/dist/services/deliveries.js +53 -0
- package/dist/services/destinations.js +91 -0
- package/dist/services/event-rules.js +229 -0
- package/dist/services/events.js +20 -0
- package/dist/services/idempotency.js +64 -0
- package/dist/services/links.js +200 -0
- package/dist/services/menu.js +95 -0
- package/dist/services/ops.js +90 -0
- package/dist/services/rekey.js +63 -0
- package/dist/services/reports.js +87 -0
- package/dist/services/schedules.js +214 -0
- package/dist/services/send.d.ts +14 -0
- package/dist/services/send.js +421 -0
- package/dist/services/subscriptions.js +28 -0
- package/dist/services/templates.js +259 -0
- package/dist/telegram/classify.d.ts +30 -0
- package/dist/telegram/classify.js +164 -0
- package/dist/telegram/client.d.ts +73 -0
- package/dist/telegram/client.js +135 -0
- package/dist/telegram/errors.d.ts +37 -0
- package/dist/telegram/errors.js +62 -0
- package/dist/telegram/inbound.d.ts +40 -0
- package/dist/telegram/inbound.js +72 -0
- package/dist/telegram/index.d.ts +8 -0
- package/dist/telegram/multipart.d.ts +5 -0
- package/dist/telegram/multipart.js +47 -0
- package/dist/telegram/types.d.ts +271 -0
- package/dist/telegram/update.d.ts +833 -0
- package/dist/telegram/update.js +129 -0
- package/dist/testing/fake-html.d.ts +9 -0
- package/dist/testing/fake-html.js +83 -0
- package/dist/testing/fake-telegram.d.ts +206 -0
- package/dist/testing/fake-telegram.js +856 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +7 -0
- package/dist/testing/reference-sql.d.ts +4 -0
- package/dist/testing/reference-sql.js +8 -0
- package/dist/testing/test-plugin.d.ts +126 -0
- package/dist/testing/test-plugin.js +182 -0
- package/dist/testing/updates.d.ts +100 -0
- package/dist/testing/updates.js +212 -0
- package/package.json +113 -3
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
import { checkTelegramHtml } from "./fake-html.js";
|
|
2
|
+
// src/testing/fake-telegram.ts
|
|
3
|
+
/** A Bot API error reply, thrown inside method handlers. */
|
|
4
|
+
var Fail = class {
|
|
5
|
+
status;
|
|
6
|
+
description;
|
|
7
|
+
parameters;
|
|
8
|
+
constructor(status, description, parameters) {
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.description = description;
|
|
11
|
+
this.parameters = parameters;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const MAX_TEXT = 4096;
|
|
15
|
+
const MAX_CAPTION = 1024;
|
|
16
|
+
const MAX_UPLOAD = 52428800;
|
|
17
|
+
const MAX_PHOTO = 10485760;
|
|
18
|
+
const SEND_METHODS = /* @__PURE__ */ new Set([
|
|
19
|
+
"sendmessage",
|
|
20
|
+
"senddocument",
|
|
21
|
+
"sendphoto"
|
|
22
|
+
]);
|
|
23
|
+
const METHOD_BY_LOWER = new Map([
|
|
24
|
+
"getMe",
|
|
25
|
+
"setWebhook",
|
|
26
|
+
"deleteWebhook",
|
|
27
|
+
"getWebhookInfo",
|
|
28
|
+
"getUpdates",
|
|
29
|
+
"setMyCommands",
|
|
30
|
+
"deleteMyCommands",
|
|
31
|
+
"getMyCommands",
|
|
32
|
+
"sendMessage",
|
|
33
|
+
"sendDocument",
|
|
34
|
+
"sendPhoto",
|
|
35
|
+
"sendChatAction",
|
|
36
|
+
"editMessageText",
|
|
37
|
+
"editMessageReplyMarkup",
|
|
38
|
+
"deleteMessage",
|
|
39
|
+
"answerCallbackQuery",
|
|
40
|
+
"getFile",
|
|
41
|
+
"getChat",
|
|
42
|
+
"getChatMember",
|
|
43
|
+
"leaveChat"
|
|
44
|
+
].map((m) => [m.toLowerCase(), m]));
|
|
45
|
+
const JSON_FIELDS = /* @__PURE__ */ new Set([
|
|
46
|
+
"reply_markup",
|
|
47
|
+
"link_preview_options",
|
|
48
|
+
"allowed_updates",
|
|
49
|
+
"commands",
|
|
50
|
+
"scope",
|
|
51
|
+
"reply_parameters"
|
|
52
|
+
]);
|
|
53
|
+
function randomToken(id) {
|
|
54
|
+
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(26));
|
|
55
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
56
|
+
let s = "AA";
|
|
57
|
+
for (const b of bytes) s += alphabet[b % 64];
|
|
58
|
+
return `${id}:${s}${"x".repeat(7)}`;
|
|
59
|
+
}
|
|
60
|
+
function num(v) {
|
|
61
|
+
if (typeof v === "number") return v;
|
|
62
|
+
if (typeof v === "string" && v.trim() !== "" && !Number.isNaN(Number(v))) return Number(v);
|
|
63
|
+
}
|
|
64
|
+
function str(v) {
|
|
65
|
+
if (typeof v === "string") return v;
|
|
66
|
+
if (typeof v === "number") return String(v);
|
|
67
|
+
}
|
|
68
|
+
function bool(v) {
|
|
69
|
+
return v === true || v === "true";
|
|
70
|
+
}
|
|
71
|
+
function jsonReply(status, body) {
|
|
72
|
+
return new Response(JSON.stringify(body), {
|
|
73
|
+
status,
|
|
74
|
+
headers: { "content-type": "application/json" }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function fileLike(v) {
|
|
78
|
+
return typeof Blob !== "undefined" && v instanceof Blob;
|
|
79
|
+
}
|
|
80
|
+
/** In-memory Telegram Bot API. */
|
|
81
|
+
var FakeTelegram = class {
|
|
82
|
+
/** Every call, in order. */
|
|
83
|
+
calls = [];
|
|
84
|
+
/** Every text message sent. */
|
|
85
|
+
messages = [];
|
|
86
|
+
/** Every document and photo sent. */
|
|
87
|
+
files = [];
|
|
88
|
+
/** Every `answerCallbackQuery`. */
|
|
89
|
+
callbackAnswers = [];
|
|
90
|
+
/** Every `sendChatAction`. */
|
|
91
|
+
chatActions = [];
|
|
92
|
+
#bots = [];
|
|
93
|
+
#failures = /* @__PURE__ */ new Map();
|
|
94
|
+
#opts;
|
|
95
|
+
#nextBotId = 7000000001;
|
|
96
|
+
#nextFile = 1;
|
|
97
|
+
constructor(opts = {}) {
|
|
98
|
+
this.#opts = opts;
|
|
99
|
+
if (!opts.noDefaultBot) this.addBot({
|
|
100
|
+
id: 7000000001,
|
|
101
|
+
username: "FakeTestBot",
|
|
102
|
+
firstName: "Fake Test"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Use as `fetchTelegram({ fetch: fake.fetch })`. */
|
|
106
|
+
fetch = async (input, init) => {
|
|
107
|
+
const request = new Request(input, init);
|
|
108
|
+
return this.#handle(request);
|
|
109
|
+
};
|
|
110
|
+
/** The first bot (created by default). */
|
|
111
|
+
get defaultBot() {
|
|
112
|
+
const first = this.#bots[0];
|
|
113
|
+
if (!first) throw new Error("FakeTelegram has no bots");
|
|
114
|
+
return first.info;
|
|
115
|
+
}
|
|
116
|
+
/** All bots, in creation order. */
|
|
117
|
+
get bots() {
|
|
118
|
+
return this.#bots.map((b) => b.info);
|
|
119
|
+
}
|
|
120
|
+
/** Adds a bot; returns its token, id and username. */
|
|
121
|
+
addBot(init = {}) {
|
|
122
|
+
const id = init.id ?? this.#nextBotId;
|
|
123
|
+
this.#nextBotId = Math.max(this.#nextBotId, id) + 1;
|
|
124
|
+
const info = {
|
|
125
|
+
token: init.token ?? randomToken(id),
|
|
126
|
+
id,
|
|
127
|
+
username: init.username ?? `Fake${id}Bot`,
|
|
128
|
+
firstName: init.firstName ?? "Fake"
|
|
129
|
+
};
|
|
130
|
+
this.#bots.push({
|
|
131
|
+
info,
|
|
132
|
+
webhook: null,
|
|
133
|
+
webhookError: null,
|
|
134
|
+
updates: [],
|
|
135
|
+
commands: /* @__PURE__ */ new Map(),
|
|
136
|
+
chats: /* @__PURE__ */ new Map(),
|
|
137
|
+
files: /* @__PURE__ */ new Map()
|
|
138
|
+
});
|
|
139
|
+
return info;
|
|
140
|
+
}
|
|
141
|
+
/** Revokes a token (later calls get 401), like `/revoke` in BotFather. */
|
|
142
|
+
revokeToken(bot) {
|
|
143
|
+
const s = this.#bot(bot);
|
|
144
|
+
s.info.token = `${s.info.id}:revoked-${crypto.randomUUID()}`;
|
|
145
|
+
}
|
|
146
|
+
/** Creates or changes a chat known to a bot. */
|
|
147
|
+
setChat(chatId, init = {}, bot) {
|
|
148
|
+
const s = this.#bot(bot);
|
|
149
|
+
const chat = s.chats.get(String(chatId)) ?? this.#newChat(chatId);
|
|
150
|
+
if (init.type) chat.type = init.type;
|
|
151
|
+
if (init.title !== void 0) chat.title = init.title;
|
|
152
|
+
if (init.username !== void 0) chat.username = init.username;
|
|
153
|
+
if (init.firstName !== void 0) chat.firstName = init.firstName;
|
|
154
|
+
if (init.isForum !== void 0) chat.isForum = init.isForum;
|
|
155
|
+
if (init.topics) chat.topics = new Set(init.topics);
|
|
156
|
+
if (init.status) chat.status = init.status;
|
|
157
|
+
if (init.migratedTo !== void 0) chat.migratedTo = init.migratedTo;
|
|
158
|
+
if (init.members) for (const [k, v] of Object.entries(init.members)) chat.members.set(Number(k), v);
|
|
159
|
+
s.chats.set(String(chatId), chat);
|
|
160
|
+
}
|
|
161
|
+
/** Current state of a chat as the fake sees it, or undefined. */
|
|
162
|
+
chat(chatId, bot) {
|
|
163
|
+
return this.#bot(bot).chats.get(String(chatId));
|
|
164
|
+
}
|
|
165
|
+
/** The next matching call(s) fail. */
|
|
166
|
+
failNext(method, failure = {}) {
|
|
167
|
+
const key = method.toLowerCase();
|
|
168
|
+
const list = this.#failures.get(key) ?? [];
|
|
169
|
+
list.push({
|
|
170
|
+
times: 1,
|
|
171
|
+
...failure
|
|
172
|
+
});
|
|
173
|
+
this.#failures.set(key, list);
|
|
174
|
+
}
|
|
175
|
+
/** Stores a file the bot can `getFile` + download (for inbound photo / document tests). */
|
|
176
|
+
addFile(bytes, o = {}) {
|
|
177
|
+
const data = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
|
|
178
|
+
return this.#storeFile(this.#bot(o.bot), data, o.name ?? "file.bin", o.mime ?? "application/octet-stream", "doc").fileId;
|
|
179
|
+
}
|
|
180
|
+
/** Queues an update for `getUpdates`. */
|
|
181
|
+
pushUpdate(update, bot) {
|
|
182
|
+
this.#bot(bot).updates.push(update);
|
|
183
|
+
}
|
|
184
|
+
/** Updates queued for `getUpdates` and not yet confirmed. */
|
|
185
|
+
pendingUpdates(bot) {
|
|
186
|
+
return this.#bot(bot).updates;
|
|
187
|
+
}
|
|
188
|
+
/** The registered webhook, or null. */
|
|
189
|
+
webhook(bot) {
|
|
190
|
+
return this.#bot(bot).webhook;
|
|
191
|
+
}
|
|
192
|
+
/** Makes getWebhookInfo report a delivery error. */
|
|
193
|
+
setWebhookError(message, bot) {
|
|
194
|
+
this.#bot(bot).webhookError = message === null ? null : {
|
|
195
|
+
date: this.#nowSec(),
|
|
196
|
+
message
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** The POST Telegram would send to the registered webhook (with the secret header). */
|
|
200
|
+
webhookRequest(update, o = {}) {
|
|
201
|
+
const s = this.#bot(o.bot);
|
|
202
|
+
const url = o.url ?? s.webhook?.url;
|
|
203
|
+
if (!url) throw new Error("no webhook registered for this bot");
|
|
204
|
+
const headers = { "content-type": "application/json" };
|
|
205
|
+
const secret = o.secret === void 0 ? s.webhook?.secretToken : o.secret;
|
|
206
|
+
if (secret) headers["x-telegram-bot-api-secret-token"] = secret;
|
|
207
|
+
return new Request(url, {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers,
|
|
210
|
+
body: JSON.stringify(update)
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Commands set for a scope + language (exact match, like getMyCommands). */
|
|
214
|
+
commands(o = {}) {
|
|
215
|
+
return this.#bot(o.bot).commands.get(this.#commandKey(o.scope, o.language));
|
|
216
|
+
}
|
|
217
|
+
/** Recorded calls to one method (case-insensitive). */
|
|
218
|
+
callsTo(method) {
|
|
219
|
+
const m = method.toLowerCase();
|
|
220
|
+
return this.calls.filter((c) => c.method.toLowerCase() === m);
|
|
221
|
+
}
|
|
222
|
+
/** Text messages sent to a chat (optionally by one bot). */
|
|
223
|
+
messagesTo(chatId, bot) {
|
|
224
|
+
const botId = bot === void 0 ? void 0 : this.#bot(bot).info.id;
|
|
225
|
+
return this.messages.filter((m) => m.chatId === String(chatId) && (botId === void 0 || m.botId === botId));
|
|
226
|
+
}
|
|
227
|
+
/** Documents sent to a chat. */
|
|
228
|
+
documentsTo(chatId, bot) {
|
|
229
|
+
return this.#filesTo(chatId, "document", bot);
|
|
230
|
+
}
|
|
231
|
+
/** Photos sent to a chat. */
|
|
232
|
+
photosTo(chatId, bot) {
|
|
233
|
+
return this.#filesTo(chatId, "photo", bot);
|
|
234
|
+
}
|
|
235
|
+
/** Clears recorded calls, messages and failures (bots, chats and webhooks stay). */
|
|
236
|
+
reset() {
|
|
237
|
+
this.calls.length = 0;
|
|
238
|
+
this.messages.length = 0;
|
|
239
|
+
this.files.length = 0;
|
|
240
|
+
this.callbackAnswers.length = 0;
|
|
241
|
+
this.chatActions.length = 0;
|
|
242
|
+
this.#failures.clear();
|
|
243
|
+
}
|
|
244
|
+
#filesTo(chatId, kind, bot) {
|
|
245
|
+
const botId = bot === void 0 ? void 0 : this.#bot(bot).info.id;
|
|
246
|
+
return this.files.filter((f) => f.kind === kind && f.chatId === String(chatId) && (botId === void 0 || f.botId === botId));
|
|
247
|
+
}
|
|
248
|
+
#now() {
|
|
249
|
+
return this.#opts.now ? this.#opts.now() : Date.now();
|
|
250
|
+
}
|
|
251
|
+
#nowSec() {
|
|
252
|
+
return Math.floor(this.#now() / 1e3);
|
|
253
|
+
}
|
|
254
|
+
#bot(ref) {
|
|
255
|
+
const s = ref === void 0 ? this.#bots[0] : this.#bots.find((b) => b.info.token === ref || b.info.username === ref || b.info.id === ref);
|
|
256
|
+
if (!s) throw new Error("FakeTelegram: unknown bot");
|
|
257
|
+
return s;
|
|
258
|
+
}
|
|
259
|
+
#newChat(id) {
|
|
260
|
+
const type = id > 0 ? "private" : String(id).startsWith("-100") ? "supergroup" : "group";
|
|
261
|
+
return {
|
|
262
|
+
id,
|
|
263
|
+
type,
|
|
264
|
+
...type === "private" ? { firstName: "User" } : { title: `Chat ${id}` },
|
|
265
|
+
isForum: false,
|
|
266
|
+
topics: /* @__PURE__ */ new Set(),
|
|
267
|
+
status: "member",
|
|
268
|
+
members: /* @__PURE__ */ new Map(),
|
|
269
|
+
recentSends: [],
|
|
270
|
+
nextMessageId: 1
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
#commandKey(scope, language) {
|
|
274
|
+
const sc = typeof scope === "object" && scope !== null ? scope : { type: "default" };
|
|
275
|
+
return `${[
|
|
276
|
+
sc.type,
|
|
277
|
+
sc.chat_id,
|
|
278
|
+
sc.user_id
|
|
279
|
+
].filter((p) => p !== void 0).map(String).join(":")}|${typeof language === "string" ? language : ""}`;
|
|
280
|
+
}
|
|
281
|
+
#storeFile(s, bytes, name, mime, kind) {
|
|
282
|
+
const n = this.#nextFile++;
|
|
283
|
+
const ext = name.includes(".") ? name.slice(name.lastIndexOf(".")) : kind === "photo" ? ".jpg" : "";
|
|
284
|
+
const file = {
|
|
285
|
+
fileId: `BQAC-fake-${s.info.id}-${n}`,
|
|
286
|
+
uniqueId: `AgAD-fake-${n}`,
|
|
287
|
+
name,
|
|
288
|
+
mime,
|
|
289
|
+
bytes,
|
|
290
|
+
path: `${kind === "photo" ? "photos" : "documents"}/file_${n}${ext}`
|
|
291
|
+
};
|
|
292
|
+
s.files.set(file.fileId, file);
|
|
293
|
+
return file;
|
|
294
|
+
}
|
|
295
|
+
async #handle(request) {
|
|
296
|
+
const url = new URL(request.url);
|
|
297
|
+
const fileMatch = /\/file\/bot([^/]+)\/(.+)$/.exec(url.pathname);
|
|
298
|
+
if (fileMatch) return this.#download(fileMatch[1] ?? "", decodeURIComponent(fileMatch[2] ?? ""));
|
|
299
|
+
const m = /\/bot([^/]+)\/([A-Za-z]+)$/.exec(url.pathname);
|
|
300
|
+
if (!m) return jsonReply(404, {
|
|
301
|
+
ok: false,
|
|
302
|
+
error_code: 404,
|
|
303
|
+
description: "Not Found"
|
|
304
|
+
});
|
|
305
|
+
const token = decodeURIComponent(m[1] ?? "");
|
|
306
|
+
const rawMethod = m[2] ?? "";
|
|
307
|
+
const s = this.#bots.find((b) => b.info.token === token);
|
|
308
|
+
if (!s) return jsonReply(401, {
|
|
309
|
+
ok: false,
|
|
310
|
+
error_code: 401,
|
|
311
|
+
description: "Unauthorized"
|
|
312
|
+
});
|
|
313
|
+
const method = METHOD_BY_LOWER.get(rawMethod.toLowerCase());
|
|
314
|
+
const params = await this.#params(request, url);
|
|
315
|
+
const record = (ok, errorCode) => this.calls.push({
|
|
316
|
+
botId: s.info.id,
|
|
317
|
+
method: method ?? rawMethod,
|
|
318
|
+
params,
|
|
319
|
+
ok,
|
|
320
|
+
...errorCode === void 0 ? {} : { errorCode },
|
|
321
|
+
at: this.#now()
|
|
322
|
+
});
|
|
323
|
+
const failure = this.#takeFailure(rawMethod, s);
|
|
324
|
+
if (failure) {
|
|
325
|
+
if (failure.hang) {
|
|
326
|
+
record(false);
|
|
327
|
+
return new Promise((_, reject) => {
|
|
328
|
+
const abort = () => reject(request.signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
329
|
+
if (request.signal.aborted) abort();
|
|
330
|
+
else request.signal.addEventListener("abort", abort, { once: true });
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (failure.network) {
|
|
334
|
+
record(false);
|
|
335
|
+
throw new TypeError(`fetch failed: ${request.url}`);
|
|
336
|
+
}
|
|
337
|
+
const status = failure.status ?? 400;
|
|
338
|
+
const parameters = {};
|
|
339
|
+
if (failure.retryAfter !== void 0) parameters.retry_after = failure.retryAfter;
|
|
340
|
+
if (failure.migrateToChatId !== void 0) parameters.migrate_to_chat_id = failure.migrateToChatId;
|
|
341
|
+
record(false, status);
|
|
342
|
+
return jsonReply(status, {
|
|
343
|
+
ok: false,
|
|
344
|
+
error_code: status,
|
|
345
|
+
description: failure.description ?? (status === 429 ? "Too Many Requests: retry after 1" : "Bad Request"),
|
|
346
|
+
...Object.keys(parameters).length > 0 ? { parameters } : {}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (!method) {
|
|
350
|
+
record(false, 404);
|
|
351
|
+
return jsonReply(404, {
|
|
352
|
+
ok: false,
|
|
353
|
+
error_code: 404,
|
|
354
|
+
description: "Not Found: method not found"
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
let reply;
|
|
358
|
+
try {
|
|
359
|
+
reply = {
|
|
360
|
+
status: 200,
|
|
361
|
+
body: {
|
|
362
|
+
ok: true,
|
|
363
|
+
result: await this.#dispatch(s, method, params)
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
} catch (e) {
|
|
367
|
+
if (!(e instanceof Fail)) throw e;
|
|
368
|
+
reply = {
|
|
369
|
+
status: e.status,
|
|
370
|
+
body: {
|
|
371
|
+
ok: false,
|
|
372
|
+
error_code: e.status,
|
|
373
|
+
description: e.description,
|
|
374
|
+
...e.parameters ? { parameters: e.parameters } : {}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
record(reply.status === 200, reply.status === 200 ? void 0 : reply.status);
|
|
379
|
+
return jsonReply(reply.status, reply.body);
|
|
380
|
+
}
|
|
381
|
+
#takeFailure(method, s) {
|
|
382
|
+
const list = this.#failures.get(method.toLowerCase());
|
|
383
|
+
if (!list) return void 0;
|
|
384
|
+
const idx = list.findIndex((f) => f.bot === void 0 || this.#bot(f.bot) === s);
|
|
385
|
+
if (idx < 0) return void 0;
|
|
386
|
+
const f = list[idx];
|
|
387
|
+
const left = (f.times ?? 1) - 1;
|
|
388
|
+
if (left <= 0) list.splice(idx, 1);
|
|
389
|
+
else list[idx] = {
|
|
390
|
+
...f,
|
|
391
|
+
times: left
|
|
392
|
+
};
|
|
393
|
+
return f;
|
|
394
|
+
}
|
|
395
|
+
async #params(request, url) {
|
|
396
|
+
const out = {};
|
|
397
|
+
for (const [k, v] of url.searchParams) out[k] = v;
|
|
398
|
+
if (request.method === "GET" || request.body === null) return out;
|
|
399
|
+
const type = request.headers.get("content-type") ?? "";
|
|
400
|
+
if (type.includes("application/json")) {
|
|
401
|
+
const body = await request.json().catch(() => ({}));
|
|
402
|
+
if (typeof body === "object" && body !== null) Object.assign(out, body);
|
|
403
|
+
} else if (type.includes("multipart/form-data") || type.includes("application/x-www-form-urlencoded")) {
|
|
404
|
+
const form = await request.formData();
|
|
405
|
+
for (const [k, v] of form.entries()) out[k] = v;
|
|
406
|
+
}
|
|
407
|
+
for (const k of JSON_FIELDS) {
|
|
408
|
+
const v = out[k];
|
|
409
|
+
if (typeof v === "string") try {
|
|
410
|
+
out[k] = JSON.parse(v);
|
|
411
|
+
} catch {
|
|
412
|
+
throw new Error(`FakeTelegram: field ${k} is not JSON`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
async #download(token, path) {
|
|
418
|
+
const s = this.#bots.find((b) => b.info.token === token);
|
|
419
|
+
if (!s) return jsonReply(401, {
|
|
420
|
+
ok: false,
|
|
421
|
+
error_code: 401,
|
|
422
|
+
description: "Unauthorized"
|
|
423
|
+
});
|
|
424
|
+
const file = [...s.files.values()].find((f) => f.path === path);
|
|
425
|
+
if (!file) return jsonReply(404, {
|
|
426
|
+
ok: false,
|
|
427
|
+
error_code: 404,
|
|
428
|
+
description: "Not Found"
|
|
429
|
+
});
|
|
430
|
+
return new Response(file.bytes.slice(), {
|
|
431
|
+
status: 200,
|
|
432
|
+
headers: { "content-type": file.mime }
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
async #dispatch(s, method, p) {
|
|
436
|
+
switch (method) {
|
|
437
|
+
case "getMe": return {
|
|
438
|
+
id: s.info.id,
|
|
439
|
+
is_bot: true,
|
|
440
|
+
first_name: s.info.firstName,
|
|
441
|
+
username: s.info.username,
|
|
442
|
+
can_join_groups: true,
|
|
443
|
+
can_read_all_group_messages: false,
|
|
444
|
+
supports_inline_queries: false
|
|
445
|
+
};
|
|
446
|
+
case "setWebhook": return this.#setWebhook(s, p);
|
|
447
|
+
case "deleteWebhook":
|
|
448
|
+
s.webhook = null;
|
|
449
|
+
if (bool(p.drop_pending_updates)) s.updates.length = 0;
|
|
450
|
+
return true;
|
|
451
|
+
case "getWebhookInfo": return {
|
|
452
|
+
url: s.webhook?.url ?? "",
|
|
453
|
+
has_custom_certificate: false,
|
|
454
|
+
pending_update_count: s.updates.length,
|
|
455
|
+
...s.webhook ? {
|
|
456
|
+
max_connections: s.webhook.maxConnections,
|
|
457
|
+
...s.webhook.allowedUpdates ? { allowed_updates: s.webhook.allowedUpdates } : {}
|
|
458
|
+
} : {},
|
|
459
|
+
...s.webhookError ? {
|
|
460
|
+
last_error_date: s.webhookError.date,
|
|
461
|
+
last_error_message: s.webhookError.message
|
|
462
|
+
} : {}
|
|
463
|
+
};
|
|
464
|
+
case "getUpdates": return this.#getUpdates(s, p);
|
|
465
|
+
case "setMyCommands": return this.#setMyCommands(s, p);
|
|
466
|
+
case "deleteMyCommands":
|
|
467
|
+
s.commands.delete(this.#commandKey(p.scope, p.language_code));
|
|
468
|
+
return true;
|
|
469
|
+
case "getMyCommands": return s.commands.get(this.#commandKey(p.scope, p.language_code)) ?? [];
|
|
470
|
+
case "sendMessage": return this.#sendMessage(s, p);
|
|
471
|
+
case "sendDocument": return this.#sendFile(s, p, "document");
|
|
472
|
+
case "sendPhoto": return this.#sendFile(s, p, "photo");
|
|
473
|
+
case "sendChatAction": {
|
|
474
|
+
const chat = this.#sendableChat(s, p, "sendchataction");
|
|
475
|
+
if (typeof p.action !== "string") throw new Fail(400, "Bad Request: wrong parameter action in request");
|
|
476
|
+
this.chatActions.push({
|
|
477
|
+
botId: s.info.id,
|
|
478
|
+
chatId: String(chat.id),
|
|
479
|
+
action: p.action
|
|
480
|
+
});
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
case "editMessageText": return this.#edit(s, p, "text");
|
|
484
|
+
case "editMessageReplyMarkup": return this.#edit(s, p, "markup");
|
|
485
|
+
case "deleteMessage": {
|
|
486
|
+
const msg = this.#findMessage(s, p);
|
|
487
|
+
if (!msg || msg.deleted) throw new Fail(400, "Bad Request: message to delete not found");
|
|
488
|
+
msg.deleted = true;
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
case "answerCallbackQuery":
|
|
492
|
+
if (typeof p.callback_query_id !== "string" || p.callback_query_id === "") throw new Fail(400, "Bad Request: query is too old and response timeout expired or query ID is invalid");
|
|
493
|
+
if (typeof p.text === "string" && p.text.length > 200) throw new Fail(400, "Bad Request: MESSAGE_TOO_LONG");
|
|
494
|
+
this.callbackAnswers.push({
|
|
495
|
+
botId: s.info.id,
|
|
496
|
+
params: p
|
|
497
|
+
});
|
|
498
|
+
return true;
|
|
499
|
+
case "getFile": {
|
|
500
|
+
const file = s.files.get(str(p.file_id) ?? "");
|
|
501
|
+
if (!file) throw new Fail(400, "Bad Request: wrong file_id or the file is temporarily unavailable");
|
|
502
|
+
return {
|
|
503
|
+
file_id: file.fileId,
|
|
504
|
+
file_unique_id: file.uniqueId,
|
|
505
|
+
file_size: file.bytes.length,
|
|
506
|
+
file_path: file.path
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
case "getChat": {
|
|
510
|
+
const chat = this.#knownChat(s, p);
|
|
511
|
+
return {
|
|
512
|
+
id: chat.id,
|
|
513
|
+
type: chat.type,
|
|
514
|
+
...chat.title === void 0 ? {} : { title: chat.title },
|
|
515
|
+
...chat.username === void 0 ? {} : { username: chat.username },
|
|
516
|
+
...chat.firstName === void 0 ? {} : { first_name: chat.firstName },
|
|
517
|
+
...chat.isForum ? { is_forum: true } : {},
|
|
518
|
+
accent_color_id: 0,
|
|
519
|
+
max_reaction_count: 11
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
case "getChatMember": {
|
|
523
|
+
const chat = this.#knownChat(s, p);
|
|
524
|
+
const userId = num(p.user_id);
|
|
525
|
+
if (userId === void 0) throw new Fail(400, "Bad Request: user_id is required");
|
|
526
|
+
const isBot = userId === s.info.id;
|
|
527
|
+
return {
|
|
528
|
+
status: chat.members.get(userId) ?? (isBot ? ["kicked", "left"].includes(chat.status) ? chat.status : "member" : "member"),
|
|
529
|
+
user: isBot ? {
|
|
530
|
+
id: userId,
|
|
531
|
+
is_bot: true,
|
|
532
|
+
first_name: s.info.firstName,
|
|
533
|
+
username: s.info.username
|
|
534
|
+
} : {
|
|
535
|
+
id: userId,
|
|
536
|
+
is_bot: false,
|
|
537
|
+
first_name: `User ${userId}`
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
case "leaveChat": {
|
|
542
|
+
const chat = this.#knownChat(s, p);
|
|
543
|
+
if (chat.type === "private") throw new Fail(400, "Bad Request: chat member status can't be changed in private chats");
|
|
544
|
+
chat.status = "left";
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
#setWebhook(s, p) {
|
|
550
|
+
const url = str(p.url) ?? "";
|
|
551
|
+
if (url === "") {
|
|
552
|
+
s.webhook = null;
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
let parsed;
|
|
556
|
+
try {
|
|
557
|
+
parsed = new URL(url);
|
|
558
|
+
} catch {
|
|
559
|
+
throw new Fail(400, "Bad Request: bad webhook: Failed to resolve host: Name or service not known");
|
|
560
|
+
}
|
|
561
|
+
if (parsed.protocol !== "https:") throw new Fail(400, "Bad Request: bad webhook: An HTTPS URL must be provided for webhook");
|
|
562
|
+
if (parsed.port !== "" && ![
|
|
563
|
+
"443",
|
|
564
|
+
"80",
|
|
565
|
+
"88",
|
|
566
|
+
"8443"
|
|
567
|
+
].includes(parsed.port)) throw new Fail(400, "Bad Request: bad webhook: Webhook can be set up only on ports 80, 88, 443 or 8443");
|
|
568
|
+
const secret = str(p.secret_token);
|
|
569
|
+
if (secret !== void 0 && !/^[A-Za-z0-9_-]{1,256}$/.test(secret)) throw new Fail(400, "Bad Request: secret token contains unallowed characters");
|
|
570
|
+
const max = num(p.max_connections) ?? 40;
|
|
571
|
+
if (max < 1 || max > 100) throw new Fail(400, "Bad Request: bad webhook: max_connections must be between 1 and 100");
|
|
572
|
+
const allowed = Array.isArray(p.allowed_updates) ? p.allowed_updates.map(String) : s.webhook?.allowedUpdates;
|
|
573
|
+
const drop = bool(p.drop_pending_updates);
|
|
574
|
+
if (drop) s.updates.length = 0;
|
|
575
|
+
s.webhook = {
|
|
576
|
+
url,
|
|
577
|
+
secretToken: secret,
|
|
578
|
+
allowedUpdates: allowed,
|
|
579
|
+
maxConnections: max,
|
|
580
|
+
dropPendingUpdates: drop
|
|
581
|
+
};
|
|
582
|
+
s.webhookError = null;
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
#getUpdates(s, p) {
|
|
586
|
+
if (s.webhook) throw new Fail(409, "Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first");
|
|
587
|
+
const offset = num(p.offset);
|
|
588
|
+
if (offset !== void 0) {
|
|
589
|
+
const keep = s.updates.filter((u) => u.update_id >= offset);
|
|
590
|
+
s.updates.length = 0;
|
|
591
|
+
s.updates.push(...keep);
|
|
592
|
+
}
|
|
593
|
+
const limit = Math.min(100, Math.max(1, num(p.limit) ?? 100));
|
|
594
|
+
return s.updates.slice(0, limit);
|
|
595
|
+
}
|
|
596
|
+
#setMyCommands(s, p) {
|
|
597
|
+
if (!Array.isArray(p.commands)) throw new Fail(400, "Bad Request: commands must be an array");
|
|
598
|
+
if (p.commands.length > 100) throw new Fail(400, "Bad Request: too many commands specified");
|
|
599
|
+
const commands = [];
|
|
600
|
+
for (const raw of p.commands) {
|
|
601
|
+
const command = str(raw.command) ?? "";
|
|
602
|
+
const description = str(raw.description) ?? "";
|
|
603
|
+
if (!/^[a-z0-9_]{1,32}$/.test(command)) throw new Fail(400, "Bad Request: BOT_COMMAND_INVALID");
|
|
604
|
+
if (description.length < 1 || description.length > 256) throw new Fail(400, "Bad Request: BOT_COMMAND_DESCRIPTION_INVALID");
|
|
605
|
+
commands.push({
|
|
606
|
+
command,
|
|
607
|
+
description,
|
|
608
|
+
...raw.is_ephemeral === true ? { is_ephemeral: true } : {}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
s.commands.set(this.#commandKey(p.scope, p.language_code), commands);
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
/** A chat the bot knows (auto-created unless `strictChats`). Applies migration errors. */
|
|
615
|
+
#knownChat(s, p) {
|
|
616
|
+
const id = num(p.chat_id);
|
|
617
|
+
if (id === void 0) throw new Fail(400, "Bad Request: chat_id is empty");
|
|
618
|
+
let chat = s.chats.get(String(id));
|
|
619
|
+
if (!chat) {
|
|
620
|
+
if (this.#opts.strictChats) throw new Fail(400, "Bad Request: chat not found");
|
|
621
|
+
chat = this.#newChat(id);
|
|
622
|
+
s.chats.set(String(id), chat);
|
|
623
|
+
}
|
|
624
|
+
if (chat.migratedTo !== void 0) throw new Fail(400, "Bad Request: group chat was upgraded to a supergroup chat", { migrate_to_chat_id: chat.migratedTo });
|
|
625
|
+
return chat;
|
|
626
|
+
}
|
|
627
|
+
/** A chat the bot may write to now (status, topic and rate-limit checks). */
|
|
628
|
+
#sendableChat(s, p, method) {
|
|
629
|
+
const chat = this.#knownChat(s, p);
|
|
630
|
+
const kind = chat.type === "channel" ? "channel" : chat.type === "supergroup" ? "supergroup" : "group";
|
|
631
|
+
switch (chat.status) {
|
|
632
|
+
case "blocked": throw new Fail(403, "Forbidden: bot was blocked by the user");
|
|
633
|
+
case "deactivated": throw new Fail(403, "Forbidden: user is deactivated");
|
|
634
|
+
case "not_started": throw new Fail(403, "Forbidden: bot can't initiate conversation with a user");
|
|
635
|
+
case "kicked": throw new Fail(403, kind === "channel" ? "Forbidden: bot is not a member of the channel chat" : `Forbidden: bot was kicked from the ${kind} chat`);
|
|
636
|
+
case "left": throw new Fail(403, `Forbidden: bot is not a member of the ${kind} chat`);
|
|
637
|
+
}
|
|
638
|
+
const thread = num(p.message_thread_id);
|
|
639
|
+
if (thread !== void 0 && chat.isForum && !chat.topics.has(thread)) throw new Fail(400, "Bad Request: message thread not found");
|
|
640
|
+
if (this.#opts.rateLimit && SEND_METHODS.has(method)) this.#rateLimit(chat);
|
|
641
|
+
return chat;
|
|
642
|
+
}
|
|
643
|
+
#rateLimit(chat) {
|
|
644
|
+
const now = this.#now();
|
|
645
|
+
if (chat.type === "private") {
|
|
646
|
+
if (chat.lastSendAt !== void 0 && now - chat.lastSendAt < 1e3) {
|
|
647
|
+
const wait = Math.max(1, Math.ceil((1e3 - (now - chat.lastSendAt)) / 1e3));
|
|
648
|
+
throw new Fail(429, `Too Many Requests: retry after ${wait}`, { retry_after: wait });
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
chat.recentSends = chat.recentSends.filter((t) => now - t < 6e4);
|
|
652
|
+
const oldest = chat.recentSends[0];
|
|
653
|
+
if (chat.recentSends.length >= 20 && oldest !== void 0) {
|
|
654
|
+
const wait = Math.max(1, Math.ceil((oldest + 6e4 - now) / 1e3));
|
|
655
|
+
throw new Fail(429, `Too Many Requests: retry after ${wait}`, { retry_after: wait });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
#markSent(chat) {
|
|
660
|
+
const now = this.#now();
|
|
661
|
+
chat.lastSendAt = now;
|
|
662
|
+
chat.recentSends.push(now);
|
|
663
|
+
return chat.nextMessageId++;
|
|
664
|
+
}
|
|
665
|
+
#chatJson(chat) {
|
|
666
|
+
return {
|
|
667
|
+
id: chat.id,
|
|
668
|
+
type: chat.type,
|
|
669
|
+
...chat.title === void 0 ? {} : { title: chat.title },
|
|
670
|
+
...chat.username === void 0 ? {} : { username: chat.username },
|
|
671
|
+
...chat.firstName === void 0 ? {} : { first_name: chat.firstName },
|
|
672
|
+
...chat.isForum ? { is_forum: true } : {}
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
#botJson(s) {
|
|
676
|
+
return {
|
|
677
|
+
id: s.info.id,
|
|
678
|
+
is_bot: true,
|
|
679
|
+
first_name: s.info.firstName,
|
|
680
|
+
username: s.info.username
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
#checkMarkup(markup) {
|
|
684
|
+
if (typeof markup !== "object" || markup === null || !("inline_keyboard" in markup)) return;
|
|
685
|
+
const rows = markup.inline_keyboard;
|
|
686
|
+
if (!Array.isArray(rows)) throw new Fail(400, "Bad Request: can't parse inline keyboard markup");
|
|
687
|
+
for (const row of rows) {
|
|
688
|
+
if (!Array.isArray(row)) throw new Fail(400, "Bad Request: can't parse inline keyboard markup");
|
|
689
|
+
for (const b of row) {
|
|
690
|
+
if (typeof b.text !== "string" || b.text === "") throw new Fail(400, "Bad Request: BUTTON_TEXT_INVALID");
|
|
691
|
+
if ([
|
|
692
|
+
"callback_data",
|
|
693
|
+
"url",
|
|
694
|
+
"copy_text",
|
|
695
|
+
"switch_inline_query_current_chat",
|
|
696
|
+
"web_app",
|
|
697
|
+
"login_url"
|
|
698
|
+
].filter((k) => k in b).length !== 1) throw new Fail(400, "Bad Request: can't parse inline keyboard button: Text buttons are unallowed in the inline keyboard");
|
|
699
|
+
if ("callback_data" in b) {
|
|
700
|
+
const bytes = new TextEncoder().encode(b.callback_data).length;
|
|
701
|
+
if (bytes < 1 || bytes > 64) throw new Fail(400, "Bad Request: BUTTON_DATA_INVALID");
|
|
702
|
+
}
|
|
703
|
+
if (b.style !== void 0 && ![
|
|
704
|
+
"danger",
|
|
705
|
+
"success",
|
|
706
|
+
"primary"
|
|
707
|
+
].includes(b.style)) throw new Fail(400, "Bad Request: BUTTON_STYLE_INVALID");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/** Validates text and returns what Telegram would store (plain text after entity parsing). */
|
|
712
|
+
#renderText(text, parseMode, max, field) {
|
|
713
|
+
let plain = text;
|
|
714
|
+
if (parseMode === "HTML") {
|
|
715
|
+
const check = checkTelegramHtml(text);
|
|
716
|
+
if (check.error) throw new Fail(400, check.error);
|
|
717
|
+
plain = check.text;
|
|
718
|
+
}
|
|
719
|
+
if (field === "text" && plain.trim() === "") throw new Fail(400, "Bad Request: message text is empty");
|
|
720
|
+
if (plain.length > max) throw new Fail(400, field === "text" ? "Bad Request: message is too long" : "Bad Request: message caption is too long");
|
|
721
|
+
return plain;
|
|
722
|
+
}
|
|
723
|
+
#sendMessage(s, p) {
|
|
724
|
+
const chat = this.#sendableChat(s, p, "sendmessage");
|
|
725
|
+
const text = str(p.text) ?? "";
|
|
726
|
+
const plain = this.#renderText(text, p.parse_mode, MAX_TEXT, "text");
|
|
727
|
+
this.#checkMarkup(p.reply_markup);
|
|
728
|
+
const messageId = this.#markSent(chat);
|
|
729
|
+
const thread = num(p.message_thread_id);
|
|
730
|
+
const parseMode = str(p.parse_mode);
|
|
731
|
+
this.messages.push({
|
|
732
|
+
botId: s.info.id,
|
|
733
|
+
chatId: String(chat.id),
|
|
734
|
+
messageId,
|
|
735
|
+
threadId: thread,
|
|
736
|
+
text,
|
|
737
|
+
currentText: text,
|
|
738
|
+
parseMode,
|
|
739
|
+
replyMarkup: p.reply_markup,
|
|
740
|
+
params: p,
|
|
741
|
+
edits: 0,
|
|
742
|
+
deleted: false,
|
|
743
|
+
at: this.#now()
|
|
744
|
+
});
|
|
745
|
+
return {
|
|
746
|
+
message_id: messageId,
|
|
747
|
+
date: this.#nowSec(),
|
|
748
|
+
chat: this.#chatJson(chat),
|
|
749
|
+
from: this.#botJson(s),
|
|
750
|
+
...thread === void 0 ? {} : {
|
|
751
|
+
message_thread_id: thread,
|
|
752
|
+
is_topic_message: true
|
|
753
|
+
},
|
|
754
|
+
text: plain,
|
|
755
|
+
...p.reply_markup && typeof p.reply_markup === "object" && "inline_keyboard" in p.reply_markup ? { reply_markup: p.reply_markup } : {}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
async #sendFile(s, p, kind) {
|
|
759
|
+
const method = kind === "document" ? "senddocument" : "sendphoto";
|
|
760
|
+
const chat = this.#sendableChat(s, p, method);
|
|
761
|
+
const caption = str(p.caption);
|
|
762
|
+
if (caption !== void 0) this.#renderText(caption, p.parse_mode, MAX_CAPTION, "caption");
|
|
763
|
+
this.#checkMarkup(p.reply_markup);
|
|
764
|
+
const input = p[kind];
|
|
765
|
+
let stored;
|
|
766
|
+
let bytes = null;
|
|
767
|
+
if (fileLike(input)) {
|
|
768
|
+
bytes = new Uint8Array(await input.arrayBuffer());
|
|
769
|
+
const limit = kind === "photo" ? MAX_PHOTO : MAX_UPLOAD;
|
|
770
|
+
if (bytes.length > limit) throw new Fail(413, "Request Entity Too Large");
|
|
771
|
+
if (bytes.length === 0) throw new Fail(400, "Bad Request: file must be non-empty");
|
|
772
|
+
const name = typeof input.name === "string" && input.name !== "" ? input.name : kind === "photo" ? "photo.jpg" : "file";
|
|
773
|
+
stored = this.#storeFile(s, bytes, name, input.type || "application/octet-stream", kind === "photo" ? "photo" : "doc");
|
|
774
|
+
} else if (typeof input === "string" && /^https?:\/\//.test(input)) {
|
|
775
|
+
if (kind === "document" && !/\.(pdf|zip)(\?|$)/i.test(input)) throw new Fail(400, "Bad Request: wrong type of the web page content");
|
|
776
|
+
const name = input.split("?")[0]?.split("/").at(-1) || "file";
|
|
777
|
+
stored = this.#storeFile(s, /* @__PURE__ */ new Uint8Array(), name, kind === "photo" ? "image/jpeg" : "application/pdf", "doc");
|
|
778
|
+
} else if (typeof input === "string" && s.files.has(input)) stored = s.files.get(input);
|
|
779
|
+
else throw new Fail(400, "Bad Request: wrong file identifier/HTTP URL specified");
|
|
780
|
+
const messageId = this.#markSent(chat);
|
|
781
|
+
const thread = num(p.message_thread_id);
|
|
782
|
+
this.files.push({
|
|
783
|
+
botId: s.info.id,
|
|
784
|
+
chatId: String(chat.id),
|
|
785
|
+
messageId,
|
|
786
|
+
kind,
|
|
787
|
+
fileId: stored.fileId,
|
|
788
|
+
fileName: stored.name,
|
|
789
|
+
mime: stored.mime,
|
|
790
|
+
size: stored.bytes.length,
|
|
791
|
+
bytes,
|
|
792
|
+
caption,
|
|
793
|
+
uploaded: bytes !== null,
|
|
794
|
+
params: p,
|
|
795
|
+
at: this.#now()
|
|
796
|
+
});
|
|
797
|
+
const media = kind === "document" ? { document: {
|
|
798
|
+
file_id: stored.fileId,
|
|
799
|
+
file_unique_id: stored.uniqueId,
|
|
800
|
+
file_name: stored.name,
|
|
801
|
+
mime_type: stored.mime,
|
|
802
|
+
file_size: stored.bytes.length
|
|
803
|
+
} } : { photo: [{
|
|
804
|
+
file_id: `${stored.fileId}-s`,
|
|
805
|
+
file_unique_id: `${stored.uniqueId}-s`,
|
|
806
|
+
width: 90,
|
|
807
|
+
height: 90
|
|
808
|
+
}, {
|
|
809
|
+
file_id: stored.fileId,
|
|
810
|
+
file_unique_id: stored.uniqueId,
|
|
811
|
+
width: 1280,
|
|
812
|
+
height: 960,
|
|
813
|
+
file_size: stored.bytes.length
|
|
814
|
+
}] };
|
|
815
|
+
return {
|
|
816
|
+
message_id: messageId,
|
|
817
|
+
date: this.#nowSec(),
|
|
818
|
+
chat: this.#chatJson(chat),
|
|
819
|
+
from: this.#botJson(s),
|
|
820
|
+
...thread === void 0 ? {} : {
|
|
821
|
+
message_thread_id: thread,
|
|
822
|
+
is_topic_message: true
|
|
823
|
+
},
|
|
824
|
+
...media,
|
|
825
|
+
...caption === void 0 ? {} : { caption }
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
#findMessage(s, p) {
|
|
829
|
+
const chat = this.#knownChat(s, p);
|
|
830
|
+
const id = num(p.message_id);
|
|
831
|
+
return this.messages.find((m) => m.botId === s.info.id && m.chatId === String(chat.id) && m.messageId === id);
|
|
832
|
+
}
|
|
833
|
+
#edit(s, p, what) {
|
|
834
|
+
const msg = this.#findMessage(s, p);
|
|
835
|
+
if (!msg || msg.deleted) throw new Fail(400, "Bad Request: message to edit not found");
|
|
836
|
+
this.#checkMarkup(p.reply_markup);
|
|
837
|
+
const newText = what === "text" ? str(p.text) ?? "" : msg.currentText;
|
|
838
|
+
const plain = what === "text" ? this.#renderText(newText, p.parse_mode, MAX_TEXT, "text") : checkTelegramHtml(newText).text;
|
|
839
|
+
const newMarkup = p.reply_markup;
|
|
840
|
+
if (newText === msg.currentText && JSON.stringify(newMarkup ?? null) === JSON.stringify(msg.replyMarkup ?? null)) throw new Fail(400, "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message");
|
|
841
|
+
msg.currentText = newText;
|
|
842
|
+
msg.replyMarkup = newMarkup;
|
|
843
|
+
msg.edits++;
|
|
844
|
+
const chat = s.chats.get(msg.chatId);
|
|
845
|
+
return {
|
|
846
|
+
message_id: msg.messageId,
|
|
847
|
+
date: Math.floor(msg.at / 1e3),
|
|
848
|
+
edit_date: this.#nowSec(),
|
|
849
|
+
chat: this.#chatJson(chat),
|
|
850
|
+
from: this.#botJson(s),
|
|
851
|
+
text: plain,
|
|
852
|
+
...newMarkup ? { reply_markup: newMarkup } : {}
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
export { FakeTelegram };
|