├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── src ├── ContextMenuBase.ts ├── CustomErrors.ts ├── MiddlewarePipeline.ts ├── OptionTypes.ts ├── Page.ts ├── PageComponents.ts ├── PingableTimedCache.ts ├── SlashCommandBase.ts ├── SlashasaurusClient.ts ├── TemplateModal.ts ├── index.ts └── utilityTypes.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | 4 | .env -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | .prettierrc 3 | .tool-versions 4 | tsconfig.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 80 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Rodentman87 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Slashasaurus

4 |
5 |

6 | Discord server 7 | npm version 8 | Gitbook 9 |

10 |
11 | 12 | ## About 13 | 14 | Slashasaurus is a framework built on top of Discord.js. It's inspired by React and Next.JS, so if you've used either before, this will feel kinda familiar to you. 15 | 16 | It is _strongly_ recommended that you use [TypeScript](https://www.typescriptlang.org/) with this library, however, it is not a requirement. The quick start is written in TypeScript, most information should be very similar for vanilla JS. 17 | 18 | ## Installation 19 | 20 | To start a new project with Slashasaurus, you need to install discord.js as well as slashasaurus. 21 | 22 | ```sh 23 | npm install --save discord.js slashasaurus 24 | 25 | # or 26 | 27 | yarn add discord.js slashasaurus 28 | ``` 29 | 30 | Alternatively, you can use [create-slashasaurus-app](https://www.npmjs.com/package/create-slashasaurus-app) to generate the boilerplate for you. 31 | 32 | ```sh 33 | npx create-slashasaurus-app 34 | 35 | # or 36 | 37 | yarn create slashasaurus-app 38 | ``` 39 | 40 | See [discord.js's readme](https://github.com/discordjs/discord.js#optional-packages) for more info about optional packages. 41 | 42 | ## Docs 43 | 44 | [View the docs here!](https://rodentman87.gitbook.io/slashasaurus/) 45 | 46 | ## Latest Changelogs 47 | 48 | Check out the [releases on GitHub](https://github.com/Rodentman87/slashasaurus/releases) for the latest changelogs. 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slashasaurus", 3 | "version": "0.13.0-beta", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rimraf ./dist", 9 | "build": "tsc" 10 | }, 11 | "devDependencies": { 12 | "@types/node": "^17.0.21", 13 | "@typescript-eslint/eslint-plugin": "^5.27.0", 14 | "@typescript-eslint/parser": "^5.27.0", 15 | "discord.js": "14.17.3", 16 | "eslint": "^8.16.0", 17 | "prettier": "^3.4.2", 18 | "rimraf": "^3.0.2", 19 | "typescript": "^5.0.2" 20 | }, 21 | "peerDependencies": { 22 | "discord.js": "^14.17.3" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/Rodentman87/slashasaurus.git" 27 | }, 28 | "dependencies": { 29 | "@discordjs/rest": "^2.4.2", 30 | "discord-api-types": "^0.37.36" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ContextMenuBase.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | MessageContextMenuCommandInteraction, 3 | UserContextMenuCommandInteraction, 4 | } from 'discord.js'; 5 | import type { LocalizationMap } from 'discord-api-types/v10'; 6 | import { SlashasaurusClient } from './SlashasaurusClient'; 7 | 8 | type ContextCommandOptions = { 9 | name: string; 10 | nameLocalizations?: LocalizationMap; 11 | type: T; 12 | defaultMemberPermissions?: string | number | bigint; 13 | dmPermission?: boolean; 14 | }; 15 | 16 | export type ContextMenuHandlerType = 17 | T extends 'MESSAGE' 18 | ? ( 19 | interaction: MessageContextMenuCommandInteraction, 20 | client: SlashasaurusClient 21 | ) => void 22 | : ( 23 | interaction: UserContextMenuCommandInteraction, 24 | client: SlashasaurusClient 25 | ) => void; 26 | 27 | export function isMessageCommand(thing: unknown): thing is MessageCommand { 28 | return thing instanceof MessageCommand; 29 | } 30 | 31 | export class MessageCommand { 32 | commandInfo: ContextCommandOptions<'MESSAGE'>; 33 | 34 | /** 35 | * 36 | * @param commandInfo The general info for the command 37 | * @param handlers 38 | */ 39 | constructor( 40 | commandInfo: Omit, 'type'>, 41 | run: ContextMenuHandlerType<'MESSAGE'> 42 | ) { 43 | this.commandInfo = { 44 | ...commandInfo, 45 | type: 'MESSAGE', 46 | }; 47 | this.run = run; 48 | } 49 | 50 | run: ContextMenuHandlerType<'MESSAGE'>; 51 | } 52 | 53 | export function isUserCommand(thing: unknown): thing is UserCommand { 54 | return thing instanceof UserCommand; 55 | } 56 | 57 | export class UserCommand { 58 | commandInfo: ContextCommandOptions<'USER'>; 59 | 60 | /** 61 | * 62 | * @param commandInfo The general info for the command 63 | * @param handlers 64 | */ 65 | constructor( 66 | commandInfo: Omit, 'type'>, 67 | run: ContextMenuHandlerType<'USER'> 68 | ) { 69 | this.commandInfo = { 70 | ...commandInfo, 71 | type: 'USER', 72 | }; 73 | this.run = run; 74 | } 75 | 76 | run: ContextMenuHandlerType<'USER'>; 77 | } 78 | -------------------------------------------------------------------------------- /src/CustomErrors.ts: -------------------------------------------------------------------------------- 1 | export class ValidationError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/MiddlewarePipeline.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export type Middleware any> = ( 3 | ...args: [...Parameters, () => Promise] 4 | ) => void; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | export class Pipeline any> { 8 | stack: Array>; 9 | 10 | constructor() { 11 | this.stack = []; 12 | } 13 | 14 | push(fn: Middleware) { 15 | this.stack.push(fn); 16 | } 17 | 18 | async execute(lastFunction: T, ...args: Parameters) { 19 | let previousIndex = -1; 20 | 21 | const runner = async (index: number) => { 22 | if (index === previousIndex) { 23 | throw new Error(`next() can only be called once per middleware`); 24 | } 25 | 26 | if (index >= this.stack.length) { 27 | // We've reached the end, run the last function 28 | await lastFunction(...args); 29 | } 30 | 31 | previousIndex = index; 32 | 33 | const middleware = this.stack[index]; 34 | 35 | if (middleware) { 36 | await middleware(...args, () => { 37 | return runner(index + 1); 38 | }); 39 | } 40 | }; 41 | 42 | await runner(0); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/OptionTypes.ts: -------------------------------------------------------------------------------- 1 | import type { LocalizationMap } from 'discord-api-types/v9'; 2 | import { ApplicationCommandOptionType } from 'discord-api-types/v10'; 3 | import { 4 | AutocompleteInteraction, 5 | CommandInteraction, 6 | ChannelType, 7 | } from 'discord.js'; 8 | import { SlashasaurusClient } from './SlashasaurusClient'; 9 | import { MaybePromise, OptionsMap } from './utilityTypes'; 10 | 11 | interface BaseApplicationCommandOptionsData { 12 | /** 13 | * The name of the option. 14 | */ 15 | readonly name: string; 16 | /** 17 | * An object of localizations for the option name. 18 | */ 19 | readonly nameLocalizations?: LocalizationMap; 20 | /** 21 | * The description of the option. 22 | */ 23 | readonly description: string; 24 | /** 25 | * An object of localizations for the option description. 26 | */ 27 | readonly descriptionLocalizations?: LocalizationMap; 28 | /** 29 | * Whether this option is required. 30 | */ 31 | readonly required?: boolean; 32 | readonly autocomplete?: never; 33 | /** 34 | * A custom validator for the option. This will only run before the command's run handler is called. 35 | * If the validator returns a string or throws a {@link ValidationError}, that will be used as the error message. 36 | * All errors will be displayed to the user as a list. 37 | */ 38 | readonly validator?: ( 39 | interaction: CommandInteraction, 40 | value: T 41 | ) => MaybePromise; 42 | } 43 | 44 | interface ApplicationCommandOptionChoiceData { 45 | name: string; 46 | nameLocalizations?: LocalizationMap; 47 | value: T; 48 | } 49 | 50 | // STRING 51 | type StringChoiceResolvableType = 52 | | ApplicationCommandOptionType.String 53 | | 'STRING'; 54 | 55 | interface StringOptionsData extends BaseApplicationCommandOptionsData { 56 | readonly type: StringChoiceResolvableType; 57 | readonly minLength?: number; 58 | readonly maxLength?: number; 59 | } 60 | 61 | interface StringChoiceOptionsData 62 | extends Omit, 'autocomplete'> { 63 | readonly type: StringChoiceResolvableType; 64 | readonly choices: 65 | | readonly [ 66 | ApplicationCommandOptionChoiceData, 67 | ...ApplicationCommandOptionChoiceData[] 68 | ]; 69 | readonly autocomplete?: false; 70 | } 71 | 72 | interface StringAutocompleteOptionsData 73 | extends Omit, 'autocomplete'> { 74 | readonly type: StringChoiceResolvableType; 75 | readonly autocomplete: true; 76 | readonly onAutocomplete?: ( 77 | interaction: AutocompleteInteraction, 78 | value: string, 79 | client: SlashasaurusClient 80 | ) => void; 81 | readonly transformer?: (value: string) => unknown; 82 | } 83 | 84 | // INTEGER 85 | type IntegerChoiceResolvableType = 86 | | ApplicationCommandOptionType.Integer 87 | | 'INTEGER'; 88 | 89 | interface IntegerOptionsData 90 | extends Omit, 'autocomplete'> { 91 | readonly type: IntegerChoiceResolvableType; 92 | readonly minValue?: number; 93 | readonly maxValue?: number; 94 | readonly autocomplete?: false; 95 | } 96 | 97 | interface IntegerChoiceOptionsData 98 | extends Omit, 'autocomplete'> { 99 | readonly type: IntegerChoiceResolvableType; 100 | readonly choices: 101 | | readonly [ 102 | ApplicationCommandOptionChoiceData, 103 | ...ApplicationCommandOptionChoiceData[] 104 | ]; 105 | readonly autocomplete?: false; 106 | } 107 | 108 | interface IntegerAutocompleteOptionsData 109 | extends Omit, 'autocomplete'> { 110 | readonly type: IntegerChoiceResolvableType; 111 | readonly autocomplete: true; 112 | readonly onAutocomplete?: ( 113 | interaction: AutocompleteInteraction, 114 | value: number, 115 | client: SlashasaurusClient 116 | ) => void; 117 | readonly transformer?: (value: number) => unknown; 118 | } 119 | 120 | // BOOLEAN 121 | type BooleanChoiceResolvableType = 122 | | ApplicationCommandOptionType.Boolean 123 | | 'BOOLEAN'; 124 | 125 | interface BooleanOptionsData 126 | extends BaseApplicationCommandOptionsData { 127 | readonly type: BooleanChoiceResolvableType; 128 | } 129 | 130 | // USER 131 | type UserChoiceResolvableType = ApplicationCommandOptionType.User | 'USER'; 132 | 133 | interface UserOptionsData 134 | extends BaseApplicationCommandOptionsData { 135 | readonly type: UserChoiceResolvableType; 136 | } 137 | 138 | // CHANNEL 139 | type ChannelChoiceResolvableType = 140 | | ApplicationCommandOptionType.Channel 141 | | 'CHANNEL'; 142 | 143 | type ValidChannelTypes = 144 | | ChannelType.GuildText 145 | | ChannelType.GuildVoice 146 | | ChannelType.GuildCategory 147 | | ChannelType.GuildNews 148 | | ChannelType.GuildNewsThread 149 | | ChannelType.GuildPublicThread 150 | | ChannelType.GuildPrivateThread 151 | | ChannelType.GuildStageVoice 152 | | ChannelType.GuildForum; 153 | 154 | interface ChannelOptionsData 155 | extends BaseApplicationCommandOptionsData { 156 | readonly type: ChannelChoiceResolvableType; 157 | readonly channelTypes?: ReadonlyArray; 158 | } 159 | 160 | // ROLE 161 | type RoleChoiceResolvableType = ApplicationCommandOptionType.Role | 'ROLE'; 162 | 163 | interface RoleOptionsData 164 | extends BaseApplicationCommandOptionsData { 165 | readonly type: RoleChoiceResolvableType; 166 | } 167 | 168 | // MENTIONABLE 169 | type MentionableChoiceResolvableType = 170 | | ApplicationCommandOptionType.Mentionable 171 | | 'MENTIONABLE'; 172 | 173 | interface MentionableOptionsData 174 | extends BaseApplicationCommandOptionsData { 175 | readonly type: MentionableChoiceResolvableType; 176 | } 177 | 178 | // NUMBER 179 | type NumberChoiceResolvableType = 180 | | ApplicationCommandOptionType.Number 181 | | 'NUMBER'; 182 | 183 | interface NumberOptionsData 184 | extends Omit, 'autocomplete'> { 185 | readonly type: NumberChoiceResolvableType; 186 | readonly minValue?: number; 187 | readonly maxValue?: number; 188 | readonly autocomplete?: false; 189 | } 190 | 191 | interface NumberChoiceOptionsData 192 | extends Omit, 'autocomplete'> { 193 | readonly type: NumberChoiceResolvableType; 194 | readonly choices: 195 | | readonly [ 196 | ApplicationCommandOptionChoiceData, 197 | ...ApplicationCommandOptionChoiceData[] 198 | ]; 199 | readonly autocomplete?: false; 200 | } 201 | 202 | interface NumberAutocompleteOptionsData 203 | extends Omit, 'autocomplete'> { 204 | readonly type: NumberChoiceResolvableType; 205 | readonly autocomplete: true; 206 | readonly onAutocomplete?: ( 207 | interaction: AutocompleteInteraction, 208 | value: number, 209 | client: SlashasaurusClient 210 | ) => void; 211 | readonly transformer?: (value: number) => unknown; 212 | } 213 | 214 | // ATTACHMENT 215 | type AttachmentChoiceResolvableType = 216 | | ApplicationCommandOptionType.Attachment 217 | | 'ATTACHMENT'; 218 | 219 | interface AttachmentOptionsData 220 | extends BaseApplicationCommandOptionsData { 221 | readonly type: AttachmentChoiceResolvableType; 222 | } 223 | 224 | export type ApplicationCommandOptionData = 225 | | StringOptionsData 226 | | StringChoiceOptionsData 227 | | StringAutocompleteOptionsData 228 | | IntegerOptionsData 229 | | IntegerChoiceOptionsData 230 | | IntegerAutocompleteOptionsData 231 | | BooleanOptionsData 232 | | UserOptionsData 233 | | ChannelOptionsData 234 | | RoleOptionsData 235 | | MentionableOptionsData 236 | | NumberOptionsData 237 | | NumberChoiceOptionsData 238 | | NumberAutocompleteOptionsData 239 | | AttachmentOptionsData; 240 | 241 | export type OptionsDataArray = 242 | | readonly [ApplicationCommandOptionData, ...ApplicationCommandOptionData[]] 243 | | readonly []; 244 | -------------------------------------------------------------------------------- /src/Page.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from '@discordjs/builders'; 2 | import { 3 | ActionRowBuilder, 4 | APIEmbed, 5 | ButtonInteraction, 6 | ChannelSelectMenuInteraction, 7 | CommandInteraction, 8 | Embed, 9 | ForumChannel, 10 | InteractionWebhook, 11 | MentionableSelectMenuInteraction, 12 | Message, 13 | MessageActionRowComponentBuilder, 14 | MessageComponentInteraction, 15 | MessageCreateOptions, 16 | MessagePayload, 17 | RoleSelectMenuInteraction, 18 | SelectMenuInteraction, 19 | SendableChannels, 20 | UserSelectMenuInteraction, 21 | WebhookMessageEditOptions, 22 | } from 'discord.js'; 23 | import { PageActionRow, PageButton, PageSelect } from './PageComponents'; 24 | import { SlashasaurusClient } from './SlashasaurusClient'; 25 | import { MaybePromise } from './utilityTypes'; 26 | 27 | interface SerializedObject { 28 | type: string; 29 | identifier: SerializableValue; 30 | } 31 | 32 | type SerializableValue = 33 | | null 34 | | string 35 | | number 36 | | SerializedObject 37 | | Array; 38 | 39 | export type PageButtonRow = PageButton[]; 40 | export type PageSelectRow = [PageSelect]; 41 | 42 | export type PageComponentArray = PageButtonRow | PageSelectRow; 43 | 44 | type PageComponentRows = (PageComponentArray | PageActionRow)[]; 45 | 46 | export interface RenderedPage 47 | extends Omit { 48 | content?: string | null; 49 | components?: PageComponentRows; 50 | embeds?: (APIEmbed | EmbedBuilder)[]; 51 | } 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 54 | export function isPage(thing: any): thing is Page['constructor'] { 55 | if (!thing.prototype) return false; 56 | return thing.prototype instanceof Page; 57 | } 58 | 59 | export const DEFAULT_PAGE_ID = 'DEFAULT_PAGE_ID'; 60 | 61 | export class PageInteractionReplyMessage { 62 | constructor( 63 | public webhook: InteractionWebhook, 64 | public id: string, 65 | ) {} 66 | 67 | async edit(options: string | MessagePayload | WebhookMessageEditOptions) { 68 | await this.webhook.editMessage(this.id, options); 69 | } 70 | 71 | async delete() { 72 | await this.webhook.deleteMessage(this.id); 73 | } 74 | } 75 | 76 | interface PageStatic { 77 | new (): Page; 78 | pageId: string; 79 | _client: SlashasaurusClient; 80 | deserializeState: DeserializeStateFn; 81 | } 82 | 83 | export interface Page

, S = Record> { 84 | constructor: PageStatic; 85 | render(): RenderedPage | Promise; 86 | pageDidSend?(): void | Promise; 87 | pageWillLeaveCache?(): void | Promise; 88 | } 89 | export abstract class Page< 90 | P = Record, 91 | S = Record, 92 | > { 93 | state: Readonly; 94 | readonly props: Readonly

; 95 | readonly client: SlashasaurusClient; 96 | // eslint-disable-next-line @typescript-eslint/ban-types 97 | handlers: Map; 98 | nextId: number; 99 | message: Message | PageInteractionReplyMessage | null; 100 | static pageId = DEFAULT_PAGE_ID; 101 | latestInteraction: MessageComponentInteraction | null = null; 102 | 103 | constructor(props: P) { 104 | if (!this.constructor._client) 105 | throw new Error( 106 | "This page hasn't been registered with your client, make sure that it's being registered correctly", 107 | ); 108 | this.props = props; 109 | this.message = null; 110 | this.client = this.constructor._client; 111 | this.handlers = new Map(); 112 | } 113 | 114 | /** 115 | * Call this function to update the state of the Page, this will trigger a re-render and a message edit. 116 | * 117 | * **IMPORTANT**: This *can* fail if the bot is unable to edit the message due to no longer seeing the channel. It will throw and exception if this is the case. 118 | * @param nextState Either the changes you want to make to the state, or a funciton that takes the current state and returns the new state 119 | */ 120 | async setState( 121 | nextState: 122 | | ((prevState: Readonly, props: Readonly

) => Pick | S | null) 123 | | (Pick | S | null), 124 | ): Promise { 125 | if (!this.message) 126 | throw new Error('You cannot update the state of a Page before it sends'); 127 | if (nextState instanceof Function) { 128 | // Run the function to get the new state values 129 | nextState = nextState(this.state, this.props); 130 | } 131 | const newState = { 132 | ...this.state, 133 | }; 134 | Object.assign(newState, nextState); 135 | await this.client.updatePage(this, newState); 136 | return; 137 | } 138 | 139 | sendToChannel(channel: SendableChannels) { 140 | return this.client.sendPageToChannel(this, channel); 141 | } 142 | 143 | sendAsForumPost(channel: ForumChannel, postTitle: string) { 144 | return this.client.sendPageToForumChannel(this, postTitle, channel); 145 | } 146 | 147 | sendAsReply( 148 | interaction: MessageComponentInteraction | CommandInteraction, 149 | ephemeral = false, 150 | ) { 151 | return this.client.replyToInteractionWithPage(this, interaction, ephemeral); 152 | } 153 | 154 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 155 | async transitionTo(newPage: Page) { 156 | newPage.message = this.message; 157 | newPage.latestInteraction = this.latestInteraction; 158 | await this.client.updatePage(newPage, newPage.state); 159 | newPage.pageDidSend?.(); 160 | return; 161 | } 162 | 163 | /** 164 | * This function serializes the current props and state of the page. 165 | * The string you return will be passed to `deserializeState()` 166 | * when loading this page if it left memory. This will be called 167 | * *every time the state changes* so that the state will be 168 | * preserved if the bot suddenly goes offline. 169 | */ 170 | abstract serializeState(): string; 171 | 172 | handleId( 173 | id: string, 174 | interaction: 175 | | ButtonInteraction 176 | | SelectMenuInteraction 177 | | UserSelectMenuInteraction 178 | | RoleSelectMenuInteraction 179 | | ChannelSelectMenuInteraction 180 | | MentionableSelectMenuInteraction, 181 | ) { 182 | const handler = this.handlers.get(id); 183 | if (handler) { 184 | handler(interaction); 185 | } else { 186 | throw new Error('Handler not registered for this component'); 187 | } 188 | } 189 | 190 | clearHandlers() { 191 | this.handlers.clear(); 192 | this.nextId = 0; 193 | } 194 | 195 | // eslint-disable-next-line @typescript-eslint/ban-types 196 | registerHandler(handler: Function) { 197 | const id = this.nextId; 198 | this.nextId++; 199 | this.handlers.set(id.toString(), handler); 200 | return id; 201 | } 202 | } 203 | 204 | /** 205 | * This function will take a serialized state and set the props 206 | * and state of the page back to what they were when it was serialized. 207 | * 208 | * ***IMPORTANT***: this function *WILL* sometimes receive state from previous 209 | * versions of this page if you change the information returned by `serializeState()` 210 | * You *WILL* need to handle this or you will have improper state. If possible 211 | * you should attempt to convert the old version to the new version. If not, you 212 | * should delete this old page and prompt the user on how to re-open it. 213 | * 214 | * @param serializedState The string that was returned previously by `serializeState()` 215 | * @param interaction The interaction that triggered this page to wake up 216 | */ 217 | export type DeserializeStateFn< 218 | P = Record, 219 | S = Record, 220 | > = ( 221 | serializedState: string, 222 | interaction?: MessageComponentInteraction | CommandInteraction, 223 | ) => MaybePromise< 224 | | { 225 | props: P; 226 | state: S; 227 | } 228 | | Record 229 | >; 230 | 231 | export function pageComponentRowsToComponents( 232 | rows: PageComponentRows, 233 | page: Page, 234 | ): ActionRowBuilder[] { 235 | page.clearHandlers(); 236 | const pageId = page.constructor.pageId; 237 | return rows 238 | .map((row) => { 239 | const actionRow = 240 | new ActionRowBuilder(); 241 | if (row instanceof PageActionRow) { 242 | row.children.forEach((component) => { 243 | actionRow.addComponents( 244 | componentToDjsComponent(component, page, pageId), 245 | ); 246 | }); 247 | } else if (Array.isArray(row)) { 248 | row.forEach((component) => { 249 | actionRow.addComponents( 250 | componentToDjsComponent(component, page, pageId), 251 | ); 252 | }); 253 | } else { 254 | return null; 255 | } 256 | return actionRow; 257 | }) 258 | .filter>( 259 | (e): e is ActionRowBuilder => 260 | e instanceof ActionRowBuilder, 261 | ); 262 | } 263 | 264 | function componentToDjsComponent( 265 | component: PageComponentArray[number], 266 | page: Page, 267 | pageId: string, 268 | ) { 269 | if ('handler' in component) { 270 | return component.toDjsComponent( 271 | `~${pageId};${page.registerHandler(component.handler)}`, 272 | ); 273 | } else { 274 | return component.toDjsComponent(); 275 | } 276 | } 277 | 278 | export function compareMessages( 279 | a: MessageComponentInteraction['message'], 280 | b: RenderedPage, 281 | ) { 282 | if (a.content !== (b.content ?? '')) return false; 283 | 284 | const bComponents = b.components?.filter( 285 | (c) => Array.isArray(c) || c instanceof PageActionRow, 286 | ); 287 | 288 | // Check Components 289 | if ( 290 | a.components && 291 | bComponents && 292 | a.components.length === bComponents.length 293 | ) { 294 | // They both have components, lets compare them 295 | const componentsMatch = [...a.components].every((row, index) => { 296 | const bRow = bComponents[index]; 297 | const bChildren = bRow instanceof PageActionRow ? bRow.children : bRow; 298 | if (row.components.length !== bChildren.length) return false; 299 | return [...bChildren].every((component, index) => { 300 | return component.compareToComponent(row.components[index]); 301 | }); 302 | }); 303 | if (!componentsMatch) return false; 304 | } else if (a.components || bComponents) { 305 | // One has components but the other doesn't 306 | return false; 307 | } 308 | 309 | // Check Embeds 310 | if ( 311 | a.embeds.filter((e) => e.data.type === 'rich').length !== 312 | (b.embeds ?? []).length 313 | ) 314 | return false; 315 | if (a.embeds.length > 0) { 316 | if ( 317 | !(b.embeds ?? []).every((bEmbedData, index) => { 318 | const bEmbed = 'data' in bEmbedData ? bEmbedData.data : bEmbedData; 319 | return embedsAreEqual(a.embeds[index], bEmbed); 320 | }) 321 | ) { 322 | return false; 323 | } 324 | } 325 | 326 | return true; 327 | } 328 | 329 | function embedsAreEqual(a: Embed, b: APIEmbed) { 330 | if ( 331 | a.title !== (b.title ? b.title.trim() : b.title) || 332 | a.description !== (b.description ? b.description.trim() : b.description) || 333 | a.url !== b.url || 334 | (a.color ?? 0) !== b.color 335 | ) 336 | return false; 337 | 338 | // Compare timestamps 339 | if (a.timestamp && b.timestamp) { 340 | if (new Date(a.timestamp).getTime() !== new Date(b.timestamp).getTime()) 341 | return false; 342 | } else if (a.timestamp || b.timestamp) return false; 343 | 344 | // Compare authors 345 | const headerIconUrl = 346 | a.author && ('iconURL' in a.author ? a.author.iconURL : undefined); 347 | if ( 348 | a.author && 349 | b.author && 350 | (a.author.name !== b.author.name?.trim() || 351 | headerIconUrl !== b.author.icon_url || 352 | a.author.url !== b.author.url) 353 | ) 354 | return false; 355 | else if ((a.author && !b.author) || (b.author && !a.author)) return false; 356 | 357 | // Compare footers 358 | const footerIconUrl = 359 | a.footer && ('iconURL' in a.footer ? a.footer.iconURL : undefined); 360 | if ( 361 | a.footer && 362 | b.footer && 363 | (a.footer?.text !== b.footer?.text?.trim() || 364 | footerIconUrl !== b.footer?.icon_url) 365 | ) 366 | return false; 367 | else if ((a.footer && !b.footer) || (b.footer && !a.footer)) return false; 368 | 369 | // Compare images 370 | if ( 371 | (a.image && !b.image) || 372 | (b.image && !a.image) || 373 | a.image?.url !== b.image?.url 374 | ) 375 | return false; 376 | if ( 377 | (a.thumbnail && !b.thumbnail) || 378 | (b.thumbnail && !a.thumbnail) || 379 | a.thumbnail?.url !== b.thumbnail?.url 380 | ) 381 | return false; 382 | 383 | // Compare fields 384 | const aFields = a.fields ?? []; 385 | const bFields = b.fields ?? []; 386 | if (aFields.length !== bFields.length) return false; 387 | return aFields.every( 388 | (f, i) => 389 | f.inline === bFields[i].inline && 390 | f.name === bFields[i].name?.trim() && 391 | f.value === bFields[i].value?.trim(), 392 | ); 393 | } 394 | -------------------------------------------------------------------------------- /src/PageComponents.ts: -------------------------------------------------------------------------------- 1 | import { SelectMenuBuilder } from '@discordjs/builders'; 2 | import { 3 | ButtonBuilder, 4 | ButtonInteraction, 5 | ButtonStyle, 6 | ComponentEmojiResolvable, 7 | ComponentType, 8 | MessageComponentInteraction, 9 | SelectMenuInteraction, 10 | parseEmoji, 11 | SelectMenuOptionBuilder, 12 | UserSelectMenuBuilder, 13 | UserSelectMenuComponentData, 14 | ChannelSelectMenuBuilder, 15 | MentionableSelectMenuBuilder, 16 | RoleSelectMenuBuilder, 17 | RoleSelectMenuComponentData, 18 | UserSelectMenuInteraction, 19 | RoleSelectMenuInteraction, 20 | ChannelSelectMenuComponentData, 21 | ChannelSelectMenuInteraction, 22 | MentionableSelectMenuComponentData, 23 | MentionableSelectMenuInteraction, 24 | ChannelType, 25 | APISelectMenuOption, 26 | } from 'discord.js'; 27 | 28 | type NonLinkStyles = 29 | | ButtonStyle.Danger 30 | | ButtonStyle.Secondary 31 | | ButtonStyle.Primary 32 | | ButtonStyle.Success; 33 | 34 | type PageButtonLabelOptions = 35 | | { 36 | label: string; 37 | emoji?: ComponentEmojiResolvable; 38 | } 39 | | { 40 | label?: string; 41 | emoji: ComponentEmojiResolvable; 42 | }; 43 | 44 | type PotentialDjsComponent = NonNullable< 45 | MessageComponentInteraction['message']['components'] 46 | >[number]['components'][number]; 47 | 48 | interface ExportableToDjsComponent { 49 | toDjsComponent( 50 | id: string 51 | ): 52 | | ButtonBuilder 53 | | SelectMenuBuilder 54 | | UserSelectMenuBuilder 55 | | RoleSelectMenuBuilder 56 | | ChannelSelectMenuBuilder 57 | | MentionableSelectMenuBuilder; 58 | } 59 | 60 | export function createInteractable

( 61 | component: new (props: P) => unknown | ((props: P) => unknown) | null, 62 | props: P, 63 | ...children: unknown[] 64 | ) { 65 | if (!component) return children; 66 | try { 67 | return new component({ 68 | ...props, 69 | children: children.length > 1 ? children : children[0], 70 | }); 71 | } catch (e) { 72 | // @ts-expect-error this should work fine, this is like the only way to check that the component is a function 73 | return component({ 74 | ...props, 75 | children: children.length > 1 ? children : children[0], 76 | }); 77 | } 78 | } 79 | 80 | type PageActionRowChild = PageButton | PageSelect; 81 | 82 | interface PageActionRowProps { 83 | children?: (PageActionRowChild | false) | (PageActionRowChild | false)[]; 84 | } 85 | 86 | export class PageActionRow { 87 | children: PageActionRowChild[]; 88 | 89 | constructor({ children }: PageActionRowProps) { 90 | if (Array.isArray(children)) 91 | this.children = children 92 | .flat() 93 | .filter( 94 | (c): c is PageActionRowChild => 95 | c instanceof PageInteractableButton || 96 | c instanceof PageLinkButton || 97 | c instanceof PageSelect 98 | ); 99 | else if (children) this.children = [children]; 100 | } 101 | } 102 | 103 | export type PageInteractableButtonOptions = { 104 | handler: (interaction: ButtonInteraction) => void; 105 | style?: NonLinkStyles; 106 | disabled?: boolean; 107 | } & PageButtonLabelOptions; 108 | 109 | export type PageLinkButtonOptions = { 110 | url: string; 111 | disabled?: boolean; 112 | } & PageButtonLabelOptions; 113 | 114 | export class PageInteractableButton implements ExportableToDjsComponent { 115 | type: ComponentType.Button = ComponentType.Button; 116 | handler: (interaction: ButtonInteraction) => void; 117 | style: NonLinkStyles = ButtonStyle.Secondary; 118 | disabled = false; 119 | label?: string; 120 | emoji?: ComponentEmojiResolvable; 121 | 122 | constructor(options: PageInteractableButtonOptions) { 123 | this.handler = options.handler; 124 | if (options.style) this.style = options.style; 125 | if (options.disabled) this.disabled = options.disabled; 126 | if (options.label) this.label = options.label; 127 | if (options.emoji) this.emoji = options.emoji; 128 | } 129 | 130 | toDjsComponent(id: string): ButtonBuilder { 131 | const builder = new ButtonBuilder({ 132 | style: this.style, 133 | disabled: this.disabled, 134 | customId: id, 135 | }); 136 | if (this.label) builder.setLabel(this.label); 137 | if (this.emoji) builder.setEmoji(this.emoji); 138 | return builder; 139 | } 140 | 141 | compareToComponent(component: PotentialDjsComponent) { 142 | if (!(component.type === ComponentType.Button)) return false; 143 | if ((this.emoji && !component.emoji) || (!this.emoji && component.emoji)) 144 | return false; 145 | if (this.emoji && component.emoji) { 146 | if (!compareEmoji(this.emoji, component.emoji)) return false; 147 | } 148 | return ( 149 | this.style === component.style && 150 | this.disabled === (component.disabled ?? false) && 151 | (this.label ?? null) === component.label 152 | ); 153 | } 154 | } 155 | 156 | export class PageLinkButton implements ExportableToDjsComponent { 157 | type: ComponentType.Button = ComponentType.Button; 158 | url: string; 159 | disabled = false; 160 | label?: string; 161 | emoji?: ComponentEmojiResolvable; 162 | 163 | constructor(options: PageLinkButtonOptions) { 164 | this.url = options.url; 165 | if (options.disabled) this.disabled = options.disabled; 166 | if (options.label) this.label = options.label; 167 | if (options.emoji) this.emoji = options.emoji; 168 | } 169 | 170 | toDjsComponent(): ButtonBuilder { 171 | const builder = new ButtonBuilder({ 172 | style: ButtonStyle.Link, 173 | disabled: this.disabled, 174 | url: this.url, 175 | }); 176 | if (this.label) builder.setLabel(this.label); 177 | if (this.emoji) builder.setEmoji(this.emoji); 178 | return builder; 179 | } 180 | 181 | compareToComponent(component: PotentialDjsComponent) { 182 | if (!(component.type === ComponentType.Button)) return false; 183 | if ((this.emoji && !component.emoji) || (!this.emoji && component.emoji)) 184 | return false; 185 | if (this.emoji && component.emoji) { 186 | if (!compareEmoji(this.emoji, component.emoji)) return false; 187 | } 188 | return ( 189 | ButtonStyle.Link === component.style && 190 | this.disabled === component.disabled && 191 | (this.label ?? null) === component.label && 192 | this.url === component.url 193 | ); 194 | } 195 | } 196 | 197 | export type PageButton = PageInteractableButton | PageLinkButton; 198 | 199 | export interface PageSelectOptions { 200 | handler: (interaction: SelectMenuInteraction) => void; 201 | options: APISelectMenuOption[] | SelectMenuOptionBuilder[]; 202 | placeholder?: string; 203 | minValues?: number; 204 | maxValues?: number; 205 | disabled?: boolean; 206 | } 207 | 208 | /** 209 | * @deprecated Use PageStringSelect instead 210 | */ 211 | export class PageSelect implements ExportableToDjsComponent { 212 | type: ComponentType.StringSelect = ComponentType.StringSelect; 213 | handler: (interaction: SelectMenuInteraction) => void; 214 | options: SelectMenuOptionBuilder[] = []; 215 | placeholder?: string; 216 | minValues = 1; 217 | maxValues = 1; 218 | disabled = false; 219 | 220 | constructor(options: PageSelectOptions) { 221 | this.handler = options.handler; 222 | // Convert the options to SelectMenuOptionBuilders so that we don't have to deal with emoji weirdness 223 | for (const option of options.options) { 224 | if (option instanceof SelectMenuOptionBuilder) { 225 | this.options.push(option); 226 | } else { 227 | this.options.push(new SelectMenuOptionBuilder(option)); 228 | } 229 | } 230 | if (options.placeholder) this.placeholder = options.placeholder; 231 | if (options.minValues) this.minValues = options.minValues; 232 | if (options.maxValues) this.maxValues = options.maxValues; 233 | if (options.disabled) this.disabled = options.disabled; 234 | } 235 | 236 | toDjsComponent(id: string) { 237 | const builder = new SelectMenuBuilder({ 238 | min_values: this.minValues, 239 | max_values: this.maxValues, 240 | disabled: this.disabled, 241 | custom_id: id, 242 | }); 243 | builder.addOptions(this.options); 244 | if (this.placeholder) builder.setPlaceholder(this.placeholder); 245 | return builder; 246 | } 247 | 248 | compareToComponent(component: PotentialDjsComponent) { 249 | if (!(component.type === ComponentType.StringSelect)) return false; 250 | if ( 251 | this.disabled !== component.disabled || 252 | this.maxValues !== component.maxValues || 253 | this.minValues !== component.minValues || 254 | this.placeholder !== component.placeholder 255 | ) 256 | return false; 257 | if (this.options.length !== component.options.length) return false; 258 | return this.options.every((option, index) => { 259 | const other = component.options[index]; 260 | 261 | if ( 262 | other.default !== (option.data.default ?? false) || 263 | other.description !== (option.data.description ?? null) || 264 | other.label !== option.data.label || 265 | other.value !== option.data.value 266 | ) 267 | return false; 268 | if ( 269 | (option.data.emoji && !other.emoji) || 270 | (!option.data.emoji && other.emoji) 271 | ) 272 | return false; 273 | if (option.data.emoji && other.emoji) { 274 | if (!compareEmoji(option.data.emoji, other.emoji)) return false; 275 | } 276 | return true; 277 | }); 278 | } 279 | } 280 | 281 | export class PageStringSelect extends PageSelect {} 282 | 283 | export interface PageUserSelectOptions { 284 | handler: (interaction: UserSelectMenuInteraction) => void; 285 | placeholder?: string; 286 | minValues?: number; 287 | maxValues?: number; 288 | disabled?: boolean; 289 | } 290 | 291 | export class PageUserSelect implements ExportableToDjsComponent { 292 | type: ComponentType.UserSelect = ComponentType.UserSelect; 293 | handler: (interaction: UserSelectMenuInteraction) => void; 294 | placeholder?: string; 295 | minValues = 1; 296 | maxValues = 1; 297 | disabled = false; 298 | 299 | constructor(options: PageUserSelectOptions) { 300 | this.handler = options.handler; 301 | if (options.placeholder) this.placeholder = options.placeholder; 302 | if (options.minValues) this.minValues = options.minValues; 303 | if (options.maxValues) this.maxValues = options.maxValues; 304 | if (options.disabled) this.disabled = options.disabled; 305 | } 306 | 307 | toDjsComponent(id: string) { 308 | const options: UserSelectMenuComponentData = { 309 | type: ComponentType.UserSelect, 310 | minValues: this.minValues, 311 | maxValues: this.maxValues, 312 | disabled: this.disabled, 313 | customId: id, 314 | }; 315 | if (this.placeholder) options.placeholder = this.placeholder; 316 | const builder = new UserSelectMenuBuilder(options); 317 | return builder; 318 | } 319 | 320 | compareToComponent(component: PotentialDjsComponent) { 321 | if (!(component.type === ComponentType.UserSelect)) return false; 322 | if ( 323 | this.disabled !== component.disabled || 324 | this.maxValues !== component.maxValues || 325 | this.minValues !== component.minValues || 326 | this.placeholder !== component.placeholder 327 | ) 328 | return false; 329 | return true; 330 | } 331 | } 332 | 333 | export interface PageRoleSelectOptions { 334 | handler: (interaction: RoleSelectMenuInteraction) => void; 335 | placeholder?: string; 336 | minValues?: number; 337 | maxValues?: number; 338 | disabled?: boolean; 339 | } 340 | 341 | export class PageRoleSelect implements ExportableToDjsComponent { 342 | type: ComponentType.RoleSelect = ComponentType.RoleSelect; 343 | handler: (interaction: RoleSelectMenuInteraction) => void; 344 | placeholder?: string; 345 | minValues = 1; 346 | maxValues = 1; 347 | disabled = false; 348 | 349 | constructor(options: PageRoleSelectOptions) { 350 | this.handler = options.handler; 351 | if (options.placeholder) this.placeholder = options.placeholder; 352 | if (options.minValues) this.minValues = options.minValues; 353 | if (options.maxValues) this.maxValues = options.maxValues; 354 | if (options.disabled) this.disabled = options.disabled; 355 | } 356 | 357 | toDjsComponent(id: string) { 358 | const options: RoleSelectMenuComponentData = { 359 | type: ComponentType.RoleSelect, 360 | minValues: this.minValues, 361 | maxValues: this.maxValues, 362 | disabled: this.disabled, 363 | customId: id, 364 | }; 365 | if (this.placeholder) options.placeholder = this.placeholder; 366 | const builder = new RoleSelectMenuBuilder(options); 367 | return builder; 368 | } 369 | 370 | compareToComponent(component: PotentialDjsComponent) { 371 | if (!(component.type === ComponentType.StringSelect)) return false; 372 | if ( 373 | this.disabled !== component.disabled || 374 | this.maxValues !== component.maxValues || 375 | this.minValues !== component.minValues || 376 | this.placeholder !== component.placeholder 377 | ) 378 | return false; 379 | return true; 380 | } 381 | } 382 | 383 | export interface PageChannelSelectOptions { 384 | handler: (interaction: ChannelSelectMenuInteraction) => void; 385 | placeholder?: string; 386 | minValues?: number; 387 | maxValues?: number; 388 | disabled?: boolean; 389 | channelTypes?: ChannelType[]; 390 | } 391 | 392 | export class PageChannelSelect implements ExportableToDjsComponent { 393 | type: ComponentType.ChannelSelect = ComponentType.ChannelSelect; 394 | handler: (interaction: ChannelSelectMenuInteraction) => void; 395 | placeholder?: string; 396 | minValues = 1; 397 | maxValues = 1; 398 | disabled = false; 399 | channelTypes?: ChannelType[]; 400 | 401 | constructor(options: PageChannelSelectOptions) { 402 | this.handler = options.handler; 403 | if (options.placeholder) this.placeholder = options.placeholder; 404 | if (options.minValues) this.minValues = options.minValues; 405 | if (options.maxValues) this.maxValues = options.maxValues; 406 | if (options.disabled) this.disabled = options.disabled; 407 | if (options.channelTypes) this.channelTypes = options.channelTypes; 408 | } 409 | 410 | toDjsComponent(id: string) { 411 | const options: ChannelSelectMenuComponentData = { 412 | type: ComponentType.ChannelSelect, 413 | minValues: this.minValues, 414 | maxValues: this.maxValues, 415 | disabled: this.disabled, 416 | customId: id, 417 | }; 418 | if (this.channelTypes) options.channelTypes = this.channelTypes; 419 | if (this.placeholder) options.placeholder = this.placeholder; 420 | const builder = new ChannelSelectMenuBuilder(options); 421 | return builder; 422 | } 423 | 424 | compareToComponent(component: PotentialDjsComponent) { 425 | if (!(component.type === ComponentType.ChannelSelect)) return false; 426 | if ( 427 | this.disabled !== component.disabled || 428 | this.maxValues !== component.maxValues || 429 | this.minValues !== component.minValues || 430 | this.placeholder !== component.placeholder 431 | ) 432 | return false; 433 | return true; 434 | } 435 | } 436 | 437 | export interface PageMentionableSelectOptions { 438 | handler: (interaction: MentionableSelectMenuInteraction) => void; 439 | placeholder?: string; 440 | minValues?: number; 441 | maxValues?: number; 442 | disabled?: boolean; 443 | } 444 | 445 | export class PageMentionableSelect implements ExportableToDjsComponent { 446 | type: ComponentType.MentionableSelect = ComponentType.MentionableSelect; 447 | handler: (interaction: MentionableSelectMenuInteraction) => void; 448 | placeholder?: string; 449 | minValues = 1; 450 | maxValues = 1; 451 | disabled = false; 452 | 453 | constructor(options: PageMentionableSelectOptions) { 454 | this.handler = options.handler; 455 | if (options.placeholder) this.placeholder = options.placeholder; 456 | if (options.minValues) this.minValues = options.minValues; 457 | if (options.maxValues) this.maxValues = options.maxValues; 458 | if (options.disabled) this.disabled = options.disabled; 459 | } 460 | 461 | toDjsComponent(id: string) { 462 | const options: MentionableSelectMenuComponentData = { 463 | type: ComponentType.MentionableSelect, 464 | minValues: this.minValues, 465 | maxValues: this.maxValues, 466 | disabled: this.disabled, 467 | customId: id, 468 | }; 469 | if (this.placeholder) options.placeholder = this.placeholder; 470 | const builder = new MentionableSelectMenuBuilder(options); 471 | return builder; 472 | } 473 | 474 | compareToComponent(component: PotentialDjsComponent) { 475 | if (!(component.type === ComponentType.MentionableSelect)) return false; 476 | if ( 477 | this.disabled !== component.disabled || 478 | this.maxValues !== component.maxValues || 479 | this.minValues !== component.minValues || 480 | this.placeholder !== component.placeholder 481 | ) 482 | return false; 483 | return true; 484 | } 485 | } 486 | 487 | function compareEmoji( 488 | a: ComponentEmojiResolvable, 489 | bEmoji: { id?: string | null; name?: string | null } 490 | ) { 491 | const aEmoji = typeof a === 'string' ? parseEmoji(a) : a; 492 | if (!aEmoji) return false; 493 | if (aEmoji.id) { 494 | return aEmoji.id === bEmoji.id; 495 | } else { 496 | return aEmoji.name === bEmoji.name; 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /src/PingableTimedCache.ts: -------------------------------------------------------------------------------- 1 | // Create a cache that clears a value after the default ttl 2 | // and will refresh the timer on every get 3 | export class PingableTimedCache { 4 | private cache: Map = new Map(); 5 | private ttl: number; 6 | public timer: NodeJS.Timer; 7 | private leaveHook?: (value: T) => void; 8 | 9 | constructor(ttl: number, leaveHook?: (value: T) => void) { 10 | this.ttl = ttl; 11 | this.timer = setInterval(() => this.clear(), 1000); 12 | if (leaveHook) this.leaveHook = leaveHook; 13 | } 14 | 15 | public get(key: string): T | undefined { 16 | const entry = this.cache.get(key); 17 | if (entry) { 18 | entry.time = Date.now(); 19 | return entry.value; 20 | } 21 | return undefined; 22 | } 23 | 24 | public set(key: string, value: T): void { 25 | this.cache.set(key, { value, time: Date.now() }); 26 | } 27 | 28 | public clear(): void { 29 | const now = Date.now(); 30 | this.cache.forEach((entry, key) => { 31 | if (now - entry.time > this.ttl) { 32 | this.leaveHook?.(entry.value); 33 | this.cache.delete(key); 34 | } 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SlashCommandBase.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { 3 | ApplicationCommandOptionType, 4 | LocalizationMap, 5 | } from 'discord-api-types/v10'; 6 | import { 7 | ApplicationIntegrationType, 8 | AutocompleteInteraction, 9 | ChatInputCommandInteraction, 10 | InteractionContextType, 11 | InteractionType, 12 | SlashCommandAttachmentOption, 13 | SlashCommandBooleanOption, 14 | SlashCommandBuilder, 15 | SlashCommandChannelOption, 16 | SlashCommandIntegerOption, 17 | SlashCommandMentionableOption, 18 | SlashCommandNumberOption, 19 | SlashCommandRoleOption, 20 | SlashCommandStringOption, 21 | SlashCommandSubcommandBuilder, 22 | SlashCommandUserOption, 23 | } from 'discord.js'; 24 | import { ValidationError } from './CustomErrors'; 25 | import { ApplicationCommandOptionData, OptionsDataArray } from './OptionTypes'; 26 | import { SlashasaurusClient } from './SlashasaurusClient'; 27 | import { 28 | CommandOptionsObject, 29 | MapOptionsToAutocompleteNames, 30 | MaybePromise, 31 | } from './utilityTypes'; 32 | 33 | type ChatCommandOptions = { 34 | name: string; 35 | nameLocalizations?: LocalizationMap; 36 | description: string; 37 | descriptionLocalizations?: LocalizationMap; 38 | options: T; 39 | defaultMemberPermissions?: string | number | bigint; 40 | /** 41 | * @deprecated use contexts instead 42 | */ 43 | dmPermission?: boolean; 44 | contexts?: InteractionContextType[]; 45 | integrationTypes?: ApplicationIntegrationType[]; 46 | }; 47 | 48 | export type CommandGroupMetadata = { 49 | nameLocalizations?: LocalizationMap; 50 | description: string; 51 | descriptionLocalizations?: LocalizationMap; 52 | defaultMemberPermissions?: string | number | bigint; 53 | /** 54 | * @deprecated use contexts instead 55 | */ 56 | dmPermission?: boolean; 57 | contexts?: InteractionContextType[]; 58 | integrationTypes?: ApplicationIntegrationType[]; 59 | }; 60 | 61 | export function isCommandGroupMetadata(arg: any): arg is CommandGroupMetadata { 62 | for (const key in arg) { 63 | if (key === 'description' && typeof arg[key] !== 'string') return false; 64 | if (key === 'descriptionLocalizations' && typeof arg[key] !== 'object') 65 | return false; 66 | if ( 67 | key === 'defaultMemberPermissions' && 68 | !( 69 | typeof arg[key] === 'number' || 70 | typeof arg[key] === 'string' || 71 | typeof arg[key] === 'bigint' 72 | ) 73 | ) 74 | return false; 75 | if (key === 'dmPermission' && typeof arg[key] !== 'boolean') return false; 76 | } 77 | return true; 78 | } 79 | 80 | export type CommandRunFunction = ( 81 | interaction: ChatInputCommandInteraction, 82 | client: SlashasaurusClient, 83 | options: CommandOptionsObject 84 | ) => void; 85 | 86 | export type AutocompleteFunction = ( 87 | interaction: AutocompleteInteraction, 88 | focusedName: MapOptionsToAutocompleteNames, 89 | focusedValue: string | number, 90 | client: SlashasaurusClient, 91 | options: Partial> 92 | ) => void; 93 | 94 | type HandlersType = 95 | MapOptionsToAutocompleteNames extends never 96 | ? { 97 | run: CommandRunFunction; 98 | } 99 | : HandlersWithAutoComplete; 100 | 101 | type HandlersWithAutoComplete = { 102 | run: CommandRunFunction; 103 | autocomplete: AutocompleteFunction; 104 | }; 105 | 106 | export function isChatCommand(command: any): command is SlashCommand { 107 | return command instanceof SlashCommand; 108 | } 109 | 110 | export class SlashCommand { 111 | commandInfo: ChatCommandOptions & { type: string }; 112 | validatorsMap: Map< 113 | string, 114 | ( 115 | interaction: ChatInputCommandInteraction, 116 | value: any 117 | ) => MaybePromise 118 | > = new Map(); 119 | transformersMap: Map MaybePromise> = new Map(); 120 | autocompleteMap: Map< 121 | string, 122 | ( 123 | interaction: AutocompleteInteraction, 124 | value: any, 125 | client: SlashasaurusClient 126 | ) => MaybePromise 127 | > = new Map(); 128 | 129 | /** 130 | * 131 | * @param commandInfo The general info for the command 132 | * @param handlers 133 | */ 134 | constructor(commandInfo: ChatCommandOptions, handlers: HandlersType) { 135 | this.commandInfo = { 136 | ...commandInfo, 137 | type: 'CHAT_INPUT', 138 | }; 139 | commandInfo.options.forEach((option) => { 140 | if ('validator' in option && option.validator) 141 | this.validatorsMap.set(option.name, option.validator); 142 | if ('transformer' in option && option.transformer) 143 | this.transformersMap.set(option.name, option.transformer); 144 | if ('onAutocomplete' in option && option.onAutocomplete) 145 | this.autocompleteMap.set(option.name, option.onAutocomplete); 146 | }); 147 | this.run = handlers.run; 148 | if ('autocomplete' in handlers) this.autocomplete = handlers.autocomplete; 149 | } 150 | 151 | run( 152 | interaction: ChatInputCommandInteraction, 153 | _client: SlashasaurusClient, 154 | _options: CommandOptionsObject 155 | ) { 156 | interaction.reply({ 157 | content: 'This command is not implemented yet', 158 | ephemeral: true, 159 | }); 160 | } 161 | 162 | autocomplete( 163 | interaction: AutocompleteInteraction, 164 | _focusedName: MapOptionsToAutocompleteNames, 165 | _focusedValue: string | number, 166 | _client: SlashasaurusClient, 167 | _options: Partial> 168 | ) { 169 | interaction.respond([ 170 | { 171 | name: "This interaction isn't implemented yet", 172 | value: 'error', 173 | }, 174 | ]); 175 | } 176 | 177 | async validateAndTransformOptions( 178 | interaction: ChatInputCommandInteraction 179 | ): Promise | string[]>; 180 | async validateAndTransformOptions( 181 | interaction: AutocompleteInteraction, 182 | skipRequiredCheck: boolean, 183 | skipValidationAndTransformation: boolean 184 | ): Promise>; 185 | async validateAndTransformOptions( 186 | interaction: ChatInputCommandInteraction | AutocompleteInteraction, 187 | skipRequiredCheck = false, 188 | skipValidationAndTransformation = false 189 | ): Promise | string[]> { 190 | const errors: string[] = []; 191 | const values: Record> = {}; 192 | for (const option of this.commandInfo.options) { 193 | // Get the option data 194 | let value = 195 | interaction.type === InteractionType.ApplicationCommand 196 | ? getCommandDataForType( 197 | interaction, 198 | option.type, 199 | option.name, 200 | skipRequiredCheck ? false : option.required ?? false 201 | ) 202 | : getAutocompleteDataForType( 203 | interaction, 204 | option.type, 205 | option.name, 206 | skipRequiredCheck ? false : option.required ?? false 207 | ); 208 | 209 | // If the value is undefined, assign early and continue to skip the rest of the validation and transformation 210 | if (value === null) { 211 | values[option.name] = null; 212 | continue; 213 | } 214 | 215 | if (skipValidationAndTransformation) { 216 | values[option.name] = value; 217 | continue; 218 | } 219 | 220 | // Check if the option has a validator 221 | let isValid = true; 222 | if ( 223 | this.validatorsMap.has(option.name) && 224 | interaction instanceof ChatInputCommandInteraction 225 | ) { 226 | // Run the validator 227 | const validator = this.validatorsMap.get(option.name); 228 | if (!validator) 229 | throw new Error(`Validator for ${option.name} not found`); 230 | try { 231 | const validateResult = await validator(interaction, value); 232 | if (typeof validateResult === 'string') { 233 | errors.push(validateResult); 234 | isValid = false; 235 | } 236 | } catch (e) { 237 | if (e instanceof ValidationError) { 238 | // This threw a validation error, add the message to our errors array 239 | errors.push(e.message); 240 | } else { 241 | // This threw a different error, throw it 242 | throw e; 243 | } 244 | } 245 | } 246 | // If the option is invalid, skip it 247 | if (!isValid) continue; 248 | 249 | // Check if the option has a transformer 250 | if (this.transformersMap.has(option.name)) { 251 | // Run the transformer 252 | const transformer = this.transformersMap.get(option.name); 253 | if (!transformer) 254 | throw new Error(`Transformer for ${option.name} not found`); 255 | value = await transformer(value); 256 | } 257 | 258 | // Add the value to the values object 259 | values[option.name] = value; 260 | } 261 | if (errors.length > 0) return errors; 262 | return values as CommandOptionsObject; 263 | } 264 | } 265 | 266 | function getCommandDataForType( 267 | interaction: ChatInputCommandInteraction, 268 | type: ApplicationCommandOptionData['type'], 269 | name: string, 270 | required: boolean 271 | ) { 272 | switch (type) { 273 | case 'STRING': 274 | case ApplicationCommandOptionType.String: 275 | return interaction.options.getString(name, required); 276 | case 'INTEGER': 277 | case ApplicationCommandOptionType.Integer: 278 | return interaction.options.getInteger(name, required); 279 | case 'BOOLEAN': 280 | case ApplicationCommandOptionType.Boolean: 281 | return interaction.options.getBoolean(name, required); 282 | case 'USER': 283 | case ApplicationCommandOptionType.User: 284 | if (interaction.inGuild()) 285 | return ( 286 | interaction.options.getMember(name) ?? 287 | interaction.options.getUser(name, required) 288 | ); 289 | return interaction.options.getUser(name, required); 290 | case 'CHANNEL': 291 | case ApplicationCommandOptionType.Channel: 292 | return interaction.options.getChannel(name, required); 293 | case 'ROLE': 294 | case ApplicationCommandOptionType.Role: 295 | return interaction.options.getRole(name, required); 296 | case 'MENTIONABLE': 297 | case ApplicationCommandOptionType.Mentionable: 298 | return interaction.options.getMentionable(name, required); 299 | case 'NUMBER': 300 | case ApplicationCommandOptionType.Number: 301 | return interaction.options.getNumber(name, required); 302 | case 'ATTACHMENT': 303 | case ApplicationCommandOptionType.Attachment: 304 | return interaction.options.getAttachment(name, required); 305 | } 306 | } 307 | 308 | function getAutocompleteDataForType( 309 | interaction: AutocompleteInteraction, 310 | type: ApplicationCommandOptionData['type'], 311 | name: string, 312 | required: boolean 313 | ) { 314 | switch (type) { 315 | case 'STRING': 316 | case ApplicationCommandOptionType.String: 317 | return interaction.options.getString(name, required); 318 | case 'INTEGER': 319 | case ApplicationCommandOptionType.Integer: 320 | return interaction.options.getInteger(name, required); 321 | case 'BOOLEAN': 322 | case ApplicationCommandOptionType.Boolean: 323 | return interaction.options.getBoolean(name, required); 324 | case 'NUMBER': 325 | case ApplicationCommandOptionType.Number: 326 | return interaction.options.getNumber(name, required); 327 | } 328 | return null; 329 | } 330 | 331 | export function populateBuilder< 332 | T extends SlashCommandBuilder | SlashCommandSubcommandBuilder 333 | >(info: ChatCommandOptions<[]>, builder: T) { 334 | builder 335 | .setName(info.name) 336 | .setNameLocalizations(info.nameLocalizations ?? null) 337 | .setDescription(info.description) 338 | .setDescriptionLocalizations(info.descriptionLocalizations ?? null); 339 | if (builder instanceof SlashCommandBuilder) { 340 | builder 341 | .setDefaultMemberPermissions(info.defaultMemberPermissions) 342 | if(info.integrationTypes != null || info.contexts != null) { 343 | builder.setContexts(info.contexts ?? []); 344 | builder.setIntegrationTypes(info.integrationTypes ?? []); 345 | } else if(info.dmPermission != null) { 346 | builder.setDMPermission(info.dmPermission); 347 | } 348 | } 349 | info.options.forEach((option: ApplicationCommandOptionData) => { 350 | let string, 351 | integer, 352 | boolean, 353 | user, 354 | channel, 355 | role, 356 | mentionable, 357 | number, 358 | attachment; 359 | switch (option.type) { 360 | case ApplicationCommandOptionType.String: 361 | case 'STRING': 362 | string = new SlashCommandStringOption() 363 | .setName(option.name) 364 | .setNameLocalizations(option.nameLocalizations ?? null) 365 | .setDescription(option.description) 366 | .setDescriptionLocalizations(option.descriptionLocalizations ?? null) 367 | .setRequired(option.required ?? false) 368 | .setAutocomplete(option.autocomplete ?? false); 369 | if ('minLength' in option && option.minLength) 370 | string.setMinLength(option.minLength); 371 | if ('maxLength' in option && option.maxLength) 372 | string.setMaxLength(option.maxLength); 373 | if ('choices' in option && option.choices) { 374 | string.setChoices( 375 | ...option.choices.map((choice) => ({ 376 | name: choice.name, 377 | name_localizations: choice.nameLocalizations ?? null, 378 | value: choice.value as string, 379 | })) 380 | ); 381 | } 382 | builder.addStringOption(string); 383 | break; 384 | case ApplicationCommandOptionType.Integer: 385 | case 'INTEGER': 386 | integer = new SlashCommandIntegerOption() 387 | .setName(option.name) 388 | .setNameLocalizations(option.nameLocalizations ?? null) 389 | .setDescription(option.description) 390 | .setDescriptionLocalizations(option.descriptionLocalizations ?? null) 391 | .setRequired(option.required ?? false) 392 | .setAutocomplete(option.autocomplete ?? false); 393 | if ('minValue' in option && option.minValue) 394 | integer.setMinValue(option.minValue); 395 | if ('maxValue' in option && option.maxValue) 396 | integer.setMaxValue(option.maxValue); 397 | if ('choices' in option && option.choices) { 398 | integer.setChoices( 399 | ...option.choices.map((choice) => ({ 400 | name: choice.name, 401 | name_localizations: choice.nameLocalizations ?? null, 402 | value: choice.value as number, 403 | })) 404 | ); 405 | } 406 | builder.addIntegerOption(integer); 407 | break; 408 | case ApplicationCommandOptionType.Boolean: 409 | case 'BOOLEAN': 410 | boolean = new SlashCommandBooleanOption() 411 | .setName(option.name) 412 | .setNameLocalizations(option.nameLocalizations ?? null) 413 | .setDescription(option.description) 414 | .setDescriptionLocalizations(option.descriptionLocalizations ?? null) 415 | .setRequired(option.required ?? false); 416 | builder.addBooleanOption(boolean); 417 | break; 418 | case ApplicationCommandOptionType.User: 419 | case 'USER': 420 | user = new SlashCommandUserOption() 421 | .setName(option.name) 422 | .setNameLocalizations(option.nameLocalizations ?? null) 423 | .setDescription(option.description) 424 | .setDescriptionLocalizations(option.descriptionLocalizations ?? null) 425 | .setRequired(option.required ?? false); 426 | builder.addUserOption(user); 427 | break; 428 | case ApplicationCommandOptionType.Channel: 429 | case 'CHANNEL': 430 | channel = new SlashCommandChannelOption() 431 | .setName(option.name) 432 | .setNameLocalizations(option.nameLocalizations ?? null) 433 | .setDescription(option.description) 434 | .setDescriptionLocalizations(option.descriptionLocalizations ?? null) 435 | .setRequired(option.required ?? false); 436 | if (option.channelTypes) { 437 | channel.addChannelTypes(...option.channelTypes); 438 | } 439 | builder.addChannelOption(channel); 440 | break; 441 | case ApplicationCommandOptionType.Role: 442 | case 'ROLE': 443 | role = new SlashCommandRoleOption() 444 | .setName(option.name) 445 | .setNameLocalizations(option.nameLocalizations ?? null) 446 | .setDescription(option.description) 447 | .setDescriptionLocalizations(option.descriptionLocalizations ?? null) 448 | .setRequired(option.required ?? false); 449 | builder.addRoleOption(role); 450 | break; 451 | case ApplicationCommandOptionType.Mentionable: 452 | case 'MENTIONABLE': 453 | mentionable = new SlashCommandMentionableOption() 454 | .setName(option.name) 455 | .setNameLocalizations(option.nameLocalizations ?? null) 456 | .setDescription(option.description) 457 | .setDescriptionLocalizations(option.descriptionLocalizations ?? null) 458 | .setRequired(option.required ?? false); 459 | builder.addMentionableOption(mentionable); 460 | break; 461 | case ApplicationCommandOptionType.Number: 462 | case 'NUMBER': 463 | number = new SlashCommandNumberOption() 464 | .setName(option.name) 465 | .setNameLocalizations(option.nameLocalizations ?? null) 466 | .setDescription(option.description) 467 | .setDescriptionLocalizations(option.descriptionLocalizations ?? null) 468 | .setRequired(option.required ?? false) 469 | .setAutocomplete(option.autocomplete ?? false); 470 | if ('minValue' in option && option.minValue) 471 | number.setMinValue(option.minValue); 472 | if ('maxValue' in option && option.maxValue) 473 | number.setMaxValue(option.maxValue); 474 | if ('choices' in option && option.choices) { 475 | number.setChoices( 476 | ...option.choices.map((choice) => ({ 477 | name: choice.name, 478 | name_localizations: choice.nameLocalizations ?? null, 479 | value: choice.value as number, 480 | })) 481 | ); 482 | } 483 | builder.addNumberOption(number); 484 | break; 485 | case ApplicationCommandOptionType.Attachment: 486 | case 'ATTACHMENT': 487 | attachment = new SlashCommandAttachmentOption() 488 | .setName(option.name) 489 | .setNameLocalizations(option.nameLocalizations ?? null) 490 | .setDescription(option.description) 491 | .setDescriptionLocalizations(option.descriptionLocalizations ?? null) 492 | .setRequired(option.required ?? false); 493 | builder.addAttachmentOption(attachment); 494 | break; 495 | } 496 | }); 497 | return builder; 498 | } -------------------------------------------------------------------------------- /src/SlashasaurusClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContextMenuCommandBuilder, 3 | SlashCommandBuilder, 4 | SlashCommandSubcommandBuilder, 5 | SlashCommandSubcommandGroupBuilder, 6 | } from '@discordjs/builders'; 7 | import { REST } from '@discordjs/rest'; 8 | import { ApplicationCommandType, Routes } from 'discord-api-types/v10'; 9 | import { 10 | AutocompleteInteraction, 11 | Awaitable, 12 | BaseGuildTextChannel, 13 | BitFieldResolvable, 14 | ButtonInteraction, 15 | ChannelSelectMenuInteraction, 16 | ChatInputCommandInteraction, 17 | Client, 18 | ClientEvents, 19 | ClientOptions, 20 | CommandInteraction, 21 | ComponentType, 22 | ContextMenuCommandInteraction, 23 | DMChannel, 24 | ForumChannel, 25 | Interaction, 26 | InteractionType, 27 | InteractionWebhook, 28 | MentionableSelectMenuInteraction, 29 | Message, 30 | MessageComponentInteraction, 31 | ModalSubmitInteraction, 32 | RoleSelectMenuInteraction, 33 | SelectMenuInteraction, 34 | SendableChannels, 35 | UserSelectMenuInteraction, 36 | } from 'discord.js'; 37 | import { readdir, stat } from 'fs/promises'; 38 | import { join } from 'path'; 39 | import { 40 | ContextMenuHandlerType, 41 | isMessageCommand, 42 | isUserCommand, 43 | MessageCommand, 44 | UserCommand, 45 | } from './ContextMenuBase'; 46 | import { Middleware, Pipeline } from './MiddlewarePipeline'; 47 | import { 48 | compareMessages, 49 | DEFAULT_PAGE_ID, 50 | DeserializeStateFn, 51 | isPage, 52 | Page, 53 | pageComponentRowsToComponents, 54 | PageInteractionReplyMessage, 55 | } from './Page'; 56 | import { PingableTimedCache } from './PingableTimedCache'; 57 | import { 58 | AutocompleteFunction, 59 | CommandGroupMetadata, 60 | CommandRunFunction, 61 | isChatCommand, 62 | isCommandGroupMetadata, 63 | populateBuilder, 64 | SlashCommand, 65 | } from './SlashCommandBase'; 66 | import { TemplateModal } from './TemplateModal'; 67 | import { MaybePromise } from './utilityTypes'; 68 | 69 | interface SlashasaurusClientEvents extends ClientEvents { 70 | commandRun: [intercation: CommandInteraction]; 71 | buttonPressed: [interaction: ButtonInteraction]; 72 | selectChanged: [interaction: SelectMenuInteraction]; 73 | contextMenuRun: [interaction: ContextMenuCommandInteraction]; 74 | autocomplete: [interaction: AutocompleteInteraction]; 75 | modalSubmit: [interaction: ModalSubmitInteraction]; 76 | userSelectChanged: [interaction: UserSelectMenuInteraction]; 77 | roleSelectChanged: [interaction: RoleSelectMenuInteraction]; 78 | channelSelectChanged: [interaction: ChannelSelectMenuInteraction]; 79 | mentionableSelectChanged: [interaction: MentionableSelectMenuInteraction]; 80 | } 81 | 82 | const JSFileRegex = /(? { 85 | on( 86 | event: K, 87 | listener: (...args: SlashasaurusClientEvents[K]) => Awaitable, 88 | ): this; 89 | 90 | once( 91 | event: K, 92 | listener: (...args: SlashasaurusClientEvents[K]) => Awaitable, 93 | ): this; 94 | 95 | emit( 96 | event: K, 97 | ...args: SlashasaurusClientEvents[K] 98 | ): boolean; 99 | 100 | off( 101 | event: K, 102 | listener: (...args: SlashasaurusClientEvents[K]) => Awaitable, 103 | ): this; 104 | 105 | removeAllListeners(event?: K): this; 106 | } 107 | 108 | interface MessageData { 109 | guildId: string; 110 | channelId: string; 111 | messageId: string; 112 | } 113 | 114 | interface InteractionMessageData { 115 | webhookToken: string; 116 | messageId: string; 117 | } 118 | 119 | type StorePageStateFn = ( 120 | messageId: string, 121 | pageId: string, 122 | state: string, 123 | messageData: string, 124 | ) => MaybePromise; 125 | type GetPageStateFn = (messageId: string) => MaybePromise<{ 126 | pageId: string; 127 | stateString: string; 128 | messageData: string; 129 | }>; 130 | 131 | export interface SlashasaurusClientOptions { 132 | /** 133 | * You can pass any logger compatible with [pino](https://getpino.io/) 134 | * and the client will log some internal information to it 135 | */ 136 | logger?: Logger; 137 | 138 | /** 139 | * This function is used to persistently store the page state in case 140 | * it needs to leave the cache or the bot suddenly shuts down. 141 | */ 142 | storePageState?: StorePageStateFn; 143 | 144 | /** 145 | * This function will retreive the page from your persistent storage 146 | * when the page is "woken up." 147 | */ 148 | getPageState?: GetPageStateFn; 149 | 150 | /** 151 | * The amount of time (in ms) that pages will stay in the cache after they 152 | * were last interacted with. The default is 30 seconds. 153 | */ 154 | pageTtl?: number; 155 | 156 | /** 157 | * Whether or not to skip validating and transforming options for autocomplete handlers. Defaults to false. 158 | */ 159 | skipValidationAndTransformationForAutocomplete?: boolean; 160 | } 161 | 162 | interface LogFn { 163 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 164 | (obj: T, msg?: string, ...args: any[]): void; 165 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 166 | (msg: string, ...args: any[]): void; 167 | } 168 | 169 | export interface Logger { 170 | info: LogFn; 171 | debug: LogFn; 172 | error: LogFn; 173 | } 174 | 175 | interface PageMapStorage { 176 | page: Page['constructor']; 177 | deserialize: DeserializeStateFn; 178 | } 179 | 180 | async function defaultPageStore() { 181 | throw new Error( 182 | `You must implement storePageState and getPageState in order to use pages`, 183 | ); 184 | } 185 | 186 | async function defaultPageGet(): Promise<{ 187 | pageId: string; 188 | stateString: string; 189 | messageData: string; 190 | }> { 191 | throw new Error( 192 | `You must implement storePageState and getPageState in order to use pages`, 193 | ); 194 | return { 195 | pageId: 'error', 196 | messageData: 'error', 197 | stateString: '', 198 | }; 199 | } 200 | 201 | export class SlashasaurusClient extends Client { 202 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 203 | private commandMap = new Map>(); 204 | private userContextMenuMap = new Map(); 205 | private messageContextMenuMap = new Map(); 206 | private pageMap = new Map(); 207 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 208 | private modalMap = new Map>(); 209 | private skipAutocompleteValidationAndTransformation: boolean; 210 | logger?: Logger; 211 | chatCommandMiddleware = new Pipeline>(); 212 | autocompleteMiddleware = new Pipeline>(); 213 | contextMenuMiddleware = new Pipeline< 214 | ContextMenuHandlerType<'MESSAGE'> | ContextMenuHandlerType<'USER'> 215 | >(); 216 | 217 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 218 | activePages: PingableTimedCache>; 219 | storePageState: StorePageStateFn; 220 | getPageState: GetPageStateFn; 221 | 222 | constructor(djsOptions: ClientOptions, options: SlashasaurusClientOptions) { 223 | super(djsOptions); 224 | if (options.logger) this.logger = options.logger; 225 | this.activePages = new PingableTimedCache( 226 | options.pageTtl ?? 30000, 227 | (page) => page.pageWillLeaveCache?.(), 228 | ); 229 | this.storePageState = options.storePageState ?? defaultPageStore; 230 | this.getPageState = options.getPageState ?? defaultPageGet; 231 | this.skipAutocompleteValidationAndTransformation = 232 | options.skipValidationAndTransformationForAutocomplete ?? false; 233 | this.on('interactionCreate', this.handleInteractionEvent); 234 | } 235 | 236 | /** 237 | * Registers all command files in the given path 238 | * 239 | * @param folderPath The relative path to the folder 240 | */ 241 | async registerCommandsFrom( 242 | folderPath: string, 243 | register: false, 244 | ): Promise; 245 | async registerCommandsFrom( 246 | folderPath: string, 247 | register: true, 248 | token: string, 249 | ): Promise; 250 | async registerCommandsFrom( 251 | folderPath: string, 252 | register: boolean, 253 | token?: string, 254 | ): Promise { 255 | this.logger?.info('Registering global commands'); 256 | const topLevelFolders = await readdir(folderPath); 257 | 258 | const commandData: (SlashCommandBuilder | ContextMenuCommandBuilder)[] = []; 259 | 260 | for (const folderName of topLevelFolders) { 261 | switch (folderName) { 262 | case 'chat': 263 | commandData.push( 264 | ...(await this.loadTopLevelCommands(join(folderPath, folderName))), 265 | ); 266 | break; 267 | case 'message': 268 | commandData.push( 269 | ...(await this.loadMessageCommands(join(folderPath, folderName))), 270 | ); 271 | break; 272 | case 'user': 273 | commandData.push( 274 | ...(await this.loadUserCommands(join(folderPath, folderName))), 275 | ); 276 | break; 277 | } 278 | } 279 | 280 | this.logger?.debug(commandData); 281 | 282 | if (register && token) { 283 | const rest = new REST({ version: '10' }).setToken(token); 284 | 285 | await rest.put(Routes.applicationCommands(this.application.id), { 286 | body: commandData.map((c) => c.toJSON()), 287 | }); 288 | } 289 | 290 | this.logger?.info('Finished registering global commands'); 291 | } 292 | 293 | async registerGuildCommandsFrom( 294 | folderPath: string, 295 | guildId: string, 296 | register: false, 297 | ): Promise; 298 | async registerGuildCommandsFrom( 299 | folderPath: string, 300 | guildId: string, 301 | register: true, 302 | token: string, 303 | ): Promise; 304 | async registerGuildCommandsFrom( 305 | folderPath: string, 306 | guildId: string, 307 | register: boolean, 308 | token?: string, 309 | ): Promise { 310 | this.logger?.info(`Registering guild commands to ${guildId}`); 311 | const topLevelFolders = await readdir(folderPath); 312 | 313 | const commandData: (SlashCommandBuilder | ContextMenuCommandBuilder)[] = []; 314 | 315 | for (const folderName of topLevelFolders) { 316 | switch (folderName) { 317 | case 'chat': 318 | commandData.push( 319 | ...(await this.loadTopLevelCommands(join(folderPath, folderName))), 320 | ); 321 | break; 322 | case 'message': 323 | commandData.push( 324 | ...(await this.loadMessageCommands(join(folderPath, folderName))), 325 | ); 326 | break; 327 | case 'user': 328 | commandData.push( 329 | ...(await this.loadUserCommands(join(folderPath, folderName))), 330 | ); 331 | break; 332 | } 333 | } 334 | 335 | this.logger?.debug(commandData); 336 | 337 | if (register && token) { 338 | const rest = new REST({ version: '10' }).setToken(token); 339 | await rest.put( 340 | Routes.applicationGuildCommands(this.application.id, guildId), 341 | { 342 | body: commandData.map((c) => c.toJSON()), 343 | }, 344 | ); 345 | } 346 | 347 | this.logger?.info(`Finished registering guild commands to ${guildId}`); 348 | } 349 | 350 | useCommandMiddleware(fn: Middleware>) { 351 | this.chatCommandMiddleware.push(fn); 352 | } 353 | 354 | useAutocompleteMiddleware(fn: Middleware>) { 355 | this.autocompleteMiddleware.push(fn); 356 | } 357 | 358 | useContextMenuMiddleware( 359 | fn: Middleware< 360 | ContextMenuHandlerType<'MESSAGE'> | ContextMenuHandlerType<'USER'> 361 | >, 362 | ) { 363 | this.contextMenuMiddleware.push(fn); 364 | } 365 | 366 | private async loadUserCommands(path: string) { 367 | const topLevel = await readdir(path); 368 | 369 | const commandData: ContextMenuCommandBuilder[] = []; 370 | 371 | this.logger?.debug( 372 | `Loading user commands from folder, found ${topLevel.join(', ')}`, 373 | ); 374 | 375 | for (const folderOrFile of topLevel) { 376 | const filePath = join(path, folderOrFile); 377 | if ((await stat(filePath)).isFile()) { 378 | this.logger?.debug(`Checking if file ${folderOrFile} is a js(x) file`); 379 | if (folderOrFile.match(JSFileRegex)) { 380 | this.logger?.debug( 381 | `Checking if file ${folderOrFile} contains a command`, 382 | ); 383 | // This is a js file 384 | const data = await import(filePath); 385 | if (!data.default) { 386 | throw new Error( 387 | `Expected a default export in file ${join( 388 | path, 389 | folderOrFile, 390 | )} but didn't find one`, 391 | ); 392 | } 393 | const command = data.default; 394 | if (!isUserCommand(command)) { 395 | throw new Error( 396 | `Expected the default export in file ${join( 397 | path, 398 | folderOrFile, 399 | )} to be a UserCommand`, 400 | ); 401 | } 402 | this.userContextMenuMap.set(command.commandInfo.name, command); 403 | const info = command.commandInfo; 404 | commandData.push( 405 | new ContextMenuCommandBuilder() 406 | .setName(info.name) 407 | .setType(ApplicationCommandType.User) 408 | .setNameLocalizations(info.nameLocalizations ?? null) 409 | .setDefaultMemberPermissions( 410 | info.defaultMemberPermissions ?? null, 411 | ) 412 | .setDMPermission(info.dmPermission ?? null), 413 | ); 414 | this.logger?.debug(`Loaded user command ${command.commandInfo.name}`); 415 | } 416 | } else { 417 | throw new Error( 418 | `Found folder at ${filePath}, context menu commands cannot have subcommands`, 419 | ); 420 | } 421 | } 422 | 423 | this.logger?.debug(`Finished loading user commands from folder`); 424 | 425 | return commandData; 426 | } 427 | 428 | private async loadMessageCommands(path: string) { 429 | const topLevel = await readdir(path); 430 | 431 | const commandData: ContextMenuCommandBuilder[] = []; 432 | 433 | this.logger?.debug( 434 | `Loading message commands from folder, found ${topLevel.join(', ')}`, 435 | ); 436 | 437 | for (const folderOrFile of topLevel) { 438 | const filePath = join(path, folderOrFile); 439 | if ((await stat(filePath)).isFile()) { 440 | this.logger?.debug(`Checking if file ${folderOrFile} is a js(x) file`); 441 | if (folderOrFile.match(JSFileRegex)) { 442 | this.logger?.debug( 443 | `Checking if file ${folderOrFile} contains a command`, 444 | ); 445 | // This is a js file 446 | const data = await import(filePath); 447 | if (!data.default) { 448 | throw new Error( 449 | `Expected a default export in file ${join( 450 | path, 451 | folderOrFile, 452 | )} but didn't find one`, 453 | ); 454 | } 455 | const command = data.default; 456 | if (!isMessageCommand(command)) { 457 | throw new Error( 458 | `Expected the default export in file ${join( 459 | path, 460 | folderOrFile, 461 | )} to be a MessageCommand`, 462 | ); 463 | } 464 | this.messageContextMenuMap.set(command.commandInfo.name, command); 465 | const info = command.commandInfo; 466 | commandData.push( 467 | new ContextMenuCommandBuilder() 468 | .setName(info.name) 469 | .setType(ApplicationCommandType.Message) 470 | .setNameLocalizations(info.nameLocalizations ?? null) 471 | .setDefaultMemberPermissions( 472 | info.defaultMemberPermissions ?? null, 473 | ) 474 | .setDMPermission(info.dmPermission ?? null), 475 | ); 476 | this.logger?.debug( 477 | `Loaded message command ${command.commandInfo.name}`, 478 | ); 479 | } 480 | } else { 481 | throw new Error( 482 | `Found folder at ${filePath}, context menu commands cannot have subcommands`, 483 | ); 484 | } 485 | } 486 | 487 | this.logger?.debug(`Finished loading message commands from folder`); 488 | 489 | return commandData; 490 | } 491 | 492 | private async loadTopLevelCommands(path: string) { 493 | const topLevel = await readdir(path); 494 | 495 | const commandData: SlashCommandBuilder[] = []; 496 | 497 | this.logger?.debug( 498 | `Loading chat commands from folder, found ${topLevel.join(', ')}`, 499 | ); 500 | 501 | for (const folderOrFile of topLevel) { 502 | const filePath = join(path, folderOrFile); 503 | if ((await stat(filePath)).isFile()) { 504 | this.logger?.debug(`Checking if file ${folderOrFile} is a js(x) file`); 505 | if (folderOrFile.match(JSFileRegex)) { 506 | this.logger?.debug( 507 | `Checking if file ${folderOrFile} contains a command`, 508 | ); 509 | // This is a js file 510 | const data = await import(filePath); 511 | if (!data.default) { 512 | throw new Error( 513 | `Expected a default export in file ${join( 514 | path, 515 | folderOrFile, 516 | )} but didn't find one`, 517 | ); 518 | } 519 | this.logger?.debug( 520 | `Checking if default export of ${folderOrFile} is a command`, 521 | ); 522 | const command = data.default; 523 | if (!isChatCommand(command)) { 524 | throw new Error( 525 | `Expected the default export in file ${join( 526 | path, 527 | folderOrFile, 528 | )} to be a SlashCommand`, 529 | ); 530 | } 531 | this.logger?.debug( 532 | `Adding command from ${folderOrFile} to command map`, 533 | ); 534 | if (this.commandMap.has(command.commandInfo.name)) 535 | throw new Error( 536 | `Duplicate command name ${command.commandInfo.name}`, 537 | ); 538 | this.commandMap.set(command.commandInfo.name, command); 539 | commandData.push( 540 | populateBuilder(command.commandInfo, new SlashCommandBuilder()), 541 | ); 542 | this.logger?.debug(`Loaded chat command ${command.commandInfo.name}`); 543 | } 544 | } else { 545 | // This has subcommands 546 | commandData.push( 547 | await this.loadSubFolderLevelOne(filePath, folderOrFile), 548 | ); 549 | } 550 | } 551 | 552 | this.logger?.debug(`Finished loading chat commands from folder`); 553 | 554 | return commandData; 555 | } 556 | 557 | private async loadSubFolderLevelOne( 558 | path: string, 559 | name: string, 560 | ): Promise { 561 | const topLevel = await readdir(path); 562 | 563 | const commandData: ( 564 | | SlashCommandSubcommandBuilder 565 | | SlashCommandSubcommandGroupBuilder 566 | )[] = []; 567 | 568 | this.logger?.debug( 569 | `Loading sub-commands from chat/${name}, found ${topLevel.join(', ')}`, 570 | ); 571 | 572 | let metaData: CommandGroupMetadata = { 573 | description: 'Default description', 574 | }; 575 | 576 | for (const folderOrFile of topLevel) { 577 | const filePath = join(path, folderOrFile); 578 | if ((await stat(filePath)).isFile()) { 579 | this.logger?.debug(`Checking if file ${folderOrFile} is a js(x) file`); 580 | 581 | // This is a file 582 | if (folderOrFile.match(/_meta(.js|.ts)x?$/)) { 583 | // This is the meta file which should export meta info about the command 584 | const data = await import(join(path, folderOrFile)); 585 | if (isCommandGroupMetadata(data)) { 586 | metaData = data; 587 | } 588 | } else if (folderOrFile.match(JSFileRegex)) { 589 | this.logger?.debug( 590 | `Checking if file ${folderOrFile} contains a command`, 591 | ); 592 | // This is a js file 593 | const data = await import(filePath); 594 | if (!data.default) { 595 | throw new Error( 596 | `Expected a default export in file ${join( 597 | path, 598 | folderOrFile, 599 | )} but didn't find one`, 600 | ); 601 | } 602 | this.logger?.debug( 603 | `Checking if default export of ${folderOrFile} is a command`, 604 | ); 605 | const command = data.default; 606 | if (!isChatCommand(command)) { 607 | throw new Error( 608 | `Expected the default export in file ${join( 609 | path, 610 | folderOrFile, 611 | )} to be a SlashCommand`, 612 | ); 613 | } 614 | this.logger?.debug( 615 | `Adding command from ${folderOrFile} to command map`, 616 | ); 617 | const mapName = name + '.' + command.commandInfo.name; 618 | if (this.commandMap.has(mapName)) 619 | throw new Error(`Duplicate command name ${mapName}`); 620 | this.commandMap.set(mapName, command); 621 | commandData.push( 622 | populateBuilder( 623 | command.commandInfo, 624 | new SlashCommandSubcommandBuilder(), 625 | ), 626 | ); 627 | this.logger?.debug( 628 | `Loaded chat command ${name}.${command.commandInfo.name}`, 629 | ); 630 | } 631 | } else { 632 | // This is either a subcommand group or a subcommand 633 | commandData.push( 634 | await this.loadSubFolderLevelTwo( 635 | join(path, folderOrFile), 636 | folderOrFile, 637 | name, 638 | ), 639 | ); 640 | } 641 | } 642 | 643 | this.logger?.debug(`Finished loading sub-commands from chat/${name}`); 644 | 645 | const builder = new SlashCommandBuilder() 646 | .setName(name) 647 | .setNameLocalizations(metaData.nameLocalizations ?? null) 648 | .setDescription(metaData.description) 649 | .setDescriptionLocalizations(metaData.descriptionLocalizations ?? null) 650 | .setDefaultMemberPermissions(metaData.defaultMemberPermissions ?? null) 651 | .setDMPermission(metaData.dmPermission ?? null); 652 | 653 | commandData.forEach((subcommand) => { 654 | if (subcommand instanceof SlashCommandSubcommandBuilder) { 655 | builder.addSubcommand(subcommand); 656 | } else { 657 | builder.addSubcommandGroup(subcommand); 658 | } 659 | }); 660 | 661 | return builder; 662 | } 663 | 664 | private async loadSubFolderLevelTwo( 665 | path: string, 666 | name: string, 667 | parentName: string, 668 | ): Promise { 669 | const topLevel = await readdir(path); 670 | 671 | const commandData: SlashCommandSubcommandBuilder[] = []; 672 | 673 | this.logger?.debug( 674 | `Loading sub-commands from chat/${parentName}/${name}, found ${topLevel.join( 675 | ', ', 676 | )}`, 677 | ); 678 | 679 | let metaData: CommandGroupMetadata = { 680 | description: 'Default description', 681 | }; 682 | 683 | for (const folderOrFile of topLevel) { 684 | const filePath = join(path, folderOrFile); 685 | this.logger?.debug(`Checking if file ${folderOrFile} is a js(x) file`); 686 | if ((await stat(filePath)).isFile()) { 687 | if (folderOrFile.match(/_meta(.js|.ts)x?$/)) { 688 | // This is the meta file which should export meta info about the command 689 | const data = await import(join(path, folderOrFile)); 690 | if (isCommandGroupMetadata(data)) { 691 | metaData = data; 692 | } 693 | } else if (folderOrFile.match(JSFileRegex)) { 694 | this.logger?.debug( 695 | `Checking if file ${folderOrFile} contains a command`, 696 | ); 697 | const data = await import(join(path, folderOrFile)); 698 | if (!data.default) { 699 | throw new Error( 700 | `Expected a default export in file ${join( 701 | path, 702 | folderOrFile, 703 | )} but didn't find one`, 704 | ); 705 | } 706 | this.logger?.debug( 707 | `Checking if default export of ${folderOrFile} is a command`, 708 | ); 709 | const command = data.default; 710 | if (!isChatCommand(command)) { 711 | throw new Error( 712 | `Expected the default export in file ${join( 713 | path, 714 | folderOrFile, 715 | )} to be a SlashCommand`, 716 | ); 717 | } 718 | this.logger?.debug( 719 | `Adding command from ${folderOrFile} to command map`, 720 | ); 721 | const mapName = 722 | parentName + '.' + name + '.' + command.commandInfo.name; 723 | if (this.commandMap.has(mapName)) 724 | throw new Error(`Duplicate command name ${mapName}`); 725 | this.commandMap.set(mapName, command); 726 | commandData.push( 727 | populateBuilder( 728 | command.commandInfo, 729 | new SlashCommandSubcommandBuilder(), 730 | ), 731 | ); 732 | this.logger?.debug( 733 | `Loaded chat command ${parentName}.${name}.${command.commandInfo.name}`, 734 | ); 735 | } 736 | } 737 | } 738 | 739 | this.logger?.debug( 740 | `Finished loading sub-commands from chat/${parentName}/${name}`, 741 | ); 742 | 743 | const builder = new SlashCommandSubcommandGroupBuilder() 744 | .setName(name) 745 | .setNameLocalizations(metaData.nameLocalizations ?? null) 746 | .setDescription(metaData.description) 747 | .setDescriptionLocalizations(metaData.descriptionLocalizations ?? null); 748 | 749 | commandData.forEach((subcommand) => { 750 | builder.addSubcommand(subcommand); 751 | }); 752 | 753 | return builder; 754 | } 755 | 756 | async registerPagesFrom(path: string) { 757 | const topLevel = await readdir(path); 758 | 759 | for (const folderOrFile of topLevel) { 760 | const filePath = join(path, folderOrFile); 761 | if ((await stat(filePath)).isFile()) { 762 | if (folderOrFile.match(JSFileRegex)) { 763 | // This is a js file 764 | const data = await import(filePath); 765 | if (!data.default) { 766 | throw new Error( 767 | `Expected a default export in file ${join( 768 | path, 769 | folderOrFile, 770 | )} but didn't find one`, 771 | ); 772 | } 773 | const page = data.default; 774 | if (!isPage(page)) { 775 | throw new Error( 776 | `Expected the default export in file ${join( 777 | path, 778 | folderOrFile, 779 | )} to be a Page`, 780 | ); 781 | } 782 | if (page.pageId === DEFAULT_PAGE_ID) { 783 | throw new Error( 784 | `The page exported in ${join( 785 | path, 786 | folderOrFile, 787 | )} does not have a static pageId set.`, 788 | ); 789 | } 790 | page._client = this; 791 | const deserialize = page.deserializeState ?? data.deserializeState; 792 | if (!deserialize) { 793 | throw new Error( 794 | `Expected the page to have a static deserializeState function or an export named "deserializeState" in file ${join( 795 | path, 796 | folderOrFile, 797 | )} but didn't find one`, 798 | ); 799 | } 800 | this.pageMap.set(page.pageId, { 801 | page, 802 | deserialize: deserialize, 803 | }); 804 | } 805 | } else { 806 | throw new Error( 807 | `Found folder in pages directory ${join(path, folderOrFile)}`, 808 | ); 809 | } 810 | } 811 | } 812 | 813 | async registerModalsFrom(path: string) { 814 | const topLevel = await readdir(path); 815 | 816 | for (const folderOrFile of topLevel) { 817 | const filePath = join(path, folderOrFile); 818 | if ((await stat(filePath)).isFile()) { 819 | if (folderOrFile.match(JSFileRegex)) { 820 | // This is a js file 821 | const data = await import(filePath); 822 | if (!data.default) { 823 | throw new Error( 824 | `Expected a default export in file ${join( 825 | path, 826 | folderOrFile, 827 | )} but didn't find one`, 828 | ); 829 | } 830 | const modal = data.default; 831 | if (!(modal instanceof TemplateModal)) { 832 | throw new Error( 833 | `Expected the default export in file ${join( 834 | path, 835 | folderOrFile, 836 | )} to be a TemplateModal`, 837 | ); 838 | } 839 | this.modalMap.set(modal.customId, modal); 840 | } 841 | } else { 842 | throw new Error( 843 | `Found folder in modals directory ${join(path, folderOrFile)}`, 844 | ); 845 | } 846 | } 847 | } 848 | 849 | private handleInteractionEvent(interaction: Interaction) { 850 | this.logger?.debug(interaction); 851 | switch (interaction.type) { 852 | case InteractionType.ApplicationCommand: 853 | if (interaction.commandType === ApplicationCommandType.ChatInput) { 854 | this.handleCommand(interaction); 855 | this.emit('commandRun', interaction); 856 | } else if ( 857 | interaction.commandType === ApplicationCommandType.Message || 858 | interaction.commandType === ApplicationCommandType.User 859 | ) { 860 | this.handleContextMenu(interaction); 861 | this.emit('contextMenuRun', interaction); 862 | } 863 | break; 864 | case InteractionType.ApplicationCommandAutocomplete: 865 | this.handleAutocomplete(interaction); 866 | this.emit('autocomplete', interaction); 867 | break; 868 | case InteractionType.MessageComponent: 869 | if (interaction.componentType === ComponentType.Button) { 870 | if (interaction.customId.startsWith('~')) { 871 | this.handlePageButton(interaction); 872 | } 873 | this.emit('buttonPressed', interaction); 874 | } else if (interaction.componentType === ComponentType.StringSelect) { 875 | if (interaction.customId.startsWith('~')) { 876 | this.handlePageSelect(interaction); 877 | } 878 | this.emit('selectChanged', interaction); 879 | } else if (interaction.componentType === ComponentType.UserSelect) { 880 | if (interaction.customId.startsWith('~')) { 881 | this.handlePageSelect(interaction); 882 | } 883 | this.emit('userSelectChanged', interaction); 884 | } else if (interaction.componentType === ComponentType.ChannelSelect) { 885 | if (interaction.customId.startsWith('~')) { 886 | this.handlePageSelect(interaction); 887 | } 888 | this.emit('channelSelectChanged', interaction); 889 | } else if (interaction.componentType === ComponentType.RoleSelect) { 890 | if (interaction.customId.startsWith('~')) { 891 | this.handlePageSelect(interaction); 892 | } 893 | this.emit('roleSelectChanged', interaction); 894 | } else if ( 895 | interaction.componentType === ComponentType.MentionableSelect 896 | ) { 897 | if (interaction.customId.startsWith('~')) { 898 | this.handlePageSelect(interaction); 899 | } 900 | this.emit('mentionableSelectChanged', interaction); 901 | } 902 | break; 903 | case InteractionType.ModalSubmit: 904 | this.handleModalSubmit(interaction); 905 | this.emit('modalSubmit', interaction); 906 | break; 907 | } 908 | } 909 | 910 | private async handleCommand(interaction: ChatInputCommandInteraction) { 911 | let commandName = interaction.commandName; 912 | // @ts-expect-error This is TS-private, but I know what I'm doing 913 | if (interaction.options._group) { 914 | // @ts-expect-error This is TS-private, but I know what I'm doing 915 | commandName += '.' + interaction.options._group; 916 | } 917 | // @ts-expect-error This is TS-private, but I know what I'm doing 918 | if (interaction.options._subcommand) { 919 | // @ts-expect-error This is TS-private, but I know what I'm doing 920 | commandName += '.' + interaction.options._subcommand; 921 | } 922 | const command = this.commandMap.get(commandName); 923 | if (!command) { 924 | this.logger?.error(`Unregistered command ${commandName} being run`); 925 | throw new Error(`Unregistered command ${commandName} was run`); 926 | } else { 927 | this.logger?.info(`Running command ${commandName}`); 928 | const optionsObj = await command.validateAndTransformOptions(interaction); 929 | // If there is errors, we want to send them back to the user 930 | if (Array.isArray(optionsObj)) { 931 | await interaction.reply({ 932 | content: optionsObj.join('\n'), 933 | ephemeral: true, 934 | }); 935 | return; 936 | } 937 | await this.chatCommandMiddleware.execute( 938 | command.run, 939 | interaction, 940 | this, 941 | optionsObj, 942 | ); 943 | } 944 | } 945 | 946 | private async handleAutocomplete(interaction: AutocompleteInteraction) { 947 | let commandName = interaction.commandName; 948 | // @ts-expect-error This is TS-private, but I know what I'm doing 949 | if (interaction.options._group) { 950 | // @ts-expect-error This is TS-private, but I know what I'm doing 951 | commandName += '.' + interaction.options._group; 952 | } 953 | // @ts-expect-error This is TS-private, but I know what I'm doing 954 | if (interaction.options._subcommand) { 955 | // @ts-expect-error This is TS-private, but I know what I'm doing 956 | commandName += '.' + interaction.options._subcommand; 957 | } 958 | const command = this.commandMap.get(commandName); 959 | if (!command) { 960 | interaction.respond([]); 961 | } else { 962 | const optionsObj = await command.validateAndTransformOptions( 963 | interaction, 964 | true, 965 | this.skipAutocompleteValidationAndTransformation, 966 | ); 967 | const focused = interaction.options.getFocused(true); 968 | const autocompleteFn = command.autocompleteMap.get(focused.name); 969 | if (autocompleteFn) { 970 | await this.autocompleteMiddleware.execute( 971 | (interaction, _name, value, client) => { 972 | autocompleteFn(interaction, value, client); 973 | }, 974 | interaction, 975 | // @ts-expect-error This will complain because the autocomplete is typed here with [] 976 | focused.name, 977 | focused.value, 978 | this, 979 | optionsObj, 980 | ); 981 | } else { 982 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 983 | // @ts-ignore Tripped the endless type resolution again 984 | await this.autocompleteMiddleware.execute( 985 | command.autocomplete, 986 | interaction, 987 | // @ts-expect-error This will complain because the autocomplete is typed here with [] 988 | focused.name, 989 | focused.value, 990 | this, 991 | optionsObj, 992 | ); 993 | } 994 | } 995 | } 996 | 997 | private async handleContextMenu(interaction: ContextMenuCommandInteraction) { 998 | const commandName = interaction.commandName; 999 | const command = 1000 | interaction.commandType === ApplicationCommandType.Message 1001 | ? this.messageContextMenuMap.get(commandName) 1002 | : this.userContextMenuMap.get(commandName); 1003 | if (!command) { 1004 | this.logger?.error( 1005 | `Unregistered context command ${commandName} being run`, 1006 | ); 1007 | throw new Error(`Unregistered command ${commandName} was run`); 1008 | } else { 1009 | this.logger?.info(`Running context command ${commandName}`); 1010 | // @ts-expect-error This is going to complain because the context menu handler is typed with a more specific type 1011 | this.contextMenuMiddleware.execute(command.run, interaction, this); 1012 | } 1013 | } 1014 | 1015 | private async handlePageButton(interaction: ButtonInteraction) { 1016 | let page = this.activePages.get(interaction.message.id); 1017 | if (!page) { 1018 | page = await this.getPageFromMessage(interaction.message.id, interaction); 1019 | if (!page) { 1020 | return; 1021 | } 1022 | this.activePages.set(interaction.message.id, page); 1023 | const renderedPage = await page.render(); 1024 | if (!compareMessages(interaction.message, renderedPage)) { 1025 | await interaction.update({ 1026 | content: null, 1027 | embeds: [], 1028 | ...renderedPage, 1029 | components: renderedPage.components 1030 | ? pageComponentRowsToComponents(renderedPage.components, page) 1031 | : [], 1032 | fetchReply: true, 1033 | flags: renderedPage.flags as any, 1034 | }); 1035 | interaction.followUp({ 1036 | content: 1037 | "An older version of this page was stored, it's been updated. Click the button you want again.", 1038 | ephemeral: true, 1039 | }); 1040 | return; 1041 | } 1042 | } 1043 | const message = page.message; 1044 | if (message instanceof PageInteractionReplyMessage) { 1045 | // If this page was an interaction reply (meaning it was ephemeral), update the interaction to extend the lifetime of the token 1046 | page.message = new PageInteractionReplyMessage( 1047 | interaction.webhook, 1048 | message.id, 1049 | ); 1050 | // Store the updated page 1051 | const state = await page.serializeState(); 1052 | this.storePageState( 1053 | page.message.id, 1054 | page.constructor.pageId, 1055 | state, 1056 | messageToMessageData(page.message), 1057 | ); 1058 | } 1059 | page.latestInteraction = interaction; 1060 | page.handleId(interaction.customId.split(';')[1], interaction); 1061 | } 1062 | 1063 | private async handlePageSelect( 1064 | interaction: 1065 | | SelectMenuInteraction 1066 | | UserSelectMenuInteraction 1067 | | RoleSelectMenuInteraction 1068 | | ChannelSelectMenuInteraction 1069 | | MentionableSelectMenuInteraction, 1070 | ) { 1071 | let page = this.activePages.get(interaction.message.id); 1072 | if (!page) { 1073 | page = await this.getPageFromMessage(interaction.message.id, interaction); 1074 | if (!page) { 1075 | return; 1076 | } 1077 | this.activePages.set(interaction.message.id, page); 1078 | const renderedPage = await page.render(); 1079 | if (!compareMessages(interaction.message, renderedPage)) { 1080 | await interaction.update({ 1081 | ...renderedPage, 1082 | components: renderedPage.components 1083 | ? pageComponentRowsToComponents(renderedPage.components, page) 1084 | : [], 1085 | fetchReply: true, 1086 | flags: renderedPage.flags as any, 1087 | }); 1088 | interaction.followUp({ 1089 | content: 1090 | "An older version of this page was stored, it's been updated. Make your selection again.", 1091 | ephemeral: true, 1092 | }); 1093 | return; 1094 | } 1095 | } 1096 | const message = page.message; 1097 | if (message instanceof PageInteractionReplyMessage) { 1098 | // If this page was an interaction reply (meaning it was ephemeral), update the interaction to extend the lifetime of the token 1099 | page.message = new PageInteractionReplyMessage( 1100 | interaction.webhook, 1101 | message.id, 1102 | ); 1103 | // Store the updated page 1104 | const state = await page.serializeState(); 1105 | this.storePageState( 1106 | page.message.id, 1107 | page.constructor.pageId, 1108 | state, 1109 | messageToMessageData(page.message), 1110 | ); 1111 | } 1112 | page.latestInteraction = interaction; 1113 | page.handleId(interaction.customId.split(';')[1], interaction); 1114 | } 1115 | 1116 | private async handleModalSubmit(interaction: ModalSubmitInteraction) { 1117 | const modal = this.modalMap.get(interaction.customId); 1118 | if (!modal) return; 1119 | const values: Record = {}; 1120 | interaction.fields.fields.forEach((field) => { 1121 | values[field.customId] = field.value; 1122 | }); 1123 | modal.handler(interaction, values); 1124 | } 1125 | 1126 | async replyToInteractionWithPage( 1127 | page: Page, 1128 | interaction: MessageComponentInteraction | CommandInteraction, 1129 | ephemeral: boolean, 1130 | ) { 1131 | const messageOptions = await page.render(); 1132 | if (ephemeral) { 1133 | // We need to save the interaction instead since it doesn't return a message we can edit 1134 | const message = await interaction.reply({ 1135 | ...messageOptions, 1136 | content: messageOptions.content ?? undefined, 1137 | components: messageOptions.components 1138 | ? pageComponentRowsToComponents(messageOptions.components, page) 1139 | : [], 1140 | ephemeral: true, 1141 | fetchReply: true, 1142 | flags: messageOptions.flags as unknown as BitFieldResolvable< 1143 | 'SuppressEmbeds' | 'Ephemeral' | 'SuppressNotifications', 1144 | number 1145 | >, 1146 | }); 1147 | page.message = new PageInteractionReplyMessage( 1148 | interaction.webhook, 1149 | message.id, 1150 | ); 1151 | const state = await page.serializeState(); 1152 | this.storePageState( 1153 | message.id, 1154 | page.constructor.pageId, 1155 | state, 1156 | messageToMessageData(page.message), 1157 | ); 1158 | this.activePages.set(message.id, page); 1159 | } else { 1160 | const message = await interaction.reply({ 1161 | ...messageOptions, 1162 | content: messageOptions.content ?? undefined, 1163 | components: messageOptions.components 1164 | ? pageComponentRowsToComponents(messageOptions.components, page) 1165 | : [], 1166 | fetchReply: true, 1167 | flags: messageOptions.flags as unknown as BitFieldResolvable< 1168 | 'SuppressEmbeds' | 'Ephemeral' | 'SuppressNotifications', 1169 | number 1170 | >, 1171 | }); 1172 | page.message = new PageInteractionReplyMessage( 1173 | interaction.webhook, 1174 | message.id, 1175 | ); 1176 | const state = await page.serializeState(); 1177 | this.storePageState( 1178 | message.id, 1179 | page.constructor.pageId, 1180 | state, 1181 | messageToMessageData(page.message), 1182 | ); 1183 | this.activePages.set(message.id, page); 1184 | } 1185 | page.pageDidSend?.(); 1186 | } 1187 | 1188 | async sendPageToChannel(page: Page, channel: SendableChannels) { 1189 | const messageOptions = await page.render(); 1190 | const message = await channel.send({ 1191 | ...messageOptions, 1192 | content: messageOptions.content ?? undefined, 1193 | components: messageOptions.components 1194 | ? pageComponentRowsToComponents(messageOptions.components, page) 1195 | : [], 1196 | }); 1197 | page.message = message; 1198 | const state = await page.serializeState(); 1199 | this.storePageState( 1200 | message.id, 1201 | page.constructor.pageId, 1202 | state, 1203 | messageToMessageData(page.message), 1204 | ); 1205 | this.activePages.set(message.id, page); 1206 | page.pageDidSend?.(); 1207 | } 1208 | 1209 | async sendPageToForumChannel( 1210 | page: Page, 1211 | postTitle: string, 1212 | channel: ForumChannel, 1213 | ) { 1214 | const messageOptions = await page.render(); 1215 | const thread = await channel.threads.create({ 1216 | name: postTitle, 1217 | message: { 1218 | ...messageOptions, 1219 | content: messageOptions.content ?? undefined, 1220 | components: messageOptions.components 1221 | ? pageComponentRowsToComponents(messageOptions.components, page) 1222 | : [], 1223 | }, 1224 | }); 1225 | page.message = thread.lastMessage!; 1226 | const state = await page.serializeState(); 1227 | this.storePageState( 1228 | thread.lastMessage!.id, 1229 | page.constructor.pageId, 1230 | state, 1231 | messageToMessageData(page.message), 1232 | ); 1233 | this.activePages.set(thread.lastMessage!.id, page); 1234 | } 1235 | 1236 | async updatePage(page: Page, newState: S) { 1237 | if (!page.message) 1238 | throw new Error('You cannot update a page before it has been sent'); 1239 | page.state = newState; 1240 | const messageOptions = await page.render(); 1241 | const { message } = page; 1242 | if ( 1243 | message instanceof PageInteractionReplyMessage && 1244 | page.latestInteraction && 1245 | !(page.latestInteraction.deferred || page.latestInteraction.replied) 1246 | ) { 1247 | await page.latestInteraction.update({ 1248 | ...messageOptions, 1249 | components: messageOptions.components 1250 | ? pageComponentRowsToComponents(messageOptions.components, page) 1251 | : [], 1252 | flags: messageOptions.flags as any, 1253 | }); 1254 | } else { 1255 | await message.edit({ 1256 | ...messageOptions, 1257 | components: messageOptions.components 1258 | ? pageComponentRowsToComponents(messageOptions.components, page) 1259 | : [], 1260 | flags: messageOptions.flags as any, 1261 | }); 1262 | } 1263 | const state = await page.serializeState(); 1264 | this.activePages.set(message.id, page); 1265 | this.storePageState( 1266 | page.message instanceof Message ? page.message.id : page.message.id, 1267 | page.constructor.pageId, 1268 | state, 1269 | messageToMessageData(page.message), 1270 | ); 1271 | } 1272 | 1273 | async getPageFromMessage( 1274 | messageOrId: Message | string, 1275 | interaction: MessageComponentInteraction | CommandInteraction, 1276 | ) { 1277 | const id = typeof messageOrId === 'string' ? messageOrId : messageOrId.id; 1278 | const cachedPage = this.activePages.get(id); 1279 | if (!cachedPage) { 1280 | const { pageId, stateString, messageData } = await this.getPageState(id); 1281 | const message = 1282 | messageOrId instanceof Message 1283 | ? messageOrId 1284 | : await this.getMessage(JSON.parse(messageData)); 1285 | if (!message) 1286 | throw new Error( 1287 | `Failed to load Page message. ${JSON.stringify(messageData)}`, 1288 | ); 1289 | const { page: pageConstructor, deserialize } = 1290 | this.pageMap.get(pageId) ?? {}; 1291 | if (!pageConstructor || !deserialize) 1292 | throw new Error( 1293 | `A component tried to load a page type that isn't registered, ${pageId}`, 1294 | ); 1295 | const deserialized = await deserialize(stateString, interaction); 1296 | if (!('props' in deserialized)) { 1297 | if (message instanceof Message) { 1298 | await message.delete(); 1299 | } else { 1300 | await message.edit({ 1301 | content: 'This page has been closed', 1302 | components: [], 1303 | }); 1304 | } 1305 | return; 1306 | } 1307 | const { props, state } = deserialized; 1308 | // @ts-expect-error will complain, but we know this is a constructor and JS will complain if we don't do `new` 1309 | const newPage: Page = new pageConstructor(props); 1310 | newPage.state = state; 1311 | newPage.message = message; 1312 | const rendered = await newPage.render(); 1313 | if (rendered.components) 1314 | pageComponentRowsToComponents(rendered.components, newPage); 1315 | this.activePages.set(message.id, newPage); 1316 | return newPage; 1317 | } 1318 | return cachedPage; 1319 | } 1320 | 1321 | private async getMessage(messageData: MessageData | InteractionMessageData) { 1322 | if ('guildId' in messageData) { 1323 | try { 1324 | if (messageData.guildId !== 'dm') { 1325 | const guild = await this.guilds.fetch(messageData.guildId); 1326 | const channel = await guild.channels.fetch(messageData.channelId); 1327 | if (!(channel instanceof BaseGuildTextChannel)) 1328 | throw new Error( 1329 | `Channel for saved Page was not a text channel, this likely means there's something wrong with the storage. ${messageData.guildId}/${messageData.channelId}/${messageData.messageId}`, 1330 | ); 1331 | return channel.messages.fetch(messageData.messageId); 1332 | } else { 1333 | const channel = await this.channels.fetch(messageData.channelId); 1334 | if (!(channel instanceof DMChannel)) 1335 | throw new Error( 1336 | `Channel for saved Page was not a DMChannel, this likely means there's something wrong with the storage. ${messageData.guildId}/${messageData.channelId}/${messageData.messageId}`, 1337 | ); 1338 | } 1339 | } catch (e) { 1340 | throw new Error( 1341 | `Tried to fetch a message the bot can no longer see: ${messageData.guildId}/${messageData.channelId}/${messageData.messageId}`, 1342 | ); 1343 | } 1344 | } else { 1345 | return new PageInteractionReplyMessage( 1346 | new InteractionWebhook( 1347 | this, 1348 | this.application.id, 1349 | messageData.webhookToken, 1350 | ), 1351 | messageData.messageId, 1352 | ); 1353 | } 1354 | return; 1355 | } 1356 | } 1357 | 1358 | function messageToMessageData( 1359 | message: Message | PageInteractionReplyMessage, 1360 | ): string { 1361 | if (message instanceof Message) { 1362 | return JSON.stringify({ 1363 | messageId: message.id, 1364 | channelId: message.channelId, 1365 | guildId: message.guildId ?? 'dm', 1366 | }); 1367 | } else { 1368 | return JSON.stringify({ 1369 | webhookToken: message.webhook.token, 1370 | messageId: message.id, 1371 | }); 1372 | } 1373 | } 1374 | -------------------------------------------------------------------------------- /src/TemplateModal.ts: -------------------------------------------------------------------------------- 1 | import { TextInputBuilder } from '@discordjs/builders'; 2 | import { 3 | ActionRowBuilder, 4 | ComponentType, 5 | ModalActionRowComponentBuilder, 6 | ModalBuilder, 7 | ModalSubmitInteraction, 8 | TextInputStyle, 9 | } from 'discord.js'; 10 | 11 | type ExtractFromDelimiters< 12 | S extends string, 13 | L extends string, 14 | R extends string 15 | > = string extends S 16 | ? string[] 17 | : S extends '' 18 | ? [] 19 | : S extends `${infer _T}${L}${infer U}${R}${infer V}` 20 | ? [U, ...ExtractFromDelimiters] 21 | : []; 22 | 23 | type ReadonlyTextInputProps = { 24 | readonly customId: string; 25 | readonly label: string; 26 | readonly required: boolean; 27 | readonly maxLength?: number; 28 | readonly minLength?: number; 29 | readonly placeholder?: string; 30 | readonly style?: TextInputStyle; 31 | readonly value?: string; 32 | }; 33 | 34 | type ModalValuesType> = { 35 | [Key in T[number]['customId']]: string; 36 | }; 37 | 38 | type ModalValuesVariablesType> = 39 | { 40 | [Key in keyof T]: T[Key] extends { 41 | readonly value: infer V; 42 | } 43 | ? V extends string 44 | ? ExtractFromDelimiters[number] 45 | : never 46 | : never; 47 | }; 48 | 49 | type ModalPlaceholderVariablesType< 50 | T extends ReadonlyArray 51 | > = { 52 | [Key in keyof T]: T[Key] extends { 53 | readonly placeholder: infer V; 54 | } 55 | ? V extends string 56 | ? ExtractFromDelimiters[number] 57 | : never 58 | : never; 59 | }; 60 | 61 | type ModalLabelVariablesType> = 62 | { 63 | [Key in keyof T]: T[Key] extends { 64 | readonly label: infer V; 65 | } 66 | ? V extends string 67 | ? ExtractFromDelimiters[number] 68 | : never 69 | : never; 70 | }; 71 | 72 | type ModalComponentVariablesType< 73 | T extends ReadonlyArray 74 | > = 75 | | ModalLabelVariablesType[number] 76 | | ModalPlaceholderVariablesType[number] 77 | | ModalValuesVariablesType[number]; 78 | 79 | type GetModalVariablesInput< 80 | T extends ReadonlyArray, 81 | U extends string 82 | > = { 83 | [Key in 84 | | ModalComponentVariablesType 85 | | ExtractFromDelimiters[number]]: string; 86 | }; 87 | 88 | function replaceVariables(target: string, variables: any) { 89 | return target.replace(/\{\{(.+?)\}\}/g, (_, variable) => variables[variable]); 90 | } 91 | 92 | export class TemplateModal< 93 | const T extends ReadonlyArray, 94 | U extends string 95 | > { 96 | public readonly title: string; 97 | public readonly customId: string; 98 | public readonly components: T; 99 | public readonly handler: ( 100 | interaction: ModalSubmitInteraction, 101 | values: ModalValuesType 102 | ) => void; 103 | 104 | public constructor( 105 | title: U, 106 | customId: string, 107 | components: T, 108 | handler: ( 109 | interaction: ModalSubmitInteraction, 110 | values: ModalValuesType 111 | ) => void 112 | ) { 113 | this.title = title; 114 | this.customId = customId; 115 | this.components = components; 116 | this.handler = handler; 117 | } 118 | 119 | public getModal(variables: GetModalVariablesInput): ModalBuilder { 120 | const title = replaceVariables(this.title, variables); 121 | const modal = new ModalBuilder({ 122 | title, 123 | customId: this.customId, 124 | components: this.components.map((component) => { 125 | const row = new ActionRowBuilder(); 126 | const textInput = new TextInputBuilder({ 127 | type: ComponentType.TextInput, 128 | custom_id: component.customId, 129 | style: component.style ?? TextInputStyle.Short, 130 | label: replaceVariables(component.label, variables), 131 | required: component.required, 132 | }); 133 | if (component.maxLength) textInput.setMaxLength(component.maxLength); 134 | if (component.minLength) textInput.setMinLength(component.minLength); 135 | if (component.placeholder) 136 | textInput.setPlaceholder( 137 | replaceVariables(component.placeholder, variables) 138 | ); 139 | if (component.value) 140 | textInput.setValue(replaceVariables(component.value, variables)); 141 | row.addComponents(textInput); 142 | return row; 143 | }), 144 | }); 145 | return modal; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ContextMenuBase'; 2 | export * from './SlashasaurusClient'; 3 | export * from './SlashCommandBase'; 4 | export * from './Page'; 5 | export { Middleware } from './MiddlewarePipeline'; 6 | export * from './utilityTypes'; 7 | export * from './CustomErrors'; 8 | export * from './OptionTypes'; 9 | export * from './PageComponents'; 10 | export { TemplateModal } from './TemplateModal'; 11 | -------------------------------------------------------------------------------- /src/utilityTypes.ts: -------------------------------------------------------------------------------- 1 | import { APIInteractionDataResolvedChannel } from 'discord-api-types/v9'; 2 | import { 3 | ApplicationCommandOptionChoiceData, 4 | CategoryChannel, 5 | ChannelType, 6 | CommandInteractionOptionResolver, 7 | ForumChannel, 8 | NewsChannel, 9 | StageChannel, 10 | TextChannel, 11 | ThreadChannel, 12 | VoiceChannel, 13 | } from 'discord.js'; 14 | import { ApplicationCommandOptionData, OptionsDataArray } from './OptionTypes'; 15 | 16 | export type ExtractArrayType = ((a: T) => never) extends ( 17 | a: Array, 18 | ) => never 19 | ? H 20 | : never; 21 | 22 | export type MaybePromise = T | Promise; 23 | 24 | type LengthOfReadonly> = T['length']; 25 | type HeadOfReadonly> = T extends [] 26 | ? never 27 | : T[0]; 28 | type TailOfReadonly> = (( 29 | ...array: T 30 | ) => never) extends (head: never, ...tail: infer Tail_) => never 31 | ? Tail_ 32 | : never; 33 | 34 | type MapChoicesToValues< 35 | T extends readonly ApplicationCommandOptionChoiceData[], 36 | > = { 37 | [K in keyof T]: T[K] extends ApplicationCommandOptionChoiceData 38 | ? T[K]['value'] 39 | : never; 40 | }[number]; 41 | 42 | type HasChoices = { 43 | choices: readonly [ 44 | ApplicationCommandOptionChoiceData, 45 | ...ApplicationCommandOptionChoiceData[], 46 | ]; 47 | }; 48 | 49 | type CommandInteractionOptionResolverReturn< 50 | T extends keyof CommandInteractionOptionResolver, 51 | // eslint-disable-next-line @typescript-eslint/ban-types 52 | > = CommandInteractionOptionResolver[T] extends Function 53 | ? // @ts-expect-error this works, it just doesn't narrow the type here 54 | NonNullable> 55 | : never; 56 | 57 | export type OptionsMap = { 58 | STRING: CommandInteractionOptionResolverReturn<'getString'>; 59 | 3: CommandInteractionOptionResolverReturn<'getString'>; 60 | INTEGER: CommandInteractionOptionResolverReturn<'getInteger'>; 61 | 4: CommandInteractionOptionResolverReturn<'getInteger'>; 62 | BOOLEAN: CommandInteractionOptionResolverReturn<'getBoolean'>; 63 | 5: CommandInteractionOptionResolverReturn<'getBoolean'>; 64 | USER: 65 | | CommandInteractionOptionResolverReturn<'getMember'> 66 | | CommandInteractionOptionResolverReturn<'getUser'>; 67 | 6: 68 | | CommandInteractionOptionResolverReturn<'getMember'> 69 | | CommandInteractionOptionResolverReturn<'getUser'>; 70 | CHANNEL: CommandInteractionOptionResolverReturn<'getChannel'>; 71 | 7: CommandInteractionOptionResolverReturn<'getChannel'>; 72 | ROLE: CommandInteractionOptionResolverReturn<'getRole'>; 73 | 8: CommandInteractionOptionResolverReturn<'getRole'>; 74 | MENTIONABLE: 75 | | CommandInteractionOptionResolverReturn<'getMember'> 76 | | CommandInteractionOptionResolverReturn<'getRole'> 77 | | CommandInteractionOptionResolverReturn<'getUser'>; 78 | 9: 79 | | CommandInteractionOptionResolverReturn<'getMember'> 80 | | CommandInteractionOptionResolverReturn<'getRole'> 81 | | CommandInteractionOptionResolverReturn<'getUser'>; 82 | NUMBER: CommandInteractionOptionResolverReturn<'getInteger'>; 83 | 10: CommandInteractionOptionResolverReturn<'getInteger'>; 84 | ATTACHMENT: CommandInteractionOptionResolverReturn<'getAttachment'>; 85 | 11: CommandInteractionOptionResolverReturn<'getAttachment'>; 86 | }; 87 | 88 | type ChannelsMap = { 89 | 0: TextChannel; 90 | 1: never; // DM 91 | 2: VoiceChannel; 92 | 3: never; // Group DM 93 | 4: CategoryChannel; 94 | 5: NewsChannel; 95 | 10: ThreadChannel; 96 | 11: ThreadChannel; 97 | 12: ThreadChannel; 98 | 13: StageChannel; 99 | 14: never; // Directory 100 | 15: ForumChannel; // Forum 101 | 16: never; // Guild Media Channel - deprecated 102 | }; 103 | 104 | type MapChannelTypesToChannels> = { 105 | [K in keyof T]: T[K] extends ChannelType ? ChannelsMap[T[K]] : never; 106 | }[number]; 107 | 108 | type OptionToValue = T extends { 109 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 110 | transformer: (value: any) => unknown; 111 | } 112 | ? Awaited> 113 | : T extends HasChoices 114 | ? MapChoicesToValues 115 | : T extends { 116 | channelTypes: ReadonlyArray; 117 | } 118 | ? 119 | | MapChannelTypesToChannels 120 | | APIInteractionDataResolvedChannel 121 | : OptionsMap[T['type']]; 122 | 123 | export type CommandOptionsObject = { 124 | [Key in T[number]['name']]: Extract< 125 | T[number], 126 | { name: Key } 127 | >['required'] extends true 128 | ? OptionToValue> 129 | : OptionToValue> | null; 130 | }; 131 | 132 | type MapOptionToAutocompleteName = 133 | T extends { autocomplete: true } 134 | ? T extends { 135 | // eslint-disable-next-line @typescript-eslint/ban-types 136 | onAutocomplete: Function; 137 | } 138 | ? never 139 | : T['name'] 140 | : never; 141 | 142 | export type MapOptionsToAutocompleteNames< 143 | T extends readonly ApplicationCommandOptionData[], 144 | > = 145 | LengthOfReadonly extends 0 146 | ? never 147 | : LengthOfReadonly extends 1 148 | ? MapOptionToAutocompleteName> 149 | : 150 | | MapOptionToAutocompleteName> 151 | | MapOptionsToAutocompleteNames>; 152 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": ["dom", "es6", "es2017", "esnext.asynciterable"], 6 | "declaration": true, 7 | "skipLibCheck": true, 8 | "outDir": "./dist", 9 | "moduleResolution": "node", 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowSyntheticDefaultImports": true, 19 | "esModuleInterop": true, 20 | "emitDecoratorMetadata": true, 21 | "experimentalDecorators": true, 22 | "resolveJsonModule": true, 23 | "baseUrl": "." 24 | }, 25 | "exclude": ["node_modules"], 26 | "include": ["./src/**/*.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@discordjs/builders@^1.10.0": 6 | version "1.10.0" 7 | resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-1.10.0.tgz#de0811ea22a3ecb893a3e37923760c9e9e722d74" 8 | integrity sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg== 9 | dependencies: 10 | "@discordjs/formatters" "^0.6.0" 11 | "@discordjs/util" "^1.1.1" 12 | "@sapphire/shapeshift" "^4.0.0" 13 | discord-api-types "^0.37.114" 14 | fast-deep-equal "^3.1.3" 15 | ts-mixer "^6.0.4" 16 | tslib "^2.6.3" 17 | 18 | "@discordjs/collection@1.5.3": 19 | version "1.5.3" 20 | resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-1.5.3.tgz#5a1250159ebfff9efa4f963cfa7e97f1b291be18" 21 | integrity sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ== 22 | 23 | "@discordjs/collection@^2.1.0", "@discordjs/collection@^2.1.1": 24 | version "2.1.1" 25 | resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-2.1.1.tgz#901917bc538c12b9c3613036d317847baee08cae" 26 | integrity sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg== 27 | 28 | "@discordjs/formatters@^0.6.0": 29 | version "0.6.0" 30 | resolved "https://registry.yarnpkg.com/@discordjs/formatters/-/formatters-0.6.0.tgz#9a4ca075ab16a4823da9258bc5c39f06a2519235" 31 | integrity sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw== 32 | dependencies: 33 | discord-api-types "^0.37.114" 34 | 35 | "@discordjs/rest@^2.4.1", "@discordjs/rest@^2.4.2": 36 | version "2.4.2" 37 | resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-2.4.2.tgz#8c18c717cd81521d6f886a33e2dd7a454be97e41" 38 | integrity sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g== 39 | dependencies: 40 | "@discordjs/collection" "^2.1.1" 41 | "@discordjs/util" "^1.1.1" 42 | "@sapphire/async-queue" "^1.5.3" 43 | "@sapphire/snowflake" "^3.5.3" 44 | "@vladfrangu/async_event_emitter" "^2.4.6" 45 | discord-api-types "^0.37.114" 46 | magic-bytes.js "^1.10.0" 47 | tslib "^2.6.3" 48 | undici "6.19.8" 49 | 50 | "@discordjs/util@^1.1.0", "@discordjs/util@^1.1.1": 51 | version "1.1.1" 52 | resolved "https://registry.yarnpkg.com/@discordjs/util/-/util-1.1.1.tgz#bafcde0faa116c834da1258d78ec237080bbab29" 53 | integrity sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g== 54 | 55 | "@discordjs/ws@^1.2.0": 56 | version "1.2.0" 57 | resolved "https://registry.yarnpkg.com/@discordjs/ws/-/ws-1.2.0.tgz#befdaab25230814dca9bc863ece6077900336047" 58 | integrity sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg== 59 | dependencies: 60 | "@discordjs/collection" "^2.1.0" 61 | "@discordjs/rest" "^2.4.1" 62 | "@discordjs/util" "^1.1.0" 63 | "@sapphire/async-queue" "^1.5.2" 64 | "@types/ws" "^8.5.10" 65 | "@vladfrangu/async_event_emitter" "^2.2.4" 66 | discord-api-types "^0.37.114" 67 | tslib "^2.6.2" 68 | ws "^8.17.0" 69 | 70 | "@eslint/eslintrc@^1.3.0": 71 | version "1.3.0" 72 | resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f" 73 | integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw== 74 | dependencies: 75 | ajv "^6.12.4" 76 | debug "^4.3.2" 77 | espree "^9.3.2" 78 | globals "^13.15.0" 79 | ignore "^5.2.0" 80 | import-fresh "^3.2.1" 81 | js-yaml "^4.1.0" 82 | minimatch "^3.1.2" 83 | strip-json-comments "^3.1.1" 84 | 85 | "@humanwhocodes/config-array@^0.9.2": 86 | version "0.9.5" 87 | resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7" 88 | integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw== 89 | dependencies: 90 | "@humanwhocodes/object-schema" "^1.2.1" 91 | debug "^4.1.1" 92 | minimatch "^3.0.4" 93 | 94 | "@humanwhocodes/object-schema@^1.2.1": 95 | version "1.2.1" 96 | resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" 97 | integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== 98 | 99 | "@nodelib/fs.scandir@2.1.5": 100 | version "2.1.5" 101 | resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" 102 | integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== 103 | dependencies: 104 | "@nodelib/fs.stat" "2.0.5" 105 | run-parallel "^1.1.9" 106 | 107 | "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": 108 | version "2.0.5" 109 | resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" 110 | integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 111 | 112 | "@nodelib/fs.walk@^1.2.3": 113 | version "1.2.8" 114 | resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" 115 | integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== 116 | dependencies: 117 | "@nodelib/fs.scandir" "2.1.5" 118 | fastq "^1.6.0" 119 | 120 | "@sapphire/async-queue@^1.5.2", "@sapphire/async-queue@^1.5.3": 121 | version "1.5.5" 122 | resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.5.5.tgz#2b18d402bb920b65b13ad4ed8dfb6c386300dd84" 123 | integrity sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg== 124 | 125 | "@sapphire/shapeshift@^4.0.0": 126 | version "4.0.0" 127 | resolved "https://registry.yarnpkg.com/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz#86c1b41002ff5d0b2ad21cbc3418b06834b89040" 128 | integrity sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg== 129 | dependencies: 130 | fast-deep-equal "^3.1.3" 131 | lodash "^4.17.21" 132 | 133 | "@sapphire/snowflake@3.5.3": 134 | version "3.5.3" 135 | resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.3.tgz#0c102aa2ec5b34f806e9bc8625fc6a5e1d0a0c6a" 136 | integrity sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ== 137 | 138 | "@sapphire/snowflake@^3.5.3": 139 | version "3.5.5" 140 | resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.5.tgz#33a60ab4231e3cab29e8a0077f342125f2c8d1bd" 141 | integrity sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ== 142 | 143 | "@types/json-schema@^7.0.9": 144 | version "7.0.11" 145 | resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" 146 | integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== 147 | 148 | "@types/node@*": 149 | version "16.9.4" 150 | resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.4.tgz#a12f0ee7847cf17a97f6fdf1093cb7a9af23cca4" 151 | integrity sha512-KDazLNYAGIuJugdbULwFZULF9qQ13yNWEBFnfVpqlpgAAo6H/qnM9RjBgh0A0kmHf3XxAKLdN5mTIng9iUvVLA== 152 | 153 | "@types/node@^17.0.21": 154 | version "17.0.21" 155 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" 156 | integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ== 157 | 158 | "@types/ws@^8.5.10": 159 | version "8.5.14" 160 | resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.14.tgz#93d44b268c9127d96026cf44353725dd9b6c3c21" 161 | integrity sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw== 162 | dependencies: 163 | "@types/node" "*" 164 | 165 | "@typescript-eslint/eslint-plugin@^5.27.0": 166 | version "5.27.0" 167 | resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz#23d82a4f21aaafd8f69dbab7e716323bb6695cc8" 168 | integrity sha512-DDrIA7GXtmHXr1VCcx9HivA39eprYBIFxbQEHI6NyraRDxCGpxAFiYQAT/1Y0vh1C+o2vfBiy4IuPoXxtTZCAQ== 169 | dependencies: 170 | "@typescript-eslint/scope-manager" "5.27.0" 171 | "@typescript-eslint/type-utils" "5.27.0" 172 | "@typescript-eslint/utils" "5.27.0" 173 | debug "^4.3.4" 174 | functional-red-black-tree "^1.0.1" 175 | ignore "^5.2.0" 176 | regexpp "^3.2.0" 177 | semver "^7.3.7" 178 | tsutils "^3.21.0" 179 | 180 | "@typescript-eslint/parser@^5.27.0": 181 | version "5.27.0" 182 | resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.27.0.tgz#62bb091ed5cf9c7e126e80021bb563dcf36b6b12" 183 | integrity sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA== 184 | dependencies: 185 | "@typescript-eslint/scope-manager" "5.27.0" 186 | "@typescript-eslint/types" "5.27.0" 187 | "@typescript-eslint/typescript-estree" "5.27.0" 188 | debug "^4.3.4" 189 | 190 | "@typescript-eslint/scope-manager@5.27.0": 191 | version "5.27.0" 192 | resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz#a272178f613050ed62f51f69aae1e19e870a8bbb" 193 | integrity sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g== 194 | dependencies: 195 | "@typescript-eslint/types" "5.27.0" 196 | "@typescript-eslint/visitor-keys" "5.27.0" 197 | 198 | "@typescript-eslint/type-utils@5.27.0": 199 | version "5.27.0" 200 | resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.27.0.tgz#36fd95f6747412251d79c795b586ba766cf0974b" 201 | integrity sha512-vpTvRRchaf628Hb/Xzfek+85o//zEUotr1SmexKvTfs7czXfYjXVT/a5yDbpzLBX1rhbqxjDdr1Gyo0x1Fc64g== 202 | dependencies: 203 | "@typescript-eslint/utils" "5.27.0" 204 | debug "^4.3.4" 205 | tsutils "^3.21.0" 206 | 207 | "@typescript-eslint/types@5.27.0": 208 | version "5.27.0" 209 | resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.27.0.tgz#c3f44b9dda6177a9554f94a74745ca495ba9c001" 210 | integrity sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A== 211 | 212 | "@typescript-eslint/typescript-estree@5.27.0": 213 | version "5.27.0" 214 | resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz#7965f5b553c634c5354a47dcce0b40b94611e995" 215 | integrity sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ== 216 | dependencies: 217 | "@typescript-eslint/types" "5.27.0" 218 | "@typescript-eslint/visitor-keys" "5.27.0" 219 | debug "^4.3.4" 220 | globby "^11.1.0" 221 | is-glob "^4.0.3" 222 | semver "^7.3.7" 223 | tsutils "^3.21.0" 224 | 225 | "@typescript-eslint/utils@5.27.0": 226 | version "5.27.0" 227 | resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.27.0.tgz#d0021cbf686467a6a9499bd0589e19665f9f7e71" 228 | integrity sha512-nZvCrkIJppym7cIbP3pOwIkAefXOmfGPnCM0LQfzNaKxJHI6VjI8NC662uoiPlaf5f6ymkTy9C3NQXev2mdXmA== 229 | dependencies: 230 | "@types/json-schema" "^7.0.9" 231 | "@typescript-eslint/scope-manager" "5.27.0" 232 | "@typescript-eslint/types" "5.27.0" 233 | "@typescript-eslint/typescript-estree" "5.27.0" 234 | eslint-scope "^5.1.1" 235 | eslint-utils "^3.0.0" 236 | 237 | "@typescript-eslint/visitor-keys@5.27.0": 238 | version "5.27.0" 239 | resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz#97aa9a5d2f3df8215e6d3b77f9d214a24db269bd" 240 | integrity sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA== 241 | dependencies: 242 | "@typescript-eslint/types" "5.27.0" 243 | eslint-visitor-keys "^3.3.0" 244 | 245 | "@vladfrangu/async_event_emitter@^2.2.4", "@vladfrangu/async_event_emitter@^2.4.6": 246 | version "2.4.6" 247 | resolved "https://registry.yarnpkg.com/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz#508b6c45b03f917112a9008180b308ba0e4d1805" 248 | integrity sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA== 249 | 250 | acorn-jsx@^5.3.2: 251 | version "5.3.2" 252 | resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" 253 | integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== 254 | 255 | acorn@^8.7.1: 256 | version "8.7.1" 257 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" 258 | integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== 259 | 260 | ajv@^6.10.0, ajv@^6.12.4: 261 | version "6.12.6" 262 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" 263 | integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== 264 | dependencies: 265 | fast-deep-equal "^3.1.1" 266 | fast-json-stable-stringify "^2.0.0" 267 | json-schema-traverse "^0.4.1" 268 | uri-js "^4.2.2" 269 | 270 | ansi-regex@^5.0.1: 271 | version "5.0.1" 272 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 273 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 274 | 275 | ansi-styles@^4.1.0: 276 | version "4.3.0" 277 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 278 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 279 | dependencies: 280 | color-convert "^2.0.1" 281 | 282 | argparse@^2.0.1: 283 | version "2.0.1" 284 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 285 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 286 | 287 | array-union@^2.1.0: 288 | version "2.1.0" 289 | resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" 290 | integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== 291 | 292 | balanced-match@^1.0.0: 293 | version "1.0.2" 294 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 295 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 296 | 297 | brace-expansion@^1.1.7: 298 | version "1.1.11" 299 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 300 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 301 | dependencies: 302 | balanced-match "^1.0.0" 303 | concat-map "0.0.1" 304 | 305 | braces@^3.0.2: 306 | version "3.0.2" 307 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 308 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 309 | dependencies: 310 | fill-range "^7.0.1" 311 | 312 | callsites@^3.0.0: 313 | version "3.1.0" 314 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 315 | integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== 316 | 317 | chalk@^4.0.0: 318 | version "4.1.2" 319 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 320 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 321 | dependencies: 322 | ansi-styles "^4.1.0" 323 | supports-color "^7.1.0" 324 | 325 | color-convert@^2.0.1: 326 | version "2.0.1" 327 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 328 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 329 | dependencies: 330 | color-name "~1.1.4" 331 | 332 | color-name@~1.1.4: 333 | version "1.1.4" 334 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 335 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 336 | 337 | concat-map@0.0.1: 338 | version "0.0.1" 339 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 340 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 341 | 342 | cross-spawn@^7.0.2: 343 | version "7.0.3" 344 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" 345 | integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== 346 | dependencies: 347 | path-key "^3.1.0" 348 | shebang-command "^2.0.0" 349 | which "^2.0.1" 350 | 351 | debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: 352 | version "4.3.4" 353 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 354 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 355 | dependencies: 356 | ms "2.1.2" 357 | 358 | deep-is@^0.1.3: 359 | version "0.1.4" 360 | resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" 361 | integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== 362 | 363 | dir-glob@^3.0.1: 364 | version "3.0.1" 365 | resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" 366 | integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== 367 | dependencies: 368 | path-type "^4.0.0" 369 | 370 | discord-api-types@^0.37.114: 371 | version "0.37.119" 372 | resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.119.tgz#29e32f635351da32dd4c92defcafdeb65156ad33" 373 | integrity sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg== 374 | 375 | discord-api-types@^0.37.36: 376 | version "0.37.36" 377 | resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.36.tgz#650a8f66dce2c5e54a8c2275db74a0bb7936430d" 378 | integrity sha512-Nlxmp10UpVr/utgZ9uODQvG2Or+5w7LFrvFMswyeKC9l/+UaqGT6H0OVgEFhu9GEO4U6K7NNO5W8Carv7irnCA== 379 | 380 | discord.js@14.17.3: 381 | version "14.17.3" 382 | resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-14.17.3.tgz#e3799a3a2bc66c1b95f3f7053ce7b0e541a81361" 383 | integrity sha512-8/j8udc3CU7dz3Eqch64UaSHoJtUT6IXK4da5ixjbav4NAXJicloWswD/iwn1ImZEMoAV3LscsdO0zhBh6H+0Q== 384 | dependencies: 385 | "@discordjs/builders" "^1.10.0" 386 | "@discordjs/collection" "1.5.3" 387 | "@discordjs/formatters" "^0.6.0" 388 | "@discordjs/rest" "^2.4.2" 389 | "@discordjs/util" "^1.1.1" 390 | "@discordjs/ws" "^1.2.0" 391 | "@sapphire/snowflake" "3.5.3" 392 | discord-api-types "^0.37.114" 393 | fast-deep-equal "3.1.3" 394 | lodash.snakecase "4.1.1" 395 | tslib "^2.6.3" 396 | undici "6.19.8" 397 | 398 | doctrine@^3.0.0: 399 | version "3.0.0" 400 | resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" 401 | integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== 402 | dependencies: 403 | esutils "^2.0.2" 404 | 405 | escape-string-regexp@^4.0.0: 406 | version "4.0.0" 407 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 408 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 409 | 410 | eslint-scope@^5.1.1: 411 | version "5.1.1" 412 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" 413 | integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== 414 | dependencies: 415 | esrecurse "^4.3.0" 416 | estraverse "^4.1.1" 417 | 418 | eslint-scope@^7.1.1: 419 | version "7.1.1" 420 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" 421 | integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== 422 | dependencies: 423 | esrecurse "^4.3.0" 424 | estraverse "^5.2.0" 425 | 426 | eslint-utils@^3.0.0: 427 | version "3.0.0" 428 | resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" 429 | integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== 430 | dependencies: 431 | eslint-visitor-keys "^2.0.0" 432 | 433 | eslint-visitor-keys@^2.0.0: 434 | version "2.1.0" 435 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" 436 | integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== 437 | 438 | eslint-visitor-keys@^3.3.0: 439 | version "3.3.0" 440 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" 441 | integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== 442 | 443 | eslint@^8.16.0: 444 | version "8.16.0" 445 | resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.16.0.tgz#6d936e2d524599f2a86c708483b4c372c5d3bbae" 446 | integrity sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA== 447 | dependencies: 448 | "@eslint/eslintrc" "^1.3.0" 449 | "@humanwhocodes/config-array" "^0.9.2" 450 | ajv "^6.10.0" 451 | chalk "^4.0.0" 452 | cross-spawn "^7.0.2" 453 | debug "^4.3.2" 454 | doctrine "^3.0.0" 455 | escape-string-regexp "^4.0.0" 456 | eslint-scope "^7.1.1" 457 | eslint-utils "^3.0.0" 458 | eslint-visitor-keys "^3.3.0" 459 | espree "^9.3.2" 460 | esquery "^1.4.0" 461 | esutils "^2.0.2" 462 | fast-deep-equal "^3.1.3" 463 | file-entry-cache "^6.0.1" 464 | functional-red-black-tree "^1.0.1" 465 | glob-parent "^6.0.1" 466 | globals "^13.15.0" 467 | ignore "^5.2.0" 468 | import-fresh "^3.0.0" 469 | imurmurhash "^0.1.4" 470 | is-glob "^4.0.0" 471 | js-yaml "^4.1.0" 472 | json-stable-stringify-without-jsonify "^1.0.1" 473 | levn "^0.4.1" 474 | lodash.merge "^4.6.2" 475 | minimatch "^3.1.2" 476 | natural-compare "^1.4.0" 477 | optionator "^0.9.1" 478 | regexpp "^3.2.0" 479 | strip-ansi "^6.0.1" 480 | strip-json-comments "^3.1.0" 481 | text-table "^0.2.0" 482 | v8-compile-cache "^2.0.3" 483 | 484 | espree@^9.3.2: 485 | version "9.3.2" 486 | resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596" 487 | integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA== 488 | dependencies: 489 | acorn "^8.7.1" 490 | acorn-jsx "^5.3.2" 491 | eslint-visitor-keys "^3.3.0" 492 | 493 | esquery@^1.4.0: 494 | version "1.4.0" 495 | resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" 496 | integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== 497 | dependencies: 498 | estraverse "^5.1.0" 499 | 500 | esrecurse@^4.3.0: 501 | version "4.3.0" 502 | resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" 503 | integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== 504 | dependencies: 505 | estraverse "^5.2.0" 506 | 507 | estraverse@^4.1.1: 508 | version "4.3.0" 509 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" 510 | integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== 511 | 512 | estraverse@^5.1.0, estraverse@^5.2.0: 513 | version "5.3.0" 514 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" 515 | integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== 516 | 517 | esutils@^2.0.2: 518 | version "2.0.3" 519 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" 520 | integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 521 | 522 | fast-deep-equal@3.1.3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: 523 | version "3.1.3" 524 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 525 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 526 | 527 | fast-glob@^3.2.9: 528 | version "3.2.11" 529 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" 530 | integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== 531 | dependencies: 532 | "@nodelib/fs.stat" "^2.0.2" 533 | "@nodelib/fs.walk" "^1.2.3" 534 | glob-parent "^5.1.2" 535 | merge2 "^1.3.0" 536 | micromatch "^4.0.4" 537 | 538 | fast-json-stable-stringify@^2.0.0: 539 | version "2.1.0" 540 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 541 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 542 | 543 | fast-levenshtein@^2.0.6: 544 | version "2.0.6" 545 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" 546 | integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== 547 | 548 | fastq@^1.6.0: 549 | version "1.13.0" 550 | resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" 551 | integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== 552 | dependencies: 553 | reusify "^1.0.4" 554 | 555 | file-entry-cache@^6.0.1: 556 | version "6.0.1" 557 | resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" 558 | integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== 559 | dependencies: 560 | flat-cache "^3.0.4" 561 | 562 | fill-range@^7.0.1: 563 | version "7.0.1" 564 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 565 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 566 | dependencies: 567 | to-regex-range "^5.0.1" 568 | 569 | flat-cache@^3.0.4: 570 | version "3.0.4" 571 | resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" 572 | integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== 573 | dependencies: 574 | flatted "^3.1.0" 575 | rimraf "^3.0.2" 576 | 577 | flatted@^3.1.0: 578 | version "3.2.5" 579 | resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" 580 | integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== 581 | 582 | fs.realpath@^1.0.0: 583 | version "1.0.0" 584 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 585 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 586 | 587 | functional-red-black-tree@^1.0.1: 588 | version "1.0.1" 589 | resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" 590 | integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== 591 | 592 | glob-parent@^5.1.2: 593 | version "5.1.2" 594 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 595 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 596 | dependencies: 597 | is-glob "^4.0.1" 598 | 599 | glob-parent@^6.0.1: 600 | version "6.0.2" 601 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" 602 | integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== 603 | dependencies: 604 | is-glob "^4.0.3" 605 | 606 | glob@^7.1.3: 607 | version "7.1.7" 608 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" 609 | integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== 610 | dependencies: 611 | fs.realpath "^1.0.0" 612 | inflight "^1.0.4" 613 | inherits "2" 614 | minimatch "^3.0.4" 615 | once "^1.3.0" 616 | path-is-absolute "^1.0.0" 617 | 618 | globals@^13.15.0: 619 | version "13.15.0" 620 | resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" 621 | integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog== 622 | dependencies: 623 | type-fest "^0.20.2" 624 | 625 | globby@^11.1.0: 626 | version "11.1.0" 627 | resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" 628 | integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== 629 | dependencies: 630 | array-union "^2.1.0" 631 | dir-glob "^3.0.1" 632 | fast-glob "^3.2.9" 633 | ignore "^5.2.0" 634 | merge2 "^1.4.1" 635 | slash "^3.0.0" 636 | 637 | has-flag@^4.0.0: 638 | version "4.0.0" 639 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 640 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 641 | 642 | ignore@^5.2.0: 643 | version "5.2.0" 644 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" 645 | integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== 646 | 647 | import-fresh@^3.0.0, import-fresh@^3.2.1: 648 | version "3.3.0" 649 | resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" 650 | integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== 651 | dependencies: 652 | parent-module "^1.0.0" 653 | resolve-from "^4.0.0" 654 | 655 | imurmurhash@^0.1.4: 656 | version "0.1.4" 657 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 658 | integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== 659 | 660 | inflight@^1.0.4: 661 | version "1.0.6" 662 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 663 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 664 | dependencies: 665 | once "^1.3.0" 666 | wrappy "1" 667 | 668 | inherits@2: 669 | version "2.0.4" 670 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 671 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 672 | 673 | is-extglob@^2.1.1: 674 | version "2.1.1" 675 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 676 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 677 | 678 | is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: 679 | version "4.0.3" 680 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 681 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 682 | dependencies: 683 | is-extglob "^2.1.1" 684 | 685 | is-number@^7.0.0: 686 | version "7.0.0" 687 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 688 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 689 | 690 | isexe@^2.0.0: 691 | version "2.0.0" 692 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 693 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 694 | 695 | js-yaml@^4.1.0: 696 | version "4.1.0" 697 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" 698 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 699 | dependencies: 700 | argparse "^2.0.1" 701 | 702 | json-schema-traverse@^0.4.1: 703 | version "0.4.1" 704 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 705 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 706 | 707 | json-stable-stringify-without-jsonify@^1.0.1: 708 | version "1.0.1" 709 | resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" 710 | integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== 711 | 712 | levn@^0.4.1: 713 | version "0.4.1" 714 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" 715 | integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== 716 | dependencies: 717 | prelude-ls "^1.2.1" 718 | type-check "~0.4.0" 719 | 720 | lodash.merge@^4.6.2: 721 | version "4.6.2" 722 | resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" 723 | integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== 724 | 725 | lodash.snakecase@4.1.1: 726 | version "4.1.1" 727 | resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" 728 | integrity sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== 729 | 730 | lodash@^4.17.21: 731 | version "4.17.21" 732 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 733 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 734 | 735 | lru-cache@^6.0.0: 736 | version "6.0.0" 737 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 738 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 739 | dependencies: 740 | yallist "^4.0.0" 741 | 742 | magic-bytes.js@^1.10.0: 743 | version "1.10.0" 744 | resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz#c41cf4bc2f802992b05e64962411c9dd44fdef92" 745 | integrity sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ== 746 | 747 | merge2@^1.3.0, merge2@^1.4.1: 748 | version "1.4.1" 749 | resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" 750 | integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== 751 | 752 | micromatch@^4.0.4: 753 | version "4.0.5" 754 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" 755 | integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== 756 | dependencies: 757 | braces "^3.0.2" 758 | picomatch "^2.3.1" 759 | 760 | minimatch@^3.0.4: 761 | version "3.0.4" 762 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 763 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 764 | dependencies: 765 | brace-expansion "^1.1.7" 766 | 767 | minimatch@^3.1.2: 768 | version "3.1.2" 769 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 770 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 771 | dependencies: 772 | brace-expansion "^1.1.7" 773 | 774 | ms@2.1.2: 775 | version "2.1.2" 776 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 777 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 778 | 779 | natural-compare@^1.4.0: 780 | version "1.4.0" 781 | resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" 782 | integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== 783 | 784 | once@^1.3.0: 785 | version "1.4.0" 786 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 787 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 788 | dependencies: 789 | wrappy "1" 790 | 791 | optionator@^0.9.1: 792 | version "0.9.1" 793 | resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" 794 | integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== 795 | dependencies: 796 | deep-is "^0.1.3" 797 | fast-levenshtein "^2.0.6" 798 | levn "^0.4.1" 799 | prelude-ls "^1.2.1" 800 | type-check "^0.4.0" 801 | word-wrap "^1.2.3" 802 | 803 | parent-module@^1.0.0: 804 | version "1.0.1" 805 | resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" 806 | integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== 807 | dependencies: 808 | callsites "^3.0.0" 809 | 810 | path-is-absolute@^1.0.0: 811 | version "1.0.1" 812 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 813 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 814 | 815 | path-key@^3.1.0: 816 | version "3.1.1" 817 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 818 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 819 | 820 | path-type@^4.0.0: 821 | version "4.0.0" 822 | resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" 823 | integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== 824 | 825 | picomatch@^2.3.1: 826 | version "2.3.1" 827 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 828 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 829 | 830 | prelude-ls@^1.2.1: 831 | version "1.2.1" 832 | resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" 833 | integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== 834 | 835 | prettier@^3.4.2: 836 | version "3.4.2" 837 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" 838 | integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== 839 | 840 | punycode@^2.1.0: 841 | version "2.1.1" 842 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 843 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 844 | 845 | queue-microtask@^1.2.2: 846 | version "1.2.3" 847 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" 848 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== 849 | 850 | regexpp@^3.2.0: 851 | version "3.2.0" 852 | resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" 853 | integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== 854 | 855 | resolve-from@^4.0.0: 856 | version "4.0.0" 857 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" 858 | integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== 859 | 860 | reusify@^1.0.4: 861 | version "1.0.4" 862 | resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" 863 | integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== 864 | 865 | rimraf@^3.0.2: 866 | version "3.0.2" 867 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" 868 | integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== 869 | dependencies: 870 | glob "^7.1.3" 871 | 872 | run-parallel@^1.1.9: 873 | version "1.2.0" 874 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" 875 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== 876 | dependencies: 877 | queue-microtask "^1.2.2" 878 | 879 | semver@^7.3.7: 880 | version "7.3.7" 881 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" 882 | integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== 883 | dependencies: 884 | lru-cache "^6.0.0" 885 | 886 | shebang-command@^2.0.0: 887 | version "2.0.0" 888 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 889 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 890 | dependencies: 891 | shebang-regex "^3.0.0" 892 | 893 | shebang-regex@^3.0.0: 894 | version "3.0.0" 895 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" 896 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 897 | 898 | slash@^3.0.0: 899 | version "3.0.0" 900 | resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" 901 | integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== 902 | 903 | strip-ansi@^6.0.1: 904 | version "6.0.1" 905 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 906 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 907 | dependencies: 908 | ansi-regex "^5.0.1" 909 | 910 | strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: 911 | version "3.1.1" 912 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" 913 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 914 | 915 | supports-color@^7.1.0: 916 | version "7.2.0" 917 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 918 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 919 | dependencies: 920 | has-flag "^4.0.0" 921 | 922 | text-table@^0.2.0: 923 | version "0.2.0" 924 | resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" 925 | integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= 926 | 927 | to-regex-range@^5.0.1: 928 | version "5.0.1" 929 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 930 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 931 | dependencies: 932 | is-number "^7.0.0" 933 | 934 | ts-mixer@^6.0.4: 935 | version "6.0.4" 936 | resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.4.tgz#1da39ceabc09d947a82140d9f09db0f84919ca28" 937 | integrity sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA== 938 | 939 | tslib@^1.8.1: 940 | version "1.14.1" 941 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" 942 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 943 | 944 | tslib@^2.6.2, tslib@^2.6.3: 945 | version "2.8.1" 946 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" 947 | integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== 948 | 949 | tsutils@^3.21.0: 950 | version "3.21.0" 951 | resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" 952 | integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== 953 | dependencies: 954 | tslib "^1.8.1" 955 | 956 | type-check@^0.4.0, type-check@~0.4.0: 957 | version "0.4.0" 958 | resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" 959 | integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== 960 | dependencies: 961 | prelude-ls "^1.2.1" 962 | 963 | type-fest@^0.20.2: 964 | version "0.20.2" 965 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" 966 | integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== 967 | 968 | typescript@^5.0.2: 969 | version "5.0.2" 970 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.2.tgz#891e1a90c5189d8506af64b9ef929fca99ba1ee5" 971 | integrity sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw== 972 | 973 | undici@6.19.8: 974 | version "6.19.8" 975 | resolved "https://registry.yarnpkg.com/undici/-/undici-6.19.8.tgz#002d7c8a28f8cc3a44ff33c3d4be4d85e15d40e1" 976 | integrity sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g== 977 | 978 | uri-js@^4.2.2: 979 | version "4.4.1" 980 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" 981 | integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== 982 | dependencies: 983 | punycode "^2.1.0" 984 | 985 | v8-compile-cache@^2.0.3: 986 | version "2.3.0" 987 | resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" 988 | integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== 989 | 990 | which@^2.0.1: 991 | version "2.0.2" 992 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 993 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 994 | dependencies: 995 | isexe "^2.0.0" 996 | 997 | word-wrap@^1.2.3: 998 | version "1.2.3" 999 | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" 1000 | integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== 1001 | 1002 | wrappy@1: 1003 | version "1.0.2" 1004 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1005 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 1006 | 1007 | ws@^8.17.0: 1008 | version "8.18.0" 1009 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" 1010 | integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== 1011 | 1012 | yallist@^4.0.0: 1013 | version "4.0.0" 1014 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 1015 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 1016 | --------------------------------------------------------------------------------