gramio 0.7.0 → 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.cjs CHANGED
@@ -8,9 +8,139 @@ var keyboards = require('@gramio/keyboards');
8
8
  var fs = require('node:fs/promises');
9
9
  var node_stream = require('node:stream');
10
10
  var debug = require('debug');
11
- var utils = require('./utils-DCXj-tb5.cjs');
11
+ var utils = require('./utils-BKLnNOBm.cjs');
12
12
  var composer = require('@gramio/composer');
13
13
 
14
+ const ALL_NAMES = [
15
+ "message",
16
+ "edited_message",
17
+ "channel_post",
18
+ "edited_channel_post",
19
+ "business_connection",
20
+ "business_message",
21
+ "edited_business_message",
22
+ "deleted_business_messages",
23
+ "message_reaction",
24
+ "message_reaction_count",
25
+ "inline_query",
26
+ "chosen_inline_result",
27
+ "callback_query",
28
+ "shipping_query",
29
+ "pre_checkout_query",
30
+ "purchased_paid_media",
31
+ "poll",
32
+ "poll_answer",
33
+ "my_chat_member",
34
+ "chat_member",
35
+ "chat_join_request",
36
+ "chat_boost",
37
+ "removed_chat_boost"
38
+ ];
39
+ const MESSAGE_PARENT_TYPES = [
40
+ "message",
41
+ "edited_message",
42
+ "channel_post",
43
+ "edited_channel_post",
44
+ "business_message"
45
+ ];
46
+ const OPT_IN_TYPES = [
47
+ "chat_member",
48
+ "message_reaction",
49
+ "message_reaction_count"
50
+ ];
51
+ const KNOWN_EVENTS = new Set(Object.keys(contexts.contextsMappings));
52
+ const ALL_NAMES_SET = new Set(ALL_NAMES);
53
+ function mapEventToAllowedUpdates(event) {
54
+ if (ALL_NAMES_SET.has(event)) return [event];
55
+ if (KNOWN_EVENTS.has(event)) return MESSAGE_PARENT_TYPES;
56
+ return void 0;
57
+ }
58
+ function detectOptInUpdates(registeredEvents) {
59
+ return OPT_IN_TYPES.filter((type) => registeredEvents.has(type));
60
+ }
61
+ class AllowedUpdatesFilter extends Array {
62
+ /** @internal use static factory methods instead */
63
+ constructor(updates) {
64
+ super(...updates);
65
+ }
66
+ /**
67
+ * All update types, including the opt-in ones:
68
+ * `chat_member`, `message_reaction`, and `message_reaction_count`.
69
+ */
70
+ static get all() {
71
+ return new AllowedUpdatesFilter(ALL_NAMES);
72
+ }
73
+ /**
74
+ * Telegram's **default** update set.
75
+ *
76
+ * Receive all updates _except_ `chat_member`, `message_reaction`, and
77
+ * `message_reaction_count` — the three types that Telegram requires to be
78
+ * explicitly listed in `allowed_updates`.
79
+ *
80
+ * This matches what Telegram does when `allowed_updates` is omitted or
81
+ * passed as an empty array.
82
+ */
83
+ static get default() {
84
+ return AllowedUpdatesFilter.all.except(
85
+ "chat_member",
86
+ "message_reaction",
87
+ "message_reaction_count"
88
+ );
89
+ }
90
+ /**
91
+ * Create a filter with **exactly** the specified update types.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * AllowedUpdatesFilter.only("message", "callback_query", "inline_query")
96
+ * ```
97
+ */
98
+ static only(...types) {
99
+ return new AllowedUpdatesFilter(types);
100
+ }
101
+ /**
102
+ * Return a new filter with the given types **added**.
103
+ * Already-present types are silently deduplicated.
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * AllowedUpdatesFilter.default.add("chat_member", "message_reaction")
108
+ * ```
109
+ */
110
+ add(...types) {
111
+ const set = new Set(this);
112
+ for (const t of types) set.add(t);
113
+ return new AllowedUpdatesFilter([...set]);
114
+ }
115
+ /**
116
+ * Return a new filter with the given types **removed**.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * AllowedUpdatesFilter.all.except("poll", "poll_answer", "chosen_inline_result")
121
+ * ```
122
+ */
123
+ except(...types) {
124
+ const excluded = new Set(types);
125
+ return new AllowedUpdatesFilter(
126
+ Array.from(this).filter((t) => !excluded.has(t))
127
+ );
128
+ }
129
+ /** Convert to a plain `AllowedUpdateName[]` array. */
130
+ toArray() {
131
+ return Array.from(this);
132
+ }
133
+ }
134
+ function buildAllowedUpdates(bot) {
135
+ const registeredEvents = bot.updates.composer.registeredEvents();
136
+ const allowedSet = /* @__PURE__ */ new Set();
137
+ for (const event of registeredEvents) {
138
+ const mapped = mapEventToAllowedUpdates(event);
139
+ if (mapped) for (const name of mapped) allowedSet.add(name);
140
+ }
141
+ return new AllowedUpdatesFilter([...allowedSet]);
142
+ }
143
+
14
144
  const methods = composer.defineComposerMethods({
15
145
  reaction(trigger, handler, macroOptions) {
16
146
  const reactions = Array.isArray(trigger) ? trigger : [trigger];
@@ -129,13 +259,32 @@ const methods = composer.defineComposerMethods({
129
259
  return macroHandler ? macroHandler(context, composer.noopNext) : handler(context);
130
260
  });
131
261
  },
132
- command(command, handler, macroOptions) {
262
+ command(command, handlerOrMeta, handlerOrOptions, macroOptions) {
263
+ let handler;
264
+ let meta;
265
+ let resolvedMacroOptions;
266
+ if (typeof handlerOrMeta === "function") {
267
+ handler = handlerOrMeta;
268
+ resolvedMacroOptions = handlerOrOptions;
269
+ } else {
270
+ meta = handlerOrMeta;
271
+ handler = handlerOrOptions;
272
+ resolvedMacroOptions = macroOptions;
273
+ }
133
274
  const normalizedCommands = typeof command === "string" ? [command] : Array.from(command);
134
275
  for (const cmd of normalizedCommands) {
135
276
  if (cmd.startsWith("/"))
136
277
  throw new Error(`Do not use / in command name (${cmd})`);
137
278
  }
138
- const macroHandler = macroOptions ? composer.buildFromOptions(this["~"].macros, macroOptions, handler) : null;
279
+ if (meta) {
280
+ if (!this["~"].commandsMeta) {
281
+ this["~"].commandsMeta = /* @__PURE__ */ new Map();
282
+ }
283
+ for (const cmd of normalizedCommands) {
284
+ this["~"].commandsMeta.set(cmd, meta);
285
+ }
286
+ }
287
+ const macroHandler = resolvedMacroOptions ? composer.buildFromOptions(this["~"].macros, resolvedMacroOptions, handler) : null;
139
288
  return this.on(["message", "business_message"], (context, next) => {
140
289
  const entity = context.entities?.find((entity2) => {
141
290
  if (entity2.type !== "bot_command" || entity2.offset > 0) return false;
@@ -182,6 +331,35 @@ const { Composer } = composer.createComposer({
182
331
  types: composer.eventTypes(),
183
332
  methods
184
333
  });
334
+ if (typeof Composer.prototype.registeredEvents !== "function") {
335
+ Composer.prototype.registeredEvents = function() {
336
+ const events = /* @__PURE__ */ new Set();
337
+ for (const mw of this["~"].middlewares) {
338
+ if ((mw.type === "on" || mw.type === "derive") && mw.name) {
339
+ for (const part of mw.name.split("|")) {
340
+ const eventPart = part.includes(":") ? part.split(":")[0] : part;
341
+ if (eventPart) events.add(eventPart);
342
+ }
343
+ }
344
+ }
345
+ return events;
346
+ };
347
+ }
348
+ {
349
+ const originalExtend = Composer.prototype.extend;
350
+ Composer.prototype.extend = function(other) {
351
+ const result = originalExtend.call(this, other);
352
+ if (other["~"]?.commandsMeta) {
353
+ if (!this["~"].commandsMeta) {
354
+ this["~"].commandsMeta = /* @__PURE__ */ new Map();
355
+ }
356
+ for (const [cmd, meta] of other["~"].commandsMeta) {
357
+ this["~"].commandsMeta.set(cmd, meta);
358
+ }
359
+ }
360
+ return result;
361
+ };
362
+ }
185
363
 
186
364
  class Plugin {
187
365
  /**
@@ -341,7 +519,7 @@ class Plugin {
341
519
  * import { Bot } from "gramio";
342
520
  *
343
521
  * const bot = new Bot(process.env.TOKEN!).onStart(
344
- * ({ plugins, info, updatesFrom }) => {
522
+ * ({ plugins, info, updatesFrom, bot }) => {
345
523
  * console.log(`plugin list - ${plugins.join(", ")}`);
346
524
  * console.log(`bot username is @${info.username}`);
347
525
  * console.log(`updates from ${updatesFrom}`);
@@ -365,7 +543,7 @@ class Plugin {
365
543
  * import { Bot } from "gramio";
366
544
  *
367
545
  * const bot = new Bot(process.env.TOKEN!).onStop(
368
- * ({ plugins, info, updatesFrom }) => {
546
+ * ({ plugins, info, bot }) => {
369
547
  * console.log(`plugin list - ${plugins.join(", ")}`);
370
548
  * console.log(`bot username is @${info.username}`);
371
549
  * }
@@ -794,7 +972,7 @@ class Bot {
794
972
  * import { Bot } from "gramio";
795
973
  *
796
974
  * const bot = new Bot(process.env.TOKEN!).onStart(
797
- * ({ plugins, info, updatesFrom }) => {
975
+ * ({ plugins, info, updatesFrom, bot }) => {
798
976
  * console.log(`plugin list - ${plugins.join(", ")}`);
799
977
  * console.log(`bot username is @${info.username}`);
800
978
  * console.log(`updates from ${updatesFrom}`);
@@ -818,7 +996,7 @@ class Bot {
818
996
  * import { Bot } from "gramio";
819
997
  *
820
998
  * const bot = new Bot(process.env.TOKEN!).onStop(
821
- * ({ plugins, info, updatesFrom }) => {
999
+ * ({ plugins, info, bot }) => {
822
1000
  * console.log(`plugin list - ${plugins.join(", ")}`);
823
1001
  * console.log(`bot username is @${info.username}`);
824
1002
  * }
@@ -1085,18 +1263,8 @@ class Bot {
1085
1263
  this.updates.composer.hears(trigger, handler, options);
1086
1264
  return this;
1087
1265
  }
1088
- /**
1089
- * Register handler to `message` and `business_message` event when entities contains a command
1090
- *
1091
- * @example
1092
- * ```ts
1093
- * new Bot().command("start", async (context) => {
1094
- * return context.send(`You message is /start ${context.args}`);
1095
- * });
1096
- * ```
1097
- */
1098
- command(command, handler, options) {
1099
- this.updates.composer.command(command, handler, options);
1266
+ command(command, handlerOrMeta, handlerOrOptions, macroOptions) {
1267
+ this.updates.composer.command(command, handlerOrMeta, handlerOrOptions, macroOptions);
1100
1268
  return this;
1101
1269
  }
1102
1270
  /**
@@ -1121,6 +1289,96 @@ class Bot {
1121
1289
  group(grouped) {
1122
1290
  return grouped(this);
1123
1291
  }
1292
+ /**
1293
+ * Sync registered command metadata with the Telegram API.
1294
+ *
1295
+ * Groups commands by `{scope, language_code}` and calls `setMyCommands` for each group.
1296
+ * When a `storage` is provided, hashes each payload and skips unchanged groups.
1297
+ *
1298
+ * @example
1299
+ * ```ts
1300
+ * bot.onStart(() => bot.syncCommands());
1301
+ * ```
1302
+ */
1303
+ async syncCommands(options) {
1304
+ const commandsMeta = this.updates.composer["~"].commandsMeta ?? /* @__PURE__ */ new Map();
1305
+ if (commandsMeta.size === 0) return;
1306
+ const storage = options?.storage;
1307
+ const botId = this.info?.id;
1308
+ const excludeSet = options?.exclude ? new Set(options.exclude) : void 0;
1309
+ const groups = /* @__PURE__ */ new Map();
1310
+ const getOrCreateGroup = (scope, langCode) => {
1311
+ const key = `${JSON.stringify(scope ?? { type: "default" })}:${langCode}`;
1312
+ let group = groups.get(key);
1313
+ if (!group) {
1314
+ group = { scope, language_code: langCode || void 0, commands: [] };
1315
+ groups.set(key, group);
1316
+ }
1317
+ return group;
1318
+ };
1319
+ for (const [name, meta] of commandsMeta) {
1320
+ if (meta.hide) continue;
1321
+ if (excludeSet?.has(name)) continue;
1322
+ const scopes = meta.scopes ? meta.scopes.map(
1323
+ (s) => typeof s === "string" ? { type: s } : s
1324
+ ) : [void 0];
1325
+ for (const scope of scopes) {
1326
+ getOrCreateGroup(scope, "").commands.push({
1327
+ command: name,
1328
+ description: meta.description
1329
+ });
1330
+ if (meta.locales) {
1331
+ for (const [lang, desc] of Object.entries(meta.locales)) {
1332
+ getOrCreateGroup(scope, lang).commands.push({
1333
+ command: name,
1334
+ description: desc
1335
+ });
1336
+ }
1337
+ }
1338
+ }
1339
+ }
1340
+ for (const [key, group] of groups) {
1341
+ if (group.commands.length > 100) {
1342
+ throw new Error(
1343
+ `Too many commands (${group.commands.length}) for scope ${key}. Telegram allows max 100 per scope+language.`
1344
+ );
1345
+ }
1346
+ }
1347
+ for (const [, group] of groups) {
1348
+ const payload = {
1349
+ commands: group.commands,
1350
+ scope: group.scope,
1351
+ language_code: group.language_code
1352
+ };
1353
+ if (storage) {
1354
+ const hash = utils.simpleHash(JSON.stringify(payload));
1355
+ const storageKey = `gramio:commands:${botId ?? "unknown"}:${JSON.stringify(group.scope ?? { type: "default" })}:${group.language_code ?? ""}`;
1356
+ const stored = await storage.get(storageKey);
1357
+ if (stored === hash) continue;
1358
+ await this.api.setMyCommands(payload);
1359
+ await storage.set(storageKey, hash);
1360
+ } else {
1361
+ await this.api.setMyCommands(payload);
1362
+ }
1363
+ }
1364
+ if (options?.cleanUnusedScopes) {
1365
+ const declaredScopes = /* @__PURE__ */ new Set();
1366
+ for (const [, group] of groups) {
1367
+ declaredScopes.add(JSON.stringify(group.scope ?? { type: "default" }));
1368
+ }
1369
+ const allScopes = [
1370
+ { type: "default" },
1371
+ { type: "all_private_chats" },
1372
+ { type: "all_group_chats" },
1373
+ { type: "all_chat_administrators" }
1374
+ ];
1375
+ for (const scope of allScopes) {
1376
+ if (!declaredScopes.has(JSON.stringify(scope))) {
1377
+ await this.api.deleteMyCommands({ scope });
1378
+ }
1379
+ }
1380
+ }
1381
+ }
1124
1382
  /**
1125
1383
  * Init bot. Call it manually only if you doesn't use {@link Bot.start}
1126
1384
  */
@@ -1159,10 +1417,21 @@ class Bot {
1159
1417
  webhook,
1160
1418
  longPolling,
1161
1419
  dropPendingUpdates,
1162
- allowedUpdates,
1420
+ allowedUpdates: allowedUpdatesRaw,
1163
1421
  deleteWebhook: deleteWebhookRaw
1164
1422
  } = {}) {
1165
1423
  await this.init();
1424
+ let allowedUpdates;
1425
+ if (allowedUpdatesRaw === "strict") {
1426
+ allowedUpdates = buildAllowedUpdates(this);
1427
+ } else if (allowedUpdatesRaw === void 0) {
1428
+ const optIn = detectOptInUpdates(
1429
+ this.updates.composer.registeredEvents()
1430
+ );
1431
+ allowedUpdates = optIn.length > 0 ? AllowedUpdatesFilter.default.add(...optIn) : void 0;
1432
+ } else {
1433
+ allowedUpdates = allowedUpdatesRaw;
1434
+ }
1166
1435
  const deleteWebhook = deleteWebhookRaw ?? "on-conflict-with-polling";
1167
1436
  if (!webhook) {
1168
1437
  if (deleteWebhook === true)
@@ -1185,7 +1454,8 @@ class Bot {
1185
1454
  plugins: this.dependencies,
1186
1455
  // biome-ignore lint/style/noNonNullAssertion: bot.init() guarantees this.info
1187
1456
  info: this.info,
1188
- updatesFrom: "long-polling"
1457
+ updatesFrom: "long-polling",
1458
+ bot: this
1189
1459
  });
1190
1460
  return this.info;
1191
1461
  }
@@ -1203,7 +1473,8 @@ class Bot {
1203
1473
  plugins: this.dependencies,
1204
1474
  // biome-ignore lint/style/noNonNullAssertion: bot.init() guarantees this.info
1205
1475
  info: this.info,
1206
- updatesFrom: "webhook"
1476
+ updatesFrom: "webhook",
1477
+ bot: this
1207
1478
  });
1208
1479
  return this.info;
1209
1480
  }
@@ -1220,7 +1491,8 @@ class Bot {
1220
1491
  await this.runImmutableHooks("onStop", {
1221
1492
  plugins: this.dependencies,
1222
1493
  // biome-ignore lint/style/noNonNullAssertion: bot.init() guarantees this.info
1223
- info: this.info
1494
+ info: this.info,
1495
+ bot: this
1224
1496
  });
1225
1497
  }
1226
1498
  }
@@ -1535,11 +1807,16 @@ Object.defineProperty(exports, "stop", {
1535
1807
  enumerable: true,
1536
1808
  get: function () { return composer.stop; }
1537
1809
  });
1810
+ exports.AllowedUpdatesFilter = AllowedUpdatesFilter;
1538
1811
  exports.Bot = Bot;
1539
1812
  exports.Composer = Composer;
1813
+ exports.OPT_IN_TYPES = OPT_IN_TYPES;
1540
1814
  exports.Plugin = Plugin;
1541
1815
  exports.Updates = Updates;
1816
+ exports.buildAllowedUpdates = buildAllowedUpdates;
1817
+ exports.detectOptInUpdates = detectOptInUpdates;
1542
1818
  exports.filters = filters;
1819
+ exports.mapEventToAllowedUpdates = mapEventToAllowedUpdates;
1543
1820
  exports.webhookHandler = webhookHandler;
1544
1821
  Object.keys(callbackData).forEach(function (k) {
1545
1822
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {