init for rewrite

This commit is contained in:
Jaron
2024-11-10 01:55:07 -06:00
parent 57a787e7ec
commit 56e10f043a
41 changed files with 1329 additions and 1673 deletions
+5 -177
View File
@@ -1,177 +1,5 @@
import { ComponentStyle, Context, Gateway, Guilds, Message, Embed } from './lib';
import { SlashCommand, SlashCommandFunction, ChatCommand, Command, CommandFunction, Intent, Presence } from './types';
export {
ComponentStyle,
Context,
Gateway,
Guilds,
Message,
Embed,
Command,
CommandFunction,
ChatCommand,
Intent,
Presence
};
type Config = {
token: string;
application_id: string;
prefix?: string;
intents?: Intent[];
};
export class Client {
readonly prefix: string;
readonly token: string;
readonly application_id: string;
private readonly gateway: Gateway;
private readonly client_commands: Map<string, ChatCommand | CommandFunction> = new Map();
private readonly slash_commands: Map<string, SlashCommand | SlashCommandFunction> = new Map();
readonly guilds: Guilds;
constructor({ token, application_id, prefix, intents }: Config) {
this.token = token;
this.application_id = application_id;
this.prefix = prefix ?? '!';
this.gateway = new Gateway({ token, intents, client: this });
this.guilds = new Guilds({ token, gateway: this.gateway });
// Do some default stuff
this.initialize();
}
get user() {
return this.gateway.user;
}
get handlers() {
return this.gateway.handlers;
}
get interactions() {
return this.gateway.interactions;
}
get api() {
return this.gateway.api;
}
public connect = () => {
this.gateway.connect();
return this;
};
public reconnect = () => {
this.gateway.reconnect();
return this;
};
public setPresence = (presence: Presence) => {
this.gateway.setPresence(presence);
return this;
};
send = (channel_id: string, message: Message | string) => {
if (typeof message === 'string') {
message = new Message().setContent(message);
}
return this.api.post(`/channels/${channel_id}/messages`, message);
};
public commands = {
chat: {
register: (command: ChatCommand | string, handler?: CommandFunction) => {
if (typeof command === 'string') {
this.client_commands.set(command, handler);
} else {
this.client_commands.set(command.name, command);
}
},
get: (command_name: string) => {
return this.client_commands.get(command_name);
},
execute: (command_name: string, context: Context, ...args: any[]) => {
const command = this.client_commands.get(command_name);
if (command) {
command instanceof ChatCommand
? command.execute(this, context, args.slice(1))
: command(this, context, args.slice(1));
}
},
delete: (command_name: string) => {
this.client_commands.delete(command_name);
},
list: () => {
return Array.from(this.client_commands.entries());
},
map: () => {
return this.client_commands;
}
},
application: {
register: (command: SlashCommand | Record<string, string>, handler?: SlashCommandFunction) => {
const payload =
command instanceof SlashCommand
? command.toJSON()
: {
name: command.name,
description: command.description,
type: command.type ?? 1
};
this.api
.post(`/applications/${this.application_id}/commands`, payload)
.then((response) => {
console.log(response);
if (command instanceof SlashCommand) {
this.slash_commands.set(command.name, command);
} else {
this.slash_commands.set(command.name, handler);
}
})
.catch((error) => {
this.log.error('Error registering application command', error);
});
},
get: (command_name: string) => {
return this.slash_commands.get(command_name);
},
list: () => {
return Array.from(this.slash_commands.entries());
},
map: () => {
return this.slash_commands;
}
}
};
private log = {
error: (error_or_message: Error | string, error?: Error) => {
if (error && typeof error_or_message === 'string') {
console.error(`[discord.ts] ${error_or_message}:`, error);
} else {
console.error('[discord.ts] Error:', error_or_message);
}
}
};
private initialize = () => {
// Register default help command
this.commands.chat.register('help', (client, context, _args) => {
const embed = new Embed()
.setTitle('Commands')
.setDescription('This is a list of commands you can use.')
.setColor('#f6c221');
const commands = client.commands.chat.list();
for (const [name, command] of commands) {
embed.addField({
name: name,
value: command instanceof ChatCommand ? command.description : 'No description provided'
});
}
context.reply(embed.toMessage());
});
};
}
export { Client } from './services/client';
export { Intent } from './types/common';
export { BaseHandler, type Handler } from './types/hander';
export type { ClientConfig } from './types/common';
export type { Context } from './types/context';
-37
View File
@@ -1,37 +0,0 @@
import { Context, Gateway } from '.';
export class ObjectCache<T> {
cache = new Map<string, T>();
private readonly token: string;
private readonly type: string;
private readonly gateway: Gateway;
constructor({ token, gateway, type, events }: { token: string; gateway: Gateway; type: string; events: string[] }) {
this.token = token;
this.type = type;
this.gateway = gateway;
gateway.handlers.register(events, (_client, _context: Context, e: Record<string, any>) => {
this.cache.set(e.id, e as T);
});
}
async fetch(object_id: string): Promise<T> {
return this.gateway.api.get(`${this.type}/${object_id}`).then((res) => {
const object: T & { id: string } = res.data;
this.cache.set(object.id, object);
return object;
});
}
get(object_id: string): T | Promise<T> {
if (this.cache.has(object_id)) {
return Promise.resolve(this.cache.get(object_id)!);
} else {
return this.fetch(object_id);
}
}
list(): Map<string, T> {
return this.cache;
}
}
-52
View File
@@ -1,52 +0,0 @@
import { Client } from '..';
import { Message } from '../lib';
export class Context {
private readonly client: Client;
// Public properties
message_id: string;
channel_id: string;
guild_id: string;
constructor({ client, message }: { client: Client; message: Partial<Message> }) {
this.client = client;
this.message_id = message.id;
this.channel_id = message.channel_id;
this.guild_id = message.guild_id;
}
get guild() {
return this.client.guilds.get(this.guild_id);
}
/**
* Reply to the message that this command
* @param message The message to send to the channel
* @param reference If true the message will be a reference the the message that triggered the command
* @returns The message that was sent
*/
public reply = (message: Message | string, reference: boolean = false) => {
if (typeof message === 'string') {
message = new Message().setContent(message);
}
if (reference) {
message.message_reference = {
message_id: this.message_id,
channel_id: this.channel_id,
guild_id: this.guild_id
};
}
return this.client.send(this.channel_id, message);
};
public delete = (message_id?: string) => {
return this.client.api.delete(`/channels/${this.channel_id}/messages/${message_id ?? this.message_id}`);
};
public edit = (message: Message | string) => {
if (typeof message === 'string') {
message = new Message().setContent(message);
}
return this.client.api.patch(`/channels/${this.channel_id}/messages/${this.message_id}`, message);
};
}
-374
View File
@@ -1,374 +0,0 @@
import { WebSocket, Data } from 'ws';
import axios, { AxiosInstance } from 'axios';
import { Client } from '..';
import {
Session,
User,
Payload,
Intent,
Handler,
HandlerFunction,
Command,
Status,
Presence,
ActivityType,
CommandFunction,
Interaction,
InteractionFunction,
SlashCommand
} from '../types';
import { Context } from './context';
import { InteractionContext } from './interaction';
export class Gateway {
socket: WebSocket;
axios: AxiosInstance;
// Config
readonly token: string;
readonly client: Client;
private intents: number = 513;
private commandNotFound: CommandFunction = null;
// Discord gateway data
private readonly url: string = 'wss://gateway.discord.gg/?v=10&encoding=json';
private heartbeat_timeout: NodeJS.Timeout;
private heartbeat_function: NodeJS.Timer;
private heartbeat_interval: number | null = null;
private session: Session = null;
public user: User = null;
// Utils
public event_handlers: Map<string, Array<Handler | HandlerFunction>> = new Map();
public interaction_handlers: Map<string, Array<Interaction | InteractionFunction>> = new Map();
constructor({
token,
intents,
client,
commandNotFound
}: {
token: string;
intents?: Intent[];
client: Client;
commandNotFound?: CommandFunction;
}) {
// Copy config
this.token = token;
this.client = client;
this.commandNotFound = commandNotFound;
// Initialize socket
this.socket = new WebSocket(this.url);
// Initialize axios
this.axios = axios.create({
headers: {
'User-Agent': 'DiscordBot (discord.ts, 0.0.1)',
Authorization: `Bot ${this.token}`
},
baseURL: 'https://discord.com/api/v10'
});
this.axios.interceptors.response.use((response) => response.data);
// Calculate intents
if (intents) {
this.intents = intents.reduce((acc, intent) => acc | intent, 0);
}
}
public connect = () => {
// Initialize socket
this.socket = new WebSocket(this.url);
// Register the listeners
this.socket.on('open', this.onOpen);
this.socket.on('message', this.onMessage);
// this.socket.on('close', this.onClose);
// this.socket.on('error', this.onError);
return this;
};
public reconnect = () => {
this.socket.terminate();
return this.connect();
};
public setPresence = (presence: Presence) => {
this.send({
op: 3,
d: presence
});
};
public handlers = {
register: (events: string | string[], handler: Handler | HandlerFunction) => {
this.registerHandler(events, handler, 'command');
},
get: (event: string) => {
return this.event_handlers.get(event);
}
};
public interactions = {
register: (custom_ids: string | string[], handler: Interaction | InteractionFunction) => {
this.registerHandler(custom_ids, handler, 'interaction');
},
get: (event: string) => {
return this.interaction_handlers.get(event);
}
};
public api = {
get: (url: string, params?: any) => {
return this.axios.get<unknown, Record<string, any>>(url, { params });
},
post: (url: string, data?: any) => {
return this.axios.post<unknown, Record<string, any>>(url, data);
},
patch: (url: string, data?: any) => {
return this.axios.patch<unknown, Record<string, any>>(url, data);
},
delete: (url: string, data?: any) => {
return this.axios.delete<unknown, Record<string, any>>(url, data);
}
};
// prettier-ignore
private registerHandler(events: string | string[], handler: Handler | HandlerFunction, type: 'command'): void;
// prettier-ignore
private registerHandler(events: string | string[], handler: Interaction | InteractionFunction, type: 'interaction'): void;
private registerHandler(events: string | string[], handler: any, type: 'command' | 'interaction') {
const handler_map = type === 'command' ? this.event_handlers : this.interaction_handlers;
if (Array.isArray(events)) {
// If Array register handler for each event
for (const event of events) {
const current_handlers = handler_map.get(event) ?? [];
handler_map.set(event, [...current_handlers, handler]);
}
} else {
// Otherwise register for the single event
const current_handlers = handler_map.get(events) ?? [];
handler_map.set(events, [...current_handlers, handler]);
}
}
private send = (payload: Partial<Payload>) => {
payload.s = this.session?.s ?? null;
console.log('sending: %s', payload);
this.socket.send(JSON.stringify(payload));
};
private indentify = () => {
this.send({
op: 2,
d: {
token: this.token,
intents: this.intents,
properties: {
os: process.platform,
browser: 'discord.ts',
device: 'discord.ts'
}
}
});
};
private heartbeat = () => {
if (!this.heartbeat_interval) {
throw new Error('No heartbeat interval set');
}
if (this.heartbeat_function) {
clearInterval(this.heartbeat_function);
}
if (this.heartbeat_timeout) {
clearTimeout(this.heartbeat_timeout);
}
this.heartbeat_function = setInterval(() => {
this.send({
op: 1,
d: {}
});
}, this.heartbeat_interval);
this.heartbeat_timeout = setTimeout(() => {
this.reconnect();
}, this.heartbeat_interval * 2);
};
private initialize = (payload: Payload) => {
this.session = payload.d.session;
this.user = payload.d.user;
this.user.tag = `${this.user.username}#${this.user.discriminator}`;
// Construct presence
const status: Presence = {
status: Status.Online,
since: null,
activities: [
{
name: payload.d.guilds.length + ' servers',
type: ActivityType.Watching,
created_at: Date.now()
}
],
afk: false
};
// Set presence
this.setPresence(status);
// Cache guilds? (these are not whole guilds, just guild ids)
// for (const guild of payload.d.guilds) {
// this.guilds.set(guild.id, guild);
// }
console.log('session: %s', this.session);
console.log('user: %s', this.user);
};
private parseEvent = (payload: Payload) => {
// If not a dispatch event, return
if (!payload.t) {
return;
}
// Run default handlers
switch (payload.t) {
case 'READY':
this.initialize(payload);
break;
case 'INTERACTION_CREATE':
this.handleInteractionCreate(payload);
break;
case 'MESSAGE_CREATE':
this.handleMessageCreate(payload);
break;
case 'MESSAGE_UPDATE':
this.handleMessageCreate(payload);
break;
default:
break;
}
// Call event handlers
const handlers = this.handlers.get(payload.t);
const context = new Context({ client: this.client, message: payload.d });
if (handlers) {
for (const handler of handlers) {
handler instanceof Handler
? handler.execute(this.client, context, payload.d)
: handler(this.client, context, payload.d);
}
}
};
private handleInteractionCreate = (payload: Payload) => {
if (payload.d.type === 2) {
this.handleShashInteraction(payload);
} else {
this.handleChatInteraction(payload);
}
};
private handleShashInteraction = (payload: Payload) => {
console.log(payload.d);
const context = new InteractionContext({ client: this.client, interaction: payload.d });
const handler = this.client.commands.application.get(payload.d.data.name);
if (handler) {
if (handler instanceof SlashCommand) {
handler.execute(this.client, context, payload.d);
} else {
handler(this.client, context, payload.d);
}
}
};
private handleChatInteraction = (payload: Payload) => {
const context = new InteractionContext({ client: this.client, interaction: payload.d });
const handlers = this.client.interactions.get(payload.d.data.custom_id);
if (handlers) {
for (const handler of handlers) {
if (handler instanceof Interaction) {
handler.execute(this.client, context, payload.d);
} else {
handler(this.client, context, payload.d);
}
}
}
};
private handleMessageCreate = async (payload: Payload) => {
// Ignore bots
if (payload.d.author.bot) {
return;
}
console.log(payload.d);
const content = payload.d.content.trim();
// Is this a command?
if (content.startsWith(this.client.prefix) | content.startsWith(`<@${this.client.user.id}>`)) {
// Parse command
const args = content
.slice(this.client.prefix.length)
.trim()
.split(/\s+(?=(?:[^\'"]*[\'"][^\'"]*[\'"])*[^\'"]*$)/g);
// Check if command exists
const handler = this.client.commands.chat.get(args[0]);
// Generate context
const context = new Context({ client: this.client, message: payload.d });
if (handler) {
// Execute command
handler instanceof Command
? handler.execute(this.client, context, args.slice(1))
: handler(this.client, context, args.slice(1));
} else {
let reply = null;
// Send error message if command does not exist
if (this.commandNotFound) {
// If custom command not found handler is set, use that
reply = this.commandNotFound(this.client, context);
} else {
// Otherwise send default message
reply = await context.reply('Command not found :(', true);
}
if (reply) {
// If reply is set, delete it after 3 seconds
setTimeout(() => {
context.delete(reply.id);
}, 3000);
}
}
}
};
private parseOpCode = (payload: Payload) => {
switch (payload.op) {
case 0:
this.parseEvent(payload);
break;
case 9:
setTimeout(this.reconnect, 500);
break;
case 10:
this.heartbeat_interval = payload.d.heartbeat_interval;
this.heartbeat();
this.indentify();
break;
case 11:
console.log('heartbeat acknowledged');
this.heartbeat_timeout.refresh();
break;
default:
break;
}
};
private onMessage = (packet: Data): void => {
console.log('============================');
let data: Payload | null = null;
try {
data = JSON.parse(packet.toString());
} catch (e) {
console.error(e);
}
if (data) {
console.log('received: %s', data);
console.log('received data: %s', data.d);
if (this.session && data.s && data.s > this.session.s) {
this.session.s = data.s;
} else {
this.parseOpCode(data);
}
}
};
private onOpen = () => {
console.log('Socket open');
};
}
-8
View File
@@ -1,8 +0,0 @@
import { Gateway, ObjectCache } from ".";
import { Guild } from "../types";
export class Guilds extends ObjectCache<Guild> {
constructor ({ token, gateway }: { token: string, gateway: Gateway }) {
super({token, gateway, type: 'guilds', events: ['GUILD_CREATE', 'GUILD_UPDATE']});
}
}
-7
View File
@@ -1,7 +0,0 @@
export * from './gateway';
export * from './cache';
export * from './guilds';
export * from './context';
export * from './message';
export * from './embed';
export * from './interaction';
-61
View File
@@ -1,61 +0,0 @@
import { Client } from "..";
import { InteractionResponseType } from "../types";
import { Message } from "./message";
export class InteractionContext {
public client: Client;
public interaction: Record<string, any>;
private is_loading: boolean;
constructor({ client, interaction, is_loading = false }: { client: Client; interaction: Record<string, any>, is_loading?: boolean }) {
this.client = client;
this.interaction = interaction;
this.is_loading = is_loading;
}
ack = (type: InteractionResponseType = InteractionResponseType.Pong, data?: Record<string, any>) => {
return this.client.api.post(`/interactions/${this.interaction.id}/${this.interaction.token}/callback`, {
type,
data
});
};
reply = (message: Message | string) => {
if (typeof message === 'string') {
message = new Message().setContent(message);
}
return this.ack(InteractionResponseType.Reply, {
...message
});
};
edit = (message: Message | string) => {
if (typeof message === 'string') {
message = new Message().setContent(message);
}
if (!this.is_loading) {
return this.ack(InteractionResponseType.UpdateMessage, {
...message
});
} else {
return this.client.api.patch(
`/webhooks/${this.client.application_id}/${this.interaction.token}/messages/@original`,
{
type: InteractionResponseType.DeferredUpdateMessage,
...message
}
);
}
};
deleteOriginal = () => {
return this.client.api.delete(
`/webhooks/${this.client.application_id}/${this.interaction.token}/messages/@original`
);
};
loading = async () => {
await this.ack(InteractionResponseType.DeferredReply);
return new InteractionContext({ client: this.client, interaction: this.interaction, is_loading: true });
};
}
+52
View File
@@ -0,0 +1,52 @@
import type { Client } from "./client";
export class API {
private readonly base_url: string = 'https://discord.com/api/v10';
constructor(private client: Client) {}
async request<T = any>(method: string, path: string, data?: any): Promise<T> {
const url = `${this.base_url}${path}`;
const headers = {
Authorization: `Bot ${this.client.token}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
};
console.log(`API Request: ${method} ${url}`);
const response = await fetch(url, {
method,
headers,
body: JSON.stringify(data),
});
if (!response.ok) {
console.error(`Failed to fetch: ${response.statusText} ${response.url} ${response.status}`);
console.error(response.json());
throw new Error(`Failed to fetch: ${response.statusText}`);
}
const body = await response.json();
console.log('API Response:', JSON.stringify(body, null, 4));
return body;
}
async get<T = any>(path: string): Promise<T> {
return this.request('GET', path);
}
async post<T = any>(path: string, data: any): Promise<T> {
return this.request('POST', path, data);
}
async patch<T = any>(path: string, data: any): Promise<T> {
return this.request('PATCH', path, data);
}
async delete<T = any>(path: string): Promise<T> {
return this.request('DELETE', path);
}
}
+52
View File
@@ -0,0 +1,52 @@
import type { Context } from "../types/context";
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;
constructor(private api: API, processor: Processor, type: string, events: string[]) {
this.type = type;
processor.events.register(events, (_context: Context, e: Record<string, any>) => {
this.cache.set(e.id, e as T);
});
}
set(object_id: string, object: T): void {
this.cache.set(object_id, object);
}
// update(object_id: string, object: Partial<T>): void {
// const existing = this.cache.get(object_id);
// if (exists(existing)) {
// // TODO: Idk wtf happens if this is called on two classes
// this.cache.set(object_id, { ...existing, ...object });
// } else {
// this.cache.set(object_id, object as 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;
});
}
get(object_id: string): T | Promise<T> {
if (this.cache.has(object_id)) {
return Promise.resolve(this.cache.get(object_id)!);
} else {
return this.fetch(object_id);
}
}
list(): Map<string, T> {
return this.cache;
}
}
+92
View File
@@ -0,0 +1,92 @@
import { API } from './api';
import { Gateway } from './gateway';
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 { Guilds } from './guilds';
import { Users } from './users';
export class Client {
readonly token: string;
readonly application_id: string;
readonly intents: Intent[] = [Intent.DEFAULT];
prefix: string;
// Internal services
protected _api: API;
protected _processor: Processor;
protected _gateway: Gateway;
guilds: Guilds;
users: Users;
constructor(config: ClientConfig) {
// Validate the configuration
Client.validateConfig(config);
// Save the configuration
this.token = config.token;
this.application_id = config.application_id;
this.intents = config.intents ?? this.intents;
this.prefix = config.prefix ?? '!';
// Initialize internal services
this._api = new API(this);
this._processor = new Processor(this);
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;
}
private static validateConfig(config: ClientConfig) {
if (isNil(config.token)) {
throw new Error('Token is required');
}
if (isNil(config.application_id)) {
throw new Error('Application ID is required');
}
if (exists(config.prefix) && typeof config.prefix !== 'string') {
throw new Error('Prefix must be a string');
}
if (exists(config.intents)) {
if (!Array.isArray(config.intents)) {
throw new Error('Intents must be an array');
}
for (const intent of config.intents) {
if (!Intent[intent]) {
throw new Error(`Invalid intent: ${intent}`);
}
}
}
}
sendMessage = async (channel_id: string, message: Message | string) => {
if (typeof message === 'string') {
message = new Message().setContent(message);
}
return Message.fromAPIResponse(await this.api.post(`/channels/${channel_id}/messages`, message.toJSON()));
}
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)
}
connect = () => this.gateway.connect();
}
+216
View File
@@ -0,0 +1,216 @@
import { WebSocket, type Data } from 'ws';
import type { Client } from "./client";
import type { Payload } from "../types/common";
import { exists, isNil, isObject, log } from "../utils";
import type { API } from "./api";
import type { Processor } from './processor';
import { Context } from '../types/context';
import { Message } from '../types/message';
import { User } from '../types/user';
export class Gateway {
private socket: WebSocket | null = null;
private heartbeat_interval: number | null = null;
private heartbeat_timeout: Timer | null = null;
private sequence: number | null = null;
// Internal state
private user: any;
constructor(private client: Client, private processor: Processor, private api: API) {}
private onOpen = () => {
log('Connected to gateway');
}
private onMessage = (packet: Data) => {
// console.log(packet);
// Allocate a variable to store the parsed payload
let data: Payload | null = null;
try {
// Attempt to parse the packet data
data = JSON.parse(packet.toString());
} catch (e) {
console.error(e);
}
if (data) {
log('='.repeat(25));
log('received: ', data.op);
log('received event: ', data.t ?? 'no event');
log('received data: ', exists(data.d) ? Object.keys(data.d) : 'no data');
log('='.repeat(25));
// If the data is successfully parsed check the opcode
const code = data.op;
if (code === 0) {
// Event received
this.parseEvent(data);
} else if (code === 1) {
// Heartbeat requested
this.send({ op: 1, d: this.sequence });
} else if (code === 9) {
// Reconnect requested
} else if (code === 10) {
// Hello
if (!isObject(data.d)) {
throw new Error('Invalid HELLO payload received');
}
// Store the heartbeat interval
this.heartbeat_interval = data.d?.heartbeat_interval;
// Kick off the heartbeat loop
this.heartbeat();
setTimeout(this.indentify, 2500);
} else if (code === 11) {
// Heartbeat ACK
log('Heartbeat ACK');
}
}
}
private heartbeat() {
log('Sending heartbeat');
if (exists(this.heartbeat_interval)) {
// If a previous heartbeat timeout exists, clear it
if (exists(this.heartbeat_timeout)) {
clearTimeout(this.heartbeat_timeout);
}
// Set a timeout to ensure the heartbeat is sent within the interval
this.heartbeat_timeout = setTimeout(() => {
this.send({ op: 1, d: this.sequence });
this.heartbeat();
}, this.heartbeat_interval * Math.random());
} else {
throw new Error('Heartbeat interval not set');
}
}
protected send = (payload: Partial<Payload>) => {
if (isNil(this.socket)) {
throw new Error('Socket is not connected');
}
log('='.repeat(25));
log('sending: ', JSON.stringify(payload, null, 4));
log('='.repeat(25));
this.socket.send(JSON.stringify(payload));
};
private indentify = () => {
this.send({
op: 2,
d: {
token: this.client.token,
intents: this.client.intents.reduce((acc, intent) => acc | intent, 0),
properties: {
os: process.platform,
browser: 'discord.ts',
device: 'discord.ts'
}
}
});
};
private parseEvent(payload: Payload) {
if (isNil(payload.t)) {
throw new Error('Event name is required');
}
log('Event:', payload.t);
const event_name = payload.t;
const event_data = payload.d;
if (!isObject(event_data)) {
throw new Error('Invalid event data received');
}
if (event_name === 'READY') {
this.user = event_data.user;
log('User:', this.user);
} else if (event_name === 'INTERACTION_CREATE') {
// this.handleInteractionCreate(payload);
} else if (event_name === 'MESSAGE_CREATE') {
this.handleMessageCreate(payload as Payload);
} else if (event_name === 'MESSAGE_UPDATE') {
// this.handleMessageCreate(payload);
}
// Allocate a var to store the context
let context = null;
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);
// And then generate the context
context = new Context(this.client, this.api, message);
}
// Call event handlers
this.processor.events.execute(event_name, context, event_data);
}
private handleMessageCreate = async (payload: Payload) => {
if (!isObject(payload.d)) {
throw new Error('Invalid MESSAGE_CREATE payload received');
}
// Ignore bots
if (payload.d.author.bot) {
return;
}
// Ignore self
if (payload.d.author.id === this.user.id) {
return;
}
// Extract the author
const author = User.fromAPIResponse(payload.d.author);
// Store the author in the cache
this.client.users.set(author.id, author);
// Extract the content
const content = payload.d.content.trim();
// Is this a command?
if (content.startsWith(this.client.prefix) || content.startsWith(`<@${this.user.id}>`)) {
// Determine the prefix
const prefix = content.startsWith(this.client.prefix) ? this.client.prefix : `<@${this.user.id}> `;
// Parse command
const args = content
.slice(prefix.length)
.trim()
.split(/\s+(?=(?:[^\'"]*[\'"][^\'"]*[\'"])*[^\'"]*$)/g);
// Ignore empty commands
if (args.length === 0) {
return;
}
// Extract the command name
// TODO: Case insensitive config?
const command = args.shift()!;
// Create a new message object from the payload
const message = Message.fromAPIResponse(payload.d);
// Generate context
const context = new Context(this.client, this.api, message);
// Check if command exists
if (this.processor.commands.has(command)) {
// Execute the handler
this.processor.commands.execute(command, context, args);
} else {
// TODO: Custom unknown command handler
context.reply(`Unknown command: ${command}`);
}
}
};
async connect() {
// Fetch the Discord gateway URL
const response = await this.api.request('GET', '/gateway/bot');
// Append the query parameters to the URL
const url = new URL(response.url);
url.searchParams.append('v', '10');
url.searchParams.append('encoding', 'json');
// Open a new WebSocket connection to the Discord gateway
this.socket = new WebSocket(url);
// Register event listeners
this.socket.on('open', this.onOpen);
this.socket.on('message', this.onMessage);
}
}
+10
View File
@@ -0,0 +1,10 @@
import type { 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']);
}
}
+86
View File
@@ -0,0 +1,86 @@
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";
export class Processor {
private handlers = {
[HandlerType.Events]: new Map<string, Handler[]>(),
[HandlerType.Commands]: new Map<string, Handler[]>(),
[HandlerType.Interactions]: new Map<string, Handler[]>()
};
constructor(private client: Client) {};
public register(type: HandlerType, event: string | string[], handler: Handler) {
// Normalize events into an array
const events = Array.isArray(event) ? event : [event];
// Register the handler for each event
for (const e of events) {
const handlers = this.handlers[type].get(e) || [];
handlers.push(handler);
this.handlers[type].set(e, handlers);
}
};
public unregister(type: HandlerType, event: string | string[], handler?: Handler) {
// Normalize events into an array
const events = Array.isArray(event) ? event : [event];
// Unregister the handler for each event
for (const e of events) {
if (handler) {
// Get the handlers for the event
const handlers = this.handlers[type].get(e);
if (handlers) {
// Find the index of the handler
const index = handlers.indexOf(handler);
if (index !== -1) {
// Remove the handler from the array
handlers.splice(index, 1);
}
}
} else {
// If no handler is provided, clear all handlers for the event
this.handlers[type].clear();
}
}
};
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) {
const handlers = this.handlers[type].get(event);
if (handlers) {
for (const handler of handlers) {
if (handler instanceof BaseHandler) {
handler.execute(context, argsOrPayload);
} else {
// TODO: Fix ts error
handler(context, argsOrPayload);
}
}
}
}
[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),
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.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),
has: (event: string) => this.handlers[HandlerType.Interactions].has(event) ?? this.handlers[HandlerType.Interactions].get(event)?.length! > 0
};
}
+10
View File
@@ -0,0 +1,10 @@
import type { User } from "../types/user";
import type { API } from "./api";
import { ObjectCache } from "./cache";
import type { Processor } from "./processor";
export class Users extends ObjectCache<User> {
constructor (api: API, processor: Processor) {
super(api, processor, 'users', []);
}
}
+39
View File
@@ -0,0 +1,39 @@
export class Channel {
version: number;
type: number;
position: number;
permission_overwrites: any[];
name: string;
id: string;
flags: number;
topic?: string | null;
rate_limit_per_user?: number;
parent_id?: string;
last_message_id?: string | null;
user_limit?: number;
rtc_region?: string | null;
bitrate?: number;
last_pin_timestamp?: string;
constructor(data: any) {
this.version = data.version;
this.type = data.type;
this.position = data.position;
this.permission_overwrites = data.permission_overwrites;
this.name = data.name;
this.id = data.id;
this.flags = data.flags;
this.topic = data.topic;
this.rate_limit_per_user = data.rate_limit_per_user;
this.parent_id = data.parent_id;
this.last_message_id = data.last_message_id;
this.user_limit = data.user_limit;
this.rtc_region = data.rtc_region;
this.bitrate = data.bitrate;
this.last_pin_timestamp = data.last_pin_timestamp;
}
static fromAPIResponse(data: any): Channel {
return new Channel(data);
}
}
-21
View File
@@ -1,21 +0,0 @@
import { Client } from '..';
import { Context } from '../lib';
export abstract class Command {
readonly name: string;
readonly description: string;
readonly execute: CommandFunction;
constructor(data: Command) {
this.name = data.name;
this.description = data.description;
this.execute = data.execute;
}
}
export type CommandFunction = (client: Client, context: Context, ...args: any[]) => void;
export class ChatCommand extends Command {
constructor(data: ChatCommand) {
super(data);
}
}
+35 -76
View File
@@ -1,81 +1,40 @@
import type { Client } from "../services/client";
import type { Context } from "./context";
import type { BaseHandler } from "./hander";
export type ClientConfig = {
token: string;
application_id: string;
prefix?: string;
intents?: Intent[];
}
export enum HandlerType {
Events = 'events',
Commands = 'commands',
Interactions = 'interactions'
}
export type EventPayload = any;
export type Primitive = string | number | boolean | null;
/**
* Represents a payload structure used in the Discord API.
*
* @typedef {Object} Payload
* @property {number} op - The opcode of the payload.
* @property {Record<string, any>} d - The data associated with the payload.
* @property {number | null} s - The sequence number, or null if not applicable.
* @property {string | null} t - The event name, or null if not applicable.
*/
export type Payload = {
op: number; // opcode
d: Record<string, any>; // data
s: number | null; // sequence number
t: string | null; // event name
op: number;
d: Record<string, any> | Primitive | null;
s: number | null;
t: string | null;
};
export type Session = {
s: number; // sequence number
session_id: string;
session_type: string;
resume_gateway_url: string;
};
export type Guild = Record<string, any>;
export type User = Record<string, any>;
export enum Status {
Online = "online",
DND = "dnd",
Idle = "idle",
Invisible = "invisible",
Offline = "offline"
}
export type Presence = {
since: number | null;
activities: Activity[];
status: Status;
afk: boolean;
}
export type Activity = {
name: string;
type: number;
created_at: number;
url?: string;
timestamps?: {
start?: number;
end?: number;
};
application_id?: string;
details?: string;
state?: string;
emoji?: {
name: string;
id?: string;
animated?: boolean;
};
party?: {
id?: string;
size?: [number, number];
};
assets?: {
large_image?: string;
large_text?: string;
small_image?: string;
small_text?: string;
};
secrets?: {
join?: string;
spectate?: string;
match?: string;
};
instance?: boolean;
flags?: number;
}
export enum ActivityType {
Game,
Streaming,
Listening,
Watching,
Custom ,
Competing
}
export enum Intent {
GUILDS = 1 << 0,
GUILD_MEMBERS = 1 << 1,
@@ -97,7 +56,7 @@ export enum Intent {
ALL = 32767
}
export const IntentGroup : {[key in Intent]: [string[], number]} = {
export const IntentGroup : {[key in Intent]?: [string[], number]} = {
[Intent.GUILDS]: [[
"GUILD_CREATE",
"GUILD_UPDATE",
+128
View File
@@ -0,0 +1,128 @@
import type { API } from "../services/api";
import type { Client } from "../services/client";
import { isNil } from "../utils";
import { 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.
*
* @class Context
*
* @property {string} message_id - The ID of the message that this context is associated with.
* @property {string} channel_id - The ID of the channel where the message was sent.
* @property {string} [guild_id] - The ID of the guild where the message was sent, if applicable.
*
* @constructor
* @param {Client} client - The client instance that is interacting with the Discord API.
* @param {API} api - The API instance used to make requests to the Discord API.
* @param {Message} message - The message object that this context is based on.
*
* @throws {Error} If the message ID is not provided.
* @throws {Error} If the channel ID is not provided.
*/
export class Context {
// Public properties
message_id: string;
channel_id: string;
guild_id?: string;
constructor(private client: Client, private api: API, private _message: Message) {
if (isNil(_message.id)) {
throw new Error('Message ID is required to create a context');
}
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;
}
// 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() {
if (isNil(this.guild_id)) {
return Promise.resolve(null);
}
return this.client.guilds.get(this.guild_id);
}
/**
* Reply to the message that this command
* @param message The message to send to the channel
* @param reference If true the message will be a reference the the message that triggered the command
* @returns The message that was sent
*/
public reply = (message: Message | string, reference: boolean = true) => {
if (typeof message === 'string') {
message = new Message().setContent(message);
}
if (reference) {
message.message_reference = {
message_id: this.message_id,
channel_id: this.channel_id,
guild_id: this.guild_id
};
}
return this.client.sendMessage(this.channel_id, message);
};
/**
* @name sendMessage
* @description Sends a message to the channel that this context is associated with.
* @param {Message | string} message - The message to send to the channel.
* @returns {Promise<Message>} The message that was sent.
*/
public sendMessage = (message: Message | string) => {
if (typeof message === 'string') {
message = new Message().setContent(message);
}
return this.client.sendMessage(this.channel_id, message);
}
/**
*
* @name delete
* @description Deletes a message in the channel that this context is associated with.
* @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) => {
return this.api.delete(`/channels/${this.channel_id}/messages/${message_id ?? this.message_id}`);
};
/**
*
* @name edit
* @description Edits a message in the channel that this context is associated with.
* @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);
}
return Message.fromAPIResponse(await this.api.patch(`/channels/${this.channel_id}/messages/${this.message_id}`, message));
};
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { Message } from '.';
import type { Message } from './message';
type EmbedFooter = {
text: string;
+25
View File
@@ -0,0 +1,25 @@
export class Emoji {
version: number;
roles: string[];
require_colons: boolean;
name: string;
managed: boolean;
id: string;
available: boolean;
animated: boolean;
constructor(data: any) {
this.version = data.version;
this.roles = data.roles;
this.require_colons = data.require_colons;
this.name = data.name;
this.managed = data.managed;
this.id = data.id;
this.available = data.available;
this.animated = data.animated;
}
static fromAPIResponse(data: any): Emoji {
return new Emoji(data);
}
}
+130
View File
@@ -0,0 +1,130 @@
import { Channel } from "./channel";
import { Emoji } from "./emoji";
import { Member } from "./member";
import { Role } from "./role";
export class Guild {
system_channel_id: string | null;
premium_progress_bar_enabled: boolean;
region: string;
application_id: string | null;
premium_tier: number;
presences: any[];
discovery_splash: string | null;
latest_onboarding_question_id: string | null;
unavailable: boolean;
afk_channel_id: string | null;
large: boolean;
nsfw: boolean;
max_video_channel_users: number;
stickers: any[];
id: string;
premium_subscription_count: number;
stage_instances: any[];
preferred_locale: string;
guild_scheduled_events: any[];
member_count: number;
name: string;
default_message_notifications: number;
system_channel_flags: number;
hub_type: string | null;
lazy: boolean;
soundboard_sounds: any[];
threads: any[];
banner: string | null;
description: string | null;
clan: string | null;
incidents_data: string | null;
application_command_counts: any;
embedded_activities: any[];
icon: string | null;
home_header: string | null;
joined_at: string;
safety_alerts_channel_id: string | null;
verification_level: number;
max_stage_video_channel_users: number;
emojis: Emoji[];
owner_id: string;
public_updates_channel_id: string | null;
features: string[];
rules_channel_id: string | null;
roles: Role[];
afk_timeout: number;
inventory_settings: any;
activity_instances: any[];
max_members: number;
channels: Channel[];
voice_states: any[];
splash: string | null;
mfa_level: number;
members: Member[];
version: number;
vanity_url_code: string | null;
nsfw_level: number;
explicit_content_filter: number;
constructor(data: any) {
this.system_channel_id = data.system_channel_id;
this.premium_progress_bar_enabled = data.premium_progress_bar_enabled;
this.region = data.region;
this.application_id = data.application_id;
this.premium_tier = data.premium_tier;
this.presences = data.presences;
this.discovery_splash = data.discovery_splash;
this.latest_onboarding_question_id = data.latest_onboarding_question_id;
this.unavailable = data.unavailable;
this.afk_channel_id = data.afk_channel_id;
this.large = data.large;
this.nsfw = data.nsfw;
this.max_video_channel_users = data.max_video_channel_users;
this.stickers = data.stickers;
this.id = data.id;
this.premium_subscription_count = data.premium_subscription_count;
this.stage_instances = data.stage_instances;
this.preferred_locale = data.preferred_locale;
this.guild_scheduled_events = data.guild_scheduled_events;
this.member_count = data.member_count;
this.name = data.name;
this.default_message_notifications = data.default_message_notifications;
this.system_channel_flags = data.system_channel_flags;
this.hub_type = data.hub_type;
this.lazy = data.lazy;
this.soundboard_sounds = data.soundboard_sounds;
this.threads = data.threads;
this.banner = data.banner;
this.description = data.description;
this.clan = data.clan;
this.incidents_data = data.incidents_data;
this.application_command_counts = data.application_command_counts;
this.embedded_activities = data.embedded_activities;
this.icon = data.icon;
this.home_header = data.home_header;
this.joined_at = data.joined_at;
this.safety_alerts_channel_id = data.safety_alerts_channel_id;
this.verification_level = data.verification_level;
this.max_stage_video_channel_users = data.max_stage_video_channel_users;
this.emojis = data.emojis.map((emoji: any) => Emoji.fromAPIResponse(emoji));
this.owner_id = data.owner_id;
this.public_updates_channel_id = data.public_updates_channel_id;
this.features = data.features;
this.rules_channel_id = data.rules_channel_id;
this.roles = data.roles.map((role: any) => Role.fromAPIResponse(role));
this.afk_timeout = data.afk_timeout;
this.inventory_settings = data.inventory_settings;
this.activity_instances = data.activity_instances;
this.max_members = data.max_members;
this.channels = data.channels.map((channel: any) => Channel.fromAPIResponse(channel));
this.voice_states = data.voice_states;
this.splash = data.splash;
this.mfa_level = data.mfa_level;
this.members = data.members.map((member: any) => Member.fromAPIResponse(member));
this.version = data.version;
this.vanity_url_code = data.vanity_url_code;
this.nsfw_level = data.nsfw_level;
this.explicit_content_filter = data.explicit_content_filter;
}
static fromAPIResponse(data: any): Guild {
return new Guild(data);
}
}
+14
View File
@@ -0,0 +1,14 @@
import type { EventPayload } from "./common";
import type { Context } from "./context";
export class BaseHandler {
constructor() {};
public 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 HandlerFunction = CommandHandlerFunction | EventHandlerFunction;
export type Handler = CommandHandlerFunction | EventHandlerFunction | BaseHandler;
-8
View File
@@ -1,8 +0,0 @@
import { Client, Context } from '..';
export abstract class Handler {
abstract events: string | string[];
abstract execute: HandlerFunction;
}
export type HandlerFunction = (client: Client, context: Context, data: Record<string, any>) => void;
-5
View File
@@ -1,5 +0,0 @@
export * from './common';
export * from './command';
export * from './slash_command';
export * from './handler';
export * from './interaction';
-19
View File
@@ -1,19 +0,0 @@
import { Client } from '..';
import { InteractionContext } from '../lib';
export abstract class Interaction {
abstract custom_id: string | string[];
abstract execute: InteractionFunction;
}
export type InteractionFunction = (client: Client, context: InteractionContext, data: Record<string, any>) => void;
export enum InteractionResponseType {
Pong = 1,
Reply = 4,
DeferredReply = 5,
DeferredUpdateMessage = 6,
UpdateMessage = 7,
AutocompleteResult = 8,
Modal = 9
}
+33
View File
@@ -0,0 +1,33 @@
export class Member {
user: any;
roles: string[];
premium_since: string | null;
pending: boolean;
nick: string | null;
mute: boolean;
joined_at: string;
flags: number;
deaf: boolean;
communication_disabled_until: string | null;
banner: string | null;
avatar: string | null;
constructor(data: any) {
this.user = data.user;
this.roles = data.roles;
this.premium_since = data.premium_since;
this.pending = data.pending;
this.nick = data.nick;
this.mute = data.mute;
this.joined_at = data.joined_at;
this.flags = data.flags;
this.deaf = data.deaf;
this.communication_disabled_until = data.communication_disabled_until;
this.banner = data.banner;
this.avatar = data.avatar;
}
static fromAPIResponse(data: any): Member {
return new Member(data);
}
}
+37 -6
View File
@@ -1,4 +1,6 @@
import { Embed } from '.';
import { isNil } from '../utils';
import { Embed } from './embed';
import { User } from './user';
export type Component = Partial<Button | SelectMenu> & {
type: ComponentType;
@@ -70,24 +72,50 @@ export enum ComponentType {
ActionRow = 1,
Button,
SelectMenu,
TextInput
TextInput,
UserSelect,
RoleSelect,
MentionableSelect,
ChannelSelect
}
export class Message {
id?: string;
author?: User;
channel_id?: string;
guild_id?: string;
public content?: string;
public embeds: Embed[] = [];
public components: Component[] = [];
public message_reference?: {
message_id?: string;
channel_id?: string;
type?: number;
message_id: string;
channel_id: string;
guild_id?: string;
fail_if_not_exists?: boolean;
};
constructor() {}
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;
}
}
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
});
}
public static button(config: Button): Component {
return {
@@ -115,7 +143,6 @@ export class Message {
public addComponent = (content: Component | string, ...components: Component[]) => {
if (typeof content === 'string') {
console.log('string');
this.components.push({
type: ComponentType.ActionRow,
content,
@@ -131,6 +158,10 @@ export class Message {
};
public setReference = (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,
+35
View File
@@ -0,0 +1,35 @@
export class Role {
version: number;
unicode_emoji: string | null;
tags: any;
position: number;
permissions: string;
name: string;
mentionable: boolean;
managed: boolean;
id: string;
icon: string | null;
hoist: boolean;
flags: number;
color: number;
constructor(data: any) {
this.version = data.version;
this.unicode_emoji = data.unicode_emoji;
this.tags = data.tags;
this.position = data.position;
this.permissions = data.permissions;
this.name = data.name;
this.mentionable = data.mentionable;
this.managed = data.managed;
this.id = data.id;
this.icon = data.icon;
this.hoist = data.hoist;
this.flags = data.flags;
this.color = data.color;
}
static fromAPIResponse(data: any): Role {
return new Role(data);
}
}
-78
View File
@@ -1,78 +0,0 @@
import { Client } from '..';
import { InteractionContext } from '../lib';
enum SlashCommandType {
ChatInput = 1,
User,
Message
}
enum SlashCommandOptionType {
SubCommand = 1,
SubCommandGroup,
String,
Integer,
Boolean,
User,
Channel,
Role,
Mentionable,
Number,
ATTACHMENT
}
type SlashCommandOptionChoice = {
name: string;
value: string | number;
};
type SlashCommandOption = {
type: SlashCommandOptionType;
name: string;
description: string;
required?: boolean;
choices?: SlashCommandOptionChoice[];
options?: SlashCommandOption[];
min_value?: number;
max_value?: number;
min_length?: number;
max_length?: number;
autocomplete?: boolean;
// channel_types?: number[];
};
export class SlashCommand {
type: SlashCommandType;
name: string;
description: string;
options: SlashCommandOption[];
guild_id?: string;
dm_permission?: boolean;
default_member_permissions?: string;
execute: SlashCommandFunction;
constructor(data: SlashCommand) {
this.type = data.type;
this.name = data.name;
this.description = data.description;
this.options = data.options;
this.guild_id = data.guild_id;
this.dm_permission = data.dm_permission;
this.default_member_permissions = data.default_member_permissions;
this.execute = data.execute;
}
public toJSON() {
return {
type: this.type,
name: this.name,
description: this.description,
options: this.options,
guild_id: this.guild_id,
dm_permission: this.dm_permission,
default_member_permissions: this.default_member_permissions
};
}
}
export type SlashCommandFunction = (client: Client, context: InteractionContext, ...args: any[]) => void;
+79
View File
@@ -0,0 +1,79 @@
// 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
// global_name ?string the user's display name, if it is set. For bots, this is the application name identify
// avatar ?string the user's avatar hash identify
// bot? boolean whether the user belongs to an OAuth2 application identify
// system? boolean whether the user is an Official Discord System user (part of the urgent message system) identify
// mfa_enabled? boolean whether the user has two factor enabled on their account identify
// banner? ?string the user's banner hash identify
// accent_color? ?integer the user's banner color encoded as an integer representation of hexadecimal color code identify
// locale? string the user's chosen language option identify
// verified? boolean whether the email on this account has been verified email
// email? ?string the user's email email
// flags? integer the flags on a user's account identify
// premium_type? integer the type of Nitro subscription on a user's account identify
// public_flags? integer the public flags on a user's account identify
// avatar_decoration_data?
export class User {
id: string;
username: string;
discriminator: string;
global_name: string;
avatar: string;
bot?: boolean;
system?: boolean;
mfa_enabled?: boolean;
banner?: string;
accent_color?: number;
locale?: string;
verified?: boolean;
email?: string;
flags?: number;
premium_type?: number;
public_flags?: number;
avatar_decoration_data?: Record<string, any>;
constructor(data: User) {
this.id = data.id;
this.username = data.username;
this.discriminator = data.discriminator;
this.global_name = data.global_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;
this.flags = data.flags;
this.premium_type = data.premium_type;
this.public_flags = data.public_flags;
this.avatar_decoration_data = data.avatar_decoration_data;
}
static fromAPIResponse(data: any): User {
return new User({
id: data.id,
username: data.username,
discriminator: data.discriminator,
global_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,
flags: data.flags,
premium_type: data.premium_type,
public_flags: data.public_flags,
avatar_decoration_data: data.avatar_decoration_data
});
}
}
+4
View File
@@ -0,0 +1,4 @@
export const isNil = (value: any): value is null | undefined => value === null || value === undefined;
export const exists = <T>(value: T | null | undefined): value is T => !isNil(value);
export const isObject = (value: any): value is Record<string, any> => typeof value === 'object' && !Array.isArray(value);
export const log = (...args: any[]) => console.log('\x1b[35m[discord.ts]\x1b[0m', ...args);