From 119ddfefd1469d7c5987fcac14cc035205e70ea5 Mon Sep 17 00:00:00 2001 From: Jaron Date: Sat, 16 Nov 2024 11:55:10 -0600 Subject: [PATCH] Prototyping application commands & base command handler --- src/index.ts | 7 +- src/services/client.ts | 4 +- src/types/applicationCommand.ts | 131 ++++++++++++++++++++++++++++++++ src/types/common.ts | 5 +- src/types/hander.ts | 67 ++++++++++++++-- testing/demo.test.ts | 39 ++++++++-- 6 files changed, 236 insertions(+), 17 deletions(-) create mode 100644 src/types/applicationCommand.ts diff --git a/src/index.ts b/src/index.ts index 7c57fa9..9bfe125 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ // Base classes export { Client } from './services/client'; -export { Intent } from './types/common'; -export { BaseHandler, type Handler } from './types/hander'; +export { Intent, Trait } from './types/common'; // Consumable types export type { ClientConfig } from './types/common'; export type { Context } from './types/context'; @@ -13,4 +12,6 @@ export { Member } from './types/member'; export { User } from './types/user'; export { Message, type MessagePayload } from './types/message'; export { Embed } from './types/embed'; -export { Emoji } from './types/emoji'; \ No newline at end of file +export { Emoji } from './types/emoji'; +// Other +export { createCommandHandler } from './types/hander'; \ No newline at end of file diff --git a/src/services/client.ts b/src/services/client.ts index 55b9e0f..a96d04a 100644 --- a/src/services/client.ts +++ b/src/services/client.ts @@ -4,7 +4,7 @@ import { Processor } from './processor'; import { Expectation, fromDiscord, Intent, type ClientConfig } from '../types/common'; import { exists, isNil } from '../utils/index'; import type { Handler } from '../types/hander'; -import { DiscordMessage, Message } from '../types/message'; +import { type DiscordMessage, Message } from '../types/message'; import { Guilds } from './caches/guilds'; import { Messages } from './caches/messages'; import { Users } from './caches/users'; @@ -103,7 +103,7 @@ export class Client { message = new Message().setContent(message); } - return Message[fromDiscord](DiscordMessage.fromAPIResponse(await this.api.post(`/channels/${channel_id}/messages`, message.toJSON()))); + return Message[fromDiscord](await this.api.post(`/channels/${channel_id}/messages`, message.toJSON())); } chatCommands = { diff --git a/src/types/applicationCommand.ts b/src/types/applicationCommand.ts new file mode 100644 index 0000000..cf9dc4e --- /dev/null +++ b/src/types/applicationCommand.ts @@ -0,0 +1,131 @@ +import { Trait } from "./common"; + +// Enum for Application Command Types +export enum ApplicationCommandType { + CHAT_INPUT = 1, + USER = 2, + MESSAGE = 3, + PRIMARY_ENTRY_POINT = 4, +} + +// Enum for Application Command Option Types +export enum ApplicationCommandOptionType { + SUB_COMMAND = 1, + SUB_COMMAND_GROUP = 2, + STRING = 3, + INTEGER = 4, + BOOLEAN = 5, + USER = 6, + CHANNEL = 7, + ROLE = 8, + MENTIONABLE = 9, + NUMBER = 10, + ATTACHMENT = 11, +} + +// Enum for Channel Types (if applicable for options) +export enum ChannelType { + GUILD_TEXT = 0, + DM = 1, + GUILD_VOICE = 2, + GROUP_DM = 3, + GUILD_CATEGORY = 4, + GUILD_NEWS = 5, + GUILD_STORE = 6, + GUILD_NEWS_THREAD = 10, + GUILD_PUBLIC_THREAD = 11, + GUILD_PRIVATE_THREAD = 12, + GUILD_STAGE_VOICE = 13, + GUILD_DIRECTORY = 14, + GUILD_FORUM = 15, +} + +// Enum for Application Command Handler Types +export enum ApplicationCommandHandlerType { + APP_HANDLER = 1, + DISCORD_LAUNCH_ACTIVITY = 2, +} + +// Define the ApplicationCommandOption type +export type ApplicationCommandOption = { + type: ApplicationCommandOptionType; + name: string; + name_localizations?: Record | null; + description: string; + description_localizations?: Record | null; + required?: boolean; + choices?: Array<{ name: string; value: string | number }>; + options?: ApplicationCommandOption[]; // Recursive structure for subcommands + channel_types?: ChannelType[]; + min_value?: number; + max_value?: number; + min_length?: number; + max_length?: number; + autocomplete?: boolean; +}; + +// Define the DiscordApplicationCommand type +export type DiscordApplicationCommand = { + id: string; + type?: ApplicationCommandType; + application_id: string; + guild_id?: string; + name: string; + name_localizations?: Record | null; + description: string; + description_localizations?: Record | null; + options?: ApplicationCommandOption[]; + default_member_permissions?: string | null; + dm_permission?: boolean; + default_permission?: boolean; + nsfw?: boolean; + integration_types?: number[]; + contexts?: number[]; + version: string; + handler?: ApplicationCommandHandlerType; +}; + +// Define the ApplicationCommand class +export class ApplicationCommand { + id: string; + type?: ApplicationCommandType; + application_id: string; + guild_id?: string; + name: string; + name_localizations?: Record | null; + description: string; + description_localizations?: Record | null; + options?: ApplicationCommandOption[]; + default_member_permissions?: string | null; + dm_permission?: boolean; + default_permission?: boolean; + nsfw?: boolean; + integration_types?: number[]; + contexts?: number[]; + version: string; + handler?: ApplicationCommandHandlerType; + + constructor(command: DiscordApplicationCommand) { + this.id = command.id; + this.type = command.type; + this.application_id = command.application_id; + this.guild_id = command.guild_id; + this.name = command.name; + this.name_localizations = command.name_localizations; + this.description = command.description; + this.description_localizations = command.description_localizations; + this.options = command.options; + this.default_member_permissions = command.default_member_permissions; + this.dm_permission = command.dm_permission; + this.default_permission = command.default_permission; + this.nsfw = command.nsfw; + this.integration_types = command.integration_types; + this.contexts = command.contexts; + this.version = command.version; + this.handler = command.handler; + } + + static [Trait.fromDiscord](command: DiscordApplicationCommand): ApplicationCommand { + return new ApplicationCommand(command); + } +} \ No newline at end of file diff --git a/src/types/common.ts b/src/types/common.ts index 95d6902..37a1b6a 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -22,11 +22,14 @@ export enum HandlerType { export type Expects, K extends Expectation> = Extract extends never ? false : true; export const fromDiscord: unique symbol = Symbol('fromDiscord') +export const execute: unique symbol = Symbol('execute') export const Trait: { fromDiscord: typeof fromDiscord + execute: typeof execute } = { - fromDiscord + fromDiscord, + execute } export type EventPayload = any; diff --git a/src/types/hander.ts b/src/types/hander.ts index 36898cc..8fa91d0 100644 --- a/src/types/hander.ts +++ b/src/types/hander.ts @@ -1,14 +1,71 @@ -import type { EventPayload } from "./common"; +import type { ApplicationCommandOption, ApplicationCommandOptionType } from "./applicationCommand"; +import { Trait, type EventPayload } from "./common"; import type { Context } from "./context"; -export class BaseHandler { - constructor() {}; +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.SUB_COMMAND]: never; // Handled by `options` + [ApplicationCommandOptionType.SUB_COMMAND_GROUP]: never; // Handled by `options` +}; - public async execute(context: Context | null, args: any[]) {}; +// Recursive type to map ApplicationCommandOption to arguments +type ArgsFromOptions = { + [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; +}; + +export type BaseHandler = { + args: T; + [Trait.execute]: (context: Context, args: ArgsFromOptions) => Promise | void; +}; + +// Helper function to infer type of `args` +export function createCommandHandler( + handler: BaseHandler +): BaseHandler { + return handler as BaseHandler; } +const handler = createCommandHandler({ + args: [ + { + type: 3, + name: "name", + description: "The name of the user", + required: true + }, + { + type: 3, + name: "age", + description: "The age of the user", + required: true, + choices: [ + { name: "18", value: "18" }, + { name: "19", value: "19" }, + { name: "20", value: "20" } + ] + } + ] as const, + [Trait.execute]: async (context, args) => { + console.log(args); + } +}); + export type CommandHandlerFunction = (context: Context, args: any[]) => Promise | void; export type EventHandlerFunction = (context: Context | null, payload: EventPayload) => Promise | void; export type HandlerFunction = CommandHandlerFunction | EventHandlerFunction; -export type Handler = HandlerFunction | BaseHandler; +export type Handler = HandlerFunction | BaseHandler; diff --git a/testing/demo.test.ts b/testing/demo.test.ts index 9bb82d5..da465ca 100644 --- a/testing/demo.test.ts +++ b/testing/demo.test.ts @@ -1,4 +1,4 @@ -import { Client, type Context, Expectation, Intent, type MessagePayload } from '../src/index'; +import { Client, type Context, createCommandHandler, Expectation, Intent, type MessagePayload, Trait } from '../src/index'; const client = new Client({ token: process.env.BOT_TOKEN!, @@ -20,6 +20,33 @@ client.chatCommands.register('ping', async (context: Context, args: any[]) => { await context.editMessage(message, `Pong! Latency: ${time}ms`); }); +const handler = createCommandHandler({ + args: [ + { + type: 3, + name: "name", + description: "The name of the user", + required: true + }, + { + type: 3, + name: "age", + description: "The age of the user", + required: true, + choices: [ + { name: "18", value: "18" }, + { name: "19", value: "19" }, + { name: "20", value: "20" } + ] + } + ] as const, + [Trait.execute]: async (context, args) => { + context.reply(`You are ${args.name} and you are ${args.age} years old`, true); + } +}); + +client.slashCommands.register('ping', handler); + client.chatCommands.register('me', async (context: Context, _args: any[]) => { // Note: Testing repeated hydration const message = await context.hydrate(context.message, [Expectation.Channel]) @@ -30,11 +57,11 @@ client.chatCommands.register('me', async (context: Context, _args: any[]) => { const hydrate = await context.hydrator(message, [Expectation.Guild]); const hasGuild = hydrate(message); - if (hasGuild) { - message.guild.name - } else { - message - } + // if (hasGuild) { + // message.guild.name + // } else { + // message + // } // await context.reply(`You are ${message.author.username} ${hasGuild ? message.guild.name : ''}`, true); });