import type { Arg, Context, Visibility } from "../types/handler"; import * as Files from "./utils/readFile" import type { ApplicationCommandOptionData, Awaitable, Client, CommandInteraction, Message } from "discord.js"; import type { possibleOutput } from "../types/handler" import { Ok, Result, None, Some } from "ts-results"; import type * as Utils from "./utils/preprocessors/args"; import { isBot, hasPrefix, fmt } from "./utils/messageHelpers"; /** * @class */ export class Handler { private wrapper: Wrapper; /** * @constructor * @param {Wrapper} wrapper Some data that is required to run sern handler */ constructor( wrapper: Wrapper, ) { this.wrapper = wrapper; this.client .on("ready", async () => { Files.buildData(this) .then(this.registerModules); if (wrapper.init !== undefined) wrapper.init(this); }) .on("messageCreate", async message => { if (isBot(message) || !hasPrefix(message, this.prefix)) return; if (message.channel.type === "DM") return; const tryFmt = fmt(message, this.prefix) const commandName = tryFmt.shift()!; const module = Files.Commands.get(commandName) ?? Files.Alias.get(commandName) if (module === undefined) { message.channel.send("Unknown legacy command") return; } const cmdResult = (await this.commandResult(module, message, tryFmt.join(" "))) if (cmdResult === undefined) return; message.channel.send(cmdResult) }) .on("interactionCreate", async interaction => { if (!interaction.isCommand()) return; const module = Files.Commands.get(interaction.commandName); const res = await this.interactionResult(module, interaction); if (res === undefined) return; await interaction.reply(res); }) } /** * * @param {Files.CommandVal | undefined} module command file information * @param {CommandInteraction} interaction a Discord.js command interaction * @returns {possibleOutput | undefined} takes return value and replies it, if possible input */ private async interactionResult( module: Files.CommandVal | undefined, interaction: CommandInteraction): Promise { if (module === undefined) return "Unknown slash command!"; const name = Array.from(Files.Commands.keys()).find(it => it === interaction.commandName); if (name === undefined) return `Could not find ${interaction.commandName} command!`; if (module.mod.type < CommandType.SLASH) return "This is not a slash command"; const context = { message: None, interaction: Some(interaction) } const parsedArgs = module.mod.parse?.(context, ["slash", interaction.options]) ?? Ok(""); if (parsedArgs.err) return parsedArgs.val; const fn = await module.mod.delegate(context, parsedArgs); return fn?.val; } /** * * @param {Files.CommandVal | undefined} module command file information * @param {Message} message a message object * @param {string} args anything after the command * @returns takes return value and replies it, if possible input */ private async commandResult(module: Files.CommandVal | undefined, message: Message, args: string): Promise { if (module?.mod === undefined) return "Unknown legacy command"; if (module.mod.type === CommandType.SLASH) return `This may be a slash command and not a legacy command` if (module.mod.visibility === "private") { const checkIsTestServer = this.privateServers.find(({ id }) => id === message.guildId!)?.test; if (checkIsTestServer === undefined) return "This command has the private modifier but is not registered under Handler#privateServers"; if (checkIsTestServer !== module.testOnly) { return "This private command is a testing command"; } } const context = { message: Some(message), interaction: None } const parsedArgs = module.mod.parse?.(context, ["text", args]) ?? Ok(""); if (parsedArgs.err) return parsedArgs.val; const fn = await module.mod.delegate(context, parsedArgs) return fn?.val } private async registerModules(modArr: { name: string, mod: Module, absPath: string }[]) { for (const { name, mod, absPath } of modArr) { const { cmdName, testOnly } = Files.fmtFileName(name); switch (mod.type) { case 1: Files.Commands.set(cmdName, { mod, options: [], testOnly }); break; case 2: case (1 | 2): { const options = ((await import(absPath)).options as ApplicationCommandOptionData[]) Files.Commands.set(cmdName, { mod, options: options ?? [], testOnly }); switch (mod.visibility) { case "private": { // loading guild slash commands only await this.reloadSlash(cmdName, mod.desc, options) } case "public": { // creating global commands! this.client.application!.commands .create({ name: cmdName, description: mod.desc, options }) } } } break; default: throw Error(`${name}.js is not a valid module type.`); } if (mod.alias.length > 0) { for (const alias of mod.alias) { Files.Alias.set(alias, { mod, options: [], testOnly }) } } } } /** * * @param {string} cmdName name of command * @param {string} description description of command * @param {ApplicationCommandOptionData[]} options any options for the slash command */ private async reloadSlash( cmdName: string, description: string, options: ApplicationCommandOptionData[] ) { for (const { id } of this.privateServers) { const guild = (await this.client.guilds.fetch(id)); guild.commands.create({ name: cmdName, description, options }) } } /** * @readonly * @returns {string} prefix used for legacy commands */ get prefix(): string { return this.wrapper.prefix; } /** * @readonly * @returns {string} directory of your commands folder */ get commandDir(): string { return this.wrapper.commands; } /** * @readonly * @returns {Client} discord.js client */ get client(): Client { return this.wrapper.client } /** * @readonly * @returns {{test: boolean, id: string}[]} private server id for testing or personal use */ get privateServers(): { test: boolean, id: string }[] { return this.wrapper.privateServers; } } /** * An object to be passed into Sern.Handler constructor. * @typedef {object} Wrapper * @property {readonly Client} client * @property {readonly string} prefix * @property {readonly string} commands * @prop {(handler : Handler) => void)} init * @property {readonly {test: boolean, id: string}[]} privateServers */ export interface Wrapper { readonly client: Client, readonly prefix: string, readonly commands: string init?: (handler: Handler) => void, readonly privateServers: { test: boolean, id: string }[], } /** * An object to be passed into Sern.Handler constructor. * @typedef {object} Module * @property {string} desc * @property {Visibility} visibility * @property {CommandType} type * @property {(eventParams : Context, args : Ok) => Awaitable | void>)} delegate * @prop {(ctx: Context, args: Arg) => Utils.ArgType} parse */ export interface Module { alias: string[], desc: string, visibility: Visibility, type: CommandType, delegate: (eventParams: Context, args: Ok) => Awaitable | void> parse?: (ctx: Context, args: Arg) => Utils.ArgType } /** * @enum { number }; */ export enum CommandType { TEXT = 1, SLASH = 2, }