From b2ab62df896c0d488e5cfe03b058024e4a026556 Mon Sep 17 00:00:00 2001 From: Kath Date: Wed, 24 Jul 2024 15:50:30 +0800 Subject: [PATCH 1/5] feat(Stats Channels) --- .eslintrc | 3 +- config.example.json | 5 +++ src/contracts/embedHandler.js | 7 +++- src/discord/CommandHandler.js | 4 +- .../commands/forceUpdateChannelsCommand.js | 40 +++++++++++++++++++ src/discord/events/interactionCreate.js | 4 ++ src/discord/handlers/StateHandler.js | 5 +++ src/discord/other/statsChannels.js | 8 ++++ 8 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 src/discord/commands/forceUpdateChannelsCommand.js create mode 100644 src/discord/other/statsChannels.js diff --git a/.eslintrc b/.eslintrc index 7108666f..a93ce935 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,7 +11,8 @@ }, "globals": { "bot": true, - "client": true + "client": true, + "guild": true }, "rules": { "curly": ["warn", "multi-line", "consistent"], diff --git a/config.example.json b/config.example.json index 43687a95..a7aee874 100644 --- a/config.example.json +++ b/config.example.json @@ -109,5 +109,10 @@ "autoUpdater": true, "autoUpdaterInterval": 12, "logToFiles": true + }, + "statsChannels": { + "enabled": false, + "autoUpdaterInterval": 5, + "channels": [{ "id": "CHANNEL_ID", "name": "Guild Level: {guildLevel}" }] } } diff --git a/src/contracts/embedHandler.js b/src/contracts/embedHandler.js index d56547c0..29a5dbf8 100644 --- a/src/contracts/embedHandler.js +++ b/src/contracts/embedHandler.js @@ -60,14 +60,19 @@ class SuccessEmbed extends Embed { /** * Constructs a new SuccessEmbed instance. * @param {string} description - The description of the success. + * @param {object} footer - The footer of the success. */ - constructor(description) { + constructor(description, footer) { super(); this.setAuthor({ name: "Success" }); this.setColor(5763719); this.setDescription(description); + + if (footer) { + this.setFooter(footer); + } } } diff --git a/src/discord/CommandHandler.js b/src/discord/CommandHandler.js index 2da12bce..c9aaa2a4 100644 --- a/src/discord/CommandHandler.js +++ b/src/discord/CommandHandler.js @@ -8,7 +8,7 @@ class CommandHandler { constructor(discord) { this.discord = discord; - const commands = []; + let commands = []; const commandFiles = fs.readdirSync("src/discord/commands").filter((file) => file.endsWith(".js")); for (const file of commandFiles) { @@ -16,6 +16,8 @@ class CommandHandler { commands.push(command); } + commands = commands.filter((command) => !command.channelsCommand); + const rest = new REST({ version: "10" }).setToken(config.discord.bot.token); const clientID = Buffer.from(config.discord.bot.token.split(".")[0], "base64").toString("ascii"); diff --git a/src/discord/commands/forceUpdateChannelsCommand.js b/src/discord/commands/forceUpdateChannelsCommand.js new file mode 100644 index 00000000..de6657e8 --- /dev/null +++ b/src/discord/commands/forceUpdateChannelsCommand.js @@ -0,0 +1,40 @@ +const hypixelRebornAPI = require("../../contracts/API/HypixelRebornAPI.js"); +const { replaceVariables } = require("../../contracts/helperFunctions.js"); +const { SuccessEmbed } = require("../../contracts/embedHandler.js"); +const config = require("../../../config.json"); + +module.exports = { + name: "force-update-channels", + description: "Unmutes the given user.", + moderatorOnly: true, + channelsCommand: true, + + execute: async (interaction, hidden = false) => { + const hypixelGuild = await hypixelRebornAPI.getGuild("player", bot.username); + const [channels, roles] = await Promise.all([guild.channels.fetch(), guild.roles.fetch()]); + + config.statsChannels.channels.forEach(async (channelInfo) => { + const channel = await guild.channels.fetch(channelInfo.id); + channel.setName( + replaceVariables(channelInfo.name, { + guildName: hypixelGuild.name, + guildLevel: hypixelGuild.level, + guildXP: hypixelGuild.experience, + guildWeeklyXP: hypixelGuild.totalWeeklyGexp, + guildMembers: hypixelGuild.members.length, + + discordMembers: guild.memberCount, + discordChannels: channels.size, + discordRoles: roles.size, + }), + "Updated Channels", + ); + }); + + const embed = new SuccessEmbed("The channels have been updated successfully.", { + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + if (hidden !== false) await interaction.followUp({ embeds: [embed] }); + }, +}; diff --git a/src/discord/events/interactionCreate.js b/src/discord/events/interactionCreate.js index 1bcc54c2..d187436d 100644 --- a/src/discord/events/interactionCreate.js +++ b/src/discord/events/interactionCreate.js @@ -24,6 +24,10 @@ module.exports = { return; } + if (command.channelsCommand === true && config.statsChannels.enabled === false) { + throw new HypixelDiscordChatBridgeError("Verification is disabled."); + } + if (command.moderatorOnly === true && isModerator(interaction) === false) { throw new HypixelDiscordChatBridgeError("You don't have permission to use this command."); } diff --git a/src/discord/handlers/StateHandler.js b/src/discord/handlers/StateHandler.js index 0db095f1..e65a3436 100644 --- a/src/discord/handlers/StateHandler.js +++ b/src/discord/handlers/StateHandler.js @@ -12,10 +12,15 @@ class StateHandler { activities: [{ name: `/help | by @duckysolucky` }], }); + global.guild = await client.guilds.fetch(config.discord.bot.serverID); + Logger.discordMessage("Guild ready, successfully fetched " + guild.name); + const channel = await this.getChannel("Guild"); if (channel === undefined) { return Logger.errorMessage(`Channel "Guild" not found!`); } + + if (config.statsChannels.enabled) require("../other/statsChannels.js"); channel.send({ embeds: [ diff --git a/src/discord/other/statsChannels.js b/src/discord/other/statsChannels.js new file mode 100644 index 00000000..dc3ab024 --- /dev/null +++ b/src/discord/other/statsChannels.js @@ -0,0 +1,8 @@ +const updateChannels = require("../commands/forceUpdateChannelsCommand.js"); +const config = require("../../../config.json"); +const cron = require("node-cron"); + +if (config.statsChannels.enabled) { + console.log("Stats channels enabled"); + cron.schedule(`*/${config.verification.autoUpdaterInterval} * * * *`, () => updateChannels.execute(null, true)); +} From d1854a68b269a044fbeb7b3d5ae7bf895b6d11b1 Mon Sep 17 00:00:00 2001 From: kath Date: Thu, 25 Jul 2024 20:59:55 +0800 Subject: [PATCH 2/5] Fix not returning --- src/discord/commands/forceUpdateChannelsCommand.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/discord/commands/forceUpdateChannelsCommand.js b/src/discord/commands/forceUpdateChannelsCommand.js index de6657e8..89766155 100644 --- a/src/discord/commands/forceUpdateChannelsCommand.js +++ b/src/discord/commands/forceUpdateChannelsCommand.js @@ -31,10 +31,11 @@ module.exports = { ); }); + if (hidden) return const embed = new SuccessEmbed("The channels have been updated successfully.", { text: `by @kathund. | /help [command] for more information`, iconURL: "https://i.imgur.com/uUuZx2E.png", }); - if (hidden !== false) await interaction.followUp({ embeds: [embed] }); + await interaction.followUp({ embeds: [embed] }); }, }; From 699b28918487bbc6644495ed95bb0fd91d81b66c Mon Sep 17 00:00:00 2001 From: kath Date: Thu, 25 Jul 2024 21:02:20 +0800 Subject: [PATCH 3/5] Update command description --- src/discord/commands/forceUpdateChannelsCommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/discord/commands/forceUpdateChannelsCommand.js b/src/discord/commands/forceUpdateChannelsCommand.js index 89766155..e2f1e24c 100644 --- a/src/discord/commands/forceUpdateChannelsCommand.js +++ b/src/discord/commands/forceUpdateChannelsCommand.js @@ -5,7 +5,7 @@ const config = require("../../../config.json"); module.exports = { name: "force-update-channels", - description: "Unmutes the given user.", + description: "Update the stats Channels", moderatorOnly: true, channelsCommand: true, From 233b7cc8343232e01fe0f680e360857ff32255b3 Mon Sep 17 00:00:00 2001 From: Kath Date: Mon, 5 Aug 2024 08:46:14 +0800 Subject: [PATCH 4/5] Squash merge main into statsChannels --- .gitignore | 3 +- README.md | 115 +++++++++++++ config.example.json | 15 ++ data/linked.json | 1 + src/contracts/API/mowojangAPI.js | 37 +++-- src/contracts/embedHandler.js | 2 + src/contracts/helperFunctions.js | 44 +++-- src/discord/CommandHandler.js | 4 + src/discord/DiscordManager.js | 4 + src/discord/commands/forceUpdateCommand.js | 26 +++ src/discord/commands/forceUpdateEveryone.js | 103 ++++++++++++ src/discord/commands/forceVerifyCommand.js | 32 ++++ src/discord/commands/linkedCommand.js | 92 +++++++++++ src/discord/commands/unverifyCommand.js | 52 ++++++ src/discord/commands/updateCommand.js | 173 ++++++++++++++++++++ src/discord/commands/verifyCommand.js | 140 ++++++++++++++++ src/discord/events/interactionCreate.js | 7 +- src/discord/handlers/StateHandler.js | 11 ++ src/discord/other/updateUsers.js | 13 ++ src/minecraft/handlers/ChatHandler.js | 45 ++++- 20 files changed, 871 insertions(+), 48 deletions(-) create mode 100644 data/linked.json create mode 100644 src/discord/commands/forceUpdateCommand.js create mode 100644 src/discord/commands/forceUpdateEveryone.js create mode 100644 src/discord/commands/forceVerifyCommand.js create mode 100644 src/discord/commands/linkedCommand.js create mode 100644 src/discord/commands/unverifyCommand.js create mode 100644 src/discord/commands/updateCommand.js create mode 100644 src/discord/commands/verifyCommand.js create mode 100644 src/discord/other/updateUsers.js diff --git a/.gitignore b/.gitignore index e8eb46ca..2e5b2331 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ .prettierrc.json config.json auth-cache/ -logs/ \ No newline at end of file +logs/ +data/ \ No newline at end of file diff --git a/README.md b/README.md index f1e7d270..45bebbf6 100644 --- a/README.md +++ b/README.md @@ -398,6 +398,121 @@ new Promise((resolve) => { }); ``` +### Verification + +The verification section contains configuration options for the verification system. These options include a full global toggle, verfied role, guild member role, auto updater, custom usernames and ranks. + +The `enabled` option determines whether the verification system is enabled. By default, this is set to false. + +The `verifiedRole` option determines what role user will receive upon successful verification. + +The `guildMemberRole` option determines what role user will receive upon being member of the guild. + +The `autoUpdater` option allows you to toggle the auto updating of all verified users. This option is set to `true` by default. + +The `autoUpdaterInterval` allows you to change how often the autoUpdater runs. By default this option is set to `24` making the autoUpdater run every 24 hours. + +The `name` option lets you set what a verfied user has there nickname set to in the server. By default its set to `{username}` Below are all the variables that are supported. + +
+Name Option Variables + +`{bedwarsStar}` - Player's BedWars Stars + +`{bedwarsTokens}` - Player's BedWars Tokens + +`{bedwarsKills}` - Player's BedWars Kills + +`{bedwarsDeaths}` - Player's BedWars Deaths + +`{bedwarsKDRatio}` - Player's BedWars KDRatio + +`{bedwarsFinalKills}` - Player's BedWars Final Kills + +`{bedwarsFinalDeathss}` - Player's BedWars Final Deaths + +`{bedwarsFinalKDRatio}` - Player's BedWars Final KDRatio + +`{bedwarsWins}` - Player's BedWars Wins + +`{bedwarsLosses}` - Player's BedWars Losses + +`{bedwarsWLRatio}` - Player's BedWars WLRatio + +`{bedwarsBedsBroken}` - Player's BedWars Beds Broken + +`{bedwarsBedsLost}` - Player's BedWars Beds Lost + +`{bedwarsBedsBLRatio}` - Player's BedWars Beds BLRatio + +`{bedwarsPlayedGames}` - Player's BedWars Played Games + +`{skywarsStar}` - Player's SkyWars Star + +`{skywarsCoins}` - Player's SkyWars Coins + +`{skywarsTokens}` - Player's SkyWars Tokens + +`{skywarsSouls}` - Player's SkyWars Souls + +`{skywarsOpals}` - Player's SkyWars Opals + +`{skywarsKills}` - Player's SkyWars Kills + +`{skywarsDeaths}` - Player's SkyWars Deaths + +`{skywarsKDRatio}` - Player's SkyWars KDRatio + +`{skywarsWins}` - Player's SkyWars Wins + +`{skywarsLosses}` - Player's SkyWars Losses + +`{skywarsWLRatio}` - Player's SkyWars WLRatio + +`{skywarsPlayedGames}` - Player's SkyWars Played Games + +`{duelsTitle}` - Player's Duels Title + +`{duelsKills}` - Player's Duels Kills + +`{duelsDeaths}` - Player's Duels Deaths + +`{duelsKDRatio}` - Player's Duels KDRatio + +`{duelsWins}` - Player's Duels Wins + +`{duelsLosses}` - Player's Duels Losses + +`{duelsWLRatio}` - Player's Duels WLRatio + +`{duelsPlayedGames}` - Player's Duels Played Games + +`{level}` - Player's Hypixel Level + +`{rank}` - Player's Hypixel Rank + +`{karma}` - Player's Hypixel Rank + +`{achievementPoints}` - Player's Hypixel Achievement Points + +`{username}` - Player's Username + +`{guildRank}` - Player's Guild Rank + +`{guildName}` - Player's Guild Name + +
+
+ +The `ranks` option is an array that takes in an object like the following example, this allows ingame ranks to have a synced discord role. +```json +{ + "name": "Sweat", + "role": "987936050649391194" +} +``` + + ### Chat Triggers Module If you think that message format is boring, you can check out my repository for ChatTriggers module which changes the way messages from Bot look like. [Click Here](https://github.com/DuckySoLucky/Hypixel-Guild-Chat-Format) diff --git a/config.example.json b/config.example.json index a7aee874..735865ce 100644 --- a/config.example.json +++ b/config.example.json @@ -105,6 +105,21 @@ "port": 1439, "token": "WEBSOCKET_TOKEN" }, + "verification": { + "enabled": false, + "verifiedRole": "VERIFIED_ROLE_ID", + "removeVerificationRole": true, + "guildMemberRole": "GUILD_MEMBER_ROLE_ID", + "autoUpdater": true, + "autoUpdaterInterval": 24, + "name": "{username}", + "ranks": [ + { + "name": "IN_GAME_RANK_NAME", + "role": "DISCORD_ROLE_ID" + } + ] + }, "other": { "autoUpdater": true, "autoUpdaterInterval": 12, diff --git a/data/linked.json b/data/linked.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/data/linked.json @@ -0,0 +1 @@ +{} diff --git a/src/contracts/API/mowojangAPI.js b/src/contracts/API/mowojangAPI.js index 8c9d9292..a2ee0fa7 100644 --- a/src/contracts/API/mowojangAPI.js +++ b/src/contracts/API/mowojangAPI.js @@ -1,12 +1,12 @@ const axios = require("axios"); -const fs = require("fs"); -const cache = new Map(); +const uuidCache = new Map(); +const usernameCache = new Map(); async function getUUID(username) { try { - if (cache.has(username)) { - const data = cache.get(username); + if (uuidCache.has(username)) { + const data = uuidCache.get(username); if (data.last_save + 43200000 > Date.now()) { return data.id; @@ -19,7 +19,7 @@ async function getUUID(username) { throw data.errorMessage ?? "Invalid username."; } - cache.set(username, { + uuidCache.set(username, { last_save: Date.now(), id: data.id, }); @@ -35,28 +35,31 @@ async function getUUID(username) { async function getUsername(uuid) { try { - let cache = JSON.parse(fs.readFileSync("data/usernameCache.json")); + if (usernameCache.has(uuid)) { + const data = usernameCache.get(uuid); - const user = cache.find((data) => data.uuid === uuid); - if (user !== undefined && user.last_save + 43200000 > Date.now()) { - return user.username; + if (data.last_save + 43200000 > Date.now()) { + return data.username; + } } const { data } = await axios.get(`https://mowojang.matdoes.dev/${uuid}`); - cache = cache.filter((data) => data.id !== uuid); - cache.push({ - username: data.name, - uuid: data.id, + if (data.errorMessage || data.name === undefined) { + throw data.errorMessage ?? "Invalid UUID."; + } + + const cache = { last_save: Date.now(), - }); + username: data.name, + }; - fs.writeFileSync("data/usernameCache.json", JSON.stringify(cache)); + usernameCache.set(uuid, cache); return data.name; } catch (error) { - // eslint-disable-next-line no-throw-literal - if (error.response.data === "Not found") throw "Invalid UUID."; console.log(error); + // eslint-disable-next-line no-throw-literal + if (error.response?.data === "Not found") throw "Invalid UUID."; throw error; } } diff --git a/src/contracts/embedHandler.js b/src/contracts/embedHandler.js index 29a5dbf8..3e7cf4dc 100644 --- a/src/contracts/embedHandler.js +++ b/src/contracts/embedHandler.js @@ -68,6 +68,8 @@ class SuccessEmbed extends Embed { this.setAuthor({ name: "Success" }); this.setColor(5763719); + if (footer) this.setFooter(footer); + this.setDescription(description); if (footer) { diff --git a/src/contracts/helperFunctions.js b/src/contracts/helperFunctions.js index 2451e5bf..b5b63e6e 100644 --- a/src/contracts/helperFunctions.js +++ b/src/contracts/helperFunctions.js @@ -1,11 +1,8 @@ -const fs = require("fs-promise"); -const { set } = require("lodash"); -const mkdirp = require("mkdirp"); -const getDirName = require("path").dirname; const nbt = require("prismarine-nbt"); +const moment = require("moment"); const util = require("util"); + const parseNbt = util.promisify(nbt.parse); -const moment = require("moment"); function replaceAllRanks(input) { input = input.replaceAll("[OWNER] ", ""); @@ -92,9 +89,8 @@ function toFixed(num, fixed) { return num.toString().match(response)[0]; } -function timeSince(timeStamp) { - var now = new Date(), - secondsPast = (now.getTime() - timeStamp) / 1000; +function timeSince(endTime, startTime = new Date().getTime()) { + var secondsPast = (startTime - endTime) / 1000; secondsPast = Math.abs(secondsPast); if (secondsPast < 60) { @@ -118,22 +114,6 @@ function timeSince(timeStamp) { } } -async function writeAt(filePath, jsonPath, value) { - mkdirp.sync(getDirName(filePath)); - - return fs - .readJson(filePath) - .then(function (json) { - set(json, jsonPath, value); - return fs.writeJson(filePath, json); - }) - .catch(function (error) { - const json = {}; - set(json, jsonPath, value); - return fs.writeJson(filePath, json); - }); -} - function capitalize(str) { if (!str) return str; const words = str.replace(/_/g, " ").toLowerCase().split(" "); @@ -285,6 +265,20 @@ function replaceVariables(template, variables) { return template.replace(/\{(\w+)\}/g, (match, name) => variables[name] ?? match); } +function getTimestamp(unixTimestamp = Date.now()) { + return new Date(unixTimestamp).toLocaleString("en-US", { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + hour12: false, + timeZoneName: "short", + timeZone: "UTC", + }); +} + module.exports = { replaceAllRanks, addNotation, @@ -293,7 +287,6 @@ module.exports = { addCommas, toFixed, timeSince, - writeAt, capitalize, decodeData, numberWithCommas, @@ -302,4 +295,5 @@ module.exports = { formatUsername, formatNumber, replaceVariables, + getTimestamp, }; diff --git a/src/discord/CommandHandler.js b/src/discord/CommandHandler.js index c9aaa2a4..ea9c81b0 100644 --- a/src/discord/CommandHandler.js +++ b/src/discord/CommandHandler.js @@ -13,6 +13,10 @@ class CommandHandler { for (const file of commandFiles) { const command = require(`./commands/${file}`); + if (command.verificationCommand === true && config.verification.enabled === false) { + continue; + } + commands.push(command); } diff --git a/src/discord/DiscordManager.js b/src/discord/DiscordManager.js index bfb0e35b..e25413c8 100644 --- a/src/discord/DiscordManager.js +++ b/src/discord/DiscordManager.js @@ -40,6 +40,10 @@ class DiscordManager extends CommunicationBridge { for (const file of commandFiles) { const command = require(`./commands/${file}`); + if (command.verificationCommand === true && config.verification.enabled === false) { + continue; + } + client.commands.set(command.name, command); } diff --git a/src/discord/commands/forceUpdateCommand.js b/src/discord/commands/forceUpdateCommand.js new file mode 100644 index 00000000..44e33eed --- /dev/null +++ b/src/discord/commands/forceUpdateCommand.js @@ -0,0 +1,26 @@ +const HypixelDiscordChatBridgeError = require("../../contracts/errorHandler.js"); + +module.exports = { + name: "force-update", + description: "Update a user's roles", + moderatorOnly: true, + verificationCommand: true, + options: [ + { + name: "user", + description: "Discord User", + type: 6, + required: true, + }, + ], + + execute: async (interaction) => { + const user = interaction.options.getUser("user"); + const updateRolesCommand = require("./updateCommand.js"); + if (updateRolesCommand === undefined) { + throw new HypixelDiscordChatBridgeError("The update command does not exist. Please contact an administrator."); + } + + await updateRolesCommand.execute(interaction, user); + }, +}; diff --git a/src/discord/commands/forceUpdateEveryone.js b/src/discord/commands/forceUpdateEveryone.js new file mode 100644 index 00000000..b39cbdea --- /dev/null +++ b/src/discord/commands/forceUpdateEveryone.js @@ -0,0 +1,103 @@ +const HypixelDiscordChatBridgeError = require("../../contracts/errorHandler.js"); +const { readFileSync, writeFileSync } = require("fs"); +const { EmbedBuilder } = require("discord.js"); + +module.exports = { + name: "force-update-everyone", + description: "Update a user's roles", + moderatorOnly: true, + verificationCommand: true, + + execute: async (interaction, doNotRespond = false) => { + try { + const updateRolesCommand = require("./updateCommand.js"); + if (updateRolesCommand === undefined) { + throw new HypixelDiscordChatBridgeError("The update command does not exist. Please contact an administrator."); + } + + const linkedData = readFileSync("data/linked.json"); + if (linkedData === undefined) { + throw new HypixelDiscordChatBridgeError( + "The linked data file does not exist. Please contact an administrator.", + ); + } + + const linked = JSON.parse(linkedData); + if (linked === undefined) { + throw new HypixelDiscordChatBridgeError("The linked data file is malformed. Please contact an administrator."); + } + + if (doNotRespond === false) { + const embed = new EmbedBuilder() + .setColor(3447003) + .setTitle("Updating Users") + .setDescription(`Progress: 0 / ${Object.keys(linked).length} (\`0%\`)`) + .setFooter({ + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + await interaction.editReply({ embeds: [embed], ephemeral: true }); + } + + const description = []; + for (const id in linked) { + const user = await guild.members.fetch(id).catch(() => {}); + if (user === undefined) { + delete linked[id]; + continue; + } + + await updateRolesCommand.execute(interaction, user.user, true).catch(() => { + description.push(`- <@${id}>`); + }); + + const embed = new EmbedBuilder() + .setColor(3447003) + .setTitle("Updating Users") + .setDescription( + `Progress: ${Object.keys(linked).indexOf(id)} / ${Object.keys(linked).length} (\`${((Object.keys(linked).indexOf(id) / Object.keys(linked).length) * 100).toFixed(2)}%\`)`, + ) + .setFooter({ + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + if (doNotRespond === false) { + await interaction.editReply({ embeds: [embed], ephemeral: true }); + } + } + + writeFileSync("data/linked.json", JSON.stringify(linked, null, 2)); + if (doNotRespond === false) { + if (description.length > 0) { + description.unshift(`\n__**Failed to update:**__`); + } + + description.unshift(`Updated **${Object.keys(linked).length}** users.`); + + const embed = new EmbedBuilder() + .setColor(3447003) + .setTitle("Users Updated") + .setDescription(description.join("\n")) + .setFooter({ + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + await interaction.editReply({ embeds: [embed], ephemeral: true }); + } + } catch (error) { + const errorEmbed = new EmbedBuilder() + .setColor(15548997) + .setAuthor({ name: "An Error has occurred" }) + .setDescription(`\`\`\`${error}\`\`\``) + .setFooter({ + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + await interaction.editReply({ embeds: [errorEmbed] }); + } + }, +}; diff --git a/src/discord/commands/forceVerifyCommand.js b/src/discord/commands/forceVerifyCommand.js new file mode 100644 index 00000000..1b61a1e0 --- /dev/null +++ b/src/discord/commands/forceVerifyCommand.js @@ -0,0 +1,32 @@ +const HypixelDiscordChatBridgeError = require("../../contracts/errorHandler.js"); + +module.exports = { + name: "force-verify", + description: "Connect Discord account to a Minecraft", + moderatorOnly: true, + verificationCommand: true, + options: [ + { + name: "user", + description: "Discord User", + type: 6, + required: true, + }, + { + name: "name", + description: "Minecraft Username", + type: 3, + required: true, + }, + ], + + execute: async (interaction) => { + const user = interaction.options.getUser("user"); + const verifyCommand = require("./verifyCommand.js"); + if (verifyCommand === undefined) { + throw new HypixelDiscordChatBridgeError("The verify command does not exist. Please contact an administrator."); + } + + await verifyCommand.execute(interaction, user, true); + }, +}; diff --git a/src/discord/commands/linkedCommand.js b/src/discord/commands/linkedCommand.js new file mode 100644 index 00000000..3961ce5b --- /dev/null +++ b/src/discord/commands/linkedCommand.js @@ -0,0 +1,92 @@ +const HypixelDiscordChatBridgeError = require("../../contracts/errorHandler.js"); +const { getUUID, getUsername } = require("../../contracts/API/mowojangAPI.js"); +const { SuccessEmbed } = require("../../contracts/embedHandler.js"); +const { EmbedBuilder } = require("discord.js"); +const { readFileSync } = require("fs"); + +module.exports = { + name: "linked", + description: "View who a user is linked to", + moderatorOnly: true, + verificationCommand: true, + options: [ + { + name: "user", + description: "Discord User", + type: 6, + required: false, + }, + { + name: "name", + description: "Minecraft Username", + type: 3, + required: false, + }, + ], + + execute: async (interaction) => { + try { + const linkedData = readFileSync("data/linked.json"); + if (linkedData === undefined) { + throw new HypixelDiscordChatBridgeError( + "The linked data file does not exist. Please contact an administrator.", + ); + } + + const linked = JSON.parse(linkedData); + if (linked === undefined) { + throw new HypixelDiscordChatBridgeError("The linked data file is malformed. Please contact an administrator."); + } + + const user = interaction.options.getUser("user"); + const name = interaction.options.getString("name"); + if (!user && !name) { + throw new HypixelDiscordChatBridgeError("Please provide a user or a name."); + } + + if (user && !name) { + const uuid = linked[user.id]; + if (uuid === undefined) { + throw new HypixelDiscordChatBridgeError("This user is not linked."); + } + + const username = await getUsername(uuid); + const embed = new SuccessEmbed(`<@${user.id}> is linked to \`${username}\` (\`${uuid}\`).`, { + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + await interaction.followUp({ embeds: [embed], ephemeral: true }); + } else if (!user && name) { + const uuid = await getUUID(name); + if (uuid === undefined) { + throw new HypixelDiscordChatBridgeError("This user does not exist."); + } + + const discordID = Object.keys(linked).find((key) => linked[key] === uuid); + if (discordID === undefined) { + throw new HypixelDiscordChatBridgeError("This user is not linked."); + } + + const embed = new SuccessEmbed(`\`${name}\` (\`${uuid}\`) is linked to <@${discordID}>.`, { + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + await interaction.followUp({ embeds: [embed], ephemeral: true }); + } else { + throw new HypixelDiscordChatBridgeError("Please provide a user or a name, not both."); + } + } catch (error) { + const errorEmbed = new EmbedBuilder() + .setColor(15548997) + .setAuthor({ name: "An Error has occurred" }) + .setDescription(`\`\`\`${error}\`\`\``) + .setFooter({ + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + await interaction.editReply({ embeds: [errorEmbed] }); + } + }, +}; diff --git a/src/discord/commands/unverifyCommand.js b/src/discord/commands/unverifyCommand.js new file mode 100644 index 00000000..3b646ad1 --- /dev/null +++ b/src/discord/commands/unverifyCommand.js @@ -0,0 +1,52 @@ +const HypixelDiscordChatBridgeError = require("../../contracts/errorHandler.js"); +const { SuccessEmbed } = require("../../contracts/embedHandler.js"); +const { writeFileSync, readFileSync } = require("fs"); +const { getUsername } = require("../../contracts/API/mowojangAPI.js"); +const { EmbedBuilder } = require("discord.js"); + +module.exports = { + name: "unverify", + description: "Remove your linked Minecraft account", + verificationCommand: true, + + execute: async (interaction) => { + try { + const linkedData = readFileSync("data/linked.json"); + if (!linkedData) { + throw new HypixelDiscordChatBridgeError( + "The linked data file does not exist. Please contact an administrator.", + ); + } + + const linked = JSON.parse(linkedData); + if (!linked) { + throw new HypixelDiscordChatBridgeError("The linked data file is malformed. Please contact an administrator."); + } + + const uuid = linked[interaction.user.id]; + if (uuid === undefined) { + throw new HypixelDiscordChatBridgeError(`You are not verified. Please run /verify to continue.`); + } + + delete linked[interaction.user.id]; + writeFileSync("data/linked.json", JSON.stringify(linked, null, 2)); + + const updateRole = new SuccessEmbed( + `You have successfully unlinked \`${await getUsername(uuid)}\`. Run \`/verify\` to link a new account.`, + { text: `by @kathund. | /help [command] for more information`, iconURL: "https://i.imgur.com/uUuZx2E.png" }, + ); + await interaction.followUp({ embeds: [updateRole] }); + } catch (error) { + const errorEmbed = new EmbedBuilder() + .setColor(15548997) + .setAuthor({ name: "An Error has occurred" }) + .setDescription(`\`\`\`${error}\`\`\``) + .setFooter({ + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + await interaction.editReply({ embeds: [errorEmbed], ephemeral: true }); + } + }, +}; diff --git a/src/discord/commands/updateCommand.js b/src/discord/commands/updateCommand.js new file mode 100644 index 00000000..6957bcc9 --- /dev/null +++ b/src/discord/commands/updateCommand.js @@ -0,0 +1,173 @@ +const HypixelDiscordChatBridgeError = require("../../contracts/errorHandler.js"); +const hypixelRebornAPI = require("../../contracts/API/HypixelRebornAPI.js"); +const { replaceVariables } = require("../../contracts/helperFunctions.js"); +const { SuccessEmbed } = require("../../contracts/embedHandler.js"); +const { EmbedBuilder } = require("discord.js"); +const config = require("../../../config.json"); +const { readFileSync } = require("fs"); + +module.exports = { + name: "update", + verificationCommand: true, + description: "Update your current roles", + + execute: async (interaction, user) => { + try { + const linkedData = readFileSync("data/linked.json"); + if (!linkedData) { + throw new HypixelDiscordChatBridgeError( + "The linked data file does not exist. Please contact an administrator.", + ); + } + + const linked = JSON.parse(linkedData); + if (!linked) { + throw new HypixelDiscordChatBridgeError("The linked data file is malformed. Please contact an administrator."); + } + + if (user !== undefined) { + interaction.user = user; + } + + if (!interaction.member) { + interaction.member = await guild.members.fetch(interaction.user.id); + } + + const uuid = linked[interaction.user.id]; + if (uuid === undefined) { + const roles = [ + config.verification.verifiedRole, + config.verification.guildMemberRole, + ...config.verification.ranks.map((r) => r.role), + ]; + + for (const role of roles) { + if (role === config.verification.verifiedRole && config.verification.removeVerificationRole === false) { + continue; + } + + if (interaction.member.roles.cache.has(role)) { + interaction.member.roles.remove(role, "Updated Roles"); + } + } + + interaction.member.setNickname(null, "Updated Roles"); + + throw new HypixelDiscordChatBridgeError("You are not linked to a Minecraft account."); + } + + if (!interaction.member.roles.cache.has(config.verification.verifiedRole)) { + interaction.member.roles.add(config.verification.verifiedRole, "Updated Roles"); + } + + const [hypixelGuild, player] = await Promise.all([ + hypixelRebornAPI.getGuild("player", bot.username), + hypixelRebornAPI.getPlayer(uuid), + ]); + + if (hypixelGuild === undefined) { + throw new HypixelDiscordChatBridgeError("Guild not found."); + } + + const guildMember = hypixelGuild.members.find((m) => m.uuid === uuid); + if (guildMember) { + interaction.member.roles.add(config.verification.guildMemberRole, "Updated Roles"); + + if (config.verification.ranks.length > 0 && guildMember.rank) { + const rank = config.verification.ranks.find((r) => r.name.toLowerCase() == guildMember.rank.toLowerCase()); + if (rank) { + for (const role of config.verification.ranks) { + if (interaction.member.roles.cache.has(role.role)) { + interaction.member.roles.remove(role.role, "Updated Roles"); + } + } + + interaction.member.roles.add(rank.role, "Updated Roles"); + } + } + } else { + if (interaction.member.roles.cache.has(config.verification.guildMemberRole)) { + interaction.member.roles.remove(config.verification.guildMemberRole, "Updated Roles"); + } + + if (config.verification.ranks.length > 0) { + for (const role of config.verification.ranks) { + if (interaction.member.roles.cache.has(role.role)) { + interaction.member.roles.remove(role.role, "Updated Roles"); + } + } + } + } + + interaction.member.setNickname( + replaceVariables(config.verification.name, { + bedwarsStar: player.stats.bedwars.level, + bedwarsTokens: player.stats.bedwars.tokens, + bedwarsKills: player.stats.bedwars.kills, + bedwarsDeaths: player.stats.bedwars.deaths, + bedwarsKDRatio: player.stats.bedwars.KDRatio, + bedwarsFinalKills: player.stats.bedwars.finalKills, + bedwarsFinalDeathss: player.stats.bedwars.finalDeaths, + bedwarsFinalKDRatio: player.stats.bedwars.finalKDRatio, + bedwarsWins: player.stats.bedwars.wins, + bedwarsLosses: player.stats.bedwars.losses, + bedwarsWLRatio: player.stats.bedwars.WLRatio, + bedwarsBedsBroken: player.stats.bedwars.beds.broken, + bedwarsBedsLost: player.stats.bedwars.beds.lost, + bedwarsBedsBLRatio: player.stats.bedwars.beds.BLRatio, + bedwarsPlayedGames: player.stats.bedwars.playedGames, + + skywarsStar: player.stats.skywars.level, + skywarsCoins: player.stats.skywars.coins, + skywarsTokens: player.stats.skywars.tokens, + skywarsSouls: player.stats.skywars.souls, + skywarsOpals: player.stats.skywars.opals, + skywarsKills: player.stats.skywars.kills, + skywarsDeaths: player.stats.skywars.deaths, + skywarsKDRatio: player.stats.skywars.KDRatio, + skywarsWins: player.stats.skywars.wins, + skywarsLosses: player.stats.skywars.losses, + skywarsWLRatio: player.stats.skywars.WLRatio, + skywarsPlayedGames: player.stats.skywars.playedGames, + + duelsTitle: player.stats.duels.division, + duelsKills: player.stats.duels.kills, + duelsDeaths: player.stats.duels.deaths, + duelsKDRatio: player.stats.duels.KDRatio, + duelsWins: player.stats.duels.wins, + duelsLosses: player.stats.duels.losses, + duelsWLRatio: player.stats.duels.WLRatio, + duelsPlayedGames: player.stats.duels.playedGames, + + level: player.level, + rank: player.rank, + karma: player.karma, + achievementPoints: player.achievementPoints, + username: player.nickname, + + guildRank: hypixelGuild.members.find((m) => m.uuid === uuid)?.rank ?? "Unknown", + guildName: hypixelGuild.name, + }), + "Updated Roles", + ); + + const updateRole = new SuccessEmbed( + `<@${interaction.user.id}>'s roles have been successfully synced with \`${player.nickname ?? "Unknown"}\`!`, + { text: `by @kathund. | /help [command] for more information`, iconURL: "https://i.imgur.com/uUuZx2E.png" }, + ); + + await interaction.followUp({ embeds: [updateRole], ephemeral: true }); + } catch (error) { + const errorEmbed = new EmbedBuilder() + .setColor(15548997) + .setAuthor({ name: "An Error has occurred" }) + .setDescription(`\`\`\`${error}\`\`\``) + .setFooter({ + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + await interaction.editReply({ embeds: [errorEmbed], ephemeral: true }); + } + }, +}; diff --git a/src/discord/commands/verifyCommand.js b/src/discord/commands/verifyCommand.js new file mode 100644 index 00000000..b7eb110a --- /dev/null +++ b/src/discord/commands/verifyCommand.js @@ -0,0 +1,140 @@ +const HypixelDiscordChatBridgeError = require("../../contracts/errorHandler.js"); +const hypixelRebornAPI = require("../../contracts/API/HypixelRebornAPI.js"); +const { writeFileSync, readFileSync } = require("fs"); +const config = require("../../../config.json"); +const { EmbedBuilder } = require("discord.js"); + +module.exports = { + name: "verify", + description: "Connect your Discord account to Minecraft", + verificationCommand: true, + options: [ + { + name: "name", + description: "Minecraft Username", + type: 3, + required: true, + }, + ], + + execute: async (interaction, user, bypassChecks = false) => { + try { + const linkedData = readFileSync("data/linked.json"); + if (linkedData === undefined) { + throw new HypixelDiscordChatBridgeError( + "The linked data file does not exist. Please contact an administrator.", + ); + } + + const linked = JSON.parse(linkedData); + if (linked === undefined) { + throw new HypixelDiscordChatBridgeError("The linked data file is malformed. Please contact an administrator."); + } + + if (bypassChecks === true && user !== undefined) { + interaction.user = user; + } + + if (Object.keys(linked).includes(interaction.user.id) === true) { + if (bypassChecks === true) { + delete linked[interaction.user.id]; + } else { + throw new HypixelDiscordChatBridgeError( + "You are already linked to a Minecraft account. Please run /unverify first.", + ); + } + } + + const username = interaction.options.getString("name"); + const { socialMedia, nickname, uuid } = await hypixelRebornAPI.getPlayer(username); + if (Object.values(linked).includes(uuid) === true) { + if (bypassChecks === true) { + delete linked[Object.keys(linked).find((key) => linked[key] === uuid)]; + } else { + throw new HypixelDiscordChatBridgeError( + "This player is already linked to a Discord account. Please contact an administrator.", + ); + } + } + + const discordUsername = socialMedia.find((media) => media.id === "DISCORD")?.link; + if (discordUsername === undefined && bypassChecks !== true) { + throw new HypixelDiscordChatBridgeError("This player does not have a Discord linked."); + } + + if (discordUsername !== interaction.user.username && bypassChecks !== true) { + throw new HypixelDiscordChatBridgeError( + `The player '${nickname}' has linked their Discord account to a different account ('${discordUsername}').`, + ); + } + + const linkedRole = guild.roles.cache.get(config.verification.verifiedRole); + if (linkedRole === undefined) { + throw new HypixelDiscordChatBridgeError("The verified role does not exist. Please contact an administrator."); + } + + linked[interaction.user.id] = uuid; + writeFileSync("data/linked.json", JSON.stringify(linked, null, 2)); + + const embed = new EmbedBuilder() + .setColor("4BB543") + .setAuthor({ name: "Successfully linked!" }) + .setDescription(`${user ? `<@${user.id}>'s` : "Your"} account has been successfully linked to \`${nickname}\``) + .setFooter({ + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + await interaction.editReply({ embeds: [embed], ephemeral: true }); + + const updateRolesCommand = require("./updateCommand.js"); + if (updateRolesCommand === undefined) { + throw new HypixelDiscordChatBridgeError("The update command does not exist. Please contact an administrator."); + } + + await updateRolesCommand.execute(interaction); + } catch (error) { + console.log(error); + // eslint-disable-next-line no-ex-assign + error = error + .toString() + .replaceAll("Error: [hypixel-api-reborn] ", "") + .replaceAll( + "Unprocessable Entity! For help join our Discord Server https://discord.gg/NSEBNMM", + "This player does not exist. (Mojang API might be down)", + ); + + const errorEmbed = new EmbedBuilder() + .setColor(15548997) + .setAuthor({ name: "An Error has occurred" }) + .setDescription(`\`\`\`${error}\`\`\``) + .setFooter({ + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + await interaction.editReply({ embeds: [errorEmbed], ephemeral: true }); + + if ( + error !== "You are already linked to a Minecraft account. Please run /unverify first." && + error.includes("linked") === true + ) { + const verificationTutorialEmbed = new EmbedBuilder() + .setColor(0x0099ff) + .setAuthor({ name: "Link with Hypixel Social Media" }) + .setDescription( + `**Instructions:**\n1) Use your Minecraft client to connect to Hypixel.\n2) Once connected, and while in the lobby, right click "My Profile" in your hotbar. It is option #2.\n3) Click "Social Media" - this button is to the left of the Redstone block (the Status button).\n4) Click "Discord" - it is the second last option.\n5) Paste your Discord username into chat and hit enter. For reference: \`${ + interaction.user.username ?? interaction.user.tag + }\`\n6) You're done! Wait around 30 seconds and then try again.\n\n**Getting "The URL isn't valid!"?**\nHypixel has limitations on the characters supported in a Discord username. Try changing your Discord username temporarily to something without special characters, updating it in-game, and trying again.`, + ) + .setImage("https://media.discordapp.net/attachments/922202066653417512/1066476136953036800/tutorial.gif") + .setFooter({ + text: `by @kathund. | /help [command] for more information`, + iconURL: "https://i.imgur.com/uUuZx2E.png", + }); + + await interaction.followUp({ embeds: [verificationTutorialEmbed], ephemeral: true }); + } + } + }, +}; diff --git a/src/discord/events/interactionCreate.js b/src/discord/events/interactionCreate.js index d187436d..db081707 100644 --- a/src/discord/events/interactionCreate.js +++ b/src/discord/events/interactionCreate.js @@ -23,6 +23,12 @@ module.exports = { if (command === undefined) { return; } + + Logger.discordMessage(`${interaction.user.username} - [${interaction.commandName}]`); + + if (command.verificationCommand === true && config.verification.enabled === false) { + throw new HypixelDiscordChatBridgeError("Verification is disabled."); + } if (command.channelsCommand === true && config.statsChannels.enabled === false) { throw new HypixelDiscordChatBridgeError("Verification is disabled."); @@ -36,7 +42,6 @@ module.exports = { throw new HypixelDiscordChatBridgeError("Bot doesn't seem to be connected to Hypixel. Please try again."); } - Logger.discordMessage(`${interaction.user.username} - [${interaction.commandName}]`); await command.execute(interaction); } } catch (error) { diff --git a/src/discord/handlers/StateHandler.js b/src/discord/handlers/StateHandler.js index e65a3436..0a0d44aa 100644 --- a/src/discord/handlers/StateHandler.js +++ b/src/discord/handlers/StateHandler.js @@ -22,6 +22,17 @@ class StateHandler { if (config.statsChannels.enabled) require("../other/statsChannels.js"); + global.guild = await client.guilds.fetch(config.discord.bot.serverID); + if (guild === undefined) { + return Logger.errorMessage(`Guild not found!`); + } + + Logger.discordMessage("Guild ready, successfully fetched " + guild.name); + + if (config.verification.autoUpdater) { + require("../other/updateUsers.js"); + } + channel.send({ embeds: [ { diff --git a/src/discord/other/updateUsers.js b/src/discord/other/updateUsers.js new file mode 100644 index 00000000..fb046fd3 --- /dev/null +++ b/src/discord/other/updateUsers.js @@ -0,0 +1,13 @@ +const updateRolesCommand = require("../commands/forceUpdateEveryone.js"); +const config = require("../../../config.json"); +const Logger = require("../../Logger.js"); +const cron = require("node-cron"); + +if (config.verification.autoUpdater) { + Logger.discordMessage(`RoleSync ready, executing every ${config.verification.autoUpdaterInterval} hours.`); + cron.schedule(`0 */${config.verification.autoUpdaterInterval} * * *`, async () => { + Logger.discordMessage("Executing RoleSync..."); + await updateRolesCommand.execute(null, true); + Logger.discordMessage("RoleSync successfully executed."); + }); +} diff --git a/src/minecraft/handlers/ChatHandler.js b/src/minecraft/handlers/ChatHandler.js index 5c4d9875..ab4e44d4 100644 --- a/src/minecraft/handlers/ChatHandler.js +++ b/src/minecraft/handlers/ChatHandler.js @@ -1,14 +1,17 @@ const { replaceAllRanks, replaceVariables } = require("../../contracts/helperFunctions.js"); const { getLatestProfile } = require("../../../API/functions/getLatestProfile.js"); +const updateRolesCommand = require("../../discord/commands/updateCommand.js"); const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const hypixel = require("../../contracts/API/HypixelRebornAPI.js"); -const { getUUID } = require("../../contracts/API/mowojangAPI.js"); +const { getUUID, getUsername } = require("../../contracts/API/mowojangAPI.js"); const eventHandler = require("../../contracts/EventHandler.js"); const getWeight = require("../../../API/stats/weight.js"); const messages = require("../../../messages.json"); const { EmbedBuilder } = require("discord.js"); const config = require("../../../config.json"); const Logger = require("../../Logger.js"); +const { readFileSync } = require("fs"); +const { isUuid } = require("../../../API/utils/uuid.js"); class StateHandler extends eventHandler { constructor(minecraft, command, discord) { @@ -41,7 +44,7 @@ class StateHandler extends eventHandler { } if (this.isLobbyJoinMessage(message) && config.discord.other.autoLimbo === true) { - return bot.chat("\u00a7"); + return bot.chat("/limbo"); } if (this.isPartyMessage(message) && config.minecraft.fragBot.enabled === true) { @@ -264,6 +267,7 @@ class StateHandler extends eventHandler { prefix: config.minecraft.bot.prefix, })} | by @duckysolucky`, ); + await this.updateUser(username); return [ this.minecraft.broadcastHeadedEmbed({ message: replaceVariables(messages.joinMessage, { username }), @@ -287,7 +291,7 @@ class StateHandler extends eventHandler { .replace(/\[(.*?)\]/g, "") .trim() .split(/ +/g)[0]; - + await this.updateUser(username); return [ this.minecraft.broadcastHeadedEmbed({ message: replaceVariables(messages.leaveMessage, { username }), @@ -311,7 +315,7 @@ class StateHandler extends eventHandler { .replace(/\[(.*?)\]/g, "") .trim() .split(/ +/g)[0]; - + await this.updateUser(username); return [ this.minecraft.broadcastHeadedEmbed({ message: replaceVariables(messages.kickMessage, { username }), @@ -341,6 +345,7 @@ class StateHandler extends eventHandler { .split(" to ") .pop() .trim(); + await this.updateUser(username); return [ this.minecraft.broadcastCleanEmbed({ message: replaceVariables(messages.promotionMessage, { @@ -372,6 +377,7 @@ class StateHandler extends eventHandler { .split(" to ") .pop() .trim(); + await this.updateUser(username); return [ this.minecraft.broadcastCleanEmbed({ message: replaceVariables(messages.demotionMessage, { @@ -1055,6 +1061,37 @@ class StateHandler extends eventHandler { return "#FFFFFF"; } } + + async updateUser(player) { + try { + if (isUuid(player) === false) { + player = await getUsername(player); + } + + if (config.verification.enabled === false) { + return; + } + + const linkedData = readFileSync("data/linked.json"); + if (linkedData === undefined) { + return; + } + const linked = JSON.parse(linkedData); + if (linked === undefined) { + return; + } + + const linkedUser = linked.find((user) => user.uuid === player); + if (linkedUser === undefined) { + return; + } + + const user = await guild.members.fetch(linkedUser.id); + await updateRolesCommand.execute(null, user); + } catch { + // + } + } } module.exports = StateHandler; From fb7bac9a84b85110de2b3d0efd4ba9713bf49781 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 6 Sep 2024 09:29:53 +0800 Subject: [PATCH 5/5] docs info --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 45bebbf6..1a00b416 100644 --- a/README.md +++ b/README.md @@ -512,6 +512,45 @@ The `ranks` option is an array that takes in an object like the following exampl } ``` +### Stats Channels + +The Stats Channels are channels that show stats for your guild. These stats can be your guild level, amount of members in the guild and other. + +The `enabled` option determines whether the Stats Channels system is enabled. By default, this is set to false. + +The `autoUpdaterInterval` allows you to change how often the autoUpdater for the channels runs. By default this option is set to 5 making the autoUpdater run every 5 minutes. + +The `channels` option is an array that takes in an object like the following example, this allows the updater to know what channels there are and what to name them. +```json +{ + "id": "CHANNEL_ID", + "name": "Guild Level: {guildLevel}" +} +``` + +
+Channel Name Variables + +`{guildName}` The name of the guild. + +`{guildLeve}` The current level of the guild. + +`{guildXP}` The **Raw** amount of xp your guild has. + +`{guildWeeklyXP}` The amount of xp your guild has gotten in the past week. + +`{guildMembers}` The amount of members currently in the guild. + +`{discordMembers}` The amount of users in your discord. + +`{discordChannels}` The amount of channels in your discord. + +`{discordRoles}` The amount of roles in your discord. + +
+
+ + ### Chat Triggers Module