gramio 0.6.2 → 0.8.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/dist/index.js CHANGED
@@ -9,10 +9,140 @@ export * from '@gramio/keyboards';
9
9
  import fs from 'node:fs/promises';
10
10
  import { Readable } from 'node:stream';
11
11
  import debug from 'debug';
12
- import { E as ErrorKind, w as withRetries, T as TelegramError, s as sleep, d as debug$updates, I as IS_BUN, a as simplifyObject, t as timeoutWebhook } from './utils-DqK73ALI.js';
12
+ import { E as ErrorKind, w as withRetries, T as TelegramError, s as sleep, d as debug$updates, a as simpleHash, I as IS_BUN, b as simplifyObject, t as timeoutWebhook } from './utils-1ejkKLIB.js';
13
13
  import { defineComposerMethods, buildFromOptions, noopNext, createComposer, eventTypes, EventQueue } from '@gramio/composer';
14
14
  export { EventQueue, buildFromOptions, compose, noopNext, skip, stop } from '@gramio/composer';
15
15
 
16
+ const ALL_NAMES = [
17
+ "message",
18
+ "edited_message",
19
+ "channel_post",
20
+ "edited_channel_post",
21
+ "business_connection",
22
+ "business_message",
23
+ "edited_business_message",
24
+ "deleted_business_messages",
25
+ "message_reaction",
26
+ "message_reaction_count",
27
+ "inline_query",
28
+ "chosen_inline_result",
29
+ "callback_query",
30
+ "shipping_query",
31
+ "pre_checkout_query",
32
+ "purchased_paid_media",
33
+ "poll",
34
+ "poll_answer",
35
+ "my_chat_member",
36
+ "chat_member",
37
+ "chat_join_request",
38
+ "chat_boost",
39
+ "removed_chat_boost"
40
+ ];
41
+ const MESSAGE_PARENT_TYPES = [
42
+ "message",
43
+ "edited_message",
44
+ "channel_post",
45
+ "edited_channel_post",
46
+ "business_message"
47
+ ];
48
+ const OPT_IN_TYPES = [
49
+ "chat_member",
50
+ "message_reaction",
51
+ "message_reaction_count"
52
+ ];
53
+ const KNOWN_EVENTS = new Set(Object.keys(contextsMappings));
54
+ const ALL_NAMES_SET = new Set(ALL_NAMES);
55
+ function mapEventToAllowedUpdates(event) {
56
+ if (ALL_NAMES_SET.has(event)) return [event];
57
+ if (KNOWN_EVENTS.has(event)) return MESSAGE_PARENT_TYPES;
58
+ return void 0;
59
+ }
60
+ function detectOptInUpdates(registeredEvents) {
61
+ return OPT_IN_TYPES.filter((type) => registeredEvents.has(type));
62
+ }
63
+ class AllowedUpdatesFilter extends Array {
64
+ /** @internal use static factory methods instead */
65
+ constructor(updates) {
66
+ super(...updates);
67
+ }
68
+ /**
69
+ * All update types, including the opt-in ones:
70
+ * `chat_member`, `message_reaction`, and `message_reaction_count`.
71
+ */
72
+ static get all() {
73
+ return new AllowedUpdatesFilter(ALL_NAMES);
74
+ }
75
+ /**
76
+ * Telegram's **default** update set.
77
+ *
78
+ * Receive all updates _except_ `chat_member`, `message_reaction`, and
79
+ * `message_reaction_count` — the three types that Telegram requires to be
80
+ * explicitly listed in `allowed_updates`.
81
+ *
82
+ * This matches what Telegram does when `allowed_updates` is omitted or
83
+ * passed as an empty array.
84
+ */
85
+ static get default() {
86
+ return AllowedUpdatesFilter.all.except(
87
+ "chat_member",
88
+ "message_reaction",
89
+ "message_reaction_count"
90
+ );
91
+ }
92
+ /**
93
+ * Create a filter with **exactly** the specified update types.
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * AllowedUpdatesFilter.only("message", "callback_query", "inline_query")
98
+ * ```
99
+ */
100
+ static only(...types) {
101
+ return new AllowedUpdatesFilter(types);
102
+ }
103
+ /**
104
+ * Return a new filter with the given types **added**.
105
+ * Already-present types are silently deduplicated.
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * AllowedUpdatesFilter.default.add("chat_member", "message_reaction")
110
+ * ```
111
+ */
112
+ add(...types) {
113
+ const set = new Set(this);
114
+ for (const t of types) set.add(t);
115
+ return new AllowedUpdatesFilter([...set]);
116
+ }
117
+ /**
118
+ * Return a new filter with the given types **removed**.
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * AllowedUpdatesFilter.all.except("poll", "poll_answer", "chosen_inline_result")
123
+ * ```
124
+ */
125
+ except(...types) {
126
+ const excluded = new Set(types);
127
+ return new AllowedUpdatesFilter(
128
+ Array.from(this).filter((t) => !excluded.has(t))
129
+ );
130
+ }
131
+ /** Convert to a plain `AllowedUpdateName[]` array. */
132
+ toArray() {
133
+ return Array.from(this);
134
+ }
135
+ }
136
+ function buildAllowedUpdates(bot) {
137
+ const registeredEvents = bot.updates.composer.registeredEvents();
138
+ const allowedSet = /* @__PURE__ */ new Set();
139
+ for (const event of registeredEvents) {
140
+ const mapped = mapEventToAllowedUpdates(event);
141
+ if (mapped) for (const name of mapped) allowedSet.add(name);
142
+ }
143
+ return new AllowedUpdatesFilter([...allowedSet]);
144
+ }
145
+
16
146
  const methods = defineComposerMethods({
17
147
  reaction(trigger, handler, macroOptions) {
18
148
  const reactions = Array.isArray(trigger) ? trigger : [trigger];
@@ -131,13 +261,32 @@ const methods = defineComposerMethods({
131
261
  return macroHandler ? macroHandler(context, noopNext) : handler(context);
132
262
  });
133
263
  },
134
- command(command, handler, macroOptions) {
264
+ command(command, handlerOrMeta, handlerOrOptions, macroOptions) {
265
+ let handler;
266
+ let meta;
267
+ let resolvedMacroOptions;
268
+ if (typeof handlerOrMeta === "function") {
269
+ handler = handlerOrMeta;
270
+ resolvedMacroOptions = handlerOrOptions;
271
+ } else {
272
+ meta = handlerOrMeta;
273
+ handler = handlerOrOptions;
274
+ resolvedMacroOptions = macroOptions;
275
+ }
135
276
  const normalizedCommands = typeof command === "string" ? [command] : Array.from(command);
136
277
  for (const cmd of normalizedCommands) {
137
278
  if (cmd.startsWith("/"))
138
279
  throw new Error(`Do not use / in command name (${cmd})`);
139
280
  }
140
- const macroHandler = macroOptions ? buildFromOptions(this["~"].macros, macroOptions, handler) : null;
281
+ if (meta) {
282
+ if (!this["~"].commandsMeta) {
283
+ this["~"].commandsMeta = /* @__PURE__ */ new Map();
284
+ }
285
+ for (const cmd of normalizedCommands) {
286
+ this["~"].commandsMeta.set(cmd, meta);
287
+ }
288
+ }
289
+ const macroHandler = resolvedMacroOptions ? buildFromOptions(this["~"].macros, resolvedMacroOptions, handler) : null;
141
290
  return this.on(["message", "business_message"], (context, next) => {
142
291
  const entity = context.entities?.find((entity2) => {
143
292
  if (entity2.type !== "bot_command" || entity2.offset > 0) return false;
@@ -184,6 +333,35 @@ const { Composer } = createComposer({
184
333
  types: eventTypes(),
185
334
  methods
186
335
  });
336
+ if (typeof Composer.prototype.registeredEvents !== "function") {
337
+ Composer.prototype.registeredEvents = function() {
338
+ const events = /* @__PURE__ */ new Set();
339
+ for (const mw of this["~"].middlewares) {
340
+ if ((mw.type === "on" || mw.type === "derive") && mw.name) {
341
+ for (const part of mw.name.split("|")) {
342
+ const eventPart = part.includes(":") ? part.split(":")[0] : part;
343
+ if (eventPart) events.add(eventPart);
344
+ }
345
+ }
346
+ }
347
+ return events;
348
+ };
349
+ }
350
+ {
351
+ const originalExtend = Composer.prototype.extend;
352
+ Composer.prototype.extend = function(other) {
353
+ const result = originalExtend.call(this, other);
354
+ if (other["~"]?.commandsMeta) {
355
+ if (!this["~"].commandsMeta) {
356
+ this["~"].commandsMeta = /* @__PURE__ */ new Map();
357
+ }
358
+ for (const [cmd, meta] of other["~"].commandsMeta) {
359
+ this["~"].commandsMeta.set(cmd, meta);
360
+ }
361
+ }
362
+ return result;
363
+ };
364
+ }
187
365
 
188
366
  class Plugin {
189
367
  /**
@@ -343,7 +521,7 @@ class Plugin {
343
521
  * import { Bot } from "gramio";
344
522
  *
345
523
  * const bot = new Bot(process.env.TOKEN!).onStart(
346
- * ({ plugins, info, updatesFrom }) => {
524
+ * ({ plugins, info, updatesFrom, bot }) => {
347
525
  * console.log(`plugin list - ${plugins.join(", ")}`);
348
526
  * console.log(`bot username is @${info.username}`);
349
527
  * console.log(`updates from ${updatesFrom}`);
@@ -367,7 +545,7 @@ class Plugin {
367
545
  * import { Bot } from "gramio";
368
546
  *
369
547
  * const bot = new Bot(process.env.TOKEN!).onStop(
370
- * ({ plugins, info, updatesFrom }) => {
548
+ * ({ plugins, info, bot }) => {
371
549
  * console.log(`plugin list - ${plugins.join(", ")}`);
372
550
  * console.log(`bot username is @${info.username}`);
373
551
  * }
@@ -796,7 +974,7 @@ class Bot {
796
974
  * import { Bot } from "gramio";
797
975
  *
798
976
  * const bot = new Bot(process.env.TOKEN!).onStart(
799
- * ({ plugins, info, updatesFrom }) => {
977
+ * ({ plugins, info, updatesFrom, bot }) => {
800
978
  * console.log(`plugin list - ${plugins.join(", ")}`);
801
979
  * console.log(`bot username is @${info.username}`);
802
980
  * console.log(`updates from ${updatesFrom}`);
@@ -820,7 +998,7 @@ class Bot {
820
998
  * import { Bot } from "gramio";
821
999
  *
822
1000
  * const bot = new Bot(process.env.TOKEN!).onStop(
823
- * ({ plugins, info, updatesFrom }) => {
1001
+ * ({ plugins, info, bot }) => {
824
1002
  * console.log(`plugin list - ${plugins.join(", ")}`);
825
1003
  * console.log(`bot username is @${info.username}`);
826
1004
  * }
@@ -1087,18 +1265,8 @@ class Bot {
1087
1265
  this.updates.composer.hears(trigger, handler, options);
1088
1266
  return this;
1089
1267
  }
1090
- /**
1091
- * Register handler to `message` and `business_message` event when entities contains a command
1092
- *
1093
- * @example
1094
- * ```ts
1095
- * new Bot().command("start", async (context) => {
1096
- * return context.send(`You message is /start ${context.args}`);
1097
- * });
1098
- * ```
1099
- */
1100
- command(command, handler, options) {
1101
- this.updates.composer.command(command, handler, options);
1268
+ command(command, handlerOrMeta, handlerOrOptions, macroOptions) {
1269
+ this.updates.composer.command(command, handlerOrMeta, handlerOrOptions, macroOptions);
1102
1270
  return this;
1103
1271
  }
1104
1272
  /**
@@ -1123,6 +1291,96 @@ class Bot {
1123
1291
  group(grouped) {
1124
1292
  return grouped(this);
1125
1293
  }
1294
+ /**
1295
+ * Sync registered command metadata with the Telegram API.
1296
+ *
1297
+ * Groups commands by `{scope, language_code}` and calls `setMyCommands` for each group.
1298
+ * When a `storage` is provided, hashes each payload and skips unchanged groups.
1299
+ *
1300
+ * @example
1301
+ * ```ts
1302
+ * bot.onStart(() => bot.syncCommands());
1303
+ * ```
1304
+ */
1305
+ async syncCommands(options) {
1306
+ const commandsMeta = this.updates.composer["~"].commandsMeta ?? /* @__PURE__ */ new Map();
1307
+ if (commandsMeta.size === 0) return;
1308
+ const storage = options?.storage;
1309
+ const botId = this.info?.id;
1310
+ const excludeSet = options?.exclude ? new Set(options.exclude) : void 0;
1311
+ const groups = /* @__PURE__ */ new Map();
1312
+ const getOrCreateGroup = (scope, langCode) => {
1313
+ const key = `${JSON.stringify(scope ?? { type: "default" })}:${langCode}`;
1314
+ let group = groups.get(key);
1315
+ if (!group) {
1316
+ group = { scope, language_code: langCode || void 0, commands: [] };
1317
+ groups.set(key, group);
1318
+ }
1319
+ return group;
1320
+ };
1321
+ for (const [name, meta] of commandsMeta) {
1322
+ if (meta.hide) continue;
1323
+ if (excludeSet?.has(name)) continue;
1324
+ const scopes = meta.scopes ? meta.scopes.map(
1325
+ (s) => typeof s === "string" ? { type: s } : s
1326
+ ) : [void 0];
1327
+ for (const scope of scopes) {
1328
+ getOrCreateGroup(scope, "").commands.push({
1329
+ command: name,
1330
+ description: meta.description
1331
+ });
1332
+ if (meta.locales) {
1333
+ for (const [lang, desc] of Object.entries(meta.locales)) {
1334
+ getOrCreateGroup(scope, lang).commands.push({
1335
+ command: name,
1336
+ description: desc
1337
+ });
1338
+ }
1339
+ }
1340
+ }
1341
+ }
1342
+ for (const [key, group] of groups) {
1343
+ if (group.commands.length > 100) {
1344
+ throw new Error(
1345
+ `Too many commands (${group.commands.length}) for scope ${key}. Telegram allows max 100 per scope+language.`
1346
+ );
1347
+ }
1348
+ }
1349
+ for (const [, group] of groups) {
1350
+ const payload = {
1351
+ commands: group.commands,
1352
+ scope: group.scope,
1353
+ language_code: group.language_code
1354
+ };
1355
+ if (storage) {
1356
+ const hash = simpleHash(JSON.stringify(payload));
1357
+ const storageKey = `gramio:commands:${botId ?? "unknown"}:${JSON.stringify(group.scope ?? { type: "default" })}:${group.language_code ?? ""}`;
1358
+ const stored = await storage.get(storageKey);
1359
+ if (stored === hash) continue;
1360
+ await this.api.setMyCommands(payload);
1361
+ await storage.set(storageKey, hash);
1362
+ } else {
1363
+ await this.api.setMyCommands(payload);
1364
+ }
1365
+ }
1366
+ if (options?.cleanUnusedScopes) {
1367
+ const declaredScopes = /* @__PURE__ */ new Set();
1368
+ for (const [, group] of groups) {
1369
+ declaredScopes.add(JSON.stringify(group.scope ?? { type: "default" }));
1370
+ }
1371
+ const allScopes = [
1372
+ { type: "default" },
1373
+ { type: "all_private_chats" },
1374
+ { type: "all_group_chats" },
1375
+ { type: "all_chat_administrators" }
1376
+ ];
1377
+ for (const scope of allScopes) {
1378
+ if (!declaredScopes.has(JSON.stringify(scope))) {
1379
+ await this.api.deleteMyCommands({ scope });
1380
+ }
1381
+ }
1382
+ }
1383
+ }
1126
1384
  /**
1127
1385
  * Init bot. Call it manually only if you doesn't use {@link Bot.start}
1128
1386
  */
@@ -1161,10 +1419,21 @@ class Bot {
1161
1419
  webhook,
1162
1420
  longPolling,
1163
1421
  dropPendingUpdates,
1164
- allowedUpdates,
1422
+ allowedUpdates: allowedUpdatesRaw,
1165
1423
  deleteWebhook: deleteWebhookRaw
1166
1424
  } = {}) {
1167
1425
  await this.init();
1426
+ let allowedUpdates;
1427
+ if (allowedUpdatesRaw === "strict") {
1428
+ allowedUpdates = buildAllowedUpdates(this);
1429
+ } else if (allowedUpdatesRaw === void 0) {
1430
+ const optIn = detectOptInUpdates(
1431
+ this.updates.composer.registeredEvents()
1432
+ );
1433
+ allowedUpdates = optIn.length > 0 ? AllowedUpdatesFilter.default.add(...optIn) : void 0;
1434
+ } else {
1435
+ allowedUpdates = allowedUpdatesRaw;
1436
+ }
1168
1437
  const deleteWebhook = deleteWebhookRaw ?? "on-conflict-with-polling";
1169
1438
  if (!webhook) {
1170
1439
  if (deleteWebhook === true)
@@ -1187,7 +1456,8 @@ class Bot {
1187
1456
  plugins: this.dependencies,
1188
1457
  // biome-ignore lint/style/noNonNullAssertion: bot.init() guarantees this.info
1189
1458
  info: this.info,
1190
- updatesFrom: "long-polling"
1459
+ updatesFrom: "long-polling",
1460
+ bot: this
1191
1461
  });
1192
1462
  return this.info;
1193
1463
  }
@@ -1205,7 +1475,8 @@ class Bot {
1205
1475
  plugins: this.dependencies,
1206
1476
  // biome-ignore lint/style/noNonNullAssertion: bot.init() guarantees this.info
1207
1477
  info: this.info,
1208
- updatesFrom: "webhook"
1478
+ updatesFrom: "webhook",
1479
+ bot: this
1209
1480
  });
1210
1481
  return this.info;
1211
1482
  }
@@ -1222,7 +1493,8 @@ class Bot {
1222
1493
  await this.runImmutableHooks("onStop", {
1223
1494
  plugins: this.dependencies,
1224
1495
  // biome-ignore lint/style/noNonNullAssertion: bot.init() guarantees this.info
1225
- info: this.info
1496
+ info: this.info,
1497
+ bot: this
1226
1498
  });
1227
1499
  }
1228
1500
  }
@@ -1511,4 +1783,4 @@ function webhookHandler(bot, framework, secretTokenOrOptions) {
1511
1783
 
1512
1784
  Symbol.metadata ??= Symbol("Symbol.metadata");
1513
1785
 
1514
- export { Bot, Composer, ErrorKind, Plugin, TelegramError, Updates, filters, webhookHandler };
1786
+ export { AllowedUpdatesFilter, Bot, Composer, ErrorKind, OPT_IN_TYPES, Plugin, TelegramError, Updates, buildAllowedUpdates, detectOptInUpdates, filters, mapEventToAllowedUpdates, webhookHandler };
@@ -48,6 +48,13 @@ function simplifyObject(obj) {
48
48
  const IS_BUN = typeof Bun !== "undefined";
49
49
  debug("gramio:api");
50
50
  const debug$updates = debug("gramio:updates");
51
+ function simpleHash(str) {
52
+ let hash = 5381;
53
+ for (let i = 0; i < str.length; i++) {
54
+ hash = (hash << 5) + hash + str.charCodeAt(i) | 0;
55
+ }
56
+ return (hash >>> 0).toString(36);
57
+ }
51
58
  function timeoutWebhook(task, timeout, mode) {
52
59
  return new Promise((resolve, reject) => {
53
60
  const timeoutTask = setTimeout(() => {
@@ -86,4 +93,4 @@ async function withRetries(resultPromise) {
86
93
  return state.value;
87
94
  }
88
95
 
89
- export { ErrorKind as E, IS_BUN as I, TelegramError as T, simplifyObject as a, debug$updates as d, sleep as s, timeoutWebhook as t, withRetries as w };
96
+ export { ErrorKind as E, IS_BUN as I, TelegramError as T, simpleHash as a, simplifyObject as b, debug$updates as d, sleep as s, timeoutWebhook as t, withRetries as w };
@@ -50,6 +50,13 @@ function simplifyObject(obj) {
50
50
  const IS_BUN = typeof Bun !== "undefined";
51
51
  debug("gramio:api");
52
52
  const debug$updates = debug("gramio:updates");
53
+ function simpleHash(str) {
54
+ let hash = 5381;
55
+ for (let i = 0; i < str.length; i++) {
56
+ hash = (hash << 5) + hash + str.charCodeAt(i) | 0;
57
+ }
58
+ return (hash >>> 0).toString(36);
59
+ }
53
60
  function timeoutWebhook(task, timeout, mode) {
54
61
  return new Promise((resolve, reject) => {
55
62
  const timeoutTask = setTimeout(() => {
@@ -92,6 +99,7 @@ exports.ErrorKind = ErrorKind;
92
99
  exports.IS_BUN = IS_BUN;
93
100
  exports.TelegramError = TelegramError;
94
101
  exports.debug$updates = debug$updates;
102
+ exports.simpleHash = simpleHash;
95
103
  exports.simplifyObject = simplifyObject;
96
104
  exports.sleep = sleep;
97
105
  exports.timeoutWebhook = timeoutWebhook;
package/dist/utils.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var utils = require('./utils-DCXj-tb5.cjs');
3
+ var utils = require('./utils-BKLnNOBm.cjs');
4
4
  require('debug');
5
5
 
6
6
 
package/dist/utils.js CHANGED
@@ -1,2 +1,2 @@
1
- export { w as withRetries } from './utils-DqK73ALI.js';
1
+ export { w as withRetries } from './utils-1ejkKLIB.js';
2
2
  import 'debug';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gramio",
3
3
  "type": "module",
4
- "version": "0.6.2",
4
+ "version": "0.8.0",
5
5
  "description": "Powerful, extensible and really type-safe Telegram Bot API framework",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",
@@ -65,12 +65,12 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@gramio/callback-data": "^0.1.0",
68
- "@gramio/composer": "^0.3.3",
69
- "@gramio/contexts": "^0.4.0",
70
- "@gramio/files": "^0.3.1",
71
- "@gramio/format": "^0.4.0",
72
- "@gramio/keyboards": "^1.3.0",
73
- "@gramio/types": "^9.4.2",
68
+ "@gramio/composer": "^0.4.0",
69
+ "@gramio/contexts": "^0.5.0",
70
+ "@gramio/files": "^0.3.2",
71
+ "@gramio/format": "^0.5.0",
72
+ "@gramio/keyboards": "^1.3.1",
73
+ "@gramio/types": "^9.5.0",
74
74
  "debug": "^4.4.3"
75
75
  },
76
76
  "files": [