From 7b5df4ca0a5bd6fcf033ef40563599593b156910 Mon Sep 17 00:00:00 2001 From: Minteck Date: Wed, 13 Jul 2022 23:34:21 +0200 Subject: Initial commit --- core/CommandAction.ts | 46 +++++++++++++++++++++ core/CommandBase.ts | 8 ++++ core/CommandInteractionManager.ts | 21 ++++++++++ core/CommandsLoader.ts | 37 +++++++++++++++++ core/CoolerPony.ts | 80 +++++++++++++++++++++++++++++++++++++ core/ErrorParser.ts | 67 +++++++++++++++++++++++++++++++ core/InteractionManager.ts | 3 ++ core/LogManager.ts | 84 +++++++++++++++++++++++++++++++++++++++ core/ManagedChannelManager.ts | 41 +++++++++++++++++++ core/PresenceManager.ts | 7 ++++ core/SlashCommandsRefresher.ts | 35 ++++++++++++++++ core/Welcome.ts | 50 +++++++++++++++++++++++ 12 files changed, 479 insertions(+) create mode 100644 core/CommandAction.ts create mode 100644 core/CommandBase.ts create mode 100644 core/CommandInteractionManager.ts create mode 100644 core/CommandsLoader.ts create mode 100644 core/CoolerPony.ts create mode 100644 core/ErrorParser.ts create mode 100644 core/InteractionManager.ts create mode 100644 core/LogManager.ts create mode 100644 core/ManagedChannelManager.ts create mode 100644 core/PresenceManager.ts create mode 100644 core/SlashCommandsRefresher.ts create mode 100644 core/Welcome.ts (limited to 'core') 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; + + 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 -- cgit