first commit

This commit is contained in:
Raven Scott 2022-01-24 19:05:30 +00:00
commit 390fff9c42
31 changed files with 10171 additions and 0 deletions

58
.eslintrc.json Normal file
View File

@ -0,0 +1,58 @@
{
"parserOptions": {
"ecmaVersion": 2021
},
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"rules": {
"no-console": "off",
"indent": [
"error",
2,
{
"SwitchCase": 1
}
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"warn",
"double"
],
"semi": [
"warn",
"always"
],
"keyword-spacing": [
"error", {
"before": true,
"after": true
}
],
"space-before-blocks": [
"error", {
"functions":"always",
"keywords": "always",
"classes": "always"
}
],
"space-before-function-paren": [
"error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
"prefer-const": [
"error", {
"destructuring": "any",
"ignoreReadBeforeAssign": false
}
]
}
}

64
.gitignore vendored Normal file
View File

@ -0,0 +1,64 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
config.json
config.js
data/
test.js
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
/.vscode/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 SNXRaven
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

106
commands/conf.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"
};

4
events/error.js Normal file
View File

@ -0,0 +1,4 @@
const logger = require("../modules/Logger.js");
module.exports = async (client, error) => {
logger.log(`An error event was sent by Discord.js: \n${JSON.stringify(error)}`, "error");
};

6
events/guildCreate.js Normal file
View File

@ -0,0 +1,6 @@
const logger = require("../modules/Logger.js");
// This event executes when a new guild (server) is joined.
module.exports = (client, guild) => {
logger.log(`[GUILD JOIN] ${guild.id} added the bot. Owner: ${guild.ownerId}`);
};

16
events/guildDelete.js Normal file
View File

@ -0,0 +1,16 @@
const logger = require("../modules/Logger.js");
const { settings } = require("../modules/settings.js");
// This event executes when a new guild (server) is left.
module.exports = (client, guild) => {
if (!guild.available) return; // If there is an outage, return.
logger.log(`[GUILD LEAVE] ${guild.id} removed the bot.`);
// If the settings Enmap contains any guild overrides, remove them.
// No use keeping stale data!
if (settings.has(guild.id)) {
settings.delete(guild.id);
}
};

17
events/guildMemberAdd.js Normal file
View File

@ -0,0 +1,17 @@
const { getSettings } = require("../modules/functions.js");
// This event executes when a new member joins a server. Let's welcome them!
module.exports = (client, member) => {
// Load the guild's settings
const settings = getSettings(member.guild);
// If welcome is off, don't proceed (don't welcome the user)
if (settings.welcomeEnabled !== "true") return;
// Replace the placeholders in the welcome message with actual data
const welcomeMessage = settings.welcomeMessage.replace("{{user}}", member.user.tag);
// Send the welcome message to the welcome channel
// There's a place for more configs here.
member.guild.channels.cache.find(c => c.name === settings.welcomeChannel).send(welcomeMessage).catch(console.error);
};

View File

@ -0,0 +1,54 @@
const logger = require("../modules/Logger.js");
const { getSettings, permlevel } = require("../modules/functions.js");
const config = require("../config.js");
module.exports = async (client, interaction) => {
// If it's not a command, stop.
if (!interaction.isCommand()) return;
// Grab the settings for this server from Enmap.
// If there is no guild, get default conf (DMs)
const settings = interaction.settings = getSettings(interaction.guild);
// Get the user or member's permission level from the elevation
const level = permlevel(interaction);
// Grab the command data from the client.container.slashcmds Collection
const cmd = client.container.slashcmds.get(interaction.commandName);
// If that command doesn't exist, silently exit and do nothing
if (!cmd) return;
// Since the permission system from Discord is rather limited in regarding to
// Slash Commands, we'll just utilise our permission checker.
if (level < client.container.levelCache[cmd.conf.permLevel]) {
// Due to the nature of interactions we **must** respond to them otherwise
// they will error out because we didn't respond to them.
return await interaction.reply({
content: `This command can only be used by ${cmd.conf.permLevel}'s only`,
// This will basically set the ephemeral response to either announce
// to everyone, or just the command executioner. But we **HAVE** to
// respond.
ephemeral: settings.systemNotice !== "true"
});
}
// If everything checks out, run the command
try {
await cmd.run(client, interaction);
logger.log(`${config.permLevels.find(l => l.level === level).name} ${interaction.user.id} ran slash command ${interaction.commandName}`, "cmd");
} catch (e) {
console.error(e);
if (interaction.replied)
interaction.followUp({ content: `There was a problem with your request.\n\`\`\`${e.message}\`\`\``, ephemeral: true })
.catch(e => console.error("An error occurred following up on an error", e));
else
if (interaction.deferred)
interaction.editReply({ content: `There was a problem with your request.\n\`\`\`${e.message}\`\`\``, ephemeral: true })
.catch(e => console.error("An error occurred following up on an error", e));
else
interaction.reply({ content: `There was a problem with your request.\n\`\`\`${e.message}\`\`\``, ephemeral: true })
.catch(e => console.error("An error occurred replying on an error", e));
}
};

90
events/messageCreate.js Normal file
View File

@ -0,0 +1,90 @@
const logger = require("../modules/Logger.js");
const { getSettings, permlevel } = require("../modules/functions.js");
const config = require("../config.js");
// The MESSAGE event runs anytime a message is received
// Note that due to the binding of client to every event, every event
// goes `client, other, args` when this function is run.
module.exports = async (client, message) => {
// Grab the container from the client to reduce line length.
const { container } = client;
// It's good practice to ignore other bots. This also makes your bot ignore itself
// and not get into a spam loop (we call that "botception").
if (message.author.bot) return;
// Grab the settings for this server from Enmap.
// If there is no guild, get default conf (DMs)
const settings = message.settings = getSettings(message.guild);
// Checks if the bot was mentioned via regex, with no message after it,
// returns the prefix. The reason why we used regex here instead of
// message.mentions is because of the mention prefix later on in the
// code, would render it useless.
const prefixMention = new RegExp(`^<@!?${client.user.id}> ?$`);
if (message.content.match(prefixMention)) {
return message.reply(`My prefix on this guild is \`${settings.prefix}\``);
}
// It's also good practice to ignore any and all messages that do not start
// with our prefix, or a bot mention.
const prefix = new RegExp(`^<@!?${client.user.id}> |^\\${settings.prefix}`).exec(message.content);
// This will return and stop the code from continuing if it's missing
// our prefix (be it mention or from the settings).
if (!prefix) return;
// Here we separate our "command" name, and our "arguments" for the command.
// e.g. if we have the message "+say Is this the real life?" , we'll get the following:
// command = say
// args = ["Is", "this", "the", "real", "life?"]
const args = message.content.slice(prefix[0].length).trim().split(/ +/g);
const command = args.shift().toLowerCase();
// If the member on a guild is invisible or not cached, fetch them.
if (message.guild && !message.member) await message.guild.members.fetch(message.author);
// Get the user or member's permission level from the elevation
const level = permlevel(message);
// Check whether the command, or alias, exist in the collections defined
// in app.js.
const cmd = container.commands.get(command) || container.commands.get(container.aliases.get(command));
// using this const varName = thing OR otherThing; is a pretty efficient
// and clean way to grab one of 2 values!
if (!cmd) return;
// Some commands may not be useable in DMs. This check prevents those commands from running
// and return a friendly error message.
if (cmd && !message.guild && cmd.conf.guildOnly)
return message.channel.send("This command is unavailable via private message. Please run this command in a guild.");
if (!cmd.conf.enabled) return;
if (level < container.levelCache[cmd.conf.permLevel]) {
if (settings.systemNotice === "true") {
return message.channel.send(`You do not have permission to use this command.
Your permission level is ${level} (${config.permLevels.find(l => l.level === level).name})
This command requires level ${container.levelCache[cmd.conf.permLevel]} (${cmd.conf.permLevel})`);
} else {
return;
}
}
// To simplify message arguments, the author's level is now put on level (not member so it is supported in DMs)
// The "level" command module argument will be deprecated in the future.
message.author.permLevel = level;
message.flags = [];
while (args[0] && args[0][0] === "-") {
message.flags.push(args.shift().slice(1));
}
// If the command exists, **AND** the user has permission, run it.
try {
await cmd.run(client, message, args, level);
logger.log(`${config.permLevels.find(l => l.level === level).name} ${message.author.id} ran command ${cmd.help.name}`, "cmd");
} catch (e) {
console.error(e);
message.channel.send({ content: `There was a problem with your request.\n\`\`\`${e.message}\`\`\`` })
.catch(e => console.error("An error occurred replying on an error", e));
}
};

9
events/ready.js Normal file
View File

@ -0,0 +1,9 @@
const logger = require("../modules/Logger.js");
const { getSettings } = require("../modules/functions.js");
module.exports = async client => {
// Log that the bot is online.
logger.log(`${client.user.tag}, ready to serve ${client.guilds.cache.map(g => g.memberCount).reduce((a, b) => a + b)} users in ${client.guilds.cache.size} servers.`, "ready");
// Make the bot "play the game" which is the help command with default prefix.
client.user.setActivity(`${getSettings("default").prefix}help`, { type: "PLAYING" });
};

91
index.js Normal file
View File

@ -0,0 +1,91 @@
// This will check if the node version you are running is the required
// Node version, if it isn't it will throw the following error to inform
// you.
if (Number(process.version.slice(1).split(".")[0]) < 16) throw new Error("Node 16.x or higher is required. Update Node on your system.");
require("dotenv").config();
// Load up the discord.js library
const { Client, Collection } = require("discord.js");
// We also load the rest of the things we need in this file:
const { readdirSync } = require("fs");
const { intents, partials, permLevels } = require("./config.js");
const logger = require("./modules/Logger.js");
// This is your client. Some people call it `bot`, some people call it `self`,
// some might call it `cootchie`. Either way, when you see `client.something`,
// or `bot.something`, this is what we're referring to. Your client.
const client = new Client({ intents, partials });
// Aliases, commands and slash commands are put in collections where they can be
// read from, catalogued, listed, etc.
const commands = new Collection();
const aliases = new Collection();
const slashcmds = new Collection();
// Generate a cache of client permissions for pretty perm names in commands.
const levelCache = {};
for (let i = 0; i < permLevels.length; i++) {
const thisLevel = permLevels[i];
levelCache[thisLevel.name] = thisLevel.level;
}
// To reduce client pollution we'll create a single container property
// that we can attach everything we need to.
client.container = {
commands,
aliases,
slashcmds,
levelCache
};
// We're doing real fancy node 8 async/await stuff here, and to do that
// we need to wrap stuff in an anonymous function. It's annoying but it works.
const init = async () => {
// Here we load **commands** into memory, as a collection, so they're accessible
// here and everywhere else.
const commands = readdirSync("./commands/").filter(file => file.endsWith(".js"));
for (const file of commands) {
const props = require(`./commands/${file}`);
logger.log(`Loading Command: ${props.help.name}. 👌`, "log");
client.container.commands.set(props.help.name, props);
props.conf.aliases.forEach(alias => {
client.container.aliases.set(alias, props.help.name);
});
}
// Now we load any **slash** commands you may have in the ./slash directory.
const slashFiles = readdirSync("./slash").filter(file => file.endsWith(".js"));
for (const file of slashFiles) {
const command = require(`./slash/${file}`);
const commandName = file.split(".")[0];
logger.log(`Loading Slash command: ${commandName}. 👌`, "log");
// Now set the name of the command with it's properties.
client.container.slashcmds.set(command.commandData.name, command);
}
// Then we load events, which will include our message and ready event.
const eventFiles = readdirSync("./events/").filter(file => file.endsWith(".js"));
for (const file of eventFiles) {
const eventName = file.split(".")[0];
logger.log(`Loading Event: ${eventName}. 👌`, "log");
const event = require(`./events/${file}`);
// Bind the client to any event, before the existing arguments
// provided by the discord.js event.
// This line is awesome by the way. Just sayin'.
client.on(eventName, event.bind(null, client));
}
// Threads are currently in BETA.
// This event will fire when a thread is created, if you want to expand
// the logic, throw this in it's own event file like the rest.
client.on("threadCreate", (thread) => thread.join());
// Here we login the client.
client.login();
// End top-level async/await function.
};
init();

27
modules/Logger.js Normal file
View File

@ -0,0 +1,27 @@
/*
Logger class for easy and aesthetically pleasing console logging
*/
const { cyan, red, magenta, gray, yellow, white, green } = require("colorette");
const { Timestamp } = require("@sapphire/time-utilities");
exports.log = (content, type = "log") => {
const timestamp = `[${cyan(new Timestamp("YYYY-MM-DD HH:mm:ss"))}]:`;
switch (type) {
case "log": return console.log(`${timestamp} ${gray(type.toUpperCase())} ${content} `);
case "warn": return console.log(`${timestamp} ${yellow(type.toUpperCase())} ${content} `);
case "error": return console.log(`${timestamp} ${red(type.toUpperCase())} ${content} `);
case "debug": return console.log(`${timestamp} ${magenta(type.toUpperCase())} ${content} `);
case "cmd": return console.log(`${timestamp} ${white(type.toUpperCase())} ${content}`);
case "ready": return console.log(`${timestamp} ${green(type.toUpperCase())} ${content}`);
default: throw new TypeError("Logger type must be either warn, debug, log, ready, cmd or error.");
}
};
exports.error = (...args) => this.log(...args, "error");
exports.warn = (...args) => this.log(...args, "warn");
exports.debug = (...args) => this.log(...args, "debug");
exports.cmd = (...args) => this.log(...args, "cmd");

99
modules/functions.js Normal file
View File

@ -0,0 +1,99 @@
const logger = require("./Logger.js");
const config = require("../config.js");
const { settings } = require("./settings.js");
// Let's start by getting some useful functions that we'll use throughout
// the bot, like logs and elevation features.
/*
PERMISSION LEVEL FUNCTION
This is a very basic permission system for commands which uses "levels"
"spaces" are intentionally left black so you can add them if you want.
NEVER GIVE ANYONE BUT OWNER THE LEVEL 10! By default this can run any
command including the VERY DANGEROUS `eval` and `exec` commands!
*/
function permlevel(message) {
let permlvl = 0;
const permOrder = config.permLevels.slice(0).sort((p, c) => p.level < c.level ? 1 : -1);
while (permOrder.length) {
const currentLevel = permOrder.shift();
if (message.guild && currentLevel.guildOnly) continue;
if (currentLevel.check(message)) {
permlvl = currentLevel.level;
break;
}
}
return permlvl;
}
/*
GUILD SETTINGS FUNCTION
This function merges the default settings (from config.defaultSettings) with any
guild override you might have for particular guild. If no overrides are present,
the default settings are used.
*/
// getSettings merges the client defaults with the guild settings. guild settings in
// enmap should only have *unique* overrides that are different from defaults.
function getSettings(guild) {
settings.ensure("default", config.defaultSettings);
if (!guild) return settings.get("default");
const guildConf = settings.get(guild.id) || {};
// This "..." thing is the "Spread Operator". It's awesome!
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
return ({...settings.get("default"), ...guildConf});
}
/*
SINGLE-LINE AWAIT MESSAGE
A simple way to grab a single reply, from the user that initiated
the command. Useful to get "precisions" on certain things...
USAGE
const response = await awaitReply(msg, "Favourite Color?");
msg.reply(`Oh, I really love ${response} too!`);
*/
async function awaitReply(msg, question, limit = 60000) {
const filter = m => m.author.id === msg.author.id;
await msg.channel.send(question);
try {
const collected = await msg.channel.awaitMessages({ filter, max: 1, time: limit, errors: ["time"] });
return collected.first().content;
} catch (e) {
return false;
}
}
/* MISCELLANEOUS NON-CRITICAL FUNCTIONS */
// toProperCase(String) returns a proper-cased string such as:
// toProperCase("Mary had a little lamb") returns "Mary Had A Little Lamb"
function toProperCase(string) {
return string.replace(/([^\W_]+[^\s-]*) */g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
}
// These 2 process methods will catch exceptions and give *more details* about the error and stack trace.
process.on("uncaughtException", (err) => {
const errorMsg = err.stack.replace(new RegExp(`${__dirname}/`, "g"), "./");
logger.error(`Uncaught Exception: ${errorMsg}`);
console.error(err);
// Always best practice to let the code crash on uncaught exceptions.
// Because you should be catching them anyway.
process.exit(1);
});
process.on("unhandledRejection", err => {
logger.error(`Unhandled rejection: ${err}`);
console.error(err);
});
module.exports = { getSettings, permlevel, awaitReply, toProperCase };

9
modules/settings.js Normal file
View File

@ -0,0 +1,9 @@
const Enmap = require("enmap");
// Now we integrate the use of Evie's awesome Enmap module, which
// essentially saves a collection to disk. This is great for per-server configs,
// and makes things extremely easy for this purpose.
module.exports = {
settings: new Enmap({
name: "settings",
}),
};

6528
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "guidebot",
"version": "2.1.0",
"description": "A boilerplate example bot with command handler and reloadable commands. Updated and Maintained by the Idiot's Guide Community",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"engines": {
"node": ">=16"
},
"repository": {
"type": "git",
"url": "git+https://github.com/AnIdiotsGuide/guidebot.git"
},
"author": "Evelyne Lachance <eslachance@gmail.com> (http://evie.codes)",
"contributors": [
"York (http://anidiots.guide)"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/AnIdiotsGuide/guidebot/issues"
},
"homepage": "https://github.com/AnIdiotsGuide/guidebot#readme",
"dependencies": {
"@aahlw/peji": "^1.0.0",
"@discordjs/builders": "^0.2.0",
"@sapphire/time-utilities": "^1.3.8",
"axios": "^0.25.0",
"colorette": "^1.3.0",
"discord.js": "^13.0.1",
"dotenv": "^10.0.0",
"enmap": "^5.8.5",
"pagination.djs": "^3.1.1",
"unirest": "^0.6.0"
},
"devDependencies": {
"eslint": "^7.30.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0"
}
}

41
slash/create_task.js Normal file
View File

@ -0,0 +1,41 @@
// For HTTP Requests
var http = require('unirest');
let boardList=[]
let embed = []
let pages =[]
require("dotenv").config();
exports.run = async (client, interaction) => {
// eslint-disable-line no-unused-vars
await interaction.deferReply();
// const reply = await interaction.editReply("Ping?");
// await interaction.editReply(`Pong! Latency is ${reply.createdTimestamp - interaction.createdTimestamp}ms. API Latency is ${Math.round(client.ws.ping)}ms.`);
var Request = http.post('https://board.grwh.work/jsonrpc.php').headers({ Accept: 'application/json', 'Content-Type': 'application/json' }).send({ "jsonrpc": "2.0", "method": "getAllProjects", "id": 0 });
Request.auth({
user: 'jsonrpc',
pass: process.env.KANBOARD_API_KEY,
sendImmediately: false
}).then(function (response) {
let data = response.body.result
console.log(data)
})
};
exports.commandData = {
name: "test",
description: "create a task",
options: [],
defaultPermission: true,
};
// Set guildOnly to true if you want it to be available on guilds only.
// Otherwise false is global.
exports.conf = {
permLevel: "User",
guildOnly: false
};

24
slash/leave.js Normal file
View File

@ -0,0 +1,24 @@
const { Permissions } = require("discord.js");
exports.run = async (client, interaction) => { // eslint-disable-line no-unused-vars
await interaction.deferReply();
if (!interaction.guild.me.permissions.has(Permissions.FLAGS.KICK_MEMBERS))
return await interaction.editReply("I do not have permission to kick members in this server.");
await interaction.member.send("You requested to leave the server, if you change your mind you can rejoin at a later date.");
await interaction.member.kick(`${interaction.member.displayName} wanted to leave.`);
await interaction.editReply(`${interaction.member.displayName} left in a hurry!`);
};
exports.commandData = {
name: "leave",
description: "Make's the user leave the guild.",
options: [],
defaultPermission: true,
};
// Set guildOnly to true if you want it to be available on guilds only.
// Otherwise false is global.
exports.conf = {
permLevel: "User",
guildOnly: true
};

70
slash/list_projects.js Normal file
View File

@ -0,0 +1,70 @@
// For HTTP Requests
var http = require('unirest');
let boardList=[]
let embed = []
let pages =[]
const { MessageEmbed, MessageButton } = require("discord.js");
const { Pagination } = require("pagination.djs");
require("dotenv").config();
exports.run = async (client, interaction) => {
const pagination = new Pagination(interaction, {
firstEmoji: "⏮", // First button emoji
prevEmoji: "◀️", // Previous button emoji
nextEmoji: "▶️", // Next button emoji
lastEmoji: "⏭", // Last button emoji
limit: 2, // number of entries per page
idle: 30000, // idle time in ms before the pagination closes
ephemeral: false, // ephemeral reply
prevDescription: "",
postDescription: "",
buttonStyle: "SECONDARY",
loop: false, // loop through the pages
});
// eslint-disable-line no-unused-vars
await interaction.deferReply();
// const reply = await interaction.editReply("Ping?");
// await interaction.editReply(`Pong! Latency is ${reply.createdTimestamp - interaction.createdTimestamp}ms. API Latency is ${Math.round(client.ws.ping)}ms.`);
var Request = http.get('https://board.grwh.work/jsonrpc.php').headers({ Accept: 'application/json', 'Content-Type': 'application/json' }).send({ "jsonrpc": "2.0", "method": "getAllProjects", "id": 0 });
Request.auth({
user: 'jsonrpc',
pass: process.env.KANBOARD_API_KEY,
sendImmediately: false
}).then(function (response) {
let data = response.body.result
const pusherFunc = board=>boardList.push({name:"Project Name", value:board.name + "\nID: " + board.id + "\n" + "https://board.grwh.work/board/" + board.id});
data.forEach(pusherFunc);
console.log(boardList)
pagination.setTitle("Project List");
pagination.setDescription("This is a list of all currently active projects.");
pagination.setColor("#00ff00");
pagination.setFooter("Boards:");
pagination.setTimestamp();
pagination.addFields(boardList);
pagination.paginateFields(true);
pagination.render();
})
};
exports.commandData = {
name: "list",
description: "Lists all of the current projects",
options: [],
defaultPermission: true,
};
// Set guildOnly to true if you want it to be available on guilds only.
// Otherwise false is global.
exports.conf = {
permLevel: "User",
guildOnly: false
};

19
slash/ping.js Normal file
View File

@ -0,0 +1,19 @@
exports.run = async (client, interaction) => { // eslint-disable-line no-unused-vars
await interaction.deferReply();
const reply = await interaction.editReply("Ping?");
await interaction.editReply(`Pong! Latency is ${reply.createdTimestamp - interaction.createdTimestamp}ms. API Latency is ${Math.round(client.ws.ping)}ms.`);
};
exports.commandData = {
name: "ping",
description: "Pongs when pinged.",
options: [],
defaultPermission: true,
};
// Set guildOnly to true if you want it to be available on guilds only.
// Otherwise false is global.
exports.conf = {
permLevel: "User",
guildOnly: false
};

31
slash/stats.js Normal file
View 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 = async (client, interaction) => { // 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}`);
await interaction.reply(stats);
};
exports.commandData = {
name: "stats",
description: "Show's the bots stats.",
options: [],
defaultPermission: true,
};
// Set guildOnly to true if you want it to be available on guilds only.
// Otherwise false is global.
exports.conf = {
permLevel: "User",
guildOnly: false
};

2377
yarn.lock Normal file

File diff suppressed because it is too large Load Diff