diff --git a/src/command.ts b/src/command.ts index e1aca63..91560cf 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,9 +1,16 @@ -import { AutocompleteFocusedOption, AutocompleteInteraction, ChatInputCommandInteraction, SharedSlashCommand } from "discord.js"; +import { AutocompleteFocusedOption, AutocompleteInteraction, ChatInputCommandInteraction, ContextMenuCommandBuilder, ContextMenuCommandInteraction, SharedSlashCommand, Snowflake, User } from "discord.js"; import { Config } from "./config"; -export abstract class Command { +export abstract class ICommand { } + +export abstract class ContextCommand extends ICommand { + abstract contextDefinition: ContextMenuCommandBuilder + abstract run(interaction: ContextMenuCommandInteraction, target: Snowflake): Promise +} + +export abstract class Command extends ICommand { abstract run(interaction: ChatInputCommandInteraction, config: Config): Promise; - autoComplete(interaction: AutocompleteInteraction, config: Config, option: AutocompleteFocusedOption) : Promise { + autoComplete(interaction: AutocompleteInteraction, config: Config, option: AutocompleteFocusedOption): Promise { throw new Error("Autocompletion called on command that does not have #autoComplete implemented."); } abstract slashCommand: SharedSlashCommand diff --git a/src/commands/botsex.ts b/src/commands/botsex.ts new file mode 100644 index 0000000..f04fcd3 --- /dev/null +++ b/src/commands/botsex.ts @@ -0,0 +1,14 @@ +import { ApplicationCommandType, ContextMenuCommandBuilder, ContextMenuCommandInteraction, InteractionContextType, Snowflake } from "discord.js"; +import { ContextCommand } from "../command.ts"; + +export default class RailUser extends ContextCommand { + contextDefinition: ContextMenuCommandBuilder = + new ContextMenuCommandBuilder() + .setName('rail') + .setType(ApplicationCommandType.User) + async run(interaction: ContextMenuCommandInteraction, target: Snowflake): Promise { + await interaction.reply(`Raililng <@${target}>.`) + await new Promise(resolve => setTimeout(resolve, 1000)) + await interaction.editReply(`UHGhghgghghgh. Railing successfull.`) + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index dda2429..24591ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,12 +3,15 @@ import { Events, GatewayIntentBits, InteractionCallback, + InteractionContextType, REST, + RestOrArray, Routes, + SlashCommandBooleanOption, } from "discord.js"; import path from "node:path"; import fs from "node:fs"; -import { Command } from "./command.ts"; +import { Command, ContextCommand, ICommand } from "./command.ts"; import { fileURLToPath } from "url"; import { config } from "./config.ts"; @@ -19,28 +22,60 @@ const client = new Client({ intents: [], }); -const commands: Command[] = [] - +const allCommands: ICommand[] = [] const commandDir = path.join(__dirname, "commands"); for (const file of fs.readdirSync(commandDir)) { if (!file.endsWith('.ts')) continue let command = await import(path.join(commandDir, file)); - commands.push(new command.default()) + let instance + try { + instance = new command.default() + } catch (e) { + throw new Error(`Could not instantiate command from ${file}`, { cause: e }) + } + if (!(instance instanceof ICommand)) + throw `${instance} is not an ICommand instance (imported from ${file})`; + allCommands.push(instance) } - +const commands = allCommands.filter(it => it instanceof Command); +const contextCommands = allCommands.filter(it => it instanceof ContextCommand); +const contextCommandLUT = Object.fromEntries(contextCommands.map(it => [it.contextDefinition.name, it])) const commandLookup = Object.fromEntries(commands.map(it => [it.slashCommand.name, it])) +function makeDefaultAvailableEverywhere): any, readonly contexts?: InteractionContextType[] }>(t: T): T { + if (!t.contexts) + t.setContexts(InteractionContextType.BotDM, InteractionContextType.Guild, InteractionContextType.PrivateChannel) + return t +} + client.once(Events.ClientReady, async () => { console.log("Ready"); const rest = new REST().setToken(config.token); - const data = await rest.put( - Routes.applicationCommands(client.user!.id), { body: commands.map(command => command.slashCommand.toJSON()) }, - ); + const data = await client.application!.commands.set( + [ + ...commands.map(it => it.slashCommand), + ...contextCommands.map(it => it.contextDefinition) + ].map(makeDefaultAvailableEverywhere) + ) // @ts-ignore - console.log(`Successfully reloaded ${data.length} application (/) commands.`); + console.log(`Successfully reloaded ${data.size} application (/) commands.`); }) - +client.on(Events.InteractionCreate, async (interaction) => { + if (!interaction.isContextMenuCommand()) return; + const { commandName } = interaction + const command = contextCommandLUT[commandName] + if (!command) { + console.error("unknown context command: " + commandName) + return + } + try { + await command.run(interaction, interaction.targetId) + } catch (e) { + console.error("error during context command execution: " + commandName, e) + interaction.reply("something sharted itself") + } +}); client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; @@ -63,7 +98,7 @@ client.on(Events.InteractionCreate, async (interaction) => { client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isAutocomplete()) return - const {commandName} = interaction; + const { commandName } = interaction; const command = commandLookup[commandName] if (!command) {