@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.
Files changed (295) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +109 -2
  4. package/dist/adapters/clock.d.ts +3 -0
  5. package/dist/adapters/clock.js +4 -0
  6. package/dist/adapters/console-log.d.ts +13 -0
  7. package/dist/adapters/console-log.js +41 -0
  8. package/dist/adapters/d1-db.d.ts +5 -0
  9. package/dist/adapters/d1-db.js +37 -0
  10. package/dist/adapters/ids.d.ts +5 -0
  11. package/dist/adapters/ids.js +25 -0
  12. package/dist/adapters/index.d.ts +7 -0
  13. package/dist/adapters/keyring.d.ts +50 -0
  14. package/dist/adapters/keyring.js +174 -0
  15. package/dist/adapters/metrics.d.ts +28 -0
  16. package/dist/adapters/metrics.js +48 -0
  17. package/dist/adapters/r2-files.d.ts +20 -0
  18. package/dist/adapters/r2-files.js +53 -0
  19. package/dist/bin/cli.d.ts +1 -0
  20. package/dist/bin/cli.js +45 -0
  21. package/dist/client/broadcasts.d.ts +35 -0
  22. package/dist/client/commands.d.ts +23 -0
  23. package/dist/client/endpoint.d.ts +34 -0
  24. package/dist/client/events.d.ts +33 -0
  25. package/dist/client/index.d.ts +11 -0
  26. package/dist/client/index.js +3 -0
  27. package/dist/client/schedules.d.ts +42 -0
  28. package/dist/client/schema.d.ts +181 -0
  29. package/dist/client/send.d.ts +15 -0
  30. package/dist/client/templates.d.ts +47 -0
  31. package/dist/contracts/bots.d.ts +129 -0
  32. package/dist/contracts/bots.js +70 -0
  33. package/dist/contracts/broadcasts.d.ts +116 -0
  34. package/dist/contracts/broadcasts.js +63 -0
  35. package/dist/contracts/chats.d.ts +97 -0
  36. package/dist/contracts/chats.js +66 -0
  37. package/dist/contracts/commands.d.ts +52 -0
  38. package/dist/contracts/commands.js +53 -0
  39. package/dist/contracts/common.d.ts +38 -0
  40. package/dist/contracts/common.js +41 -0
  41. package/dist/contracts/deliveries.d.ts +103 -0
  42. package/dist/contracts/deliveries.js +74 -0
  43. package/dist/contracts/destinations.d.ts +24 -0
  44. package/dist/contracts/destinations.js +28 -0
  45. package/dist/contracts/events.d.ts +165 -0
  46. package/dist/contracts/events.js +98 -0
  47. package/dist/contracts/features.d.ts +20 -0
  48. package/dist/contracts/features.js +21 -0
  49. package/dist/contracts/health.d.ts +55 -0
  50. package/dist/contracts/health.js +58 -0
  51. package/dist/contracts/index.d.ts +22 -0
  52. package/dist/contracts/index.js +22 -0
  53. package/dist/contracts/labels.d.ts +38 -0
  54. package/dist/contracts/labels.js +52 -0
  55. package/dist/contracts/links.d.ts +51 -0
  56. package/dist/contracts/links.js +50 -0
  57. package/dist/contracts/meta.d.ts +132 -0
  58. package/dist/contracts/meta.js +85 -0
  59. package/dist/contracts/permissions.d.ts +24 -0
  60. package/dist/contracts/permissions.js +27 -0
  61. package/dist/contracts/problems.d.ts +106 -0
  62. package/dist/contracts/problems.js +101 -0
  63. package/dist/contracts/recipients.d.ts +36 -0
  64. package/dist/contracts/recipients.js +22 -0
  65. package/dist/contracts/schedules.d.ts +276 -0
  66. package/dist/contracts/schedules.js +143 -0
  67. package/dist/contracts/send.d.ts +126 -0
  68. package/dist/contracts/send.js +69 -0
  69. package/dist/contracts/subscriptions.d.ts +38 -0
  70. package/dist/contracts/subscriptions.js +31 -0
  71. package/dist/contracts/templates.d.ts +358 -0
  72. package/dist/contracts/templates.js +174 -0
  73. package/dist/contracts/users.d.ts +15 -0
  74. package/dist/contracts/users.js +19 -0
  75. package/dist/core/guards.d.ts +22 -0
  76. package/dist/core/guards.js +47 -0
  77. package/dist/core/html/escape.d.ts +2 -0
  78. package/dist/core/html/escape.js +36 -0
  79. package/dist/core/html/tokenize.js +65 -0
  80. package/dist/core/index.d.ts +18 -0
  81. package/dist/core/pacing.d.ts +1 -0
  82. package/dist/core/pacing.js +86 -0
  83. package/dist/core/random.d.ts +2 -0
  84. package/dist/core/random.js +38 -0
  85. package/dist/core/recipients.d.ts +1 -0
  86. package/dist/core/recipients.js +86 -0
  87. package/dist/core/schedule/describe.d.ts +3 -0
  88. package/dist/core/schedule/describe.js +74 -0
  89. package/dist/core/schedule/index.d.ts +4 -0
  90. package/dist/core/schedule/policy.d.ts +1 -0
  91. package/dist/core/schedule/policy.js +37 -0
  92. package/dist/core/schedule/rule.d.ts +25 -0
  93. package/dist/core/schedule/rule.js +142 -0
  94. package/dist/core/schedule/tz.d.ts +2 -0
  95. package/dist/core/schedule/tz.js +59 -0
  96. package/dist/core/split/plain.d.ts +2 -0
  97. package/dist/core/split/plain.js +23 -0
  98. package/dist/core/split/split.d.ts +15 -0
  99. package/dist/core/split/split.js +158 -0
  100. package/dist/core/template/ast.d.ts +58 -0
  101. package/dist/core/template/ast.js +5 -0
  102. package/dist/core/template/expr.js +108 -0
  103. package/dist/core/template/filters.d.ts +20 -0
  104. package/dist/core/template/filters.js +127 -0
  105. package/dist/core/template/format.d.ts +17 -0
  106. package/dist/core/template/format.js +88 -0
  107. package/dist/core/template/index.d.ts +20 -0
  108. package/dist/core/template/index.js +29 -0
  109. package/dist/core/template/parse.d.ts +8 -0
  110. package/dist/core/template/parse.js +369 -0
  111. package/dist/core/template/render.d.ts +17 -0
  112. package/dist/core/template/render.js +118 -0
  113. package/dist/core/template/shape.d.ts +28 -0
  114. package/dist/core/template/shape.js +151 -0
  115. package/dist/core/template/tags.js +105 -0
  116. package/dist/core/template/validate.d.ts +10 -0
  117. package/dist/core/template/validate.js +144 -0
  118. package/dist/core/template/variables.d.ts +15 -0
  119. package/dist/core/template/variables.js +41 -0
  120. package/dist/do/index.d.ts +3 -0
  121. package/dist/do/index.js +3 -0
  122. package/dist/do/telegram-bot-do.d.ts +45 -0
  123. package/dist/do/telegram-bot-do.js +219 -0
  124. package/dist/engine/actions/callback.js +256 -0
  125. package/dist/engine/actions/tokens.d.ts +2 -0
  126. package/dist/engine/actions/tokens.js +51 -0
  127. package/dist/engine/bot-api.js +20 -0
  128. package/dist/engine/bot-features.js +39 -0
  129. package/dist/engine/chat-state.js +66 -0
  130. package/dist/engine/content/audience.js +54 -0
  131. package/dist/engine/content/buttons.d.ts +1 -0
  132. package/dist/engine/content/buttons.js +113 -0
  133. package/dist/engine/content/fanout.js +284 -0
  134. package/dist/engine/content/intersect.d.ts +6 -0
  135. package/dist/engine/content/intersect.js +26 -0
  136. package/dist/engine/content/providers.d.ts +6 -0
  137. package/dist/engine/content/providers.js +142 -0
  138. package/dist/engine/content/recipients.js +128 -0
  139. package/dist/engine/content/render.js +138 -0
  140. package/dist/engine/content/reply.js +156 -0
  141. package/dist/engine/content/report-files.js +78 -0
  142. package/dist/engine/content/template-rows.js +74 -0
  143. package/dist/engine/dialogs/dialog.d.ts +2 -0
  144. package/dist/engine/dialogs/dialog.js +375 -0
  145. package/dist/engine/dispatch/builtins.js +198 -0
  146. package/dist/engine/dispatch/commands.js +194 -0
  147. package/dist/engine/dispatch/context.d.ts +2 -0
  148. package/dist/engine/dispatch/context.js +194 -0
  149. package/dist/engine/dispatch/dispatcher.js +66 -0
  150. package/dist/engine/dispatch/handlers.d.ts +1 -0
  151. package/dist/engine/dispatch/handlers.js +103 -0
  152. package/dist/engine/dispatch/lifecycle.js +85 -0
  153. package/dist/engine/dispatch/link-chat.js +31 -0
  154. package/dist/engine/dispatch/linking.js +273 -0
  155. package/dist/engine/errors.d.ts +60 -0
  156. package/dist/engine/errors.js +72 -0
  157. package/dist/engine/events/filters.d.ts +5 -0
  158. package/dist/engine/events/filters.js +82 -0
  159. package/dist/engine/events/kick.js +30 -0
  160. package/dist/engine/events/payload.d.ts +3 -0
  161. package/dist/engine/events/payload.js +16 -0
  162. package/dist/engine/events/tick.js +208 -0
  163. package/dist/engine/i18n.d.ts +12 -0
  164. package/dist/engine/i18n.js +55 -0
  165. package/dist/engine/inbound/files.js +121 -0
  166. package/dist/engine/inbound/handler.js +46 -0
  167. package/dist/engine/jobs.d.ts +32 -0
  168. package/dist/engine/jobs.js +85 -0
  169. package/dist/engine/link-codes.js +46 -0
  170. package/dist/engine/metrics.js +12 -0
  171. package/dist/engine/options.d.ts +369 -0
  172. package/dist/engine/options.js +81 -0
  173. package/dist/engine/outbox/dead-letter.js +78 -0
  174. package/dist/engine/outbox/do-registry.d.ts +10 -0
  175. package/dist/engine/outbox/do-registry.js +10 -0
  176. package/dist/engine/outbox/do-transport.d.ts +33 -0
  177. package/dist/engine/outbox/do-transport.js +73 -0
  178. package/dist/engine/outbox/drain.d.ts +13 -0
  179. package/dist/engine/outbox/drain.js +381 -0
  180. package/dist/engine/outbox/enqueue.d.ts +1 -0
  181. package/dist/engine/outbox/enqueue.js +63 -0
  182. package/dist/engine/outbox/select.js +24 -0
  183. package/dist/engine/outbox/send.js +362 -0
  184. package/dist/engine/outbox/transport.d.ts +35 -0
  185. package/dist/engine/outbox/transport.js +175 -0
  186. package/dist/engine/outbox/wake.js +28 -0
  187. package/dist/engine/registry.d.ts +502 -0
  188. package/dist/engine/registry.js +258 -0
  189. package/dist/engine/rows.d.ts +25 -0
  190. package/dist/engine/rows.js +131 -0
  191. package/dist/engine/runtime.d.ts +95 -0
  192. package/dist/engine/runtime.js +149 -0
  193. package/dist/engine/schedules/fire.js +208 -0
  194. package/dist/engine/schedules/resume.js +75 -0
  195. package/dist/engine/schedules/rows.js +69 -0
  196. package/dist/engine/schedules/tick.js +30 -0
  197. package/dist/engine/schedules/wake.js +50 -0
  198. package/dist/engine/state.d.ts +11 -0
  199. package/dist/engine/state.js +78 -0
  200. package/dist/locales/en.js +339 -0
  201. package/dist/locales/en.json +341 -0
  202. package/dist/locales/mr.js +339 -0
  203. package/dist/locales/mr.json +341 -0
  204. package/dist/ports/db.d.ts +22 -0
  205. package/dist/ports/index.d.ts +4 -0
  206. package/dist/ports/ports.d.ts +257 -0
  207. package/dist/ports/sql.d.ts +39 -0
  208. package/dist/ports/sql.js +76 -0
  209. package/dist/renderers/csv.d.ts +14 -0
  210. package/dist/renderers/csv.js +51 -0
  211. package/dist/renderers/html.d.ts +44 -0
  212. package/dist/renderers/html.js +81 -0
  213. package/dist/renderers/index.d.ts +5 -0
  214. package/dist/renderers/index.js +4 -0
  215. package/dist/renderers/table.d.ts +30 -0
  216. package/dist/renderers/table.js +114 -0
  217. package/dist/routes/admin.js +146 -0
  218. package/dist/routes/app.d.ts +3111 -0
  219. package/dist/routes/app.js +9 -0
  220. package/dist/routes/broadcasts.js +37 -0
  221. package/dist/routes/commands.js +35 -0
  222. package/dist/routes/content.js +12 -0
  223. package/dist/routes/events.js +36 -0
  224. package/dist/routes/http.d.ts +30 -0
  225. package/dist/routes/http.js +223 -0
  226. package/dist/routes/me.js +59 -0
  227. package/dist/routes/schedules.js +62 -0
  228. package/dist/routes/send.js +24 -0
  229. package/dist/routes/templates.js +70 -0
  230. package/dist/routes/webhook.js +147 -0
  231. package/dist/schema/columns.js +307 -0
  232. package/dist/schema/ddl.d.ts +7 -0
  233. package/dist/schema/ddl.js +45 -0
  234. package/dist/schema/index.d.ts +4255 -0
  235. package/dist/schema/index.js +44 -0
  236. package/dist/schema/reference.sql +359 -0
  237. package/dist/schema/tables.d.ts +4243 -0
  238. package/dist/schema/tables.js +327 -0
  239. package/dist/schema/version.d.ts +5 -0
  240. package/dist/schema/version.js +7 -0
  241. package/dist/server/index.d.ts +55 -0
  242. package/dist/server/index.js +46 -0
  243. package/dist/server/plugin.d.ts +81 -0
  244. package/dist/server/plugin.js +247 -0
  245. package/dist/server/version.d.ts +2 -0
  246. package/dist/server/version.js +4 -0
  247. package/dist/services/ai-tool.d.ts +26 -0
  248. package/dist/services/ai-tool.js +148 -0
  249. package/dist/services/bots.js +300 -0
  250. package/dist/services/broadcasts.js +390 -0
  251. package/dist/services/chats.js +83 -0
  252. package/dist/services/cleanup.js +83 -0
  253. package/dist/services/commands.js +140 -0
  254. package/dist/services/deliveries.js +53 -0
  255. package/dist/services/destinations.js +91 -0
  256. package/dist/services/event-rules.js +229 -0
  257. package/dist/services/events.js +20 -0
  258. package/dist/services/idempotency.js +64 -0
  259. package/dist/services/links.js +200 -0
  260. package/dist/services/menu.js +95 -0
  261. package/dist/services/ops.js +90 -0
  262. package/dist/services/rekey.js +63 -0
  263. package/dist/services/reports.js +87 -0
  264. package/dist/services/schedules.js +214 -0
  265. package/dist/services/send.d.ts +14 -0
  266. package/dist/services/send.js +421 -0
  267. package/dist/services/subscriptions.js +28 -0
  268. package/dist/services/templates.js +259 -0
  269. package/dist/telegram/classify.d.ts +30 -0
  270. package/dist/telegram/classify.js +164 -0
  271. package/dist/telegram/client.d.ts +73 -0
  272. package/dist/telegram/client.js +135 -0
  273. package/dist/telegram/errors.d.ts +37 -0
  274. package/dist/telegram/errors.js +62 -0
  275. package/dist/telegram/inbound.d.ts +40 -0
  276. package/dist/telegram/inbound.js +72 -0
  277. package/dist/telegram/index.d.ts +8 -0
  278. package/dist/telegram/multipart.d.ts +5 -0
  279. package/dist/telegram/multipart.js +47 -0
  280. package/dist/telegram/types.d.ts +271 -0
  281. package/dist/telegram/update.d.ts +833 -0
  282. package/dist/telegram/update.js +129 -0
  283. package/dist/testing/fake-html.d.ts +9 -0
  284. package/dist/testing/fake-html.js +83 -0
  285. package/dist/testing/fake-telegram.d.ts +206 -0
  286. package/dist/testing/fake-telegram.js +856 -0
  287. package/dist/testing/index.d.ts +7 -0
  288. package/dist/testing/index.js +7 -0
  289. package/dist/testing/reference-sql.d.ts +4 -0
  290. package/dist/testing/reference-sql.js +8 -0
  291. package/dist/testing/test-plugin.d.ts +126 -0
  292. package/dist/testing/test-plugin.js +182 -0
  293. package/dist/testing/updates.d.ts +100 -0
  294. package/dist/testing/updates.js +212 -0
  295. 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 };