diff --git a/.env.example b/.env.example index c519e97f..fdc9bc6e 100644 --- a/.env.example +++ b/.env.example @@ -43,18 +43,18 @@ SENTRY_DSN= # Put HTML help page output location here, leave blank to disable OUTPUT= -# Put temporary image dir here (make sure it's accessible via a web server), leave blank to disable +# Put temporary media dir here (make sure it's accessible via a web server), leave blank to disable TEMPDIR= -# Put temporary image web server domain +# Put temporary media web server domain TMP_DOMAIN= # Threshold where optional space saving methods will be performed THRESHOLD= # Port for serving metrics. Metrics served are compatible with Prometheus. METRICS= -# The image API type to be used -# Set this to `none` to process all images locally -# Set this to `ws` if you want to use the external image API script, located in api/index.js +# The media API type to be used +# Set this to `none` to process all media locally +# Set this to `ws` if you want to use the external media API script, located in api/index.js API_TYPE=none # Put ID of server to limit owner-only commands to diff --git a/.github/ISSUE_TEMPLATE/error-report.yml b/.github/ISSUE_TEMPLATE/error-report.yml index 33a3cb5a..d0000ddc 100644 --- a/.github/ISSUE_TEMPLATE/error-report.yml +++ b/.github/ISSUE_TEMPLATE/error-report.yml @@ -10,10 +10,10 @@ body: validations: required: true - type: input - id: image + id: media attributes: - label: Image that caused the error - description: If the error is regarding an image command, please post a direct link to the image here. + label: Media file that caused the error + description: If the error is regarding a media command, please post a direct link to the input file here. validations: required: false - type: textarea diff --git a/README.md b/README.md index b3f57764..64ec106d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ esmBot is a free and open-source Discord bot designed to entertain your server. - Powerful, efficient, and performant image processing powered by [libvips](https://github.com/libvips/libvips) - Lots of image manipulation and processing commands out of the box - Handling of output images larger than Discord's upload limit via a local web server -- Optional WebSocket/HTTP-based external image API with load balancing +- Optional WebSocket/HTTP-based external media API - Music playback from many different configurable sources via [Lavalink](https://github.com/lavalink-devs/Lavalink) - Server tags system for saving/retrieving content - Low RAM and CPU usage when idle diff --git a/commands/general/imagereload.js b/commands/general/mediareload.js similarity index 51% rename from commands/general/imagereload.js rename to commands/general/mediareload.js index 1c860d8d..f6c1b9b6 100644 --- a/commands/general/imagereload.js +++ b/commands/general/mediareload.js @@ -1,33 +1,34 @@ import process from "node:process"; import Command from "#cmd-classes/command.js"; -import { reloadImageConnections } from "#utils/image.js"; +import { reloadMediaConnections } from "#utils/media.js"; -class ImageReloadCommand extends Command { +class MediaReloadCommand extends Command { async run() { const owners = process.env.OWNER?.split(",") ?? []; if (!owners.includes(this.author.id)) { this.success = false; - return this.getString("commands.responses.imagereload.owner"); + return this.getString("commands.responses.mediareload.owner"); } await this.acknowledge(); - const length = await reloadImageConnections(); + const length = await reloadMediaConnections(); if (length) { if (process.env.PM2_USAGE) { process.send?.({ type: "process:msg", data: { - type: "imagereload", + type: "mediareload", from: process.env.pm_id, }, }); } - return this.getString("commands.responses.imagereload.connected", { params: { length: length.toString() } }); + return this.getString("commands.responses.mediareload.connected", { params: { length: length.toString() } }); } - return this.getString("commands.responses.imagereload.couldNotConnect"); + return this.getString("commands.responses.mediareload.couldNotConnect"); } - static description = "Attempts to reconnect to all available image processing servers"; + static description = "Attempts to reconnect to all available media processing servers"; static adminOnly = true; + static aliases = ["imagereload"]; } -export default ImageReloadCommand; +export default MediaReloadCommand; diff --git a/commands/general/imagestats.js b/commands/general/mediastats.js similarity index 64% rename from commands/general/imagestats.js rename to commands/general/mediastats.js index 0ebad5d7..b7ce7dce 100644 --- a/commands/general/imagestats.js +++ b/commands/general/mediastats.js @@ -1,7 +1,7 @@ import Command from "#cmd-classes/command.js"; -import { connections } from "#utils/image.js"; +import { connections } from "#utils/media.js"; -class ImageStatsCommand extends Command { +class MediaStatsCommand extends Command { async run() { if (!this.permissions.has("EMBED_LINKS")) { this.success = false; @@ -12,11 +12,11 @@ class ImageStatsCommand extends Command { embeds: [ { author: { - name: this.getString("commands.responses.imagestats.header"), + name: this.getString("commands.responses.mediastats.header"), iconURL: this.client.user.avatarURL(), }, color: 0xff0000, - description: this.getString("commands.responses.imagestats.connected", { + description: this.getString("commands.responses.mediastats.connected", { params: { length: connections.size.toString() }, }), fields: [{ name: "", value: "" }], @@ -28,17 +28,17 @@ class ImageStatsCommand extends Command { for (const connection of connections.values()) { const count = await connection.getCount(); embed.embeds[0].fields.push({ - name: this.getString("commands.responses.imagestats.server", { + name: this.getString("commands.responses.mediastats.server", { params: { num: (connection.name ? `${i++} (${connection.name})` : i++).toString() }, }), - value: this.getString("commands.responses.imagestats.runningJobs", { params: { count: count.toString() } }), + value: this.getString("commands.responses.mediastats.runningJobs", { params: { count: count.toString() } }), }); } return embed; } - static description = "Gets some statistics about the image servers"; - static aliases = ["imgstat", "imstats", "imgstats", "imstat"]; + static description = "Gets some statistics about the media servers"; + static aliases = ["imgstat", "imstats", "imgstats", "imstat", "imagestats", "mstat", "mstats"]; } -export default ImageStatsCommand; +export default MediaStatsCommand; diff --git a/docs/config.md b/docs/config.md index 20732a26..99fadc37 100644 --- a/docs/config.md +++ b/docs/config.md @@ -24,11 +24,11 @@ These variables that are not necessarily required for the bot to run, but can gr - `REST_PROXY`: A Discord REST URL to use besides the default (`https://discord.com/api/v{version_number}`). Can be useful for proxying requests to Discord through an external server. - `SENTRY_DSN`: A DSN endpoint URL from [Sentry](https://sentry.io). This is a fully optional integration for error monitoring and statistics. - `OUTPUT`: A directory to output the help documentation in Markdown format to. It's recommended to set this to a directory being served by a web server. -- `TEMPDIR`: A directory that will store generated images larger than Discord's filesize limit. It's recommended to set this to a directory being served by a web server. -- `TMP_DOMAIN`: The root domain/directory that the images larger than Discord's filesize limit are stored at. Example: `https://projectlounge.pw/tmp` +- `TEMPDIR`: A directory that will store generated media files larger than Discord's filesize limit. It's recommended to set this to a directory being served by a web server. +- `TMP_DOMAIN`: The root domain/directory that the media files larger than Discord's filesize limit are stored at. Example: `https://projectlounge.pw/tmp` - `THRESHOLD`: A filesize threshold that the bot will start deleting old files in `TEMPDIR` at. - `METRICS`: The HTTP port to serve [Prometheus](https://prometheus.io/)-compatible metrics on. -- `API_TYPE`: Set this to "none" if you want to process all images locally. Alternatively, set it to "ws" to use an image API server specified in the `image` block of `config/servers.json`. +- `API_TYPE`: Set this to "none" if you want to process all media locally. Alternatively, set it to "ws" to use a media API server specified in the `image` block of `config/servers.json`. - `ADMIN_SERVER`: A Discord server/guild ID to limit owner-only commands such as eval to. ## JSON @@ -73,7 +73,7 @@ The JSON-based configuration files are located in `config/`. "auth": "youshallnotpass" // Password/authorization code for the server } ], - "image": [ // Objects containing info for connecting to WS image server(s) + "image": [ // Objects containing info for connecting to WS media server(s) { "name": "test", // A human-friendly name for the server "server": "localhost", // IP address or domain name for the server diff --git a/docs/runtimes.md b/docs/runtimes.md index 72e2c2f8..a26107f0 100644 --- a/docs/runtimes.md +++ b/docs/runtimes.md @@ -37,4 +37,4 @@ Here's the current list of behavior changes exclusive to Deno: ## Known Issues -- You may experience crashes on startup involving native modules (e.g. better-sqlite3, the image natives) when running with Deno or Bun. For Deno, try switching your Node.js version to 22 and running `pnpm rebuild`. For Bun, you should switch to Node.js 24 and then run the same command. +- You may experience crashes on startup involving native modules (e.g. better-sqlite3, the media natives) when running with Deno or Bun. For Deno, try switching your Node.js version to 22 and running `pnpm rebuild`. For Bun, you should switch to Node.js 24 and then run the same command. diff --git a/locales/en-US.json b/locales/en-US.json index 72e1cfac..5cb4071a 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -21,8 +21,8 @@ "exec": "Executes a shell command", "help": "Gets a list of commands", "image": "Searches for images across the web", - "imagereload": "Attempts to reconnect to all available image processing servers", - "imagestats": "Gets some statistics about the image servers", + "mediareload": "Attempts to reconnect to all available media processing servers", + "mediastats": "Gets some statistics about the media servers", "info": "Gets some info and credits about me", "lengthen": "Lengthens a short URL", "ping": "Pings Discord's servers", @@ -551,8 +551,8 @@ "exec": "exec", "help": "help", "image": "image", - "imagereload": "imagereload", - "imagestats": "imagestats", + "mediareload": "mediareload", + "mediastats": "mediastats", "info": "info", "lengthen": "lengthen", "ping": "ping", @@ -827,14 +827,14 @@ "noResults": "I couldn't find any results!", "results": "Search Results" }, - "imagereload": { - "connected": "Successfully connected to {{length}} image server(s).", - "couldNotConnect": "I couldn't connect to any image servers!", - "owner": "Only the bot owner can reload the image servers!" + "mediareload": { + "connected": "Successfully connected to {{length}} media server(s).", + "couldNotConnect": "I couldn't connect to any media servers!", + "owner": "Only the bot owner can reload the media servers!" }, - "imagestats": { - "connected": "The bot is currently connected to {{length}} image server(s).", - "header": "esmBot Image Statistics", + "mediastats": { + "connected": "The bot is currently connected to {{length}} media server(s).", + "header": "esmBot Media Statistics", "runningJobs": "Running Jobs: {{count}}", "server": "Server {{num}}" }, diff --git a/natives/node/worker.cc b/natives/node/worker.cc index c35a6970..1c49f988 100644 --- a/natives/node/worker.cc +++ b/natives/node/worker.cc @@ -23,7 +23,7 @@ void ImageAsyncWorker::OnError(const Error &e) { vips_error_clear(); vips_thread_shutdown(); if (shouldKill) { - deferred.Reject(Napi::Error::New(Env(), "image_job_killed").Value()); + deferred.Reject(Napi::Error::New(Env(), "media_job_killed").Value()); } else { deferred.Reject(e.Value()); } diff --git a/src/api/IMPLEMENTATION.md b/src/api/IMPLEMENTATION.md index 16393dfa..72a969b9 100644 --- a/src/api/IMPLEMENTATION.md +++ b/src/api/IMPLEMENTATION.md @@ -1,12 +1,12 @@ -# esmBot Image API +# esmBot Media API -The esmBot image API is a combined HTTP and WebSocket API. The default port to access the API is 3762. The API supports very basic authentication, which is defined on the server via the `PASS` environment variable and is sent from the client via the Authentication header in both HTTP and WS requests. +The esmBot Media API is a combined HTTP and WebSocket API. The default port to access the API is 3762. The API supports very basic authentication, which is defined on the server via the `PASS` environment variable and is sent from the client via the Authentication header in both HTTP and WS requests. ## HTTP ### GET `/image/?id=` -Get image data after job is finished running. The Content-Type header is properly set. +Get output data after job is finished running. The Content-Type header is properly set. ### GET `/count` @@ -53,9 +53,9 @@ The job object is formatted like this: ```js { - "cmd": string, // name of internal image command, e.g. caption - "path": string, // canonical image URL, used for getting the actual image - "url": string, // original image URL, used for message filtering + "cmd": string, // name of internal media command, e.g. caption + "path": string, // canonical media URL, used for getting the actual input + "url": string, // original media URL, used for message filtering "params": { // content varies depending on the command, some common parameters are listed here "togif": boolean, // convert output to gif ... @@ -63,7 +63,7 @@ The job object is formatted like this: "input": { "type": string // mime type of output, should usually be the same as input }, - "name": string, // filename of the image, without extension + "name": string, // filename of the media file, without extension "ephemeral": string, // whether to post the output as an ephemeral message (only when responding directly, see below section) "spoiler": string // whether to post the output as a spoiler (only when responding directly, see below section) } @@ -71,7 +71,7 @@ The job object is formatted like this: ### Direct Posting -The image API will attempt to respond to a command by itself if all of the following criteria is met: +The media API will attempt to respond to a command by itself if all of the following criteria is met: - The original request was done through an interaction/slash command - The bot's application/user ID is specified on the API server through the `CLIENT_ID` environment variable diff --git a/src/api/index.ts b/src/api/index.ts index dfe65456..7f59a205 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -86,13 +86,13 @@ const jobs = new JobCache(); const PASS = process.env.PASS ? process.env.PASS : undefined; -// Used for direct image uploads +// Used for direct media uploads const clientID = process.env.CLIENT_ID; const discordBaseURL = process.env.REST_PROXY && process.env.REST_PROXY !== "" ? process.env.REST_PROXY : "https://discord.com/api/v10"; /** - * Accept an image job. + * Accept a media job. */ async function acceptJob(id: bigint, sock: WSocket): Promise { const job = jobs.get(id)!; @@ -121,7 +121,7 @@ async function acceptJob(id: bigint, sock: WSocket): Promise { // Because malloc_trim can sometimes take longer than expected, // we wait until all* jobs are finished before trimming to avoid potential issues. - // See the comment in natives/node/image.cc for more info + // See the comment in natives/node/media.cc for more info if (jobs.size <= 1) { media.trim(); } @@ -236,7 +236,7 @@ httpServer.on("request", (req, res) => { res.statusCode = 404; return res.end("404 Not Found"); } - log(`Sending image data for job ${id} to ${req.socket.remoteAddress}:${req.socket.remotePort} via HTTP`); + log(`Sending media data for job ${id} to ${req.socket.remoteAddress}:${req.socket.remotePort} via HTTP`); const job = jobs.get(id)!; let contentType: string | undefined; switch (job.ext) { @@ -419,15 +419,15 @@ async function finishJob( } /** - * Run an image job. + * Run a media job. */ async function runJob(job: MiniJob, ws: WSocket): Promise { log(`Job ${job.id} starting...`, job.num); const object = job.msg; - // If the image has a path, it must also have a type + // If the input has a path, it must also have a type if (object.path && !object.input?.type) { - throw new TypeError("Unknown image type"); + throw new TypeError("Unknown media type"); } log(`Job ${job.id} started`, job.num); diff --git a/src/app.ts b/src/app.ts index 00fb0505..7698b63f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -46,8 +46,8 @@ import commandConfig from "#config/commands.json" with { type: "json" }; import { locales, paths } from "#utils/collections.js"; import detectRuntime from "#utils/detectRuntime.js"; import { load } from "#utils/handler.js"; -import { initImageLib, reloadImageConnections } from "#utils/image.js"; import logger from "#utils/logger.js"; +import { initMediaLib, reloadMediaConnections } from "#utils/media.js"; import { endBroadcast, exit, startBroadcast } from "#utils/misc.js"; import { connect, connected, reload } from "#utils/soundplayer.js"; import { parseThreshold } from "#utils/tempimages.js"; @@ -144,8 +144,8 @@ logger.log("info", "Finished loading commands."); if (database) { await database.setup(); } -if (process.env.API_TYPE === "ws") await reloadImageConnections(); -else initImageLib(); +if (process.env.API_TYPE === "ws") await reloadMediaConnections(); +else initMediaLib(); const shardArray = process.env.SHARDS && process.env.pm_id @@ -224,8 +224,8 @@ if (process.env.PM2_USAGE) { case "soundreload": await reload(client); break; - case "imagereload": - await reloadImageConnections(); + case "mediareload": + await reloadMediaConnections(); break; case "broadcastStart": startBroadcast(client, packet.data.message); diff --git a/src/classes/imageCommand.ts b/src/classes/imageCommand.ts index 4e832197..13768b0e 100644 --- a/src/classes/imageCommand.ts +++ b/src/classes/imageCommand.ts @@ -11,7 +11,7 @@ import { import messages from "#config/messages.json" with { type: "json" }; import { runningCommands, selectedImages } from "#utils/collections.js"; import { getAllLocalizations } from "#utils/i18n.js"; -import { runImageJob } from "#utils/image.js"; +import { runMediaJob } from "#utils/media.js"; import imageDetect, { type ImageMeta } from "#utils/mediadetect.js"; import { clean, isEmpty, random } from "#utils/misc.js"; import type { MediaParams } from "#utils/types.js"; @@ -161,7 +161,7 @@ class ImageCommand extends Command { } try { - const result = await runImageJob(imageParams); + const result = await runMediaJob(imageParams); const buffer = result.buffer; const type = result.type; if (type === "sent") { @@ -203,10 +203,10 @@ class ImageCommand extends Command { }; } catch (e) { const err = e as Error; - if (err.toString().includes("image_not_working")) return this.getString("image.notWorking"); + if (err.toString().includes("media_not_working")) return this.getString("image.notWorking"); if (err.toString().includes("Request ended prematurely due to a closed connection")) return this.getString("image.tryAgain"); - if (err.toString().includes("image_job_killed") || err.toString().includes("Timeout")) + if (err.toString().includes("media_job_killed") || err.toString().includes("Timeout")) return this.getString("image.tooLong"); if (err.toString().includes("No available servers")) return this.getString("image.noServers"); throw err; diff --git a/src/utils/image.ts b/src/utils/media.ts similarity index 92% rename from src/utils/image.ts rename to src/utils/media.ts index 8a3aa3d8..184e56e4 100644 --- a/src/utils/image.ts +++ b/src/utils/media.ts @@ -7,7 +7,7 @@ import { fileTypeFromStream } from "file-type"; import ipaddr from "ipaddr.js"; import serversConfig from "#config/servers.json" with { type: "json" }; import logger from "./logger.ts"; -import ImageConnection from "./mediaConnection.ts"; +import MediaConnection from "./mediaConnection.ts"; import { random } from "./misc.ts"; import type { MediaParams, MediaTypeData } from "./types.ts"; @@ -31,10 +31,10 @@ const formats = [ "video/quicktime", "image/avif", ]; -export const connections = new Map(); +export const connections = new Map(); export let servers: ServerConfig[] = process.env.API_TYPE === "ws" ? serversConfig.image : []; -export function initImageLib() { +export function initMediaLib() { const nodeRequire = createRequire(import.meta.url); const imgLib = nodeRequire( `../../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/esmbmedia.node`, @@ -113,7 +113,7 @@ export async function getType(image: URL, extraReturnTypes: boolean): Promise { +async function getIdeal(object: MediaParams): Promise { const idealServers = []; for (const connection of connections.values()) { if (connection.conn.readyState !== 0 && connection.conn.readyState !== 1) { @@ -163,7 +163,7 @@ async function getIdeal(object: MediaParams): Promise { +export async function runMediaJob(params: MediaParams): Promise<{ buffer: Buffer; type: string }> { if (process.env.API_TYPE === "ws") { const currentServer = await getIdeal(params); if (!currentServer) @@ -193,7 +193,7 @@ export async function runImageJob(params: MediaParams): Promise<{ buffer: Buffer }; } if (run) { - // Called from command (not using image API) + // Called from command (not using media API) running++; const data = await run(params).finally(() => { running--; @@ -207,5 +207,5 @@ export async function runImageJob(params: MediaParams): Promise<{ buffer: Buffer type: data.fileExtension, }; } - throw "image_not_working"; + throw "media_not_working"; } diff --git a/src/utils/mediadetect.ts b/src/utils/mediadetect.ts index be11cb40..71cb67a7 100644 --- a/src/utils/mediadetect.ts +++ b/src/utils/mediadetect.ts @@ -11,8 +11,8 @@ import { TextableChannel, ThreadChannel, } from "oceanic.js"; -import { getType } from "./image.ts"; import logger from "./logger.ts"; +import { getType } from "./media.ts"; import type { MediaTypeData } from "./types.ts"; const tenorURLs = ["tenor.com", "www.tenor.com"]; diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 21e9f5ba..04d705f1 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -8,7 +8,7 @@ import commandsConfig from "#config/commands.json" with { type: "json" }; import messagesConfig from "#config/messages.json" with { type: "json" }; import type { DatabasePlugin } from "../database.ts"; -import { disconnect, servers } from "./image.ts"; +import { disconnect, servers } from "./media.ts"; const pm2 = process.env.PM2_USAGE ? (await import("pm2")).default : null; diff --git a/src/utils/tempimages.ts b/src/utils/tempimages.ts index d357bf2e..d28f6d21 100644 --- a/src/utils/tempimages.ts +++ b/src/utils/tempimages.ts @@ -13,8 +13,8 @@ import { import { selectedImages } from "./collections.ts"; import { getString } from "./i18n.ts"; -import { getType } from "./image.ts"; import logger from "./logger.ts"; +import { getType } from "./media.ts"; type SizeSuffix = "K" | "M" | "G" | "T"; type FileStats = {