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 -1
View File
@@ -3,7 +3,8 @@
"module": "src/index.ts", "module": "src/index.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "dotenvx run -f .env -- bun run testing/demo.test.ts" "test": "dotenvx run -f .env -- bun run testing/demo.test.ts",
"test:inspect": "dotenvx run -f .env -- bun --inspect testing/demo.test.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest" "@types/bun": "latest"
+11 -1
View File
@@ -1,5 +1,15 @@
// Base classes
export { Client } from './services/client'; export { Client } from './services/client';
export { Intent } from './types/common'; export { Intent } from './types/common';
export { BaseHandler, type Handler } from './types/hander'; export { BaseHandler, type Handler } from './types/hander';
// Consumable types
export type { ClientConfig } from './types/common'; export type { ClientConfig } from './types/common';
export type { Context } from './types/context'; export type { Context } from './types/context';
// 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 { Embed } from './types/embed';
export { Emoji } from './types/emoji';
+2 -2
View File
@@ -6,7 +6,7 @@ export class API {
constructor(private client: Client) {} constructor(private client: Client) {}
async request<T = any>(method: string, path: string, data?: any): Promise<T> { 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 = { const headers = {
Authorization: `Bot ${this.client.token}`, Authorization: `Bot ${this.client.token}`,
'Accept': 'application/json', 'Accept': 'application/json',
@@ -29,7 +29,7 @@ export class API {
const body = await response.json(); 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; return body;
} }
+13 -15
View File
@@ -3,15 +3,14 @@ import { exists } from "../utils";
import type { API } from "./api"; import type { API } from "./api";
import type { Processor } from "./processor"; import type { Processor } from "./processor";
export class ObjectCache<T> { export class ObjectCache<T> {
cache = new Map<string, T>(); cache = new Map<string, Promise<T> | T>();
private readonly type: string; 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; this.type = type;
processor.events.register(events, (_context: Context, e: Record<string, any>) => { processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
this.cache.set(e.id, e as T); this.cache.set(e.id, factory(e));
}); });
} }
@@ -30,23 +29,22 @@ export class ObjectCache<T> {
// } // }
// } // }
async fetch(object_id: string): Promise<T> { async fetch(object_id: string, ..._args: any[]): Promise<T> {
return this.api.get(`${this.type}/${object_id}`).then((res) => { const req = this.api.get(`/${this.type}/${object_id}`).then((data) => this.factory(data));
const object: T & { id: string } = res.data; this.cache.set(object_id, req);
this.cache.set(object.id, object); req.catch(() => this.cache.delete(object_id));
return object; 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)) { if (this.cache.has(object_id)) {
return Promise.resolve(this.cache.get(object_id)!); return await Promise.resolve(this.cache.get(object_id)!);
} else { } else {
return this.fetch(object_id); return this.fetch(object_id);
} }
} }
list(): Map<string, T> { list(): Map<string, Promise<T> | T> {
return this.cache; return this.cache;
} }
} }
+20 -23
View File
@@ -4,9 +4,10 @@ import { Processor } from './processor';
import { Intent, type ClientConfig } from '../types/common'; import { Intent, type ClientConfig } from '../types/common';
import { exists, isNil } from '../utils/index'; import { exists, isNil } from '../utils/index';
import type { Handler } from '../types/hander'; import type { Handler } from '../types/hander';
import { Message } from '../types/message'; import { DiscordMessage, Message } from '../types/message';
import { Guilds } from './guilds'; import { Guilds } from './guilds';
import { Users } from './users'; import { Users } from './users';
import { Messages } from './messages';
export class Client { export class Client {
readonly token: string; readonly token: string;
@@ -14,11 +15,12 @@ export class Client {
readonly intents: Intent[] = [Intent.DEFAULT]; readonly intents: Intent[] = [Intent.DEFAULT];
prefix: string; prefix: string;
// Internal services // Internal services
protected _api: API; protected api: API;
protected _processor: Processor; protected processor: Processor;
protected _gateway: Gateway; protected gateway: Gateway;
guilds: Guilds; guilds: Guilds;
users: Users; users: Users;
messages: Messages
constructor(config: ClientConfig) { constructor(config: ClientConfig) {
// Validate the configuration // Validate the configuration
@@ -29,23 +31,12 @@ export class Client {
this.intents = config.intents ?? this.intents; this.intents = config.intents ?? this.intents;
this.prefix = config.prefix ?? '!'; this.prefix = config.prefix ?? '!';
// Initialize internal services // Initialize internal services
this._api = new API(this); this.api = new API(this);
this._processor = new Processor(this); this.processor = new Processor(this, this.api);
this._gateway = new Gateway(this, this.processor, this.api); this.gateway = new Gateway(this, this.processor, this.api);
this.guilds = new Guilds(this.api, this.processor); this.guilds = new Guilds(this.api, this.processor);
this.users = new Users(this.api, this.processor); this.users = new Users(this.api, this.processor);
} this.messages = new Messages(this, this.api, this.processor);
private get api() {
return this._api;
}
private get gateway() {
return this._gateway;
}
private get processor() {
return this._processor;
} }
private static validateConfig(config: ClientConfig) { private static validateConfig(config: ClientConfig) {
@@ -79,13 +70,19 @@ export class Client {
message = new Message().setContent(message); 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 = { chatCommands = {
register: (name: string | string[], handler: Handler) => this.processor.commands.register(name, handler), register: (name: string | string[], handler: Handler) => this.processor.chat_commands.register(name, handler),
unregister: (name: string | string[]) => this.processor.commands.unregister(name), unregister: (name: string | string[]) => this.processor.chat_commands.unregister(name),
has: (name: string) => this.processor.commands.has(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(); 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 { API } from "./api";
import type { Processor } from './processor'; import type { Processor } from './processor';
import { Context } from '../types/context'; import { Context } from '../types/context';
import { Message } from '../types/message'; import { DiscordMessage, Message } from '../types/message';
import { User } from '../types/user'; import { DiscordUser, User } from '../types/user';
export class Gateway { export class Gateway {
private socket: WebSocket | null = null; private socket: WebSocket | null = null;
@@ -22,7 +22,7 @@ export class Gateway {
log('Connected to gateway'); log('Connected to gateway');
} }
private onMessage = (packet: Data) => { private onMessage = async (packet: Data) => {
// console.log(packet); // console.log(packet);
// Allocate a variable to store the parsed payload // Allocate a variable to store the parsed payload
let data: Payload | null = null; let data: Payload | null = null;
@@ -44,7 +44,7 @@ export class Gateway {
const code = data.op; const code = data.op;
if (code === 0) { if (code === 0) {
// Event received // Event received
this.parseEvent(data); await this.parseEvent(data);
} else if (code === 1) { } else if (code === 1) {
// Heartbeat requested // Heartbeat requested
this.send({ op: 1, d: this.sequence }); 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)) { if (isNil(payload.t)) {
throw new Error('Event name is required'); throw new Error('Event name is required');
} }
@@ -131,7 +131,7 @@ export class Gateway {
} else if (event_name === 'INTERACTION_CREATE') { } else if (event_name === 'INTERACTION_CREATE') {
// this.handleInteractionCreate(payload); // this.handleInteractionCreate(payload);
} else if (event_name === 'MESSAGE_CREATE') { } else if (event_name === 'MESSAGE_CREATE') {
this.handleMessageCreate(payload as Payload); await this.handleMessageCreate(payload as Payload);
} else if (event_name === 'MESSAGE_UPDATE') { } else if (event_name === 'MESSAGE_UPDATE') {
// this.handleMessageCreate(payload); // this.handleMessageCreate(payload);
} }
@@ -141,7 +141,8 @@ export class Gateway {
if (['MESSAGE_CREATE', 'MESSAGE_UPDATE'].includes(event_name)) { if (['MESSAGE_CREATE', 'MESSAGE_UPDATE'].includes(event_name)) {
// If the event is a message event, create a message object // 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 // And then generate the context
context = new Context(this.client, this.api, message); context = new Context(this.client, this.api, message);
} }
@@ -164,7 +165,7 @@ export class Gateway {
return; return;
} }
// Extract the author // 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 // Store the author in the cache
this.client.users.set(author.id, author); this.client.users.set(author.id, author);
// Extract the content // Extract the content
@@ -185,14 +186,15 @@ export class Gateway {
// Extract the command name // Extract the command name
// TODO: Case insensitive config? // TODO: Case insensitive config?
const command = args.shift()!; const command = args.shift()!;
console.log('payload.d', payload.d)
// Create a new message object from the payload // 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 // Generate context
const context = new Context(this.client, this.api, message); const context = new Context(this.client, this.api, message);
// Check if command exists // Check if command exists
if (this.processor.commands.has(command)) { if (this.processor.chat_commands.has(command)) {
// Execute the handler // Execute the handler
this.processor.commands.execute(command, context, args); this.processor.chat_commands.execute(command, context, args);
} else { } else {
// TODO: Custom unknown command handler // TODO: Custom unknown command handler
context.reply(`Unknown command: ${command}`); 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 type { API } from "./api";
import { ObjectCache } from "./cache"; import { ObjectCache } from "./cache";
import type { Processor } from "./processor"; import type { Processor } from "./processor";
export class Guilds extends ObjectCache<Guild> { export class Guilds extends ObjectCache<Guild> {
constructor (api: API, processor: Processor) { 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 type { Context } from "../types/context";
import { HandlerType, type EventPayload, type Payload } from "../types/common"; import { HandlerType, type EventPayload, type Payload } from "../types/common";
import { BaseHandler, type Handler } from "../types/hander"; import { BaseHandler, type Handler } from "../types/hander";
import type { API } from "./api";
export class Processor { export class Processor {
private handlers = { private handlers = {
[HandlerType.Events]: new Map<string, Handler[]>(), [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[]>() [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) { public register(type: HandlerType, event: string | string[], handler: Handler) {
// Normalize events into an array // 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 async execute(type: HandlerType.Events, event: string, context: Context | null, payload: EventPayload): Promise<void>
public execute(type: Omit<HandlerType, HandlerType.Events>, event: string, context: Context, args: any[]): void public async execute(type: Omit<HandlerType, HandlerType.Events>, event: string, context: Context, args: any[]): Promise<void>
public execute(type: HandlerType, event: string, context: Context | null, argsOrPayload: any[] | EventPayload) { public async execute(type: HandlerType, event: string, context: Context | null, argsOrPayload: any[] | EventPayload) {
const handlers = this.handlers[type].get(event); const handlers = this.handlers[type].get(event);
if (handlers) { if (handlers) {
for (const handler of handlers) { for (const handler of handlers) {
if (handler instanceof BaseHandler) { if (handler instanceof BaseHandler) {
handler.execute(context, argsOrPayload); await Promise.resolve(handler.execute(context, argsOrPayload));
} else { } else {
// TODO: Fix ts error // TODO: Fix ts error
handler(context, argsOrPayload); await Promise.resolve(handler(context, argsOrPayload))
} }
} }
} }
@@ -66,21 +68,28 @@ export class Processor {
[HandlerType.Events] = { [HandlerType.Events] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Events, event, handler), register: (event: string | string[], handler: Handler) => this.register(HandlerType.Events, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(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 has: (event: string) => this.handlers[HandlerType.Events].has(event) && this.handlers[HandlerType.Events].get(event)?.length! > 0
}; };
[HandlerType.Commands] = { [HandlerType.ChatCommands] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Commands, event, handler), register: (event: string | string[], handler: Handler) => this.register(HandlerType.ChatCommands, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.Commands, event, handler), unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.ChatCommands, event, handler),
execute: (event: string, context: Context, args: any[]) => this.execute(HandlerType.Commands, event, context, args), execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.ChatCommands, event, context, args),
has: (event: string) => this.handlers[HandlerType.Commands].has(event) ?? this.handlers[HandlerType.Commands].get(event)?.length! > 0 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] = { [HandlerType.Interactions] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Interactions, event, handler), register: (event: string | string[], handler: Handler) => this.register(HandlerType.Interactions, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(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 has: (event: string) => this.handlers[HandlerType.Interactions].has(event) ?? this.handlers[HandlerType.Interactions].get(event)?.length! > 0
}; };
} }
+2 -1
View File
@@ -10,7 +10,8 @@ export type ClientConfig = {
} }
export enum HandlerType { export enum HandlerType {
Events = 'events', Events = 'events',
Commands = 'commands', ChatCommands = 'chat_commands',
SlashCommands = 'slash_commands',
Interactions = 'interactions' Interactions = 'interactions'
} }
+16 -31
View File
@@ -1,7 +1,7 @@
import type { API } from "../services/api"; import type { API } from "../services/api";
import type { Client } from "../services/client"; import type { Client } from "../services/client";
import { isNil } from "../utils"; import { isNil } from "../utils";
import { Message } from "./message"; import { DiscordMessage, Message } from "./message";
/** /**
* Represents the context of a message in a Discord channel. * Represents the context of a message in a Discord channel.
* Provides methods to interact with the message and the channel it was sent in. * Provides methods to interact with the message and the channel it was sent in.
@@ -26,39 +26,27 @@ export class Context {
channel_id: string; channel_id: string;
guild_id?: string; guild_id?: string;
constructor(private client: Client, private api: API, private _message: Message) { constructor(private client: Client, private api: API, public message: Message) {
if (isNil(_message.id)) { console.log('Context created: ', );
if (isNil(message.id)) {
throw new Error('Message ID is required to create a context'); throw new Error('Message ID is required to create a context');
} }
if (isNil(_message.channel_id)) { if (isNil(message.channel_id)) {
throw new Error('Channel ID is required to create a context'); throw new Error('Channel ID is required to create a context');
} }
this.client = client; this.client = client;
this.message_id = _message.id; this.message_id = message.id;
this.channel_id = _message.channel_id; this.channel_id = message.channel_id;
this.guild_id = _message.guild_id; this.guild_id = message.guild_id;
} }
// get author() { // get author() {
// return this.client.users.get(this.author_id); // return this.client.users.get(this.author_id);
// }; // };
get message(): Message & { getReference: () => Promise<Message | null> } { async fetchGuild() {
return Object.assign(this._message, {
getReference: async () => {
const reference = this._message.message_reference;
if (isNil(reference)) {
return null;
}
return Message.fromAPIResponse(await this.api.get(`/channels/${reference.channel_id}/messages/${reference.message_id}`));
}
});
}
fetchGuild() {
if (isNil(this.guild_id)) { if (isNil(this.guild_id)) {
return Promise.resolve(null); return Promise.resolve(null);
} }
@@ -78,11 +66,7 @@ export class Context {
} }
if (reference) { if (reference) {
message.message_reference = { message.setReference(this.message)
message_id: this.message_id,
channel_id: this.channel_id,
guild_id: this.guild_id
};
} }
return this.client.sendMessage(this.channel_id, message); return this.client.sendMessage(this.channel_id, message);
@@ -108,7 +92,7 @@ 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. * @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<void>} A promise that resolves when the message is deleted. * @returns {Promise<void>} A promise that resolves when the message is deleted.
*/ */
public delete = (message_id?: string) => { public deleteMessage = (message_id: string) => {
return this.api.delete(`/channels/${this.channel_id}/messages/${message_id ?? this.message_id}`); return this.api.delete(`/channels/${this.channel_id}/messages/${message_id ?? this.message_id}`);
}; };
@@ -119,10 +103,11 @@ export class Context {
* @param {Message | string} message - The updated message content. * @param {Message | string} message - The updated message content.
* @returns {Promise<Message>} The updated message. * @returns {Promise<Message>} The updated message.
*/ */
public edit = async (message: Message | string) => { public async editMessage (message: Message, content?: string) {
if (typeof message === 'string') { const clone = new Message(message);
message = new Message().setContent(message); if (content) {
clone.setContent(content);
} }
return Message.fromAPIResponse(await this.api.patch(`/channels/${this.channel_id}/messages/${this.message_id}`, message)); return Message.hydrate(DiscordMessage.fromAPIResponse(await this.api.patch(`/channels/${clone.channel_id}/messages/${clone.id}`, clone)), this.client, this.api);
}; };
} }
+43 -2
View File
@@ -4,6 +4,47 @@ import { Member } from "./member";
import { Role } from "./role"; import { Role } from "./role";
export class Guild { export class Guild {
id: string;
name: string;
description: string | null;
icon: string | null;
banner: string | null;
owner_id: string;
// emojis: Emoji[];
// roles: Role[];
constructor(data: Guild) {
this.id = data.id;
this.name = data.name;
this.description = data.description;
this.icon = data.icon;
this.banner = data.banner;
this.owner_id = data.owner_id;
// this.emojis = data.emojis;
// this.roles = data.roles;
}
public static async factory(data: any) {
return await Guild.hydrate(DiscordGuild.fromAPIResponse(data));
}
public static async hydrate(data: DiscordGuild) {
return new Guild({
id: data.id,
name: data.name,
description: data.description,
icon: data.icon,
banner: data.banner,
owner_id: data.owner_id,
// emojis: data.emojis,
// roles: data.roles,
});
}
}
export class DiscordGuild {
system_channel_id: string | null; system_channel_id: string | null;
premium_progress_bar_enabled: boolean; premium_progress_bar_enabled: boolean;
region: string; region: string;
@@ -124,7 +165,7 @@ export class Guild {
this.explicit_content_filter = data.explicit_content_filter; this.explicit_content_filter = data.explicit_content_filter;
} }
static fromAPIResponse(data: any): Guild { static fromAPIResponse(data: any): DiscordGuild {
return new Guild(data); return new DiscordGuild(data);
} }
} }
+3 -3
View File
@@ -4,11 +4,11 @@ import type { Context } from "./context";
export class BaseHandler { export class BaseHandler {
constructor() {}; constructor() {};
public execute(context: Context | null, args: any[]) {}; public async execute(context: Context | null, args: any[]) {};
} }
export type CommandHandlerFunction = (context: Context, args: any[]) => void; export type CommandHandlerFunction = (context: Context, args: any[]) => Promise<void> | void;
export type EventHandlerFunction = (context: Context | null, payload: EventPayload) => void; export type EventHandlerFunction = (context: Context | null, payload: EventPayload) => Promise<void> | void;
export type HandlerFunction = CommandHandlerFunction | EventHandlerFunction; export type HandlerFunction = CommandHandlerFunction | EventHandlerFunction;
export type Handler = CommandHandlerFunction | EventHandlerFunction | BaseHandler; export type Handler = CommandHandlerFunction | EventHandlerFunction | BaseHandler;
+163 -32
View File
@@ -1,6 +1,8 @@
import type { API } from '../services/api';
import type { Client } from '../services/client';
import { isNil } from '../utils'; import { isNil } from '../utils';
import { Embed } from './embed'; import { Embed } from './embed';
import { User } from './user'; import { DiscordUser, User } from './user';
export type Component = Partial<Button | SelectMenu> & { export type Component = Partial<Button | SelectMenu> & {
type: ComponentType; type: ComponentType;
@@ -84,36 +86,42 @@ export class Message {
author?: User; author?: User;
channel_id?: string; channel_id?: string;
guild_id?: string; guild_id?: string;
public content?: string; public content: string;
public embeds: Embed[] = []; public embeds: Embed[] = [];
public components: Component[] = []; public components: Component[] = [];
public message_reference?: { public reference?: Message;
type?: number; timestamp: Date;
message_id: string;
channel_id: string;
guild_id?: string;
fail_if_not_exists?: boolean;
};
constructor(data?: Partial<Message>) { constructor(data?: Partial<Message>) {
if (data) { this.id = data?.id;
this.id = data.id; this.author = data?.author;
this.author = User.fromAPIResponse(data.author); this.channel_id = data?.channel_id;
this.channel_id = data.channel_id; this.guild_id = data?.guild_id;
this.guild_id = data.guild_id; this.content = data?.content ?? '';
this.content = data.content; this.reference = data?.reference;
this.message_reference = data.message_reference; this.timestamp = data?.timestamp ?? new Date();
}
} }
public static fromAPIResponse = (data: any) => { public static factory = async (data: any, client: Client, api: API, resolveReference: boolean = true): Promise<Message> => {
return Message.hydrate(DiscordMessage.fromAPIResponse(data), client, api, resolveReference);
}
public static hydrate = async (discord_message: DiscordMessage, client: Client, api: API, resolveReference: boolean = true): Promise<Message> => {
let reference: Message | undefined;
if (discord_message.message_reference && resolveReference) {
const { channel_id, message_id } = discord_message.message_reference;
reference = await client.messages.get(channel_id, message_id, false);
}
return new Message({ return new Message({
id: data.id, id: discord_message.id,
author: data.author, author: User.hydrate(discord_message.author),
channel_id: data.channel_id, channel_id: discord_message.channel_id,
guild_id: data.guild_id, guild_id: discord_message?.guild_id,
content: data.content, content: discord_message.content,
message_reference: data.message_reference reference,
timestamp: new Date(discord_message.timestamp)
}); });
} }
@@ -157,17 +165,20 @@ export class Message {
return this; return this;
}; };
public setReference = (message: Partial<Message>) => { public setReference = (message: Message | Partial<Message>) => {
if (isNil(message.id) || isNil(message.channel_id)) { if (isNil(message.id) || isNil(message.channel_id)) {
throw new Error('Message ID and Channel ID are required'); throw new Error('Message ID and Channel ID are required');
} }
this.message_reference = { let ref: Message;
message_id: message.id,
channel_id: message.channel_id, if (!(message instanceof Message)) {
guild_id: message.guild_id, ref = new Message(message);
fail_if_not_exists: false } else {
}; ref = message;
}
this.reference = ref;
return this; return this;
}; };
@@ -176,7 +187,127 @@ export class Message {
content: this.content, content: this.content,
embeds: this.embeds, embeds: this.embeds,
components: this.components, components: this.components,
message_reference: this.message_reference message_reference: {
message_id: this.reference?.id,
channel_id: this.reference?.channel_id,
guild_id: this.reference?.guild_id,
fail_if_not_exists: false
}
}; };
}; };
} }
// Raw API Class
export class DiscordMessage {
id: string;
channel_id: string;
guild_id?: string;
author: DiscordUser;
// member?: DiscordGuildMember;
content: string;
// ISO8601 timestamp
timestamp: string;
edited_timestamp: string | null;
tts: boolean;
mention_everyone: boolean;
mentions: DiscordUser[];
mention_roles: string[];
mention_channels?: { id: string; guild_id: string; type: number; name: string }[];
attachments: { id: string; filename: string; url: string; proxy_url: string; size: number }[];
// embeds: DiscordEmbed[];
reactions?: { count: number; me: boolean; emoji: Emoji }[];
nonce?: string | number;
pinned: boolean;
webhook_id?: string;
type: number;
activity?: { type: number; party_id?: string };
application?: { id: string; cover_image?: string; description: string; icon?: string; name: string };
application_id?: string;
message_reference?: { message_id: string; channel_id: string; guild_id?: string; fail_if_not_exists?: boolean };
flags?: number;
referenced_message?: DiscordMessage | null;
interaction?: { id: string; type: number; name: string; user: DiscordUser };
thread?: { id: string; name: string; type: number };
components?: any[];
sticker_items?: { id: string; name: string; format_type: number }[];
stickers?: {
id: string;
pack_id?: string;
name: string;
description?: string;
tags: string;
asset: string;
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
});
}
}
+59 -4
View File
@@ -1,3 +1,58 @@
export class User {
id: string;
username: string;
discriminator: string;
display_name: string;
avatar: string;
bot?: boolean;
system?: boolean;
mfa_enabled?: boolean;
banner?: string;
accent_color?: number;
locale?: string;
verified?: boolean;
email?: string;
// TODO: Add the rest of the properties
constructor(data: Partial<User> & { id: string; username: string; discriminator: string; display_name: string; avatar: string }) {
this.id = data.id;
this.username = data.username;
this.discriminator = data.discriminator;
this.display_name = data.display_name;
this.avatar = data.avatar;
this.bot = data.bot;
this.system = data.system;
this.mfa_enabled = data.mfa_enabled;
this.banner = data.banner;
this.accent_color = data.accent_color;
this.locale = data.locale;
this.verified = data.verified;
this.email = data.email;
}
public static async factory(data: DiscordUser) {
return User.hydrate(DiscordUser.fromAPIResponse(data));
}
public static hydrate(data: DiscordUser) {
return new User({
id: data.id,
username: data.username,
discriminator: data.discriminator,
display_name: data.global_name,
avatar: data.avatar,
bot: data.bot,
system: data.system,
mfa_enabled: data.mfa_enabled,
banner: data.banner,
accent_color: data.accent_color,
locale: data.locale,
verified: data.verified,
email: data.email
});
}
}
// id snowflake the user's id identify // id snowflake the user's id identify
// username string the user's username, not unique across the platform identify // username string the user's username, not unique across the platform identify
// discriminator string the user's Discord-tag identify // discriminator string the user's Discord-tag identify
@@ -16,7 +71,7 @@
// public_flags? integer the public flags on a user's account identify // public_flags? integer the public flags on a user's account identify
// avatar_decoration_data? // avatar_decoration_data?
export class User { export class DiscordUser {
id: string; id: string;
username: string; username: string;
discriminator: string; discriminator: string;
@@ -35,7 +90,7 @@ export class User {
public_flags?: number; public_flags?: number;
avatar_decoration_data?: Record<string, any>; avatar_decoration_data?: Record<string, any>;
constructor(data: User) { constructor(data: DiscordUser) {
this.id = data.id; this.id = data.id;
this.username = data.username; this.username = data.username;
this.discriminator = data.discriminator; this.discriminator = data.discriminator;
@@ -55,8 +110,8 @@ export class User {
this.avatar_decoration_data = data.avatar_decoration_data; this.avatar_decoration_data = data.avatar_decoration_data;
} }
static fromAPIResponse(data: any): User { static fromAPIResponse(data: any): DiscordUser {
return new User({ return new DiscordUser({
id: data.id, id: data.id,
username: data.username, username: data.username,
discriminator: data.discriminator, discriminator: data.discriminator,
+23 -8
View File
@@ -13,25 +13,40 @@ const client = new Client({
}); });
client.chatCommands.register('ping', async (context: Context, args: any[]) => { client.chatCommands.register('ping', async (context: Context, args: any[]) => {
await context.reply(`Pong!: got ${args.join(', ')}`, true) const message = await context.reply(`Pong!: got ${args.join(', ')}`, true);
// Calculate the time it took to send the message
const time = message.timestamp.getTime() - context.message.timestamp.getTime();
// Edit the message to include the time
await context.editMessage(message, `Pong! Latency: ${time}ms`);
}); });
client.chatCommands.register('here', async (context: Context, _args: any[]) => { client.chatCommands.register('here', async (context: Context, _args: any[]) => {
const guild = await context.fetchGuild(); const guild = await context.fetchGuild();
if (guild !== null) { if (guild !== null) {
await context.reply(`You are in ${guild?.name}`, true); await context.reply(`You are in ${guild.name}`, true);
} else { } else {
await context.reply('You are not in a guild', true); await context.reply('This is not a server!', true);
} }
}); });
client.chatCommands.register('quote', async (context: Context, args: any[]) => { client.chatCommands.register('quote', async (context: Context, _args: any[]) => {
const referenced = await context.message.getReference(); const referenced = context.message.reference;
if (referenced === null) {
await context.reply('No message to quote', true); let reply = null;
if (referenced === null || referenced === undefined) {
reply = await context.reply('No message to quote', true);
return; return;
} else { } else {
await context.reply(`> "${referenced.content}"\n> — ${referenced.author?.global_name}`, true); reply = await context.reply(`> "${referenced.content}"\n> — ${referenced.author?.display_name ?? referenced.author?.username}`, true);
// Save to quote database
// TODO
}
if (reply?.id) {
const message_id = reply.id;
setTimeout(() => {
context.deleteMessage(message_id);
}, 3000);
} }
}); });