From 57a787e7ec7935ea83ebecc46cbc49ad02f6069c Mon Sep 17 00:00:00 2001 From: Jaron Pate Date: Fri, 7 Oct 2022 01:12:31 -0500 Subject: [PATCH] working application commands? --- src/index.ts | 125 ++++++++++++++++++++++++++++--------- src/lib/embed.ts | 4 +- src/lib/gateway.ts | 45 ++++++++++--- src/types/command.ts | 23 +++++-- src/types/index.ts | 1 + src/types/slash_command.ts | 37 ++++++++--- test/main.test.ts | 27 +++++--- 7 files changed, 199 insertions(+), 63 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6dbdaae..5b3ad2b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,18 @@ import { ComponentStyle, Context, Gateway, Guilds, Message, Embed } from './lib'; -import { Command, CommandFuntion, Intent, Presence } from './types'; -export { ComponentStyle, Context, Gateway, Guilds, Message, Embed, Command, CommandFuntion, Intent, Presence }; +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; @@ -14,8 +26,8 @@ export class Client { readonly token: string; readonly application_id: string; private readonly gateway: Gateway; - private readonly client_commands: Map = new Map(); - private readonly slash_commands: Map = new Map(); + private readonly client_commands: Map = new Map(); + private readonly slash_commands: Map = new Map(); readonly guilds: Guilds; constructor({ token, application_id, prefix, intents }: Config) { @@ -67,48 +79,99 @@ export class Client { }; public commands = { - register: (command: Command | string, handler: CommandFuntion) => { - if (typeof command === 'string') { - this.client_commands.set(command, handler); + 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, 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 { - this.client_commands.set(command.name, command); + console.error('[discord.ts] Error:', error_or_message); } - }, - 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 Command - ? 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 this.client_commands; } }; private initialize = () => { // Register default help command - this.commands.register('help', (client, context, _args) => { - const embed = new Embed().setTitle('Commands').setDescription('This is a list of commands you can use.'); + 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 = Array.from(client.commands.list().entries()); + const commands = client.commands.chat.list(); for (const [name, command] of commands) { embed.addField({ name: name, - value: command instanceof Command ? command.description : 'No description provided' + value: command instanceof ChatCommand ? command.description : 'No description provided' }); } context.reply(embed.toMessage()); }); - } - + }; } diff --git a/src/lib/embed.ts b/src/lib/embed.ts index bcc7f46..66aa2a6 100644 --- a/src/lib/embed.ts +++ b/src/lib/embed.ts @@ -68,8 +68,8 @@ export class Embed { return this; }; - setColor = (color: number) => { - this.color = color; + setColor = (color: number | string) => { + this.color = typeof color === 'string' ? parseInt(`0x${color.replace('#', '')}`) : color; return this; }; diff --git a/src/lib/gateway.ts b/src/lib/gateway.ts index 526f6f3..d545fa4 100644 --- a/src/lib/gateway.ts +++ b/src/lib/gateway.ts @@ -12,9 +12,10 @@ import { Status, Presence, ActivityType, - CommandFuntion, + CommandFunction, Interaction, - InteractionFunction + InteractionFunction, + SlashCommand } from '../types'; import { Context } from './context'; import { InteractionContext } from './interaction'; @@ -26,9 +27,11 @@ export class Gateway { readonly token: string; readonly client: Client; private intents: number = 513; - private commandNotFound: CommandFuntion = null; + 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; @@ -45,7 +48,7 @@ export class Gateway { token: string; intents?: Intent[]; client: Client; - commandNotFound?: CommandFuntion; + commandNotFound?: CommandFunction; }) { // Copy config this.token = token; @@ -80,6 +83,7 @@ export class Gateway { }; public reconnect = () => { + this.socket.terminate(); return this.connect(); }; @@ -167,12 +171,21 @@ export class Gateway { if (!this.heartbeat_interval) { throw new Error('No heartbeat interval set'); } - setInterval(() => { + 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) => { @@ -244,7 +257,18 @@ export class Gateway { } }; - private handleShashInteraction = (payload: 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 }); @@ -266,15 +290,16 @@ export class Gateway { return; } console.log(payload.d); + const content = payload.d.content.trim(); // Is this a command? - if (payload.d.content.startsWith(this.client.prefix)) { + if (content.startsWith(this.client.prefix) | content.startsWith(`<@${this.client.user.id}>`)) { // Parse command - const args = payload.d.content + const args = content .slice(this.client.prefix.length) .trim() .split(/\s+(?=(?:[^\'"]*[\'"][^\'"]*[\'"])*[^\'"]*$)/g); // Check if command exists - const handler = this.client.commands.get(args[0]); + const handler = this.client.commands.chat.get(args[0]); // Generate context const context = new Context({ client: this.client, message: payload.d }); if (handler) { @@ -317,6 +342,7 @@ export class Gateway { break; case 11: console.log('heartbeat acknowledged'); + this.heartbeat_timeout.refresh(); break; default: break; @@ -324,6 +350,7 @@ export class Gateway { }; private onMessage = (packet: Data): void => { + console.log('============================'); let data: Payload | null = null; try { data = JSON.parse(packet.toString()); diff --git a/src/types/command.ts b/src/types/command.ts index 61b63ba..16df10e 100644 --- a/src/types/command.ts +++ b/src/types/command.ts @@ -1,10 +1,21 @@ -import { Client } from ".."; -import { Context } from "../lib"; +import { Client } from '..'; +import { Context } from '../lib'; export abstract class Command { - name: string; - description: string; - execute: CommandFuntion; + 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 CommandFuntion = (client: Client, context: Context, ...args: any[]) => void; \ No newline at end of file +export type CommandFunction = (client: Client, context: Context, ...args: any[]) => void; + +export class ChatCommand extends Command { + constructor(data: ChatCommand) { + super(data); + } +} diff --git a/src/types/index.ts b/src/types/index.ts index 34c1e86..3a4ef41 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,5 @@ export * from './common'; export * from './command'; +export * from './slash_command'; export * from './handler'; export * from './interaction'; diff --git a/src/types/slash_command.ts b/src/types/slash_command.ts index 6da4e39..563efdb 100644 --- a/src/types/slash_command.ts +++ b/src/types/slash_command.ts @@ -1,5 +1,5 @@ -import { Client } from ".."; -import { Context } from "../lib"; +import { Client } from '..'; +import { InteractionContext } from '../lib'; enum SlashCommandType { ChatInput = 1, @@ -24,7 +24,7 @@ enum SlashCommandOptionType { type SlashCommandOptionChoice = { name: string; value: string | number; -} +}; type SlashCommandOption = { type: SlashCommandOptionType; @@ -39,9 +39,9 @@ type SlashCommandOption = { max_length?: number; autocomplete?: boolean; // channel_types?: number[]; -} +}; -export abstract class SlashCommand { +export class SlashCommand { type: SlashCommandType; name: string; description: string; @@ -49,7 +49,30 @@ export abstract class SlashCommand { guild_id?: string; dm_permission?: boolean; default_member_permissions?: string; - execute: SlashCommandFuntion; + 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 SlashCommandFuntion = (client: Client, context: Context, ...args: any[]) => void; \ No newline at end of file +export type SlashCommandFunction = (client: Client, context: InteractionContext, ...args: any[]) => void; diff --git a/test/main.test.ts b/test/main.test.ts index 2a9eaac..6dd64dc 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -1,7 +1,7 @@ import * as dotenv from 'dotenv'; dotenv.config(); -import { Client, ComponentStyle, Intent, Message } from '../src'; +import { Client, ChatCommand, ComponentStyle, Intent, Message } from '../src'; const bot = new Client({ token: process.env.BOT_TOKEN!, @@ -9,15 +9,26 @@ const bot = new Client({ intents: [Intent.GUILDS, Intent.GUILD_MESSAGES, Intent.GUILD_MESSAGE_REACTIONS, Intent.MESSAGE_CONTENT] }); -bot.commands.register('ping', (client, context, args) => { - context.reply('Pong!', true); +bot.commands.chat.register(new ChatCommand({ + name: 'ping', + description: 'Check if he is alive', + execute: (client, context, args) => { + context.reply('Pong!', true); + } +})) + +bot.commands.application.register({ + name: 'ping', + description: 'Check if he is alive' +}, (client, context, args) => { + context.reply('Pong!'); }); -bot.commands.register('help', (client, context, args) => { - context.reply('Help yourself.'); -}); +// bot.commands.register('help', (client, context, args) => { +// context.reply('Help yourself.'); +// }); -bot.commands.register('demo', (_client, context, _args) => { +bot.commands.chat.register('demo', (_client, context, _args) => { const msg = new Message() .setContent('Hello World!') .addComponent( @@ -94,7 +105,7 @@ bot.commands.register('demo', (_client, context, _args) => { context.reply(msg); }); -bot.commands.register('test', (_client, context, _args) => { +bot.commands.chat.register('test', (_client, context, _args) => { const msg = new Message().setContent('This is a test interaction!').addComponent( 'This is an action row', Message.button({