diff --git a/app.js b/app.js index 3c9d806..06a2ec1 100644 --- a/app.js +++ b/app.js @@ -71,12 +71,14 @@ const services = [ if (process.env.METRICS && process.env.METRICS !== "") services.push({ name: "prometheus", ServiceWorker: PrometheusWorker }); const intents = [ - "guilds", "guildVoiceStates", - "guildMessages", "directMessages" ]; -if (types.classic) intents.push("messageContent"); +if (types.classic) { + intents.push("guilds"); + intents.push("guildMessages"); + intents.push("messageContent"); +} const Admiral = new Fleet({ BotWorker: Shard, diff --git a/commands/music/stop.js b/commands/music/stop.js index f5a4fbc..d11a59a 100644 --- a/commands/music/stop.js +++ b/commands/music/stop.js @@ -18,7 +18,7 @@ class StopCommand extends MusicCommand { players.delete(this.channel.guild.id); queues.delete(this.channel.guild.id); this.success = true; - return "🔊 The current voice channel session has ended."; + return `🔊 The voice channel session in \`${this.connection.voiceChannel.name}\` has ended.`; } static description = "Stops the music"; diff --git a/docs/config.md b/docs/config.md index 5d14e03..14e4731 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,15 +1,18 @@ # Config -esmBot uses environment variables for configuration. To make managing them easier, a `.env` file is included with the bot and can be used to load the variables on bot startup. +esmBot uses a mix of environment variables and JSON for configuration. -Here's an overview of the environment variables required to run the bot: +## Environment Variables (.env) +To make managing environment variables easier, an example `.env` file is included with the bot at `.env.example` and can be used to load the variables on startup. +### Required - `NODE_ENV`: Used for tuning the bot to different environments. If you don't know what to set it to, leave it as is. - `TOKEN`: Your bot's token. You can find this [here](https://discord.com/developers/applications) under your application's Bot tab. - `DB`: The database connection string. By default the `sqlite` and `postgresql` protocols are available, but this can be expanded by putting proper DB driver scripts into `utils/database/`. You can also set this to `dummy` to make the bot not use a database at all. - `OWNER`: Your Discord user ID. This is used for granting yourself access to certain management commands. Adding multiple users is supported by separating the IDs with a comma; however, this is not recommended for security purposes. -- `PREFIX`: The bot's default command prefix. Note that servers can set their own individual prefixes via the `prefix` command. +- `PREFIX`: The bot's default command prefix for classic commands. Note that servers can set their own individual prefixes via the `prefix` command. -Here's an overview of the variables that are not necessarily required for the bot to run, but can greatly enhance its functionality: +### Optional +These variables that are not necessarily required for the bot to run, but can greatly enhance its functionality: - `STAYVC`: Set this to true if you want the bot to stay in voice chat after playing music/a sound effect. You can make it leave by using the stop command. - `DBL`: An API token from [Top.gg](https://top.gg/). Unnecessary for most users since Top.gg tends to ban forks of bots like esmBot from their list. @@ -19,7 +22,60 @@ Here's an overview of the variables that are not necessarily required for the bo - `TMP_DOMAIN`: The root domain/directory that the images larger than 8MB 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`: 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 `servers.json`, or "azure" to use the Azure Functions-based API. +- `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`, or "azure" to use the Azure Functions-based API. - `AZURE_URL`: Your Azure webhook URL. Only applies if `API` is set to "azure". - `AZURE_PASS`: An optional password used for Azure requests. Only applies if `API` is set to "azure". -- `ADMIN_SERVER`: A server to limit owner-only commands to. \ No newline at end of file +- `ADMIN_SERVER`: A Discord server/guild ID to limit owner-only commands such as eval to. + +## JSON +The JSON-based configuration files are located in `config/`. + +### commands.json +```js +{ + "types": { + "classic": false, // Enable/disable "classic" (prefixed) commands, note that classic commands in direct messages will still work + "application": true // Enable/disable application commands (slash and context menu commands) + }, + "blacklist": [ + // Names of commands that you don't want the bot to load + ] +} +``` + +### messages.json +```js +{ + "emotes": [ + // Discord emote strings to use in the "Processing... this may take a while" messages, e.g. "" or "⚙️" + ], + "messages": [ + // Strings to use in the bot's activity message/playing status + ] +} +``` + +### servers.json +```js +{ + "lava": [ // Objects containing info for connecting to Lavalink audio server(s) + { + "name": "test", // A human-friendly name for the server + "url": "localhost:2333", // IP address/domain name and port for the server + "auth": "youshallnotpass", // Password/authorization code for the server + "local": false // Whether or not the esmBot "assets" folder is located next to the Lavalink jar file + } + ], + "image": [ // Objects containing info for connecting to WS image server(s) + { + "server": "localhost", // IP address or domain name for the server + "auth": "verycoolpass100", // Password/authorization code for the server + "tls": false // Whether or not this is a secure TLS/wss connection + } + ], + "searx": [ + // URLs for Searx/SearXNG instances used for image/YouTube searches, e.g. "https://searx.projectlounge.pw" + // Note: instances must support getting results over JSON + ] +} +``` \ No newline at end of file diff --git a/docs/custom-commands.md b/docs/custom-commands.md index 2425b5c..605a2bf 100644 --- a/docs/custom-commands.md +++ b/docs/custom-commands.md @@ -1,5 +1,5 @@ # Custom Commands -esmBot has a flexible command handler, allowing you to create new commands and categories simply by creating new files. This page will provide a reference for creating new commands. +esmBot has a powerful and flexible command handler, allowing you to create new commands and categories simply by creating new files. This page will provide a reference for creating new commands. ## Directory Structure The bot loads commands from subdirectories inside of the `commands` directory, which looks something like this by default: @@ -19,6 +19,9 @@ commands/ ``` As you can see, each command is grouped into categories, which are represented by subdirectories. To create a new category, you can simply create a new directory inside of the `commands` directory, and to create a new command, you can create a new JS file under one of those subdirectories. +!!! tip + The `message` category is special; commands in here act as right-click context menu message commands instead of "classic" or slash commands. + ## Commnand Structure It's recommended to use the `Command` class located in `classes/command.js` to create a new command in most cases. This class provides various parameters and fields that will likely be useful when creating a command. Here is a simple example of a working command file: ```js @@ -46,7 +49,7 @@ The parameters available to your command consist of the following: - `this.worker`: The ID of the current eris-fleet worker. This should be a number greater than or equal to 0. - `this.ipc`: An eris-fleet [`IPC`](https://danclay.github.io/eris-fleet/classes/IPC.html) instance, useful for communication between worker processes. - `this.origOptions`: The raw options object provided to the command by the command handler. -- `this.type`: The type of message that activated the command. Can be "classic" (a regular message) or "application" (slash commands). +- `this.type`: The type of message that activated the command. Can be "classic" (a regular message) or "application" (slash/context menu commands). - `this.channel`: An Eris [`TextChannel`](https://abal.moe/Eris/docs/TextChannel) object of the channel that the command was run in, useful for getting info about a server and how to respond to a message. - `this.author`: An Eris [`User`](https://abal.moe/Eris/docs/User) object of the user who ran the command, or a [`Member`](https://abal.moe/Eris/docs/Member) object identical to `this.member` if run in a server as a slash command. - `this.member`: An Eris [`Member`](https://abal.moe/Eris/docs/Member) object of the server member who ran the command. When running the command outside of a server, this parameter is undefined when run as a "classic" command or a [`User`](https://abal.moe/Eris/docs/User) object identical to `this.author` when run as a slash command. @@ -59,10 +62,11 @@ Some options are only available depending on the context/original message type, - `this.content`: A string of the raw content of the command message, excluding the prefix and command name. - `this.reference`: An object that's useful if you ever decide to reply to a user inside the command. You can use [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) to combine your message content with this parameter. -The options only available with "application"/slash commands are listed below: +The options only available with application (slash and context menu) commands are listed below: - `this.interaction`: An Eris [`CommandInteraction`](https://abal.moe/Eris/docs/CommandInteraction) object of the incoming slash command data. - `this.optionsArray`: A raw array of command options. Should rarely be used. +- `this.success`: A boolean value that causes the bot to respond with a normal message when `true`, or an "ephemeral" message (a message that's only visible to the person who ran the command) when `false`. Some static fields are also available and can be set depending on your command. These fields are listed below: @@ -80,6 +84,7 @@ static flags = [{ ``` - `slashAllowed`: Specifies whether or not the command is available via slash commands. - `directAllowed`: Specifies whether or not a command is available in direct messages. +- `adminOnly`: Specifies whether or not a command should be limited to the bot owner(s). ## The `run` Function The main JS code of your command is specified in the `run` function. This function should return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) of your command output, which is why the `run` function [is an async function by default](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function). The return value inside the `Promise` should be either a string or an object; you should return a string whenever you intend to reply with plain text, or an object if you intend to reply with something else, such as an embed or attachment. \ No newline at end of file diff --git a/docs/docker.md b/docs/docker.md index 217c701..6475c81 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -16,7 +16,7 @@ You should then modify the `config/servers.json` file to change the IP addresses ```json { "lava": [ - { "name": "localhost", "url": "localhost:2333", "auth": "youshallnotpass", "local": true } + { "name": "localhost", "url": "lavalink:2333", "auth": "youshallnotpass", "local": true } ], "image": [ { "server": "api", "auth": "verycoolpass100", "tls": false } diff --git a/docs/setup.md b/docs/setup.md index 44ed504..c037b6b 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -37,7 +37,7 @@ Choose the distro you're using below for insallation instructions. ```sh sudo pacman -S git curl cmake pango ffmpeg npm imagemagick libvips sqlite3 libltdl noto-fonts-emoji gobject-introspection libcgif libimagequant meson ``` - You'll also need to install [`ttf-ms-fonts`](https://aur.archlinux.org/packages/ttf-ms-fonts/) from the AUR. + You'll also need to install [`ttf-ms-win10-auto`](https://aur.archlinux.org/packages/ttf-ms-win10-auto/) from the AUR. *** diff --git a/events/voiceChannelLeave.js b/events/voiceChannelLeave.js index 1d941e4..8d03784 100644 --- a/events/voiceChannelLeave.js +++ b/events/voiceChannelLeave.js @@ -42,7 +42,7 @@ export default async (client, cluster, worker, ipc, member, oldChannel) => { players.delete(connection.originalChannel.guild.id); queues.delete(connection.originalChannel.guild.id); skipVotes.delete(connection.originalChannel.guild.id); - client.createMessage(connection.originalChannel.id, "🔊 The current voice channel session has ended."); + client.createMessage(connection.originalChannel.id, `🔊 The voice channel session in \`${connection.originalChannel.name}\` has ended.`); } }); } else if (member.id === connection.host) { @@ -74,7 +74,7 @@ export default async (client, cluster, worker, ipc, member, oldChannel) => { players.delete(connection.originalChannel.guild.id); queues.delete(connection.originalChannel.guild.id); skipVotes.delete(connection.originalChannel.guild.id); - client.createMessage(connection.originalChannel.id, "🔊 The current voice channel session has ended."); + client.createMessage(connection.originalChannel.id, `🔊 The voice channel session in \`${connection.originalChannel.name}\` has ended.`); } else { const randomMember = random(members); players.set(connection.voiceChannel.guild.id, { player: connection.player, type: connection.type, host: randomMember.id, voiceChannel: connection.voiceChannel, originalChannel: connection.originalChannel, loop: connection.loop, shuffle: connection.shuffle, playMessage: connection.playMessage }); @@ -92,7 +92,7 @@ export default async (client, cluster, worker, ipc, member, oldChannel) => { players.delete(connection.originalChannel.guild.id); queues.delete(connection.originalChannel.guild.id); skipVotes.delete(connection.originalChannel.guild.id); - await client.createMessage(connection.originalChannel.id, "🔊 The current voice channel session has ended."); + await client.createMessage(connection.originalChannel.id, `🔊 The voice channel session in \`${connection.originalChannel.name}\` has ended.`); } } }; diff --git a/shard.js b/shard.js index d953b8d..5083977 100644 --- a/shard.js +++ b/shard.js @@ -78,10 +78,7 @@ class Shard extends BaseClusterWorker { log("log", `Loading event from ${file}...`); const eventArray = file.split("/"); const eventName = eventArray[eventArray.length - 1].split(".")[0]; - if (eventName === "messageCreate" && !types.classic) { - log("warn", `Skipped loading event from ${file} because classic commands are disabled...`); - continue; - } else if (eventName === "interactionCreate" && !types.application) { + if (eventName === "interactionCreate" && !types.application) { log("warn", `Skipped loading event from ${file} because application commands are disabled`); continue; } diff --git a/utils/awaitrejoin.js b/utils/awaitrejoin.js index 6556c45..55408ef 100644 --- a/utils/awaitrejoin.js +++ b/utils/awaitrejoin.js @@ -14,17 +14,19 @@ class AwaitRejoin extends EventEmitter { this.listener = (member, newChannel) => this.verify(member, newChannel); this.bot.on("voiceChannelJoin", this.listener); this.bot.on("voiceChannelSwitch", this.listener); - setTimeout(() => this.stop(), 10000); + this.stopTimeout = setTimeout(() => this.stop(), 10000); this.checkInterval = setInterval(() => this.verify({ id: memberID }, channel, true), 1000); } verify(member, channel, checked) { if (this.channel.id === channel.id) { if ((this.member === member.id && this.channel.voiceMembers.has(member.id)) || (this.anyone && !checked)) { + clearTimeout(this.stopTimeout); this.rejoined = true; this.stop(member); return true; } else if (this.anyone && (!checked || this.channel.voiceMembers.size > 1)) { + clearTimeout(this.stopTimeout); this.rejoined = true; this.stop(random(this.channel.voiceMembers.filter((i) => i.id !== this.bot.user.id && !i.bot))); return true; diff --git a/utils/services/image.js b/utils/services/image.js index 62744e4..41ad952 100644 --- a/utils/services/image.js +++ b/utils/services/image.js @@ -11,7 +11,7 @@ import EventEmitter from "events"; // only requiring this to work around an issue regarding worker threads const nodeRequire = createRequire(import.meta.url); -if (process.env.API_TYPE === "none") { +if (!process.env.API_TYPE || process.env.API_TYPE === "none") { nodeRequire(`../../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`); } diff --git a/utils/soundplayer.js b/utils/soundplayer.js index d237495..4b45979 100644 --- a/utils/soundplayer.js +++ b/utils/soundplayer.js @@ -23,7 +23,7 @@ export async function checkStatus() { const response = await request(`http://${node.url}/version`, { headers: { authorization: node.auth } }).then(res => res.body.text()); if (response) newNodes.push(node); } catch { - logger.error(`Failed to get status of Lavalink node ${node.host}.`); + logger.error(`Failed to get status of Lavalink node ${node.url}.`); } } nodes = newNodes; @@ -230,8 +230,10 @@ export async function nextSong(client, options, connection, track, info, music, const newTrack = await connection.node.rest.decode(newQueue[0]); nextSong(client, options, connection, newQueue[0], newTrack, music, voiceChannel, host, player.loop, player.shuffle, track); try { - if (newQueue[0] !== track && playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); - if (newQueue[0] !== track && player.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete(); + if (options.type === "classic") { + if (newQueue[0] !== track && playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); + if (newQueue[0] !== track && player.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete(); + } } catch { // no-op } @@ -241,19 +243,14 @@ export async function nextSong(client, options, connection, track, info, music, players.delete(voiceChannel.guild.id); queues.delete(voiceChannel.guild.id); skipVotes.delete(voiceChannel.guild.id); - const content = "🔊 The current voice channel session has ended."; + const content = `🔊 The voice channel session in \`${voiceChannel.name}\` has ended.`; if (options.type === "classic") { await client.createMessage(options.channel.id, content); } else { await options.interaction.createMessage(content); } - try { - if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); - if (player?.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete(); - } catch { - // no-op - } - } else { + } + if (options.type === "classic") { try { if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); if (player?.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete();