2025-10-13 23:53:20 -05:00
import { type DiscordAPIEvent , Expectation , HandlerType , Intent , type ClientConfig } from '../types/common' ;
2025-02-15 17:47:27 -06:00
import { Message , type DiscordMessage } from '../types/message' ;
2024-11-10 01:55:07 -06:00
import { exists , isNil } from '../utils/index' ;
2025-02-15 17:47:27 -06:00
import { API } from './api' ;
import { EventBus , type Emitter } from './bus' ;
import { Channels } from './caches/channels' ;
2024-11-15 13:49:33 -06:00
import { Guilds } from './caches/guilds' ;
import { Messages } from './caches/messages' ;
import { Users } from './caches/users' ;
2025-02-15 17:47:27 -06:00
import { Gateway } from './connectors/gateway' ;
2024-11-15 13:49:33 -06:00
import { hydrate , hydrator , type Hydrateable , type Hydrated } from './hydrator' ;
2025-02-15 17:47:27 -06:00
import { Processor } from './processor' ;
2024-11-10 01:55:07 -06:00
export class Client {
2025-10-14 00:39:28 -05:00
readonly config : Readonly < Required < ClientConfig > > ;
2024-11-10 01:55:07 -06:00
// Internal services
2024-12-15 15:11:59 -06:00
protected bus : Emitter ;
2024-11-12 00:38:08 -06:00
protected api : API ;
protected processor : Processor ;
protected gateway : Gateway ;
2025-10-14 00:39:28 -05:00
// Caches
caches : {
guilds : Guilds ;
users : Users ;
messages : Messages ;
channels : Channels ;
} ;
2024-11-10 01:55:07 -06:00
constructor ( config : ClientConfig ) {
// Validate the configuration
Client . validateConfig ( config ) ;
// Save the configuration
2025-10-14 00:39:28 -05:00
this . config = Object . freeze ( {
token : config.token ,
application_id : config.application_id ,
intents : config.intents ? ? [ Intent . DEFAULT ] ,
prefix : config.prefix ? ? '!' ,
} ) ;
2024-11-10 01:55:07 -06:00
// Initialize internal services
2024-12-15 15:11:59 -06:00
this . bus = EventBus ;
2024-11-12 00:38:08 -06:00
this . api = new API ( this ) ;
2024-12-15 15:11:59 -06:00
this . processor = new Processor ( this , this . api , this . bus ) ;
this . gateway = new Gateway ( this , this . processor , this . api , this . bus ) ;
2025-10-14 00:39:28 -05:00
// 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 ) ,
} ;
2024-11-10 01:55:07 -06:00
}
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 } ` ) ;
}
}
}
}
2024-11-16 17:07:31 -06:00
get self() {
return this . gateway . user ;
}
2025-10-14 00:39:28 -05:00
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 ] ;
}
2024-11-15 13:49:33 -06:00
/**
* Hydrates a given data object based on the provided expectations.
2025-10-13 23:53:20 -05:00
*
2024-11-15 13:49:33 -06:00
* @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.
2025-10-13 23:53:20 -05:00
*
2024-11-15 13:49:33 -06:00
* @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.
2025-10-13 23:53:20 -05:00
*
2024-11-15 13:49:33 -06:00
* @returns A promise that resolves to the hydrated data object.
2025-10-13 23:53:20 -05:00
*
2024-11-15 13:49:33 -06:00
* @throws Will throw an error if an expectation cannot be resolved.
*/
2025-10-13 23:53:20 -05:00
hydrate = async < T extends Hydrateable | Hydrated < Hydrateable , Array < Expectation > > , K extends Array < Expectation > > (
data : T ,
expectations : K ,
) = > {
2024-11-15 13:49:33 -06:00
return hydrate < T , K > ( data , expectations , this , this . api ) ;
2025-10-13 23:53:20 -05:00
} ;
2024-11-15 13:49:33 -06:00
/**
* A wrapper function that attempts to hydrate a given data object and returns a type guard function.
2025-10-13 23:53:20 -05:00
*
2024-11-15 13:49:33 -06:00
* @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.
2025-10-13 23:53:20 -05:00
*
2024-11-15 13:49:33 -06:00
* @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.
2025-10-13 23:53:20 -05:00
*
2024-11-15 13:49:33 -06:00
* @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`.
*/
2025-10-13 23:53:20 -05:00
hydrator = async < T extends Hydrateable , K extends Expectation [ ] > ( data : T , expectations : K ) = > {
return hydrator < T , K > ( data , expectations , this , this . api ) ;
} ;
2024-11-13 13:34:43 -06:00
2024-11-10 01:55:07 -06:00
sendMessage = async ( channel_id : string , message : Message | string ) = > {
if ( typeof message === 'string' ) {
message = new Message ( ) . setContent ( message ) ;
}
2025-10-13 23:53:20 -05:00
return Message . fromDiscord (
await this . api . post < DiscordMessage > ( ` /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 < T = Record < string , any > > (
method : 'GET' | 'POST' | 'PUT' | 'DELETE' ,
endpoint : string ,
body? : Record < string , any > ,
) : Promise < T > = > {
return this . api . request < T > ( method , endpoint , body ) ;
} ;
2024-11-10 01:55:07 -06:00
2024-11-16 17:07:31 -06:00
// 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)
// }
2024-11-12 00:38:08 -06:00
2024-11-16 17:07:31 -06:00
// 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)
// }
2024-11-10 01:55:07 -06:00
2025-10-13 23:53:20 -05:00
on = ( event : DiscordAPIEvent , handler : ( . . . args : any [ ] ) = > void ) = > this . bus . on ( event , handler ) ;
2024-12-15 15:11:59 -06:00
2025-10-13 23:53:20 -05:00
once = ( event : DiscordAPIEvent , handler : ( . . . args : any [ ] ) = > void ) = > this . bus . once ( event , handler ) ;
2024-12-15 15:11:59 -06:00
2025-10-13 23:53:20 -05:00
off = ( event : DiscordAPIEvent , handler : ( . . . args : any [ ] ) = > void ) = > this . bus . off ( event , handler ) ;
2024-12-15 15:11:59 -06:00
2024-11-10 01:55:07 -06:00
connect = ( ) = > this . gateway . connect ( ) ;
2025-02-15 11:57:07 -06:00
}