diff --git a/src/services/cache.ts b/src/services/cache.ts deleted file mode 100644 index 92d9571..0000000 --- a/src/services/cache.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Context } from "../types/context"; -import { exists } from "../utils"; -import type { API } from "./api"; -import type { Processor } from "./processor"; - -export class ObjectCache { - cache = 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[]) { - this.type = type; - processor.events.register(events, async (_context: Context, e: Record) => { - this.cache.set(e.id, Promise.resolve(factory.fromDiscord(e))); - }); - } - - set(object_id: string, object: K): void { - this.cache.set(object_id, Promise.resolve(object)); - } - - // update(object_id: string, object: Partial): void { - // const existing = this.cache.get(object_id); - - // if (exists(existing)) { - // // TODO: Idk wtf happens if this is called on two classes - // this.cache.set(object_id, { ...existing, ...object }); - // } else { - // this.cache.set(object_id, object as T); - // } - // } - - async fetch(object_id: string, ..._args: any[]): Promise { - const req = this.api.get(`/${this.type}/${object_id}`).then((data) => this.factory.fromDiscord(data)); - this.cache.set(object_id, req); - req.catch(() => this.cache.delete(object_id)); - return await req; - } - - async get(object_id: string, ..._args: any[]): Promise { - if (this.cache.has(object_id)) { - return await Promise.resolve(this.cache.get(object_id)!); - } else { - return this.fetch(object_id); - } - } - - list(): Map | T> { - return this.cache; - } -} diff --git a/src/services/caches/cache.ts b/src/services/caches/cache.ts new file mode 100644 index 0000000..d055e39 --- /dev/null +++ b/src/services/caches/cache.ts @@ -0,0 +1,71 @@ +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 = new Map(); + 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[]) { + this.type = type; + processor.events.register(events, async (_context: Context, e: Record) => { + this.cache.set(e.id, factory[fromDiscord](e)); + }); + } + + set(object_id: string, object: K): void { + this.cache.set(object_id, object); + } + + // update(object_id: string, object: Partial): void { + // const existing = this.cache.get(object_id); + + // if (exists(existing)) { + // // TODO: Idk wtf happens if this is called on two classes + // this.cache.set(object_id, { ...existing, ...object }); + // } else { + // this.cache.set(object_id, object as T); + // } + // } + + 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 { + // Dispatch the request + const req = this.api.get(`/${this.type}/${object_id}`).then((data) => { + // Create the object + const object = this.factory[fromDiscord](data); + // Save the object + this.cache.set(object_id, object); + // Return the object + return object; + }); + // 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)); + // Return the request + return req; + } + } + + async get(object_id: string, ..._args: any[]): Promise { + if (this.cache.has(object_id)) { + return await Promise.resolve(this.cache.get(object_id)!); + } else { + return this.fetch(object_id); + } + } + + list(): Map | T> { + return this.cache; + } +} diff --git a/src/services/guilds.ts b/src/services/caches/guilds.ts similarity index 65% rename from src/services/guilds.ts rename to src/services/caches/guilds.ts index 4ee06d4..2532900 100644 --- a/src/services/guilds.ts +++ b/src/services/caches/guilds.ts @@ -1,7 +1,7 @@ -import { Guild } from "../types/guild"; -import type { API } from "./api"; +import { Guild } from "../../types/guild"; +import type { API } from "../api"; +import type { Processor } from "../processor"; import { ObjectCache } from "./cache"; -import type { Processor } from "./processor"; export class Guilds extends ObjectCache { constructor (api: API, processor: Processor) { diff --git a/src/services/caches/messages.ts b/src/services/caches/messages.ts new file mode 100644 index 0000000..404f046 --- /dev/null +++ b/src/services/caches/messages.ts @@ -0,0 +1,42 @@ +import { fromDiscord } from "../../types/common"; +import { Message, type MessagePayload } from "../../types/message"; +import type { API } from "../api"; +import type { Client } from "../client"; +import type { Processor } from "../processor"; +import { ObjectCache } from "./cache"; + + +export class Messages extends ObjectCache { + constructor (protected client: Client, api: API, processor: Processor) { + super(api, processor, 'message', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']); + } + + async fetch(channel_id: string, message_id: string): Promise { + if (this.requests.has(message_id)) { + return this.requests.get(message_id)!; + } else { + // Dispatch the request + const req = this.api.get(`/channels/${channel_id}/messages/${message_id}`).then((data) => { + const message = Message[fromDiscord](data); + this.cache.set(message_id, message); + return message; + }); + // Save the request + this.requests.set(message_id, req); + // Remove the request from the cache if it succeeds + req.then(() => this.requests.delete(message_id)); + // Remove the request from the cache if it fails + req.catch(() => this.requests.delete(message_id)); + // Return the request + return req; + } + } + + async get(channel_id: string, message_id: string): Promise { + if (this.cache.has(message_id)) { + return await Promise.resolve(this.cache.get(message_id)!); + } else { + return this.fetch(channel_id, message_id); + } + } +} \ No newline at end of file diff --git a/src/services/users.ts b/src/services/caches/users.ts similarity index 61% rename from src/services/users.ts rename to src/services/caches/users.ts index 4d8c1fd..db148b3 100644 --- a/src/services/users.ts +++ b/src/services/caches/users.ts @@ -1,7 +1,8 @@ -import { User } from "../types/user"; -import type { API } from "./api"; +import { User } from "../../types/user"; +import type { API } from "../api"; +import type { Processor } from "../processor"; import { ObjectCache } from "./cache"; -import type { Processor } from "./processor"; + export class Users extends ObjectCache { constructor (api: API, processor: Processor) { diff --git a/src/services/client.ts b/src/services/client.ts index 764f62e..55b9e0f 100644 --- a/src/services/client.ts +++ b/src/services/client.ts @@ -1,14 +1,14 @@ import { API } from './api'; -import { Gateway } from './gateway'; +import { Gateway } from './connectors/gateway'; import { Processor } from './processor'; -import { Expectation, Intent, type ClientConfig } from '../types/common'; +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 { Guilds } from './guilds'; -import { Users } from './users'; -import { Messages } from './messages'; -import { hydrator, type Hydrateable } from './hydrator'; +import { Guilds } from './caches/guilds'; +import { Messages } from './caches/messages'; +import { Users } from './caches/users'; +import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator'; export class Client { readonly token: string; @@ -66,6 +66,34 @@ 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) => { + 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); } @@ -75,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](DiscordMessage.fromAPIResponse(await this.api.post(`/channels/${channel_id}/messages`, message.toJSON()))); } chatCommands = { diff --git a/src/services/gateway.ts b/src/services/connectors/gateway.ts similarity index 91% rename from src/services/gateway.ts rename to src/services/connectors/gateway.ts index 7823f26..03994a8 100644 --- a/src/services/gateway.ts +++ b/src/services/connectors/gateway.ts @@ -1,12 +1,12 @@ import { WebSocket, type Data } from 'ws'; -import type { Client } from "./client"; -import 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 { DiscordMessage, Message } from '../types/message'; -import { DiscordUser, User } from '../types/user'; +import type { Client } from "../client"; +import { fromDiscord, 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'; export class Gateway { private socket: WebSocket | null = null; @@ -141,7 +141,7 @@ export class Gateway { if (['MESSAGE_CREATE', 'MESSAGE_UPDATE'].includes(event_name)) { // If the event is a message event, create a message object - const message = Message.fromDiscord(DiscordMessage.fromAPIResponse(event_data)); + const message = Message[fromDiscord](event_data as DiscordMessage); // And then generate the context context = new Context(this.client, this.api, message); } @@ -164,7 +164,7 @@ export class Gateway { return; } // Extract the author - const author = User.fromDiscord(DiscordUser.fromAPIResponse(payload.d.author)); + const author = User[fromDiscord](DiscordUser.fromAPIResponse(payload.d.author)); // Store the author in the cache this.client.users.set(author.id, author); // Extract the content @@ -186,7 +186,7 @@ export class Gateway { // TODO: Case insensitive config? const command = args.shift()!; // Create a new message object from the payload - const message = Message.fromDiscord(DiscordMessage.fromAPIResponse(payload.d)); + const message = Message[fromDiscord](payload.d as DiscordMessage); // Generate context const context = new Context(this.client, this.api, message); // Check if command exists diff --git a/src/services/hydrator.ts b/src/services/hydrator.ts index cd2ba4c..5971a22 100644 --- a/src/services/hydrator.ts +++ b/src/services/hydrator.ts @@ -7,12 +7,34 @@ import type { Client } from "./client"; export type GatewayObject = Message; export type Hydrateable = Context | GatewayObject -export type Hydrated> = +export type Hydrated> = T extends Message ? HydratedMessage : T extends Context ? HydratedContext : never; -export async function hydrator>(data: T, expectations: K, client: Client, api: API): Promise<(data: unknown) => data is Hydrated> { - let hydrated = true; +/** + * 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. + * @param client - The client instance used to fetch additional data required for hydration. + * @param api - The API instance used to fetch additional data required for hydration. + * + * @returns A promise that resolves to the hydrated data object. + * + * @throws Will throw an error if an expectation cannot be resolved. + */ +export async function hydrate, K extends Array>( + data: T, + expectations: K, + client: Client, + api: API +): Promise ? Hydrated : Hydrated> { + // TODO: add clone trait. Make it a requirement for hydrateable objects. + // Then clone the object and return the hydrated object. + let hydrated: Hydrated; if (data instanceof Context) { for (const expectation of expectations) { @@ -20,8 +42,7 @@ export async function hydrator; context.guild = await client.guilds.get(data.guild_id); } else { - hydrated = false; - break; + throw new Error(`Could not resolve expectation: ${expectation}`); } } } else if (data instanceof Message) { @@ -33,13 +54,45 @@ export async function hydrator; message.guild = await client.guilds.get(data.guild_id); } else { - hydrated = false; - break; + throw new Error(`Could not resolve expectation: ${expectation}`); } } } - return function (data: unknown): data is Hydrated { + return data as unknown as (T extends Hydrated ? Hydrated : Hydrated); +} + +/** + * 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. + * @param client - The client instance used to fetch additional data required for hydration. + * @param api - The API instance used to fetch additional data required for hydration. + * + * @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`. + */ +export async function hydrator, K extends Array>( + data: T, + expectations: K, + client: Client, + api: API +): Promise<(data: unknown) => data is T extends Hydrated ? Hydrated : Hydrated> { + let hydrated = true; + + try { + // TODO: When clone is implemented we will receive a clone of the object here + // So in hydrator we will need to copy the properties from the clone to the original object + await hydrate(data, expectations, client, api); + } catch (error) { + hydrated = false; + } + + // BUG: Resolves to never after hydrations on an already hydrated object + return function (data: unknown): data is T extends Hydrated ? Hydrated : Hydrated { return hydrated; } } \ No newline at end of file diff --git a/src/services/messages.ts b/src/services/messages.ts deleted file mode 100644 index dd5b34b..0000000 --- a/src/services/messages.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Message, type MessagePayload } from "../types/message"; -import type { API } from "./api"; -import { ObjectCache } from "./cache"; -import type { Client } from "./client"; -import type { Processor } from "./processor"; - -export class Messages extends ObjectCache { - constructor (protected client: Client, api: API, processor: Processor) { - super(api, processor, 'message', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']); - } - - async fetch(channel_id: string, message_id: string, resolveReference: boolean = true): Promise { - const req = this.api.get(`/channels/${channel_id}/messages/${message_id}`).then((data) => Message.fromDiscord(data)); - this.cache.set(message_id, req); - req.catch(() => this.cache.delete(message_id)); - return await req; - } - - async get(channel_id: string, message_id: string, resolveReference: boolean = true): Promise { - if (this.cache.has(message_id)) { - return await Promise.resolve(this.cache.get(message_id)!); - } else { - return this.fetch(channel_id, message_id, resolveReference); - } - } -} \ No newline at end of file diff --git a/src/services/processor.ts b/src/services/processor.ts index e876215..ba7bcce 100644 --- a/src/services/processor.ts +++ b/src/services/processor.ts @@ -1,6 +1,6 @@ import type { Client } from "./client"; import type { Context } from "../types/context"; -import { HandlerType, type EventPayload, type Payload } from "../types/common"; +import { HandlerType, type EventPayload } from "../types/common"; import { BaseHandler, type Handler } from "../types/hander"; import type { API } from "./api"; diff --git a/src/types/common.ts b/src/types/common.ts index 8f5e112..95d6902 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -6,10 +6,10 @@ export type ClientConfig = { } export enum Expectation { - Guild = 'guild', - Channel = 'channel', - User = 'user', - Message = 'message', + Guild = 'guild', + Channel = 'channel', + User = 'user', + Message = 'message', } export enum HandlerType { @@ -19,6 +19,16 @@ export enum HandlerType { Interactions = 'interactions' } +export type Expects, K extends Expectation> = Extract extends never ? false : true; + +export const fromDiscord: unique symbol = Symbol('fromDiscord') + +export const Trait: { + fromDiscord: typeof fromDiscord +} = { + fromDiscord +} + export type EventPayload = any; export type Primitive = string | number | boolean | null; @@ -60,107 +70,107 @@ export enum Intent { ALL = 32767 } -export const IntentGroup : {[key in Intent]?: [string[], number]} = { +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" + "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], - + "GUILD_MEMBER_ADD", + "GUILD_MEMBER_UPDATE", + "GUILD_MEMBER_REMOVE", + "THREAD_MEMBERS_UPDATE *", + ], 1], + [Intent.GUILD_BANS]: [[ - "GUILD_BAN_ADD", - "GUILD_BAN_REMOVE", + "GUILD_BAN_ADD", + "GUILD_BAN_REMOVE", ], 2], - + [Intent.GUILD_EMOJIS_AND_STICKERS]: [[ - "GUILD_EMOJIS_UPDATE", - "GUILD_STICKERS_UPDATE", + "GUILD_EMOJIS_UPDATE", + "GUILD_STICKERS_UPDATE", ], 3], - + [Intent.GUILD_INTEGRATIONS]: [[ - "GUILD_INTEGRATIONS_UPDATE", - "INTEGRATION_CREATE", - "INTEGRATION_UPDATE", - "INTEGRATION_DELETE", + "GUILD_INTEGRATIONS_UPDATE", + "INTEGRATION_CREATE", + "INTEGRATION_UPDATE", + "INTEGRATION_DELETE", ], 4], - + [Intent.GUILD_WEBHOOKS]: [[ - "WEBHOOKS_UPDATE" + "WEBHOOKS_UPDATE" ], 5], - + [Intent.GUILD_INVITES]: [[ - "INVITE_CREATE", - "INVITE_DELETE", + "INVITE_CREATE", + "INVITE_DELETE", ], 6], - + [Intent.GUILD_VOICE_STATES]: [[ - "VOICE_STATE_UPDATE" + "VOICE_STATE_UPDATE" ], 7], - + [Intent.GUILD_PRESENCES]: [[ - "PRESENCE_UPDATE" + "PRESENCE_UPDATE" ], 8], - + [Intent.GUILD_MESSAGES]: [[ - "MESSAGE_CREATE", - "MESSAGE_UPDATE", - "MESSAGE_DELETE", - "MESSAGE_DELETE_BULK", + "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", + "MESSAGE_REACTION_ADD", + "MESSAGE_REACTION_REMOVE", + "MESSAGE_REACTION_REMOVE_ALL", + "MESSAGE_REACTION_REMOVE_EMOJI", ], 10], - + [Intent.GUILD_MESSAGE_TYPING]: [[ - "TYPING_START" + "TYPING_START" ], 11], - + [Intent.DIRECT_MESSAGES]: [[ - "MESSAGE_CREATE", - "MESSAGE_UPDATE", - "MESSAGE_DELETE", - "CHANNEL_PINS_UPDATE", + "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", + "MESSAGE_REACTION_ADD", + "MESSAGE_REACTION_REMOVE", + "MESSAGE_REACTION_REMOVE_ALL", + "MESSAGE_REACTION_REMOVE_EMOJI", ], 13], - + [Intent.DIRECT_MESSAGE_TYPING]: [[ - "TYPING_START" + "TYPING_START" ], 14], - + [Intent.MESSAGE_CONTENT]: [[ - "MESSAGE_CONTENT" + "MESSAGE_CONTENT" ], 15] - } \ No newline at end of file +} \ No newline at end of file diff --git a/src/types/context.ts b/src/types/context.ts index 0e28a7b..03cd7e0 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -1,10 +1,10 @@ import type { API } from "../services/api"; import type { Client } from "../services/client"; -import { hydrator, type Hydrateable } from "../services/hydrator"; +import { hydrate, hydrator, type Hydrateable, type Hydrated } from "../services/hydrator"; import { isNil } from "../utils"; -import type { Expectation } from "./common"; +import { fromDiscord, type Expectation } from "./common"; import type { Guild } from "./guild"; -import { DiscordMessage, Message, type MessagePayload } from "./message"; +import { type DiscordMessage, Message, type MessagePayload } from "./message"; /** * Represents the context of a message in a Discord channel. * Provides methods to interact with the message and the channel it was sent in. @@ -50,7 +50,38 @@ export class Context { this.guild_id = message.guild_id; } - hydrator = >(data: T, expectations: K) => { + /** + * 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 < + T extends Hydrateable | Hydrated>, + K extends Array + >(data: T, expectations: K) => { + return hydrate(data, expectations, this.client, 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 >, K extends Array>(data: T, expectations: K) => { return hydrator(data, expectations, this.client, this.api); } @@ -112,6 +143,6 @@ export class Context { if (content) { clone.setContent(content); } - return Message.fromDiscord(DiscordMessage.fromAPIResponse(await this.api.patch(`/channels/${clone.channel_id}/messages/${clone.id}`, clone))); + return Message[fromDiscord](await this.api.patch(`/channels/${clone.channel_id}/messages/${clone.id}`, clone)); }; } \ No newline at end of file diff --git a/src/types/guild.ts b/src/types/guild.ts index 7a8e505..ca87ac7 100644 --- a/src/types/guild.ts +++ b/src/types/guild.ts @@ -1,4 +1,5 @@ import { Channel } from "./channel"; +import { Trait } from "./common"; import { Emoji } from "./emoji"; import { Member } from "./member"; import { Role } from "./role"; @@ -24,7 +25,7 @@ export class Guild { // this.roles = data.roles; } - public static fromDiscord(data: DiscordGuild) { + public static [Trait.fromDiscord](data: DiscordGuild) { return new Guild({ id: data.id, name: data.name, diff --git a/src/types/hander.ts b/src/types/hander.ts index a0ca79c..36898cc 100644 --- a/src/types/hander.ts +++ b/src/types/hander.ts @@ -11,4 +11,4 @@ export type CommandHandlerFunction = (context: Context, args: any[]) => Promise< export type EventHandlerFunction = (context: Context | null, payload: EventPayload) => Promise | void; export type HandlerFunction = CommandHandlerFunction | EventHandlerFunction; -export type Handler = CommandHandlerFunction | EventHandlerFunction | BaseHandler; +export type Handler = HandlerFunction | BaseHandler; diff --git a/src/types/message.ts b/src/types/message.ts index ecbb466..5767087 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -1,7 +1,5 @@ -import type { API } from '../services/api'; -import type { Client } from '../services/client'; import { isNil } from '../utils'; -import type { Expectation } from './common'; +import { fromDiscord, Trait, type Expectation, type Expects } from './common'; import { Embed } from './embed'; import type { Guild } from './guild'; import { DiscordUser, User } from './user'; @@ -128,10 +126,10 @@ export class Message { this.timestamp = data?.timestamp; } - public static fromDiscord(data: DiscordMessage): MessagePayload { + public static [Trait.fromDiscord](data: DiscordMessage): MessagePayload { return new Message({ id: data.id, - author: User.fromDiscord(data.author), + author: User[fromDiscord](data.author), channel_id: data.channel_id, guild_id: data.guild_id, content: data.content, @@ -214,8 +212,8 @@ export class Message { }; } -// Raw API Class -export class DiscordMessage { +// Raw API Object +export type DiscordMessage = { id: string; channel_id: string; guild_id?: string; @@ -257,74 +255,4 @@ export class DiscordMessage { preview_asset?: string; format_type: number; }[]; - - constructor(data: DiscordMessage) { - this.id = data.id; - this.channel_id = data.channel_id; - this.guild_id = data.guild_id; - this.author = data.author; - // this.member = data.member; - this.content = data.content; - this.timestamp = data.timestamp; - this.edited_timestamp = data.edited_timestamp; - this.tts = data.tts; - this.mention_everyone = data.mention_everyone; - this.mentions = data.mentions; - this.mention_roles = data.mention_roles; - this.mention_channels = data.mention_channels; - this.attachments = data.attachments; - // this.embeds = data.embeds; - this.reactions = data.reactions; - this.nonce = data.nonce; - this.pinned = data.pinned; - this.webhook_id = data.webhook_id; - this.type = data.type; - this.activity = data.activity; - this.application = data.application; - this.application_id = data.application_id; - this.message_reference = data.message_reference; - this.flags = data.flags; - this.referenced_message = data.referenced_message; - this.interaction = data.interaction; - this.thread = data.thread; - this.components = data.components; - this.sticker_items = data.sticker_items; - this.stickers = data.stickers; - } - - static fromAPIResponse(data: any): DiscordMessage { - return new DiscordMessage({ - id: data.id, - channel_id: data.channel_id, - guild_id: data.guild_id, - author: data.author, - // member: data.member, - content: data.content, - timestamp: data.timestamp, - edited_timestamp: data.edited_timestamp, - tts: data.tts, - mention_everyone: data.mention_everyone, - mentions: data.mentions, - mention_roles: data.mention_roles, - mention_channels: data.mention_channels, - attachments: data.attachments, - // embeds: data.embeds, - reactions: data.reactions, - nonce: data.nonce, - pinned: data.pinned, - webhook_id: data.webhook_id, - type: data.type, - activity: data.activity, - application: data.application, - application_id: data.application_id, - message_reference: data.message_reference, - flags: data.flags, - referenced_message: data.referenced_message, - interaction: data.interaction, - thread: data.thread, - components: data.components, - sticker_items: data.sticker_items, - stickers: data.stickers - }); - } } \ No newline at end of file diff --git a/src/types/user.ts b/src/types/user.ts index 6f608be..10715ef 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -1,3 +1,5 @@ +import { Trait } from "./common"; + export class User { id: string; username: string; @@ -30,7 +32,7 @@ export class User { this.email = data.email; } - public static fromDiscord(data: DiscordUser): User { + public static [Trait.fromDiscord](data: DiscordUser): User { return new User({ id: data.id, username: data.username, diff --git a/testing/demo.test.ts b/testing/demo.test.ts index c66bd8a..9bb82d5 100644 --- a/testing/demo.test.ts +++ b/testing/demo.test.ts @@ -20,6 +20,25 @@ client.chatCommands.register('ping', async (context: Context, args: any[]) => { await context.editMessage(message, `Pong! Latency: ${time}ms`); }); +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]); + + rehydrated.guild.name + + const hydrate = await context.hydrator(message, [Expectation.Guild]); + const hasGuild = hydrate(message); + + if (hasGuild) { + message.guild.name + } else { + message + } + + // await context.reply(`You are ${message.author.username} ${hasGuild ? message.guild.name : ''}`, true); +}); + client.chatCommands.register('here', async (context: Context, _args: any[]) => { const guild = (await context.hydrator(context, [Expectation.Guild]))(context);