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'
|
'Content-Type': 'application/json'
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`API Request: ${method} ${url}`);
|
|
||||||
console.log(`API Body: ${JSON.stringify(data, null, 4)}`);
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
@@ -30,6 +27,9 @@ export class API {
|
|||||||
|
|
||||||
const body = await response.json();
|
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));
|
// console.log('API Response:', JSON.stringify(body, null, 4));
|
||||||
|
|
||||||
return body;
|
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 { Messages } from './caches/messages';
|
||||||
import { Users } from './caches/users';
|
import { Users } from './caches/users';
|
||||||
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
|
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
|
||||||
import type { User } from '../types/user';
|
import { EventBus, type Emitter } from './bus';
|
||||||
|
|
||||||
export class Client {
|
export class Client {
|
||||||
readonly token: string;
|
readonly token: string;
|
||||||
@@ -16,6 +16,7 @@ export class Client {
|
|||||||
readonly intents: Intent[] = [Intent.DEFAULT];
|
readonly intents: Intent[] = [Intent.DEFAULT];
|
||||||
prefix: string;
|
prefix: string;
|
||||||
// Internal services
|
// Internal services
|
||||||
|
protected bus: Emitter;
|
||||||
protected api: API;
|
protected api: API;
|
||||||
protected processor: Processor;
|
protected processor: Processor;
|
||||||
protected gateway: Gateway;
|
protected gateway: Gateway;
|
||||||
@@ -34,9 +35,10 @@ 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.bus = EventBus;
|
||||||
this.api = new API(this);
|
this.api = new API(this);
|
||||||
this.processor = new Processor(this, this.api);
|
this.processor = new Processor(this, this.api, this.bus);
|
||||||
this.gateway = new Gateway(this, this.processor, this.api);
|
this.gateway = new Gateway(this, this.processor, this.api, this.bus);
|
||||||
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);
|
this.messages = new Messages(this, this.api, this.processor);
|
||||||
@@ -112,7 +114,7 @@ export class Client {
|
|||||||
message = new Message().setContent(message);
|
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 = {
|
// chatCommands = {
|
||||||
@@ -127,5 +129,11 @@ export class Client {
|
|||||||
// has: (name: string) => this.processor.application_commands.has(name)
|
// 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();
|
connect = () => this.gateway.connect();
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { WebSocket, type Data } from 'ws';
|
import { WebSocket, type Data } from 'ws';
|
||||||
import type { Client } from "../client";
|
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 { 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';
|
||||||
@@ -9,6 +9,7 @@ import { type DiscordMessage, Message } from '../../types/message';
|
|||||||
import { DiscordUser, User } from '../../types/user';
|
import { DiscordUser, User } from '../../types/user';
|
||||||
import { Guild } from '../../types/guild';
|
import { Guild } from '../../types/guild';
|
||||||
import { Channel } from '../../types/channel';
|
import { Channel } from '../../types/channel';
|
||||||
|
import type { Emitter } from '../bus';
|
||||||
|
|
||||||
export class Gateway {
|
export class Gateway {
|
||||||
private socket: WebSocket | null = null;
|
private socket: WebSocket | null = null;
|
||||||
@@ -18,14 +19,13 @@ export class Gateway {
|
|||||||
// Internal state
|
// Internal state
|
||||||
user: User | null = null;
|
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 = () => {
|
private onOpen = () => {
|
||||||
log('Connected to gateway');
|
log('Connected to gateway');
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMessage = async (packet: Data) => {
|
private onMessage = async (packet: Data) => {
|
||||||
// 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;
|
||||||
|
|
||||||
@@ -36,12 +36,13 @@ export class Gateway {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (exists(data)) {
|
||||||
log('='.repeat(25));
|
// NOTE: Uncomment for debugging recieved event data
|
||||||
log('received: ', data.op);
|
// log('='.repeat(25));
|
||||||
log('received event: ', data.t ?? 'no event');
|
// log('received: ', data.op);
|
||||||
log('received data: ', exists(data.d) ? Object.keys(data.d) : 'no data');
|
// log('received event: ', data.t ?? 'no event');
|
||||||
log('='.repeat(25));
|
// log('received data: ', exists(data.d) ? Object.keys(data.d) : 'no data');
|
||||||
|
// log('='.repeat(25));
|
||||||
// If the data is successfully parsed check the opcode
|
// If the data is successfully parsed check the opcode
|
||||||
const code = data.op;
|
const code = data.op;
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
@@ -61,6 +62,9 @@ export class Gateway {
|
|||||||
this.heartbeat_interval = data.d?.heartbeat_interval;
|
this.heartbeat_interval = data.d?.heartbeat_interval;
|
||||||
// Kick off the heartbeat loop
|
// Kick off the heartbeat loop
|
||||||
this.heartbeat();
|
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);
|
setTimeout(this.indentify, 2500);
|
||||||
} else if (code === 11) {
|
} else if (code === 11) {
|
||||||
// Heartbeat ACK
|
// Heartbeat ACK
|
||||||
@@ -70,7 +74,6 @@ export class Gateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private heartbeat() {
|
private heartbeat() {
|
||||||
log('Sending heartbeat');
|
|
||||||
if (exists(this.heartbeat_interval)) {
|
if (exists(this.heartbeat_interval)) {
|
||||||
// If a previous heartbeat timeout exists, clear it
|
// If a previous heartbeat timeout exists, clear it
|
||||||
if (exists(this.heartbeat_timeout)) {
|
if (exists(this.heartbeat_timeout)) {
|
||||||
@@ -91,9 +94,9 @@ export class Gateway {
|
|||||||
throw new Error('Socket is not connected');
|
throw new Error('Socket is not connected');
|
||||||
}
|
}
|
||||||
|
|
||||||
log('='.repeat(25));
|
// log('='.repeat(25));
|
||||||
log('sending: ', JSON.stringify(payload, null, 4));
|
// log('sending: ', JSON.stringify(payload, null, 4));
|
||||||
log('='.repeat(25));
|
// log('='.repeat(25));
|
||||||
|
|
||||||
this.socket.send(JSON.stringify(payload));
|
this.socket.send(JSON.stringify(payload));
|
||||||
};
|
};
|
||||||
@@ -118,15 +121,18 @@ export class Gateway {
|
|||||||
throw new Error('Event name is required');
|
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_name = payload.t;
|
||||||
const event_data = payload.d;
|
const event_data = payload.d;
|
||||||
|
|
||||||
|
// Simple validation on the event data
|
||||||
if (!isObject(event_data)) {
|
if (!isObject(event_data)) {
|
||||||
throw new Error('Invalid event data received');
|
throw new Error('Invalid event data received');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a known event
|
||||||
if (event_name === 'READY') {
|
if (event_name === 'READY') {
|
||||||
this.user = User[fromDiscord](DiscordUser.fromAPIResponse(event_data.user));
|
this.user = User[fromDiscord](DiscordUser.fromAPIResponse(event_data.user));
|
||||||
} else if (event_name === 'INTERACTION_CREATE') {
|
} else if (event_name === 'INTERACTION_CREATE') {
|
||||||
@@ -140,6 +146,7 @@ export class Gateway {
|
|||||||
// Allocate a var to store the context
|
// Allocate a var to store the context
|
||||||
let context = null;
|
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 (['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[fromDiscord](event_data as DiscordMessage);
|
const message = Message[fromDiscord](event_data as DiscordMessage);
|
||||||
@@ -148,7 +155,8 @@ export class Gateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call event handlers
|
// 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) => {
|
private handleMessageCreate = async (payload: Payload) => {
|
||||||
@@ -169,7 +177,7 @@ export class Gateway {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Extract the author
|
// 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
|
// 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
|
||||||
@@ -191,13 +199,15 @@ export class Gateway {
|
|||||||
// TODO: Case insensitive config?
|
// TODO: Case insensitive config?
|
||||||
const command = args.shift()!;
|
const command = args.shift()!;
|
||||||
// Create a new message object from the payload
|
// 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
|
// 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.chat_commands.has(command)) {
|
if (this.processor[HandlerType.ChatCommands].has(command)) {
|
||||||
// Execute the handler
|
// Execute the handler
|
||||||
this.processor.chat_commands.execute(command, context, args);
|
this.processor[HandlerType.ChatCommands].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}`);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { HandlerType, Trait, type EventPayload } from "../types/common";
|
|||||||
import { CommandHandler, type Handler, type ArgsFromOptions, OptionTypeMap } from "../types/handler";
|
import { CommandHandler, type Handler, type ArgsFromOptions, OptionTypeMap } from "../types/handler";
|
||||||
import type { API } from "./api";
|
import type { API } from "./api";
|
||||||
import { ApplicationCommandType, type ApplicationCommandOption, type ApplicationCommandOptionResult } from "../types/applicationCommand";
|
import { ApplicationCommandType, type ApplicationCommandOption, type ApplicationCommandOptionResult } from "../types/applicationCommand";
|
||||||
|
import type { Emitter } from "./bus";
|
||||||
|
|
||||||
export class Processor {
|
export class Processor {
|
||||||
private handlers = {
|
private handlers = {
|
||||||
@@ -13,7 +14,7 @@ export class Processor {
|
|||||||
[HandlerType.Interactions]: new Map<string, Handler[]>()
|
[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) {
|
public register(type: HandlerType, event: string | string[], handler: Handler) {
|
||||||
// Normalize events into an array
|
// Normalize events into an array
|
||||||
@@ -76,6 +77,7 @@ export class Processor {
|
|||||||
const zipped: [ApplicationCommandOption, ApplicationCommandOptionResult][] = args.map((arg, i) => [definition[i], arg]);
|
const zipped: [ApplicationCommandOption, ApplicationCommandOptionResult][] = args.map((arg, i) => [definition[i], arg]);
|
||||||
// Validate the args
|
// Validate the args
|
||||||
const validated: Record<string, any> = {};
|
const validated: Record<string, any> = {};
|
||||||
|
// TODO: Attempt some type coresion/resolution here (mostly for text commands)
|
||||||
for (const [def, arg] of zipped) {
|
for (const [def, arg] of zipped) {
|
||||||
if (def.choices) {
|
if (def.choices) {
|
||||||
// If the definition has choices, validate the arg against them
|
// If the definition has choices, validate the arg against them
|
||||||
@@ -125,7 +127,10 @@ export class Processor {
|
|||||||
|
|
||||||
// Ensure the command name is valid
|
// Ensure the command name is valid
|
||||||
// @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming
|
// @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');
|
throw new Error('Invalid application command name');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Trait } from "./common";
|
||||||
|
|
||||||
|
export abstract class GatewayObject {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
// TODO: Add a clone method to subclasses
|
||||||
|
// public abstract [Trait.clone]<T extends GatewayObject>(this: new (data: any) => T): T;
|
||||||
|
|
||||||
|
// public get clone() {
|
||||||
|
// return this[Trait.clone];
|
||||||
|
// }
|
||||||
|
|
||||||
|
static [Trait.fromDiscord]<T extends GatewayObject>(this: new (...args: any[]) => T, data: any): T {
|
||||||
|
throw new Error('Method should be overridden - you should not see this. If you do, please report it.');
|
||||||
|
};
|
||||||
|
|
||||||
|
static get fromDiscord() {
|
||||||
|
return this[Trait.fromDiscord];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,13 +21,16 @@ export enum HandlerType {
|
|||||||
|
|
||||||
export type Expects<T extends Array<Expectation>, K extends Expectation> = Extract<K, T[number]> extends never ? false : true;
|
export type Expects<T extends Array<Expectation>, K extends Expectation> = Extract<K, T[number]> extends never ? false : true;
|
||||||
|
|
||||||
|
export const clone: unique symbol = Symbol('clone')
|
||||||
export const fromDiscord: unique symbol = Symbol('fromDiscord')
|
export const fromDiscord: unique symbol = Symbol('fromDiscord')
|
||||||
export const execute: unique symbol = Symbol('execute')
|
export const execute: unique symbol = Symbol('execute')
|
||||||
|
|
||||||
export const Trait: {
|
export const Trait: {
|
||||||
|
clone: typeof clone
|
||||||
fromDiscord: typeof fromDiscord
|
fromDiscord: typeof fromDiscord
|
||||||
execute: typeof execute
|
execute: typeof execute
|
||||||
} = {
|
} = {
|
||||||
|
clone,
|
||||||
fromDiscord,
|
fromDiscord,
|
||||||
execute
|
execute
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-9
@@ -29,20 +29,19 @@ export type HydratedContext<T extends Array<Expectation>> = Context & {
|
|||||||
message: Extract<Expectation.Message, T[number]> extends never ? undefined : Message;
|
message: Extract<Expectation.Message, T[number]> extends never ? undefined : Message;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextData =
|
export type ContextData =
|
||||||
| { channel_id?: string; message_id?: string; guild_id?: string; message?: MessagePayload, interaction: InteractionPayload }
|
| { channel_id?: string; message_id?: string; guild_id?: string; message?: MessagePayload, interaction: InteractionPayload }
|
||||||
| { channel_id?: string; message_id?: string; guild_id?: string; message: MessagePayload, interaction?: InteractionPayload }
|
| { channel_id?: string; message_id?: string; guild_id?: string; message: MessagePayload, interaction?: InteractionPayload };
|
||||||
|
|
||||||
export class Context {
|
export class Context<D extends ContextData = ContextData> {
|
||||||
// Public properties
|
// Public properties
|
||||||
message_id?: string;
|
message_id?: string;
|
||||||
channel_id: string;
|
channel_id: string;
|
||||||
guild_id?: string;
|
guild_id?: string;
|
||||||
message?: MessagePayload;
|
message?: D extends { message: MessagePayload } ? MessagePayload : undefined;
|
||||||
// TODO: Type this
|
interaction?: D extends { interaction: InteractionPayload } ? InteractionPayload : undefined;
|
||||||
interaction?: InteractionPayload;
|
|
||||||
|
|
||||||
constructor(private client: Client, private api: API, data: ContextData) {
|
constructor(private client: Client, private api: API, data: D) {
|
||||||
const channel_id = data.channel_id ?? data.message?.channel_id ?? data.interaction?.channel_id;
|
const channel_id = data.channel_id ?? data.message?.channel_id ?? data.interaction?.channel_id;
|
||||||
|
|
||||||
if (isNil(channel_id)) {
|
if (isNil(channel_id)) {
|
||||||
@@ -53,8 +52,8 @@ export class Context {
|
|||||||
this.message_id = data.message_id ?? data.message?.id;
|
this.message_id = data.message_id ?? data.message?.id;
|
||||||
this.channel_id = channel_id;
|
this.channel_id = channel_id;
|
||||||
this.guild_id = data.guild_id ?? data.message?.guild_id ?? data.interaction?.guild_id;
|
this.guild_id = data.guild_id ?? data.message?.guild_id ?? data.interaction?.guild_id;
|
||||||
this.message = data.message;
|
this.message = data.message as D extends { message: MessagePayload } ? MessagePayload : undefined;
|
||||||
this.interaction = data.interaction;
|
this.interaction = data.interaction as D extends { interaction: InteractionPayload } ? InteractionPayload : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get self() {
|
get self() {
|
||||||
@@ -104,6 +103,11 @@ export class Context {
|
|||||||
*/
|
*/
|
||||||
public reply = async (message: Message | string, reference: boolean = true): Promise<MessagePayload> => {
|
public reply = async (message: Message | string, reference: boolean = true): Promise<MessagePayload> => {
|
||||||
console.log('this.interaction', this.interaction)
|
console.log('this.interaction', this.interaction)
|
||||||
|
|
||||||
|
if (isNil(this.message)) {
|
||||||
|
throw new Error('Cannot reply without a message');
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof message === 'string') {
|
if (typeof message === 'string') {
|
||||||
message = new Message().setContent(message);
|
message = new Message().setContent(message);
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-6
@@ -17,7 +17,7 @@ export const OptionTypeMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Recursive type to map ApplicationCommandOption to arguments
|
// Recursive type to map ApplicationCommandOption to arguments
|
||||||
type ArgsFromOptions<T extends readonly ApplicationCommandOption[]> = {
|
export type ArgsFromOptions<T extends readonly ApplicationCommandOption[]> = {
|
||||||
[Option in T[number] as Option['name']]: Option extends { options: readonly ApplicationCommandOption[] }
|
[Option in T[number] as Option['name']]: Option extends { options: readonly ApplicationCommandOption[] }
|
||||||
? ArgsFromOptions<Option['options']> // Recursive inference for subcommands
|
? ArgsFromOptions<Option['options']> // Recursive inference for subcommands
|
||||||
: Option['choices'] extends readonly { value: infer U }[]
|
: Option['choices'] extends readonly { value: infer U }[]
|
||||||
@@ -33,16 +33,26 @@ type ArgsFromOptions<T extends readonly ApplicationCommandOption[]> = {
|
|||||||
: unknown | undefined;
|
: unknown | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CommandHandlerExecuteFunction<T extends readonly ApplicationCommandOption[]> = (context: Context, args: ArgsFromOptions<T>) => Promise<void> | void;
|
||||||
|
|
||||||
export type CommandHandlerInput<T extends readonly ApplicationCommandOption[]> = {
|
export type CommandHandlerInput<T extends readonly ApplicationCommandOption[]> = {
|
||||||
args: T;
|
args: T;
|
||||||
description: string;
|
description: string;
|
||||||
[Trait.execute]: (context: Context, args: ArgsFromOptions<T>) => Promise<void> | void;
|
execute: CommandHandlerExecuteFunction<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CommandHandler<T extends readonly ApplicationCommandOption[]> {
|
export class CommandHandler<T extends readonly ApplicationCommandOption[]> {
|
||||||
args: T;
|
args: T;
|
||||||
description: string;
|
description: string;
|
||||||
[Trait.execute]!: (context: Context, args: ArgsFromOptions<T>) => Promise<void> | void;
|
[Trait.execute]!: CommandHandlerExecuteFunction<T>;
|
||||||
|
|
||||||
|
get execute() {
|
||||||
|
return this[Trait.execute];
|
||||||
|
}
|
||||||
|
|
||||||
|
set execute(fn: CommandHandlerExecuteFunction<T>) {
|
||||||
|
this[Trait.execute] = fn;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(input: CommandHandlerInput<T>) {
|
constructor(input: CommandHandlerInput<T>) {
|
||||||
// Validate option names
|
// Validate option names
|
||||||
@@ -54,7 +64,7 @@ export class CommandHandler<T extends readonly ApplicationCommandOption[]> {
|
|||||||
}
|
}
|
||||||
this.args = input.args;
|
this.args = input.args;
|
||||||
this.description = input.description
|
this.description = input.description
|
||||||
this[Trait.execute] = input[Trait.execute];
|
this[Trait.execute] = input.execute;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@@ -74,5 +84,7 @@ export function createCommandHandler<T extends readonly ApplicationCommandOption
|
|||||||
export type CommandHandlerFunction = (context: Context, args: any[]) => Promise<void> | void;
|
export type CommandHandlerFunction = (context: Context, args: any[]) => Promise<void> | void;
|
||||||
export type EventHandlerFunction = (context: Context | null, payload: EventPayload) => Promise<void> | void;
|
export type EventHandlerFunction = (context: Context | null, payload: EventPayload) => Promise<void> | void;
|
||||||
|
|
||||||
export type HandlerFunction = CommandHandlerFunction | EventHandlerFunction;
|
export type HandlerWithoutContext = EventHandlerFunction;
|
||||||
export type Handler = HandlerFunction | CommandHandler<readonly ApplicationCommandOption[]>;
|
export type HanderWithContext = CommandHandlerFunction | CommandHandler<readonly ApplicationCommandOption[]>;
|
||||||
|
|
||||||
|
export type Handler = EventHandlerFunction | CommandHandlerFunction | CommandHandler<readonly ApplicationCommandOption[]>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isNil } from '../utils';
|
import { isNil } from '../utils';
|
||||||
|
import { GatewayObject } from './base';
|
||||||
import { fromDiscord, Trait, type Expectation, type Expects } from './common';
|
import { fromDiscord, Trait, type Expectation, type Expects } from './common';
|
||||||
import { Embed } from './embed';
|
import { Embed } from './embed';
|
||||||
import type { Guild } from './guild';
|
import type { Guild } from './guild';
|
||||||
@@ -102,7 +103,7 @@ export type MessagePayload = Message & {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Message {
|
export class Message extends GatewayObject {
|
||||||
id?: string;
|
id?: string;
|
||||||
author?: User;
|
author?: User;
|
||||||
channel_id?: string;
|
channel_id?: string;
|
||||||
@@ -118,15 +119,16 @@ export class Message {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
|
|
||||||
constructor(data?: Partial<Message>) {
|
constructor(data?: Partial<Message>) {
|
||||||
|
super();
|
||||||
this.id = data?.id;
|
this.id = data?.id;
|
||||||
this.author = data?.author;
|
this.author = 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.timestamp = data?.timestamp;
|
this.timestamp = data?.timestamp;
|
||||||
}
|
};
|
||||||
|
|
||||||
public static [Trait.fromDiscord](data: DiscordMessage): MessagePayload {
|
public static [Trait.fromDiscord]<T = MessagePayload>(data: DiscordMessage): T {
|
||||||
return new Message({
|
return new Message({
|
||||||
id: data.id,
|
id: data.id,
|
||||||
author: User[fromDiscord](data.author),
|
author: User[fromDiscord](data.author),
|
||||||
@@ -134,22 +136,22 @@ export class Message {
|
|||||||
guild_id: data.guild_id,
|
guild_id: data.guild_id,
|
||||||
content: data.content,
|
content: data.content,
|
||||||
timestamp: new Date(data.timestamp)
|
timestamp: new Date(data.timestamp)
|
||||||
}) as MessagePayload;
|
}) as T;
|
||||||
}
|
};
|
||||||
|
|
||||||
public static button(config: Button): Component {
|
public static button(config: Button): Component {
|
||||||
return {
|
return {
|
||||||
type: ComponentType.Button,
|
type: ComponentType.Button,
|
||||||
...config
|
...config
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
public static selectMenu(config: SelectMenu): Component {
|
public static selectMenu(config: SelectMenu): Component {
|
||||||
return {
|
return {
|
||||||
type: ComponentType.SelectMenu,
|
type: ComponentType.SelectMenu,
|
||||||
...config
|
...config
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
public setContent = (content: string) => {
|
public setContent = (content: string) => {
|
||||||
this.content = content;
|
this.content = content;
|
||||||
|
|||||||
+9
-4
@@ -1,6 +1,7 @@
|
|||||||
|
import { GatewayObject } from "./base";
|
||||||
import { Trait } from "./common";
|
import { Trait } from "./common";
|
||||||
|
|
||||||
export class User {
|
export class User extends GatewayObject {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
discriminator: string;
|
discriminator: string;
|
||||||
@@ -17,6 +18,7 @@ export class User {
|
|||||||
// TODO: Add the rest of the properties
|
// TODO: Add the rest of the properties
|
||||||
|
|
||||||
constructor(data: Partial<User> & { id: string; username: string; discriminator: string; display_name: string; avatar: string }) {
|
constructor(data: Partial<User> & { id: string; username: string; discriminator: string; display_name: string; avatar: string }) {
|
||||||
|
super();
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.username = data.username;
|
this.username = data.username;
|
||||||
this.discriminator = data.discriminator;
|
this.discriminator = data.discriminator;
|
||||||
@@ -32,7 +34,10 @@ export class User {
|
|||||||
this.email = data.email;
|
this.email = data.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static [Trait.fromDiscord](data: DiscordUser): User {
|
// TODO: Add a clone method to subclasses
|
||||||
|
// public [Trait.clone]<T = User>(data: DiscordUser): T { ... };
|
||||||
|
|
||||||
|
public static [Trait.fromDiscord]<T = User>(data: DiscordUser): T {
|
||||||
return new User({
|
return new User({
|
||||||
id: data.id,
|
id: data.id,
|
||||||
username: data.username,
|
username: data.username,
|
||||||
@@ -47,8 +52,8 @@ export class User {
|
|||||||
locale: data.locale,
|
locale: data.locale,
|
||||||
verified: data.verified,
|
verified: data.verified,
|
||||||
email: data.email
|
email: data.email
|
||||||
});
|
}) as T;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// id snowflake the user's id identify
|
// id snowflake the user's id identify
|
||||||
|
|||||||
+22
-10
@@ -1,4 +1,4 @@
|
|||||||
import { Client, type Context, createCommandHandler, Expectation, Intent, type MessagePayload, Trait } from '../src/index';
|
import { Client, type Context, createCommandHandler, Expectation, Intent, type MessagePayload } from '../src/index';
|
||||||
import { ApplicationCommandOptionType, ApplicationCommandType } from '../src/types/applicationCommand';
|
import { ApplicationCommandOptionType, ApplicationCommandType } from '../src/types/applicationCommand';
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
@@ -13,14 +13,22 @@ const client = new Client({
|
|||||||
prefix: '??'
|
prefix: '??'
|
||||||
});
|
});
|
||||||
|
|
||||||
client.chatCommands.register('ping', async (context: Context, args: any[]) => {
|
// Register arbitrary event handler to the client bus
|
||||||
const message = await context.reply(`Pong!: got ${args.join(', ')}`, true);
|
client.on('READY', async (_, payload) => {
|
||||||
// Calculate the time it took to send the message
|
console.log(`Logged in as ${payload.user.username}${payload.user.discriminator?.length > 0 ? `#${payload.user.discriminator}` : ''}`);
|
||||||
const time = message.timestamp.getTime() - context.message.timestamp.getTime();
|
|
||||||
// Edit the message to include the time
|
|
||||||
await context.editMessage(message, `Pong! Latency: ${time}ms`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register a chat command
|
||||||
|
client.chatCommands.register('ping', async (context: Context, args: any[]) => {
|
||||||
|
const { message } = await context.hydrate(context, [Expectation.Message]);
|
||||||
|
const response = await context.reply(`Pong!: got ${args.join(', ')}`, true);
|
||||||
|
// Calculate the time it took to send the message
|
||||||
|
const time = message.timestamp.getTime() - message.timestamp.getTime();
|
||||||
|
// Edit the message to include the time
|
||||||
|
await context.editMessage(response, `Pong! Latency: ${time}ms`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register an application command
|
||||||
client.applicationCommands.register('hello', ApplicationCommandType.Chat, createCommandHandler({
|
client.applicationCommands.register('hello', ApplicationCommandType.Chat, createCommandHandler({
|
||||||
description: "Introduce yourself",
|
description: "Introduce yourself",
|
||||||
args: [
|
args: [
|
||||||
@@ -56,7 +64,7 @@ client.applicationCommands.register('hello', ApplicationCommandType.Chat, create
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
] as const,
|
] as const,
|
||||||
[Trait.execute]: async (context, args) => {
|
execute: async (context, args) => {
|
||||||
context.reply(
|
context.reply(
|
||||||
`\
|
`\
|
||||||
Hello ${args.name}! I see you are ${args.age} years old${!!args.favorite_animal ? ` and your favorite animal is ${args.favorite_animal}.`: '.'}
|
Hello ${args.name}! I see you are ${args.age} years old${!!args.favorite_animal ? ` and your favorite animal is ${args.favorite_animal}.`: '.'}
|
||||||
@@ -65,9 +73,11 @@ Nice to meet you, I'm ${context.self.username}!`
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Repeated hydration
|
||||||
client.chatCommands.register('me', async (context: Context, _args: any[]) => {
|
client.chatCommands.register('me', async (context: Context, _args: any[]) => {
|
||||||
// Note: Testing repeated hydration
|
// Note: Testing repeated hydration
|
||||||
const hydrated = await context.hydrate(context.message, [Expectation.Channel])
|
const { message } = await context.hydrate(context, [Expectation.Message])
|
||||||
|
const hydrated = await context.hydrate(message, [Expectation.Channel])
|
||||||
const rehydrated = await context.hydrate(hydrated, [Expectation.Guild]);
|
const rehydrated = await context.hydrate(hydrated, [Expectation.Guild]);
|
||||||
|
|
||||||
rehydrated.guild.name
|
rehydrated.guild.name
|
||||||
@@ -84,6 +94,7 @@ client.chatCommands.register('me', async (context: Context, _args: any[]) => {
|
|||||||
// await context.reply(`You are ${message.author.username} ${hasGuild ? message.guild.name : ''}`, true);
|
// await context.reply(`You are ${message.author.username} ${hasGuild ? message.guild.name : ''}`, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Hydrator - hydration function generator
|
||||||
client.chatCommands.register('here', async (context: Context, _args: any[]) => {
|
client.chatCommands.register('here', async (context: Context, _args: any[]) => {
|
||||||
const guild = (await context.hydrator(context, [Expectation.Guild]))(context);
|
const guild = (await context.hydrator(context, [Expectation.Guild]))(context);
|
||||||
|
|
||||||
@@ -94,8 +105,9 @@ client.chatCommands.register('here', async (context: Context, _args: any[]) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Quote command
|
||||||
client.chatCommands.register('quote', async (context: Context, _args: any[]) => {
|
client.chatCommands.register('quote', async (context: Context, _args: any[]) => {
|
||||||
const message = context.message;
|
const { message } = await context.hydrate(context, [Expectation.Message]);
|
||||||
const reference = (await context.hydrator(message, [Expectation.Message]))(message);
|
const reference = (await context.hydrator(message, [Expectation.Message]))(message);
|
||||||
|
|
||||||
let reply: MessagePayload;
|
let reply: MessagePayload;
|
||||||
|
|||||||
Reference in New Issue
Block a user