first commit
This commit is contained in:
106
commands/conf.js
Normal file
106
commands/conf.js
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
FOR GUILD SETTINGS SEE set.js !
|
||||
This command is used to modify the bot's default configuration values, which affects all guilds.
|
||||
If a default setting is not specifically overwritten by a guild, changing a default here will
|
||||
change it for that guild. The `add` action adds a key to the configuration of every guild in
|
||||
your bot. The `del` action removes the key also from every guild, and loses its value forever.
|
||||
*/
|
||||
|
||||
const { codeBlock } = require("@discordjs/builders");
|
||||
const config = require("../config.js");
|
||||
const { awaitReply } = require("../modules/functions.js");
|
||||
const { settings } = require("../modules/settings.js");
|
||||
|
||||
exports.run = async (client, message, [action, key, ...value], level) => { // eslint-disable-line no-unused-vars
|
||||
|
||||
// Retrieve Default Values from the default settings in the bot.
|
||||
const defaults = settings.get("default");
|
||||
const replying = settings.ensure(message.guild.id, config.defaultSettings).commandReply;
|
||||
|
||||
// Adding a new key adds it to every guild (it will be visible to all of them)
|
||||
if (action === "add") {
|
||||
if (!key) return message.reply({ content: "Please specify a key to add", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
if (defaults[key]) return message.reply({ content: "This key already exists in the default settings", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
if (value.length < 1) return message.reply({ content: "Please specify a value", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
|
||||
// `value` being an array, we need to join it first.
|
||||
defaults[key] = value.join(" ");
|
||||
|
||||
// One the settings is modified, we write it back to the collection
|
||||
settings.set("default", defaults);
|
||||
message.reply({ content: `${key} successfully added with the value of ${value.join(" ")}`, allowedMentions: { repliedUser: (replying === "true") }});
|
||||
} else
|
||||
|
||||
// Changing the default value of a key only modified it for guilds that did not change it to another value.
|
||||
if (action === "edit") {
|
||||
if (!key) return message.reply({ content: "Please specify a key to edit", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
if (!defaults[key]) return message.reply({ content: "This key does not exist in the settings", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
if (value.length < 1) return message.reply({ content: "Please specify a new value", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
|
||||
defaults[key] = value.join(" ");
|
||||
|
||||
settings.set("default", defaults);
|
||||
message.reply({ content: `${key} successfully edited to ${value.join(" ")}`, allowedMentions: { repliedUser: (replying === "true") }});
|
||||
} else
|
||||
|
||||
// WARNING: DELETING A KEY FROM THE DEFAULTS ALSO REMOVES IT FROM EVERY GUILD
|
||||
// MAKE SURE THAT KEY IS REALLY NO LONGER NEEDED!
|
||||
if (action === "del") {
|
||||
if (!key) return message.reply({ content: "Please specify a key to delete.", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
if (!defaults[key]) return message.reply({ content: "This key does not exist in the settings", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
|
||||
// Throw the 'are you sure?' text at them.
|
||||
const response = await awaitReply(message, `Are you sure you want to permanently delete ${key} from all guilds? This **CANNOT** be undone.`);
|
||||
|
||||
// If they respond with y or yes, continue.
|
||||
if (["y", "yes"].includes(response)) {
|
||||
|
||||
// We delete the default `key` here.
|
||||
delete defaults[key];
|
||||
settings.set("default", defaults);
|
||||
|
||||
// then we loop on all the guilds and remove this key if it exists.
|
||||
// "if it exists" is done with the filter (if the key is present and it's not the default config!)
|
||||
for (const [guildId, conf] of settings.filter((setting, id) => setting[key] && id !== "default")) {
|
||||
delete conf[key];
|
||||
settings.set(guildId, conf);
|
||||
}
|
||||
|
||||
message.reply({ content: `${key} was successfully deleted.`, allowedMentions: { repliedUser: (replying === "true") }});
|
||||
} else
|
||||
// If they respond with n or no, we inform them that the action has been cancelled.
|
||||
if (["n","no","cancel"].includes(response)) {
|
||||
message.reply({ content: "Action cancelled.", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
}
|
||||
} else
|
||||
|
||||
// Display a key's default value
|
||||
if (action === "get") {
|
||||
if (!key) return message.reply({ content: "Please specify a key to view", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
if (!defaults[key]) return message.reply({ content: "This key does not exist in the settings", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
message.reply({ content: `The value of ${key} is currently ${defaults[key]}`, allowedMentions: { repliedUser: (replying === "true") }});
|
||||
|
||||
// Display all default settings.
|
||||
} else {
|
||||
const array = [];
|
||||
Object.entries(settings.get("default")).forEach(([key, value]) => {
|
||||
array.push(`${key}${" ".repeat(20 - key.length)}:: ${value}`);
|
||||
});
|
||||
await message.channel.send(codeBlock("asciidoc", `= Bot Default Settings =
|
||||
${array.join("\n")}`));
|
||||
}
|
||||
};
|
||||
|
||||
exports.conf = {
|
||||
enabled: true,
|
||||
guildOnly: true,
|
||||
aliases: ["defaults"],
|
||||
permLevel: "Bot Admin"
|
||||
};
|
||||
|
||||
exports.help = {
|
||||
name: "conf",
|
||||
category: "System",
|
||||
description: "Modify the default configuration for all guilds.",
|
||||
usage: "conf <view/get/edit> <key> <value>"
|
||||
};
|
33
commands/deploy.js
Normal file
33
commands/deploy.js
Normal file
@ -0,0 +1,33 @@
|
||||
exports.run = async (client, message, args, level) => { // eslint-disable-line no-unused-vars
|
||||
|
||||
// We'll partition the slash commands based on the guildOnly boolean.
|
||||
// Separating them into the correct objects defined in the array below.
|
||||
const [globalCmds, guildCmds] = client.container.slashcmds.partition(c => !c.conf.guildOnly);
|
||||
|
||||
// Give the user a notification the commands are deploying.
|
||||
await message.channel.send("Deploying commands!");
|
||||
|
||||
// We'll use set but please keep in mind that `set` is overkill for a singular command.
|
||||
// Set the guild commands like
|
||||
await client.guilds.cache.get(message.guild.id)?.commands.set(guildCmds.map(c => c.commandData));
|
||||
|
||||
// Then set the global commands like
|
||||
await client.application?.commands.set(globalCmds.map(c => c.commandData)).catch(e => console.log(e));
|
||||
|
||||
// Reply to the user that the commands have been deployed.
|
||||
await message.channel.send("All commands deployed!");
|
||||
};
|
||||
|
||||
exports.conf = {
|
||||
enabled: true,
|
||||
guildOnly: true,
|
||||
aliases: [],
|
||||
permLevel: "Bot Owner"
|
||||
};
|
||||
|
||||
exports.help = {
|
||||
name: "deploy",
|
||||
category: "System",
|
||||
description: "This will deploy all slash commands.",
|
||||
usage: "deploy"
|
||||
};
|
52
commands/eval.js
Normal file
52
commands/eval.js
Normal file
@ -0,0 +1,52 @@
|
||||
// The EVAL command will execute **ANY** arbitrary javascript code given to it.
|
||||
// THIS IS PERMISSION LEVEL 10 FOR A REASON! It's perm level 10 because eval
|
||||
// can be used to do **anything** on your machine, from stealing information to
|
||||
// purging the hard drive. DO NOT LET ANYONE ELSE USE THIS
|
||||
|
||||
const { codeBlock } = require("@discordjs/builders");
|
||||
|
||||
/*
|
||||
MESSAGE CLEAN FUNCTION
|
||||
|
||||
"Clean" removes @everyone pings, as well as tokens, and makes code blocks
|
||||
escaped so they're shown more easily. As a bonus it resolves promises
|
||||
and stringifies objects!
|
||||
This is mostly only used by the Eval and Exec commands.
|
||||
*/
|
||||
async function clean(client, text) {
|
||||
if (text && text.constructor.name == "Promise")
|
||||
text = await text;
|
||||
if (typeof text !== "string")
|
||||
text = require("util").inspect(text, {depth: 1});
|
||||
|
||||
text = text
|
||||
.replace(/`/g, "`" + String.fromCharCode(8203))
|
||||
.replace(/@/g, "@" + String.fromCharCode(8203));
|
||||
|
||||
text = text.replaceAll(client.token, "[REDACTED]");
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
// However it's, like, super ultra useful for troubleshooting and doing stuff
|
||||
// you don't want to put in a command.
|
||||
exports.run = async (client, message, args, level) => { // eslint-disable-line no-unused-vars
|
||||
const code = args.join(" ");
|
||||
const evaled = eval(code);
|
||||
const cleaned = await clean(client, evaled);
|
||||
message.channel.send(codeBlock("js", cleaned));
|
||||
};
|
||||
|
||||
exports.conf = {
|
||||
enabled: true,
|
||||
guildOnly: false,
|
||||
aliases: [],
|
||||
permLevel: "Bot Owner"
|
||||
};
|
||||
|
||||
exports.help = {
|
||||
name: "eval",
|
||||
category: "System",
|
||||
description: "Evaluates arbitrary javascript.",
|
||||
usage: "eval [...code]"
|
||||
};
|
69
commands/help.js
Normal file
69
commands/help.js
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
The HELP command is used to display every command's name and description
|
||||
to the user, so that he may see what commands are available. The help
|
||||
command is also filtered by level, so if a user does not have access to
|
||||
a command, it is not shown to them. If a command name is given with the
|
||||
help command, its extended help is shown.
|
||||
*/
|
||||
const { codeBlock } = require("@discordjs/builders");
|
||||
const { toProperCase } = require("../modules/functions.js");
|
||||
|
||||
exports.run = (client, message, args, level) => {
|
||||
// Grab the container from the client to reduce line length.
|
||||
const { container } = client;
|
||||
// If no specific command is called, show all filtered commands.
|
||||
if (!args[0]) {
|
||||
// Load guild settings (for prefixes and eventually per-guild tweaks)
|
||||
const settings = message.settings;
|
||||
|
||||
// Filter all commands by which are available for the user's level, using the <Collection>.filter() method.
|
||||
const myCommands = message.guild ? container.commands.filter(cmd => container.levelCache[cmd.conf.permLevel] <= level) :
|
||||
container.commands.filter(cmd => container.levelCache[cmd.conf.permLevel] <= level && cmd.conf.guildOnly !== true);
|
||||
|
||||
// Then we will filter the myCommands collection again to get the enabled commands.
|
||||
const enabledCommands = myCommands.filter(cmd => cmd.conf.enabled);
|
||||
|
||||
// Here we have to get the command names only, and we use that array to get the longest name.
|
||||
const commandNames = [...enabledCommands.keys()];
|
||||
|
||||
// This make the help commands "aligned" in the output.
|
||||
const longest = commandNames.reduce((long, str) => Math.max(long, str.length), 0);
|
||||
|
||||
let currentCategory = "";
|
||||
let output = `= Command List =\n[Use ${settings.prefix}help <commandname> for details]\n`;
|
||||
const sorted = enabledCommands.sort((p, c) => p.help.category > c.help.category ? 1 :
|
||||
p.help.name > c.help.name && p.help.category === c.help.category ? 1 : -1 );
|
||||
|
||||
sorted.forEach( c => {
|
||||
const cat = toProperCase(c.help.category);
|
||||
if (currentCategory !== cat) {
|
||||
output += `\u200b\n== ${cat} ==\n`;
|
||||
currentCategory = cat;
|
||||
}
|
||||
output += `${settings.prefix}${c.help.name}${" ".repeat(longest - c.help.name.length)} :: ${c.help.description}\n`;
|
||||
});
|
||||
message.channel.send(codeBlock("asciidoc", output));
|
||||
|
||||
} else {
|
||||
// Show individual command's help.
|
||||
let command = args[0];
|
||||
if (container.commands.has(command) || container.commands.has(container.aliases.get(command))) {
|
||||
command = container.commands.get(command) ?? container.commands.get(container.aliases.get(command));
|
||||
if (level < container.levelCache[command.conf.permLevel]) return;
|
||||
message.channel.send(codeBlock("asciidoc", `= ${command.help.name} = \n${command.help.description}\nusage:: ${command.help.usage}\naliases:: ${command.conf.aliases.join(", ")}`));
|
||||
} else return message.channel.send("No command with that name, or alias exists.");
|
||||
}};
|
||||
|
||||
exports.conf = {
|
||||
enabled: true,
|
||||
guildOnly: false,
|
||||
aliases: ["h", "halp"],
|
||||
permLevel: "User"
|
||||
};
|
||||
|
||||
exports.help = {
|
||||
name: "help",
|
||||
category: "System",
|
||||
description: "Displays all the available commands for your permission level.",
|
||||
usage: "help [command]"
|
||||
};
|
21
commands/mylevel.js
Normal file
21
commands/mylevel.js
Normal file
@ -0,0 +1,21 @@
|
||||
const config = require("../config.js");
|
||||
const { settings } = require("../modules/settings.js");
|
||||
exports.run = async (client, message, args, level) => {
|
||||
const friendly = config.permLevels.find(l => l.level === level).name;
|
||||
const replying = settings.ensure(message.guild.id, config.defaultSettings).commandReply;
|
||||
message.reply({ content: `Your permission level is: ${level} - ${friendly}`, allowedMentions: { repliedUser: (replying === "true") }});
|
||||
};
|
||||
|
||||
exports.conf = {
|
||||
enabled: true,
|
||||
guildOnly: true,
|
||||
aliases: [],
|
||||
permLevel: "User"
|
||||
};
|
||||
|
||||
exports.help = {
|
||||
name: "mylevel",
|
||||
category: "Miscellaneous",
|
||||
description: "Tells you your permission level for the current message location.",
|
||||
usage: "mylevel"
|
||||
};
|
27
commands/reboot.js
Normal file
27
commands/reboot.js
Normal file
@ -0,0 +1,27 @@
|
||||
const config = require("../config.js");
|
||||
const { settings } = require("../modules/settings.js");
|
||||
exports.run = async (client, message, args, level) => { // eslint-disable-line no-unused-vars
|
||||
const replying = settings.ensure(message.guild.id, config.defaultSettings).commandReply;
|
||||
await message.reply({ content: "Bot is shutting down.", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
await Promise.all(client.container.commands.map(cmd => {
|
||||
// the path is relative to the *current folder*, so just ./filename.js
|
||||
delete require.cache[require.resolve(`./${cmd.help.name}.js`)];
|
||||
// We also need to delete and reload the command from the container.commands Enmap
|
||||
client.container.commands.delete(cmd.help.name);
|
||||
}));
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
exports.conf = {
|
||||
enabled: true,
|
||||
guildOnly: false,
|
||||
aliases: ["restart"],
|
||||
permLevel: "Bot Admin"
|
||||
};
|
||||
|
||||
exports.help = {
|
||||
name: "reboot",
|
||||
category: "System",
|
||||
description: "Shuts down the bot. If running under PM2, bot will restart automatically.",
|
||||
usage: "reboot"
|
||||
};
|
35
commands/reload.js
Normal file
35
commands/reload.js
Normal file
@ -0,0 +1,35 @@
|
||||
const config = require("../config.js");
|
||||
const { settings } = require("../modules/settings.js");
|
||||
exports.run = async (client, message, args, level) => { // eslint-disable-line no-unused-vars
|
||||
// Grab the container from the client to reduce line length.
|
||||
const { container } = client;
|
||||
const replying = settings.ensure(message.guild.id, config.defaultSettings).commandReply;
|
||||
if (!args || args.length < 1) return message.reply("Must provide a command name to reload.");
|
||||
const command = container.commands.get(args[0]) || container.commands.get(container.aliases.get(args[0]));
|
||||
// Check if the command exists and is valid
|
||||
if (!command) {
|
||||
return message.reply("That command does not exist");
|
||||
}
|
||||
// the path is relative to the *current folder*, so just ./filename.js
|
||||
delete require.cache[require.resolve(`./${command.help.name}.js`)];
|
||||
// We also need to delete and reload the command from the container.commands Enmap
|
||||
container.commands.delete(command.help.name);
|
||||
const props = require(`./${command.help.name}.js`);
|
||||
container.commands.set(command.help.name, props);
|
||||
|
||||
message.reply({ content: `The command \`${command.help.name}\` has been reloaded`, allowedMentions: { repliedUser: (replying === "true") }});
|
||||
};
|
||||
|
||||
exports.conf = {
|
||||
enabled: true,
|
||||
guildOnly: false,
|
||||
aliases: [],
|
||||
permLevel: "Bot Admin"
|
||||
};
|
||||
|
||||
exports.help = {
|
||||
name: "reload",
|
||||
category: "System",
|
||||
description: "Reloads a command that\"s been modified.",
|
||||
usage: "reload [command]"
|
||||
};
|
97
commands/set.js
Normal file
97
commands/set.js
Normal file
@ -0,0 +1,97 @@
|
||||
// This command is to modify/edit guild configuration. Perm Level 3 for admins
|
||||
// and owners only. Used for changing prefixes and role names and such.
|
||||
|
||||
// Note that there's no "checks" in this basic version - no config "types" like
|
||||
// Role, String, Int, etc... It's basic, to be extended with your deft hands!
|
||||
|
||||
// Note the **destructuring** here. instead of `args` we have :
|
||||
// [action, key, ...value]
|
||||
// This gives us the equivalent of either:
|
||||
// const action = args[0]; const key = args[1]; const value = args.slice(2);
|
||||
// OR the same as:
|
||||
// const [action, key, ...value] = args;
|
||||
const { codeBlock } = require("@discordjs/builders");
|
||||
const { settings } = require("../modules/settings.js");
|
||||
const { awaitReply } = require("../modules/functions.js");
|
||||
|
||||
exports.run = async (client, message, [action, key, ...value], level) => { // eslint-disable-line no-unused-vars
|
||||
|
||||
// Retrieve current guild settings (merged) and overrides only.
|
||||
const serverSettings = message.settings;
|
||||
const defaults = settings.get("default");
|
||||
const overrides = settings.get(message.guild.id);
|
||||
const replying = serverSettings.commandReply;
|
||||
if (!settings.has(message.guild.id)) settings.set(message.guild.id, {});
|
||||
|
||||
// Edit an existing key value
|
||||
if (action === "edit") {
|
||||
// User must specify a key.
|
||||
if (!key) return message.reply({ content: "Please specify a key to edit", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
// User must specify a key that actually exists!
|
||||
if (!defaults[key]) return message.reply({ content: "This key does not exist in the settings", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
const joinedValue = value.join(" ");
|
||||
// User must specify a value to change.
|
||||
if (joinedValue.length < 1) return message.reply({ content: "Please specify a new value", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
// User must specify a different value than the current one.
|
||||
if (joinedValue === serverSettings[key]) return message.reply({ content: "This setting already has that value!", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
|
||||
// If the guild does not have any overrides, initialize it.
|
||||
if (!settings.has(message.guild.id)) settings.set(message.guild.id, {});
|
||||
|
||||
// Modify the guild overrides directly.
|
||||
settings.set(message.guild.id, joinedValue, key);
|
||||
|
||||
// Confirm everything is fine!
|
||||
message.reply({ content: `${key} successfully edited to ${joinedValue}`, allowedMentions: { repliedUser: (replying === "true") }});
|
||||
} else
|
||||
|
||||
// Resets a key to the default value
|
||||
if (action === "del" || action === "reset") {
|
||||
if (!key) return message.reply({ content: "Please specify a key to reset.", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
if (!defaults[key]) return message.reply({ content: "This key does not exist in the settings", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
if (!overrides[key]) return message.reply({ content: "This key does not have an override and is already using defaults.", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
|
||||
// Good demonstration of the custom awaitReply method in `./modules/functions.js` !
|
||||
const response = await awaitReply(message, `Are you sure you want to reset ${key} to the default value?`);
|
||||
|
||||
// If they respond with y or yes, continue.
|
||||
if (["y", "yes"].includes(response.toLowerCase())) {
|
||||
// We delete the `key` here.
|
||||
settings.delete(message.guild.id, key);
|
||||
message.reply({ content: `${key} was successfully reset to default.`, allowedMentions: { repliedUser: (replying === "true") }});
|
||||
} else
|
||||
// If they respond with n or no, we inform them that the action has been cancelled.
|
||||
if (["n","no","cancel"].includes(response)) {
|
||||
message.reply({ content: `Your setting for \`${key}\` remains at \`${serverSettings[key]}\``, allowedMentions: { repliedUser: (replying === "true") }});
|
||||
}
|
||||
} else
|
||||
|
||||
if (action === "get") {
|
||||
if (!key) return message.reply({ content: "Please specify a key to view", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
if (!defaults[key]) return message.reply({ content: "This key does not exist in the settings", allowedMentions: { repliedUser: (replying === "true") }});
|
||||
const isDefault = !overrides[key] ? "\nThis is the default global default value." : "";
|
||||
message.reply({ content: `The value of ${key} is currently ${serverSettings[key]}${isDefault}`, allowedMentions: { repliedUser: (replying === "true") }});
|
||||
} else {
|
||||
// Otherwise, the default action is to return the whole configuration;
|
||||
const array = [];
|
||||
Object.entries(serverSettings).forEach(([key, value]) => {
|
||||
array.push(`${key}${" ".repeat(20 - key.length)}:: ${value}`);
|
||||
});
|
||||
await message.channel.send(codeBlock("asciidoc", `= Current Guild Settings =
|
||||
${array.join("\n")}`));
|
||||
}
|
||||
};
|
||||
|
||||
exports.conf = {
|
||||
enabled: true,
|
||||
guildOnly: true,
|
||||
aliases: ["setting", "settings", "conf"],
|
||||
permLevel: "Administrator"
|
||||
};
|
||||
|
||||
exports.help = {
|
||||
name: "set",
|
||||
category: "System",
|
||||
description: "View or change settings for your server.",
|
||||
usage: "set <view/get/edit> <key> <value>"
|
||||
};
|
31
commands/stats.js
Normal file
31
commands/stats.js
Normal file
@ -0,0 +1,31 @@
|
||||
const { version } = require("discord.js");
|
||||
const { codeBlock } = require("@discordjs/builders");
|
||||
const { DurationFormatter } = require("@sapphire/time-utilities");
|
||||
const durationFormatter = new DurationFormatter();
|
||||
|
||||
exports.run = (client, message, args, level) => { // eslint-disable-line no-unused-vars
|
||||
const duration = durationFormatter.format(client.uptime);
|
||||
const stats = codeBlock("asciidoc", `= STATISTICS =
|
||||
• Mem Usage :: ${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB
|
||||
• Uptime :: ${duration}
|
||||
• Users :: ${client.guilds.cache.map(g => g.memberCount).reduce((a, b) => a + b).toLocaleString()}
|
||||
• Servers :: ${client.guilds.cache.size.toLocaleString()}
|
||||
• Channels :: ${client.channels.cache.size.toLocaleString()}
|
||||
• Discord.js :: v${version}
|
||||
• Node :: ${process.version}`);
|
||||
message.channel.send(stats);
|
||||
};
|
||||
|
||||
exports.conf = {
|
||||
enabled: true,
|
||||
guildOnly: false,
|
||||
aliases: [],
|
||||
permLevel: "User"
|
||||
};
|
||||
|
||||
exports.help = {
|
||||
name: "stats",
|
||||
category: "Miscellaneous",
|
||||
description: "Gives some useful bot statistics",
|
||||
usage: "stats"
|
||||
};
|
Reference in New Issue
Block a user