mirror of
				https://github.com/keanuplayz/TravBot-v3.git
				synced 2024-08-15 02:33:12 +00:00 
			
		
		
		
	Split command resolution part of help command
This commit is contained in:
		
							parent
							
								
									20fb2135c7
								
							
						
					
					
						commit
						72ff144cc0
					
				
					 12 changed files with 146 additions and 86 deletions
				
			
		|  | @ -1,61 +1,28 @@ | ||||||
| import {Command, NamedCommand, loadableCommands, categories, getPermissionName, CHANNEL_TYPE} from "../../core"; | import {Command, NamedCommand, CHANNEL_TYPE, getPermissionName, getCommandList, getCommandInfo} from "../../core"; | ||||||
| import {toTitleCase, requireAllCasesHandledFor} from "../../lib"; | import {requireAllCasesHandledFor} from "../../lib"; | ||||||
| 
 | 
 | ||||||
| export default new NamedCommand({ | export default new NamedCommand({ | ||||||
|     description: "Lists all commands. If a command is specified, their arguments are listed as well.", |     description: "Lists all commands. If a command is specified, their arguments are listed as well.", | ||||||
|     usage: "([command, [subcommand/type], ...])", |     usage: "([command, [subcommand/type], ...])", | ||||||
|     aliases: ["h"], |     aliases: ["h"], | ||||||
|     async run({message, channel, guild, author, member, client, args}) { |     async run({message, channel, guild, author, member, client, args}) { | ||||||
|         const commands = await loadableCommands; |         const commands = await getCommandList(); | ||||||
|         let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\``; |         let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\``; | ||||||
| 
 | 
 | ||||||
|         for (const [category, headers] of categories) { |         for (const [category, commandList] of commands) { | ||||||
|             let tmp = `\n\n===[ ${toTitleCase(category)} ]===`; |             output += `\n\n===[ ${category} ]===`; | ||||||
|             // Ignore empty categories, including ["test"].
 |             for (const command of commandList) output += `\n- \`${command.name}\`: ${command.description}`; | ||||||
|             let hasActualCommands = false; |  | ||||||
| 
 |  | ||||||
|             for (const header of headers) { |  | ||||||
|                 if (header !== "test") { |  | ||||||
|                     const command = commands.get(header)!; |  | ||||||
|                     tmp += `\n- \`${header}\`: ${command.description}`; |  | ||||||
|                     hasActualCommands = true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (hasActualCommands) output += tmp; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         channel.send(output, {split: true}); |         channel.send(output, {split: true}); | ||||||
|     }, |     }, | ||||||
|     any: new Command({ |     any: new Command({ | ||||||
|         async run({message, channel, guild, author, member, client, args}) { |         async run({message, channel, guild, author, member, client, args}) { | ||||||
|             // Setup the root command
 |             const [result, category] = await getCommandInfo(args); | ||||||
|             const commands = await loadableCommands; |             if (typeof result === "string") return channel.send(result); | ||||||
|             let header = args.shift() as string; |  | ||||||
|             let command = commands.get(header); |  | ||||||
|             if (!command || header === "test") return channel.send(`No command found by the name \`${header}\`.`); |  | ||||||
|             if (!(command instanceof NamedCommand)) |  | ||||||
|                 return channel.send(`Command is not a proper instance of NamedCommand.`); |  | ||||||
|             if (command.name) header = command.name; |  | ||||||
| 
 |  | ||||||
|             // Search categories
 |  | ||||||
|             let category = "Unknown"; |  | ||||||
|             for (const [referenceCategory, headers] of categories) { |  | ||||||
|                 if (headers.includes(header)) { |  | ||||||
|                     category = toTitleCase(referenceCategory); |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Gather info
 |  | ||||||
|             const result = await command.resolveInfo(args); |  | ||||||
| 
 |  | ||||||
|             if (result.type === "error") return channel.send(result.message); |  | ||||||
| 
 |  | ||||||
|             let append = ""; |             let append = ""; | ||||||
|             command = result.command; |             const command = result.command; | ||||||
| 
 |             const header = result.args.length > 0 ? `${result.header} ${result.args.join(" ")}` : result.header; | ||||||
|             if (result.args.length > 0) header += " " + result.args.join(" "); |  | ||||||
| 
 | 
 | ||||||
|             if (command.usage === "") { |             if (command.usage === "") { | ||||||
|                 const list: string[] = []; |                 const list: string[] = []; | ||||||
|  |  | ||||||
|  | @ -102,7 +102,7 @@ export default new NamedCommand({ | ||||||
|                 description: "Display info about a guild by finding its name or ID.", |                 description: "Display info about a guild by finding its name or ID.", | ||||||
|                 async run({message, channel, guild, author, member, client, args}) { |                 async run({message, channel, guild, author, member, client, args}) { | ||||||
|                     // If a guild ID is provided (avoid the "number" subcommand because of inaccuracies), search for that guild
 |                     // If a guild ID is provided (avoid the "number" subcommand because of inaccuracies), search for that guild
 | ||||||
|                     if (args.length === 1 && /^\d{17,19}$/.test(args[0])) { |                     if (args.length === 1 && /^\d{17,}$/.test(args[0])) { | ||||||
|                         const id = args[0]; |                         const id = args[0]; | ||||||
|                         const targetGuild = client.guilds.cache.get(id); |                         const targetGuild = client.guilds.cache.get(id); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ export default new NamedCommand({ | ||||||
|             "Filters emotes by via a regular expression. Flags can be added by adding a dash at the end. For example, to do a case-insensitive search, do %prefix%lsemotes somepattern -i", |             "Filters emotes by via a regular expression. Flags can be added by adding a dash at the end. For example, to do a case-insensitive search, do %prefix%lsemotes somepattern -i", | ||||||
|         async run({message, channel, guild, author, member, client, args}) { |         async run({message, channel, guild, author, member, client, args}) { | ||||||
|             // If a guild ID is provided, filter all emotes by that guild (but only if there aren't any arguments afterward)
 |             // If a guild ID is provided, filter all emotes by that guild (but only if there aren't any arguments afterward)
 | ||||||
|             if (args.length === 1 && /^\d{17,19}$/.test(args[0])) { |             if (args.length === 1 && /^\d{17,}$/.test(args[0])) { | ||||||
|                 const guildID: string = args[0]; |                 const guildID: string = args[0]; | ||||||
| 
 | 
 | ||||||
|                 displayEmoteList( |                 displayEmoteList( | ||||||
|  |  | ||||||
|  | @ -17,8 +17,8 @@ export default new NamedCommand({ | ||||||
|         // handles reacts by message id/distance
 |         // handles reacts by message id/distance
 | ||||||
|         else if (args.length >= 2) { |         else if (args.length >= 2) { | ||||||
|             const last = args[args.length - 1]; // Because this is optional, do not .pop() unless you're sure it's a message link indicator.
 |             const last = args[args.length - 1]; // Because this is optional, do not .pop() unless you're sure it's a message link indicator.
 | ||||||
|             const URLPattern = /^(?:https:\/\/discord.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19}))$/; |             const URLPattern = /^(?:https:\/\/discord.com\/channels\/(\d{17,})\/(\d{17,})\/(\d{17,}))$/; | ||||||
|             const copyIDPattern = /^(?:(\d{17,19})-(\d{17,19}))$/; |             const copyIDPattern = /^(?:(\d{17,})-(\d{17,}))$/; | ||||||
| 
 | 
 | ||||||
|             // https://discord.com/channels/<Guild ID>/<Channel ID>/<Message ID> ("Copy Message Link" Button)
 |             // https://discord.com/channels/<Guild ID>/<Channel ID>/<Message ID> ("Copy Message Link" Button)
 | ||||||
|             if (URLPattern.test(last)) { |             if (URLPattern.test(last)) { | ||||||
|  | @ -70,7 +70,7 @@ export default new NamedCommand({ | ||||||
|                 args.pop(); |                 args.pop(); | ||||||
|             } |             } | ||||||
|             // <Message ID>
 |             // <Message ID>
 | ||||||
|             else if (/^\d{17,19}$/.test(last)) { |             else if (/^\d{17,}$/.test(last)) { | ||||||
|                 try { |                 try { | ||||||
|                     target = await channel.messages.fetch(last); |                     target = await channel.messages.fetch(last); | ||||||
|                 } catch { |                 } catch { | ||||||
|  |  | ||||||
|  | @ -30,14 +30,16 @@ import {parseVars, requireAllCasesHandledFor} from "../lib"; | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| // RegEx patterns used for identifying/extracting each type from a string argument.
 | // RegEx patterns used for identifying/extracting each type from a string argument.
 | ||||||
|  | // The reason why \d{17,} is used is because the max safe number for JS numbers is 16 characters when stringified (decimal). Beyond that are IDs.
 | ||||||
| const patterns = { | const patterns = { | ||||||
|     channel: /^<#(\d{17,19})>$/, |     channel: /^<#(\d{17,})>$/, | ||||||
|     role: /^<@&(\d{17,19})>$/, |     role: /^<@&(\d{17,})>$/, | ||||||
|     emote: /^<a?:.*?:(\d{17,19})>$/, |     emote: /^<a?:.*?:(\d{17,})>$/, | ||||||
|     messageLink: /^https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(?:\d{17,19}|@me)\/(\d{17,19})\/(\d{17,19})$/, |     // The message type won't include <username>#<tag>. At that point, you may as well just use a search usernames function. Even then, tags would only be taken into account to differentiate different users with identical usernames.
 | ||||||
|     messagePair: /^(\d{17,19})-(\d{17,19})$/, |     messageLink: /^https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(?:\d{17,}|@me)\/(\d{17,})\/(\d{17,})$/, | ||||||
|     user: /^<@!?(\d{17,19})>$/, |     messagePair: /^(\d{17,})-(\d{17,})$/, | ||||||
|     id: /^(\d{17,19})$/ |     user: /^<@!?(\d{17,})>$/, | ||||||
|  |     id: /^(\d{17,})$/ | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // Maybe add a guild redirect... somehow?
 | // Maybe add a guild redirect... somehow?
 | ||||||
|  | @ -106,9 +108,10 @@ interface ExecuteCommandMetadata { | ||||||
|     permission: number; |     permission: number; | ||||||
|     nsfw: boolean; |     nsfw: boolean; | ||||||
|     channelType: CHANNEL_TYPE; |     channelType: CHANNEL_TYPE; | ||||||
|  |     symbolicArgs: string[]; // i.e. <channel> instead of <#...>
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface CommandInfo { | export interface CommandInfo { | ||||||
|     readonly type: "info"; |     readonly type: "info"; | ||||||
|     readonly command: Command; |     readonly command: Command; | ||||||
|     readonly subcommandInfo: Collection<string, Command>; |     readonly subcommandInfo: Collection<string, Command>; | ||||||
|  | @ -117,6 +120,7 @@ interface CommandInfo { | ||||||
|     readonly nsfw: boolean; |     readonly nsfw: boolean; | ||||||
|     readonly channelType: CHANNEL_TYPE; |     readonly channelType: CHANNEL_TYPE; | ||||||
|     readonly args: string[]; |     readonly args: string[]; | ||||||
|  |     readonly header: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface CommandInfoError { | interface CommandInfoError { | ||||||
|  | @ -131,14 +135,9 @@ interface CommandInfoMetadata { | ||||||
|     args: string[]; |     args: string[]; | ||||||
|     usage: string; |     usage: string; | ||||||
|     readonly originalArgs: string[]; |     readonly originalArgs: string[]; | ||||||
|  |     readonly header: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const defaultMetadata = { |  | ||||||
|     permission: 0, |  | ||||||
|     nsfw: false, |  | ||||||
|     channelType: CHANNEL_TYPE.ANY |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // Each Command instance represents a block that links other Command instances under it.
 | // Each Command instance represents a block that links other Command instances under it.
 | ||||||
| export class Command { | export class Command { | ||||||
|     public readonly description: string; |     public readonly description: string; | ||||||
|  | @ -298,12 +297,15 @@ export class Command { | ||||||
|             // Then capture any potential errors.
 |             // Then capture any potential errors.
 | ||||||
|             try { |             try { | ||||||
|                 if (typeof this.run === "string") { |                 if (typeof this.run === "string") { | ||||||
|  |                     // Although I *could* add an option in the launcher to attach arbitrary variables to this var string...
 | ||||||
|  |                     // I'll just leave it like this, because instead of using var strings for user stuff, you could just make "run" a template string.
 | ||||||
|                     await menu.channel.send( |                     await menu.channel.send( | ||||||
|                         parseVars( |                         parseVars( | ||||||
|                             this.run, |                             this.run, | ||||||
|                             { |                             { | ||||||
|                                 author: menu.author.toString(), |                                 author: menu.author.toString(), | ||||||
|                                 prefix: getPrefix(menu.guild) |                                 prefix: getPrefix(menu.guild), | ||||||
|  |                                 command: `${metadata.header} ${metadata.symbolicArgs.join(", ")}` | ||||||
|                             }, |                             }, | ||||||
|                             "???" |                             "???" | ||||||
|                         ) |                         ) | ||||||
|  | @ -332,6 +334,7 @@ export class Command { | ||||||
|         const isMessagePair = patterns.messagePair.test(param); |         const isMessagePair = patterns.messagePair.test(param); | ||||||
| 
 | 
 | ||||||
|         if (this.subcommands.has(param)) { |         if (this.subcommands.has(param)) { | ||||||
|  |             metadata.symbolicArgs.push(param); | ||||||
|             return this.subcommands.get(param)!.execute(args, menu, metadata); |             return this.subcommands.get(param)!.execute(args, menu, metadata); | ||||||
|         } else if (this.channel && patterns.channel.test(param)) { |         } else if (this.channel && patterns.channel.test(param)) { | ||||||
|             const id = patterns.channel.exec(param)![1]; |             const id = patterns.channel.exec(param)![1]; | ||||||
|  | @ -339,6 +342,7 @@ export class Command { | ||||||
| 
 | 
 | ||||||
|             // Users can only enter in this format for text channels, so this restricts it to that.
 |             // Users can only enter in this format for text channels, so this restricts it to that.
 | ||||||
|             if (channel instanceof TextChannel) { |             if (channel instanceof TextChannel) { | ||||||
|  |                 metadata.symbolicArgs.push("<channel>"); | ||||||
|                 menu.args.push(channel); |                 menu.args.push(channel); | ||||||
|                 return this.channel.execute(args, menu, metadata); |                 return this.channel.execute(args, menu, metadata); | ||||||
|             } else { |             } else { | ||||||
|  | @ -358,6 +362,7 @@ export class Command { | ||||||
|             const role = menu.guild.roles.cache.get(id); |             const role = menu.guild.roles.cache.get(id); | ||||||
| 
 | 
 | ||||||
|             if (role) { |             if (role) { | ||||||
|  |                 metadata.symbolicArgs.push("<role>"); | ||||||
|                 menu.args.push(role); |                 menu.args.push(role); | ||||||
|                 return this.role.execute(args, menu, metadata); |                 return this.role.execute(args, menu, metadata); | ||||||
|             } else { |             } else { | ||||||
|  | @ -370,6 +375,7 @@ export class Command { | ||||||
|             const emote = menu.client.emojis.cache.get(id); |             const emote = menu.client.emojis.cache.get(id); | ||||||
| 
 | 
 | ||||||
|             if (emote) { |             if (emote) { | ||||||
|  |                 metadata.symbolicArgs.push("<emote>"); | ||||||
|                 menu.args.push(emote); |                 menu.args.push(emote); | ||||||
|                 return this.emote.execute(args, menu, metadata); |                 return this.emote.execute(args, menu, metadata); | ||||||
|             } else { |             } else { | ||||||
|  | @ -395,6 +401,7 @@ export class Command { | ||||||
| 
 | 
 | ||||||
|             if (channel instanceof TextChannel || channel instanceof DMChannel) { |             if (channel instanceof TextChannel || channel instanceof DMChannel) { | ||||||
|                 try { |                 try { | ||||||
|  |                     metadata.symbolicArgs.push("<message>"); | ||||||
|                     menu.args.push(await channel.messages.fetch(messageID)); |                     menu.args.push(await channel.messages.fetch(messageID)); | ||||||
|                     return this.message.execute(args, menu, metadata); |                     return this.message.execute(args, menu, metadata); | ||||||
|                 } catch { |                 } catch { | ||||||
|  | @ -411,6 +418,7 @@ export class Command { | ||||||
|             const id = patterns.user.exec(param)![1]; |             const id = patterns.user.exec(param)![1]; | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
|  |                 metadata.symbolicArgs.push("<user>"); | ||||||
|                 menu.args.push(await menu.client.users.fetch(id)); |                 menu.args.push(await menu.client.users.fetch(id)); | ||||||
|                 return this.user.execute(args, menu, metadata); |                 return this.user.execute(args, menu, metadata); | ||||||
|             } catch { |             } catch { | ||||||
|  | @ -419,6 +427,7 @@ export class Command { | ||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
|         } else if (this.id && this.idType && patterns.id.test(param)) { |         } else if (this.id && this.idType && patterns.id.test(param)) { | ||||||
|  |             metadata.symbolicArgs.push("<id>"); | ||||||
|             const id = patterns.id.exec(param)![1]; |             const id = patterns.id.exec(param)![1]; | ||||||
| 
 | 
 | ||||||
|             // Probably modularize the findXByY code in general in libd.
 |             // Probably modularize the findXByY code in general in libd.
 | ||||||
|  | @ -486,9 +495,11 @@ export class Command { | ||||||
|                     requireAllCasesHandledFor(this.idType); |                     requireAllCasesHandledFor(this.idType); | ||||||
|             } |             } | ||||||
|         } else if (this.number && !Number.isNaN(Number(param)) && param !== "Infinity" && param !== "-Infinity") { |         } else if (this.number && !Number.isNaN(Number(param)) && param !== "Infinity" && param !== "-Infinity") { | ||||||
|  |             metadata.symbolicArgs.push("<number>"); | ||||||
|             menu.args.push(Number(param)); |             menu.args.push(Number(param)); | ||||||
|             return this.number.execute(args, menu, metadata); |             return this.number.execute(args, menu, metadata); | ||||||
|         } else if (this.any) { |         } else if (this.any) { | ||||||
|  |             metadata.symbolicArgs.push("<any>"); | ||||||
|             menu.args.push(param); |             menu.args.push(param); | ||||||
|             return this.any.execute(args, menu, metadata); |             return this.any.execute(args, menu, metadata); | ||||||
|         } else { |         } else { | ||||||
|  | @ -502,8 +513,16 @@ export class Command { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // What this does is resolve the resulting subcommand as well as the inherited properties and the available subcommands.
 |     // What this does is resolve the resulting subcommand as well as the inherited properties and the available subcommands.
 | ||||||
|     public async resolveInfo(args: string[]): Promise<CommandInfo | CommandInfoError> { |     public async resolveInfo(args: string[], header: string): Promise<CommandInfo | CommandInfoError> { | ||||||
|         return this.resolveInfoInternal(args, {...defaultMetadata, args: [], usage: "", originalArgs: [...args]}); |         return this.resolveInfoInternal(args, { | ||||||
|  |             permission: 0, | ||||||
|  |             nsfw: false, | ||||||
|  |             channelType: CHANNEL_TYPE.ANY, | ||||||
|  |             header, | ||||||
|  |             args: [], | ||||||
|  |             usage: "", | ||||||
|  |             originalArgs: [...args] | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async resolveInfoInternal( |     private async resolveInfoInternal( | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import {Client, Permissions, Message, TextChannel, DMChannel, NewsChannel} from "discord.js"; | import {Client, Permissions, Message, TextChannel, DMChannel, NewsChannel} from "discord.js"; | ||||||
| import {loadableCommands} from "./loader"; | import {loadableCommands} from "./loader"; | ||||||
| import {defaultMetadata} from "./command"; |  | ||||||
| import {getPrefix} from "./interface"; | import {getPrefix} from "./interface"; | ||||||
| 
 | 
 | ||||||
| // For custom message events that want to cancel the command handler on certain conditions.
 | // For custom message events that want to cancel the command handler on certain conditions.
 | ||||||
|  | @ -20,6 +19,13 @@ const lastCommandInfo: { | ||||||
|     channel: null |     channel: null | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const defaultMetadata = { | ||||||
|  |     permission: 0, | ||||||
|  |     nsfw: false, | ||||||
|  |     channelType: 0, // CHANNEL_TYPE.ANY, apparently isn't initialized at this point yet
 | ||||||
|  |     symbolicArgs: [] | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| // Note: client.user is only undefined before the bot logs in, so by this point, client.user cannot be undefined.
 | // Note: client.user is only undefined before the bot logs in, so by this point, client.user cannot be undefined.
 | ||||||
| // Note: guild.available will never need to be checked because the message starts in either a DM channel or an already-available guild.
 | // Note: guild.available will never need to be checked because the message starts in either a DM channel or an already-available guild.
 | ||||||
| export function attachMessageHandlerToClient(client: Client) { | export function attachMessageHandlerToClient(client: Client) { | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | // Onion Lasers Command Handler //
 | ||||||
| export {Command, NamedCommand, CHANNEL_TYPE} from "./command"; | export {Command, NamedCommand, CHANNEL_TYPE} from "./command"; | ||||||
| export {addInterceptRule} from "./handler"; | export {addInterceptRule} from "./handler"; | ||||||
| export {launch} from "./interface"; | export {launch} from "./interface"; | ||||||
|  | @ -12,5 +13,5 @@ export { | ||||||
|     getMemberByUsername, |     getMemberByUsername, | ||||||
|     callMemberByUsername |     callMemberByUsername | ||||||
| } from "./libd"; | } from "./libd"; | ||||||
| export {loadableCommands, categories} from "./loader"; | export {getCommandList, getCommandInfo} from "./loader"; | ||||||
| export {hasPermission, getPermissionLevel, getPermissionName} from "./permissions"; | export {hasPermission, getPermissionLevel, getPermissionName} from "./permissions"; | ||||||
|  |  | ||||||
|  | @ -25,6 +25,11 @@ export function botHasPermission(guild: Guild | null, permission: number): boole | ||||||
| // Pagination function that allows for customization via a callback.
 | // Pagination function that allows for customization via a callback.
 | ||||||
| // Define your own pages outside the function because this only manages the actual turning of pages.
 | // Define your own pages outside the function because this only manages the actual turning of pages.
 | ||||||
| 
 | 
 | ||||||
|  | const FIVE_BACKWARDS_EMOJI = "⏪"; | ||||||
|  | const BACKWARDS_EMOJI = "⬅️"; | ||||||
|  | const FORWARDS_EMOJI = "➡️"; | ||||||
|  | const FIVE_FORWARDS_EMOJI = "⏩"; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Takes a message and some additional parameters and makes a reaction page with it. All the pagination logic is taken care of but nothing more, the page index is returned and you have to send a callback to do something with it. |  * Takes a message and some additional parameters and makes a reaction page with it. All the pagination logic is taken care of but nothing more, the page index is returned and you have to send a callback to do something with it. | ||||||
|  */ |  */ | ||||||
|  | @ -43,30 +48,42 @@ export async function paginate( | ||||||
|         const turn = (amount: number) => { |         const turn = (amount: number) => { | ||||||
|             page += amount; |             page += amount; | ||||||
| 
 | 
 | ||||||
|             if (page < 0) page += total; |             if (page >= total) { | ||||||
|             else if (page >= total) page -= total; |                 page %= total; | ||||||
|  |             } else if (page < 0) { | ||||||
|  |                 // Assuming 3 total pages, it's a bit tricker, but if we just take the modulo of the absolute value (|page| % total), we get (1 2 0 ...), and we just need the pattern (2 1 0 ...). It needs to reverse order except for when it's 0. I want to find a better solution, but for the time being... total - (|page| % total) unless (|page| % total) = 0, then return 0.
 | ||||||
|  |                 const flattened = Math.abs(page) % total; | ||||||
|  |                 if (flattened !== 0) page = total - flattened; | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             message.edit(callback(page, true)); |             message.edit(callback(page, true)); | ||||||
|         }; |         }; | ||||||
|         const BACKWARDS_EMOJI = "⬅️"; |  | ||||||
|         const FORWARDS_EMOJI = "➡️"; |  | ||||||
|         const handle = (emote: string, reacterID: string) => { |         const handle = (emote: string, reacterID: string) => { | ||||||
|             if (senderID === reacterID) { |             if (senderID === reacterID) { | ||||||
|                 switch (emote) { |                 switch (emote) { | ||||||
|  |                     case FIVE_BACKWARDS_EMOJI: | ||||||
|  |                         if (total > 5) turn(-5); | ||||||
|  |                         break; | ||||||
|                     case BACKWARDS_EMOJI: |                     case BACKWARDS_EMOJI: | ||||||
|                         turn(-1); |                         turn(-1); | ||||||
|                         break; |                         break; | ||||||
|                     case FORWARDS_EMOJI: |                     case FORWARDS_EMOJI: | ||||||
|                         turn(1); |                         turn(1); | ||||||
|                         break; |                         break; | ||||||
|  |                     case FIVE_FORWARDS_EMOJI: | ||||||
|  |                         if (total > 5) turn(5); | ||||||
|  |                         break; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         // Listen for reactions and call the handler.
 |         // Listen for reactions and call the handler.
 | ||||||
|  |         let backwardsReactionFive = total > 5 ? await message.react(FIVE_BACKWARDS_EMOJI) : null; | ||||||
|         let backwardsReaction = await message.react(BACKWARDS_EMOJI); |         let backwardsReaction = await message.react(BACKWARDS_EMOJI); | ||||||
|         let forwardsReaction = await message.react(FORWARDS_EMOJI); |         let forwardsReaction = await message.react(FORWARDS_EMOJI); | ||||||
|  |         let forwardsReactionFive = total > 5 ? await message.react(FIVE_FORWARDS_EMOJI) : null; | ||||||
|         unreactEventListeners.set(message.id, handle); |         unreactEventListeners.set(message.id, handle); | ||||||
|  | 
 | ||||||
|         const collector = message.createReactionCollector( |         const collector = message.createReactionCollector( | ||||||
|             (reaction, user) => { |             (reaction, user) => { | ||||||
|                 if (user.id === senderID) { |                 if (user.id === senderID) { | ||||||
|  | @ -88,8 +105,10 @@ export async function paginate( | ||||||
|         // When time's up, remove the bot's own reactions.
 |         // When time's up, remove the bot's own reactions.
 | ||||||
|         collector.on("end", () => { |         collector.on("end", () => { | ||||||
|             unreactEventListeners.delete(message.id); |             unreactEventListeners.delete(message.id); | ||||||
|  |             backwardsReactionFive?.users.remove(message.author); | ||||||
|             backwardsReaction.users.remove(message.author); |             backwardsReaction.users.remove(message.author); | ||||||
|             forwardsReaction.users.remove(message.author); |             forwardsReaction.users.remove(message.author); | ||||||
|  |             forwardsReactionFive?.users.remove(message.author); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,13 +1,14 @@ | ||||||
| import {Collection} from "discord.js"; | import {Collection} from "discord.js"; | ||||||
| import glob from "glob"; | import glob from "glob"; | ||||||
| import {Command, NamedCommand} from "./command"; | import {NamedCommand, CommandInfo} from "./command"; | ||||||
|  | import {toTitleCase} from "../lib"; | ||||||
| 
 | 
 | ||||||
| // Internally, it'll keep its original capitalization. It's up to you to convert it to title case when you make a help command.
 | // Internally, it'll keep its original capitalization. It's up to you to convert it to title case when you make a help command.
 | ||||||
| export const categories = new Collection<string, string[]>(); | const categories = new Collection<string, string[]>(); | ||||||
| 
 | 
 | ||||||
| /** Returns the cache of the commands if it exists and searches the directory if not. */ | /** Returns the cache of the commands if it exists and searches the directory if not. */ | ||||||
| export const loadableCommands = (async () => { | export const loadableCommands = (async () => { | ||||||
|     const commands = new Collection<string, Command>(); |     const commands = new Collection<string, NamedCommand>(); | ||||||
|     // Include all .ts files recursively in "src/commands/".
 |     // Include all .ts files recursively in "src/commands/".
 | ||||||
|     const files = await globP("src/commands/**/*.ts"); |     const files = await globP("src/commands/**/*.ts"); | ||||||
|     // Extract the usable parts from "src/commands/" if:
 |     // Extract the usable parts from "src/commands/" if:
 | ||||||
|  | @ -79,3 +80,53 @@ function globP(path: string) { | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Returns a list of categories and their associated commands. | ||||||
|  |  */ | ||||||
|  | export async function getCommandList(): Promise<Collection<string, NamedCommand[]>> { | ||||||
|  |     const list = new Collection<string, NamedCommand[]>(); | ||||||
|  |     const commands = await loadableCommands; | ||||||
|  | 
 | ||||||
|  |     for (const [category, headers] of categories) { | ||||||
|  |         const commandList: NamedCommand[] = []; | ||||||
|  |         for (const header of headers.filter((header) => header !== "test")) commandList.push(commands.get(header)!); | ||||||
|  |         // Ignore empty categories like "miscellaneous" (if it's empty).
 | ||||||
|  |         if (commandList.length > 0) list.set(toTitleCase(category), commandList); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return list; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Resolves a command based on the arguments given. | ||||||
|  |  * - Returns a string if there was an error. | ||||||
|  |  * - Returns a CommandInfo/category tuple if it was a success. | ||||||
|  |  */ | ||||||
|  | export async function getCommandInfo(args: string[]): Promise<[CommandInfo, string] | string> { | ||||||
|  |     // Use getCommandList() instead if you're just getting the list of all commands.
 | ||||||
|  |     if (args.length === 0) return "No arguments were provided!"; | ||||||
|  | 
 | ||||||
|  |     // Setup the root command
 | ||||||
|  |     const commands = await loadableCommands; | ||||||
|  |     let header = args.shift()!; | ||||||
|  |     const command = commands.get(header); | ||||||
|  |     if (!command || header === "test") return `No command found by the name \`${header}\`.`; | ||||||
|  |     if (!(command instanceof NamedCommand)) return "Command is not a proper instance of NamedCommand."; | ||||||
|  |     // If it's an alias, set the header to the original command name.
 | ||||||
|  |     if (command.name) header = command.name; | ||||||
|  | 
 | ||||||
|  |     // Search categories
 | ||||||
|  |     let category = "Unknown"; | ||||||
|  |     for (const [referenceCategory, headers] of categories) { | ||||||
|  |         if (headers.includes(header)) { | ||||||
|  |             category = toTitleCase(referenceCategory); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Gather info
 | ||||||
|  |     const result = await command.resolveInfo(args, header); | ||||||
|  |     if (result.type === "error") return result.message; | ||||||
|  |     else return [result, category]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -144,14 +144,13 @@ export abstract class GenericStructure { | ||||||
| 
 | 
 | ||||||
|     constructor(tag?: string) { |     constructor(tag?: string) { | ||||||
|         this.__meta__ = tag || this.__meta__; |         this.__meta__ = tag || this.__meta__; | ||||||
|  |         Object.defineProperty(this, "__meta__", { | ||||||
|  |             enumerable: false | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public save(asynchronous = true) { |     public save(asynchronous = true) { | ||||||
|         const tag = this.__meta__; |         FileManager.write(this.__meta__, this, asynchronous); | ||||||
|         /// @ts-ignore
 |  | ||||||
|         delete this.__meta__; |  | ||||||
|         FileManager.write(tag, this, asynchronous); |  | ||||||
|         this.__meta__ = tag; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -57,7 +57,7 @@ client.on("message", async (message) => { | ||||||
| 
 | 
 | ||||||
| export function extractFirstMessageLink(message: string): [string, string, string] | null { | export function extractFirstMessageLink(message: string): [string, string, string] | null { | ||||||
|     const messageLinkMatch = message.match( |     const messageLinkMatch = message.match( | ||||||
|         /([!<])?https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19})(>)?/ |         /([!<])?https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(\d{17,})\/(\d{17,})\/(\d{17,})(>)?/ | ||||||
|     ); |     ); | ||||||
|     if (messageLinkMatch === null) return null; |     if (messageLinkMatch === null) return null; | ||||||
|     const [, leftToken, guildID, channelID, messageID, rightToken] = messageLinkMatch; |     const [, leftToken, guildID, channelID, messageID, rightToken] = messageLinkMatch; | ||||||
|  |  | ||||||
|  | @ -91,15 +91,13 @@ class StorageStructure extends GenericStructure { | ||||||
|         super("storage"); |         super("storage"); | ||||||
|         this.users = {}; |         this.users = {}; | ||||||
|         this.guilds = {}; |         this.guilds = {}; | ||||||
| 
 |         for (let id in data.users) if (/\d{17,}/g.test(id)) this.users[id] = new User(data.users[id]); | ||||||
|         for (let id in data.users) if (/\d{17,19}/g.test(id)) this.users[id] = new User(data.users[id]); |         for (let id in data.guilds) if (/\d{17,}/g.test(id)) this.guilds[id] = new Guild(data.guilds[id]); | ||||||
| 
 |  | ||||||
|         for (let id in data.guilds) if (/\d{17,19}/g.test(id)) this.guilds[id] = new Guild(data.guilds[id]); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** Gets a user's profile if they exist and generate one if not. */ |     /** Gets a user's profile if they exist and generate one if not. */ | ||||||
|     public getUser(id: string): User { |     public getUser(id: string): User { | ||||||
|         if (!/\d{17,19}/g.test(id)) |         if (!/\d{17,}/g.test(id)) | ||||||
|             console.warn(`"${id}" is not a valid user ID! It will be erased when the data loads again.`); |             console.warn(`"${id}" is not a valid user ID! It will be erased when the data loads again.`); | ||||||
| 
 | 
 | ||||||
|         if (id in this.users) return this.users[id]; |         if (id in this.users) return this.users[id]; | ||||||
|  | @ -112,7 +110,7 @@ class StorageStructure extends GenericStructure { | ||||||
| 
 | 
 | ||||||
|     /** Gets a guild's settings if they exist and generate one if not. */ |     /** Gets a guild's settings if they exist and generate one if not. */ | ||||||
|     public getGuild(id: string): Guild { |     public getGuild(id: string): Guild { | ||||||
|         if (!/\d{17,19}/g.test(id)) |         if (!/\d{17,}/g.test(id)) | ||||||
|             console.warn(`"${id}" is not a valid guild ID! It will be erased when the data loads again.`); |             console.warn(`"${id}" is not a valid guild ID! It will be erased when the data loads again.`); | ||||||
| 
 | 
 | ||||||
|         if (id in this.guilds) return this.guilds[id]; |         if (id in this.guilds) return this.guilds[id]; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue