diff --git a/data/strings.json b/data/strings.json index c6679fe..25f2974 100755 --- a/data/strings.json +++ b/data/strings.json @@ -51,7 +51,10 @@ }, "error": { "noGuild": "Setup has not been completed yet. Try running or ", - "invalidSubcommand": "Invalid subcommand detected." + "invalidSubcommand": "Invalid subcommand detected.", + "noTreeMessage": " - Make sure you've sent or refreshed a Tree recently.", + "noLeaderboardMessage": " - Make sure you've sent or refreshed the Tallest Trees leaderboard recently.", + "noCompareMessage": " - This is awkward, I've lost my own comparison message!" }, "status": { "treeAndLeaderboard": "Tree and leaderboard messages were both found, setup is complete. Run to verify. Run to get started!", diff --git a/main.js b/main.js index 1ba7f95..1e091c9 100755 --- a/main.js +++ b/main.js @@ -86,9 +86,12 @@ client.on('interactionCreate', async interaction => { } }); -client.on('messageUpdate', async message => { - await fn.sleep(50); - await fn.messages.updateHandler(message); +client.on('messageUpdate', async (oldMessage, message) => { + await fn.messages.updateHandler(message).catch(e => console.error(e)); +}); + +client.on('messageCreate', async message => { + await fn.messages.updateHandler(message).catch(e => console.error(e)); }); async function checkRateLimits(hi) { diff --git a/modules/CustomClasses.js b/modules/CustomClasses.js index 6762c84..827aecc 100755 --- a/modules/CustomClasses.js +++ b/modules/CustomClasses.js @@ -24,6 +24,8 @@ module.exports = { this.reminderChannelId = ""; this.watchChannelId = ""; this.notificationsEnabled = false; + this.compareChannelId = ""; + this.compareMessageId = ""; } setId(id) { @@ -43,9 +45,21 @@ module.exports = { this.treeChannelId = channelId; return this; } + setTreeInfo(name, height, channelId, messageId) { + this.treeName = name ? name : this.treeName; + this.treeHeight = height; + this.treeChannelId = channelId ? channelId : this.treeChannelId; + this.treeMessageId = messageId ? messageId : this.treeMessageId; + return this; + } setLeaderboardMessage(messageId, channelId) { this.leaderboardMessageId = messageId ? messageId : this.leaderboardMessageId; - this.leaderboardChannelId = channelId; + this.leaderboardChannelId = channelId ? channelId : this.leaderboardChannelId; + return this; + } + setCompareMessage(channelId, messageId) { + this.compareChannelId = channelId; + this.compareMessageId = messageId; return this; } setReminders(waterMessage, fruitMessage, reminderChannelId, watchChannelId, enabled) { @@ -156,11 +170,22 @@ module.exports = { case "setTreeInfo": queryParts = [ `INSERT INTO guild_info (`, - `guild_id, tree_name, tree_height`, + `guild_id, tree_name, tree_height, tree_channel_id, tree_message_id`, `) VALUES (`, - `${db.escape(this.guildId)}, ${db.escape(this.treeName)}, ${db.escape(this.treeHeight)}`, + `${db.escape(this.guildId)}, ${db.escape(this.treeName)}, ${db.escape(this.treeHeight)}, ${db.escape(this.treeChannelId)}, ${db.escape(this.treeMessageId)}`, `) ON DUPLICATE KEY UPDATE tree_name = ${db.escape(this.treeName)}, `, - `tree_height = ${db.escape(this.treeHeight)}` + `tree_height = ${db.escape(this.treeHeight)}, `, + `tree_channel_id = ${db.escape(this.treeChannelId)}, `, + `tree_message_id = ${db.escape(this.treeMessageId)}` + ]; + return queryParts.join(''); + case "setCompareMessage": + queryParts = [ + `INSERT INTO guild_info (`, + `guild_id, compare_channel_id, compare_message_id`, + `) VALUES (`, + `${db.escape(this.guildId)}, ${db.escape(this.compareChannelId)}, ${db.escape(this.compareMessageId)}`, + `) ON DUPLICATE KEY UPDATE compare_channel_id = ${db.escape(this.compareChannelId)}, compare_message_id = ${db.escape(this.compareMessageId)}`, ]; return queryParts.join(''); default: diff --git a/modules/dbfn.js b/modules/dbfn.js index 2eb1854..b8b1f9e 100755 --- a/modules/dbfn.js +++ b/modules/dbfn.js @@ -69,7 +69,8 @@ module.exports = { .setTreeMessage(row.tree_message_id, row.tree_channel_id) .setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id) .setReminders(row.water_message, row.fruit_message, row.reminder_channel_id, row.watch_channel_id, row.notifications_enabled) - .setRoles(row.water_role_id, row.fruit_role_id); + .setRoles(row.water_role_id, row.fruit_role_id) + .setCompareMessage(row.compare_channel_id, row.compare_message_id); db.end(); resolve(guildInfo); }); @@ -113,6 +114,7 @@ module.exports = { .setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id) .setReminders(row.water_message, row.fruit_message, row.reminder_channel_id, row.watch_channel_id, row.notifications_enabled) .setRoles(row.water_role_id, row.fruit_role_id) + .setCompareMessage(row.compare_channel_id, row.compare_message_id) ); } @@ -141,6 +143,7 @@ module.exports = { return; } db.end(); + console.log("Updated the database"); resolve(); }); }); @@ -170,6 +173,7 @@ module.exports = { return; } db.end(); + console.log("Updated the database"); resolve({ "status": "Successfully set the guild information", "data": null }); }); }); @@ -298,46 +302,5 @@ module.exports = { resolve({ "status": "Successfully uploaded the leaderboard", "data": res }); }); }); - }, - get24hTree(guildId, treeName) { - const db = mysql.createConnection({ - host: process.env.DBHOST, - user: process.env.DBUSER, - password: process.env.DBPASS, - database: process.env.DBNAME, - port: process.env.DBPORT - }); - db.connect((err) => { - if (err) throw `Error connecting to the database: ${err.message}`; - }); - // Returns a Promise, resolve({ "status": "", "data": leaderboard }) - const select24hTreeQuery = `SELECT id, tree_name, tree_rank, tree_height, has_pin FROM leaderboard WHERE guild_id = ${db.escape(guildId)} AND tree_name = ${db.escape(treeName)} AND timestamp > date_sub(now(), interval 1 day) ORDER BY id ASC LIMIT 1`; - // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } - return new Promise((resolve, reject) => { - db.query(select24hTreeQuery, (err, res) => { - if (err) { - console.error(err); - db.end(); - reject("Error fetching the historic 24hr tree height: " + err.message); - return; - } - let hist24hTree = {}; - if (res.length > 0) { - hist24hTree = { - "treeName": res[0].tree_name, - "treeRank": res[0].tree_rank, - "treeHeight": res[0].tree_height, - "hasPin": res[0].has_pin - } - } else { - hist24hTree = { - - } - } - - db.end(); - resolve({ "status": "Successfully fetched historic 24hr tree.", "data": hist24hTree }); - }); - }); } }; \ No newline at end of file diff --git a/modules/functions.js b/modules/functions.js index b793ef3..8016b44 100755 --- a/modules/functions.js +++ b/modules/functions.js @@ -2,7 +2,7 @@ // dotenv for handling environment variables const dotenv = require('dotenv'); dotenv.config(); -const isDev = process.env.isDev; +const isDev = process.env.DEBUG === "true"; const package = require('../package.json'); // filesystem @@ -275,9 +275,9 @@ const functions = { }); }, - async compare(interaction, guildInfo) { + async compare(guildInfo) { try { - const getLeaderboardResponse = await dbfn.getLeaderboard(interaction.guildId); + const getLeaderboardResponse = await dbfn.getLeaderboard(guildInfo.guildId); const leaderboard = getLeaderboardResponse.data; // [ { treeName: "Name", treeHeight: 1234.5, treeRank: 67 }, {...}, {...} ] // Prepare the beginning of the comparison message @@ -494,7 +494,7 @@ const functions = { isTree(message) { if (message.embeds.length > 0) { // Grab the description and title - const {description, title} = message.embeds[0].data; + const { description, title } = message.embeds[0].data; // Make sure it's a tree message if (description.includes("Your tree is")) { // Grab the name @@ -509,21 +509,73 @@ const functions = { treeName: treeName, treeHeight: treeHeightFloat }; + } else { + return false; } } else { return false; } }, + // Checks if a message is a leaderboard message, and returns the entries if it is isLeaderboard(message) { if (message.embeds.length > 0) { - return message.embeds[0].data.title == "Tallest Trees"; + // Grab the description and title from the embed + const { title, description } = message.embeds[0].data; + // Check that it's a Top Trees message + if (title == "Tallest Trees") { + // Break the description into an array of each line + const lines = description.split("\n"); + const leaderboard = { + guildId: message.guildId, + entries: [] + }; + + lines.forEach(line => { + // Skeleton entry: + const entry = { + treeRank: 0, + treeName: "", + treeHeight: 0, + hasPin: 0 + } + // Break the line into parts separated by a hyphen - + const parts = line.split(" - "); + // Grab the rank + + // Preset the indices to split the lines to get the data + const rankIndices = [parts[0].indexOf("#") + 1, parts[0].lastIndexOf("``")]; + const nameIndices = [parts[1].indexOf("``") + 2, parts[1].lastIndexOf("``")]; + const heightIndices = [0, parts[2].lastIndexOf("ft")]; + + // Set the data from the parts using the indices + entry.treeRank = parts[0].slice(...rankIndices); + entry.treeName = parts[1].slice(...nameIndices); + entry.treeHeight = parts[2].slice(...heightIndices); + + // Go back and check for the first 3 as they use emojis instead of numbers, this will overwrite above + if (line.includes("🥇")) entry.treeRank = 1; + if (line.includes("🥈")) entry.treeRank = 2; + if (line.includes("🥉")) entry.treeRank = 3; + + // Check for the pin showing ownership + if (line.includes("📍")) entry.hasPin = 1; + + // Add the entry to the array + leaderboard.entries.push(entry); + }); + return leaderboard; + } else { + return false; + } } else { return false; } }, async updateHandler(message) { if (message.partial) { - message = await message.fetch(); + message = await message.fetch().catch(e => { + throw e; + }); } // Make sure the message is from Grow A Tree if (message.author.id != strings.ids.growATree) return; @@ -532,22 +584,79 @@ const functions = { const isTree = this.isTree(message); // Check if the message is a leaderboard if (isLeaderboard) { - // Need to actually handle this later - // console.log("I've seen a leaderboard update."); - } else if (isTree) { // Check if the message is a tree - // console.log(`I've seen a tree update: ${isTree.treeName}: ${isTree.treeHeight}ft`); + if (isDev) console.log(`LU: ${message.guild.name}`); let guildInfo; + let doDbUpdate = false; if (message.client.guildInfos.has(message.guildId)) { guildInfo = message.client.guildInfos.get(message.guildId); - guildInfo.setName(isTree.treeName) - .setHeight(isTree.treeHeight); + if ((guildInfo.leaderboardChannelId != message.channel.id) || (guildInfo.leaderboardMessageId != message.id)) { + guildInfo.setLeaderboardMessage(message.id, message.channel.id); + doDbUpdate = true; + } } else { guildInfo = new GuildInfo().setId(message.guildId) - .setName(isTree.treeName) - .setHeight(isTree.treeHeight); + .setLeaderboardMessage(message.id, message.channel.id); + doDbUpdate = true; + } + if (doDbUpdate) { + const query = guildInfo.queryBuilder("setLeaderboardMessage"); + await dbfn.setGuildInfo(query); + } + await dbfn.uploadLeaderboard(isLeaderboard); + // Update the comparison message + // Check for valid message IDs + if (guildInfo.treeMessageId === "") throw strings.error.noTreeMessage; + if (guildInfo.leaderboardMessageId === "") throw strings.error.noLeaderboardMessage; + if (guildInfo.compareMessageId === "") throw strings.error.noCompareMessage; + + // Fetch the messages + const { guild } = message; + // Tree + const treeChannel = await guild.channels.fetch(guildInfo.treeChannelId); + const treeMessage = await treeChannel.messages.fetch(guildInfo.treeMessageId); + // Leaderboard + const leaderboardChannel = await guild.channels.fetch(guildInfo.leaderboardChannelId); + const leaderboardMessage = await leaderboardChannel.messages.fetch(guildInfo.leaderboardMessageId); + // Comparison + const compareChannel = await guild.channels.fetch(guildInfo.compareChannelId); + const compareMessage = await compareChannel.messages.fetch(guildInfo.compareMessageId); + + // Update the tree information + // Make sure we have a tree message and parse it + const isTree = this.isTree(treeMessage); + if (!isTree) throw "Tree message isn't actually a tree message!"; + guildInfo.setTreeInfo(isTree.treeName, isTree.treeHeight); + + // Grab the leaderboard + // Make sure it's a leaderboard and parse it + // const isLeaderboard = this.messages.isLeaderboard(leaderboardMessage); + // if (!isLeaderboard) throw "Leaderboard message isn't actually a leaderboard message!"; + // Upload the leaderboard + // await dbfn.uploadLeaderboard(isLeaderboard); + // Build the string that shows the comparison // TODO Move the string building section to fn.builders? + const comparedRankings = await functions.rankings.compare(guildInfo); + const embed = functions.builders.comparisonEmbed(comparedRankings, guildInfo); + await compareMessage.edit(embed).catch(e => console.error(e)); + } else if (isTree) { + // Check if the message is a tree + if (isDev) console.log(`TU: ${isTree.treeName}: ${isTree.treeHeight}ft`); + let guildInfo; + let doDbUpdate = false; + if (message.client.guildInfos.has(message.guildId)) { + guildInfo = message.client.guildInfos.get(message.guildId); + if ((guildInfo.treeName != isTree.treeName) || (guildInfo.treeHeight != isTree.treeHeight)) { + guildInfo.setTreeInfo(isTree.treeName, isTree.treeHeight, message.channel.id, message.id); + doDbUpdate = true; + } + } else { + guildInfo = new GuildInfo().setId(message.guildId) + .setTreeInfo(isTree.treeName, isTree.treeHeight, message.channel.id, message.id); + doDbUpdate = true; + } + if (doDbUpdate) { + const query = guildInfo.queryBuilder("setTreeInfo"); + await dbfn.setGuildInfo(query); } - const query = guildInfo.queryBuilder("setTreeInfo"); - await dbfn.setGuildInfo(query); } } }, @@ -652,23 +761,45 @@ const functions = { } }, async refresh(interaction) { - // const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); - // let guildInfo = getGuildInfoResponse.data; - let guildInfo = interaction.client.guildInfos.get(interaction.guild.id); - const findMessagesResponse = await this.messages.find(interaction, guildInfo); - if (findMessagesResponse.code == 1) { - guildInfo = findMessagesResponse.data; - // Parse the tree - await this.tree.parse(interaction, guildInfo); - // Parse the leaderboard - await this.rankings.parse(interaction, guildInfo); - // Build the string that shows the comparison // TODO Move the string building section to fn.builders? - const comparedRankings = await this.rankings.compare(interaction, guildInfo); + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + // Check for valid message IDs + if (guildInfo.treeMessageId === "") throw strings.error.noTreeMessage; + if (guildInfo.leaderboardMessageId === "") throw strings.error.noLeaderboardMessage; + + // Fetch the messages + const { guild } = interaction; + // Tree + const treeChannel = await guild.channels.fetch(guildInfo.treeChannelId); + const treeMessage = await treeChannel.messages.fetch(guildInfo.treeMessageId); + // Leaderboard + const leaderboardChannel = await guild.channels.fetch(guildInfo.leaderboardChannelId); + const leaderboardMessage = await leaderboardChannel.messages.fetch(guildInfo.leaderboardMessageId); + + // Update the comparison message information + guildInfo.setCompareMessage(interaction.channel.id, interaction.message.id); + const query = guildInfo.queryBuilder("setCompareMessage"); + await dbfn.setGuildInfo(query); + + // Update the tree information + // Make sure we have a tree message and parse it + const isTree = this.messages.isTree(treeMessage); + if (!isTree) throw "Tree message isn't actually a tree message!"; + guildInfo.setTreeInfo(isTree.treeName, isTree.treeHeight); + + // Grab the leaderboard + // Make sure it's a leaderboard and parse it + const isLeaderboard = this.messages.isLeaderboard(leaderboardMessage); + if (!isLeaderboard) throw "Leaderboard message isn't actually a leaderboard message!"; + // Upload the leaderboard + await dbfn.uploadLeaderboard(isLeaderboard); + // Build the string that shows the comparison // TODO Move the string building section to fn.builders? + const comparedRankings = await this.rankings.compare(guildInfo); const embed = this.builders.comparisonEmbed(comparedRankings, guildInfo); await interaction.update(embed).catch(e => console.error(e)); } else { - await interaction.update(this.builders.errorEmbed(findMessagesResponse.status)); + throw "Guild doesn't exist in database!"; } }, reset(interaction) { @@ -764,7 +895,7 @@ const functions = { async sendWaterReminder(guildInfo, message, channelId, guild) { const reminderChannel = await guild.channels.fetch(channelId); const reminderEmbed = functions.builders.waterReminderEmbed(message, guildInfo); - console.log(`Water Relay: ${guild.name}: ${guildInfo.treeName}`); + if (isDev) console.log(`WR: ${guild.name}: ${guildInfo.treeName}`); await reminderChannel.send(reminderEmbed).then(async m => { if (!m.deletable) return; await this.sleep(500).then(async () => { @@ -777,7 +908,7 @@ const functions = { async sendFruitReminder(guildInfo, message, channelId, guild) { const reminderChannel = await guild.channels.fetch(channelId); const reminderEmbed = functions.builders.fruitReminderEmbed(message, guildInfo); - console.log(`Fruit Relay: ${guild.name}: ${guildInfo.treeName}`); + if (isDev) console.log(`FR: ${guild.name}: ${guildInfo.treeName}`); await reminderChannel.send(reminderEmbed).then(async m => { if (!m.deletable) return; await this.sleep(500).then(async () => { diff --git a/package.json b/package.json index 0322ea9..7081a8a 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silvanus", - "version": "1.2.3", + "version": "1.2.4", "description": "Grow A Tree Companion Bot", "main": "main.js", "scripts": { diff --git a/slash-commands/compare.js b/slash-commands/compare.js index 4a34ca2..7c78f8f 100755 --- a/slash-commands/compare.js +++ b/slash-commands/compare.js @@ -1,6 +1,7 @@ const { SlashCommandBuilder, Guild } = require('discord.js'); const dbfn = require('../modules/dbfn.js'); const fn = require('../modules/functions.js'); +const strings = require('../data/strings.json'); const { GuildInfo } = require('../modules/CustomClasses.js'); module.exports = { @@ -14,36 +15,33 @@ module.exports = { if (interaction.client.guildInfos.has(interaction.guildId)) { let guildInfo = interaction.client.guildInfos.get(interaction.guildId); - const findMessagesResponse = await fn.messages.find(interaction, guildInfo); - if (findMessagesResponse.code == 1) { - let guildInfo = interaction.client.guildInfos.get(interaction.guildId); - // Parse the leaderboard message - await fn.rankings.parse(interaction, guildInfo); - // Build the string that shows the comparison // TODO Move the string building section to fn.builders? - const comparedRankings = await fn.rankings.compare(interaction, guildInfo); + // Handle a missing tree or leaderboard + let errors = []; + if (guildInfo.treeMessageId === "") { + errors.push(strings.error.noTreeMessage); + } else if (guildInfo.leaderboardMessageId === "") { + errors.push(strings.error.noLeaderboardMessage); + } + if (errors.length > 0) { + const embed = fn.builders.errorEmbed(`Unable to complete comparison, unable to find message(s):\n${errors.join("\n")}`); + await interaction.editReply(embed); + return; + } + // Build the string that shows the comparison // TODO Move the string building section to fn.builders? + const comparedRankings = await fn.rankings.compare(guildInfo); - const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo); - await interaction.editReply(embed).catch(e => console.error(e)); - } else { - await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); - } + const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo); + await interaction.editReply(embed).then(async m => { + guildInfo.setCompareMessage(m.channel.id, m.id); + const query = guildInfo.queryBuilder("setCompareMessage"); + await dbfn.setGuildInfo(query); + }).catch(e => console.error(e)); } else { - // Create a basic guildInfo with blank data - let guildInfo = new GuildInfo() - .setId(interaction.guildId) - .setTreeMessage("", interaction.channelId) - .setLeaderboardMessage("", interaction.channelId) - // Using the above guildInfo, try to find the Grow A Tree messages - const findMessagesResponse = await fn.messages.find(interaction, guildInfo); - guildInfo = findMessagesResponse.data; - if (findMessagesResponse.code == 1) { - // Build the string that shows the comparison // TODO Move the string building section to fn.builders? - const comparedRankings = await fn.rankings.compare(interaction, guildInfo); - const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo); - await interaction.editReply(embed).catch(e => console.error(e)); - } else { - await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); - } + let errors = []; + errors.push(strings.error.noTreeMessage); + errors.push(strings.error.noLeaderboardMessage); + const embed = fn.builders.errorEmbed(`Unable to complete comparison, unable to find message(s):\n${errors.join("\n")}`); + await interaction.editReply(embed); } } catch (err) { interaction.editReply(fn.builders.errorEmbed(err)).catch(err => {