diff --git a/classes/command.js b/classes/command.js index bf4d1dc..1c2e78c 100644 --- a/classes/command.js +++ b/classes/command.js @@ -1,35 +1,49 @@ class Command { - constructor(client, cluster, worker, ipc, message, args, content, specialArgs) { + constructor(client, cluster, worker, ipc, options) { this.client = client; this.cluster = cluster; this.worker = worker; this.ipc = ipc; - this.message = message; - this.args = args; - this.content = content; - this.specialArgs = specialArgs; - this.reference = { - messageReference: { - channelID: this.message.channel.id, - messageID: this.message.id, - guildID: this.message.channel.guild ? this.message.channel.guild.id : undefined, - failIfNotExists: false - }, - allowedMentions: { - repliedUser: false - } - }; + this.type = options.type; + this.args = options.args; + if (options.type === "classic") { + this.message = options.message; + this.content = options.content; + this.specialArgs = options.specialArgs; + this.reference = { + messageReference: { + channelID: this.message.channel.id, + messageID: this.message.id, + guildID: this.message.channel.guild ? this.message.channel.guild.id : undefined, + failIfNotExists: false + }, + allowedMentions: { + repliedUser: false + } + }; + } else { + this.interaction = options.interaction; + } } async run() { return "It works!"; } + async acknowledge() { + if (this.type === "classic") { + await this.message.channel.sendTyping(); + } else { + await this.interaction.acknowledge(); + } + } + static description = "No description found"; static aliases = []; static arguments = []; static flags = []; static requires = []; + static slashAllowed = true; } export default Command; \ No newline at end of file diff --git a/classes/imageCommand.js b/classes/imageCommand.js index a93d284..f7e18e4 100644 --- a/classes/imageCommand.js +++ b/classes/imageCommand.js @@ -96,7 +96,7 @@ class ImageCommand extends Command { if (magickParams.params.type === "image/gif") { status = await this.processMessage(this.message); } else { - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); } try { diff --git a/commands/fun/ancient.js b/commands/fun/ancient.js index 07796eb..a9ad913 100644 --- a/commands/fun/ancient.js +++ b/commands/fun/ancient.js @@ -3,7 +3,7 @@ import Command from "../../classes/command.js"; class AncientCommand extends Command { async run() { - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const controller = new AbortController(); // eslint-disable-line no-undef const timeout = setTimeout(() => { controller.abort(); diff --git a/commands/fun/bird.js b/commands/fun/bird.js index 87d9f09..cfd5b27 100644 --- a/commands/fun/bird.js +++ b/commands/fun/bird.js @@ -3,7 +3,7 @@ import Command from "../../classes/command.js"; class BirdCommand extends Command { async run() { - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const imageData = await fetch("http://shibe.online/api/birds"); const json = await imageData.json(); return { diff --git a/commands/fun/cat.js b/commands/fun/cat.js index a5e2837..62451cd 100644 --- a/commands/fun/cat.js +++ b/commands/fun/cat.js @@ -3,7 +3,7 @@ import Command from "../../classes/command.js"; class CatCommand extends Command { async run() { - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const controller = new AbortController(); // eslint-disable-line no-undef const timeout = setTimeout(() => { controller.abort(); diff --git a/commands/fun/dog.js b/commands/fun/dog.js index 3837c15..8014fd2 100644 --- a/commands/fun/dog.js +++ b/commands/fun/dog.js @@ -3,7 +3,7 @@ import Command from "../../classes/command.js"; class DogCommand extends Command { async run() { - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const imageData = await fetch("https://dog.ceo/api/breeds/image/random"); const json = await imageData.json(); return { diff --git a/commands/fun/mc.js b/commands/fun/mc.js index 118b32a..c769239 100644 --- a/commands/fun/mc.js +++ b/commands/fun/mc.js @@ -4,7 +4,7 @@ import Command from "../../classes/command.js"; class MCCommand extends Command { async run() { if (this.args.length === 0) return "You need to provide some text to generate a Minecraft achievement!"; - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const request = await fetch(`https://www.minecraftskinstealer.com/achievement/a.php?i=13&h=Achievement+get%21&t=${encodeURIComponent(this.args.join("+"))}`); return { file: Buffer.from(await request.arrayBuffer()), diff --git a/commands/fun/wikihow.js b/commands/fun/wikihow.js index 1c9f6cb..09d0366 100644 --- a/commands/fun/wikihow.js +++ b/commands/fun/wikihow.js @@ -3,7 +3,7 @@ import Command from "../../classes/command.js"; class WikihowCommand extends Command { async run() { - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const request = await fetch("https://www.wikihow.com/api.php?action=query&generator=random&prop=imageinfo&format=json&iiprop=url&grnnamespace=6"); const json = await request.json(); const id = Object.keys(json.query.pages)[0]; diff --git a/commands/general/channel.js b/commands/general/channel.js index 25291b0..979b6c5 100644 --- a/commands/general/channel.js +++ b/commands/general/channel.js @@ -3,6 +3,7 @@ import Command from "../../classes/command.js"; class ChannelCommand extends Command { async run() { + if (this.type !== "classic") return "This command only works with the old command style!"; if (!this.message.channel.guild) return "This command only works in servers!"; const owners = process.env.OWNER.split(","); if (!this.message.member.permissions.has("administrator") && !owners.includes(this.message.member.id)) return "You need to be an administrator to enable/disable me!"; @@ -40,8 +41,9 @@ class ChannelCommand extends Command { } } - static description = "Enables/disables me in a channel"; + static description = "Enables/disables me in a channel (does not work with slash commands)"; static arguments = ["[enable/disable]", "{id}"]; + static slashAllowed = false; } export default ChannelCommand; diff --git a/commands/general/donate.js b/commands/general/donate.js index d5e2bf6..5c2016b 100644 --- a/commands/general/donate.js +++ b/commands/general/donate.js @@ -3,7 +3,7 @@ import Command from "../../classes/command.js"; class DonateCommand extends Command { async run() { - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); let prefix = ""; const controller = new AbortController(); // eslint-disable-line no-undef const timeout = setTimeout(() => { diff --git a/commands/general/image.js b/commands/general/image.js index cd95317..2b10a2c 100644 --- a/commands/general/image.js +++ b/commands/general/image.js @@ -9,7 +9,7 @@ class ImageSearchCommand extends Command { async run() { if (this.message.channel.guild && !this.message.channel.permissionsOf(this.client.user.id).has("embedLinks")) return "I don't have the `Embed Links` permission!"; if (this.args.length === 0) return "You need to provide something to search for!"; - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const embeds = []; const rawImages = await fetch(`${random(searx)}/search?format=json&safesearch=2&categories=images&q=!goi%20!ddi%20${encodeURIComponent(this.args.join(" "))}`).then(res => res.json()); if (rawImages.results.length === 0) return "I couldn't find any results!"; diff --git a/commands/general/imagestats.js b/commands/general/imagestats.js index 75faa07..7296659 100644 --- a/commands/general/imagestats.js +++ b/commands/general/imagestats.js @@ -2,7 +2,7 @@ import Command from "../../classes/command.js"; class ImageStatsCommand extends Command { async run() { - await this.client.sendChannelTyping(this.message.channel.id); + await this.acknowledge(); const servers = await this.ipc.serviceCommand("image", { type: "stats" }, true); const embed = { embeds: [{ diff --git a/commands/general/lengthen.js b/commands/general/lengthen.js index ebe6e99..6fd0542 100644 --- a/commands/general/lengthen.js +++ b/commands/general/lengthen.js @@ -4,7 +4,7 @@ import Command from "../../classes/command.js"; class LengthenCommand extends Command { async run() { - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); if (this.args.length === 0 || !urlCheck(this.args[0])) return "You need to provide a short URL to lengthen!"; if (urlCheck(this.args[0])) { const url = await fetch(encodeURI(this.args[0]), { redirect: "manual" }); diff --git a/commands/general/qrcreate.js b/commands/general/qrcreate.js index 20ee181..a2f27da 100644 --- a/commands/general/qrcreate.js +++ b/commands/general/qrcreate.js @@ -5,7 +5,7 @@ import Command from "../../classes/command.js"; class QrCreateCommand extends Command { async run() { if (this.args.length === 0) return "You need to provide some text to generate a QR code!"; - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const writable = new PassThrough(); qrcode.toFileStream(writable, this.content, { margin: 1 }); const file = await this.streamToBuf(writable); diff --git a/commands/general/qrread.js b/commands/general/qrread.js index 0bda0cf..dec50a1 100644 --- a/commands/general/qrread.js +++ b/commands/general/qrread.js @@ -9,7 +9,7 @@ class QrReadCommand extends Command { async run() { const image = await imageDetect(this.client, this.message); if (image === undefined) return "You need to provide an image/GIF with a QR code to read!"; - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const data = await (await fetch(image.path)).buffer(); const rawData = await sharp(data).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); const qrBuffer = jsqr(rawData.data, rawData.info.width, rawData.info.height); diff --git a/commands/general/raw.js b/commands/general/raw.js index 4896bc9..d1d6800 100644 --- a/commands/general/raw.js +++ b/commands/general/raw.js @@ -3,7 +3,7 @@ import imageDetect from "../../utils/imagedetect.js"; class RawCommand extends Command { async run() { - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const image = await imageDetect(this.client, this.message); if (image === undefined) return "You need to provide an image/GIF to get a raw URL!"; return image.path; diff --git a/commands/general/soundreload.js b/commands/general/soundreload.js index 82b5a33..1f2e63c 100644 --- a/commands/general/soundreload.js +++ b/commands/general/soundreload.js @@ -6,7 +6,7 @@ class SoundReloadCommand extends Command { return new Promise((resolve) => { const owners = process.env.OWNER.split(","); if (!owners.includes(this.message.author.id)) return "Only the bot owner can reload Lavalink!"; - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); this.ipc.broadcast("soundreload"); this.ipc.register("soundReloadSuccess", (msg) => { this.ipc.unregister("soundReloadSuccess"); diff --git a/commands/general/youtube.js b/commands/general/youtube.js index 1a79989..3edbbc2 100644 --- a/commands/general/youtube.js +++ b/commands/general/youtube.js @@ -8,7 +8,7 @@ import Command from "../../classes/command.js"; class YouTubeCommand extends Command { async run() { if (this.args.length === 0) return "You need to provide something to search for!"; - this.client.sendChannelTyping(this.message.channel.id); + this.acknowledge(); const messages = []; const videos = await fetch(`${random(searx)}/search?format=json&safesearch=1&categories=videos&q=!youtube%20${encodeURIComponent(this.args.join(" "))}`).then(res => res.json()); if (videos.results.length === 0) return "I couldn't find any results!"; diff --git a/events/interactionCreate.js b/events/interactionCreate.js new file mode 100644 index 0000000..91099f8 --- /dev/null +++ b/events/interactionCreate.js @@ -0,0 +1,86 @@ +import { promises } from "fs"; +import database from "../utils/database.js"; +import * as logger from "../utils/logger.js"; +import { commands } from "../utils/collections.js"; +import { CommandInteraction } from "eris"; +import { clean } from "../utils/misc.js"; + +// run when a slash command is executed +export default async (client, cluster, worker, ipc, interaction) => { + if (!(interaction instanceof CommandInteraction)) return; + + // check if command exists and if it's enabled + const command = interaction.data.name; + const cmd = commands.get(command); + if (!cmd) return; + + const invoker = interaction.member ?? interaction.user; + + // actually run the command + logger.log("log", `${invoker.username} (${invoker.id}) ran command ${command}`); + try { + await database.addCount(command); + // eslint-disable-next-line no-unused-vars + const commandClass = new cmd(client, cluster, worker, ipc, { type: "application", interaction }); + const result = await commandClass.run(); + if (typeof result === "string" || (typeof result === "object" && result.embeds)) { + await interaction.createMessage(result); + } else if (typeof result === "object" && result.file) { + let fileSize = 8388119; + if (interaction.channel.guild) { + switch (interaction.channel.guild.premiumTier) { + case 2: + fileSize = 52428308; + break; + case 3: + fileSize = 104856616; + break; + } + } + if (result.file.length > fileSize) { + if (process.env.TEMPDIR && process.env.TEMPDIR !== "") { + const filename = `${Math.random().toString(36).substring(2, 15)}.${result.name.split(".")[1]}`; + await promises.writeFile(`${process.env.TEMPDIR}/${filename}`, result.file); + const imageURL = `${process.env.TMP_DOMAIN || "https://tmp.projectlounge.pw"}/${filename}`; + await interaction.createMessage({ + embeds: [{ + color: 16711680, + title: "Here's your image!", + url: imageURL, + image: { + url: imageURL + }, + footer: { + text: "The result image was more than 8MB in size, so it was uploaded to an external site instead." + }, + }] + }); + } else { + await interaction.createMessage("The resulting image was more than 8MB in size, so I can't upload it."); + } + } else { + await interaction.createMessage({ + content: result.text ? result.text : undefined + }, result); + } + } + } catch (error) { + if (error.toString().includes("Request entity too large")) { + await interaction.createMessage("The resulting file was too large to upload. Try again with a smaller image if possible."); + } else if (error.toString().includes("Job ended prematurely")) { + await interaction.createMessage("Something happened to the image servers before I could receive the image. Try running your command again."); + } else if (error.toString().includes("Timed out")) { + await interaction.createMessage("The request timed out before I could download that image. Try uploading your image somewhere else or reducing its size."); + } else { + logger.error(`Error occurred with slash command ${command} with arguments ${interaction.data.options}: ${error.toString()}`); + try { + await interaction.createMessage({ + content: "Uh oh! I ran into an error while running this command. Please report the content of the attached file at the following link or on the esmBot Support server: " + }, [{ + file: `Message: ${await clean(error)}\n\nStack Trace: ${await clean(error.stack)}`, + name: "error.txt" + }]); + } catch { /* silently ignore */ } + } + } +}; diff --git a/events/messageCreate.js b/events/messageCreate.js index cceb03d..ccec305 100644 --- a/events/messageCreate.js +++ b/events/messageCreate.js @@ -101,7 +101,7 @@ export default async (client, cluster, worker, ipc, message) => { await database.addCount(aliases.get(command) ?? command); const startTime = new Date(); // eslint-disable-next-line no-unused-vars - const commandClass = new cmd(client, cluster, worker, ipc, message, parsed._, message.content.substring(prefix.length).trim().replace(command, "").trim(), (({ _, ...o }) => o)(parsed)); // we also provide the message content as a parameter for cases where we need more accuracy + const commandClass = new cmd(client, cluster, worker, ipc, { type: "classic", message, args: parsed._, content: message.content.substring(prefix.length).trim().replace(command, "").trim(), specialArgs: (({ _, ...o }) => o)(parsed) }); // we also provide the message content as a parameter for cases where we need more accuracy const result = await commandClass.run(); const endTime = new Date(); if ((endTime - startTime) >= 180000) reference.allowedMentions.repliedUser = true;