From fbdff25c147958ab13094f972e3e1c9dd40546d5 Mon Sep 17 00:00:00 2001 From: Jaron Pate Date: Sat, 16 Nov 2024 17:07:31 -0600 Subject: [PATCH] WE ARE COOKING (slash commands with some arg validation proto working) --- package.json | 4 +- src/services/client.ts | 33 ++++++---- src/services/connectors/gateway.ts | 70 +++++++++++++++++++--- src/services/processor.ts | 96 +++++++++++++++++++++++------- src/types/applicationCommand.ts | 8 ++- src/types/common.ts | 10 +++- src/types/context.ts | 76 ++++++++++++++++------- src/types/handler.ts | 74 ++++++++++++++++------- testing/demo.test.ts | 33 +++++----- 9 files changed, 302 insertions(+), 102 deletions(-) diff --git a/package.json b/package.json index b544a78..b181d40 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "module": "src/index.ts", "type": "module", "scripts": { - "test": "dotenvx run -f .env -- bun run testing/demo.test.ts", - "test:inspect": "dotenvx run -f .env -- bun --inspect testing/demo.test.ts" + "test": "bun run testing/demo.test.ts", + "test:inspect": "bun --inspect testing/demo.test.ts" }, "devDependencies": { "@types/bun": "latest" diff --git a/src/services/client.ts b/src/services/client.ts index 9e87e7a..c22c735 100644 --- a/src/services/client.ts +++ b/src/services/client.ts @@ -3,12 +3,12 @@ import { Gateway } from './connectors/gateway'; import { Processor } from './processor'; import { Expectation, fromDiscord, Intent, type ClientConfig } from '../types/common'; import { exists, isNil } from '../utils/index'; -import type { Handler } from '../types/handler'; import { type DiscordMessage, Message } from '../types/message'; import { Guilds } from './caches/guilds'; import { Messages } from './caches/messages'; import { Users } from './caches/users'; import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator'; +import type { User } from '../types/user'; export class Client { readonly token: string; @@ -21,7 +21,9 @@ export class Client { protected gateway: Gateway; guilds: Guilds; users: Users; - messages: Messages + messages: Messages; + chatCommands: Processor['chat_commands']; + applicationCommands: Processor['application_commands']; constructor(config: ClientConfig) { // Validate the configuration @@ -38,6 +40,9 @@ export class Client { this.guilds = new Guilds(this.api, this.processor); this.users = new Users(this.api, this.processor); this.messages = new Messages(this, this.api, this.processor); + // Save references to the chat and application command handlers + this.chatCommands = this.processor.chat_commands; + this.applicationCommands = this.processor.application_commands; } private static validateConfig(config: ClientConfig) { @@ -66,6 +71,10 @@ export class Client { } } + get self() { + return this.gateway.user; + } + /** * Hydrates a given data object based on the provided expectations. * @@ -106,17 +115,17 @@ export class Client { return Message[fromDiscord](await this.api.post(`/channels/${channel_id}/messages`, message.toJSON())); } - chatCommands = { - register: (name: string | string[], handler: Handler) => this.processor.chat_commands.register(name, handler), - unregister: (name: string | string[]) => this.processor.chat_commands.unregister(name), - has: (name: string) => this.processor.chat_commands.has(name) - } + // chatCommands = { + // register: (name: string | string[], handler: Handler) => this.processor.chat_commands.register(name, handler), + // unregister: (name: string | string[]) => this.processor.chat_commands.unregister(name), + // has: (name: string) => this.processor.chat_commands.has(name) + // } - slashCommands = { - register: (name: string, handler: Handler) => this.processor.slash_commands.register(name, handler), - unregister: (name: string) => this.processor.slash_commands.unregister(name), - has: (name: string) => this.processor.slash_commands.has(name) - } + // applicationCommands = { + // register: (name: string, type: ApplicationCommandType, handler: Handler) => this.processor.application_commands.register(name, type, handler), + // unregister: (name: string) => this.processor.application_commands.unregister(name), + // has: (name: string) => this.processor.application_commands.has(name) + // } connect = () => this.gateway.connect(); } \ No newline at end of file diff --git a/src/services/connectors/gateway.ts b/src/services/connectors/gateway.ts index 03994a8..734abc7 100644 --- a/src/services/connectors/gateway.ts +++ b/src/services/connectors/gateway.ts @@ -1,12 +1,14 @@ import { WebSocket, type Data } from 'ws'; import type { Client } from "../client"; -import { fromDiscord, type Payload } from "../../types/common"; +import { fromDiscord, type InteractionPayload, type Payload } from "../../types/common"; import { exists, isNil, isObject, log } from "../../utils"; import type { API } from "../api"; import type { Processor } from '../processor'; import { Context } from '../../types/context'; import { type DiscordMessage, Message } from '../../types/message'; import { DiscordUser, User } from '../../types/user'; +import { Guild } from '../../types/guild'; +import { Channel } from '../../types/channel'; export class Gateway { private socket: WebSocket | null = null; @@ -14,7 +16,7 @@ export class Gateway { private heartbeat_timeout: Timer | null = null; private sequence: number | null = null; // Internal state - private user: any; + user: User | null = null; constructor(private client: Client, private processor: Processor, private api: API) {} @@ -126,10 +128,9 @@ export class Gateway { } if (event_name === 'READY') { - this.user = event_data.user; - log('User:', this.user); + this.user = User[fromDiscord](DiscordUser.fromAPIResponse(event_data.user)); } else if (event_name === 'INTERACTION_CREATE') { - // this.handleInteractionCreate(payload); + this.handleInteractionCreate(payload); } else if (event_name === 'MESSAGE_CREATE') { await this.handleMessageCreate(payload as Payload); } else if (event_name === 'MESSAGE_UPDATE') { @@ -143,7 +144,7 @@ export class Gateway { // If the event is a message event, create a message object const message = Message[fromDiscord](event_data as DiscordMessage); // And then generate the context - context = new Context(this.client, this.api, message); + context = new Context(this.client, this.api, { message }); } // Call event handlers @@ -151,6 +152,10 @@ export class Gateway { } private handleMessageCreate = async (payload: Payload) => { + if (isNil(this.user)) { + throw new Error('Cannot handle MESSAGE_CREATE event without a user'); + } + if (!isObject(payload.d)) { throw new Error('Invalid MESSAGE_CREATE payload received'); } @@ -188,7 +193,7 @@ export class Gateway { // Create a new message object from the payload const message = Message[fromDiscord](payload.d as DiscordMessage); // Generate context - const context = new Context(this.client, this.api, message); + const context = new Context(this.client, this.api, { message }); // Check if command exists if (this.processor.chat_commands.has(command)) { // Execute the handler @@ -200,6 +205,57 @@ export class Gateway { } }; + private handleInteractionCreate = (payload: Payload) => { + if (isNil(payload.d)) { + throw new Error('Invalid INTERACTION_CREATE payload received'); + } + + if (!isObject(payload.d)) { + throw new Error('Invalid INTERACTION_CREATE data received'); + } + + if (typeof payload.d.type !== 'number') { + throw new Error('Invalid INTERACTION_CREATE type received'); + } + + if (payload.d.type === 2) { + this.handleShashInteraction(payload as Payload & { d: InteractionPayload }); + } else { + // this.handleChatInteraction(payload); + } + }; + + private handleShashInteraction = (payload: Payload & { d: InteractionPayload }) => { + console.log('handleShashInteraction') + const command = payload.d.data.name; + const args = payload.d.data.options; + + const context = new Context(this.client, this.api, { interaction: payload.d }); + + if (payload.d.data.guild) { + const guild = Guild[fromDiscord](payload.d.data.guild); + this.client.guilds.set(guild.id, guild); + } + + if (payload.d.data.member) { + const member = User[fromDiscord](payload.d.data.member.user); + this.client.users.set(member.id, member); + } + + // TODO: Fix when channel class is implemented + // if (payload.d.data.channel) { + // const channel = Channel[fromDiscord](payload.d.data.channel); + // this.client.channels.set(channel.id, channel); + // } + + if (this.processor.application_commands.has(command)) { + this.processor.application_commands.execute(command, context, args); + } else { + // TODO: Custom unknown command handler + context.reply(`Unknown command: ${command}`); + } + }; + async connect() { // Fetch the Discord gateway URL const response = await this.api.request('GET', '/gateway/bot'); diff --git a/src/services/processor.ts b/src/services/processor.ts index febe325..c092d6a 100644 --- a/src/services/processor.ts +++ b/src/services/processor.ts @@ -1,15 +1,15 @@ import type { Client } from "./client"; import type { Context } from "../types/context"; import { HandlerType, Trait, type EventPayload } from "../types/common"; -import { type CommandHandler, type Handler } from "../types/handler"; +import { CommandHandler, type Handler, type ArgsFromOptions, OptionTypeMap } from "../types/handler"; import type { API } from "./api"; -import type { ApplicationCommandOption } from "../types/applicationCommand"; +import { ApplicationCommandType, type ApplicationCommandOption, type ApplicationCommandOptionResult } from "../types/applicationCommand"; export class Processor { private handlers = { [HandlerType.Events]: new Map(), [HandlerType.ChatCommands]: new Map(), - [HandlerType.SlashCommands]: new Map(), + [HandlerType.ApplicationCommands]: new Map(), [HandlerType.Interactions]: new Map() }; @@ -19,7 +19,7 @@ export class Processor { // Normalize events into an array const events = Array.isArray(event) ? event : [event]; // Register the handler for each event - for (const e of events) { + for (const e of events) { const handlers = this.handlers[type].get(e) || []; handlers.push(handler); this.handlers[type].set(e, handlers); @@ -56,12 +56,13 @@ export class Processor { if (handlers) { for (const handler of handlers) { - if (isCommandHandler(handler)) { - if (type === HandlerType.Events) { - throw new Error('Event handlers cannot be a CommandHandler'); - } - - await Promise.resolve(handler[Trait.execute](context!, argsOrPayload)); + if (Processor.isCommandHandler(handler)) { + // TODO: Enforce this? + // if (type === HandlerType.Events) { + // throw new Error('Event handlers cannot be a CommandHandler'); + // } + const resolvedArgs = await this.validateAndResolveArgs(argsOrPayload, handler.args); + await Promise.resolve(handler[Trait.execute](context!, resolvedArgs)); } else { // TODO: Fix ts error await Promise.resolve(handler(context, argsOrPayload)); @@ -69,6 +70,33 @@ export class Processor { } } } + + private validateAndResolveArgs = (args: ApplicationCommandOptionResult[], definition: T): Promise> => { + // Zip the args and definition together + const zipped: [ApplicationCommandOption, ApplicationCommandOptionResult][] = args.map((arg, i) => [definition[i], arg]); + // Validate the args + const validated: Record = {}; + for (const [def, arg] of zipped) { + if (def.choices) { + // If the definition has choices, validate the arg against them + const choice = def.choices.find((choice: any) => choice.value === arg.value); + if (!choice) { + throw new Error(`Invalid choice for ${def.name}: expected one of ${def.choices.map((c: any) => c.value).join(', ')}, got ${arg.value}`); + } + validated[def.name] = choice.value; + } else { + // Otherwise just check the type + if (arg.value instanceof OptionTypeMap[def.type]) { + // TODO: Coerce the type if possible + throw new Error(`Invalid type for ${def.name}: expected ${def.type}, got ${typeof arg}`); + } else { + validated[def.name] = arg.value; + } + } + } + // TODO: Fix types so we don't need to cast here + return Promise.resolve(validated as ArgsFromOptions); + }; [HandlerType.Events] = { register: (event: string | string[], handler: Handler) => this.register(HandlerType.Events, event, handler), @@ -84,11 +112,37 @@ export class Processor { has: (event: string) => this.handlers[HandlerType.ChatCommands].has(event) ?? this.handlers[HandlerType.ChatCommands].get(event)?.length! > 0 }; - [HandlerType.SlashCommands] = { - register: (event: string | string[], handler: Handler) => this.register(HandlerType.SlashCommands, event, handler), - unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.SlashCommands, event, handler), - execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.SlashCommands, event, context, args), - has: (event: string) => this.handlers[HandlerType.SlashCommands].has(event) ?? this.handlers[HandlerType.SlashCommands].get(event)?.length! > 0 + [HandlerType.ApplicationCommands] = { + register: (name: string | string[], type: ApplicationCommandType, handler: Handler) => { + const names = Array.isArray(name) ? name : [name]; + + // This is an application command so we need to register it with the API + // Ensure the handler is a CommandHandler + // TODO: Don't enforce this? + if (!Processor.isCommandHandler(handler)) { + throw new Error('Handler must be a CommandHandler'); + } + + // Ensure the command name is valid + // @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming + if (!names.every(/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u.test.bind(/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u))) { + throw new Error('Invalid application command name'); + }; + + for (const n of names) { + // Register the command with the API + const command: Record = handler.toJSON(); + command.name = n; + command.description = handler.description; + command.type = type; + this.api.post(`/applications/${this.client.application_id}/commands`, command); + } + + this.register(HandlerType.ApplicationCommands, names, handler) + }, + unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.ApplicationCommands, event, handler), + execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.ApplicationCommands, event, context, args), + has: (event: string) => this.handlers[HandlerType.ApplicationCommands].has(event) ?? this.handlers[HandlerType.ApplicationCommands].get(event)?.length! > 0, }; [HandlerType.Interactions] = { @@ -97,12 +151,12 @@ export class Processor { execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.Interactions, event, context, args), has: (event: string) => this.handlers[HandlerType.Interactions].has(event) ?? this.handlers[HandlerType.Interactions].get(event)?.length! > 0 }; -} -const isCommandHandler = (handler: Handler): handler is CommandHandler => { - if (typeof handler === 'object') { - return 'args' in handler && Trait.execute in handler; - } else { - return false; + static isCommandHandler = (handler: Handler): handler is CommandHandler => { + if (typeof handler === 'object') { + return 'args' in handler && Trait.execute in handler && handler instanceof CommandHandler; + } else { + return false; + } } } \ No newline at end of file diff --git a/src/types/applicationCommand.ts b/src/types/applicationCommand.ts index 701f2ff..53dd545 100644 --- a/src/types/applicationCommand.ts +++ b/src/types/applicationCommand.ts @@ -2,7 +2,7 @@ import { Trait } from "./common"; // Enum for Application Command Types export enum ApplicationCommandType { - ChatInput = 1, + Chat = 1, User = 2, Message = 3, PrimaryEntryPoint = 4, @@ -65,6 +65,12 @@ export type ApplicationCommandOption = { autocomplete?: boolean; }; +export type ApplicationCommandOptionResult = { + value: any, + type: number, + name: string +}; + // Define the DiscordApplicationCommand type export type DiscordApplicationCommand = { id: string; diff --git a/src/types/common.ts b/src/types/common.ts index 37a1b6a..a353d91 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -15,7 +15,7 @@ export enum Expectation { export enum HandlerType { Events = 'events', ChatCommands = 'chat_commands', - SlashCommands = 'slash_commands', + ApplicationCommands = 'application_commands', Interactions = 'interactions' } @@ -52,6 +52,14 @@ export type Payload = { t: string | null; }; +export type InteractionPayload = Record; + +export enum InteractionResponseType { + Pong = 1, + Message = 4, + DeferredMessage = 5 +} + export enum Intent { GUILDS = 1 << 0, GUILD_MEMBERS = 1 << 1, diff --git a/src/types/context.ts b/src/types/context.ts index 03cd7e0..a11b9a5 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -1,8 +1,8 @@ import type { API } from "../services/api"; import type { Client } from "../services/client"; import { hydrate, hydrator, type Hydrateable, type Hydrated } from "../services/hydrator"; -import { isNil } from "../utils"; -import { fromDiscord, type Expectation } from "./common"; +import { isNil, exists } from "../utils"; +import { fromDiscord, InteractionResponseType, type Expectation, type InteractionPayload } from "./common"; import type { Guild } from "./guild"; import { type DiscordMessage, Message, type MessagePayload } from "./message"; /** @@ -26,28 +26,39 @@ import { type DiscordMessage, Message, type MessagePayload } from "./message"; export type HydratedContext> = Context & { guild: Extract extends never ? undefined : Guild; + message: Extract extends never ? undefined : Message; }; +type ContextData = + | { channel_id?: string; message_id?: string; guild_id?: string; message?: MessagePayload, interaction: InteractionPayload } + | { channel_id?: string; message_id?: string; guild_id?: string; message: MessagePayload, interaction?: InteractionPayload } + export class Context { // Public properties - message_id: string; + message_id?: string; channel_id: string; guild_id?: string; + message?: MessagePayload; + // TODO: Type this + interaction?: InteractionPayload; - constructor(private client: Client, private api: API, public message: MessagePayload) { - console.log('Context created: ', ); - if (isNil(message.id)) { - throw new Error('Message ID is required to create a context'); - } + constructor(private client: Client, private api: API, data: ContextData) { + const channel_id = data.channel_id ?? data.message?.channel_id ?? data.interaction?.channel_id; - if (isNil(message.channel_id)) { + if (isNil(channel_id)) { throw new Error('Channel ID is required to create a context'); } this.client = client; - this.message_id = message.id; - this.channel_id = message.channel_id; - this.guild_id = message.guild_id; + this.message_id = data.message_id ?? data.message?.id; + this.channel_id = channel_id; + this.guild_id = data.guild_id ?? data.message?.guild_id ?? data.interaction?.guild_id; + this.message = data.message; + this.interaction = data.interaction; + } + + get self() { + return this.client.self!; } /** @@ -63,7 +74,7 @@ export class Context { * * @throws Will throw an error if an expectation cannot be resolved. */ - hydrate = async < + public hydrate = async < T extends Hydrateable | Hydrated>, K extends Array >(data: T, expectations: K) => { @@ -81,14 +92,10 @@ export class Context { * * @returns A promise that resolves to a type guard function. The type guard function returns `true` if the data object was successfully hydrated, otherwise `false`. */ - hydrator = async >, K extends Array>(data: T, expectations: K) => { + public hydrator = async >, K extends Array>(data: T, expectations: K) => { return hydrator(data, expectations, this.client, this.api); } - // get author() { - // return this.client.users.get(this.author_id); - // }; - /** * Reply to the message that this command * @param message The message to send to the channel @@ -96,15 +103,22 @@ export class Context { * @returns The message that was sent */ public reply = async (message: Message | string, reference: boolean = true): Promise => { + console.log('this.interaction', this.interaction) if (typeof message === 'string') { message = new Message().setContent(message); } - if (reference) { - message.setReference(this.message) + if (exists(this.message)) { + if (reference) { + message.setReference(this.message) + } + + return this.client.sendMessage(this.channel_id, message); + } else if (exists(this.interaction)) { + return this.ack(InteractionResponseType.Message, message.toJSON()); + } else { + throw new Error('Cannot reply without a message or interaction payload'); } - - return this.client.sendMessage(this.channel_id, message); }; /** @@ -138,11 +152,27 @@ export class Context { * @param {Message | string} message - The updated message content. * @returns {Promise} The updated message. */ - public async editMessage (message: Message, content?: string) { + public editMessage = async (message: Message, content?: string) => { const clone = new Message(message); if (content) { clone.setContent(content); } return Message[fromDiscord](await this.api.patch(`/channels/${clone.channel_id}/messages/${clone.id}`, clone)); }; + + // Interaction methods + public ack = async (type: InteractionResponseType = InteractionResponseType.Pong, data?: Record) => { + if (isNil(this.interaction)) { + throw new Error('Cannot acknowledge an interaction without an interaction payload'); + } + + return this.api.post(`/interactions/${this.interaction.id}/${this.interaction.token}/callback`, { + type, + data + }); + }; + + public defer = async () => { + return this.ack(InteractionResponseType.DeferredMessage); + }; } \ No newline at end of file diff --git a/src/types/handler.ts b/src/types/handler.ts index a4aa100..3c3cac0 100644 --- a/src/types/handler.ts +++ b/src/types/handler.ts @@ -1,42 +1,74 @@ -import type { ApplicationCommandOption, ApplicationCommandOptionType } from "./applicationCommand"; +import { ApplicationCommandOptionType, type ApplicationCommandOption } from "./applicationCommand"; import { Trait, type EventPayload } from "./common"; import type { Context } from "./context"; -type PrimitiveTypeMap = { - [ApplicationCommandOptionType.String]: string; - [ApplicationCommandOptionType.Integer]: number; - [ApplicationCommandOptionType.Boolean]: boolean; - [ApplicationCommandOptionType.Number]: number; - [ApplicationCommandOptionType.User]: string; // Could be a user ID or object depending on your use case - [ApplicationCommandOptionType.Channel]: string; // Could be a channel ID or object - [ApplicationCommandOptionType.Role]: string; // Could be a role ID or object - [ApplicationCommandOptionType.Mentionable]: string; // Could be a mentionable ID or object - [ApplicationCommandOptionType.Attachment]: string; // Could represent attachment IDs - [ApplicationCommandOptionType.SubCommand]: never; // Handled by `options` - [ApplicationCommandOptionType.SubCommandGroup]: never; // Handled by `options` +export const OptionTypeMap = { + [ApplicationCommandOptionType.String]: String, + [ApplicationCommandOptionType.Integer]: Number, + [ApplicationCommandOptionType.Boolean]: Boolean, + [ApplicationCommandOptionType.Number]: Number, + [ApplicationCommandOptionType.User]: String, // Could be a user ID or object depending on your use case + [ApplicationCommandOptionType.Channel]: String, // Could be a channel ID or object + [ApplicationCommandOptionType.Role]: String, // Could be a role ID or object + [ApplicationCommandOptionType.Mentionable]: String, // Could be a mentionable ID or object + [ApplicationCommandOptionType.Attachment]: String, // Could represent attachment IDs + [ApplicationCommandOptionType.SubCommand]: String, // Handled by `options` + [ApplicationCommandOptionType.SubCommandGroup]: String, // Handled by `options` }; // Recursive type to map ApplicationCommandOption to arguments type ArgsFromOptions = { - [Option in T[number] as Option['id']]: Option extends { options: readonly ApplicationCommandOption[] } + [Option in T[number] as Option['name']]: Option extends { options: readonly ApplicationCommandOption[] } ? ArgsFromOptions // Recursive inference for subcommands : Option['choices'] extends readonly { value: infer U }[] - ? U // Infer from choices if defined - : Option['type'] extends keyof PrimitiveTypeMap - ? PrimitiveTypeMap[Option['type']] // Otherwise, map from ApplicationCommandOptionType - : unknown; + ? Option['required'] extends true // If required, keep as is; otherwise, make optional + ? U + : U | undefined + : Option['type'] extends keyof typeof OptionTypeMap + ? Option['required'] extends true // If required, keep as is; otherwise, make optional + ? typeof OptionTypeMap[Option['type']] + : typeof OptionTypeMap[Option['type']] | undefined + : Option['required'] extends true + ? unknown + : unknown | undefined; }; -export type CommandHandler = { +export type CommandHandlerInput = { args: T; + description: string; [Trait.execute]: (context: Context, args: ArgsFromOptions) => Promise | void; }; +export class CommandHandler { + args: T; + description: string; + [Trait.execute]!: (context: Context, args: ArgsFromOptions) => Promise | void; + + constructor(input: CommandHandlerInput) { + // Validate option names + // @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming + for (const name of input.args.map((arg) => arg.name)) { + if (/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u.test(name) === false) { + throw new Error(`Invalid option name: ${name}`); + } + } + this.args = input.args; + this.description = input.description + this[Trait.execute] = input[Trait.execute]; + } + + toJSON() { + return { + options: this.args, + }; + } +}; + // Helper function to infer type of `args` export function createCommandHandler( - handler: CommandHandler + handler: CommandHandlerInput ): CommandHandler { - return handler as CommandHandler; + return new CommandHandler(handler); } export type CommandHandlerFunction = (context: Context, args: any[]) => Promise | void; diff --git a/testing/demo.test.ts b/testing/demo.test.ts index 7c7e344..f453a90 100644 --- a/testing/demo.test.ts +++ b/testing/demo.test.ts @@ -1,5 +1,5 @@ import { Client, type Context, createCommandHandler, Expectation, Intent, type MessagePayload, Trait } from '../src/index'; -import { ApplicationCommandOptionType } from '../src/types/applicationCommand'; +import { ApplicationCommandOptionType, ApplicationCommandType } from '../src/types/applicationCommand'; const client = new Client({ token: process.env.BOT_TOKEN!, @@ -21,25 +21,26 @@ client.chatCommands.register('ping', async (context: Context, args: any[]) => { await context.editMessage(message, `Pong! Latency: ${time}ms`); }); -client.slashCommands.register('ping', createCommandHandler({ +client.applicationCommands.register('hello', ApplicationCommandType.Chat, createCommandHandler({ + description: "Introduce yourself", args: [ { type: ApplicationCommandOptionType.String, - name: "Name", + name: "name", id: "name", description: "Your name", required: true }, { type: ApplicationCommandOptionType.Integer, - name: "Age", + name: "age", id: "age", description: "Your age", required: true }, { type: ApplicationCommandOptionType.String, - name: "Favorite Animal", + name: "favorite_animal", id: "favorite_animal", description: "Your favorite animal", required: false, @@ -56,24 +57,28 @@ client.slashCommands.register('ping', createCommandHandler({ } ] as const, [Trait.execute]: async (context, args) => { - context.reply(`You are ${args.name} and you are ${args.age} years old and your favorite animal is ${args.favorite_animal}`, true); + context.reply( + `\ +Hello ${args.name}! I see you are ${args.age} years old${!!args.favorite_animal ? ` and your favorite animal is ${args.favorite_animal}.`: '.'} +Nice to meet you, I'm ${context.self.username}!` + , true); } })); client.chatCommands.register('me', async (context: Context, _args: any[]) => { // Note: Testing repeated hydration - const message = await context.hydrate(context.message, [Expectation.Channel]) - const rehydrated = await context.hydrate(message, [Expectation.Guild]); + const hydrated = await context.hydrate(context.message, [Expectation.Channel]) + const rehydrated = await context.hydrate(hydrated, [Expectation.Guild]); rehydrated.guild.name - const hydrate = await context.hydrator(message, [Expectation.Guild]); - const hasGuild = hydrate(message); + const hydrate = await context.hydrator(hydrated, [Expectation.Guild]); + const hasGuild = hydrate(hydrated); // if (hasGuild) { - // message.guild.name + // hydrated.guild.name // } else { - // message + // hydrated // } // await context.reply(`You are ${message.author.username} ${hasGuild ? message.guild.name : ''}`, true); @@ -90,8 +95,8 @@ client.chatCommands.register('here', async (context: Context, _args: any[]) => { }); client.chatCommands.register('quote', async (context: Context, _args: any[]) => { - const { message, hydrator } = context; - const reference = (await hydrator(message, [Expectation.Message]))(message); + const message = context.message; + const reference = (await context.hydrator(message, [Expectation.Message]))(message); let reply: MessagePayload;