diff --git a/prettier.config.js b/prettier.config.js index 002b0a1..f27d170 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -10,5 +10,5 @@ module.exports = { bracketSpacing: false, jsxBracketSameLine: false, arrowParens: "always", - endOfLine: "lf" + endOfLine: "auto" // Apparently, the GitHub repository still uses CRLF. I don't know how to force it to use LF, and until someone figures that out, I'm changing this to auto because I don't want more than one line ending commit. }; diff --git a/src/commands/utilities/emote.ts b/src/commands/utilities/emote.ts index 916b7cd..7b841e5 100644 --- a/src/commands/utilities/emote.ts +++ b/src/commands/utilities/emote.ts @@ -1,19 +1,19 @@ -import {MessageEmbed} from "discord.js"; import Command from "../../core/command"; -import {CommonLibrary} from "../../core/lib"; +import {queryClosestEmoteByName} from "./subcommands/emote-utils"; +import {botHasPermission} from "../../core/lib"; +import {Permissions} from "discord.js"; export default new Command({ description: "Send the specified emote.", run: "Please provide a command name.", any: new Command({ - description: "The emote to send.", - usage: "", - async run($: CommonLibrary): Promise { - const search = $.args[0].toLowerCase(); - const emote = $.client.emojis.cache.find((emote) => emote.name.toLowerCase().includes(search)); - if (!emote) return $.channel.send("That's not a valid emote name!"); - $.message.delete(); - $.channel.send(`${emote}`); + description: "The emote(s) to send.", + usage: "", + async run({guild, channel, message, args}) { + let output = ""; + for (const query of args) output += queryClosestEmoteByName(query).toString(); + if (botHasPermission(guild, Permissions.FLAGS.MANAGE_MESSAGES)) message.delete(); + channel.send(output); } }) }); diff --git a/src/commands/utilities/react.ts b/src/commands/utilities/react.ts index 035ae44..ceef800 100644 --- a/src/commands/utilities/react.ts +++ b/src/commands/utilities/react.ts @@ -1,6 +1,7 @@ import Command from "../../core/command"; import {CommonLibrary} from "../../core/lib"; import {Message, Channel, TextChannel} from "discord.js"; +import {queryClosestEmoteByName} from "./subcommands/emote-utils"; export default new Command({ description: @@ -94,26 +95,16 @@ export default new Command({ ).last(); } - let anyEmoteIsValid = false; - for (const search of $.args) { - const emoji = $.client.emojis.cache.find((emoji) => emoji.name === search); + // Even though the bot will always grab *some* emote, the user can choose not to keep that emote there if it isn't what they want + const emote = queryClosestEmoteByName(search); + const reaction = await target!.react(emote); - if (emoji) { - // Call the delete function only once to avoid unnecessary errors. - if (!anyEmoteIsValid && distance !== 0) $.message.delete(); - anyEmoteIsValid = true; - const reaction = await target?.react(emoji); - - // This part is called with a promise because you don't want to wait 5 seconds between each reaction. - - setTimeout(() => { - /// @ts-ignore - reaction.users.remove($.client.user); - }, 5000); - } + // This part is called with a promise because you don't want to wait 5 seconds between each reaction. + setTimeout(() => { + // This reason for this null assertion is that by the time you use this command, the client is going to be loaded. + reaction.users.remove($.client.user!); + }, 5000); } - - if (!anyEmoteIsValid && !$.message.deleted) $.message.react("❓"); } }); diff --git a/src/commands/utilities/subcommands/emote-utils.ts b/src/commands/utilities/subcommands/emote-utils.ts new file mode 100644 index 0000000..0e18652 --- /dev/null +++ b/src/commands/utilities/subcommands/emote-utils.ts @@ -0,0 +1,33 @@ +import {client} from "../../../index"; + +// Calculate and match the list of emotes against the queried emote, then sort the IDs based on calculated priority. +export function queryClosestEmoteByName(query: string) { + const priorityTable: {[id: string]: number} = {}; + + for (const emote of client.emojis.cache.values()) priorityTable[emote.id] = compareEmoteNames(emote.name, query); + + const resultingIDs = Object.keys(priorityTable).sort((a, b) => priorityTable[b] - priorityTable[a]); + return client.emojis.cache.get(resultingIDs[0])!; +} + +// Compare an emote's name against a query to see how alike the two are. The higher the number, the closer they are. Takes into account length and capitalization. +function compareEmoteNames(emote: string, query: string) { + let likeness = -Math.abs(emote.length - query.length); + const isQueryLonger = query.length > emote.length; + + // Loop through all indexes that the two strings share then compare each letter. + for (let i = 0; i < (isQueryLonger ? emote.length : query.length); i++) { + const c = emote[i]; + const q = query[i]; + + // If they're the exact same character + if (c === q) likeness += 1.5; + // If the emote is uppercase but the query is lowercase + else if (c === q.toUpperCase()) likeness += 1; + // If the emote is lowercase but the query is uppercase + else if (c === q.toLowerCase()) likeness += 0.5; + // Otherwise, if they're different characters, don't add anything (this isn't a spellchecker) + } + + return likeness; +}