aboutsummaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorMinteck <contact@minteck.org>2022-07-13 23:34:21 +0200
committerMinteck <contact@minteck.org>2022-07-13 23:34:21 +0200
commit7b5df4ca0a5bd6fcf033ef40563599593b156910 (patch)
tree56cb89f3900adde9da67e7558793a20fb40d7197 /core
downloadcooler-pony-7b5df4ca0a5bd6fcf033ef40563599593b156910.tar.gz
cooler-pony-7b5df4ca0a5bd6fcf033ef40563599593b156910.tar.bz2
cooler-pony-7b5df4ca0a5bd6fcf033ef40563599593b156910.zip
Initial commit
Diffstat (limited to 'core')
-rw-r--r--core/CommandAction.ts46
-rw-r--r--core/CommandBase.ts8
-rw-r--r--core/CommandInteractionManager.ts21
-rw-r--r--core/CommandsLoader.ts37
-rw-r--r--core/CoolerPony.ts80
-rw-r--r--core/ErrorParser.ts67
-rw-r--r--core/InteractionManager.ts3
-rw-r--r--core/LogManager.ts84
-rw-r--r--core/ManagedChannelManager.ts41
-rw-r--r--core/PresenceManager.ts7
-rw-r--r--core/SlashCommandsRefresher.ts35
-rw-r--r--core/Welcome.ts50
12 files changed, 479 insertions, 0 deletions
diff --git a/core/CommandAction.ts b/core/CommandAction.ts
new file mode 100644
index 0000000..ccc997b
--- /dev/null
+++ b/core/CommandAction.ts
@@ -0,0 +1,46 @@
+import {
+ CommandInteraction,
+ CommandInteractionOptionResolver,
+ MessageComponentInteraction,
+ ModalSubmitInteraction
+} from "discord.js";
+
+class CommandName extends String {
+ constructor(...args) {
+ super(...args);
+ }
+}
+
+export class CommandAction {
+ private readonly command: CommandName;
+ private readonly args: object | CommandInteractionOptionResolver;
+ private readonly interaction: CommandInteraction | ModalSubmitInteraction | MessageComponentInteraction;
+
+ constructor(command: string, interaction: CommandInteraction | ModalSubmitInteraction | MessageComponentInteraction, args?: object) {
+ this.command = new CommandName(command);
+ this.interaction = interaction;
+ if (args) {
+ this.args = args;
+ } else if (interaction instanceof CommandInteraction) {
+ this.args = interaction.options;
+ } else {
+ this.args = {};
+ }
+ }
+
+ public getCommand(): CommandName {
+ return this.command;
+ }
+
+ public getInteraction(): CommandInteraction | ModalSubmitInteraction | MessageComponentInteraction {
+ return this.interaction;
+ }
+
+ public getArgument(argument: string): any {
+ if (this.args instanceof CommandInteractionOptionResolver) {
+ return this.args.get(argument, true).value;
+ } else {
+ return this.args[argument];
+ }
+ }
+} \ No newline at end of file
diff --git a/core/CommandBase.ts b/core/CommandBase.ts
new file mode 100644
index 0000000..8be766b
--- /dev/null
+++ b/core/CommandBase.ts
@@ -0,0 +1,8 @@
+import {SlashCommandBuilder} from "@discordjs/builders";
+
+export class CommandBase {
+ public slashCommandData: Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">;
+
+ constructor() {
+ }
+} \ No newline at end of file
diff --git a/core/CommandInteractionManager.ts b/core/CommandInteractionManager.ts
new file mode 100644
index 0000000..0f7cb49
--- /dev/null
+++ b/core/CommandInteractionManager.ts
@@ -0,0 +1,21 @@
+import {InteractionManager} from "./InteractionManager";
+import {CommandInteraction} from "discord.js";
+import {LogManager} from "./LogManager";
+import {CommandsLoader} from "./CommandsLoader";
+import {CommandAction} from "./CommandAction";
+
+export class CommandInteractionManager extends InteractionManager {
+ public static commands = new CommandsLoader().getCommands();
+
+ constructor(interaction: CommandInteraction) {
+ super();
+ LogManager.verbose("CommandInteractionManager: " + interaction.commandName);
+
+ if (Object.keys(CommandInteractionManager.commands).includes(interaction.commandName)) {
+ CommandInteractionManager.commands[interaction.commandName].handle(new CommandAction(interaction.commandName, interaction));
+ } else {
+ LogManager.error("Command not found: " + interaction.commandName);
+ interaction.reply(":x: Command not found: `" + interaction.commandName + "`, this is most likely a bug.");
+ }
+ }
+} \ No newline at end of file
diff --git a/core/CommandsLoader.ts b/core/CommandsLoader.ts
new file mode 100644
index 0000000..4d435ae
--- /dev/null
+++ b/core/CommandsLoader.ts
@@ -0,0 +1,37 @@
+import {LogManager} from "./LogManager";
+import * as fs from 'fs';
+
+import {SlashCommandBuilder} from "@discordjs/builders";
+
+export class CommandsLoader {
+ private commands: object = {};
+
+ constructor() {
+ LogManager.verbose("Generate CommandsLoader list");
+ let list = fs.readdirSync("./commands").filter((i: string) => i.endsWith(".js"));
+
+ for (let item of list) {
+ LogManager.verbose(" load: " + item);
+ let imported = require('../commands/' + item);
+ let cmd = imported[Object.keys(imported)[0]];
+ this.commands[item.substring(0, item.length - 3)] = new cmd();
+ }
+ }
+
+ public slashCommands(): SlashCommandBuilder[] {
+ let slashCommands = [];
+
+ for (let name of Object.keys(this.commands)) {
+ let command = this.commands[name];
+ LogManager.verbose("CommandsLoader: " + name);
+
+ slashCommands.push(command.slashCommandData);
+ }
+
+ return slashCommands;
+ }
+
+ public getCommands(): object {
+ return this.commands;
+ }
+} \ No newline at end of file
diff --git a/core/CoolerPony.ts b/core/CoolerPony.ts
new file mode 100644
index 0000000..8a02f48
--- /dev/null
+++ b/core/CoolerPony.ts
@@ -0,0 +1,80 @@
+import {Client, GuildMember, Message, MessageEmbed} from 'discord.js';
+import * as fs from 'fs';
+import * as crypto from 'crypto';
+import {LogManager} from "./LogManager";
+import {CommandInteractionManager} from "./CommandInteractionManager";
+import {PresenceManager} from "./PresenceManager";
+import {SlashCommandsRefresher} from "./SlashCommandsRefresher";
+import {ErrorParser} from "./ErrorParser";
+import {Welcome} from "./Welcome";
+import {ManagedChannelManager} from "./ManagedChannelManager";
+
+process.on('uncaughtException', (error) => {
+ if (error.message === "WebSocket was closed before the connection was established") {
+ LogManager.error("Bot has encountered a connection error, restarting.");
+ fs.writeFileSync("./RESTART-FORCE", "");
+ return;
+ }
+ LogManager.error(error.stack);
+
+ let message = {
+ embeds: [
+ new MessageEmbed()
+ .setTitle(":no_entry: An internal error occurred with the bot")
+ .setDescription("We are sorry, but an error has occurred and the bot cannot process your request for now. This is most likely a problem on our side, please try again later.\n\n> " + ErrorParser.parse(error.name, error.message) + "\n\n\\*\\*\\*\\* BugCheck 0x" + crypto.createHash('md5').update(error.name + error.message).digest('hex').substring(0, 8).toUpperCase())
+ .addField("Are you a developer?", "Check the bot logs (e.g. with `/logs`) for additional details and a stack trace.")
+ .addField("Version information", "version " + (fs.existsSync("./.git/refs/heads/mane") ? fs.readFileSync("./.git/refs/heads/mane").toString().trim().substring(0, 8) : (fs.existsSync("../.git/refs/heads/mane") ? fs.readFileSync("../.git/refs/heads/mane").toString().trim().substring(0, 8) : (fs.existsSync("./version.txt") ? fs.readFileSync("./version.txt").toString().trim() : (fs.existsSync("../version.txt") ? fs.readFileSync("../version.txt").toString().trim() : "live")))) + ", build " + (fs.existsSync("./build.txt") ? fs.readFileSync("./build.txt").toString().trim() : (fs.existsSync("../build.txt") ? fs.readFileSync("../build.txt").toString().trim() : "dev")) + " (use `/about` for more details)")
+ ]
+ }
+
+ if (global.lastKnownInteraction) {
+ global.lastKnownInteraction.channel.send(message);
+ }
+})
+
+export class CoolerPony {
+ constructor(token: string) {
+ const client = global.client = new Client({intents: ['GUILD_VOICE_STATES', 'GUILD_MESSAGES', 'GUILDS', 'GUILD_MEMBERS']});
+
+ client.on('ready', () => {
+ LogManager.info(`Logged in as ${client.user.tag}!`);
+ PresenceManager.start(client);
+ SlashCommandsRefresher.refresh(client.user.id, token);
+ });
+
+ client.on('guildMemberAdd', (member: GuildMember) => {
+ Welcome.welcome(member);
+ })
+
+ client.on('guildMemberRemove', (member: GuildMember) => {
+ Welcome.unwelcome(member);
+ })
+
+ client.on('messageCreate', (message: Message) => {
+ ManagedChannelManager.handleMessage(message);
+ })
+
+ client.on('interactionCreate', async interaction => {
+ global.lastKnownInteraction = interaction;
+ if (!interaction.guild) {
+ // @ts-ignore
+ if (interaction.channel) { // @ts-ignore
+ interaction.channel.send({
+ embeds: [
+ new MessageEmbed()
+ .setDescription(":x: The bot can not be used in direct messages.")
+ ]
+ });
+ }
+ return;
+ }
+
+ global.processingStart = new Date();
+ if (interaction.isCommand()) {
+ new CommandInteractionManager(interaction);
+ }
+ });
+
+ client.login(token);
+ }
+} \ No newline at end of file
diff --git a/core/ErrorParser.ts b/core/ErrorParser.ts
new file mode 100644
index 0000000..954fcc5
--- /dev/null
+++ b/core/ErrorParser.ts
@@ -0,0 +1,67 @@
+export class ErrorParser {
+ public static parse(type: string, message: string): string {
+ let p1 = "Internal system failure.";
+
+ switch (type) {
+ case "Error":
+ p1 = "General failure.";
+ break;
+
+ case "InternalError":
+ p1 = "Internal engine failure.";
+ break;
+
+ case "RangeError":
+ p1 = "Invalid item range.";
+ break;
+
+ case "ReferenceError":
+ p1 = "Unable to reference element.";
+ break;
+
+ case "SyntaxError":
+ p1 = "Syntax failure.";
+ break;
+
+ case "TypeError":
+ p1 = "Inconsistent element type.";
+ break;
+
+ case "SongTooLongError":
+ p1 = "Song is too long.";
+ break;
+
+ case "URIError":
+ p1 = "Resource address parser error.";
+ break;
+
+ case "Warning":
+ p1 = "Non-critical error.";
+ break;
+ }
+
+ let p2 = message;
+
+ p2 = p2.replace(/Permission denied to access property (.*)/gm, "The system cannot access the $1 property.")
+ p2 = p2.replace(/(.*) is not defined/gm, "The system attempted to access the $1 variable while it is not defined at this point.")
+ p2 = p2.replace(/assignment to undeclared variable (.*)/gm, "The system attempted to assign a value to the $1 variable while it is not defined at this point.")
+ p2 = p2.replace(/assignment to undeclared variable (.*)/gm, "The system attempted to assign a value to the $1 variable while it is not defined at this point.")
+ p2 = p2.replace(/can't access lexical declaration (.*) before initialization/gm, "The system attempted to access the $1 variable while it has not been declared yet.")
+ p2 = p2.replace(/reference to undefined property (.*)/gm, "The system attempted to access the $1 property while it is not defined in that object.")
+ p2 = p2.replace(/reference to undefined property (.*)/gm, "The system attempted to access the $1 property while it is not defined in that object.")
+ p2 = p2.replace(/(.*) has no properties/gm, "The system attempted to access properties of $1 while it doesn't have any.")
+ p2 = p2.replace(/(.*) is \(not\) (.*)/gm, "The system attempted to access the $1 variable while it is not the same as $2.")
+ p2 = p2.replace(/(.*) is not a constructor/gm, "The system attempted to use $1 as a class constructor while it is not.")
+ p2 = p2.replace(/(.*) is not a function/gm, "The system attempted to use $1 as a function or method while it is not.")
+ p2 = p2.replace(/(.*) is not a non-null object/gm, "The system attempted to use $1 as an object while it is not null.")
+ p2 = p2.replace(/(.*) is read-only/gm, "The system attempted to change $1 while it is read-only.")
+ p2 = p2.replace(/(.*) is not iterable/gm, "The system attempted to count iterations of $1 while this is not possible.")
+ p2 = p2.replace(/(.*)\.prototype\.(.*) called on incompatible type/gm, "The system attempted to call prototype $2 of class $1 while it is not compatible.")
+ p2 = p2.replace(/can't access property (.*) of (.*)/gm, "The system attempted to access property $1 of $2 while that is not possible.")
+ p2 = p2.replace(/can't access property (.*) of (.*)/gm, "The system attempted to access property $1 of $2 while that is not possible.")
+ p2 = p2.replace(/can't assign to property (.*) on (.*): not an object/gm, "The system attempted to assign a value to property $1 of $2 while $2 is not an object.")
+ p2 = p2.replace(/can't define property (.*): (.*) is not extensible/gm, "The system attempted to define a $1 property on $2 while it is not extensible.")
+
+ return p1 + " " + p2;
+ }
+} \ No newline at end of file
diff --git a/core/InteractionManager.ts b/core/InteractionManager.ts
new file mode 100644
index 0000000..af2b62f
--- /dev/null
+++ b/core/InteractionManager.ts
@@ -0,0 +1,3 @@
+export class InteractionManager {
+
+} \ No newline at end of file
diff --git a/core/LogManager.ts b/core/LogManager.ts
new file mode 100644
index 0000000..916bc67
--- /dev/null
+++ b/core/LogManager.ts
@@ -0,0 +1,84 @@
+import chalk from 'chalk';
+import * as fs from 'fs';
+
+if (!fs.existsSync("logs")) fs.mkdirSync("logs");
+
+export class LogManager {
+ public static logID = new Date().toISOString();
+
+ /**
+ * @return void
+ * @param message - The message to display
+ * @description Shows a warning message
+ */
+ public static warn(message: string): void {
+ let messageParts = message.split(":");
+ let messagePrefix = messageParts[0];
+ messageParts.shift()
+
+ console.log(
+ chalk.gray("[" + new Date().toISOString() + "] ") +
+ chalk.yellow("[warn] ") +
+ (messageParts.length > 0 ? chalk.underline(messagePrefix) + ":" + messageParts.join(":") : messagePrefix)
+ );
+
+ fs.appendFileSync("logs/" + LogManager.logID + ".txt", "\n[" + new Date().toISOString() + "] [warn] " + message);
+ }
+
+ /**
+ * @return void
+ * @param message - The message to display
+ * @description Shows an information message
+ */
+ public static info(message: string): void {
+ let messageParts = message.split(":");
+ let messagePrefix = messageParts[0];
+ messageParts.shift()
+
+ console.log(
+ chalk.gray("[" + new Date().toISOString() + "] ") +
+ chalk.cyan("[info] ") +
+ (messageParts.length > 0 ? chalk.underline(messagePrefix) + ":" + messageParts.join(":") : messagePrefix)
+ );
+
+ fs.appendFileSync("logs/" + LogManager.logID + ".txt", "\n[" + new Date().toISOString() + "] [info] " + message);
+ }
+
+ /**
+ * @return void
+ * @param message - The message to display
+ * @description Shows a debugging message
+ */
+ public static verbose(message: string): void {
+ let messageParts = message.split(":");
+ let messagePrefix = messageParts[0];
+ messageParts.shift()
+
+ console.log(
+ chalk.gray("[" + new Date().toISOString() + "] ") +
+ chalk.green("[verb] ") +
+ (messageParts.length > 0 ? chalk.underline(messagePrefix) + ":" + messageParts.join(":") : messagePrefix)
+ );
+
+ fs.appendFileSync("logs/" + LogManager.logID + ".txt", "\n[" + new Date().toISOString() + "] [verb] " + message);
+ }
+
+ /**
+ * @return void
+ * @param message - The message to display
+ * @description Shows an error message
+ */
+ public static error(message: string): void {
+ let messageParts = message.split(":");
+ let messagePrefix = messageParts[0];
+ messageParts.shift()
+
+ console.log(
+ chalk.gray("[" + new Date().toISOString() + "] ") +
+ chalk.red("[crit] ") +
+ (messageParts.length > 0 ? chalk.underline(messagePrefix) + ":" + messageParts.join(":") : messagePrefix)
+ );
+
+ fs.appendFileSync("logs/" + LogManager.logID + ".txt", "\n[" + new Date().toISOString() + "] [crit] " + message);
+ }
+}
diff --git a/core/ManagedChannelManager.ts b/core/ManagedChannelManager.ts
new file mode 100644
index 0000000..538b345
--- /dev/null
+++ b/core/ManagedChannelManager.ts
@@ -0,0 +1,41 @@
+import {Message} from "discord.js";
+import {Welcome} from "./Welcome";
+import Fuse from "fuse.js";
+import {LogManager} from "./LogManager";
+
+export class ManagedChannelManager {
+ public static handleMessage(message: Message): void {
+ if (message.author.id === global.client.user.id) return;
+ let data = global.data;
+
+ let channels = data.managedChannels.map(i => i.channel);
+ if (channels.includes(message.channel.id)) {
+ let channel = data.managedChannels.find(i => i.channel === message.channel.id);
+ if (channel.user === message.author.id) {
+ let question = Welcome.questions[channel.question];
+
+ const fuse = new Fuse(question.response, {
+ includeScore: true
+ })
+
+ let results = fuse.search(message.content);
+
+ if (results.length === 0) {
+ message.react("❌");
+ return;
+ }
+
+ LogManager.verbose("Question \"" + question.question + "\": response \"" + message.content + "\", score " + results[0].score.toFixed(5));
+
+ if (results[0].score < 0.5) {
+ message.react("✅");
+ message.member.roles.add("996411960293859400");
+ message.channel.delete();
+ data.managedChannels.splice(data.managedChannels.indexOf(channel), 1);
+ } else {
+ message.react("❌");
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/core/PresenceManager.ts b/core/PresenceManager.ts
new file mode 100644
index 0000000..3e88dbc
--- /dev/null
+++ b/core/PresenceManager.ts
@@ -0,0 +1,7 @@
+import {Client} from "discord.js";
+
+export class PresenceManager {
+ public static start(client: Client): void {
+ client.user.setPresence({activities: [{name: 'with you!'}], status: 'online'});
+ }
+} \ No newline at end of file
diff --git a/core/SlashCommandsRefresher.ts b/core/SlashCommandsRefresher.ts
new file mode 100644
index 0000000..0503366
--- /dev/null
+++ b/core/SlashCommandsRefresher.ts
@@ -0,0 +1,35 @@
+import {LogManager} from "./LogManager";
+import {REST} from '@discordjs/rest';
+import {Routes} from 'discord-api-types/v9';
+import {CommandsLoader} from "./CommandsLoader";
+
+export class SlashCommandsRefresher {
+ public static async refresh(clientId: string, token: string) {
+ const rest = new REST({version: '9'}).setToken(token);
+ try {
+ const commandsLoader = new CommandsLoader();
+ LogManager.info('Started refreshing application (/) commands.');
+
+ await rest.put(
+ Routes.applicationCommands(clientId),
+ {body: []},
+ );
+
+ try {
+ await rest.put(
+ Routes.applicationGuildCommands(clientId, "996331009744318534"),
+ {body: commandsLoader.slashCommands()},
+ );
+ } catch (e) {
+ await rest.put(
+ Routes.applicationGuildCommands(clientId, "969994404184084560"),
+ {body: commandsLoader.slashCommands()},
+ );
+ }
+
+ LogManager.info('Successfully reloaded application (/) commands. Changes may take a while to appear on Discord.');
+ } catch (error) {
+ console.error(error);
+ }
+ }
+} \ No newline at end of file
diff --git a/core/Welcome.ts b/core/Welcome.ts
new file mode 100644
index 0000000..1a5e7c1
--- /dev/null
+++ b/core/Welcome.ts
@@ -0,0 +1,50 @@
+import {GuildMember} from "discord.js";
+import {LogManager} from "./LogManager";
+import * as fs from "fs";
+
+export class Welcome {
+ public static questions = JSON.parse(fs.readFileSync("./assets/questions.json", "utf8"));
+
+ public static welcome(member: GuildMember) {
+ let data = global.data;
+ LogManager.warn(member.user.tag + " joined the server");
+
+ let guild = member.guild;
+ guild.channels.create("welcome").then(channel => {
+ let thisChannel = {
+ channel: channel.id,
+ user: member.user.id,
+ question: Math.floor(Math.random() * Welcome.questions.length),
+ };
+ data.managedChannels.push(thisChannel);
+
+ channel.permissionOverwrites.create(guild.roles.everyone, {
+ VIEW_CHANNEL: false,
+ });
+
+ channel.permissionOverwrites.create(member.id, {
+ VIEW_CHANNEL: true,
+ });
+
+ let welcome = guild.channels.resolve("996412407217926224");
+ // @ts-ignore
+ welcome.permissionOverwrites.create(member.id, {
+ VIEW_CHANNEL: false,
+ });
+
+ channel.send("**Welcome to " + guild.name + ", <@" + member.id + ">!**\n\nBefore you can access the server, you need to read and agree to the <#996403284246016031>. Once done, send a reply to the following question:\n\n> " + Welcome.questions[thisChannel.question].question + "\n\nIf you reply correctly, you will be able to access the server. Contact the mods if you need help (managed channel ID: `" + thisChannel.channel + "`).\n\n:warning: Note that the bot might not accept your answer if it is not spelled properly. If it is not accepted, try again with a different phrasing. In case of doubt, the bot will direct you to a mod.");
+ });
+ }
+
+ public static unwelcome(member: GuildMember) {
+ let data = global.data;
+ LogManager.warn(member.user.tag + " left the server");
+
+ let users = data.managedChannels.map(i => i.user);
+ if (users.includes(member.id)) {
+ let channel = data.managedChannels.find(i => i.user === member.id);
+ member.guild.channels.resolve(channel.channel).delete();
+ data.managedChannels.splice(data.managedChannels.indexOf(channel), 1);
+ }
+ }
+} \ No newline at end of file