@grammyjs/commands 0.3.0 → 0.5.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
@@ -1,35 +1,37 @@
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
- On NPM, this plugin is available as `@grammyjs/commands`, and on Deno it's available at [deno.land/x/grammy_commands](https://deno.land/x/grammy_commands).
8
-
9
- You can install it for Node.js with this command:
10
-
11
8
  ```sh
12
9
  npm i @grammyjs/commands
13
10
  ```
14
11
 
15
-
16
12
  ## Usage
17
13
 
18
- 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:
19
-
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:
20
16
 
21
17
  ```typescript
22
- import { Bot } from "https://deno.land/x/grammy/mod.ts";
23
- import { Commands } from "https://deno.land/x/grammy_commands/mod.ts";
18
+ import { Bot } from "grammy";
19
+ import { Commands } from "@grammyjs/commands";
24
20
 
25
21
  const bot = new Bot("<telegram token>");
26
22
 
27
23
  const myCommands = new Commands();
28
24
 
29
25
  myCommands.command("start", "Initializes bot configuration")
30
- .localize("pt", "start", "Inicializa as configurações do bot")
31
- .addToScope({ type: "all_private_chats" }, (ctx) => ctx.reply(`Hello, ${ctx.chat.first_name}!`))
32
- .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
+ );
33
35
 
34
36
  // Calls `setMyCommands`
35
37
  await myCommands.setCommands(bot);
@@ -40,17 +42,17 @@ bot.use(myCommands);
40
42
  bot.start();
41
43
  ```
42
44
 
43
- It is very important that you call `bot.use` with your instance of the `Commands` class.
44
- 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.
45
47
 
46
48
  ### Context shortcuts
47
49
 
48
- This plugin provides a shortcut for setting the commands for the current chat.
49
- 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:
50
52
 
51
53
  ```typescript
52
- import { Bot, Context } from "https://deno.land/x/grammy/mod.ts";
53
- import { Commands, CommandsFlavor, commands } from "https://deno.land/x/grammy_commands/mod.ts";
54
+ import { Bot, Context } from "grammy";
55
+ import { Commands, commands, CommandsFlavor } from "@grammyjs/commands";
54
56
 
55
57
  type BotContext = CommandsFlavor;
56
58
 
@@ -58,15 +60,15 @@ const bot = new Bot<BotContext>("<telegram_token>");
58
60
  bot.use(commands());
59
61
 
60
62
  bot.on("message", async (ctx) => {
61
- const cmds = new Commands();
63
+ const cmds = new Commands();
62
64
 
63
- cmds.command("start", "Initializes bot configuration")
64
- .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");
65
67
 
66
- await ctx.setMyCommands(cmds);
68
+ await ctx.setMyCommands(cmds);
67
69
 
68
- return ctx.reply("Commands set!");
70
+ return ctx.reply("Commands set!");
69
71
  });
70
72
 
71
- bot.start()
72
- ```
73
+ bot.start();
74
+ ```
package/out/command.d.ts CHANGED
@@ -1,24 +1,48 @@
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
2
  export type MaybeArray<T> = T | T[];
3
+ export type CommandOptions = {
4
+ /**
5
+ * The prefix used to identify a command.
6
+ * Defaults to `/`.
7
+ */
8
+ prefix: string;
9
+ /**
10
+ * Whether the command should only be matched at the start of the message.
11
+ * Defaults to `true`.
12
+ */
13
+ matchOnlyAtStart: boolean;
14
+ /**
15
+ * Whether to ignore or only care about commands ending with the bot's username.
16
+ * Defaults to `"optional"`.
17
+ *
18
+ * - `"ignored"`: only non-targeted commands are matched
19
+ * - `"optional"`: both targeted and non-targeted commands are matched
20
+ * - `"ignored"`: only targeted commands are matched
21
+ */
22
+ targetedCommands: "ignored" | "optional" | "required";
23
+ };
3
24
  type BotCommandGroupsScope = BotCommandScopeAllGroupChats | BotCommandScopeAllChatAdministrators;
25
+ export declare const matchesPattern: (value: string, pattern: string | RegExp) => boolean;
4
26
  export declare class Command<C extends Context = Context> implements MiddlewareObj<C> {
5
27
  private _scopes;
6
28
  private _languages;
7
29
  private _composer;
8
- constructor(name: string, description: string);
30
+ private _options;
31
+ constructor(name: string | RegExp, description: string, options?: Partial<CommandOptions>);
9
32
  get scopes(): BotCommandScope[];
10
33
  get languages(): Map<string, {
11
- name: string;
34
+ name: string | RegExp;
12
35
  description: string;
13
36
  }>;
14
- get names(): string[];
15
- get name(): string;
37
+ get names(): (string | RegExp)[];
38
+ get name(): string | RegExp;
16
39
  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;
40
+ addToScope(scope: BotCommandGroupsScope, middleware: MaybeArray<ChatTypeMiddleware<C, "group" | "supergroup">>, options?: Partial<CommandOptions>): this;
41
+ addToScope(scope: BotCommandScopeAllPrivateChats, middleware: MaybeArray<ChatTypeMiddleware<C, "private">>, options?: Partial<CommandOptions>): this;
42
+ addToScope(scope: BotCommandScope, middleware: MaybeArray<Middleware<C>>, options?: Partial<CommandOptions>): this;
43
+ static hasCommand(command: MaybeArray<string | RegExp>, options: CommandOptions): (ctx: Context) => boolean;
44
+ localize(languageCode: string, name: string | RegExp, description: string): this;
45
+ getLocalizedName(languageCode: string): string | RegExp;
22
46
  getLocalizedDescription(languageCode: string): string;
23
47
  toObject(languageCode?: string): BotCommand;
24
48
  middleware(): import("grammy").MiddlewareFn<C>;
package/out/command.js CHANGED
@@ -1,14 +1,26 @@
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 errors_js_1 = require("./errors.js");
6
+ const ensureArray = (value) => Array.isArray(value) ? value : [value];
5
7
  const isAdmin = (ctx) => ctx.getAuthor().then((author) => ["administrator", "creator"].includes(author.status));
8
+ const matchesPattern = (value, pattern) => typeof pattern === "string" ? value === pattern : pattern.test(value);
9
+ exports.matchesPattern = matchesPattern;
6
10
  class Command {
7
- constructor(name, description) {
11
+ constructor(name, description, options = {}) {
8
12
  this._scopes = [];
9
13
  this._languages = new Map();
10
14
  this._composer = new deps_node_js_1.Composer();
11
- this._languages.set("default", { name, description });
15
+ this._options = {
16
+ prefix: "/",
17
+ matchOnlyAtStart: true,
18
+ targetedCommands: "optional",
19
+ };
20
+ this._options = { ...this._options, ...options };
21
+ if (this._options.prefix === "")
22
+ this._options.prefix = "/";
23
+ this._languages.set("default", { name: name, description });
12
24
  }
13
25
  get scopes() {
14
26
  return this._scopes;
@@ -25,21 +37,89 @@ class Command {
25
37
  get description() {
26
38
  return this._languages.get("default").description;
27
39
  }
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));
40
+ addToScope(scope, middleware, options = this._options) {
41
+ const middlewareArray = ensureArray(middleware);
42
+ const optionsObject = { ...this._options, ...options };
43
+ switch (scope.type) {
44
+ case "default":
45
+ this._composer
46
+ .filter(Command.hasCommand(this.names, optionsObject))
47
+ .use(...middlewareArray);
48
+ break;
49
+ case "all_chat_administrators":
50
+ this._composer
51
+ .filter(Command.hasCommand(this.names, optionsObject))
52
+ .filter(isAdmin)
53
+ .use(...middlewareArray);
54
+ break;
55
+ case "all_private_chats":
56
+ this._composer
57
+ .filter(Command.hasCommand(this.names, optionsObject))
58
+ .chatType("private")
59
+ .use(...middlewareArray);
60
+ break;
61
+ case "all_group_chats":
62
+ this._composer
63
+ .filter(Command.hasCommand(this.names, optionsObject))
64
+ .chatType(["group", "supergroup"])
65
+ .use(...middlewareArray);
66
+ break;
67
+ case "chat":
68
+ case "chat_administrators":
69
+ if (scope.chat_id) {
70
+ this._composer
71
+ .filter(Command.hasCommand(this.names, optionsObject))
72
+ .filter((ctx) => { var _a; return ((_a = ctx.chat) === null || _a === void 0 ? void 0 : _a.id) === scope.chat_id; })
73
+ .filter(isAdmin)
74
+ .use(...middlewareArray);
75
+ }
76
+ break;
77
+ case "chat_member":
78
+ if (scope.chat_id && scope.user_id) {
79
+ this._composer
80
+ .filter(Command.hasCommand(this.names, optionsObject))
81
+ .filter((ctx) => { var _a; return ((_a = ctx.chat) === null || _a === void 0 ? void 0 : _a.id) === scope.chat_id; })
82
+ .filter((ctx) => { var _a; return ((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id) === scope.user_id; })
83
+ .use(...middlewareArray);
84
+ }
85
+ break;
86
+ default:
87
+ throw new errors_js_1.InvalidScopeError(scope);
88
+ }
38
89
  this._scopes.push(scope);
39
90
  return this;
40
91
  }
92
+ static hasCommand(command, options) {
93
+ const { matchOnlyAtStart, prefix, targetedCommands } = options;
94
+ return (ctx) => {
95
+ if (!ctx.has(":text"))
96
+ return false;
97
+ if (matchOnlyAtStart && !ctx.msg.text.startsWith(prefix)) {
98
+ return false;
99
+ }
100
+ const commandNames = ensureArray(command);
101
+ const commands = prefix === "/"
102
+ ? ctx.entities("bot_command")
103
+ : ctx.msg.text.split(prefix).map((text) => ({ text }));
104
+ for (const { text } of commands) {
105
+ const [command, username] = text.split("@");
106
+ if (targetedCommands === "ignored" && username)
107
+ continue;
108
+ if (targetedCommands === "required" && !username)
109
+ continue;
110
+ if (username && username !== ctx.me.username)
111
+ continue;
112
+ if (commandNames.some((name) => (0, exports.matchesPattern)(command.replace(prefix, ""), name)))
113
+ return true;
114
+ }
115
+ return false;
116
+ };
117
+ }
41
118
  localize(languageCode, name, description) {
42
- this._languages.set(languageCode, { name, description });
119
+ this._languages.set(languageCode, {
120
+ name: new RegExp(name),
121
+ description,
122
+ });
43
123
  return this;
44
124
  }
45
125
  getLocalizedName(languageCode) {
@@ -51,8 +131,9 @@ class Command {
51
131
  return (_b = (_a = this._languages.get(languageCode)) === null || _a === void 0 ? void 0 : _a.description) !== null && _b !== void 0 ? _b : this.description;
52
132
  }
53
133
  toObject(languageCode = "default") {
134
+ const localizedName = this.getLocalizedName(languageCode);
54
135
  return {
55
- command: this.getLocalizedName(languageCode),
136
+ command: localizedName instanceof RegExp ? "" : localizedName,
56
137
  description: this.getLocalizedDescription(languageCode),
57
138
  };
58
139
  }
@@ -1,4 +1,4 @@
1
- import { Command } from "./command.js";
1
+ import { Command, CommandOptions } from "./command.js";
2
2
  import { Api, BotCommand, BotCommandScope, Context } from "./deps.node.js";
3
3
  type SetMyCommandsParams = {
4
4
  scope?: BotCommandScope;
@@ -10,11 +10,12 @@ export declare class Commands<C extends Context> {
10
10
  private _scopes;
11
11
  private _commands;
12
12
  private _composer;
13
- constructor(commands?: Command<C>[]);
13
+ private _commandOptions;
14
+ constructor(options?: Partial<CommandOptions>);
14
15
  private _addCommandToScope;
15
16
  private _populateComposer;
16
17
  private _populateMetadata;
17
- command(name: string, description: string): Command<C>;
18
+ command(name: string | RegExp, description: string, options?: Partial<CommandOptions>): Command<C>;
18
19
  toArgs(): SetMyCommandsParams[];
19
20
  toSingleScopeArgs(scope: BotCommandScope): SetMyCommandsParams[];
20
21
  setCommands({ api }: {
@@ -4,12 +4,13 @@ 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
6
  class Commands {
7
- constructor(commands = []) {
7
+ constructor(options = {}) {
8
8
  this._languages = new Set();
9
9
  this._scopes = new Map();
10
10
  this._commands = [];
11
11
  this._composer = new deps_node_js_1.Composer();
12
- commands.forEach((command) => this._commands.push(command));
12
+ this._commandOptions = {};
13
+ this._commandOptions = options;
13
14
  }
14
15
  _addCommandToScope(scope, command) {
15
16
  var _a;
@@ -18,9 +19,7 @@ class Commands {
18
19
  }
19
20
  _populateComposer() {
20
21
  for (const command of this._commands) {
21
- for (const args of command.languages.values()) {
22
- this._composer.command(args.name, command.middleware());
23
- }
22
+ this._composer.use(command.middleware());
24
23
  }
25
24
  }
26
25
  _populateMetadata() {
@@ -35,8 +34,8 @@ class Commands {
35
34
  }
36
35
  });
37
36
  }
38
- command(name, description) {
39
- const command = new command_js_1.Command(name, description);
37
+ command(name, description, options = this._commandOptions) {
38
+ const command = new command_js_1.Command(name, description, options);
40
39
  this._commands.push(command);
41
40
  return command;
42
41
  }
@@ -47,8 +46,11 @@ class Commands {
47
46
  for (const language of this._languages) {
48
47
  params.push({
49
48
  scope: JSON.parse(scope),
50
- language_code: language === "default" ? undefined : language,
51
- commands: commands.map((command) => command.toObject(language)),
49
+ language_code: language === "default"
50
+ ? undefined
51
+ : language,
52
+ commands: commands.map((command) => command.toObject(language))
53
+ .filter((args) => args.command.length > 0),
52
54
  });
53
55
  }
54
56
  }
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,2 @@
1
+ export * from "./commands.js";
1
2
  export * from "./context.js";
2
- export * from "./plugin.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/package.json CHANGED
@@ -1,27 +1,28 @@
1
1
  {
2
- "name": "@grammyjs/commands",
3
- "version": "0.3.0",
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.0",
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
  }