diff --git a/package.json b/package.json index 338fbbe..a9ac698 100644 --- a/package.json +++ b/package.json @@ -35,5 +35,11 @@ }, "dependencies": { "ws": "^8.18.0" + }, + "prettier": { + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "tabWidth": 4 } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index f6de7a5..dc9b69f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,11 @@ // Base classes -export { Client } from './services/client'; +export { Client, Client as unicord } from './services/client'; export { Intent, Trait } from './types/common'; // Consumable types export type { ClientConfig } from './types/common'; export type { Context } from './types/context'; export { Expectation } from './types/common'; -export { - ChannelType, - ChannelFlags, - VideoQualityMode, - SortOrderType, - ForumLayoutType -} from './types/channel'; +export { ChannelType, ChannelFlags, VideoQualityMode, SortOrderType, ForumLayoutType } from './types/channel'; // Consumable util classes export { Guild } from './types/guild'; export { Role } from './types/role'; @@ -21,5 +15,11 @@ export { Message, type MessagePayload, ComponentStyle, ComponentType } from './t export { Embed } from './types/embed'; export { Emoji } from './types/emoji'; // Other -export { createCommandHandler, type InteractionData, type InteractionCommpoentData, type InteractionCommandData, type InteractionHandlerFunction } from './types/handler'; +export { + createCommandHandler, + type InteractionData, + type InteractionCommpoentData, + type InteractionCommandData, + type InteractionHandlerFunction, +} from './types/handler'; export { ApplicationCommandType, ApplicationCommandOptionType as CommandOptionType } from './types/applicationCommand'; diff --git a/src/services/caches/cache.ts b/src/services/caches/cache.ts index 20de3ce..7bed8c6 100644 --- a/src/services/caches/cache.ts +++ b/src/services/caches/cache.ts @@ -1,16 +1,22 @@ -import { fromDiscord } from "../../types/common"; -import type { Context } from "../../types/context"; -import type { API } from "../api"; -import type { Processor } from "../processor"; - +import { fromDiscord } from '../../types/common'; +import type { Context } from '../../types/context'; +import type { API } from '../api'; +import type { Processor } from '../processor'; export class ObjectCache { cache: LRUCache; requests = new Map>(); readonly type: string; - constructor(protected api: API, processor: Processor, type: string, private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), events: string[], capacity: number = 10_000) { - this.cache = new LRUCache(10_000) + constructor( + protected api: API, + processor: Processor, + type: string, + private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), + events: string[], + maxCapacity: number = 10_000, + ) { + this.cache = new LRUCache(maxCapacity); this.type = type; processor.events.register(events, async (_context: Context, e: Record) => { this.cache.set(e.id, factory[fromDiscord](e)); @@ -35,8 +41,8 @@ export class ObjectCache { async fetch(object_id: string, ..._args: any[]): Promise { if (this.requests.has(object_id)) { return this.requests.get(object_id)!; - // } else if (this.cache.has(object_id)) { - // return Promise.resolve(this.cache.get(object_id)!); + // } else if (this.cache.has(object_id)) { + // return Promise.resolve(this.cache.get(object_id)!); } else { // Dispatch the request const req = this.api.get(`/${this.type}/${object_id}`).then((data) => { @@ -49,10 +55,8 @@ export class ObjectCache { }); // Save the request this.requests.set(object_id, req); - // Remove the request from the cache if it succeeds - req.then(() => this.requests.delete(object_id)); - // Remove the request from the cache if it fails - req.catch(() => this.requests.delete(object_id)); + // Remove the request from the cache when it finishes + req.finally(() => this.requests.delete(object_id)); // Return the request return req; } @@ -157,4 +161,4 @@ class LRUCache { current = current.next; } } -} \ No newline at end of file +} diff --git a/src/services/client.ts b/src/services/client.ts index a841b19..77753d7 100644 --- a/src/services/client.ts +++ b/src/services/client.ts @@ -1,4 +1,4 @@ -import { Expectation, HandlerType, Intent, type ClientConfig } from '../types/common'; +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'; @@ -84,43 +84,63 @@ export class Client { /** * 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) => { + 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); - } + 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())); - } + 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), @@ -134,11 +154,11 @@ export class Client { // has: (name: string) => this.processor.application_commands.has(name) // } - on = (event: string, handler: (...args: any[]) => void) => this.bus.on(event, handler); + on = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.on(event, handler); - once = (event: string, handler: (...args: any[]) => void) => this.bus.once(event, handler); + once = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.once(event, handler); - off = (event: string, handler: (...args: any[]) => void) => this.bus.off(event, handler); + off = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.off(event, handler); connect = () => this.gateway.connect(); } diff --git a/src/types/common.ts b/src/types/common.ts index 7ea6802..f09b9bb 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -3,7 +3,7 @@ export type ClientConfig = { application_id: string; prefix?: string; intents?: Intent[]; -} +}; export enum Expectation { Guild = 'guild', @@ -16,24 +16,26 @@ export enum HandlerType { Events = 'events', ChatCommands = 'chat_commands', ApplicationCommands = 'application_commands', - Interactions = 'interactions' + Interactions = 'interactions', } -export type Expects, K extends Expectation> = Extract extends never ? false : true; +export type Expects, K extends Expectation> = Extract extends never + ? false + : true; -export const clone: unique symbol = Symbol('clone') -export const fromDiscord: unique symbol = Symbol('fromDiscord') -export const execute: unique symbol = Symbol('execute') +export const clone: unique symbol = Symbol('clone'); +export const fromDiscord: unique symbol = Symbol('fromDiscord'); +export const execute: unique symbol = Symbol('execute'); export const Trait: { - clone: typeof clone - fromDiscord: typeof fromDiscord - execute: typeof execute + clone: typeof clone; + fromDiscord: typeof fromDiscord; + execute: typeof execute; } = { clone, fromDiscord, - execute -} + execute, +}; export type EventPayload = any; @@ -41,7 +43,7 @@ export type Primitive = string | number | boolean | null; /** * Represents a payload structure used in the Discord API. - * + * * @typedef {Object} Payload * @property {number} op - The opcode of the payload. * @property {Record} d - The data associated with the payload. @@ -58,7 +60,7 @@ export type Payload = { export enum InteractionResponseType { Pong = 1, Message = 4, - DeferredMessage = 5 + DeferredMessage = 5, } export enum Intent { @@ -79,110 +81,189 @@ export enum Intent { DIRECT_MESSAGE_TYPING = 1 << 14, MESSAGE_CONTENT = 1 << 15, DEFAULT = 513, - ALL = 32767 + ALL = 32767, } export const IntentGroup: { [key in Intent]?: [string[], number] } = { - [Intent.GUILDS]: [[ - "GUILD_CREATE", - "GUILD_UPDATE", - "GUILD_DELETE", - "GUILD_ROLE_CREATE", - "GUILD_ROLE_UPDATE", - "GUILD_ROLE_DELETE", - "CHANNEL_CREATE", - "CHANNEL_UPDATE", - "CHANNEL_DELETE", - "CHANNEL_PINS_UPDATE", - "THREAD_CREATE", - "THREAD_UPDATE", - "THREAD_DELETE", - "THREAD_LIST_SYNC", - "THREAD_MEMBER_UPDATE", - "THREAD_MEMBERS_UPDATE *", - "STAGE_INSTANCE_CREATE", - "STAGE_INSTANCE_UPDATE", - "STAGE_INSTANCE_DELETE" - ], 0], + [Intent.GUILDS]: [ + [ + 'GUILD_CREATE', + 'GUILD_UPDATE', + 'GUILD_DELETE', + 'GUILD_ROLE_CREATE', + 'GUILD_ROLE_UPDATE', + 'GUILD_ROLE_DELETE', + 'CHANNEL_CREATE', + 'CHANNEL_UPDATE', + 'CHANNEL_DELETE', + 'CHANNEL_PINS_UPDATE', + 'THREAD_CREATE', + 'THREAD_UPDATE', + 'THREAD_DELETE', + 'THREAD_LIST_SYNC', + 'THREAD_MEMBER_UPDATE', + 'THREAD_MEMBERS_UPDATE *', + 'STAGE_INSTANCE_CREATE', + 'STAGE_INSTANCE_UPDATE', + 'STAGE_INSTANCE_DELETE', + ], + 0, + ], - [Intent.GUILD_MEMBERS]: [[ - "GUILD_MEMBER_ADD", - "GUILD_MEMBER_UPDATE", - "GUILD_MEMBER_REMOVE", - "THREAD_MEMBERS_UPDATE *", - ], 1], + [Intent.GUILD_MEMBERS]: [ + ['GUILD_MEMBER_ADD', 'GUILD_MEMBER_UPDATE', 'GUILD_MEMBER_REMOVE', 'THREAD_MEMBERS_UPDATE *'], + 1, + ], - [Intent.GUILD_BANS]: [[ - "GUILD_BAN_ADD", - "GUILD_BAN_REMOVE", - ], 2], + [Intent.GUILD_BANS]: [['GUILD_BAN_ADD', 'GUILD_BAN_REMOVE'], 2], - [Intent.GUILD_EMOJIS_AND_STICKERS]: [[ - "GUILD_EMOJIS_UPDATE", - "GUILD_STICKERS_UPDATE", - ], 3], + [Intent.GUILD_EMOJIS_AND_STICKERS]: [['GUILD_EMOJIS_UPDATE', 'GUILD_STICKERS_UPDATE'], 3], - [Intent.GUILD_INTEGRATIONS]: [[ - "GUILD_INTEGRATIONS_UPDATE", - "INTEGRATION_CREATE", - "INTEGRATION_UPDATE", - "INTEGRATION_DELETE", - ], 4], + [Intent.GUILD_INTEGRATIONS]: [ + ['GUILD_INTEGRATIONS_UPDATE', 'INTEGRATION_CREATE', 'INTEGRATION_UPDATE', 'INTEGRATION_DELETE'], + 4, + ], - [Intent.GUILD_WEBHOOKS]: [[ - "WEBHOOKS_UPDATE" - ], 5], + [Intent.GUILD_WEBHOOKS]: [['WEBHOOKS_UPDATE'], 5], - [Intent.GUILD_INVITES]: [[ - "INVITE_CREATE", - "INVITE_DELETE", - ], 6], + [Intent.GUILD_INVITES]: [['INVITE_CREATE', 'INVITE_DELETE'], 6], - [Intent.GUILD_VOICE_STATES]: [[ - "VOICE_STATE_UPDATE" - ], 7], + [Intent.GUILD_VOICE_STATES]: [['VOICE_STATE_UPDATE'], 7], - [Intent.GUILD_PRESENCES]: [[ - "PRESENCE_UPDATE" - ], 8], + [Intent.GUILD_PRESENCES]: [['PRESENCE_UPDATE'], 8], - [Intent.GUILD_MESSAGES]: [[ - "MESSAGE_CREATE", - "MESSAGE_UPDATE", - "MESSAGE_DELETE", - "MESSAGE_DELETE_BULK", - ], 9], + [Intent.GUILD_MESSAGES]: [['MESSAGE_CREATE', 'MESSAGE_UPDATE', 'MESSAGE_DELETE', 'MESSAGE_DELETE_BULK'], 9], - [Intent.GUILD_MESSAGE_REACTIONS]: [[ - "MESSAGE_REACTION_ADD", - "MESSAGE_REACTION_REMOVE", - "MESSAGE_REACTION_REMOVE_ALL", - "MESSAGE_REACTION_REMOVE_EMOJI", - ], 10], + [Intent.GUILD_MESSAGE_REACTIONS]: [ + [ + 'MESSAGE_REACTION_ADD', + 'MESSAGE_REACTION_REMOVE', + 'MESSAGE_REACTION_REMOVE_ALL', + 'MESSAGE_REACTION_REMOVE_EMOJI', + ], + 10, + ], - [Intent.GUILD_MESSAGE_TYPING]: [[ - "TYPING_START" - ], 11], + [Intent.GUILD_MESSAGE_TYPING]: [['TYPING_START'], 11], - [Intent.DIRECT_MESSAGES]: [[ - "MESSAGE_CREATE", - "MESSAGE_UPDATE", - "MESSAGE_DELETE", - "CHANNEL_PINS_UPDATE", - ], 12], + [Intent.DIRECT_MESSAGES]: [['MESSAGE_CREATE', 'MESSAGE_UPDATE', 'MESSAGE_DELETE', 'CHANNEL_PINS_UPDATE'], 12], - [Intent.DIRECT_MESSAGE_REACTIONS]: [[ - "MESSAGE_REACTION_ADD", - "MESSAGE_REACTION_REMOVE", - "MESSAGE_REACTION_REMOVE_ALL", - "MESSAGE_REACTION_REMOVE_EMOJI", - ], 13], + [Intent.DIRECT_MESSAGE_REACTIONS]: [ + [ + 'MESSAGE_REACTION_ADD', + 'MESSAGE_REACTION_REMOVE', + 'MESSAGE_REACTION_REMOVE_ALL', + 'MESSAGE_REACTION_REMOVE_EMOJI', + ], + 13, + ], - [Intent.DIRECT_MESSAGE_TYPING]: [[ - "TYPING_START" - ], 14], + [Intent.DIRECT_MESSAGE_TYPING]: [['TYPING_START'], 14], - [Intent.MESSAGE_CONTENT]: [[ - "MESSAGE_CONTENT" - ], 15] -} \ No newline at end of file + [Intent.MESSAGE_CONTENT]: [['MESSAGE_CONTENT'], 15], +}; + +export type DiscordAPIEvent = + | 'READY' + | 'RESUMED' + | 'CHANNEL_CREATE' + | 'CHANNEL_UPDATE' + | 'CHANNEL_DELETE' + | 'CHANNEL_PINS_UPDATE' + | 'THREAD_CREATE' + | 'THREAD_UPDATE' + | 'THREAD_DELETE' + | 'THREAD_LIST_SYNC' + | 'THREAD_MEMBER_UPDATE' + | 'THREAD_MEMBERS_UPDATE *' + | 'STAGE_INSTANCE_CREATE' + | 'STAGE_INSTANCE_UPDATE' + | 'STAGE_INSTANCE_DELETE' + | 'GUILD_CREATE' + | 'GUILD_UPDATE' + | 'GUILD_DELETE' + | 'GUILD_ROLE_CREATE' + | 'GUILD_ROLE_UPDATE' + | 'GUILD_ROLE_DELETE' + | 'GUILD_MEMBER_ADD' + | 'GUILD_MEMBER_UPDATE' + | 'GUILD_MEMBER_REMOVE' + | 'GUILD_MEMBERS_CHUNK *' + | 'GUILD_BAN_ADD' + | 'GUILD_BAN_REMOVE' + | 'GUILD_EMOJIS_UPDATE' + | 'GUILD_STICKERS_UPDATE' + | 'GUILD_INTEGRATIONS_UPDATE' + | 'INTEGRATION_CREATE' + | 'INTEGRATION_UPDATE' + | 'INTEGRATION_DELETE' + | 'WEBHOOKS_UPDATE' + | 'INVITE_CREATE' + | 'INVITE_DELETE' + | 'VOICE_STATE_UPDATE' + | 'MESSAGE_CREATE' + | 'MESSAGE_UPDATE' + | 'MESSAGE_DELETE' + | 'MESSAGE_DELETE_BULK' + | 'MESSAGE_REACTION_ADD' + | 'MESSAGE_REACTION_REMOVE' + | 'MESSAGE_REACTION_REMOVE_ALL' + | 'MESSAGE_REACTION_REMOVE_EMOJI' + | 'TYPING_START' + | 'PRESENCE_UPDATE' + | 'APPLICATION_COMMAND_CREATE' + | 'APPLICATION_COMMAND_UPDATE' + | 'APPLICATION_COMMAND_DELETE'; + +export enum DiscordAPIEvents { + READY = 'READY', + RESUMED = 'RESUMED', + CHANNEL_CREATE = 'CHANNEL_CREATE', + CHANNEL_UPDATE = 'CHANNEL_UPDATE', + CHANNEL_DELETE = 'CHANNEL_DELETE', + CHANNEL_PINS_UPDATE = 'CHANNEL_PINS_UPDATE', + THREAD_CREATE = 'THREAD_CREATE', + THREAD_UPDATE = 'THREAD_UPDATE', + THREAD_DELETE = 'THREAD_DELETE', + THREAD_LIST_SYNC = 'THREAD_LIST_SYNC', + THREAD_MEMBER_UPDATE = 'THREAD_MEMBER_UPDATE', + THREAD_MEMBERS_UPDATE = 'THREAD_MEMBERS_UPDATE *', + STAGE_INSTANCE_CREATE = 'STAGE_INSTANCE_CREATE', + STAGE_INSTANCE_UPDATE = 'STAGE_INSTANCE_UPDATE', + STAGE_INSTANCE_DELETE = 'STAGE_INSTANCE_DELETE', + GUILD_CREATE = 'GUILD_CREATE', + GUILD_UPDATE = 'GUILD_UPDATE', + GUILD_DELETE = 'GUILD_DELETE', + GUILD_ROLE_CREATE = 'GUILD_ROLE_CREATE', + GUILD_ROLE_UPDATE = 'GUILD_ROLE_UPDATE', + GUILD_ROLE_DELETE = 'GUILD_ROLE_DELETE', + GUILD_MEMBER_ADD = 'GUILD_MEMBER_ADD', + GUILD_MEMBER_UPDATE = 'GUILD_MEMBER_UPDATE', + GUILD_MEMBER_REMOVE = 'GUILD_MEMBER_REMOVE', + GUILD_MEMBERS_CHUNK = 'GUILD_MEMBERS_CHUNK *', + GUILD_BAN_ADD = 'GUILD_BAN_ADD', + GUILD_BAN_REMOVE = 'GUILD_BAN_REMOVE', + GUILD_EMOJIS_UPDATE = 'GUILD_EMOJIS_UPDATE', + GUILD_STICKERS_UPDATE = 'GUILD_STICKERS_UPDATE', + GUILD_INTEGRATIONS_UPDATE = 'GUILD_INTEGRATIONS_UPDATE', + INTEGRATION_CREATE = 'INTEGRATION_CREATE', + INTEGRATION_UPDATE = 'INTEGRATION_UPDATE', + INTEGRATION_DELETE = 'INTEGRATION_DELETE', + WEBHOOKS_UPDATE = 'WEBHOOKS_UPDATE', + INVITE_CREATE = 'INVITE_CREATE', + INVITE_DELETE = 'INVITE_DELETE', + VOICE_STATE_UPDATE = 'VOICE_STATE_UPDATE', + MESSAGE_CREATE = 'MESSAGE_CREATE', + MESSAGE_UPDATE = 'MESSAGE_UPDATE', + MESSAGE_DELETE = 'MESSAGE_DELETE', + MESSAGE_DELETE_BULK = 'MESSAGE_DELETE_BULK', + MESSAGE_REACTION_ADD = 'MESSAGE_REACTION_ADD', + MESSAGE_REACTION_REMOVE = 'MESSAGE_REACTION_REMOVE', + MESSAGE_REACTION_REMOVE_ALL = 'MESSAGE_REACTION_REMOVE_ALL', + MESSAGE_REACTION_REMOVE_EMOJI = 'MESSAGE_REACTION_REMOVE_EMOJI', + TYPING_START = 'TYPING_START', + PRESENCE_UPDATE = 'PRESENCE_UPDATE', + APPLICATION_COMMAND_CREATE = 'APPLICATION_COMMAND_CREATE', + APPLICATION_COMMAND_UPDATE = 'APPLICATION_COMMAND_UPDATE', + APPLICATION_COMMAND_DELETE = 'APPLICATION_COMMAND_DELETE', +}