@grammyjs/commands 0.9.0 → 0.11.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/README.md CHANGED
@@ -16,22 +16,22 @@ The main functionality of this plugin is to define your commands, localize them,
16
16
 
17
17
  ```typescript
18
18
  import { Bot } from "grammy";
19
- import { Commands } from "@grammyjs/commands";
19
+ import { CommandGroup } from "@grammyjs/commands";
20
20
 
21
21
  const bot = new Bot("<telegram token>");
22
22
 
23
- const myCommands = new Commands();
23
+ const myCommands = new CommandGroup();
24
24
 
25
25
  myCommands.command("start", "Initializes bot configuration")
26
- .localize("pt", "start", "Inicializa as configurações do bot")
27
- .addToScope(
28
- { type: "all_private_chats" },
29
- (ctx) => ctx.reply(`Hello, ${ctx.chat.first_name}!`),
30
- )
31
- .addToScope(
32
- { type: "all_group_chats" },
33
- (ctx) => ctx.reply(`Hello, members of ${ctx.chat.title}!`),
34
- );
26
+ .localize("pt", "start", "Inicializa as configurações do bot")
27
+ .addToScope(
28
+ { type: "all_private_chats" },
29
+ (ctx) => ctx.reply(`Hello, ${ctx.chat.first_name}!`),
30
+ )
31
+ .addToScope(
32
+ { type: "all_group_chats" },
33
+ (ctx) => ctx.reply(`Hello, members of ${ctx.chat.title}!`),
34
+ );
35
35
 
36
36
  // Calls `setMyCommands`
37
37
  await myCommands.setCommands(bot);
@@ -52,7 +52,7 @@ commands flavor and the plugin itself, like so:
52
52
 
53
53
  ```typescript
54
54
  import { Bot, Context } from "grammy";
55
- import { Commands, commands, CommandsFlavor } from "@grammyjs/commands";
55
+ import { CommandGroup, commands, CommandsFlavor } from "@grammyjs/commands";
56
56
 
57
57
  type BotContext = CommandsFlavor;
58
58
 
@@ -60,14 +60,14 @@ const bot = new Bot<BotContext>("<telegram_token>");
60
60
  bot.use(commands());
61
61
 
62
62
  bot.on("message", async (ctx) => {
63
- const cmds = new Commands();
63
+ const cmds = new CommandGroup();
64
64
 
65
- cmds.command("start", "Initializes bot configuration")
66
- .localize("pt", "start", "Inicializa as configurações do bot");
65
+ cmds.command("start", "Initializes bot configuration")
66
+ .localize("pt", "start", "Inicializa as configurações do bot");
67
67
 
68
- await ctx.setMyCommands(cmds);
68
+ await ctx.setMyCommands(cmds);
69
69
 
70
- return ctx.reply("Commands set!");
70
+ return ctx.reply("Commands set!");
71
71
  });
72
72
 
73
73
  bot.start();
@@ -1,12 +1,14 @@
1
- import { Command } from "./command.js";
1
+ import { Command, CommandsFlavor } from "./mod.js";
2
2
  import { Api, BotCommand, BotCommandScope, CommandContext, Context, type LanguageCode, Middleware } from "./deps.node.js";
3
3
  import type { CommandElementals, CommandOptions } from "./types.js";
4
- import { type MaybeArray } from "./utils.js";
4
+ import { type MaybeArray } from "./utils/array.js";
5
+ import { SetBotCommandsOptions } from "./utils/set-bot-commands.js";
6
+ import { JaroWinklerOptions } from "./utils/jaro-winkler.js";
5
7
  /**
6
8
  * Interface for grouping {@link BotCommand}s that might (or not)
7
9
  * be related to each other by scope and/or language.
8
10
  */
9
- export type SetMyCommandsParams = {
11
+ export interface SetMyCommandsParams {
10
12
  /** If defined: scope on which the commands will take effect */
11
13
  scope?: BotCommandScope;
12
14
  /** If defined: Language on which the commands will take effect.
@@ -15,21 +17,33 @@ export type SetMyCommandsParams = {
15
17
  language_code?: LanguageCode;
16
18
  /** Commands that can be each one passed to a SetMyCommands Call */
17
19
  commands: BotCommand[];
18
- };
20
+ }
21
+ /**
22
+ * Interface to represent uncompliance of a command
23
+ * with the Bot API
24
+ */
25
+ export interface UncompliantCommand {
26
+ /** Name of the uncompliant command */
27
+ name: string;
28
+ /** Reason why the command was considered uncompliant */
29
+ reasons: string[];
30
+ /** Language in which the command is uncompliant */
31
+ language: LanguageCode | "default";
32
+ }
19
33
  /**
20
34
  * Central class that manages all registered commands.
21
35
  * This is the starting point for the plugin, and this is what you should pass to `bot.use` so your commands get properly registered.
22
36
  *
23
37
  * @example
24
38
  * ```typescript
25
- * const myCommands = new Commands()
39
+ * const myCommands = new CommandGroup()
26
40
  * commands.command("start", "start the bot configuration", (ctx) => ctx.reply("Hello there!"))
27
41
  *
28
42
  * // Registers the commands with the bot instance.
29
43
  * bot.use(myCommands)
30
44
  * ```
31
45
  */
32
- export declare class Commands<C extends Context> {
46
+ export declare class CommandGroup<C extends Context> {
33
47
  private _languages;
34
48
  private _scopes;
35
49
  private _commands;
@@ -56,18 +70,31 @@ export declare class Commands<C extends Context> {
56
70
  * @returns An instance of the `Command` class
57
71
  */
58
72
  command(name: string | RegExp, description: string, options?: Partial<CommandOptions>): Command<C>;
73
+ /**
74
+ * Registers a Command that was created by it's own.
75
+ *
76
+ * @param command the command or list of commands being added to the group
77
+ */
78
+ add(command: Command<C> | Command<C>[]): this;
59
79
  /**
60
80
  * Serializes the commands into multiple objects that can each be passed to a `setMyCommands` call.
81
+ *
61
82
  * @returns One item for each combination of command + scope + language
62
83
  */
63
- toArgs(): SetMyCommandsParams[];
84
+ toArgs(): {
85
+ scopes: SetMyCommandsParams[];
86
+ uncompliantCommands: UncompliantCommand[];
87
+ };
64
88
  /**
65
89
  * Serializes the commands of a single scope into objects that can each be passed to a `setMyCommands` call.
66
90
  *
67
91
  * @param scope Selected scope to be serialized
68
92
  * @returns One item per command per language
69
93
  */
70
- toSingleScopeArgs(scope: BotCommandScope): SetMyCommandsParams[];
94
+ toSingleScopeArgs(scope: BotCommandScope): {
95
+ commandParams: SetMyCommandsParams[];
96
+ uncompliantCommands: UncompliantCommand[];
97
+ };
71
98
  /**
72
99
  * Registers all commands to be displayed by clients according to their scopes and languages
73
100
  * Calls `setMyCommands` for each language of each scope of each command.
@@ -81,7 +108,7 @@ export declare class Commands<C extends Context> {
81
108
  */
82
109
  setCommands({ api }: {
83
110
  api: Api;
84
- }): Promise<void>;
111
+ }, options?: Partial<SetBotCommandsOptions>): Promise<void>;
85
112
  /**
86
113
  * Serialize all register commands into it's name, prefix and language
87
114
  *
@@ -99,4 +126,17 @@ export declare class Commands<C extends Context> {
99
126
  */
100
127
  toString(): string;
101
128
  middleware(): import("grammy").MiddlewareFn<C>;
129
+ /**
130
+ * @returns all {@link Command}s contained in the instance
131
+ */
132
+ get commands(): Command<C>[];
133
+ /**
134
+ * @returns all prefixes registered in this instance
135
+ */
136
+ get prefixes(): string[];
102
137
  }
138
+ type HaveCommandLike<C extends Context = Context, CF extends CommandsFlavor<C> = CommandsFlavor<C>> = C & CF & {
139
+ commandSuggestion: string | null;
140
+ };
141
+ export declare function commandNotFound<CF extends CommandsFlavor<C>, C extends Context = Context>(commands: CommandGroup<C> | CommandGroup<C>[], opts?: Omit<Partial<JaroWinklerOptions>, "language">): (ctx: C) => ctx is HaveCommandLike<C, CF>;
142
+ export {};
@@ -1,37 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Commands = void 0;
4
- const command_js_1 = require("./command.js");
3
+ exports.commandNotFound = exports.CommandGroup = void 0;
4
+ const mod_js_1 = require("./mod.js");
5
5
  const deps_node_js_1 = require("./deps.node.js");
6
- const isMiddleware = (obj) => {
7
- if (!obj)
8
- return false;
9
- if (Array.isArray(obj))
10
- return obj.every(isMiddleware);
11
- const objType = typeof obj;
12
- switch (objType) {
13
- case "function":
14
- return true;
15
- case "object":
16
- return Object.keys(obj).includes("middleware");
17
- }
18
- return false;
19
- };
6
+ const array_js_1 = require("./utils/array.js");
7
+ const set_bot_commands_js_1 = require("./utils/set-bot-commands.js");
8
+ const checks_js_1 = require("./utils/checks.js");
20
9
  /**
21
10
  * Central class that manages all registered commands.
22
11
  * This is the starting point for the plugin, and this is what you should pass to `bot.use` so your commands get properly registered.
23
12
  *
24
13
  * @example
25
14
  * ```typescript
26
- * const myCommands = new Commands()
15
+ * const myCommands = new CommandGroup()
27
16
  * commands.command("start", "start the bot configuration", (ctx) => ctx.reply("Hello there!"))
28
17
  *
29
18
  * // Registers the commands with the bot instance.
30
19
  * bot.use(myCommands)
31
20
  * ```
32
21
  */
33
- class Commands {
22
+ class CommandGroup {
34
23
  constructor(options = {}) {
24
+ var _a;
35
25
  this._languages = new Set();
36
26
  this._scopes = new Map();
37
27
  this._commands = [];
@@ -39,6 +29,9 @@ class Commands {
39
29
  this._cachedComposerInvalidated = false;
40
30
  this._commandOptions = {};
41
31
  this._commandOptions = options;
32
+ if (((_a = this._commandOptions.prefix) === null || _a === void 0 ? void 0 : _a.trim()) === "") {
33
+ this._commandOptions.prefix = "/";
34
+ }
42
35
  }
43
36
  _addCommandToScope(scope, command) {
44
37
  var _a;
@@ -58,41 +51,63 @@ class Commands {
58
51
  });
59
52
  }
60
53
  command(name, description, handlerOrOptions, _options) {
61
- var _a;
62
- const handler = isMiddleware(handlerOrOptions)
54
+ const handler = (0, checks_js_1.isMiddleware)(handlerOrOptions)
63
55
  ? handlerOrOptions
64
56
  : undefined;
65
- const options = handler
66
- ? _options !== null && _options !== void 0 ? _options : this._commandOptions
67
- : (_a = handlerOrOptions) !== null && _a !== void 0 ? _a : this._commandOptions;
68
- const command = new command_js_1.Command(name, description, options);
69
- if (handler)
70
- command.addToScope({ type: "default" }, handler);
57
+ const options = !handler && (0, checks_js_1.isCommandOptions)(handlerOrOptions)
58
+ ? { ...this._commandOptions, ...handlerOrOptions }
59
+ : { ...this._commandOptions, ..._options };
60
+ const command = new mod_js_1.Command(name, description, handler, options);
71
61
  this._commands.push(command);
72
62
  this._cachedComposerInvalidated = true;
73
63
  return command;
74
64
  }
65
+ /**
66
+ * Registers a Command that was created by it's own.
67
+ *
68
+ * @param command the command or list of commands being added to the group
69
+ */
70
+ add(command) {
71
+ this._commands.push(...(0, array_js_1.ensureArray)(command));
72
+ this._cachedComposerInvalidated = true;
73
+ return this;
74
+ }
75
75
  /**
76
76
  * Serializes the commands into multiple objects that can each be passed to a `setMyCommands` call.
77
+ *
77
78
  * @returns One item for each combination of command + scope + language
78
79
  */
79
80
  toArgs() {
80
81
  this._populateMetadata();
81
- const params = [];
82
+ const scopes = [];
83
+ const uncompliantCommands = [];
82
84
  for (const [scope, commands] of this._scopes.entries()) {
83
85
  for (const language of this._languages) {
84
- params.push({
85
- scope: JSON.parse(scope),
86
- language_code: language === "default"
87
- ? undefined
88
- : language,
89
- commands: commands
90
- .filter((command) => typeof command.name === "string")
91
- .map((command) => command.toObject(language)),
86
+ const compliantScopedCommands = [];
87
+ commands.forEach((command) => {
88
+ const [isApiCompliant, ...reasons] = command.isApiCompliant(language);
89
+ if (isApiCompliant) {
90
+ return compliantScopedCommands.push(command);
91
+ }
92
+ uncompliantCommands.push({
93
+ name: command.stringName,
94
+ reasons: reasons,
95
+ language,
96
+ });
92
97
  });
98
+ if (compliantScopedCommands.length) {
99
+ scopes.push({
100
+ scope: JSON.parse(scope),
101
+ language_code: language === "default" ? undefined : language,
102
+ commands: compliantScopedCommands.map((command) => command.toObject(language)),
103
+ });
104
+ }
93
105
  }
94
106
  }
95
- return params.filter((params) => params.commands.length > 0);
107
+ return {
108
+ scopes,
109
+ uncompliantCommands,
110
+ };
96
111
  }
97
112
  /**
98
113
  * Serializes the commands of a single scope into objects that can each be passed to a `setMyCommands` call.
@@ -102,18 +117,29 @@ class Commands {
102
117
  */
103
118
  toSingleScopeArgs(scope) {
104
119
  this._populateMetadata();
105
- const params = [];
120
+ const commandParams = [];
121
+ const uncompliantCommands = [];
106
122
  for (const language of this._languages) {
107
- params.push({
123
+ const compliantCommands = [];
124
+ this._commands.forEach((command) => {
125
+ const [isApiCompliant, ...reasons] = command.isApiCompliant(language);
126
+ if (!isApiCompliant) {
127
+ return uncompliantCommands.push({
128
+ name: command.stringName,
129
+ reasons: reasons,
130
+ language,
131
+ });
132
+ }
133
+ if (command.scopes.length)
134
+ compliantCommands.push(command);
135
+ });
136
+ commandParams.push({
108
137
  scope,
109
138
  language_code: language === "default" ? undefined : language,
110
- commands: this._commands
111
- .filter((command) => command.scopes.length)
112
- .filter((command) => typeof command.name === "string")
113
- .map((command) => command.toObject(language)),
139
+ commands: compliantCommands.map((command) => command.toObject(language)),
114
140
  });
115
141
  }
116
- return params;
142
+ return { commandParams, uncompliantCommands };
117
143
  }
118
144
  /**
119
145
  * Registers all commands to be displayed by clients according to their scopes and languages
@@ -126,8 +152,9 @@ class Commands {
126
152
  *
127
153
  * @param Instance of `bot` or { api: bot.api }
128
154
  */
129
- async setCommands({ api }) {
130
- await Promise.all(this.toArgs().map((args) => api.raw.setMyCommands(args)));
155
+ async setCommands({ api }, options) {
156
+ const { scopes, uncompliantCommands } = this.toArgs();
157
+ await (0, set_bot_commands_js_1.setBotCommands)(api, scopes, uncompliantCommands, options);
131
158
  }
132
159
  /**
133
160
  * Serialize all register commands into it's name, prefix and language
@@ -179,6 +206,20 @@ class Commands {
179
206
  }
180
207
  return this._cachedComposer.middleware();
181
208
  }
209
+ /**
210
+ * @returns all {@link Command}s contained in the instance
211
+ */
212
+ get commands() {
213
+ return this._commands;
214
+ }
215
+ /**
216
+ * @returns all prefixes registered in this instance
217
+ */
218
+ get prefixes() {
219
+ return [
220
+ ...new Set(this._commands.flatMap((command) => command.prefix)),
221
+ ];
222
+ }
182
223
  /**
183
224
  * Replaces the `toString` method on Deno
184
225
  *
@@ -196,4 +237,30 @@ class Commands {
196
237
  return this.toString();
197
238
  }
198
239
  }
199
- exports.Commands = Commands;
240
+ exports.CommandGroup = CommandGroup;
241
+ function commandNotFound(commands, opts = {}) {
242
+ return function (ctx) {
243
+ if (containsCommands(ctx, commands)) {
244
+ ctx
245
+ .commandSuggestion = ctx
246
+ .getNearestCommand(commands, opts);
247
+ return true;
248
+ }
249
+ return false;
250
+ };
251
+ }
252
+ exports.commandNotFound = commandNotFound;
253
+ function containsCommands(ctx, commands) {
254
+ let allPrefixes = [
255
+ ...new Set((0, array_js_1.ensureArray)(commands).flatMap((cmds) => cmds.prefixes)),
256
+ ];
257
+ if (allPrefixes.length < 1) {
258
+ allPrefixes = ["/"];
259
+ }
260
+ for (const prefix of allPrefixes) {
261
+ const regex = (0, array_js_1.getCommandsRegex)(prefix);
262
+ if (ctx.hasText(regex))
263
+ return true;
264
+ }
265
+ return false;
266
+ }
package/out/command.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { type BotCommand, type BotCommandScope, type BotCommandScopeAllChatAdministrators, type BotCommandScopeAllGroupChats, type BotCommandScopeAllPrivateChats, type ChatTypeMiddleware, type Context, type LanguageCode, type Middleware, type MiddlewareObj } from "./deps.node.js";
1
+ import { type BotCommand, type BotCommandScope, type BotCommandScopeAllChatAdministrators, type BotCommandScopeAllGroupChats, type BotCommandScopeAllPrivateChats, type ChatTypeMiddleware, CommandContext, type Context, type LanguageCode, type Middleware, type MiddlewareObj } from "./deps.node.js";
2
2
  import type { CommandOptions } from "./types.js";
3
- import { type MaybeArray } from "./utils.js";
3
+ import { type MaybeArray } from "./utils/array.js";
4
4
  type BotCommandGroupsScope = BotCommandScopeAllGroupChats | BotCommandScopeAllChatAdministrators;
5
- export declare const matchesPattern: (value: string, pattern: string | RegExp, ignoreCase?: boolean) => boolean;
6
5
  /**
7
6
  * Class that represents a single command and allows you to configure it.
8
7
  */
@@ -10,17 +9,62 @@ export declare class Command<C extends Context = Context> implements MiddlewareO
10
9
  private _scopes;
11
10
  private _languages;
12
11
  private _composer;
12
+ private _defaultScopeComposer;
13
13
  private _options;
14
14
  /**
15
- * Constructor for the `Command` class.
16
- * Do not call this directly. Instead, use the `command` method from the `Commands` class
15
+ * Initialize a new command with a default handler.
16
+ *
17
+ * [!IMPORTANT] This class by its own does nothing. It needs to be imported into a `CommandGroup`
18
+ * via the `add` method.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const sayHi = new Command("hi","say hi", (ctx) => ctx.reply("hi"))
23
+ * const myCmds = new CommandGroup().add(sayHi)
24
+ * ```
17
25
  *
18
26
  * @param name Default command name
19
27
  * @param description Default command description
20
- * @param options Options object that should apply to this command only
21
- * @access package
28
+ * @param handler Default command handler
29
+ * @param options Extra options that should apply only to this command
30
+ * @returns An instance of the `Command` class
31
+ */
32
+ constructor(name: string | RegExp, description: string, handler: MaybeArray<Middleware<CommandContext<C>>>, options?: Partial<CommandOptions>);
33
+ /**
34
+ * Initialize a new command with no handlers.
35
+ *
36
+ * [!IMPORTANT] This class by its own does nothing. It needs to be imported into a `CommandGroup`
37
+ * via the `add` method
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const sayHi = new Command("hi","say hi", (ctx) => ctx.reply("hi") )
42
+ * const myCmds = new CommandGroup().add(sayHi)
43
+ * ```
44
+ *
45
+ * @param name Default command name
46
+ * @param description Default command description
47
+ * @param options Extra options that should apply only to this command
48
+ * @returns An instance of the `Command` class
22
49
  */
23
50
  constructor(name: string | RegExp, description: string, options?: Partial<CommandOptions>);
51
+ constructor(name: string | RegExp, description: string, handlerOrOptions?: MaybeArray<Middleware<CommandContext<C>>> | Partial<CommandOptions>, options?: Partial<CommandOptions>);
52
+ /**
53
+ * Whether the command has a custom prefix
54
+ */
55
+ get hasCustomPrefix(): boolean | "";
56
+ /**
57
+ * Gets the command name as string
58
+ */
59
+ get stringName(): string;
60
+ /**
61
+ * Whether the command can be passed to a `setMyCommands` API call
62
+ * and, if not, the reason.
63
+ */
64
+ isApiCompliant(language?: LanguageCode | "default"): [result: true] | [
65
+ result: false,
66
+ ...reasons: string[]
67
+ ];
24
68
  /**
25
69
  * Get registered scopes for this command
26
70
  */
@@ -54,7 +98,7 @@ export declare class Command<C extends Context = Context> implements MiddlewareO
54
98
  *
55
99
  * @example
56
100
  * ```ts
57
- * const myCommands = new Commands();
101
+ * const myCommands = new CommandGroup();
58
102
  * myCommands.command("start", "Initializes bot configuration")
59
103
  * .addToScope(
60
104
  * { type: "all_private_chats" },
@@ -70,9 +114,9 @@ export declare class Command<C extends Context = Context> implements MiddlewareO
70
114
  * @param middleware The handler for this command on the specified scope
71
115
  * @param options Additional options that should apply only to this scope
72
116
  */
73
- addToScope(scope: BotCommandGroupsScope, middleware: MaybeArray<ChatTypeMiddleware<C, "group" | "supergroup">>, options?: Partial<CommandOptions>): this;
74
- addToScope(scope: BotCommandScopeAllPrivateChats, middleware: MaybeArray<ChatTypeMiddleware<C, "private">>, options?: Partial<CommandOptions>): this;
75
- addToScope(scope: BotCommandScope, middleware: MaybeArray<Middleware<C>>, options?: Partial<CommandOptions>): this;
117
+ addToScope(scope: BotCommandGroupsScope, middleware?: MaybeArray<ChatTypeMiddleware<C, "group" | "supergroup">>, options?: Partial<CommandOptions>): this;
118
+ addToScope(scope: BotCommandScopeAllPrivateChats, middleware?: MaybeArray<ChatTypeMiddleware<C, "private">>, options?: Partial<CommandOptions>): this;
119
+ addToScope(scope: BotCommandScope, middleware?: MaybeArray<Middleware<C>>, options?: Partial<CommandOptions>): this;
76
120
  /**
77
121
  * Creates a matcher for the given command that can be used in filtering operations
78
122
  *