Allow classic commands in DMs even when disabled, add channel name to music end message, better check for API_TYPE, update docs

This commit is contained in:
Essem 2022-09-09 14:55:03 -05:00
parent 50bff306c0
commit e474d838b0
No known key found for this signature in database
GPG Key ID: 7D497397CC3A2A8C
11 changed files with 94 additions and 35 deletions

8
app.js
View File

@ -71,12 +71,14 @@ const services = [
if (process.env.METRICS && process.env.METRICS !== "") services.push({ name: "prometheus", ServiceWorker: PrometheusWorker }); if (process.env.METRICS && process.env.METRICS !== "") services.push({ name: "prometheus", ServiceWorker: PrometheusWorker });
const intents = [ const intents = [
"guilds",
"guildVoiceStates", "guildVoiceStates",
"guildMessages",
"directMessages" "directMessages"
]; ];
if (types.classic) intents.push("messageContent"); if (types.classic) {
intents.push("guilds");
intents.push("guildMessages");
intents.push("messageContent");
}
const Admiral = new Fleet({ const Admiral = new Fleet({
BotWorker: Shard, BotWorker: Shard,

View File

@ -18,7 +18,7 @@ class StopCommand extends MusicCommand {
players.delete(this.channel.guild.id); players.delete(this.channel.guild.id);
queues.delete(this.channel.guild.id); queues.delete(this.channel.guild.id);
this.success = true; 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"; static description = "Stops the music";

View File

@ -1,15 +1,18 @@
# Config # 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. - `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. - `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. - `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. - `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. - `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. - `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` - `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. - `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. - `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_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". - `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. - `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. "<a:processing:818243325891051581>" 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
]
}
```

View File

@ -1,5 +1,5 @@
# Custom Commands # 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 ## Directory Structure
The bot loads commands from subdirectories inside of the `commands` directory, which looks something like this by default: 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. 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 ## 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: 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 ```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.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.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.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.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.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. - `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.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. - `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.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.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: 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. - `slashAllowed`: Specifies whether or not the command is available via slash commands.
- `directAllowed`: Specifies whether or not a command is available in direct messages. - `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 `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. 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.

View File

@ -16,7 +16,7 @@ You should then modify the `config/servers.json` file to change the IP addresses
```json ```json
{ {
"lava": [ "lava": [
{ "name": "localhost", "url": "localhost:2333", "auth": "youshallnotpass", "local": true } { "name": "localhost", "url": "lavalink:2333", "auth": "youshallnotpass", "local": true }
], ],
"image": [ "image": [
{ "server": "api", "auth": "verycoolpass100", "tls": false } { "server": "api", "auth": "verycoolpass100", "tls": false }

View File

@ -37,7 +37,7 @@ Choose the distro you're using below for insallation instructions.
```sh ```sh
sudo pacman -S git curl cmake pango ffmpeg npm imagemagick libvips sqlite3 libltdl noto-fonts-emoji gobject-introspection libcgif libimagequant meson 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.
*** ***

View File

@ -42,7 +42,7 @@ export default async (client, cluster, worker, ipc, member, oldChannel) => {
players.delete(connection.originalChannel.guild.id); players.delete(connection.originalChannel.guild.id);
queues.delete(connection.originalChannel.guild.id); queues.delete(connection.originalChannel.guild.id);
skipVotes.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) { } 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); players.delete(connection.originalChannel.guild.id);
queues.delete(connection.originalChannel.guild.id); queues.delete(connection.originalChannel.guild.id);
skipVotes.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 { } else {
const randomMember = random(members); 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 }); 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); players.delete(connection.originalChannel.guild.id);
queues.delete(connection.originalChannel.guild.id); queues.delete(connection.originalChannel.guild.id);
skipVotes.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.`);
} }
} }
}; };

View File

@ -78,10 +78,7 @@ class Shard extends BaseClusterWorker {
log("log", `Loading event from ${file}...`); log("log", `Loading event from ${file}...`);
const eventArray = file.split("/"); const eventArray = file.split("/");
const eventName = eventArray[eventArray.length - 1].split(".")[0]; const eventName = eventArray[eventArray.length - 1].split(".")[0];
if (eventName === "messageCreate" && !types.classic) { if (eventName === "interactionCreate" && !types.application) {
log("warn", `Skipped loading event from ${file} because classic commands are disabled...`);
continue;
} else if (eventName === "interactionCreate" && !types.application) {
log("warn", `Skipped loading event from ${file} because application commands are disabled`); log("warn", `Skipped loading event from ${file} because application commands are disabled`);
continue; continue;
} }

View File

@ -14,17 +14,19 @@ class AwaitRejoin extends EventEmitter {
this.listener = (member, newChannel) => this.verify(member, newChannel); this.listener = (member, newChannel) => this.verify(member, newChannel);
this.bot.on("voiceChannelJoin", this.listener); this.bot.on("voiceChannelJoin", this.listener);
this.bot.on("voiceChannelSwitch", 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); this.checkInterval = setInterval(() => this.verify({ id: memberID }, channel, true), 1000);
} }
verify(member, channel, checked) { verify(member, channel, checked) {
if (this.channel.id === channel.id) { if (this.channel.id === channel.id) {
if ((this.member === member.id && this.channel.voiceMembers.has(member.id)) || (this.anyone && !checked)) { if ((this.member === member.id && this.channel.voiceMembers.has(member.id)) || (this.anyone && !checked)) {
clearTimeout(this.stopTimeout);
this.rejoined = true; this.rejoined = true;
this.stop(member); this.stop(member);
return true; return true;
} else if (this.anyone && (!checked || this.channel.voiceMembers.size > 1)) { } else if (this.anyone && (!checked || this.channel.voiceMembers.size > 1)) {
clearTimeout(this.stopTimeout);
this.rejoined = true; this.rejoined = true;
this.stop(random(this.channel.voiceMembers.filter((i) => i.id !== this.bot.user.id && !i.bot))); this.stop(random(this.channel.voiceMembers.filter((i) => i.id !== this.bot.user.id && !i.bot)));
return true; return true;

View File

@ -11,7 +11,7 @@ import EventEmitter from "events";
// only requiring this to work around an issue regarding worker threads // only requiring this to work around an issue regarding worker threads
const nodeRequire = createRequire(import.meta.url); 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`); nodeRequire(`../../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`);
} }

View File

@ -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()); const response = await request(`http://${node.url}/version`, { headers: { authorization: node.auth } }).then(res => res.body.text());
if (response) newNodes.push(node); if (response) newNodes.push(node);
} catch { } 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; 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]); const newTrack = await connection.node.rest.decode(newQueue[0]);
nextSong(client, options, connection, newQueue[0], newTrack, music, voiceChannel, host, player.loop, player.shuffle, track); nextSong(client, options, connection, newQueue[0], newTrack, music, voiceChannel, host, player.loop, player.shuffle, track);
try { try {
if (newQueue[0] !== track && playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); if (options.type === "classic") {
if (newQueue[0] !== track && player.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete(); 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 { } catch {
// no-op // no-op
} }
@ -241,19 +243,14 @@ export async function nextSong(client, options, connection, track, info, music,
players.delete(voiceChannel.guild.id); players.delete(voiceChannel.guild.id);
queues.delete(voiceChannel.guild.id); queues.delete(voiceChannel.guild.id);
skipVotes.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") { if (options.type === "classic") {
await client.createMessage(options.channel.id, content); await client.createMessage(options.channel.id, content);
} else { } else {
await options.interaction.createMessage(content); await options.interaction.createMessage(content);
} }
try { }
if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); if (options.type === "classic") {
if (player?.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete();
} catch {
// no-op
}
} else {
try { try {
if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
if (player?.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete(); if (player?.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete();