@grammyjs/commands 0.3.1 → 0.5.1

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
@@ -1,18 +1,18 @@
1
1
  ## grammY Commands Plugin
2
2
 
3
- This plugin provides a convenient way to define and manage commands for your grammY bot. It simplifies the process of setting up commands with scopes and localization.
3
+ This plugin provides a convenient way to define and manage commands for your grammY bot. It simplifies the process of
4
+ setting up commands with scopes and localization.
4
5
 
5
6
  ## Installation
6
7
 
7
-
8
8
  ```sh
9
9
  npm i @grammyjs/commands
10
10
  ```
11
11
 
12
12
  ## Usage
13
13
 
14
- The main functionality of this plugin is to define your commands, localize them, and give them handlers for each [scope](https://core.telegram.org/bots/api#botcommandscope), like so:
15
-
14
+ The main functionality of this plugin is to define your commands, localize them, and give them handlers for each
15
+ [scope](https://core.telegram.org/bots/api#botcommandscope), like so:
16
16
 
17
17
  ```typescript
18
18
  import { Bot } from "grammy";
@@ -23,9 +23,15 @@ const bot = new Bot("<telegram token>");
23
23
  const myCommands = new Commands();
24
24
 
25
25
  myCommands.command("start", "Initializes bot configuration")
26
- .localize("pt", "start", "Inicializa as configurações do bot")
27
- .addToScope({ type: "all_private_chats" }, (ctx) => ctx.reply(`Hello, ${ctx.chat.first_name}!`))
28
- .addToScope({ type: "all_group_chats" }, (ctx) => ctx.reply(`Hello, members of ${ctx.chat.title}!`));
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
+ );
29
35
 
30
36
  // Calls `setMyCommands`
31
37
  await myCommands.setCommands(bot);
@@ -36,17 +42,17 @@ bot.use(myCommands);
36
42
  bot.start();
37
43
  ```
38
44
 
39
- It is very important that you call `bot.use` with your instance of the `Commands` class.
40
- Otherwise, the command handlers will not be registered, and your bot will not respond to those commands.
45
+ It is very important that you call `bot.use` with your instance of the `Commands` class. Otherwise, the command handlers
46
+ will not be registered, and your bot will not respond to those commands.
41
47
 
42
48
  ### Context shortcuts
43
49
 
44
- This plugin provides a shortcut for setting the commands for the current chat.
45
- To use it, you need to install the commands flavor and the plugin itself, like so:
50
+ This plugin provides a shortcut for setting the commands for the current chat. To use it, you need to install the
51
+ commands flavor and the plugin itself, like so:
46
52
 
47
53
  ```typescript
48
54
  import { Bot, Context } from "grammy";
49
- import { Commands, CommandsFlavor, commands } from "@grammyjs/commands";
55
+ import { Commands, commands, CommandsFlavor } from "@grammyjs/commands";
50
56
 
51
57
  type BotContext = CommandsFlavor;
52
58
 
@@ -54,15 +60,15 @@ const bot = new Bot<BotContext>("<telegram_token>");
54
60
  bot.use(commands());
55
61
 
56
62
  bot.on("message", async (ctx) => {
57
- const cmds = new Commands();
63
+ const cmds = new Commands();
58
64
 
59
- cmds.command("start", "Initializes bot configuration")
60
- .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");
61
67
 
62
- await ctx.setMyCommands(cmds);
68
+ await ctx.setMyCommands(cmds);
63
69
 
64
- return ctx.reply("Commands set!");
70
+ return ctx.reply("Commands set!");
65
71
  });
66
72
 
67
- bot.start()
68
- ```
73
+ bot.start();
74
+ ```
package/out/command.d.ts CHANGED
@@ -1,25 +1,125 @@
1
1
  import { type BotCommand, type BotCommandScope, type BotCommandScopeAllChatAdministrators, type BotCommandScopeAllGroupChats, type BotCommandScopeAllPrivateChats, type ChatTypeMiddleware, type Context, type Middleware, type MiddlewareObj } from "./deps.node.js";
2
+ import { CommandOptions } from "./types.js";
2
3
  export type MaybeArray<T> = T | T[];
3
4
  type BotCommandGroupsScope = BotCommandScopeAllGroupChats | BotCommandScopeAllChatAdministrators;
5
+ export declare const matchesPattern: (value: string, pattern: string | RegExp) => boolean;
6
+ /**
7
+ * Class that represents a single command and allows you to configure it.
8
+ */
4
9
  export declare class Command<C extends Context = Context> implements MiddlewareObj<C> {
5
10
  private _scopes;
6
11
  private _languages;
7
12
  private _composer;
8
- constructor(name: string, description: string);
13
+ private _options;
14
+ /**
15
+ * Constructor for the `Command` class.
16
+ * Do not call this directly. Instead, use the `command` method from the `Commands` class
17
+ *
18
+ * @param name Default command name
19
+ * @param description Default command description
20
+ * @param options Options object that shuold apply to this command only
21
+ * @access package
22
+ */
23
+ constructor(name: string | RegExp, description: string, options?: Partial<CommandOptions>);
24
+ /**
25
+ * Get registered scopes for this command
26
+ */
9
27
  get scopes(): BotCommandScope[];
28
+ /**
29
+ * Get registered languages for this command
30
+ */
10
31
  get languages(): Map<string, {
11
- name: string;
32
+ name: string | RegExp;
12
33
  description: string;
13
34
  }>;
14
- get names(): string[];
15
- get name(): string;
35
+ /**
36
+ * Get registered names for this command
37
+ */
38
+ get names(): (string | RegExp)[];
39
+ /**
40
+ * Get the default name for this command
41
+ */
42
+ get name(): string | RegExp;
43
+ /**
44
+ * Get the default description for this command
45
+ */
16
46
  get description(): string;
17
- addToScope(scope: BotCommandGroupsScope, ...middleware: ChatTypeMiddleware<C, "group" | "supergroup">[]): this;
18
- addToScope(scope: BotCommandScopeAllPrivateChats, ...middleware: ChatTypeMiddleware<C, "private">[]): this;
19
- addToScope(scope: BotCommandScope, ...middleware: Array<Middleware<C>>): this;
20
- localize(languageCode: string, name: string, description: string): this;
21
- getLocalizedName(languageCode: string): string;
47
+ /**
48
+ * Registers the command to a scope to allow it to be handled and used with `setMyCommands`.
49
+ * This will automatically apply filtering middlewares for you, so the handler only runs on the specified scope.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const myCommands = new Commands();
54
+ * myCommands.command("start", "Initializes bot configuration")
55
+ * .addToScope(
56
+ * { type: "all_private_chats" },
57
+ * (ctx) => ctx.reply(`Hello, ${ctx.chat.first_name}!`),
58
+ * )
59
+ * .addToScope(
60
+ * { type: "all_group_chats" },
61
+ * (ctx) => ctx.reply(`Hello, members of ${ctx.chat.title}!`),
62
+ * );
63
+ * ```
64
+ *
65
+ * @param scope Which scope this command should be available on
66
+ * @param middleware The handler for this command on the specified scope
67
+ * @param options Additional options that should apply only to this scope
68
+ */
69
+ addToScope(scope: BotCommandGroupsScope, middleware: MaybeArray<ChatTypeMiddleware<C, "group" | "supergroup">>, options?: Partial<CommandOptions>): this;
70
+ addToScope(scope: BotCommandScopeAllPrivateChats, middleware: MaybeArray<ChatTypeMiddleware<C, "private">>, options?: Partial<CommandOptions>): this;
71
+ addToScope(scope: BotCommandScope, middleware: MaybeArray<Middleware<C>>, options?: Partial<CommandOptions>): this;
72
+ /**
73
+ * Creates a matcher for the given command that can be used in filtering operations
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * bot
78
+ * .filter(
79
+ * Command.hasCommand(/\/delete_(.*)/),
80
+ * (ctx) => ctx.reply(`Deleting ${ctx.message?.text?.split("_")[1]}`)
81
+ * )
82
+ * ```
83
+ *
84
+ * @param command Command name or RegEx
85
+ * @param options Options that should apply to the matching algorithm
86
+ * @returns A predicate that matches the given command
87
+ */
88
+ static hasCommand(command: MaybeArray<string | RegExp>, options: CommandOptions): (ctx: Context) => boolean;
89
+ /**
90
+ * Adds a new translation for the command
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * myCommands
95
+ * .command("start", "Starts the bot configuration")
96
+ * .localize("pt", "iniciar", "Inicia a configuração do bot")
97
+ * ```
98
+ *
99
+ * @param languageCode Language this translation applies to
100
+ * @param name Localized command name
101
+ * @param description Localized command description
102
+ */
103
+ localize(languageCode: string, name: string | RegExp, description: string): this;
104
+ /**
105
+ * Gets the localized command name of an existing translation
106
+ * @param languageCode Language to get the name for
107
+ * @returns Localized command name
108
+ */
109
+ getLocalizedName(languageCode: string): string | RegExp;
110
+ /**
111
+ * Gets the localized command name of an existing translation
112
+ * @param languageCode Language to get the name for
113
+ * @returns Localized command name
114
+ */
22
115
  getLocalizedDescription(languageCode: string): string;
116
+ /**
117
+ * Converts command to an object representation.
118
+ * Useful for JSON serialization.
119
+ *
120
+ * @param languageCode If specified, uses localized versions of the command name and description
121
+ * @returns Object representation of this command
122
+ */
23
123
  toObject(languageCode?: string): BotCommand;
24
124
  middleware(): import("grammy").MiddlewareFn<C>;
25
125
  }
package/out/command.js CHANGED
@@ -1,58 +1,216 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Command = void 0;
3
+ exports.Command = exports.matchesPattern = void 0;
4
4
  const deps_node_js_1 = require("./deps.node.js");
5
- const isAdmin = (ctx) => ctx.getAuthor().then((author) => ["administrator", "creator"].includes(author.status));
5
+ const errors_js_1 = require("./errors.js");
6
+ const ensureArray = (value) => Array.isArray(value) ? value : [value];
7
+ const isAdmin = (ctx) => ctx
8
+ .getAuthor()
9
+ .then((author) => ["administrator", "creator"].includes(author.status));
10
+ const matchesPattern = (value, pattern) => typeof pattern === "string" ? value === pattern : pattern.test(value);
11
+ exports.matchesPattern = matchesPattern;
12
+ /**
13
+ * Class that represents a single command and allows you to configure it.
14
+ */
6
15
  class Command {
7
- constructor(name, description) {
16
+ /**
17
+ * Constructor for the `Command` class.
18
+ * Do not call this directly. Instead, use the `command` method from the `Commands` class
19
+ *
20
+ * @param name Default command name
21
+ * @param description Default command description
22
+ * @param options Options object that shuold apply to this command only
23
+ * @access package
24
+ */
25
+ constructor(name, description, options = {}) {
8
26
  this._scopes = [];
9
27
  this._languages = new Map();
10
28
  this._composer = new deps_node_js_1.Composer();
11
- this._languages.set("default", { name, description });
29
+ this._options = {
30
+ prefix: "/",
31
+ matchOnlyAtStart: true,
32
+ targetedCommands: "optional",
33
+ };
34
+ this._options = { ...this._options, ...options };
35
+ if (this._options.prefix === "")
36
+ this._options.prefix = "/";
37
+ this._languages.set("default", { name: name, description });
12
38
  }
39
+ /**
40
+ * Get registered scopes for this command
41
+ */
13
42
  get scopes() {
14
43
  return this._scopes;
15
44
  }
45
+ /**
46
+ * Get registered languages for this command
47
+ */
16
48
  get languages() {
17
49
  return this._languages;
18
50
  }
51
+ /**
52
+ * Get registered names for this command
53
+ */
19
54
  get names() {
20
55
  return Array.from(this._languages.values()).map(({ name }) => name);
21
56
  }
57
+ /**
58
+ * Get the default name for this command
59
+ */
22
60
  get name() {
23
61
  return this._languages.get("default").name;
24
62
  }
63
+ /**
64
+ * Get the default description for this command
65
+ */
25
66
  get description() {
26
67
  return this._languages.get("default").description;
27
68
  }
28
- addToScope(scope, ...middleware) {
29
- (0, deps_node_js_1.match)(scope)
30
- .with({ type: "default" }, () => this._composer.command(this.names, ...middleware))
31
- .with({ type: "all_chat_administrators" }, () => this._composer.filter(isAdmin).command(this.names, ...middleware))
32
- .with({ type: "all_private_chats" }, () => this._composer.chatType("private").command(this.names, ...middleware))
33
- .with({ type: "all_group_chats" }, () => this._composer.chatType(["group", "supergroup"]).command(this.names, ...middleware))
34
- .with({ type: deps_node_js_1.P.union("chat", "chat_administrators"), chat_id: deps_node_js_1.P.not(deps_node_js_1.P.nullish).select() }, (chatId) => this._composer.filter((ctx) => { var _a; return ((_a = ctx.chat) === null || _a === void 0 ? void 0 : _a.id) === chatId; }).filter(isAdmin).command(this.names, ...middleware))
35
- .with({ type: "chat_member", chat_id: deps_node_js_1.P.not(deps_node_js_1.P.nullish).select("chatId"), user_id: deps_node_js_1.P.not(deps_node_js_1.P.nullish).select("userId") }, ({ chatId, userId }) => this._composer.filter((ctx) => { var _a; return ((_a = ctx.chat) === null || _a === void 0 ? void 0 : _a.id) === chatId; })
36
- .filter((ctx) => { var _a; return ((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id) === userId; })
37
- .command(this.names, ...middleware));
69
+ addToScope(scope, middleware, options = this._options) {
70
+ const middlewareArray = ensureArray(middleware);
71
+ const optionsObject = { ...this._options, ...options };
72
+ switch (scope.type) {
73
+ case "default":
74
+ this._composer
75
+ .filter(Command.hasCommand(this.names, optionsObject))
76
+ .use(...middlewareArray);
77
+ break;
78
+ case "all_chat_administrators":
79
+ this._composer
80
+ .filter(Command.hasCommand(this.names, optionsObject))
81
+ .filter(isAdmin)
82
+ .use(...middlewareArray);
83
+ break;
84
+ case "all_private_chats":
85
+ this._composer
86
+ .filter(Command.hasCommand(this.names, optionsObject))
87
+ .chatType("private")
88
+ .use(...middlewareArray);
89
+ break;
90
+ case "all_group_chats":
91
+ this._composer
92
+ .filter(Command.hasCommand(this.names, optionsObject))
93
+ .chatType(["group", "supergroup"])
94
+ .use(...middlewareArray);
95
+ break;
96
+ case "chat":
97
+ case "chat_administrators":
98
+ if (scope.chat_id) {
99
+ this._composer
100
+ .filter(Command.hasCommand(this.names, optionsObject))
101
+ .filter((ctx) => { var _a; return ((_a = ctx.chat) === null || _a === void 0 ? void 0 : _a.id) === scope.chat_id; })
102
+ .filter(isAdmin)
103
+ .use(...middlewareArray);
104
+ }
105
+ break;
106
+ case "chat_member":
107
+ if (scope.chat_id && scope.user_id) {
108
+ this._composer
109
+ .filter(Command.hasCommand(this.names, optionsObject))
110
+ .filter((ctx) => { var _a; return ((_a = ctx.chat) === null || _a === void 0 ? void 0 : _a.id) === scope.chat_id; })
111
+ .filter((ctx) => { var _a; return ((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id) === scope.user_id; })
112
+ .use(...middlewareArray);
113
+ }
114
+ break;
115
+ default:
116
+ throw new errors_js_1.InvalidScopeError(scope);
117
+ }
38
118
  this._scopes.push(scope);
39
119
  return this;
40
120
  }
121
+ /**
122
+ * Creates a matcher for the given command that can be used in filtering operations
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * bot
127
+ * .filter(
128
+ * Command.hasCommand(/\/delete_(.*)/),
129
+ * (ctx) => ctx.reply(`Deleting ${ctx.message?.text?.split("_")[1]}`)
130
+ * )
131
+ * ```
132
+ *
133
+ * @param command Command name or RegEx
134
+ * @param options Options that should apply to the matching algorithm
135
+ * @returns A predicate that matches the given command
136
+ */
137
+ static hasCommand(command, options) {
138
+ const { matchOnlyAtStart, prefix, targetedCommands } = options;
139
+ return (ctx) => {
140
+ if (!ctx.has(":text"))
141
+ return false;
142
+ if (matchOnlyAtStart && !ctx.msg.text.startsWith(prefix)) {
143
+ return false;
144
+ }
145
+ const commandNames = ensureArray(command);
146
+ const commands = prefix === "/"
147
+ ? ctx.entities("bot_command")
148
+ : ctx.msg.text.split(prefix).map((text) => ({ text }));
149
+ for (const { text } of commands) {
150
+ const [command, username] = text.split("@");
151
+ if (targetedCommands === "ignored" && username)
152
+ continue;
153
+ if (targetedCommands === "required" && !username)
154
+ continue;
155
+ if (username && username !== ctx.me.username)
156
+ continue;
157
+ if (commandNames.some((name) => (0, exports.matchesPattern)(command.replace(prefix, ""), name))) {
158
+ return true;
159
+ }
160
+ }
161
+ return false;
162
+ };
163
+ }
164
+ /**
165
+ * Adds a new translation for the command
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * myCommands
170
+ * .command("start", "Starts the bot configuration")
171
+ * .localize("pt", "iniciar", "Inicia a configuração do bot")
172
+ * ```
173
+ *
174
+ * @param languageCode Language this translation applies to
175
+ * @param name Localized command name
176
+ * @param description Localized command description
177
+ */
41
178
  localize(languageCode, name, description) {
42
- this._languages.set(languageCode, { name, description });
179
+ this._languages.set(languageCode, {
180
+ name: new RegExp(name),
181
+ description,
182
+ });
43
183
  return this;
44
184
  }
185
+ /**
186
+ * Gets the localized command name of an existing translation
187
+ * @param languageCode Language to get the name for
188
+ * @returns Localized command name
189
+ */
45
190
  getLocalizedName(languageCode) {
46
191
  var _a, _b;
47
192
  return (_b = (_a = this._languages.get(languageCode)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : this.name;
48
193
  }
194
+ /**
195
+ * Gets the localized command name of an existing translation
196
+ * @param languageCode Language to get the name for
197
+ * @returns Localized command name
198
+ */
49
199
  getLocalizedDescription(languageCode) {
50
200
  var _a, _b;
51
201
  return (_b = (_a = this._languages.get(languageCode)) === null || _a === void 0 ? void 0 : _a.description) !== null && _b !== void 0 ? _b : this.description;
52
202
  }
203
+ /**
204
+ * Converts command to an object representation.
205
+ * Useful for JSON serialization.
206
+ *
207
+ * @param languageCode If specified, uses localized versions of the command name and description
208
+ * @returns Object representation of this command
209
+ */
53
210
  toObject(languageCode = "default") {
211
+ const localizedName = this.getLocalizedName(languageCode);
54
212
  return {
55
- command: this.getLocalizedName(languageCode),
213
+ command: localizedName instanceof RegExp ? "" : localizedName,
56
214
  description: this.getLocalizedDescription(languageCode),
57
215
  };
58
216
  }
@@ -0,0 +1,75 @@
1
+ import { Command } from "./command.js";
2
+ import { Api, BotCommand, BotCommandScope, Context } from "./deps.node.js";
3
+ import { CommandOptions } from "./types.js";
4
+ type SetMyCommandsParams = {
5
+ /**
6
+ * Scope
7
+ */
8
+ scope?: BotCommandScope;
9
+ language_code?: string;
10
+ commands: BotCommand[];
11
+ };
12
+ /**
13
+ * Central class that manages all registered commands.
14
+ * This is the starting point for the plugin, and this is what you should pass to `bot.use` so your commands get properly registered.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const myCommands = new Commands()
19
+ * commands.command("start", "start the bot configuration", (ctx) => ctx.reply("Hello there!"))
20
+ *
21
+ * // Registers the commands with the bot instance.
22
+ * bot.use(myCommands)
23
+ * ```
24
+ */
25
+ export declare class Commands<C extends Context> {
26
+ private _languages;
27
+ private _scopes;
28
+ private _commands;
29
+ private _composer;
30
+ private _commandOptions;
31
+ constructor(options?: Partial<CommandOptions>);
32
+ private _addCommandToScope;
33
+ private _populateComposer;
34
+ private _populateMetadata;
35
+ /**
36
+ * Registers a new command and returns it.
37
+ * @param name Command name
38
+ * @param description Default command description
39
+ * @param options Extra options that should apply only to this command
40
+ * @returns An instance of the `Command` class
41
+ */
42
+ command(name: string | RegExp, description: string, options?: Partial<CommandOptions>): Command<C>;
43
+ /**
44
+ * Serializes the commands into multiple objects that can each be passed to a `setMyCommands` call.
45
+ *
46
+ * @returns One item for each combination of command + scope + language
47
+ */
48
+ toArgs(): SetMyCommandsParams[];
49
+ /**
50
+ * Serializes the commands of a single scope into objects that can each be passed to a `setMyCommands` call.
51
+ *
52
+ * @param scope Selected scope to be serialized
53
+ * @returns One item per command per language
54
+ */
55
+ toSingleScopeArgs(scope: BotCommandScope): SetMyCommandsParams[];
56
+ /**
57
+ * Registers all commands to be displayed by clients according to their scopes and languages
58
+ * Calls `setMyCommands` for each language of each scope of each command.
59
+ *
60
+ * @param Instance of `bot` or { api: bot.api }
61
+ */
62
+ setCommands({ api }: {
63
+ api: Api;
64
+ }): Promise<void>;
65
+ /**
66
+ * Alias for {@link toArgs}
67
+ */
68
+ toJSON(): SetMyCommandsParams[];
69
+ /**
70
+ * @returns A JSON serialized version of all the currently registered commands
71
+ */
72
+ toString(): string;
73
+ middleware(): import("grammy").MiddlewareFn<C>;
74
+ }
75
+ export {};
@@ -3,13 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Commands = void 0;
4
4
  const command_js_1 = require("./command.js");
5
5
  const deps_node_js_1 = require("./deps.node.js");
6
+ /**
7
+ * Central class that manages all registered commands.
8
+ * This is the starting point for the plugin, and this is what you should pass to `bot.use` so your commands get properly registered.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const myCommands = new Commands()
13
+ * commands.command("start", "start the bot configuration", (ctx) => ctx.reply("Hello there!"))
14
+ *
15
+ * // Registers the commands with the bot instance.
16
+ * bot.use(myCommands)
17
+ * ```
18
+ */
6
19
  class Commands {
7
- constructor(commands = []) {
20
+ constructor(options = {}) {
8
21
  this._languages = new Set();
9
22
  this._scopes = new Map();
10
23
  this._commands = [];
11
24
  this._composer = new deps_node_js_1.Composer();
12
- commands.forEach((command) => this._commands.push(command));
25
+ this._commandOptions = {};
26
+ this._commandOptions = options;
13
27
  }
14
28
  _addCommandToScope(scope, command) {
15
29
  var _a;
@@ -18,9 +32,7 @@ class Commands {
18
32
  }
19
33
  _populateComposer() {
20
34
  for (const command of this._commands) {
21
- for (const args of command.languages.values()) {
22
- this._composer.command(args.name, command.middleware());
23
- }
35
+ this._composer.use(command.middleware());
24
36
  }
25
37
  }
26
38
  _populateMetadata() {
@@ -35,11 +47,23 @@ class Commands {
35
47
  }
36
48
  });
37
49
  }
38
- command(name, description) {
39
- const command = new command_js_1.Command(name, description);
50
+ /**
51
+ * Registers a new command and returns it.
52
+ * @param name Command name
53
+ * @param description Default command description
54
+ * @param options Extra options that should apply only to this command
55
+ * @returns An instance of the `Command` class
56
+ */
57
+ command(name, description, options = this._commandOptions) {
58
+ const command = new command_js_1.Command(name, description, options);
40
59
  this._commands.push(command);
41
60
  return command;
42
61
  }
62
+ /**
63
+ * Serializes the commands into multiple objects that can each be passed to a `setMyCommands` call.
64
+ *
65
+ * @returns One item for each combination of command + scope + language
66
+ */
43
67
  toArgs() {
44
68
  this._populateMetadata();
45
69
  const params = [];
@@ -47,13 +71,22 @@ class Commands {
47
71
  for (const language of this._languages) {
48
72
  params.push({
49
73
  scope: JSON.parse(scope),
50
- language_code: language === "default" ? undefined : language,
51
- commands: commands.map((command) => command.toObject(language)),
74
+ language_code: language === "default"
75
+ ? undefined
76
+ : language,
77
+ commands: commands.map((command) => command.toObject(language))
78
+ .filter((args) => args.command.length > 0),
52
79
  });
53
80
  }
54
81
  }
55
82
  return params.filter((params) => params.commands.length > 0);
56
83
  }
84
+ /**
85
+ * Serializes the commands of a single scope into objects that can each be passed to a `setMyCommands` call.
86
+ *
87
+ * @param scope Selected scope to be serialized
88
+ * @returns One item per command per language
89
+ */
57
90
  toSingleScopeArgs(scope) {
58
91
  this._populateMetadata();
59
92
  const params = [];
@@ -68,12 +101,24 @@ class Commands {
68
101
  }
69
102
  return params;
70
103
  }
104
+ /**
105
+ * Registers all commands to be displayed by clients according to their scopes and languages
106
+ * Calls `setMyCommands` for each language of each scope of each command.
107
+ *
108
+ * @param Instance of `bot` or { api: bot.api }
109
+ */
71
110
  async setCommands({ api }) {
72
111
  await Promise.all(this.toArgs().map((args) => api.raw.setMyCommands(args)));
73
112
  }
113
+ /**
114
+ * Alias for {@link toArgs}
115
+ */
74
116
  toJSON() {
75
117
  return this.toArgs();
76
118
  }
119
+ /**
120
+ * @returns A JSON serialized version of all the currently registered commands
121
+ */
77
122
  toString() {
78
123
  return JSON.stringify(this);
79
124
  }
@@ -81,9 +126,19 @@ class Commands {
81
126
  this._populateComposer();
82
127
  return this._composer.middleware();
83
128
  }
129
+ /**
130
+ * Replaces the `toString` method on node.js
131
+ *
132
+ * @see toString
133
+ */
84
134
  [Symbol.for("Deno.customInspect")]() {
85
135
  return this.toString();
86
136
  }
137
+ /**
138
+ * Replaces the `toString` method on Deno
139
+ *
140
+ * @see toString
141
+ */
87
142
  [Symbol.for("nodejs.util.inspect.custom")]() {
88
143
  return this.toString();
89
144
  }
package/out/context.d.ts CHANGED
@@ -1,6 +1,7 @@
1
+ import { Commands } from "./commands.js";
1
2
  import { Context, NextFunction } from "./deps.node.js";
2
- import { Commands } from "./plugin.js";
3
- export type CommandsFlavor<C extends Context = Context> = C & {
3
+ import { JaroWinklerOptions } from "./jaro-winkler.js";
4
+ export interface CommandsFlavor<C extends Context = Context> extends Context {
4
5
  /**
5
6
  * Sets the provided commands for the current chat.
6
7
  * Cannot be called on updates that don't have a `chat` property.
@@ -8,6 +9,18 @@ export type CommandsFlavor<C extends Context = Context> = C & {
8
9
  * @param commands List of available commands
9
10
  * @returns Promise with the result of the operations
10
11
  */
11
- setMyCommands: (commands: Commands<C>) => Promise<true[]>;
12
- };
12
+ setMyCommands: (commands: Commands<C>) => Promise<void>;
13
+ /**
14
+ * Returns the nearest command to the user input.
15
+ * If no command is found, returns `null`.
16
+ *
17
+ * @param commands List of available commands
18
+ * @param options Options for the Jaro-Winkler algorithm
19
+ * @returns The nearest command or `null`
20
+ */
21
+ getNearestCommand: (commands: Commands<C>, options?: Partial<JaroWinklerOptions>) => string | null;
22
+ }
23
+ /**
24
+ * Installs the commands flavor into the context.
25
+ */
13
26
  export declare function commands<C extends Context>(): (ctx: CommandsFlavor<C>, next: NextFunction) => Promise<void>;
package/out/context.js CHANGED
@@ -1,15 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.commands = void 0;
4
+ const jaro_winkler_js_1 = require("./jaro-winkler.js");
5
+ /**
6
+ * Installs the commands flavor into the context.
7
+ */
4
8
  function commands() {
5
9
  return (ctx, next) => {
6
- ctx.setMyCommands = (commands) => {
7
- if (!ctx.chat)
10
+ ctx.setMyCommands = async (commands) => {
11
+ if (!ctx.chat) {
8
12
  throw new Error("cannot call `ctx.setMyCommands` on an update with no `chat` property");
9
- return Promise.all(commands
13
+ }
14
+ await Promise.all(commands
10
15
  .toSingleScopeArgs({ type: "chat", chat_id: ctx.chat.id })
11
16
  .map((args) => ctx.api.raw.setMyCommands(args)));
12
17
  };
18
+ ctx.getNearestCommand = (commands, options) => {
19
+ var _a;
20
+ if ((_a = ctx.msg) === null || _a === void 0 ? void 0 : _a.text) {
21
+ const userInput = ctx.msg.text.substring(1);
22
+ return (0, jaro_winkler_js_1.fuzzyMatch)(userInput, commands, { ...options });
23
+ }
24
+ return null;
25
+ };
13
26
  return next();
14
27
  };
15
28
  }
@@ -1,3 +1,2 @@
1
1
  export { Api, Bot, type ChatTypeContext, type ChatTypeMiddleware, type CommandMiddleware, Composer, Context, type Middleware, type MiddlewareObj, type NextFunction, } from "grammy";
2
2
  export type { BotCommand, BotCommandScope, BotCommandScopeAllChatAdministrators, BotCommandScopeAllGroupChats, BotCommandScopeAllPrivateChats, Chat, } from "grammy/types";
3
- export { match, P } from "ts-pattern";
package/out/deps.node.js CHANGED
@@ -1,12 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.P = exports.match = exports.Context = exports.Composer = exports.Bot = exports.Api = void 0;
4
- // TODO: Replace with official deno module, once it arrives (https://github.com/gvergnaud/ts-pattern/pull/108)
3
+ exports.Context = exports.Composer = exports.Bot = exports.Api = void 0;
5
4
  var grammy_1 = require("grammy");
6
5
  Object.defineProperty(exports, "Api", { enumerable: true, get: function () { return grammy_1.Api; } });
7
6
  Object.defineProperty(exports, "Bot", { enumerable: true, get: function () { return grammy_1.Bot; } });
8
7
  Object.defineProperty(exports, "Composer", { enumerable: true, get: function () { return grammy_1.Composer; } });
9
8
  Object.defineProperty(exports, "Context", { enumerable: true, get: function () { return grammy_1.Context; } });
10
- var ts_pattern_1 = require("ts-pattern");
11
- Object.defineProperty(exports, "match", { enumerable: true, get: function () { return ts_pattern_1.match; } });
12
- Object.defineProperty(exports, "P", { enumerable: true, get: function () { return ts_pattern_1.P; } });
@@ -0,0 +1,4 @@
1
+ import { BotCommandScope } from "./deps.node.js";
2
+ export declare class InvalidScopeError extends Error {
3
+ constructor(scope: BotCommandScope);
4
+ }
package/out/errors.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InvalidScopeError = void 0;
4
+ class InvalidScopeError extends Error {
5
+ constructor(scope) {
6
+ super(`Invalid scope: ${scope}`);
7
+ this.name = "InvalidScopeError";
8
+ }
9
+ }
10
+ exports.InvalidScopeError = InvalidScopeError;
@@ -0,0 +1,9 @@
1
+ import { Commands } from "./commands.js";
2
+ import { Context } from "./deps.node.js";
3
+ export declare function distance(s1: string, s2: string): number;
4
+ export type JaroWinklerOptions = {
5
+ ignoreCase?: boolean;
6
+ similarityThreshold?: number;
7
+ };
8
+ export declare function JaroWinklerDistance(s1: string, s2: string, options: Partial<JaroWinklerOptions>): number;
9
+ export declare function fuzzyMatch<C extends Context>(userInput: string, commands: Commands<C>, options: Partial<JaroWinklerOptions>): string | null;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fuzzyMatch = exports.JaroWinklerDistance = exports.distance = void 0;
4
+ function distance(s1, s2) {
5
+ if (s1.length === 0 || s2.length === 0) {
6
+ return 0;
7
+ }
8
+ const matchWindow = Math.floor(Math.max(s1.length, s2.length) / 2.0) - 1;
9
+ const matches1 = new Array(s1.length);
10
+ const matches2 = new Array(s2.length);
11
+ let m = 0; // number of matches
12
+ let t = 0; // number of transpositions
13
+ let i = 0; // index for string 1
14
+ let k = 0; // index for string 2
15
+ for (i = 0; i < s1.length; i++) {
16
+ // loop to find matched characters
17
+ const start = Math.max(0, i - matchWindow); // use the higher of the window diff
18
+ const end = Math.min(i + matchWindow + 1, s2.length); // use the min of the window and string 2 length
19
+ for (k = start; k < end; k++) {
20
+ // iterate second string index
21
+ if (matches2[k]) {
22
+ // if second string character already matched
23
+ continue;
24
+ }
25
+ if (s1[i] !== s2[k]) {
26
+ // characters don't match
27
+ continue;
28
+ }
29
+ // assume match if the above 2 checks don't continue
30
+ matches1[i] = true;
31
+ matches2[k] = true;
32
+ m++;
33
+ break;
34
+ }
35
+ }
36
+ // nothing matched
37
+ if (m === 0) {
38
+ return 0.0;
39
+ }
40
+ k = 0; // reset string 2 index
41
+ for (i = 0; i < s1.length; i++) {
42
+ // loop to find transpositions
43
+ if (!matches1[i]) {
44
+ // non-matching character
45
+ continue;
46
+ }
47
+ while (!matches2[k]) {
48
+ // move k index to the next match
49
+ k++;
50
+ }
51
+ if (s1[i] !== s2[k]) {
52
+ // if the characters don't match, increase transposition
53
+ // HtD: t is always less than the number of matches m, because transpositions are a subset of matches
54
+ t++;
55
+ }
56
+ k++; // iterate k index normally
57
+ }
58
+ // transpositions divided by 2
59
+ t /= 2.0;
60
+ return (m / s1.length + m / s2.length + (m - t) / m) / 3.0; // HtD: therefore, m - t > 0, and m - t < m
61
+ // HtD: => return value is between 0 and 1
62
+ }
63
+ exports.distance = distance;
64
+ // Computes the Winkler distance between two string -- intrepreted from:
65
+ // http://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance
66
+ // s1 is the first string to compare
67
+ // s2 is the second string to compare
68
+ // dj is the Jaro Distance (if you've already computed it), leave blank and the method handles it
69
+ // ignoreCase: if true strings are first converted to lower case before comparison
70
+ function JaroWinklerDistance(s1, s2, options) {
71
+ if (s1 === s2) {
72
+ return 1;
73
+ }
74
+ else {
75
+ if (options.ignoreCase) {
76
+ s1 = s1.toLowerCase();
77
+ s2 = s2.toLowerCase();
78
+ }
79
+ const jaro = distance(s1, s2);
80
+ const p = 0.1; // default scaling factor
81
+ let l = 0; // length of the matching prefix
82
+ while (s1[l] === s2[l] && l < 4) {
83
+ l++;
84
+ }
85
+ // HtD: 1 - jaro >= 0
86
+ return jaro + l * p * (1 - jaro);
87
+ }
88
+ }
89
+ exports.JaroWinklerDistance = JaroWinklerDistance;
90
+ function fuzzyMatch(userInput, commands, options) {
91
+ const defaultSimilarityThreshold = 0.85;
92
+ const similarityThreshold = options.similarityThreshold ||
93
+ defaultSimilarityThreshold;
94
+ const commandsSet = new Set(commands
95
+ .toJSON()
96
+ .flatMap((item) => item.commands.map((command) => command.command)));
97
+ const bestMatch = Array.from(commandsSet).reduce((best, command) => {
98
+ const similarity = JaroWinklerDistance(userInput, command, {
99
+ ...options,
100
+ });
101
+ return similarity > best.similarity
102
+ ? { command, similarity }
103
+ : best;
104
+ }, { command: null, similarity: 0 });
105
+ return bestMatch.similarity > similarityThreshold
106
+ ? bestMatch.command
107
+ : null;
108
+ }
109
+ exports.fuzzyMatch = fuzzyMatch;
package/out/mod.d.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * from "./commands.js";
1
2
  export * from "./context.js";
2
- export * from "./plugin.js";
3
+ export type { CommandOptions } from "./types.js";
package/out/mod.js CHANGED
@@ -14,5 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./commands.js"), exports);
17
18
  __exportStar(require("./context.js"), exports);
18
- __exportStar(require("./plugin.js"), exports);
package/out/types.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Supported command options
3
+ */
4
+ export interface CommandOptions {
5
+ /**
6
+ * The prefix used to identify a command.
7
+ * Defaults to `/`.
8
+ */
9
+ prefix: string;
10
+ /**
11
+ * Whether the command should only be matched at the start of the message.
12
+ * Defaults to `true`.
13
+ */
14
+ matchOnlyAtStart: boolean;
15
+ /**
16
+ * Whether to ignore or only care about commands ending with the bot's username.
17
+ * Defaults to `"optional"`.
18
+ *
19
+ * - `"ignored"`: only non-targeted commands are matched
20
+ * - `"optional"`: both targeted and non-targeted commands are matched
21
+ * - `"required"`: only targeted commands are matched
22
+ */
23
+ targetedCommands: "ignored" | "optional" | "required";
24
+ }
package/out/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,27 +1,28 @@
1
1
  {
2
- "name": "@grammyjs/commands",
3
- "version": "0.3.1",
4
- "description": "grammY Commands Plugin",
5
- "main": "out/mod.js",
6
- "scripts": {
7
- "prepare": "deno task backport"
8
- },
9
- "keywords": [
10
- "grammY",
11
- "telegram",
12
- "bot",
13
- "commands"
14
- ],
15
- "author": "Roz <roz@rjmunhoz.me>",
16
- "license": "MIT",
17
- "dependencies": {
18
- "grammy": "^1.17.1",
19
- "ts-pattern": "^5.0.1"
20
- },
21
- "devDependencies": {
22
- "typescript": "^5.1.6"
23
- },
24
- "files": [
25
- "out"
26
- ]
2
+ "name": "@grammyjs/commands",
3
+ "version": "0.5.1",
4
+ "description": "grammY Commands Plugin",
5
+ "main": "out/mod.js",
6
+ "scripts": {
7
+ "backport": "deno task backport",
8
+ "prepare": "deno task backport"
9
+ },
10
+ "keywords": [
11
+ "grammY",
12
+ "telegram",
13
+ "bot",
14
+ "commands"
15
+ ],
16
+ "author": "Roz <roz@rjmunhoz.me>",
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "grammy": "^1.17.1",
20
+ "ts-pattern": "^5.0.1"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.1.6"
24
+ },
25
+ "files": [
26
+ "out"
27
+ ]
27
28
  }
package/out/plugin.d.ts DELETED
@@ -1,27 +0,0 @@
1
- import { Command } from "./command.js";
2
- import { Api, BotCommand, BotCommandScope, Context } from "./deps.node.js";
3
- type SetMyCommandsParams = {
4
- scope?: BotCommandScope;
5
- language_code?: string;
6
- commands: BotCommand[];
7
- };
8
- export declare class Commands<C extends Context> {
9
- private _languages;
10
- private _scopes;
11
- private _commands;
12
- private _composer;
13
- constructor(commands?: Command<C>[]);
14
- private _addCommandToScope;
15
- private _populateComposer;
16
- private _populateMetadata;
17
- command(name: string, description: string): Command<C>;
18
- toArgs(): SetMyCommandsParams[];
19
- toSingleScopeArgs(scope: BotCommandScope): SetMyCommandsParams[];
20
- setCommands({ api }: {
21
- api: Api;
22
- }): Promise<void>;
23
- toJSON(): SetMyCommandsParams[];
24
- toString(): string;
25
- middleware(): import("grammy").MiddlewareFn<C>;
26
- }
27
- export {};