diff --git a/.cspell/custom-dictionary-workspace.txt b/.cspell/custom-dictionary-workspace.txt index a1e5544..92db357 100644 --- a/.cspell/custom-dictionary-workspace.txt +++ b/.cspell/custom-dictionary-workspace.txt @@ -13,6 +13,7 @@ inställningar inte Krediter multistream +nastyox Nivå omdöme Omdöme @@ -20,11 +21,15 @@ Otillgänglig pino Poäng Profil +rando satta senaste +Sifell själv Språk Språkkod upsert uuidv +Vermium +xyter Zyner diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..76add87 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..39d4adc --- /dev/null +++ b/.eslintrc @@ -0,0 +1,15 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint", "no-loops", "prettier"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "rules": { + "no-console": 1, + "no-loops/no-loops": 2 + } +} diff --git a/.eslintrc.json b/.eslintrc.json index 57791b5..39d4adc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "root": true, "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint", "prettier"], + "plugins": ["@typescript-eslint", "no-loops", "prettier"], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", @@ -9,10 +9,7 @@ "prettier" ], "rules": { - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/no-explicit-any": 0, - "no-async-promise-executor": 0, - "prettier/prettier": "error" + "no-console": 1, + "no-loops/no-loops": 2 } } diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..c37466e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged \ No newline at end of file diff --git a/config.json.example b/config.json.example deleted file mode 100644 index 15e1f6a..0000000 --- a/config.json.example +++ /dev/null @@ -1,34 +0,0 @@ -{ - "bot": { - "token": "required", - "clientId": "required", - "guildId": "required" - }, - "colors": { - "success": "0x22bb33", - "error": "0xbb2124", - "wait": "0xf0ad4e" - }, - "mongodb": { - "url": "mongodb+srv://username:password@server/database?retryWrites=true&w=majority" - }, - "footer": { - "icon": "https://avatars.githubusercontent.com/u/83163073", - "text": "https://github.com/ZynerOrg/xyter" - }, - "disable": { - "redeem": false - }, - "hoster": { - "name": "someone", - "url": "scheme://domain.tld" - }, - "debug": false, - "reputation": { - "timeout": 86400000 - }, - "secretKey": "SET A LONG RANDOM PASSWORD HERE, ITS FOR USE OF ENCRYPTION WITH LENGTH OF 32", - "importToDB": false, - "clearUnused": false, - "devMode": false -} diff --git a/package.json b/package.json index ca1435c..a87d369 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,17 @@ "description": "Earn credits while chatting! And more", "main": "src/index.ts", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "nodemon | pino-pretty -i pid,hostname -t yyyy-mm-dd HH:MM:s" + "test": "jest", + "start": "nodemon | pino-pretty -i pid,hostname -t yyyy-mm-dd HH:MM:s", + "prettier-format": "prettier \"src/**/*.ts\" --write", + "lint": "eslint ./src --ext .ts", + "prepare": "husky install" }, "keywords": [ - "zyner", + "Zyner", + "xyter", "controlpanel", - "controlpanel.gg", - "vermium" + "controlpanel.gg" ], "repository": { "type": "git", @@ -26,25 +29,15 @@ "dependencies": { "@discordjs/builders": "^0.12.0", "@discordjs/rest": "^0.3.0", - "@nastyox/rando.js": "^2.0.5", "axios": "^0.26.0", - "better-sqlite3": "^7.5.0", "chance": "^1.1.8", "common": "^0.2.5", "crypto": "^1.0.1", - "dbd-dark-dashboard": "^1.6.45", "discord-api-types": "^0.31.0", - "discord-dashboard": "^2.3.14", - "discord-easy-dashboard": "^1.2.1", "discord.js": "^13.6.0", - "dotenv": "^16.0.0", "i18next": "^21.6.13", - "module-alias": "^2.2.2", "mongoose": "^6.2.3", "node-schedule": "^2.1.0", - "pino": "^7.0.0-rc.9", - "pino-pretty": "^7.6.1", - "quick.db": "^7.1.3", "ts-node": "^10.7.0", "tsconfig-paths": "^3.14.1", "typescript": "^4.6.3", @@ -61,7 +54,14 @@ "eslint-config-airbnb-base": "15.0.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-import": "2.26.0", + "eslint-plugin-no-loops": "^0.3.0", "eslint-plugin-prettier": "^4.0.0", + "husky": "^7.0.0", + "jest": "^27.5.1", + "lint-staged": "^12.3.7", "prettier": "^2.6.0" + }, + "lint-staged": { + "*.ts": "eslint --cache --fix" } } diff --git a/src/config/example.api.ts b/src/config/example.api.ts deleted file mode 100644 index 7d5621c..0000000 --- a/src/config/example.api.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Controlpanel.gg (Pterodactyl) API token -export const cpggToken = ""; diff --git a/src/config/example.discord.ts b/src/config/example.discord.ts index 682ff99..7b22c28 100644 --- a/src/config/example.discord.ts +++ b/src/config/example.discord.ts @@ -1,5 +1,14 @@ +import { Intents } from "discord.js"; // discord.js + // Discord API token export const token = ""; // Discord API id export const clientId = ""; + +// Discord API intents +export const intents = [ + Intents.FLAGS.GUILDS, + Intents.FLAGS.GUILD_MESSAGES, + Intents.FLAGS.GUILD_MEMBERS, +]; diff --git a/src/database/index.ts b/src/database/index.ts index 7d59bd4..4fd3870 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -8,13 +8,13 @@ import logger from "@logger"; import { url } from "@config/database"; export default async () => { - mongoose.connect(url).then(async (connection: any) => { + mongoose.connect(url).then(async (connection) => { logger?.info(`Connected to database: ${connection.connection.name}`); }); - mongoose.connection.on("error", (error: any) => { + mongoose.connection.on("error", (error) => { logger?.error(error); }); - mongoose.connection.on("warn", (warning: any) => { + mongoose.connection.on("warn", (warning) => { logger?.warn(warning); }); }; diff --git a/src/database/schemas/guild.ts b/src/database/schemas/guild.ts index ff3328f..792596b 100644 --- a/src/database/schemas/guild.ts +++ b/src/database/schemas/guild.ts @@ -17,6 +17,14 @@ interface IGuild { minimumLength: number; timeout: number; }; + welcome: { + status: boolean; + joinChannel: string; + leaveChannel: string; + joinChannelMessage: string; + leaveChannelMessage: string; + }; + audits: { status: boolean; channelId: string }; } const guildSchema = new Schema( @@ -83,6 +91,20 @@ const guildSchema = new Schema( default: 5000, }, }, + welcome: { + status: { + type: Boolean, + default: false, + }, + joinChannel: { type: String }, + leaveChannel: { type: String }, + joinChannelMessage: { type: String }, + leaveChannelMessage: { type: String }, + }, + audits: { + status: { type: Boolean, default: false }, + channelId: { type: String }, + }, }, { timestamps: true } ); diff --git a/src/events/guildMemberAdd/audits.ts b/src/events/guildMemberAdd/audits.ts new file mode 100644 index 0000000..6d76893 --- /dev/null +++ b/src/events/guildMemberAdd/audits.ts @@ -0,0 +1,43 @@ +import logger from "@logger"; +import { GuildMember, MessageEmbed, TextChannel } from "discord.js"; + +import guildSchema from "@schemas/guild"; + +import { footerText, footerIcon, successColor } from "@config/embed"; + +export default { + execute: async (member: GuildMember) => { + const guildData = await guildSchema.findOne({ guildId: member.guild.id }); + + const { client } = member; + + if (guildData === null) return; + + if (guildData.audits.status !== true) return; + if (!guildData.audits.channelId) return; + + const channel = client.channels.cache.get(`${guildData.audits.channelId}`); + + if (channel === null) return; + + (channel as TextChannel).send({ + embeds: [ + new MessageEmbed() + .setColor(successColor) + .setAuthor({ + name: "Member Joined", + iconURL: member.user.displayAvatarURL(), + }) + .setDescription(`${member.user} ${member.user.tag}`) + .addFields([ + { name: "Account Age", value: `${member.user.createdAt}` }, + ]) + .setTimestamp() + .setFooter({ + text: footerText, + iconURL: footerIcon, + }), + ], + }); + }, +}; diff --git a/src/events/guildMemberAdd/index.ts b/src/events/guildMemberAdd/index.ts index c3c2808..4b2cbe0 100644 --- a/src/events/guildMemberAdd/index.ts +++ b/src/events/guildMemberAdd/index.ts @@ -5,6 +5,8 @@ import { GuildMember } from "discord.js"; import updatePresence from "@helpers/updatePresence"; import fetchUser from "@helpers/fetchUser"; import logger from "@logger"; +import joinMessage from "../guildMemberAdd/joinMessage"; +import audits from "../guildMemberAdd/audits"; export default { name: "guildMemberAdd", @@ -15,6 +17,8 @@ export default { `New member: ${user.tag} (${user.id}) added to guild: ${guild.name} (${guild.id})` ); + await audits.execute(member); + await joinMessage.execute(member); await fetchUser(user, guild); await updatePresence(client); }, diff --git a/src/events/guildMemberAdd/joinMessage.ts b/src/events/guildMemberAdd/joinMessage.ts new file mode 100644 index 0000000..8c7e9bb --- /dev/null +++ b/src/events/guildMemberAdd/joinMessage.ts @@ -0,0 +1,45 @@ +import logger from "@logger"; +import { GuildMember, MessageEmbed, TextChannel } from "discord.js"; + +import guildSchema from "@schemas/guild"; + +import { footerText, footerIcon, successColor } from "@config/embed"; + +export default { + execute: async (member: GuildMember) => { + logger.info(member); + + const guildData = await guildSchema.findOne({ guildId: member.guild.id }); + + const { client } = member; + + if (guildData === null) return; + + if (guildData.welcome.status !== true) return; + if (!guildData.welcome.joinChannel) return; + + const channel = client.channels.cache.get( + `${guildData.welcome.joinChannel}` + ); + + if (channel === null) return; + + (channel as TextChannel).send({ + embeds: [ + new MessageEmbed() + .setColor(successColor) + .setTitle(`${member.user.username} has joined the server!`) + .setThumbnail(member.user.displayAvatarURL()) + .setDescription( + guildData.welcome.joinChannelMessage || + "Configure a join message in the `/settings guild welcome`." + ) + .setTimestamp() + .setFooter({ + text: footerText, + iconURL: footerIcon, + }), + ], + }); + }, +}; diff --git a/src/events/guildMemberRemove/audits.ts b/src/events/guildMemberRemove/audits.ts new file mode 100644 index 0000000..e096814 --- /dev/null +++ b/src/events/guildMemberRemove/audits.ts @@ -0,0 +1,40 @@ +import logger from "@logger"; +import { GuildMember, MessageEmbed, TextChannel } from "discord.js"; + +import guildSchema from "@schemas/guild"; + +import { footerText, footerIcon, errorColor } from "@config/embed"; + +export default { + execute: async (member: GuildMember) => { + const guildData = await guildSchema.findOne({ guildId: member.guild.id }); + + const { client } = member; + + if (guildData === null) return; + + if (guildData.audits.status !== true) return; + if (!guildData.audits.channelId) return; + + const channel = client.channels.cache.get(`${guildData.audits.channelId}`); + + if (channel === null) return; + + (channel as TextChannel).send({ + embeds: [ + new MessageEmbed() + .setColor(errorColor) + .setAuthor({ + name: "Member Left", + iconURL: member.user.displayAvatarURL(), + }) + .setDescription(`${member.user} ${member.user.tag}`) + .setTimestamp() + .setFooter({ + text: footerText, + iconURL: footerIcon, + }), + ], + }); + }, +}; diff --git a/src/events/guildMemberRemove/index.ts b/src/events/guildMemberRemove/index.ts index 24ae581..e2fe667 100644 --- a/src/events/guildMemberRemove/index.ts +++ b/src/events/guildMemberRemove/index.ts @@ -5,6 +5,8 @@ import { GuildMember } from "discord.js"; import updatePresence from "@helpers/updatePresence"; import dropUser from "@helpers/dropUser"; import logger from "@logger"; +import leaveMessage from "./leaveMessage"; +import audits from "./audits"; export default { name: "guildMemberRemove", @@ -15,6 +17,8 @@ export default { `Removed member: ${user.tag} (${user.id}) from guild: ${guild.name} (${guild.id})` ); + await audits.execute(member); + await leaveMessage.execute(member); await dropUser(user, guild); await updatePresence(client); }, diff --git a/src/events/guildMemberRemove/leaveMessage.ts b/src/events/guildMemberRemove/leaveMessage.ts new file mode 100644 index 0000000..b4f6100 --- /dev/null +++ b/src/events/guildMemberRemove/leaveMessage.ts @@ -0,0 +1,45 @@ +import logger from "@logger"; +import { GuildMember, MessageEmbed, TextChannel } from "discord.js"; + +import guildSchema from "@schemas/guild"; + +import { footerText, footerIcon, errorColor } from "@config/embed"; + +export default { + execute: async (member: GuildMember) => { + logger.info(member); + + const guildData = await guildSchema.findOne({ guildId: member.guild.id }); + + const { client } = member; + + if (guildData === null) return; + + if (guildData.welcome.status !== true) return; + if (!guildData.welcome.leaveChannel) return; + + const channel = client.channels.cache.get( + `${guildData.welcome.leaveChannel}` + ); + + if (channel === null) return; + + (channel as TextChannel).send({ + embeds: [ + new MessageEmbed() + .setColor(errorColor) + .setTitle(`${member.user.username} has left the server!`) + .setThumbnail(member.user.displayAvatarURL()) + .setDescription( + guildData.welcome.leaveChannelMessage || + "Configure a leave message in the `/settings guild welcome`." + ) + .setTimestamp() + .setFooter({ + text: footerText, + iconURL: footerIcon, + }), + ], + }); + }, +}; diff --git a/src/events/interactionCreate/audits.ts b/src/events/interactionCreate/audits.ts new file mode 100644 index 0000000..5fe29e6 --- /dev/null +++ b/src/events/interactionCreate/audits.ts @@ -0,0 +1,48 @@ +import logger from "@logger"; +import { Interaction, MessageEmbed, TextChannel } from "discord.js"; + +import guildSchema from "@schemas/guild"; + +import { footerText, footerIcon, successColor } from "@config/embed"; + +export default { + execute: async (interaction: Interaction) => { + if (interaction === null) return; + + if (interaction.guild === null) return; + + const guildData = await guildSchema.findOne({ + guildId: interaction.guild.id, + }); + + const { client } = interaction; + + if (guildData === null) return; + + if (guildData.audits.status !== true) return; + if (!guildData.audits.channelId) return; + + const channel = client.channels.cache.get(`${guildData.audits.channelId}`); + + if (channel === null) return; + + (channel as TextChannel).send({ + embeds: [ + new MessageEmbed() + .setColor(successColor) + .setDescription( + ` + **Interaction created by** ${interaction.user.username} **in** ${interaction.channel} + ` + ) + .setThumbnail(interaction.user.displayAvatarURL()) + .addFields([{ name: "Event", value: "interactionCreate" }]) + .setTimestamp() + .setFooter({ + text: footerText, + iconURL: footerIcon, + }), + ], + }); + }, +}; diff --git a/src/events/interactionCreate/index.ts b/src/events/interactionCreate/index.ts index e93d10a..fb35bc9 100644 --- a/src/events/interactionCreate/index.ts +++ b/src/events/interactionCreate/index.ts @@ -4,6 +4,7 @@ import { CommandInteraction } from "discord.js"; // Dependencies import isCommand from "@root/events/interactionCreate/components/isCommand"; import logger from "@logger"; +import audits from "./audits"; export default { name: "interactionCreate", @@ -14,6 +15,7 @@ export default { `New interaction: ${id} in guild: ${guild?.name} (${guild?.id})` ); + await audits.execute(interaction); await isCommand(interaction); }, }; diff --git a/src/events/messageDelete/audits.ts b/src/events/messageDelete/audits.ts new file mode 100644 index 0000000..f7f0ed1 --- /dev/null +++ b/src/events/messageDelete/audits.ts @@ -0,0 +1,51 @@ +import logger from "@logger"; +import { Message, MessageEmbed, TextChannel } from "discord.js"; + +import guildSchema from "@schemas/guild"; + +import { footerText, footerIcon, successColor } from "@config/embed"; + +export default { + execute: async (message: Message) => { + if (message === null) return; + + if (message.guild === null) return; + + const guildData = await guildSchema.findOne({ + guildId: message.guild.id, + }); + + const { client } = message; + + if (guildData === null) return; + + if (guildData.audits.status !== true) return; + if (!guildData.audits.channelId) return; + + const channel = client.channels.cache.get(`${guildData.audits.channelId}`); + + if (channel === null) return; + + (channel as TextChannel).send({ + embeds: [ + new MessageEmbed() + .setColor(successColor) + .setAuthor({ + name: message.author.username, + iconURL: message.author.displayAvatarURL(), + }) + .setDescription( + ` + **Message sent by** ${message.author} **deleted in** ${message.channel} + ${message.content} + ` + ) + .setTimestamp() + .setFooter({ + text: footerText, + iconURL: footerIcon, + }), + ], + }); + }, +}; diff --git a/src/events/messageDelete/index.ts b/src/events/messageDelete/index.ts new file mode 100644 index 0000000..97fcfde --- /dev/null +++ b/src/events/messageDelete/index.ts @@ -0,0 +1,9 @@ +import { Message } from "discord.js"; +import audits from "@events/messageDelete/audits"; + +export default { + name: "messageDelete", + async execute(message: Message) { + await audits.execute(message); + }, +}; diff --git a/src/events/messageUpdate/audits.ts b/src/events/messageUpdate/audits.ts new file mode 100644 index 0000000..50c76b5 --- /dev/null +++ b/src/events/messageUpdate/audits.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-loops/no-loops */ +import logger from "@logger"; +import { Message, MessageEmbed, TextChannel } from "discord.js"; + +import guildSchema from "@schemas/guild"; + +import { footerText, footerIcon, successColor } from "@config/embed"; + +export default { + execute: async (oldMessage: Message, newMessage: Message) => { + if (oldMessage === null) return; + if (newMessage === null) return; + + if (oldMessage.guild === null) return; + if (newMessage.guild === null) return; + + const guildData = await guildSchema.findOne({ + guildId: oldMessage.guild.id, + }); + + const { client } = oldMessage; + + if (guildData === null) return; + + if (guildData.audits.status !== true) return; + if (!guildData.audits.channelId) return; + + const channel = client.channels.cache.get(`${guildData.audits.channelId}`); + + if (channel === null) return; + + (channel as TextChannel).send({ + embeds: [ + new MessageEmbed() + .setColor(successColor) + .setAuthor({ + name: newMessage.author.username, + iconURL: newMessage.author.displayAvatarURL(), + }) + .setDescription( + ` + **Message edited in** ${newMessage.channel} [jump to message](https://discord.com/channels/${newMessage.guild.id}/${newMessage.channel.id}/${newMessage.id}) + ` + ) + .setTimestamp() + .setFooter({ + text: footerText, + iconURL: footerIcon, + }), + ], + }); + }, +}; diff --git a/src/events/messageUpdate/index.ts b/src/events/messageUpdate/index.ts index 52547b5..2ea1e42 100644 --- a/src/events/messageUpdate/index.ts +++ b/src/events/messageUpdate/index.ts @@ -5,11 +5,15 @@ import logger from "@logger"; // Modules import counter from "./modules/counter"; +import audits from "./audits"; + export default { name: "messageUpdate", - async execute(_oldMessage: Message, newMessage: Message) { + async execute(oldMessage: Message, newMessage: Message) { const { author, guild } = newMessage; + await audits.execute(oldMessage, newMessage); + logger?.verbose( `Message update event fired by ${author.tag} (${author.id}) in guild: ${guild?.name} (${guild?.id})` ); diff --git a/src/events/ready/index.ts b/src/events/ready/index.ts index d39f97f..b840bb3 100644 --- a/src/events/ready/index.ts +++ b/src/events/ready/index.ts @@ -1,25 +1,25 @@ // Dependencies import { Client } from "discord.js"; -import logger from "../../logger"; +import logger from "@logger"; // Helpers -import deployCommands from "../../handlers/deployCommands"; -import updatePresence from "../../helpers/updatePresence"; -import devMode from "../../helpers/devMode"; +import updatePresence from "@helpers/updatePresence"; +import deployCommands from "@handlers/deployCommands"; +import devMode from "@handlers/devMode"; export default { name: "ready", once: true, async execute(client: Client) { - logger?.info(`${client.user?.tag} (${client.user?.id}) is ready`); + logger.info(`${client.user?.tag} (${client.user?.id}) is ready`); await updatePresence(client); await devMode(client); - await deployCommands(); + await deployCommands(client); - client?.guilds?.cache?.forEach((guild) => { - logger?.info( - `${client.user?.tag} (${client.user?.id}) is in guild: ${guild.name} (${guild.id})` + client.guilds?.cache.forEach((guild) => { + logger.verbose( + `${client.user?.tag} (${client.user?.id}) is in guild: ${guild.name} (${guild.id}) with member count of ${guild.memberCount}` ); }); }, diff --git a/src/handlers/commands.ts b/src/handlers/commands.ts index 415ec15..dd4df30 100644 --- a/src/handlers/commands.ts +++ b/src/handlers/commands.ts @@ -1,18 +1,18 @@ import fs from "fs"; // fs import { Collection } from "discord.js"; // discord.js -import { Client } from "../types/common/discord"; -import logger from "../logger"; +import { Client } from "@root/types/common/discord"; +import logger from "@logger"; export default async (client: Client) => { client.commands = new Collection(); - fs.readdir("./src/plugins", async (error: any, plugins: any) => { + fs.readdir("./src/plugins", async (error, plugins) => { if (error) { - return logger?.error(`Error reading plugins: ${error}`); + return logger.error(`Error reading plugins: ${error}`); } await Promise.all( - plugins?.map(async (pluginName: any) => { + plugins.map(async (pluginName) => { const plugin = await import(`../plugins/${pluginName}`); await client?.commands?.set( @@ -20,8 +20,14 @@ export default async (client: Client) => { plugin?.default ); - logger?.verbose(`Loaded plugin: ${pluginName}`); + logger.verbose(`Loaded plugin: ${pluginName}`); }) - ); + ) + .then(async () => { + logger.debug("Successfully loaded plugins."); + }) + .catch(async (err) => { + logger.error(err); + }); }); }; diff --git a/src/handlers/deployCommands.ts b/src/handlers/deployCommands.ts index 65d4627..d408886 100644 --- a/src/handlers/deployCommands.ts +++ b/src/handlers/deployCommands.ts @@ -3,52 +3,51 @@ import { token, clientId } from "@config/discord"; import { devMode, guildId } from "@config/other"; import logger from "../logger"; -import fs from "fs"; +import { Client } from "@root/types/common/discord"; import { REST } from "@discordjs/rest"; import { Routes } from "discord-api-types/v9"; -export default async () => { - fs.readdir("./src/plugins", async (error: any, plugins: any) => { - if (error) { - return logger?.error(new Error(error)); - } +export default async (client: Client) => { + const pluginList = [] as string[]; - const pluginList = [] as any; + await Promise.all( + client.commands.map(async (pluginData: any) => { + pluginList.push(pluginData.data.toJSON()); + logger.verbose( + `${pluginData.data.name} successfully pushed to plugin list.` + ); + }) + ) + .then(async () => { + logger.debug("Successfully pushed all plugins to plugin list."); + }) + .catch(async (error) => { + logger.error(error); + }); - await Promise.all( - plugins?.map(async (pluginName: any) => { - const plugin = await import(`../plugins/${pluginName}`); + const rest = new REST({ version: "9" }).setToken(token); - pluginList.push(plugin.default.data.toJSON()); - - logger?.verbose(`Loaded plugin: ${pluginName} for deployment`); - }) - ); - - const rest = new REST({ version: "9" }).setToken(token); + await rest + .put(Routes.applicationCommands(clientId), { + body: pluginList, + }) + .then(async () => { + logger.debug(`Successfully deployed plugins to Discord`); + }) + .catch(async (error) => { + logger.error(error); + }); + if (devMode) { await rest - .put(Routes.applicationCommands(clientId), { + .put(Routes.applicationGuildCommands(clientId, guildId), { body: pluginList, }) - .then(async () => { - logger?.info(`Successfully deployed plugins to Discord`); - }) - .catch(async (err: any) => { - logger.error(err); + .then(async () => + logger.debug(`Successfully deployed guild plugins to Discord`) + ) + .catch(async (error) => { + logger.error(error); }); - - if (devMode) { - await rest - .put(Routes.applicationGuildCommands(clientId, guildId), { - body: pluginList, - }) - .then(async () => - logger?.info(`Successfully deployed guild plugins to Discord`) - ) - .catch(async (err: any) => { - logger.error(err); - }); - } - }); + } }; diff --git a/src/helpers/devMode.ts b/src/handlers/devMode.ts similarity index 77% rename from src/helpers/devMode.ts rename to src/handlers/devMode.ts index 6695799..3d4c78f 100644 --- a/src/helpers/devMode.ts +++ b/src/handlers/devMode.ts @@ -9,11 +9,11 @@ import { devMode, guildId } from "@config/other"; export default async (client: Client) => { if (!devMode) { return client?.application?.commands?.set([], guildId).then(async () => { - return logger?.verbose( + return logger.debug( `Development commands disabled for guild: ${guildId}` ); }); } - return logger?.verbose(`Development commands enabled for guild: ${guildId}`); + return logger.debug(`Development commands enabled for guild: ${guildId}`); }; diff --git a/src/handlers/events.ts b/src/handlers/events.ts index f72010a..1428325 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -3,20 +3,33 @@ import { Client } from "discord.js"; // discord.js import logger from "@logger"; export default async (client: Client) => { - const eventFiles = fs.readdirSync("./src/events"); - - for (const file of eventFiles) { - const event = require(`../events/${file}`); - if (event.once) { - client.once(event.default.name, (...args) => - event.default.execute(...args) - ); - logger?.verbose(`Loaded event: ${event.default.name}`); - } else { - client.on(event.default.name, (...args) => - event.default.execute(...args) - ); - logger?.verbose(`Loaded event: ${event.default.name}`); + fs.readdir("./src/events", async (error, events) => { + if (error) { + return logger.error(`Error reading plugins: ${error}`); } - } + + await Promise.all( + events.map(async (eventName) => { + const event = await import(`../events/${eventName}`); + + logger.verbose(`Loaded event: ${eventName}`); + + if (event.once) { + return client.once(event.default.name, async (...args) => + event.default.execute(...args) + ); + } + + return client.on(event.default.name, async (...args) => + event.default.execute(...args) + ); + }) + ) + .then(async () => { + logger.debug("Successfully loaded events."); + }) + .catch(async (err) => { + logger.error(err); + }); + }); }; diff --git a/src/helpers/dropUser.ts b/src/helpers/dropUser.ts index cb47db9..10662c9 100644 --- a/src/helpers/dropUser.ts +++ b/src/helpers/dropUser.ts @@ -6,7 +6,7 @@ import { Guild, User } from "discord.js"; export default async (user: User, guild: Guild) => { await userSchema - .deleteOne({ userId: user?.id, guildId: guild?.id }) + .deleteOne({ userId: user.id, guildId: guild.id }) .then(async () => { logger?.verbose(`Deleted user: ${user?.id} from guild: ${guild?.id}`); }) diff --git a/src/helpers/saveUser.ts b/src/helpers/saveUser.ts index cb1045b..48e4a7a 100644 --- a/src/helpers/saveUser.ts +++ b/src/helpers/saveUser.ts @@ -1,5 +1,5 @@ -import sleep from "./sleep"; -import logger from "../logger"; +import sleep from "@helpers/sleep"; +import logger from "@logger"; import Chance from "chance"; export default async function saveUser(data: any, data2: any) { diff --git a/src/index.ts b/src/index.ts index 5b9c81b..270ab96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,30 +1,28 @@ -// Dependencies import "tsconfig-paths/register"; // Allows using tsconfig.json paths during runtime -import { Client, Intents } from "discord.js"; // discord.js + +import { token, intents } from "@config/discord"; + +import { Client } from "discord.js"; // discord.js import locale from "@locale"; import database from "@database"; import schedules from "@schedules"; - import events from "@handlers/events"; import commands from "@handlers/commands"; -// Configurations -import { token } from "@config/discord"; +async function main() { + const client = new Client({ + intents, + }); -const client = new Client({ - intents: [ - Intents?.FLAGS?.GUILDS, - Intents?.FLAGS?.GUILD_MESSAGES, - Intents?.FLAGS?.GUILD_MEMBERS, - ], -}); + await locale(); + await database(); + await schedules(client); -locale(); -database(); -schedules(client); + await commands(client); + await events(client); -commands(client); -events(client); + await client.login(token); +} -client?.login(token); +main(); diff --git a/src/locale/index.ts b/src/locale/index.ts index 4749232..a3eb2df 100644 --- a/src/locale/index.ts +++ b/src/locale/index.ts @@ -135,9 +135,9 @@ export default async () => { }, }) .then(async () => { - logger?.verbose(`i18next initialized`); + logger.debug(`i18next initialized`); }) .catch(async (error) => { - logger?.error(`i18next failed to initialize: ${error}`); + logger.error(`i18next failed to initialize: ${error}`); }); }; diff --git a/src/plugins/reputation/index.ts b/src/plugins/reputation/index.ts index 6e4de17..2ab6ce5 100644 --- a/src/plugins/reputation/index.ts +++ b/src/plugins/reputation/index.ts @@ -3,10 +3,10 @@ import { SlashCommandBuilder } from "@discordjs/builders"; import { CommandInteraction } from "discord.js"; // Modules -import give from "./modules/give"; +import give from "@plugins/reputation/modules/give"; // Handlers -import logger from "../../logger"; +import logger from "@logger"; // Function export default { diff --git a/src/plugins/settings/guild/index.ts b/src/plugins/settings/guild/index.ts index c19e701..f9e8939 100644 --- a/src/plugins/settings/guild/index.ts +++ b/src/plugins/settings/guild/index.ts @@ -11,6 +11,8 @@ import logger from "@logger"; import pterodactyl from "./modules/pterodactyl"; import credits from "./modules/credits"; import points from "./modules/points"; +import welcome from "./modules/welcome"; +import audits from "./modules/audits"; import { SlashCommandSubcommandGroupBuilder } from "@discordjs/builders"; // Function @@ -21,7 +23,9 @@ export default { .setDescription("Guild settings.") .addSubcommand(pterodactyl.data) .addSubcommand(credits.data) - .addSubcommand(points.data); + .addSubcommand(points.data) + .addSubcommand(welcome.data) + .addSubcommand(audits.data); }, execute: async (interaction: CommandInteraction) => { // Destructure member @@ -51,16 +55,32 @@ export default { logger?.verbose(`Executing pterodactyl subcommand`); return pterodactyl.execute(interaction); - } else if (options?.getSubcommand() === "credits") { + } + + if (options?.getSubcommand() === "credits") { logger?.verbose(`Executing credits subcommand`); return credits.execute(interaction); - } else if (options?.getSubcommand() === "points") { + } + + if (options?.getSubcommand() === "points") { logger?.verbose(`Executing points subcommand`); return points.execute(interaction); } + if (options?.getSubcommand() === "welcome") { + logger?.verbose(`Executing welcome subcommand`); + + return welcome.execute(interaction); + } + + if (options?.getSubcommand() === "audits") { + logger?.verbose(`Executing audit subcommand`); + + return audits.execute(interaction); + } + logger?.verbose(`No subcommand found`); }, }; diff --git a/src/plugins/settings/guild/modules/audits.ts b/src/plugins/settings/guild/modules/audits.ts new file mode 100644 index 0000000..f04ff97 --- /dev/null +++ b/src/plugins/settings/guild/modules/audits.ts @@ -0,0 +1,85 @@ +// Dependencies +import { CommandInteraction } from "discord.js"; + +// Configurations +import { successColor, footerText, footerIcon } from "@config/embed"; + +// Handlers +import logger from "@logger"; + +// Models +import guildSchema from "@schemas/guild"; +import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import { ChannelType } from "discord-api-types/v10"; + +// Function +export default { + data: (command: SlashCommandSubcommandBuilder) => { + return command + .setName("audits") + .setDescription("Audits") + .addBooleanOption((option) => + option.setName("status").setDescription("Should audits be enabled?") + ) + .addChannelOption((option) => + option + .setName("channel") + .setDescription("Channel for audit messages.") + .addChannelType(ChannelType.GuildText as number) + ); + }, + execute: async (interaction: CommandInteraction) => { + // Destructure member + const { options, guild } = interaction; + + // Get options + const status = options?.getBoolean("status"); + const channel = options?.getChannel("channel"); + + // Get guild object + const guildDB = await guildSchema?.findOne({ + guildId: guild?.id, + }); + + if (guildDB === null) { + return logger?.verbose(`Guild not found in database.`); + } + + // Modify values + guildDB.audits.status = status !== null ? status : guildDB?.audits?.status; + guildDB.audits.channelId = + channel !== null ? channel.id : guildDB?.audits?.channelId; + + // Save guild + await guildDB?.save()?.then(async () => { + logger?.verbose(`Guild audits updated.`); + + return interaction?.editReply({ + embeds: [ + { + title: ":hammer: Settings - Guild [Audits]", + description: `Audits settings updated.`, + color: successColor, + fields: [ + { + name: "🤖 Status", + value: `${guildDB?.audits?.status}`, + inline: true, + }, + { + name: "🌊 Channel", + value: `${guildDB?.audits?.channelId}`, + inline: true, + }, + ], + timestamp: new Date(), + footer: { + iconURL: footerIcon, + text: footerText, + }, + }, + ], + }); + }); + }, +}; diff --git a/src/plugins/settings/guild/modules/welcome.ts b/src/plugins/settings/guild/modules/welcome.ts new file mode 100644 index 0000000..d45b642 --- /dev/null +++ b/src/plugins/settings/guild/modules/welcome.ts @@ -0,0 +1,131 @@ +// Dependencies +import { CommandInteraction } from "discord.js"; + +// Configurations +import { successColor, footerText, footerIcon } from "@config/embed"; + +// Handlers +import logger from "@logger"; + +// Models +import guildSchema from "@schemas/guild"; +import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import { ChannelType } from "discord-api-types/v10"; + +// Function +export default { + data: (command: SlashCommandSubcommandBuilder) => { + return command + .setName("welcome") + .setDescription("Welcome") + .addBooleanOption((option) => + option.setName("status").setDescription("Should welcome be enabled?") + ) + .addChannelOption((option) => + option + .setName("join-channel") + .setDescription("Channel for join messages.") + .addChannelType(ChannelType.GuildText as number) + ) + .addChannelOption((option) => + option + .setName("leave-channel") + .setDescription("Channel for leave messages.") + .addChannelType(ChannelType.GuildText as number) + ) + .addStringOption((option) => + option + .setName("leave-message") + .setDescription("Message for leave messages.") + ) + .addStringOption((option) => + option + .setName("join-message") + .setDescription("Message for join messages.") + ); + }, + execute: async (interaction: CommandInteraction) => { + // Destructure member + const { options, guild } = interaction; + + // Get options + const status = options?.getBoolean("status"); + const joinChannel = options?.getChannel("join-channel"); + const leaveChannel = options?.getChannel("leave-channel"); + const joinChannelMessage = options?.getString("join-message"); + const leaveChannelMessage = options?.getString("leave-message"); + + // Get guild object + const guildDB = await guildSchema?.findOne({ + guildId: guild?.id, + }); + + if (guildDB === null) { + return logger?.verbose(`Guild not found in database.`); + } + + // Modify values + guildDB.welcome.status = + status !== null ? status : guildDB?.welcome?.status; + guildDB.welcome.joinChannel = + joinChannel !== null ? joinChannel.id : guildDB?.welcome?.joinChannel; + guildDB.welcome.leaveChannel = + leaveChannel !== null ? leaveChannel.id : guildDB?.welcome?.leaveChannel; + + guildDB.welcome.joinChannelMessage = + joinChannelMessage !== null + ? joinChannelMessage + : guildDB?.welcome?.joinChannelMessage; + guildDB.welcome.leaveChannelMessage = + leaveChannelMessage !== null + ? leaveChannelMessage + : guildDB?.welcome?.leaveChannelMessage; + + // Save guild + await guildDB?.save()?.then(async () => { + logger?.verbose(`Guild welcome updated.`); + + return interaction?.editReply({ + embeds: [ + { + title: ":hammer: Settings - Guild [Welcome]", + description: `Welcome settings updated.`, + color: successColor, + fields: [ + { + name: "🤖 Status", + value: `${guildDB?.welcome?.status}`, + inline: true, + }, + { + name: "🌊 Join Channel", + value: `${guildDB?.welcome?.joinChannel}`, + inline: true, + }, + { + name: "🌊 Leave Channel", + value: `${guildDB?.welcome?.leaveChannel}`, + inline: true, + }, + { + name: "📄 Join Channel Message", + value: `${guildDB?.welcome?.joinChannelMessage}`, + inline: true, + }, + { + name: "📄 Leave Channel Message", + value: `${guildDB?.welcome?.leaveChannelMessage}`, + inline: true, + }, + ], + timestamp: new Date(), + footer: { + iconURL: footerIcon, + text: footerText, + }, + }, + ], + }); + }); + }, +}; diff --git a/src/schedules/index.ts b/src/schedules/index.ts index ce563e7..71ce35b 100644 --- a/src/schedules/index.ts +++ b/src/schedules/index.ts @@ -11,7 +11,14 @@ export default async (client: Client) => { const expression = "*/5 * * * *"; schedule.scheduleJob(expression, async () => { - logger?.verbose("Running shop roles job."); - await shopRoles(client); + logger.info("Running jobs."); + + await shopRoles(client) + .then(() => { + logger.info("Shop roles job finished."); + }) + .catch((err) => { + logger.error(`Shop roles job failed: ${err}`); + }); }); }; diff --git a/src/schedules/jobs/shopRoles.ts b/src/schedules/jobs/shopRoles.ts index 444786c..877e3f1 100644 --- a/src/schedules/jobs/shopRoles.ts +++ b/src/schedules/jobs/shopRoles.ts @@ -9,51 +9,131 @@ import shopRoleSchema from "@schemas/shopRole"; import guildSchema from "@schemas/guild"; export default async (client: Client) => { - await shopRoleSchema?.find()?.then(async (shopRoles: any) => { - shopRoles?.map(async (shopRole: any) => { - const payed = new Date(shopRole?.lastPayed); + const roles = await shopRoleSchema.find(); - const oneHourAfterPayed = payed?.setHours(payed?.getHours() + 1); + await Promise.all( + roles.map(async (role) => { + const { guildId, userId, roleId } = role; - if (new Date() > new Date(oneHourAfterPayed)) { - logger?.verbose(`Shop role ${shopRole?.roleId} is expired.`); + const lastPayment = new Date(role.lastPayed); - // Get guild object - const guild = await guildSchema?.findOne({ - guildId: shopRole?.guildId, - }); + const nextPayment = new Date( + lastPayment.setHours(lastPayment.getHours() + 1) + ); - if (guild === null) return; - const userDB = await userSchema?.findOne({ - userId: shopRole?.userId, - guildId: shopRole?.guildId, - }); - const { pricePerHour } = guild.shop.roles; + if (new Date() < nextPayment) { + logger.silly(`Shop role ${roleId} is not due for payment.`); + } - if (userDB === null) return; + const guildData = await guildSchema.findOne({ guildId }); - if (userDB?.credits < pricePerHour) { - const rGuild = client?.guilds?.cache?.get(`${shopRole?.guildId}`); - const rMember = await rGuild?.members?.fetch(`${shopRole?.userId}`); + if (!guildData) { + logger.error(`Guild ${guildId} not found.`); + return; + } - shopRoleSchema - ?.deleteOne({ _id: shopRole?._id }) - ?.then(async () => - logger?.verbose(`Shop role ${shopRole?.roleId} was deleted.`) - ) - .catch(async (error) => { - return logger?.error(error); - }); + if (!userId) { + logger.error(`User ID not found for shop role ${roleId}.`); + return; + } - return rMember?.roles?.remove(`${shopRole?.roleId}`); + const userData = await userSchema.findOne({ guildId, userId }); + + if (!userData) { + logger.error(`User ${userId} not found for shop role ${roleId}.`); + return; + } + + const rGuild = client?.guilds?.cache?.get(guildId); + + const rMember = await rGuild?.members?.fetch(userId); + + if (!rMember) { + logger.error(`Member ${userId} not found for shop role ${roleId}.`); + return; + } + + const rRole = rMember.roles.cache.get(roleId); + + if (!rRole) { + logger.error(`Role ${roleId} not found for shop role ${roleId}.`); + return; + } + + if (!rMember) { + logger.error(`Member ${userId} not found for shop role ${roleId}.`); + return; + } + + if (new Date() > nextPayment) { + logger.verbose( + `Shop role ${roleId} is due for payment. Withdrawing credits from user ${userId}.` + ); + + const { pricePerHour } = guildData.shop.roles; + + if (userData.credits < pricePerHour) { + logger.error( + `User ${userId} does not have enough credits to pay for shop role ${roleId}.` + ); + + if (!rMember) { + logger.error(`Member ${userId} not found for shop role ${roleId}.`); + await shopRoleSchema + .deleteOne({ + userId, + roleId, + guildId, + }) + .then(async () => { + logger.verbose( + `Shop role document ${roleId} has been deleted from user ${userId}.` + ); + }) + .catch(async (error) => { + logger.error( + `Error deleting shop role document ${roleId} from user ${userId}.`, + error + ); + }); + return; + } + + rMember.roles.remove(roleId); + + return; } - shopRole.lastPayed = new Date(); - shopRole?.save()?.then(async () => { - userDB.credits -= pricePerHour; - userDB?.save(); - }); + userData.credits -= pricePerHour; + + await userData + .save() + .then(async () => { + role.lastPayed = new Date(); + + await role + .save() + .then(async () => { + logger.verbose(`Shop role ${roleId} has been paid for.`); + }) + .catch(async (err) => { + logger.error( + `Error saving shop role ${roleId} last payed date.`, + err + ); + }); + + logger.verbose( + `Shop role ${roleId} has been paid for. Keeping role ${roleId} for user ${userId}.` + ); + }) + .catch(async (err) => { + logger.error( + `Error saving user ${userId} credits for shop role ${roleId}.`, + err + ); + }); } - }); - }); + }) + ); }; diff --git a/tsconfig.json b/tsconfig.json index 722bd83..93449a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,34 +1,34 @@ -{ - "compilerOptions": { - "target": "es2019", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "module": "commonjs", - "resolveJsonModule": true, - "isolatedModules": true, - "outDir": "./build", - "baseUrl": "./src", - "typeRoots": ["/types/common", "./node_modules/@types"], - "paths": { - "@interface/*": ["Interfaces/*"], - "@root/*": ["*"], - "@config/*": ["config/*"], - "@events/*": ["events/*"], - "@logger": ["logger"], - "@database": ["database"], - "@schedules": ["schedules"], - "@handlers/*": ["handlers/*"], - "@helpers/*": ["helpers/*"], - "@locale": ["locale"], - "@schemas/*": ["database/schemas/*"] - } - }, - "include": ["./src"], - "exclude": ["./node_modules", "./test"] -} \ No newline at end of file +{ + "compilerOptions": { + "target": "es2022", + "module": "CommonJS", + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "isolatedModules": true, + "outDir": "./build", + "baseUrl": "./src", + "typeRoots": ["/types/common", "./node_modules/@types"], + "paths": { + "@interface/*": ["Interfaces/*"], + "@root/*": ["*"], + "@config/*": ["config/*"], + "@events/*": ["events/*"], + "@logger": ["logger"], + "@database": ["database"], + "@schedules": ["schedules"], + "@handlers/*": ["handlers/*"], + "@helpers/*": ["helpers/*"], + "@locale": ["locale"], + "@plugins/*": ["plugins/*"], + "@schemas/*": ["database/schemas/*"] + } + }, + "include": ["./src"], + "exclude": ["./node_modules", "./test"] +}