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",
"type": "module",
"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": {
"@types/bun": "latest"
+10
View File
@@ -1,5 +1,15 @@
// Base classes
export { Client } from './services/client';
export { Intent } from './types/common';
export { BaseHandler, type Handler } from './types/hander';
// Consumable types
export type { ClientConfig } from './types/common';
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) {}
async request<T = any>(method: string, path: string, data?: any): Promise<T> {
const url = `${this.base_url}${path}`;
const url = new URL(path, this.base_url).toString();
const headers = {
Authorization: `Bot ${this.client.token}`,
'Accept': 'application/json',
@@ -29,7 +29,7 @@ export class API {
const body = await response.json();
console.log('API Response:', JSON.stringify(body, null, 4));
// console.log('API Response:', JSON.stringify(body, null, 4));
return body;
}
+13 -15
View File
@@ -3,15 +3,14 @@ import { exists } from "../utils";
import type { API } from "./api";
import type { Processor } from "./processor";
export class ObjectCache<T> {
cache = new Map<string, T>();
private readonly type: string;
cache = new Map<string, Promise<T> | T>();
readonly type: string;
constructor(private api: API, processor: Processor, type: string, events: string[]) {
constructor(protected api: API, processor: Processor, type: string, private readonly factory: (...args: any[]) => Promise<T>, events: string[]) {
this.type = type;
processor.events.register(events, (_context: Context, e: Record<string, any>) => {
this.cache.set(e.id, e as T);
processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
this.cache.set(e.id, factory(e));
});
}
@@ -30,23 +29,22 @@ export class ObjectCache<T> {
// }
// }
async fetch(object_id: string): Promise<T> {
return this.api.get(`${this.type}/${object_id}`).then((res) => {
const object: T & { id: string } = res.data;
this.cache.set(object.id, object);
return object;
});
async fetch(object_id: string, ..._args: any[]): Promise<T> {
const req = this.api.get(`/${this.type}/${object_id}`).then((data) => this.factory(data));
this.cache.set(object_id, req);
req.catch(() => this.cache.delete(object_id));
return await req;
}
get(object_id: string): T | Promise<T> {
async get(object_id: string, ..._args: any[]): Promise<T> {
if (this.cache.has(object_id)) {
return Promise.resolve(this.cache.get(object_id)!);
return await Promise.resolve(this.cache.get(object_id)!);
} else {
return this.fetch(object_id);
}
}
list(): Map<string, T> {
list(): Map<string, Promise<T> | T> {
return this.cache;
}
}
+20 -23
View File
@@ -4,9 +4,10 @@ import { Processor } from './processor';
import { Intent, type ClientConfig } from '../types/common';
import { exists, isNil } from '../utils/index';
import type { Handler } from '../types/hander';
import { Message } from '../types/message';
import { DiscordMessage, Message } from '../types/message';
import { Guilds } from './guilds';
import { Users } from './users';
import { Messages } from './messages';
export class Client {
readonly token: string;
@@ -14,11 +15,12 @@ export class Client {
readonly intents: Intent[] = [Intent.DEFAULT];
prefix: string;
// Internal services
protected _api: API;
protected _processor: Processor;
protected _gateway: Gateway;
protected api: API;
protected processor: Processor;
protected gateway: Gateway;
guilds: Guilds;
users: Users;
messages: Messages
constructor(config: ClientConfig) {
// Validate the configuration
@@ -29,23 +31,12 @@ export class Client {
this.intents = config.intents ?? this.intents;
this.prefix = config.prefix ?? '!';
// Initialize internal services
this._api = new API(this);
this._processor = new Processor(this);
this._gateway = new Gateway(this, this.processor, this.api);
this.api = new API(this);
this.processor = new Processor(this, this.api);
this.gateway = new Gateway(this, this.processor, this.api);
this.guilds = new Guilds(this.api, this.processor);
this.users = new Users(this.api, this.processor);
}
private get api() {
return this._api;
}
private get gateway() {
return this._gateway;
}
private get processor() {
return this._processor;
this.messages = new Messages(this, this.api, this.processor);
}
private static validateConfig(config: ClientConfig) {
@@ -79,13 +70,19 @@ export class Client {
message = new Message().setContent(message);
}
return Message.fromAPIResponse(await this.api.post(`/channels/${channel_id}/messages`, message.toJSON()));
return Message.hydrate(DiscordMessage.fromAPIResponse(await this.api.post(`/channels/${channel_id}/messages`, message.toJSON())), this, this.api);
}
chatCommands = {
register: (name: string | string[], handler: Handler) => this.processor.commands.register(name, handler),
unregister: (name: string | string[]) => this.processor.commands.unregister(name),
has: (name: string) => this.processor.commands.has(name)
register: (name: string | string[], handler: Handler) => this.processor.chat_commands.register(name, handler),
unregister: (name: string | string[]) => this.processor.chat_commands.unregister(name),
has: (name: string) => this.processor.chat_commands.has(name)
}
slashCommands = {
register: (name: string, handler: Handler) => this.processor.slash_commands.register(name, handler),
unregister: (name: string) => this.processor.slash_commands.unregister(name),
has: (name: string) => this.processor.slash_commands.has(name)
}
connect = () => this.gateway.connect();
+13 -11
View File
@@ -5,8 +5,8 @@ import { exists, isNil, isObject, log } from "../utils";
import type { API } from "./api";
import type { Processor } from './processor';
import { Context } from '../types/context';
import { Message } from '../types/message';
import { User } from '../types/user';
import { DiscordMessage, Message } from '../types/message';
import { DiscordUser, User } from '../types/user';
export class Gateway {
private socket: WebSocket | null = null;
@@ -22,7 +22,7 @@ export class Gateway {
log('Connected to gateway');
}
private onMessage = (packet: Data) => {
private onMessage = async (packet: Data) => {
// console.log(packet);
// Allocate a variable to store the parsed payload
let data: Payload | null = null;
@@ -44,7 +44,7 @@ export class Gateway {
const code = data.op;
if (code === 0) {
// Event received
this.parseEvent(data);
await this.parseEvent(data);
} else if (code === 1) {
// Heartbeat requested
this.send({ op: 1, d: this.sequence });
@@ -111,7 +111,7 @@ export class Gateway {
});
};
private parseEvent(payload: Payload) {
private async parseEvent(payload: Payload) {
if (isNil(payload.t)) {
throw new Error('Event name is required');
}
@@ -131,7 +131,7 @@ export class Gateway {
} else if (event_name === 'INTERACTION_CREATE') {
// this.handleInteractionCreate(payload);
} else if (event_name === 'MESSAGE_CREATE') {
this.handleMessageCreate(payload as Payload);
await this.handleMessageCreate(payload as Payload);
} else if (event_name === 'MESSAGE_UPDATE') {
// this.handleMessageCreate(payload);
}
@@ -141,7 +141,8 @@ export class Gateway {
if (['MESSAGE_CREATE', 'MESSAGE_UPDATE'].includes(event_name)) {
// If the event is a message event, create a message object
const message = Message.fromAPIResponse(event_data);
const discord_message = DiscordMessage.fromAPIResponse(event_data);
const message = await Message.hydrate(discord_message, this.client, this.api);
// And then generate the context
context = new Context(this.client, this.api, message);
}
@@ -164,7 +165,7 @@ export class Gateway {
return;
}
// Extract the author
const author = User.fromAPIResponse(payload.d.author);
const author = User.hydrate(DiscordUser.fromAPIResponse(payload.d.author));
// Store the author in the cache
this.client.users.set(author.id, author);
// Extract the content
@@ -185,14 +186,15 @@ export class Gateway {
// Extract the command name
// TODO: Case insensitive config?
const command = args.shift()!;
console.log('payload.d', payload.d)
// Create a new message object from the payload
const message = Message.fromAPIResponse(payload.d);
const message = await Message.hydrate(DiscordMessage.fromAPIResponse(payload.d), this.client, this.api);
// Generate context
const context = new Context(this.client, this.api, message);
// Check if command exists
if (this.processor.commands.has(command)) {
if (this.processor.chat_commands.has(command)) {
// Execute the handler
this.processor.commands.execute(command, context, args);
this.processor.chat_commands.execute(command, context, args);
} else {
// TODO: Custom unknown command handler
context.reply(`Unknown command: ${command}`);
+2 -2
View File
@@ -1,10 +1,10 @@
import type { Guild } from "../types/guild";
import { Guild } from "../types/guild";
import type { API } from "./api";
import { ObjectCache } from "./cache";
import type { Processor } from "./processor";
export class Guilds extends ObjectCache<Guild> {
constructor (api: API, processor: Processor) {
super(api, processor, 'guilds', ['GUILD_CREATE', 'GUILD_UPDATE']);
super(api, processor, 'guilds', Guild.factory, ['GUILD_CREATE', 'GUILD_UPDATE']);
}
}
+26
View File
@@ -0,0 +1,26 @@
import { Message } from "../types/message";
import type { API } from "./api";
import { ObjectCache } from "./cache";
import type { Client } from "./client";
import type { Processor } from "./processor";
export class Messages extends ObjectCache<Message> {
constructor (protected client: Client, api: API, processor: Processor) {
super(api, processor, 'message', (data) => Message.factory(data, client, api), ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
}
async fetch(channel_id: string, message_id: string, resolveReference: boolean = true): Promise<Message> {
const req = this.api.get(`/channels/${channel_id}/messages/${message_id}`).then((data) => Message.factory(data, this.client, this.api, undefined, resolveReference));
this.cache.set(message_id, req);
req.catch(() => this.cache.delete(message_id));
return await req;
}
async get(channel_id: string, message_id: string, resolveReference: boolean = true): Promise<Message> {
if (this.cache.has(message_id)) {
return await Promise.resolve(this.cache.get(message_id)!);
} else {
return this.fetch(channel_id, message_id, resolveReference);
}
}
}
+23 -14
View File
@@ -2,15 +2,17 @@ import type { Client } from "./client";
import type { Context } from "../types/context";
import { HandlerType, type EventPayload, type Payload } from "../types/common";
import { BaseHandler, type Handler } from "../types/hander";
import type { API } from "./api";
export class Processor {
private handlers = {
[HandlerType.Events]: new Map<string, Handler[]>(),
[HandlerType.Commands]: new Map<string, Handler[]>(),
[HandlerType.ChatCommands]: new Map<string, Handler[]>(),
[HandlerType.SlashCommands]: new Map<string, Handler[]>(),
[HandlerType.Interactions]: new Map<string, Handler[]>()
};
constructor(private client: Client) {};
constructor(private client: Client, private api: API) {};
public register(type: HandlerType, event: string | string[], handler: Handler) {
// Normalize events into an array
@@ -46,18 +48,18 @@ export class Processor {
}
};
public execute(type: HandlerType.Events, event: string, context: Context | null, payload: EventPayload): void
public execute(type: Omit<HandlerType, HandlerType.Events>, event: string, context: Context, args: any[]): void
public execute(type: HandlerType, event: string, context: Context | null, argsOrPayload: any[] | EventPayload) {
public async execute(type: HandlerType.Events, event: string, context: Context | null, payload: EventPayload): Promise<void>
public async execute(type: Omit<HandlerType, HandlerType.Events>, event: string, context: Context, args: any[]): Promise<void>
public async execute(type: HandlerType, event: string, context: Context | null, argsOrPayload: any[] | EventPayload) {
const handlers = this.handlers[type].get(event);
if (handlers) {
for (const handler of handlers) {
if (handler instanceof BaseHandler) {
handler.execute(context, argsOrPayload);
await Promise.resolve(handler.execute(context, argsOrPayload));
} else {
// TODO: Fix ts error
handler(context, argsOrPayload);
await Promise.resolve(handler(context, argsOrPayload))
}
}
}
@@ -66,21 +68,28 @@ export class Processor {
[HandlerType.Events] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Events, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.Events, event, handler),
execute: (event: string, context: Context | null, payload: EventPayload) => this.execute(HandlerType.Events, event, context, payload),
execute: async (event: string, context: Context | null, payload: EventPayload) => this.execute(HandlerType.Events, event, context, payload),
has: (event: string) => this.handlers[HandlerType.Events].has(event) && this.handlers[HandlerType.Events].get(event)?.length! > 0
};
[HandlerType.Commands] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Commands, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.Commands, event, handler),
execute: (event: string, context: Context, args: any[]) => this.execute(HandlerType.Commands, event, context, args),
has: (event: string) => this.handlers[HandlerType.Commands].has(event) ?? this.handlers[HandlerType.Commands].get(event)?.length! > 0
[HandlerType.ChatCommands] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.ChatCommands, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.ChatCommands, event, handler),
execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.ChatCommands, event, context, args),
has: (event: string) => this.handlers[HandlerType.ChatCommands].has(event) ?? this.handlers[HandlerType.ChatCommands].get(event)?.length! > 0
};
[HandlerType.SlashCommands] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.SlashCommands, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.SlashCommands, event, handler),
execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.SlashCommands, event, context, args),
has: (event: string) => this.handlers[HandlerType.SlashCommands].has(event) ?? this.handlers[HandlerType.SlashCommands].get(event)?.length! > 0
};
[HandlerType.Interactions] = {
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Interactions, event, handler),
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.Interactions, event, handler),
execute: (event: string, context: Context, args: any[]) => this.execute(HandlerType.Interactions, event, context, args),
execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.Interactions, event, context, args),
has: (event: string) => this.handlers[HandlerType.Interactions].has(event) ?? this.handlers[HandlerType.Interactions].get(event)?.length! > 0
};
}
+2 -1
View File
@@ -10,7 +10,8 @@ export type ClientConfig = {
}
export enum HandlerType {
Events = 'events',
Commands = 'commands',
ChatCommands = 'chat_commands',
SlashCommands = 'slash_commands',
Interactions = 'interactions'
}
+16 -31
View File
@@ -1,7 +1,7 @@
import type { API } from "../services/api";
import type { Client } from "../services/client";
import { isNil } from "../utils";
import { Message } from "./message";
import { DiscordMessage, Message } from "./message";
/**
* Represents the context of a message in a Discord channel.
* Provides methods to interact with the message and the channel it was sent in.
@@ -26,39 +26,27 @@ export class Context {
channel_id: string;
guild_id?: string;
constructor(private client: Client, private api: API, private _message: Message) {
if (isNil(_message.id)) {
constructor(private client: Client, private api: API, public message: Message) {
console.log('Context created: ', );
if (isNil(message.id)) {
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');
}
this.client = client;
this.message_id = _message.id;
this.channel_id = _message.channel_id;
this.guild_id = _message.guild_id;
this.message_id = message.id;
this.channel_id = message.channel_id;
this.guild_id = message.guild_id;
}
// get author() {
// return this.client.users.get(this.author_id);
// };
get message(): Message & { getReference: () => Promise<Message | null> } {
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() {
async fetchGuild() {
if (isNil(this.guild_id)) {
return Promise.resolve(null);
}
@@ -78,11 +66,7 @@ export class Context {
}
if (reference) {
message.message_reference = {
message_id: this.message_id,
channel_id: this.channel_id,
guild_id: this.guild_id
};
message.setReference(this.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.
* @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}`);
};
@@ -119,10 +103,11 @@ export class Context {
* @param {Message | string} message - The updated message content.
* @returns {Promise<Message>} The updated message.
*/
public edit = async (message: Message | string) => {
if (typeof message === 'string') {
message = new Message().setContent(message);
public async editMessage (message: Message, content?: string) {
const clone = new Message(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";
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;
premium_progress_bar_enabled: boolean;
region: string;
@@ -124,7 +165,7 @@ export class Guild {
this.explicit_content_filter = data.explicit_content_filter;
}
static fromAPIResponse(data: any): Guild {
return new Guild(data);
static fromAPIResponse(data: any): DiscordGuild {
return new DiscordGuild(data);
}
}
+3 -3
View File
@@ -4,11 +4,11 @@ import type { Context } from "./context";
export class BaseHandler {
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 EventHandlerFunction = (context: Context | null, payload: EventPayload) => void;
export type CommandHandlerFunction = (context: Context, args: any[]) => Promise<void> | void;
export type EventHandlerFunction = (context: Context | null, payload: EventPayload) => Promise<void> | void;
export type HandlerFunction = CommandHandlerFunction | EventHandlerFunction;
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 { Embed } from './embed';
import { User } from './user';
import { DiscordUser, User } from './user';
export type Component = Partial<Button | SelectMenu> & {
type: ComponentType;
@@ -84,36 +86,42 @@ export class Message {
author?: User;
channel_id?: string;
guild_id?: string;
public content?: string;
public content: string;
public embeds: Embed[] = [];
public components: Component[] = [];
public message_reference?: {
type?: number;
message_id: string;
channel_id: string;
guild_id?: string;
fail_if_not_exists?: boolean;
};
public reference?: Message;
timestamp: Date;
constructor(data?: Partial<Message>) {
if (data) {
this.id = data.id;
this.author = User.fromAPIResponse(data.author);
this.channel_id = data.channel_id;
this.guild_id = data.guild_id;
this.content = data.content;
this.message_reference = data.message_reference;
}
this.id = data?.id;
this.author = data?.author;
this.channel_id = data?.channel_id;
this.guild_id = data?.guild_id;
this.content = data?.content ?? '';
this.reference = data?.reference;
this.timestamp = data?.timestamp ?? new Date();
}
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);
}
public static fromAPIResponse = (data: any) => {
return new Message({
id: data.id,
author: data.author,
channel_id: data.channel_id,
guild_id: data.guild_id,
content: data.content,
message_reference: data.message_reference
id: discord_message.id,
author: User.hydrate(discord_message.author),
channel_id: discord_message.channel_id,
guild_id: discord_message?.guild_id,
content: discord_message.content,
reference,
timestamp: new Date(discord_message.timestamp)
});
}
@@ -157,17 +165,20 @@ export class Message {
return this;
};
public setReference = (message: Partial<Message>) => {
public setReference = (message: Message | Partial<Message>) => {
if (isNil(message.id) || isNil(message.channel_id)) {
throw new Error('Message ID and Channel ID are required');
}
this.message_reference = {
message_id: message.id,
channel_id: message.channel_id,
guild_id: message.guild_id,
fail_if_not_exists: false
};
let ref: Message;
if (!(message instanceof Message)) {
ref = new Message(message);
} else {
ref = message;
}
this.reference = ref;
return this;
};
@@ -176,7 +187,127 @@ export class Message {
content: this.content,
embeds: this.embeds,
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
// username string the user's username, not unique across the platform 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
// avatar_decoration_data?
export class User {
export class DiscordUser {
id: string;
username: string;
discriminator: string;
@@ -35,7 +90,7 @@ export class User {
public_flags?: number;
avatar_decoration_data?: Record<string, any>;
constructor(data: User) {
constructor(data: DiscordUser) {
this.id = data.id;
this.username = data.username;
this.discriminator = data.discriminator;
@@ -55,8 +110,8 @@ export class User {
this.avatar_decoration_data = data.avatar_decoration_data;
}
static fromAPIResponse(data: any): User {
return new User({
static fromAPIResponse(data: any): DiscordUser {
return new DiscordUser({
id: data.id,
username: data.username,
discriminator: data.discriminator,
+23 -8
View File
@@ -13,25 +13,40 @@ const client = new Client({
});
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[]) => {
const guild = await context.fetchGuild();
if (guild !== null) {
await context.reply(`You are in ${guild?.name}`, true);
await context.reply(`You are in ${guild.name}`, true);
} 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[]) => {
const referenced = await context.message.getReference();
if (referenced === null) {
await context.reply('No message to quote', true);
client.chatCommands.register('quote', async (context: Context, _args: any[]) => {
const referenced = context.message.reference;
let reply = null;
if (referenced === null || referenced === undefined) {
reply = await context.reply('No message to quote', true);
return;
} 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);
}
});