mirror of
https://github.com/jaronpate/unicord.git
synced 2026-10-18 21:47:40 +00:00
feat: Refactor client and API structure to use config object cache getters
This commit is contained in:
@@ -0,0 +1 @@
|
||||
src/services/processor.ts
|
||||
+7
-8
@@ -1,5 +1,5 @@
|
||||
import { exists, log } from "../utils";
|
||||
import type { Client } from "./client";
|
||||
import { exists, log } from '../utils';
|
||||
import type { Client } from './client';
|
||||
|
||||
export class API {
|
||||
private readonly base_url: string = 'https://discord.com/api/v10';
|
||||
@@ -9,9 +9,9 @@ export class API {
|
||||
async request<T = any>(method: string, path: string, data?: any): Promise<T> {
|
||||
const url = `${this.base_url}${path.startsWith('/') ? '' : '/'}${path}`;
|
||||
const headers = {
|
||||
Authorization: `Bot ${this.client.token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
Authorization: `Bot ${this.client.config.token}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
@@ -28,7 +28,6 @@ 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));
|
||||
@@ -49,7 +48,7 @@ export class API {
|
||||
const retryAfter = response.headers.get('Retry-After');
|
||||
if (exists(retryAfter)) {
|
||||
// Wait for the retry-after time
|
||||
await new Promise(resolve => setTimeout(resolve, parseInt(retryAfter) * 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve, parseInt(retryAfter) * 1000));
|
||||
} else {
|
||||
// TODO: Handle this case better
|
||||
// Maybe wait some arbitrary time??
|
||||
@@ -75,4 +74,4 @@ export class API {
|
||||
async delete<T = any>(path: string): Promise<T> {
|
||||
return this.request('DELETE', path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-23
@@ -12,44 +12,42 @@ import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
|
||||
import { Processor } from './processor';
|
||||
|
||||
export class Client {
|
||||
readonly token: string;
|
||||
readonly application_id: string;
|
||||
readonly intents: Intent[] = [Intent.DEFAULT];
|
||||
prefix: string;
|
||||
readonly config: Readonly<Required<ClientConfig>>;
|
||||
// Internal services
|
||||
protected bus: Emitter;
|
||||
protected api: API;
|
||||
protected processor: Processor;
|
||||
protected gateway: Gateway;
|
||||
guilds: Guilds;
|
||||
users: Users;
|
||||
messages: Messages;
|
||||
channels: Channels;
|
||||
chatCommands: Processor[HandlerType.ChatCommands];
|
||||
applicationCommands: Processor[HandlerType.ApplicationCommands];
|
||||
interactions: Processor[HandlerType.Interactions];
|
||||
// Caches
|
||||
caches: {
|
||||
guilds: Guilds;
|
||||
users: Users;
|
||||
messages: Messages;
|
||||
channels: Channels;
|
||||
};
|
||||
|
||||
constructor(config: ClientConfig) {
|
||||
// Validate the configuration
|
||||
Client.validateConfig(config);
|
||||
// Save the configuration
|
||||
this.token = config.token;
|
||||
this.application_id = config.application_id;
|
||||
this.intents = config.intents ?? this.intents;
|
||||
this.prefix = config.prefix ?? '!';
|
||||
this.config = Object.freeze({
|
||||
token: config.token,
|
||||
application_id: config.application_id,
|
||||
intents: config.intents ?? [Intent.DEFAULT],
|
||||
prefix: config.prefix ?? '!',
|
||||
});
|
||||
// Initialize internal services
|
||||
this.bus = EventBus;
|
||||
this.api = new API(this);
|
||||
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.api, this.processor);
|
||||
this.channels = new Channels(this.api, this.processor);
|
||||
// Save references to command and event handlers
|
||||
this.chatCommands = this.processor[HandlerType.ChatCommands];
|
||||
this.applicationCommands = this.processor[HandlerType.ApplicationCommands];
|
||||
this.interactions = this.processor[HandlerType.Interactions];
|
||||
// Initialize caches
|
||||
this.caches = {
|
||||
guilds: new Guilds(this.api, this.processor),
|
||||
users: new Users(this.api, this.processor),
|
||||
messages: new Messages(this.api, this.processor),
|
||||
channels: new Channels(this.api, this.processor),
|
||||
};
|
||||
}
|
||||
|
||||
private static validateConfig(config: ClientConfig) {
|
||||
@@ -82,6 +80,22 @@ export class Client {
|
||||
return this.gateway.user;
|
||||
}
|
||||
|
||||
get chatCommands() {
|
||||
return this.processor[HandlerType.ChatCommands];
|
||||
}
|
||||
|
||||
get applicationCommands() {
|
||||
return this.processor[HandlerType.ApplicationCommands];
|
||||
}
|
||||
|
||||
get interactions() {
|
||||
return this.processor[HandlerType.Interactions];
|
||||
}
|
||||
|
||||
get events() {
|
||||
return this.processor[HandlerType.Events];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrates a given data object based on the provided expectations.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { WebSocket, type Data } from 'ws';
|
||||
import type { Client } from "../client";
|
||||
import { fromDiscord, HandlerType, type Payload } from "../../types/common";
|
||||
import { exists, isNil, isObject, log } from "../../utils";
|
||||
import type { API } from "../api";
|
||||
import type { Client } from '../client';
|
||||
import { fromDiscord, HandlerType, 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';
|
||||
@@ -23,7 +23,7 @@ export class Gateway {
|
||||
|
||||
private onOpen = () => {
|
||||
log('Connected to gateway');
|
||||
}
|
||||
};
|
||||
|
||||
private onMessage = async (packet: Data) => {
|
||||
// Allocate a variable to store the parsed payload
|
||||
@@ -71,7 +71,7 @@ export class Gateway {
|
||||
log('Heartbeat ACK');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private heartbeat() {
|
||||
if (exists(this.heartbeat_interval)) {
|
||||
@@ -105,14 +105,14 @@ export class Gateway {
|
||||
this.send({
|
||||
op: 2,
|
||||
d: {
|
||||
token: this.client.token,
|
||||
intents: this.client.intents.reduce((acc, intent) => acc | intent, 0),
|
||||
token: this.client.config.token,
|
||||
intents: this.client.config.intents.reduce((acc, intent) => acc | intent, 0),
|
||||
properties: {
|
||||
os: process.platform,
|
||||
browser: 'unicord',
|
||||
device: 'unicord'
|
||||
}
|
||||
}
|
||||
device: 'unicord',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -179,13 +179,15 @@ export class Gateway {
|
||||
// Extract the author
|
||||
const author = User.fromDiscord(DiscordUser.fromAPIResponse(payload.d.author));
|
||||
// Store the author in the cache
|
||||
this.client.users.set(author.id, author);
|
||||
this.client.caches.users.set(author.id, author);
|
||||
// Extract the content
|
||||
const content = payload.d.content.trim();
|
||||
// Is this a command?
|
||||
if (content.startsWith(this.client.prefix) || content.startsWith(`<@${this.user.id}>`)) {
|
||||
if (content.startsWith(this.client.config.prefix) || content.startsWith(`<@${this.user.id}>`)) {
|
||||
// Determine the prefix
|
||||
const prefix = content.startsWith(this.client.prefix) ? this.client.prefix : `<@${this.user.id}> `;
|
||||
const prefix = content.startsWith(this.client.config.prefix)
|
||||
? this.client.config.prefix
|
||||
: `<@${this.user.id}> `;
|
||||
// Parse command
|
||||
const args = content
|
||||
.slice(prefix.length)
|
||||
@@ -201,7 +203,7 @@ export class Gateway {
|
||||
// Create a new message object from the payload
|
||||
const message = Message.fromDiscord(payload.d as DiscordMessage);
|
||||
// Store the message in the cache
|
||||
this.client.messages.set(message.id, message);
|
||||
this.client.caches.messages.set(message.id, message);
|
||||
// Generate context
|
||||
const context = new Context(this.client, this.api, { message });
|
||||
// Dispatch event for the command
|
||||
@@ -231,15 +233,21 @@ export class Gateway {
|
||||
}
|
||||
|
||||
if (payload.d.type === 2) {
|
||||
this.handleShashInteraction(payload as Payload & { d: InteractionPayload & { data: InteractionCommandData } });
|
||||
this.handleShashInteraction(
|
||||
payload as Payload & { d: InteractionPayload & { data: InteractionCommandData } },
|
||||
);
|
||||
} else if (payload.d.type === 3) {
|
||||
this.handleChatInteraction(payload as Payload & { d: InteractionPayload & { data: InteractionCommpoentData } });
|
||||
this.handleChatInteraction(
|
||||
payload as Payload & { d: InteractionPayload & { data: InteractionCommpoentData } },
|
||||
);
|
||||
} else {
|
||||
// this.handleChatInteraction(payload);
|
||||
}
|
||||
};
|
||||
|
||||
private handleShashInteraction = (payload: Payload & { d: InteractionPayload & { data: InteractionCommandData } }) => {
|
||||
private handleShashInteraction = (
|
||||
payload: Payload & { d: InteractionPayload & { data: InteractionCommandData } },
|
||||
) => {
|
||||
if (isNil(payload.d.data)) {
|
||||
throw new Error('Invalid INTERACTION_CREATE data received');
|
||||
}
|
||||
@@ -251,17 +259,17 @@ export class Gateway {
|
||||
|
||||
if (payload.d.guild) {
|
||||
const guild = Guild.fromDiscord(payload.d.guild);
|
||||
this.client.guilds.set(guild.id, guild);
|
||||
this.client.caches.guilds.set(guild.id, guild);
|
||||
}
|
||||
|
||||
if (payload.d.member) {
|
||||
const member = User.fromDiscord(payload.d.member?.user);
|
||||
this.client.users.set(member.id, member);
|
||||
this.client.caches.users.set(member.id, member);
|
||||
}
|
||||
|
||||
if (payload.d.user) {
|
||||
const user = User.fromDiscord(payload.d.user);
|
||||
this.client.users.set(user.id, user);
|
||||
this.client.caches.users.set(user.id, user);
|
||||
}
|
||||
|
||||
// TODO: Fix when channel class is implemented
|
||||
@@ -271,14 +279,20 @@ export class Gateway {
|
||||
// }
|
||||
|
||||
if (this.processor.application_commands.has(command)) {
|
||||
this.processor.application_commands.execute(command, context, args?.map(arg => arg.value));
|
||||
this.processor.application_commands.execute(
|
||||
command,
|
||||
context,
|
||||
args?.map((arg) => arg.value),
|
||||
);
|
||||
} else {
|
||||
// TODO: Custom unknown command handler
|
||||
context.reply(`Unknown command: ${command}`);
|
||||
}
|
||||
};
|
||||
|
||||
private handleChatInteraction = (payload: Payload & { d: InteractionPayload & { data: InteractionCommpoentData } }) => {
|
||||
private handleChatInteraction = (
|
||||
payload: Payload & { d: InteractionPayload & { data: InteractionCommpoentData } },
|
||||
) => {
|
||||
const context = new Context(this.client, this.api, { interaction: payload.d });
|
||||
|
||||
if (exists(payload.d.data)) {
|
||||
@@ -305,4 +319,4 @@ export class Gateway {
|
||||
this.socket.on('open', this.onOpen);
|
||||
this.socket.on('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,13 +41,13 @@ export async function hydrate<T extends Hydrateable, K extends Array<Expectation
|
||||
for (const expectation of expectations) {
|
||||
if (expectation === Expectation.Message && exists(data.message_id)) {
|
||||
let context = data as unknown as HydratedContext<[Expectation.Message, ...Expectation[]]>;
|
||||
context.message = await client.messages.get(data.channel_id, data.message_id);
|
||||
context.message = await client.caches.messages.get(data.channel_id, data.message_id);
|
||||
} else 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);
|
||||
context.guild = await client.caches.guilds.get(data.guild_id);
|
||||
} else if (expectation === Expectation.Channel && exists(data.channel_id)) {
|
||||
let context = data as unknown as HydratedContext<[Expectation.Channel, ...Expectation[]]>;
|
||||
context.channel = await client.channels.get(data.channel_id);
|
||||
context.channel = await client.caches.channels.get(data.channel_id);
|
||||
} else {
|
||||
throw new Error(`Invalid expectation requested for Context: ${expectation}`);
|
||||
}
|
||||
@@ -56,13 +56,13 @@ export async function hydrate<T extends Hydrateable, K extends Array<Expectation
|
||||
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);
|
||||
message.reference = await client.caches.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);
|
||||
message.guild = await client.caches.guilds.get(data.guild_id);
|
||||
} else if (expectation === Expectation.Channel && exists(data.channel_id)) {
|
||||
let message = data as unknown as HydratedMessage<[Expectation.Channel, ...Expectation[]]>;
|
||||
message.channel = await client.channels.get(data.channel_id);
|
||||
message.channel = await client.caches.channels.get(data.channel_id);
|
||||
} else {
|
||||
throw new Error(`Invalid expectation requested for Message: ${expectation}`);
|
||||
}
|
||||
|
||||
+15
-15
@@ -1,10 +1,10 @@
|
||||
import type { Client } from "./client";
|
||||
import type { Context } from "../types/context";
|
||||
import { HandlerType, Trait, type EventPayload } from "../types/common";
|
||||
import { CommandHandler, type Handler, type ArgsFromOptions, OptionConstructorMap, type InteractionData } from "../types/handler";
|
||||
import type { API } from "./api";
|
||||
import { ApplicationCommandOptionType, ApplicationCommandType, type ApplicationCommandOption } from "../types/applicationCommand";
|
||||
import { HandlerType, Trait, type EventPayload } from "../types/common";
|
||||
import type { Context } from "../types/context";
|
||||
import { CommandHandler, OptionConstructorMap, type ArgsFromOptions, type Handler, type InteractionData } from "../types/handler";
|
||||
import type { API } from "./api";
|
||||
import type { Emitter } from "./bus";
|
||||
import type { Client } from "./client";
|
||||
|
||||
export class Processor {
|
||||
private handlers = {
|
||||
@@ -14,13 +14,13 @@ export class Processor {
|
||||
[HandlerType.Interactions]: new Map<string, Handler[]>()
|
||||
};
|
||||
|
||||
constructor(private client: Client, private api: API, private bus: Emitter) {};
|
||||
constructor(private client: Client, private api: API, private bus: Emitter) { };
|
||||
|
||||
public register(type: HandlerType, event: string | string[], handler: Handler) {
|
||||
// Normalize events into an array
|
||||
const events = Array.isArray(event) ? event : [event];
|
||||
// Register the handler for each event
|
||||
for (const e of events) {
|
||||
for (const e of events) {
|
||||
const handlers = this.handlers[type].get(e) || [];
|
||||
handlers.push(handler);
|
||||
this.handlers[type].set(e, handlers);
|
||||
@@ -78,7 +78,7 @@ export class Processor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private validateAndResolveArgs = async <T extends readonly ApplicationCommandOption[]>(args: any[], definition: T): Promise<ArgsFromOptions<T>> => {
|
||||
// Zip the args and definition together
|
||||
const zipped: [ApplicationCommandOption, any][] = args.map((arg, i) => [definition[i], arg]);
|
||||
@@ -102,11 +102,11 @@ export class Processor {
|
||||
if (!userId) {
|
||||
throw new Error(`Invalid user for ${def.name}: expected a user mention or snowflake id, got ${arg}`);
|
||||
}
|
||||
validated[def.name] = await this.client.users.get(userId);
|
||||
validated[def.name] = await this.client.caches.users.get(userId);
|
||||
} else if (typeof arg === 'object') {
|
||||
// Check if the object has an ID
|
||||
if (arg.id) {
|
||||
validated[def.name] = await this.client.users.get(arg.id);
|
||||
validated[def.name] = await this.client.caches.users.get(arg.id);
|
||||
} else {
|
||||
throw new Error(`Invalid user for ${def.name}: expected a user object with an ID, got ${arg}`);
|
||||
}
|
||||
@@ -138,7 +138,7 @@ export class Processor {
|
||||
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
|
||||
has: (event: string) => this.handlers[HandlerType.ChatCommands].has(event) && this.handlers[HandlerType.ChatCommands].get(event)?.length! > 0
|
||||
};
|
||||
|
||||
[HandlerType.ApplicationCommands] = {
|
||||
@@ -154,7 +154,7 @@ export class Processor {
|
||||
|
||||
// Ensure the command name is valid
|
||||
// @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming
|
||||
// TODO: can this sus bind be removed? Is the only way to kill the arrow function?
|
||||
// TODO: can this sus bind be removed? Is the only way to convert from arrow to regular 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)) {
|
||||
@@ -166,21 +166,21 @@ export class Processor {
|
||||
const command: Record<string, any> = handler.toJSON();
|
||||
command.name = n;
|
||||
command.type = type;
|
||||
this.api.post(`/applications/${this.client.application_id}/commands`, command);
|
||||
this.api.post(`/applications/${this.client.config.application_id}/commands`, command);
|
||||
}
|
||||
|
||||
this.register(HandlerType.ApplicationCommands, names, handler)
|
||||
},
|
||||
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.ApplicationCommands, event, handler),
|
||||
execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.ApplicationCommands, event, context, args),
|
||||
has: (event: string) => this.handlers[HandlerType.ApplicationCommands].has(event) ?? this.handlers[HandlerType.ApplicationCommands].get(event)?.length! > 0,
|
||||
has: (event: string) => this.handlers[HandlerType.ApplicationCommands].has(event) && this.handlers[HandlerType.ApplicationCommands].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: async (event: string, context: Context, args: InteractionData) => this.execute(HandlerType.Interactions, event, context, args),
|
||||
has: (event: string) => this.handlers[HandlerType.Interactions].has(event) ?? this.handlers[HandlerType.Interactions].get(event)?.length! > 0
|
||||
has: (event: string) => this.handlers[HandlerType.Interactions].has(event) && this.handlers[HandlerType.Interactions].get(event)?.length! > 0
|
||||
};
|
||||
|
||||
static isCommandHandler = (handler: Handler): handler is CommandHandler<ApplicationCommandOption[]> => {
|
||||
|
||||
Reference in New Issue
Block a user