Messages service, ceched promises, adding 'hydration' models

This commit is contained in:
Jaron
2024-11-12 00:38:08 -06:00
parent 17646e7f36
commit f419e82579
16 changed files with 421 additions and 150 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ export class API {
constructor(private client: Client) {}
async request<T = any>(method: string, path: string, data?: any): Promise<T> {
const url = `${this.base_url}${path}`;
const url = new URL(path, this.base_url).toString();
const headers = {
Authorization: `Bot ${this.client.token}`,
'Accept': 'application/json',
@@ -29,7 +29,7 @@ export class API {
const body = await response.json();
console.log('API Response:', JSON.stringify(body, null, 4));
// console.log('API Response:', JSON.stringify(body, null, 4));
return body;
}
+13 -15
View File
@@ -3,15 +3,14 @@ import { exists } from "../utils";
import type { API } from "./api";
import type { Processor } from "./processor";
export class ObjectCache<T> {
cache = new Map<string, T>();
private readonly type: string;
cache = new Map<string, Promise<T> | T>();
readonly type: string;
constructor(private api: API, processor: Processor, type: string, events: string[]) {
constructor(protected api: API, processor: Processor, type: string, private readonly factory: (...args: any[]) => Promise<T>, events: string[]) {
this.type = type;
processor.events.register(events, (_context: Context, e: Record<string, any>) => {
this.cache.set(e.id, e as T);
processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
this.cache.set(e.id, factory(e));
});
}
@@ -30,23 +29,22 @@ export class ObjectCache<T> {
// }
// }
async fetch(object_id: string): Promise<T> {
return this.api.get(`${this.type}/${object_id}`).then((res) => {
const object: T & { id: string } = res.data;
this.cache.set(object.id, object);
return object;
});
async fetch(object_id: string, ..._args: any[]): Promise<T> {
const req = this.api.get(`/${this.type}/${object_id}`).then((data) => this.factory(data));
this.cache.set(object_id, req);
req.catch(() => this.cache.delete(object_id));
return await req;
}
get(object_id: string): T | Promise<T> {
async get(object_id: string, ..._args: any[]): Promise<T> {
if (this.cache.has(object_id)) {
return Promise.resolve(this.cache.get(object_id)!);
return await Promise.resolve(this.cache.get(object_id)!);
} else {
return this.fetch(object_id);
}
}
list(): Map<string, T> {
list(): Map<string, Promise<T> | T> {
return this.cache;
}
}
+20 -23
View File
@@ -4,9 +4,10 @@ import { Processor } from './processor';
import { Intent, type ClientConfig } from '../types/common';
import { exists, isNil } from '../utils/index';
import type { Handler } from '../types/hander';
import { Message } from '../types/message';
import { DiscordMessage, Message } from '../types/message';
import { Guilds } from './guilds';
import { Users } from './users';
import { Messages } from './messages';
export class Client {
readonly token: string;
@@ -14,11 +15,12 @@ export class Client {
readonly intents: Intent[] = [Intent.DEFAULT];
prefix: string;
// Internal services
protected _api: API;
protected _processor: Processor;
protected _gateway: Gateway;
protected api: API;
protected processor: Processor;
protected gateway: Gateway;
guilds: Guilds;
users: Users;
messages: Messages
constructor(config: ClientConfig) {
// Validate the configuration
@@ -29,23 +31,12 @@ export class Client {
this.intents = config.intents ?? this.intents;
this.prefix = config.prefix ?? '!';
// Initialize internal services
this._api = new API(this);
this._processor = new Processor(this);
this._gateway = new Gateway(this, this.processor, this.api);
this.api = new API(this);
this.processor = new Processor(this, this.api);
this.gateway = new Gateway(this, this.processor, this.api);
this.guilds = new Guilds(this.api, this.processor);
this.users = new Users(this.api, this.processor);
}
private get api() {
return this._api;
}
private get gateway() {
return this._gateway;
}
private get processor() {
return this._processor;
this.messages = new Messages(this, this.api, this.processor);
}
private static validateConfig(config: ClientConfig) {
@@ -79,13 +70,19 @@ export class Client {
message = new Message().setContent(message);
}
return Message.fromAPIResponse(await this.api.post(`/channels/${channel_id}/messages`, message.toJSON()));
return Message.hydrate(DiscordMessage.fromAPIResponse(await this.api.post(`/channels/${channel_id}/messages`, message.toJSON())), this, this.api);
}
chatCommands = {
register: (name: string | string[], handler: Handler) => this.processor.commands.register(name, handler),
unregister: (name: string | string[]) => this.processor.commands.unregister(name),
has: (name: string) => this.processor.commands.has(name)
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)
}
slashCommands = {
register: (name: string, handler: Handler) => this.processor.slash_commands.register(name, handler),
unregister: (name: string) => this.processor.slash_commands.unregister(name),
has: (name: string) => this.processor.slash_commands.has(name)
}
connect = () => this.gateway.connect();
+13 -11
View File
@@ -5,8 +5,8 @@ import { exists, isNil, isObject, log } from "../utils";
import type { API } from "./api";
import type { Processor } from './processor';
import { Context } from '../types/context';
import { Message } from '../types/message';
import { User } from '../types/user';
import { DiscordMessage, Message } from '../types/message';
import { DiscordUser, User } from '../types/user';
export class Gateway {
private socket: WebSocket | null = null;
@@ -22,7 +22,7 @@ export class Gateway {
log('Connected to gateway');
}
private onMessage = (packet: Data) => {
private onMessage = async (packet: Data) => {
// console.log(packet);
// Allocate a variable to store the parsed payload
let data: Payload | null = null;
@@ -44,7 +44,7 @@ export class Gateway {
const code = data.op;
if (code === 0) {
// Event received
this.parseEvent(data);
await this.parseEvent(data);
} else if (code === 1) {
// Heartbeat requested
this.send({ op: 1, d: this.sequence });
@@ -111,7 +111,7 @@ export class Gateway {
});
};
private parseEvent(payload: Payload) {
private async parseEvent(payload: Payload) {
if (isNil(payload.t)) {
throw new Error('Event name is required');
}
@@ -131,7 +131,7 @@ export class Gateway {
} else if (event_name === 'INTERACTION_CREATE') {
// this.handleInteractionCreate(payload);
} else if (event_name === 'MESSAGE_CREATE') {
this.handleMessageCreate(payload as Payload);
await this.handleMessageCreate(payload as Payload);
} else if (event_name === 'MESSAGE_UPDATE') {
// this.handleMessageCreate(payload);
}
@@ -141,7 +141,8 @@ 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.fromAPIResponse(event_data);
const discord_message = DiscordMessage.fromAPIResponse(event_data);
const message = await Message.hydrate(discord_message, this.client, this.api);
// And then generate the context
context = new Context(this.client, this.api, message);
}
@@ -164,7 +165,7 @@ export class Gateway {
return;
}
// Extract the author
const author = User.fromAPIResponse(payload.d.author);
const author = User.hydrate(DiscordUser.fromAPIResponse(payload.d.author));
// Store the author in the cache
this.client.users.set(author.id, author);
// Extract the content
@@ -185,14 +186,15 @@ 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 = Message.fromAPIResponse(payload.d);
const message = await Message.hydrate(DiscordMessage.fromAPIResponse(payload.d), this.client, this.api);
// Generate context
const context = new Context(this.client, this.api, message);
// Check if command exists
if (this.processor.commands.has(command)) {
if (this.processor.chat_commands.has(command)) {
// Execute the handler
this.processor.commands.execute(command, context, args);
this.processor.chat_commands.execute(command, context, args);
} else {
// TODO: Custom unknown command handler
context.reply(`Unknown command: ${command}`);
+2 -2
View File
@@ -1,10 +1,10 @@
import type { Guild } from "../types/guild";
import { Guild } from "../types/guild";
import type { API } from "./api";
import { ObjectCache } from "./cache";
import type { Processor } from "./processor";
export class Guilds extends ObjectCache<Guild> {
constructor (api: API, processor: Processor) {
super(api, processor, 'guilds', ['GUILD_CREATE', 'GUILD_UPDATE']);
super(api, processor, 'guilds', Guild.factory, ['GUILD_CREATE', 'GUILD_UPDATE']);
}
}
+26
View File
@@ -0,0 +1,26 @@
import { Message } 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<Message> {
constructor (protected client: Client, api: API, processor: Processor) {
super(api, processor, 'message', (data) => Message.factory(data, client, api), ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
}
async fetch(channel_id: string, message_id: string, resolveReference: boolean = true): Promise<Message> {
const req = this.api.get(`/channels/${channel_id}/messages/${message_id}`).then((data) => Message.factory(data, this.client, this.api, undefined, resolveReference));
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<Message> {
if (this.cache.has(message_id)) {
return await Promise.resolve(this.cache.get(message_id)!);
} else {
return this.fetch(channel_id, message_id, resolveReference);
}
}
}
+23 -14
View File
@@ -2,15 +2,17 @@ import type { Client } from "./client";
import type { Context } from "../types/context";
import { HandlerType, type EventPayload, type Payload } from "../types/common";
import { BaseHandler, type Handler } from "../types/hander";
import type { API } from "./api";
export class Processor {
private handlers = {
[HandlerType.Events]: new Map<string, Handler[]>(),
[HandlerType.Commands]: new Map<string, Handler[]>(),
[HandlerType.ChatCommands]: new Map<string, Handler[]>(),
[HandlerType.SlashCommands]: new Map<string, Handler[]>(),
[HandlerType.Interactions]: new Map<string, Handler[]>()
};
constructor(private client: Client) {};
constructor(private client: Client, private api: API) {};
public register(type: HandlerType, event: string | string[], handler: Handler) {
// Normalize events into an array
@@ -46,18 +48,18 @@ export class Processor {
}
};
public execute(type: HandlerType.Events, event: string, context: Context | null, payload: EventPayload): void
public execute(type: Omit<HandlerType, HandlerType.Events>, event: string, context: Context, args: any[]): void
public execute(type: HandlerType, event: string, context: Context | null, argsOrPayload: any[] | EventPayload) {
public async execute(type: HandlerType.Events, event: string, context: Context | null, payload: EventPayload): Promise<void>
public async execute(type: Omit<HandlerType, HandlerType.Events>, event: string, context: Context, args: any[]): Promise<void>
public async execute(type: HandlerType, event: string, context: Context | null, argsOrPayload: any[] | EventPayload) {
const handlers = this.handlers[type].get(event);
if (handlers) {
for (const handler of handlers) {
if (handler instanceof BaseHandler) {
handler.execute(context, argsOrPayload);
await Promise.resolve(handler.execute(context, argsOrPayload));
} else {
// TODO: Fix ts error
handler(context, argsOrPayload);
await Promise.resolve(handler(context, argsOrPayload))
}
}
}
@@ -66,21 +68,28 @@ export class Processor {
[HandlerType.Events] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Events, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.Events, event, handler),
execute: (event: string, context: Context | null, payload: EventPayload) => this.execute(HandlerType.Events, event, context, payload),
execute: async (event: string, context: Context | null, payload: EventPayload) => this.execute(HandlerType.Events, event, context, payload),
has: (event: string) => this.handlers[HandlerType.Events].has(event) && this.handlers[HandlerType.Events].get(event)?.length! > 0
};
[HandlerType.Commands] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Commands, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.Commands, event, handler),
execute: (event: string, context: Context, args: any[]) => this.execute(HandlerType.Commands, event, context, args),
has: (event: string) => this.handlers[HandlerType.Commands].has(event) ?? this.handlers[HandlerType.Commands].get(event)?.length! > 0
[HandlerType.ChatCommands] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.ChatCommands, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.ChatCommands, event, handler),
execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.ChatCommands, event, context, args),
has: (event: string) => this.handlers[HandlerType.ChatCommands].has(event) ?? this.handlers[HandlerType.ChatCommands].get(event)?.length! > 0
};
[HandlerType.SlashCommands] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.SlashCommands, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.SlashCommands, event, handler),
execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.SlashCommands, event, context, args),
has: (event: string) => this.handlers[HandlerType.SlashCommands].has(event) ?? this.handlers[HandlerType.SlashCommands].get(event)?.length! > 0
};
[HandlerType.Interactions] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Interactions, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.Interactions, event, handler),
execute: (event: string, context: Context, args: any[]) => this.execute(HandlerType.Interactions, event, context, args),
execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.Interactions, event, context, args),
has: (event: string) => this.handlers[HandlerType.Interactions].has(event) ?? this.handlers[HandlerType.Interactions].get(event)?.length! > 0
};
}