mirror of
https://github.com/jaronpate/unicord.git
synced 2026-10-18 21:47:40 +00:00
added event bus. working on a gateway object base class.
This commit is contained in:
+3
-3
@@ -13,9 +13,6 @@ export class API {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
console.log(`API Request: ${method} ${url}`);
|
||||
console.log(`API Body: ${JSON.stringify(data, null, 4)}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
@@ -30,6 +27,9 @@ export class API {
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
|
||||
// console.log(`API Request: ${method} ${url}`);
|
||||
// console.log(`API Body: ${JSON.stringify(data, null, 4)}`);
|
||||
// console.log('API Response:', JSON.stringify(body, null, 4));
|
||||
|
||||
return body;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { Context } from '../types/context';
|
||||
|
||||
const EventBus = new EventEmitter() as Emitter;
|
||||
|
||||
export type Emitter = EventEmitter & {
|
||||
emit: (event_name: string, context: Context | null, ...args: any[]) => boolean
|
||||
}
|
||||
|
||||
export { EventBus };
|
||||
+12
-4
@@ -8,7 +8,7 @@ import { Guilds } from './caches/guilds';
|
||||
import { Messages } from './caches/messages';
|
||||
import { Users } from './caches/users';
|
||||
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
|
||||
import type { User } from '../types/user';
|
||||
import { EventBus, type Emitter } from './bus';
|
||||
|
||||
export class Client {
|
||||
readonly token: string;
|
||||
@@ -16,6 +16,7 @@ export class Client {
|
||||
readonly intents: Intent[] = [Intent.DEFAULT];
|
||||
prefix: string;
|
||||
// Internal services
|
||||
protected bus: Emitter;
|
||||
protected api: API;
|
||||
protected processor: Processor;
|
||||
protected gateway: Gateway;
|
||||
@@ -34,9 +35,10 @@ export class Client {
|
||||
this.intents = config.intents ?? this.intents;
|
||||
this.prefix = config.prefix ?? '!';
|
||||
// Initialize internal services
|
||||
this.bus = EventBus;
|
||||
this.api = new API(this);
|
||||
this.processor = new Processor(this, this.api);
|
||||
this.gateway = new Gateway(this, this.processor, this.api);
|
||||
this.processor = new Processor(this, this.api, this.bus);
|
||||
this.gateway = new Gateway(this, this.processor, this.api, this.bus);
|
||||
this.guilds = new Guilds(this.api, this.processor);
|
||||
this.users = new Users(this.api, this.processor);
|
||||
this.messages = new Messages(this, this.api, this.processor);
|
||||
@@ -112,7 +114,7 @@ export class Client {
|
||||
message = new Message().setContent(message);
|
||||
}
|
||||
|
||||
return Message[fromDiscord](await this.api.post<DiscordMessage>(`/channels/${channel_id}/messages`, message.toJSON()));
|
||||
return Message.fromDiscord(await this.api.post<DiscordMessage>(`/channels/${channel_id}/messages`, message.toJSON()));
|
||||
}
|
||||
|
||||
// chatCommands = {
|
||||
@@ -127,5 +129,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);
|
||||
|
||||
once = (event: string, handler: (...args: any[]) => void) => this.bus.once(event, handler);
|
||||
|
||||
off = (event: string, handler: (...args: any[]) => void) => this.bus.off(event, handler);
|
||||
|
||||
connect = () => this.gateway.connect();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WebSocket, type Data } from 'ws';
|
||||
import type { Client } from "../client";
|
||||
import { fromDiscord, type InteractionPayload, type Payload } from "../../types/common";
|
||||
import { fromDiscord, HandlerType, type InteractionPayload, type Payload } from "../../types/common";
|
||||
import { exists, isNil, isObject, log } from "../../utils";
|
||||
import type { API } from "../api";
|
||||
import type { Processor } from '../processor';
|
||||
@@ -9,6 +9,7 @@ import { type DiscordMessage, Message } from '../../types/message';
|
||||
import { DiscordUser, User } from '../../types/user';
|
||||
import { Guild } from '../../types/guild';
|
||||
import { Channel } from '../../types/channel';
|
||||
import type { Emitter } from '../bus';
|
||||
|
||||
export class Gateway {
|
||||
private socket: WebSocket | null = null;
|
||||
@@ -18,14 +19,13 @@ export class Gateway {
|
||||
// Internal state
|
||||
user: User | null = null;
|
||||
|
||||
constructor(private client: Client, private processor: Processor, private api: API) {}
|
||||
constructor(private client: Client, private processor: Processor, private api: API, private bus: Emitter) {}
|
||||
|
||||
private onOpen = () => {
|
||||
log('Connected to gateway');
|
||||
}
|
||||
|
||||
private onMessage = async (packet: Data) => {
|
||||
// console.log(packet);
|
||||
// Allocate a variable to store the parsed payload
|
||||
let data: Payload | null = null;
|
||||
|
||||
@@ -36,12 +36,13 @@ export class Gateway {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
log('='.repeat(25));
|
||||
log('received: ', data.op);
|
||||
log('received event: ', data.t ?? 'no event');
|
||||
log('received data: ', exists(data.d) ? Object.keys(data.d) : 'no data');
|
||||
log('='.repeat(25));
|
||||
if (exists(data)) {
|
||||
// NOTE: Uncomment for debugging recieved event data
|
||||
// log('='.repeat(25));
|
||||
// log('received: ', data.op);
|
||||
// log('received event: ', data.t ?? 'no event');
|
||||
// log('received data: ', exists(data.d) ? Object.keys(data.d) : 'no data');
|
||||
// log('='.repeat(25));
|
||||
// If the data is successfully parsed check the opcode
|
||||
const code = data.op;
|
||||
if (code === 0) {
|
||||
@@ -61,6 +62,9 @@ export class Gateway {
|
||||
this.heartbeat_interval = data.d?.heartbeat_interval;
|
||||
// Kick off the heartbeat loop
|
||||
this.heartbeat();
|
||||
// Identify with the gateway
|
||||
// TODO: Is there a way identify without the timeout? Or waiting for the heartbeat?
|
||||
// We need to wait a bit before sending the identify payload so that the heartbeat can be established
|
||||
setTimeout(this.indentify, 2500);
|
||||
} else if (code === 11) {
|
||||
// Heartbeat ACK
|
||||
@@ -70,7 +74,6 @@ export class Gateway {
|
||||
}
|
||||
|
||||
private heartbeat() {
|
||||
log('Sending heartbeat');
|
||||
if (exists(this.heartbeat_interval)) {
|
||||
// If a previous heartbeat timeout exists, clear it
|
||||
if (exists(this.heartbeat_timeout)) {
|
||||
@@ -91,9 +94,9 @@ export class Gateway {
|
||||
throw new Error('Socket is not connected');
|
||||
}
|
||||
|
||||
log('='.repeat(25));
|
||||
log('sending: ', JSON.stringify(payload, null, 4));
|
||||
log('='.repeat(25));
|
||||
// log('='.repeat(25));
|
||||
// log('sending: ', JSON.stringify(payload, null, 4));
|
||||
// log('='.repeat(25));
|
||||
|
||||
this.socket.send(JSON.stringify(payload));
|
||||
};
|
||||
@@ -118,15 +121,18 @@ export class Gateway {
|
||||
throw new Error('Event name is required');
|
||||
}
|
||||
|
||||
log('Event:', payload.t);
|
||||
// log('Event:', payload.t);
|
||||
|
||||
// Extract the event name and data
|
||||
const event_name = payload.t;
|
||||
const event_data = payload.d;
|
||||
|
||||
// Simple validation on the event data
|
||||
if (!isObject(event_data)) {
|
||||
throw new Error('Invalid event data received');
|
||||
}
|
||||
|
||||
// Check if this is a known event
|
||||
if (event_name === 'READY') {
|
||||
this.user = User[fromDiscord](DiscordUser.fromAPIResponse(event_data.user));
|
||||
} else if (event_name === 'INTERACTION_CREATE') {
|
||||
@@ -140,6 +146,7 @@ export class Gateway {
|
||||
// Allocate a var to store the context
|
||||
let context = null;
|
||||
|
||||
// If the event is a message event, create a context from the message
|
||||
if (['MESSAGE_CREATE', 'MESSAGE_UPDATE'].includes(event_name)) {
|
||||
// If the event is a message event, create a message object
|
||||
const message = Message[fromDiscord](event_data as DiscordMessage);
|
||||
@@ -148,7 +155,8 @@ export class Gateway {
|
||||
}
|
||||
|
||||
// Call event handlers
|
||||
this.processor.events.execute(event_name, context, event_data);
|
||||
this.bus.emit(event_name, context, event_data);
|
||||
// this.processor.events.execute(event_name, context, event_data);
|
||||
}
|
||||
|
||||
private handleMessageCreate = async (payload: Payload) => {
|
||||
@@ -169,7 +177,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
|
||||
@@ -191,13 +199,15 @@ export class Gateway {
|
||||
// TODO: Case insensitive config?
|
||||
const command = args.shift()!;
|
||||
// Create a new message object from the payload
|
||||
const message = Message[fromDiscord](payload.d as DiscordMessage);
|
||||
const message = Message.fromDiscord(payload.d as DiscordMessage);
|
||||
// Store the message in the cache
|
||||
this.client.messages.set(message.id, message);
|
||||
// Generate context
|
||||
const context = new Context(this.client, this.api, { message });
|
||||
// Check if command exists
|
||||
if (this.processor.chat_commands.has(command)) {
|
||||
// // Check if command exists
|
||||
if (this.processor[HandlerType.ChatCommands].has(command)) {
|
||||
// Execute the handler
|
||||
this.processor.chat_commands.execute(command, context, args);
|
||||
this.processor[HandlerType.ChatCommands].execute(command, context, args);
|
||||
} else {
|
||||
// TODO: Custom unknown command handler
|
||||
context.reply(`Unknown command: ${command}`);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { HandlerType, Trait, type EventPayload } from "../types/common";
|
||||
import { CommandHandler, type Handler, type ArgsFromOptions, OptionTypeMap } from "../types/handler";
|
||||
import type { API } from "./api";
|
||||
import { ApplicationCommandType, type ApplicationCommandOption, type ApplicationCommandOptionResult } from "../types/applicationCommand";
|
||||
import type { Emitter } from "./bus";
|
||||
|
||||
export class Processor {
|
||||
private handlers = {
|
||||
@@ -13,7 +14,7 @@ export class Processor {
|
||||
[HandlerType.Interactions]: new Map<string, Handler[]>()
|
||||
};
|
||||
|
||||
constructor(private client: Client, private api: API) {};
|
||||
constructor(private client: Client, private api: API, private bus: Emitter) {};
|
||||
|
||||
public register(type: HandlerType, event: string | string[], handler: Handler) {
|
||||
// Normalize events into an array
|
||||
@@ -76,6 +77,7 @@ export class Processor {
|
||||
const zipped: [ApplicationCommandOption, ApplicationCommandOptionResult][] = args.map((arg, i) => [definition[i], arg]);
|
||||
// Validate the args
|
||||
const validated: Record<string, any> = {};
|
||||
// TODO: Attempt some type coresion/resolution here (mostly for text commands)
|
||||
for (const [def, arg] of zipped) {
|
||||
if (def.choices) {
|
||||
// If the definition has choices, validate the arg against them
|
||||
@@ -125,7 +127,10 @@ export class Processor {
|
||||
|
||||
// Ensure the command name is valid
|
||||
// @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming
|
||||
if (!names.every(/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u.test.bind(/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u))) {
|
||||
// TODO: can this sus bind be removed? Is the only way to kill the arrow function?
|
||||
const commandNameRegex = /^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u;
|
||||
const commandNameTest = commandNameRegex.test.bind(commandNameRegex)
|
||||
if (!names.every(commandNameTest)) {
|
||||
throw new Error('Invalid application command name');
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user