diff --git a/commands/general/restart.js b/commands/general/restart.js index e39f3c40..b9a02c5e 100644 --- a/commands/general/restart.js +++ b/commands/general/restart.js @@ -1,5 +1,4 @@ import process from "node:process"; -import { setTimeout } from "node:timers/promises"; import Command from "#cmd-classes/command.js"; import { exit } from "#utils/misc.js"; @@ -10,12 +9,11 @@ class RestartCommand extends Command { this.success = false; return this.getString("commands.responses.restart.owner"); } - try { - return this.getString("commands.responses.restart.restarting"); - } finally { - // we can't do it instantly because we need to send the message first - setTimeout(3000).then(() => exit(this.client, this.database)); - } + return this.getString("commands.responses.restart.restarting"); + } + + async finalize() { + await exit(this.client, this.database); } static description = "Restarts me"; diff --git a/src/classes/command.ts b/src/classes/command.ts index 0fb58e84..a9505da3 100644 --- a/src/classes/command.ts +++ b/src/classes/command.ts @@ -136,6 +136,11 @@ class Command { return this.getString(`commands.responses.${this.cmdName}.invalid`); } + /** + * The function to run after sending the output. + */ + async finalize(_res?: Message) {} + async acknowledge() { if (this.type === "classic" && this.message) { await this.client.rest.channels.sendTyping(this.message.channelID); diff --git a/src/classes/mediaCommand.ts b/src/classes/mediaCommand.ts index 660adc78..d77316d5 100644 --- a/src/classes/mediaCommand.ts +++ b/src/classes/mediaCommand.ts @@ -12,9 +12,11 @@ import messages from "#config/messages.json" with { type: "json" }; import { runningCommands, selectedImages } from "#utils/collections.js"; import { convFlagType } from "#utils/handler.js"; import { getAllLocalizations } from "#utils/i18n.js"; +import logger from "#utils/logger.js"; import { runMediaJob } from "#utils/media.js"; import mediaDetect from "#utils/mediadetect.js"; -import { clean, isEmpty, random } from "#utils/misc.js"; +import { clean, isEmpty, maxFileSize, random } from "#utils/misc.js"; +import { upload } from "#utils/tempimages.js"; import type { ExtendedConstructedCommandOptions, MediaParams, MediaMeta, MediaTypes } from "#utils/types.js"; import Command from "./command.ts"; @@ -157,21 +159,40 @@ class MediaCommand extends Command { if (type === "nomedia") return `${this.getString(`commands.noImage.${this.cmdName}`, { returnNull: true }) || this.getString("image.noImage", { returnNull: true }) || staticProps.noImage} ${this.getString("image.tip", { params: { name: this.client.user.globalName ?? this.client.user.username } })}`; if (type === "empty") return staticProps.empty; + this.success = true; - if (type === "text") + const flags = ephemeral ? 64 : undefined; + + if (type === "text") { return { content: `\`\`\`\n${clean(buffer.toString("utf8"), [], true)}\n\`\`\``, - flags: ephemeral ? 64 : undefined, + flags, }; - return { - files: [ - { - contents: buffer, - name: `${spoiler || result.spoiler ? "SPOILER_" : ""}${staticProps.command}.${type}`, - }, - ], - flags: ephemeral ? 64 : undefined, + } + + const file = { + contents: buffer, + name: `${spoiler || result.spoiler ? "SPOILER_" : ""}${staticProps.command}.${type}`, }; + if (buffer.length > (this.interaction?.attachmentSizeLimit ?? maxFileSize(this.guild))) { + if (process.env.TEMPDIR && process.env.TEMPDIR !== "" && this.permissions.has("EMBED_LINKS")) { + if (this.interaction) { + await upload(this.client, { ...file, flags }, this.interaction); + } else if (this.message) { + await upload(this.client, { ...file, flags }, this.message); + } + } else { + return { + content: this.getString("image.noTempServer"), + flags: 64, + }; + } + } else { + return { + files: [file], + flags, + }; + } } catch (e) { const err = e as Error; if (err.toString().includes("media_not_working")) return this.getString("image.notWorking"); @@ -182,15 +203,24 @@ class MediaCommand extends Command { if (err.toString().includes("No available servers")) return this.getString("image.noServers"); throw err; } finally { - try { - if (status) await status.delete(); - } catch { - // no-op - } + if (status) await status.delete().catch((e) => logger.warn(`Failed to delete status message: ${e}`)); runningCommands.delete(this.author?.id); } } + async finalize(res?: Message) { + if (!this.interaction || !res) return; + const attachment = res.attachments.first(); + if (attachment) { + const path = new URL(attachment.proxyURL); + path.searchParams.set("animated", "true"); + selectedImages.set(this.interaction.user.id, { + path: path.toString(), + spoiler: attachment.filename.startsWith("SPOILER_"), + }); + } + } + processMessage(channel: AnyTextableChannel): Promise { return channel.createMessage({ content: `${random(messages.emotes) || "⚙️"} ${this.getString("image.processing")}`, diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 903accb9..c9542ab8 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -2,13 +2,11 @@ import { Buffer } from "node:buffer"; import process from "node:process"; import type { AnyInteractionGateway } from "oceanic.js"; import Command from "#cmd-classes/command.js"; -import MediaCommand from "#cmd-classes/mediaCommand.js"; -import { collectors, commands, messageCommands, selectedImages, userCommands } from "#utils/collections.js"; +import { collectors, commands, messageCommands, userCommands } from "#utils/collections.js"; import detectRuntime from "#utils/detectRuntime.js"; import { getString } from "#utils/i18n.js"; import logger from "#utils/logger.js"; import { clean } from "#utils/misc.js"; -import { upload } from "#utils/tempimages.js"; import type { EventParams } from "#utils/types.js"; let Sentry: typeof import("@sentry/node-core"); @@ -64,69 +62,27 @@ export default async ({ client, database }: EventParams, interaction: AnyInterac try { const commandClass = new cmd(client, database, { type: "application", interaction }); const result = await commandClass.run(); + + let res; if (typeof result === "string") { - await interaction.createFollowup({ + res = await interaction.createFollowup({ content: result, flags: commandClass.success ? 0 : 64, }); } else if (typeof result === "object") { - if (commandClass instanceof MediaCommand && result.files) { - const fileSize = interaction.attachmentSizeLimit; - const file = result.files[0]; - if (file.contents.length > fileSize) { - if (process.env.TEMPDIR && process.env.TEMPDIR !== "" && interaction.appPermissions.has("EMBED_LINKS")) { - await upload( - client, - { ...file, flags: result.flags }, - interaction, - commandClass.success, - interaction.authorizingIntegrationOwners[0] === undefined, - ); - } else { - await interaction.createFollowup({ - content: getString("image.noTempServer", { locale: interaction.locale }), - flags: 64, - }); - } - } else { - const imgMessage = await interaction.createFollowup({ - flags: result.flags ?? (commandClass.success ? 0 : 64), - files: [file], - }); - if (interaction.authorizingIntegrationOwners[0] === undefined) { - const attachment = imgMessage.message.attachments.first(); - if (attachment) { - const path = new URL(attachment.proxyURL); - path.searchParams.set("animated", "true"); - selectedImages.set(interaction.user.id, { - path: path.toString(), - spoiler: attachment.filename.startsWith("SPOILER_"), - }); - } - } - } - } else { - await interaction.createFollowup( - Object.assign( - { - flags: result.flags ?? (commandClass.success ? 0 : 64), - }, - result, - ), - ); - } - } else { - logger.debug(`Unknown return type for command ${cmdName}: ${result} (${typeof result})`); - if (!result) return; - await interaction.createFollowup( + res = await interaction.createFollowup( Object.assign( { - flags: commandClass.success ? 0 : 64, + flags: result.flags ?? (commandClass.success ? 0 : 64), }, result, ), ); + } else { + logger.debug(`Unknown return type for command ${cmdName}: ${result} (${typeof result})`); } + + await commandClass.finalize(res?.message); } catch (e) { const error = e as Error; if (process.env.SENTRY_DSN && process.env.SENTRY_DSN !== "") diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index ed12627c..5d9fe75a 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -2,14 +2,12 @@ import { Buffer } from "node:buffer"; import process from "node:process"; import { type AnyTextableChannel, GroupChannel, type Message, PrivateChannel, ThreadChannel } from "oceanic.js"; import Command from "#cmd-classes/command.js"; -import MediaCommand from "#cmd-classes/mediaCommand.js"; import { aliases, commands, disabledCache, disabledCmdCache, prefixCache } from "#utils/collections.js"; import detectRuntime from "#utils/detectRuntime.js"; import { getString } from "#utils/i18n.js"; -import { error as _error, log } from "#utils/logger.js"; +import logger from "#utils/logger.js"; import { clean } from "#utils/misc.js"; import parseCommand from "#utils/parseCommand.js"; -import { upload } from "#utils/tempimages.js"; import type { DBGuild, EventParams } from "#utils/types.js"; let Sentry: typeof import("@sentry/node-core"); @@ -146,7 +144,7 @@ export default async ({ client, database }: EventParams, message: Message) => { } // actually run the command - log("log", `${message.author.username} (${message.author.id}) ran classic command ${command}`); + logger.log("log", `${message.author.username} (${message.author.id}) ran classic command ${command}`); const reference = { messageReference: { channelID: message.channelID, @@ -171,9 +169,11 @@ export default async ({ client, database }: EventParams, message: Message) => { const result = await commandClass.run(); const endTime = new Date(); if (endTime.getTime() - startTime.getTime() >= 180000) reference.allowedMentions.repliedUser = true; + + let res; if (typeof result === "string") { reference.allowedMentions.repliedUser = true; - await client.rest.channels.createMessage( + res = await client.rest.channels.createMessage( message.channelID, Object.assign( { @@ -183,42 +183,12 @@ export default async ({ client, database }: EventParams, message: Message) => { ), ); } else if (typeof result === "object") { - if (commandClass instanceof MediaCommand && result.files) { - let fileSize = 10485760; - if (message.guild) { - switch (message.guild.premiumTier) { - case 2: - fileSize = 52428800; - break; - case 3: - fileSize = 104857600; - break; - } - } - const file = result.files[0]; - if (file.contents.length > fileSize) { - if (process.env.TEMPDIR && process.env.TEMPDIR !== "" && commandClass.permissions.has("EMBED_LINKS")) { - await upload(client, { ...file, flags: result.flags }, message); - } else { - await client.rest.channels.createMessage(message.channelID, { - content: getString("image.noTempServer"), - }); - } - } else { - await client.rest.channels.createMessage( - message.channelID, - Object.assign( - { - files: [file], - }, - reference, - ), - ); - } - } else { - await client.rest.channels.createMessage(message.channelID, Object.assign(result, reference)); - } + res = await client.rest.channels.createMessage(message.channelID, Object.assign(result, reference)); + } else { + logger.debug(`Unknown return type for command ${cmdName}: ${result} (${typeof result})`); } + + await commandClass.finalize(res); } catch (e) { const error = e as Error; if (process.env.SENTRY_DSN && process.env.SENTRY_DSN !== "") @@ -260,7 +230,7 @@ export default async ({ client, database }: EventParams, message: Message) => { ), ); } else { - _error(`Error occurred with command message ${message.content}: ${(error as Error).stack || error}`); + logger.error(`Error occurred with command message ${message.content}: ${(error as Error).stack || error}`); try { await client.rest.channels.createMessage( message.channelID, @@ -291,7 +261,7 @@ export default async ({ client, database }: EventParams, message: Message) => { ), ); } catch (err) { - _error( + logger.error( `While attempting to send the previous error message, another error occurred: ${(err as Error).stack || err}`, ); } diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 434652d5..a65cf866 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -2,7 +2,7 @@ import { execFile as baseExecFile } from "node:child_process"; import process from "node:process"; import util, { promisify } from "node:util"; import { type DotenvParseOutput, config } from "dotenv"; -import type { AnyChannel, AnyPrivateChannel, Client, CommandInteraction, Message } from "oceanic.js"; +import type { AnyChannel, AnyPrivateChannel, Client, CommandInteraction, Guild, Message } from "oceanic.js"; import commandsConfig from "#config/commands.json" with { type: "json" }; import messagesConfig from "#config/messages.json" with { type: "json" }; import packageJson from "../../package.json" with { type: "json" }; @@ -297,3 +297,16 @@ export function safeBigInt(input: string | number | bigint | boolean) { return -1; } } + +export function maxFileSize(guild: Guild | null) { + let fileSize = 10485760; + switch (guild?.premiumTier) { + case 2: + fileSize = 52428800; + break; + case 3: + fileSize = 104857600; + break; + } + return fileSize; +} diff --git a/src/utils/tempimages.ts b/src/utils/tempimages.ts index 74da1504..58088dca 100644 --- a/src/utils/tempimages.ts +++ b/src/utils/tempimages.ts @@ -23,13 +23,7 @@ type FileStats = { let dirSizeCache: number; let threshold: number | undefined; -export async function upload( - client: Client, - result: { flags?: number } & File, - context: CommandInteraction | Message, - success = true, - save = false, -) { +export async function upload(client: Client, result: File & { flags?: number }, context: CommandInteraction | Message) { const filename = `${Math.random().toString(36).substring(2, 15)}.${result.name.split(".")[1]}`; await writeFile(`${process.env.TEMPDIR}/${filename}`, result.contents); const imageURL = `${process.env.TMP_DOMAIN || "https://tmp.esmbot.net"}/${filename}`; @@ -51,12 +45,21 @@ export async function upload( })}`, }, ], - flags: (result.flags ?? (success ? 0 : 64)) | Constants.MessageFlags.IS_COMPONENTS_V2, + flags: (result.flags ?? 0) | Constants.MessageFlags.IS_COMPONENTS_V2, }; let authorId: string; if (context instanceof CommandInteraction) { authorId = context.user.id; await context.createFollowup(payload); + + // save media if used outside of guild/dms + if (context.authorizingIntegrationOwners[0] === undefined) { + const type = await request(new URL(imageURL), [], true).catch(() => {}); + selectedImages.set(authorId, { + path: type?.url ?? imageURL, + spoiler: result.name.startsWith("SPOILER_"), + }); + } } else { authorId = context.author.id; await client.rest.channels.createMessage( @@ -74,13 +77,7 @@ export async function upload( }), ); } - if (save) { - const type = await request(new URL(imageURL), [], true).catch(() => {}); - selectedImages.set(authorId, { - path: type?.url ?? imageURL, - spoiler: result.name.startsWith("SPOILER_"), - }); - } + if (threshold) { const size = dirSizeCache + result.contents.length; dirSizeCache = size;