From 5b1fa15e665c7c5097e646196bf5c680573f11a2 Mon Sep 17 00:00:00 2001 From: Jaron Date: Wed, 13 Nov 2024 13:34:43 -0600 Subject: [PATCH] Hydrator is coming along --- src/index.ts | 3 +- src/services/api.ts | 7 ++-- src/services/cache.ts | 18 ++++----- src/services/client.ts | 9 ++++- src/services/gateway.ts | 8 ++-- src/services/guilds.ts | 4 +- src/services/hydrator.ts | 45 +++++++++++++++++++++++ src/services/messages.ts | 12 +++--- src/services/users.ts | 6 +-- src/types/common.ts | 13 ++++--- src/types/context.ts | 32 +++++++++------- src/types/guild.ts | 8 +--- src/types/message.ts | 79 ++++++++++++++++++++++++---------------- src/types/user.ts | 6 +-- testing/demo.test.ts | 34 ++++++++--------- 15 files changed, 174 insertions(+), 110 deletions(-) create mode 100644 src/services/hydrator.ts diff --git a/src/index.ts b/src/index.ts index 70943db..7c57fa9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,11 +5,12 @@ export { BaseHandler, type Handler } from './types/hander'; // Consumable types export type { ClientConfig } from './types/common'; export type { Context } from './types/context'; +export { Expectation } from './types/common'; // Consumable util classes export { Guild } from './types/guild'; export { Role } from './types/role'; export { Member } from './types/member'; export { User } from './types/user'; -export { Message } from './types/message'; +export { Message, type MessagePayload } from './types/message'; export { Embed } from './types/embed'; export { Emoji } from './types/emoji'; \ No newline at end of file diff --git a/src/services/api.ts b/src/services/api.ts index 42daa31..6517855 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -6,7 +6,7 @@ export class API { constructor(private client: Client) {} async request(method: string, path: string, data?: any): Promise { - const url = new URL(path, this.base_url).toString(); + const url = `${this.base_url}${path.startsWith('/') ? '' : '/'}${path}`; const headers = { Authorization: `Bot ${this.client.token}`, 'Accept': 'application/json', @@ -14,6 +14,7 @@ export class API { }; console.log(`API Request: ${method} ${url}`); + console.log(`API Body: ${JSON.stringify(data, null, 4)}`); const response = await fetch(url, { method, @@ -22,8 +23,8 @@ export class API { }); if (!response.ok) { - console.error(`Failed to fetch: ${response.statusText} ${response.url} ${response.status}`); - console.error(response.json()); + // console.error(`Failed to fetch: ${response.statusText} ${response.url} ${response.status}`); + console.log('Failed Request Response: ', await response.text()); throw new Error(`Failed to fetch: ${response.statusText}`); } diff --git a/src/services/cache.ts b/src/services/cache.ts index 257bb1a..92d9571 100644 --- a/src/services/cache.ts +++ b/src/services/cache.ts @@ -3,19 +3,19 @@ import { exists } from "../utils"; import type { API } from "./api"; import type { Processor } from "./processor"; -export class ObjectCache { - cache = new Map | T>(); +export class ObjectCache { + cache = new Map>(); readonly type: string; - constructor(protected api: API, processor: Processor, type: string, private readonly factory: (...args: any[]) => Promise, events: 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(e)); + this.cache.set(e.id, Promise.resolve(factory.fromDiscord(e))); }); } - set(object_id: string, object: T): void { - this.cache.set(object_id, object); + set(object_id: string, object: K): void { + this.cache.set(object_id, Promise.resolve(object)); } // update(object_id: string, object: Partial): void { @@ -29,14 +29,14 @@ export class ObjectCache { // } // } - async fetch(object_id: string, ..._args: any[]): Promise { - const req = this.api.get(`/${this.type}/${object_id}`).then((data) => this.factory(data)); + 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 { + async get(object_id: string, ..._args: any[]): Promise { if (this.cache.has(object_id)) { return await Promise.resolve(this.cache.get(object_id)!); } else { diff --git a/src/services/client.ts b/src/services/client.ts index 8786246..764f62e 100644 --- a/src/services/client.ts +++ b/src/services/client.ts @@ -1,13 +1,14 @@ import { API } from './api'; import { Gateway } from './gateway'; import { Processor } from './processor'; -import { Intent, type ClientConfig } from '../types/common'; +import { Expectation, 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'; export class Client { readonly token: string; @@ -65,12 +66,16 @@ export class Client { } } + 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.hydrate(DiscordMessage.fromAPIResponse(await this.api.post(`/channels/${channel_id}/messages`, message.toJSON())), this, this.api); + 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/gateway.ts index eb114aa..7823f26 100644 --- a/src/services/gateway.ts +++ b/src/services/gateway.ts @@ -141,8 +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 discord_message = DiscordMessage.fromAPIResponse(event_data); - const message = await Message.hydrate(discord_message, this.client, this.api); + const message = Message.fromDiscord(DiscordMessage.fromAPIResponse(event_data)); // And then generate the context context = new Context(this.client, this.api, message); } @@ -165,7 +164,7 @@ export class Gateway { return; } // Extract the author - const author = User.hydrate(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,9 +185,8 @@ export class Gateway { // Extract the command name // TODO: Case insensitive config? const command = args.shift()!; - console.log('payload.d', payload.d) // Create a new message object from the payload - const message = await Message.hydrate(DiscordMessage.fromAPIResponse(payload.d), this.client, this.api); + const message = Message.fromDiscord(DiscordMessage.fromAPIResponse(payload.d)); // Generate context const context = new Context(this.client, this.api, message); // Check if command exists diff --git a/src/services/guilds.ts b/src/services/guilds.ts index 433fa9d..4ee06d4 100644 --- a/src/services/guilds.ts +++ b/src/services/guilds.ts @@ -3,8 +3,8 @@ import type { API } from "./api"; import { ObjectCache } from "./cache"; import type { Processor } from "./processor"; -export class Guilds extends ObjectCache { +export class Guilds extends ObjectCache { constructor (api: API, processor: Processor) { - super(api, processor, 'guilds', Guild.factory, ['GUILD_CREATE', 'GUILD_UPDATE']); + super(api, processor, 'guilds', Guild, ['GUILD_CREATE', 'GUILD_UPDATE']); } } \ No newline at end of file diff --git a/src/services/hydrator.ts b/src/services/hydrator.ts new file mode 100644 index 0000000..cd2ba4c --- /dev/null +++ b/src/services/hydrator.ts @@ -0,0 +1,45 @@ +import { Expectation } from "../types/common"; +import { Context, type HydratedContext } from "../types/context"; +import { Message, type HydratedMessage } from "../types/message"; +import { exists } from "../utils"; +import type { API } from "./api"; +import type { Client } from "./client"; + +export type GatewayObject = Message; +export type Hydrateable = Context | GatewayObject +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; + + if (data instanceof Context) { + for (const expectation of expectations) { + if (expectation === Expectation.Guild && exists(data.guild_id)) { + let context = data as unknown as HydratedContext<[Expectation.Guild, ...Expectation[]]>; + context.guild = await client.guilds.get(data.guild_id); + } else { + hydrated = false; + break; + } + } + } else if (data instanceof Message) { + for (const expectation of expectations) { + if (expectation === Expectation.Message && exists(data.message_reference?.id)) { + let message = data as unknown as HydratedMessage<[Expectation.Message, ...Expectation[]]>; + message.reference = await client.messages.get(data.message_reference.channel_id, data.message_reference.id); + } else if (expectation === Expectation.Guild && exists(data.guild_id)) { + let message = data as unknown as HydratedMessage<[Expectation.Guild, ...Expectation[]]>; + message.guild = await client.guilds.get(data.guild_id); + } else { + hydrated = false; + break; + } + } + } + + return function (data: unknown): data is Hydrated { + return hydrated; + } +} \ No newline at end of file diff --git a/src/services/messages.ts b/src/services/messages.ts index 30ad324..dd5b34b 100644 --- a/src/services/messages.ts +++ b/src/services/messages.ts @@ -1,22 +1,22 @@ -import { Message } from "../types/message"; +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 { +export class Messages extends ObjectCache { constructor (protected client: Client, api: API, processor: Processor) { - super(api, processor, 'message', (data) => Message.factory(data, client, api), ['MESSAGE_CREATE', 'MESSAGE_UPDATE']); + 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.factory(data, this.client, this.api, undefined, resolveReference)); + 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 { + 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 { diff --git a/src/services/users.ts b/src/services/users.ts index 1b59288..4d8c1fd 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -1,10 +1,10 @@ -import type { User } from "../types/user"; +import { User } from "../types/user"; import type { API } from "./api"; import { ObjectCache } from "./cache"; import type { Processor } from "./processor"; -export class Users extends ObjectCache { +export class Users extends ObjectCache { constructor (api: API, processor: Processor) { - super(api, processor, 'users', []); + super(api, processor, 'users', User, []); } } \ No newline at end of file diff --git a/src/types/common.ts b/src/types/common.ts index 0563bf4..8f5e112 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,13 +1,17 @@ -import type { Client } from "../services/client"; -import type { Context } from "./context"; -import type { BaseHandler } from "./hander"; - export type ClientConfig = { token: string; application_id: string; prefix?: string; intents?: Intent[]; } + +export enum Expectation { + Guild = 'guild', + Channel = 'channel', + User = 'user', + Message = 'message', +} + export enum HandlerType { Events = 'events', ChatCommands = 'chat_commands', @@ -15,7 +19,6 @@ export enum HandlerType { Interactions = 'interactions' } - export type EventPayload = any; export type Primitive = string | number | boolean | null; diff --git a/src/types/context.ts b/src/types/context.ts index 7c90c4a..0e28a7b 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -1,7 +1,10 @@ import type { API } from "../services/api"; import type { Client } from "../services/client"; +import { hydrator, type Hydrateable } from "../services/hydrator"; import { isNil } from "../utils"; -import { DiscordMessage, Message } from "./message"; +import type { Expectation } from "./common"; +import type { Guild } from "./guild"; +import { 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. @@ -20,13 +23,18 @@ import { DiscordMessage, Message } from "./message"; * @throws {Error} If the message ID is not provided. * @throws {Error} If the channel ID is not provided. */ + +export type HydratedContext> = Context & { + guild: Extract extends never ? undefined : Guild; +}; + export class Context { // Public properties message_id: string; channel_id: string; guild_id?: string; - constructor(private client: Client, private api: API, public message: Message) { + constructor(private client: Client, private api: API, public message: MessagePayload) { console.log('Context created: ', ); if (isNil(message.id)) { throw new Error('Message ID is required to create a context'); @@ -42,25 +50,21 @@ export class Context { this.guild_id = message.guild_id; } + hydrator = >(data: T, expectations: K) => { + return hydrator(data, expectations, this.client, this.api); + } + // get author() { // return this.client.users.get(this.author_id); // }; - async fetchGuild() { - if (isNil(this.guild_id)) { - return Promise.resolve(null); - } - - return this.client.guilds.get(this.guild_id); - } - /** * Reply to the message that this command * @param message The message to send to the channel * @param reference If true the message will be a reference the the message that triggered the command * @returns The message that was sent */ - public reply = (message: Message | string, reference: boolean = true) => { + public reply = async (message: Message | string, reference: boolean = true): Promise => { if (typeof message === 'string') { message = new Message().setContent(message); } @@ -92,8 +96,8 @@ export class Context { * @param {string} [message_id] - The ID of the message to delete. If not provided, the message ID of this context will be used. * @returns {Promise} A promise that resolves when the message is deleted. */ - public deleteMessage = (message_id: string) => { - return this.api.delete(`/channels/${this.channel_id}/messages/${message_id ?? this.message_id}`); + public deleteMessage = (message_id: string): Promise => { + return this.api.delete(`/channels/${this.channel_id}/messages/${message_id ?? this.message_id}`).then(() => {}); }; /** @@ -108,6 +112,6 @@ export class Context { if (content) { clone.setContent(content); } - return Message.hydrate(DiscordMessage.fromAPIResponse(await this.api.patch(`/channels/${clone.channel_id}/messages/${clone.id}`, clone)), this.client, this.api); + return Message.fromDiscord(DiscordMessage.fromAPIResponse(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 593a5f3..7a8e505 100644 --- a/src/types/guild.ts +++ b/src/types/guild.ts @@ -24,13 +24,7 @@ export class Guild { // this.roles = data.roles; } - - public static async factory(data: any) { - return await Guild.hydrate(DiscordGuild.fromAPIResponse(data)); - } - - - public static async hydrate(data: DiscordGuild) { + public static fromDiscord(data: DiscordGuild) { return new Guild({ id: data.id, name: data.name, diff --git a/src/types/message.ts b/src/types/message.ts index 28641c9..ecbb466 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -1,7 +1,9 @@ import type { API } from '../services/api'; import type { Client } from '../services/client'; import { isNil } from '../utils'; +import type { Expectation } from './common'; import { Embed } from './embed'; +import type { Guild } from './guild'; import { DiscordUser, User } from './user'; export type Component = Partial