import { type DiscordAPIEvent, Expectation, HandlerType, Intent, type ClientConfig } from '../types/common'; import { Message, type DiscordMessage } from '../types/message'; import { exists, isNil } from '../utils/index'; import { API } from './api'; import { EventBus, type Emitter } from './bus'; import { Channels } from './caches/channels'; import { Guilds } from './caches/guilds'; import { Messages } from './caches/messages'; import { Users } from './caches/users'; import { Gateway } from './connectors/gateway'; import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator'; import { Processor } from './processor'; export class Client { readonly config: Readonly>; // Internal services protected bus: Emitter; protected api: API; protected processor: Processor; protected gateway: Gateway; // Caches readonly caches: { readonly guilds: Guilds; readonly users: Users; readonly messages: Messages; readonly channels: Channels; }; constructor(config: ClientConfig) { // Validate the configuration Client.validateConfig(config); // Save the configuration this.config = Object.freeze({ token: config.token, application_id: config.application_id, intents: config.intents ?? [Intent.DEFAULT], prefix: config.prefix ?? '!', }); // Initialize internal services this.bus = EventBus; this.api = new API(this); this.processor = new Processor(this, this.api, this.bus); this.gateway = new Gateway(this, this.processor, this.api, this.bus); // Initialize caches this.caches = { guilds: new Guilds(this.api, this.processor), users: new Users(this.api, this.processor), messages: new Messages(this.api, this.processor), channels: new Channels(this.api, this.processor), }; } private static validateConfig(config: ClientConfig) { if (isNil(config.token)) { throw new Error('Token is required'); } if (isNil(config.application_id)) { throw new Error('Application ID is required'); } if (exists(config.prefix) && typeof config.prefix !== 'string') { throw new Error('Prefix must be a string'); } if (exists(config.intents)) { if (!Array.isArray(config.intents)) { throw new Error('Intents must be an array'); } for (const intent of config.intents) { if (!Intent[intent]) { throw new Error(`Invalid intent: ${intent}`); } } } } get self() { return this.gateway.user; } get chatCommands() { return this.processor[HandlerType.ChatCommands]; } get applicationCommands() { return this.processor[HandlerType.ApplicationCommands]; } get interactions() { return this.processor[HandlerType.Interactions]; } get events() { return this.processor[HandlerType.Events]; } /** * Hydrates a given data object based on the provided expectations. * * @template T - The type of the data object to be hydrated. Must extend `Hydrateable`. * @template K - An array of `Expectation` types that specify what properties should be hydrated. * * @param data - The data object to be hydrated. Can be of type `Context` or `Message`. * @param expectations - An array of expectations that specify what properties should be hydrated. * * @returns A promise that resolves to the hydrated data object. * * @throws Will throw an error if an expectation cannot be resolved. */ hydrate = async >, K extends Array>( data: T, expectations: K, ) => { return hydrate(data, expectations, this, this.api); }; /** * A wrapper function that attempts to hydrate a given data object and returns a type guard function. * * @template T - The type of the data object to be hydrated. Must extend `Hydrateable`. * @template K - An array of `Expectation` types that specify what properties should be hydrated. * * @param data - The data object to be hydrated. Can be of type `Context` or `Message`. * @param expectations - An array of expectations that specify what properties should be hydrated. * * @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 (data: T, expectations: K) => { return hydrator(data, expectations, this, this.api); }; sendMessage = async (channel_id: string, message: Message | string) => { if (typeof message === 'string') { message = new Message().setContent(message); } return Message.fromDiscord( await this.api.post(`/channels/${channel_id}/messages`, message.toJSON()), ); }; /** * Makes a raw HTTP request to the Discord API. * @param method HTTP method to use for the request (GET, POST, PUT, DELETE). * @param endpoint The API endpoint to request; can start with a slash or be a full URL. * @param body Optional body to send with the request, typically used for POST or PUT requests. * @returns A promise that resolves to the response data of type T. */ rawRequest = async >( method: 'GET' | 'POST' | 'PUT' | 'DELETE', endpoint: string, body?: Record, ): Promise => { return this.api.request(method, endpoint, body); }; // 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) // } // 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) // } on = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.on(event, handler); once = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.once(event, handler); off = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.off(event, handler); connect = () => this.gateway.connect(); }