diff --git a/README.md b/README.md index de31df7..4d5bbba 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,15 @@ To begin analyzing your Tree, first you must set up the reference messages. Silvanus requires permissions to `Send Messages` and `Send Messages in Threads` if applicable. ## Commands -* `/setup` - Automatically detects and saves the location of your server's Tree and Tallest Trees messages. Run this command in the channel(s) that contain these messages. +* `/setup` - You only need to run this command if your server has its `/tree` and `/top trees` messages in separate channels. * `/setupinfo` - Displays links to the current Tree and Tallest Trees messages configured in your server. -* `/compare` - Sends a refreshable embed that calculcates the height difference between your tree and the trees currently displayed on your Tallest Trees message. There is also an Active Growth Indicator (`[+]`) and a water wait time calculation (`[20 mins]`) -* `/watertime height` - Calculates the wait time between waters for a tree of a given height. +* `/compare` - Sends a refreshable embed that calculcates the height difference between your tree and the trees currently displayed on your Tallest Trees message. There is also an Active Growth Indicator (`[💧]`). +* `/setping` - Guild members with the `Manage Roles` permission can run this command to set up automatic reminders when your tree is ready to be watered. + * Type a reminder message (including any `@pings` desired) and select a channel to send the reminders in. + * Once this command has been run a new `Reset Ping` button will appear next time you refresh the `/compare` message. + * Click the `Reset Ping` button to be sent a reminder the next time the tree is ready to be watered. + * Use `/optout` to disable reminder messages for your server. +* `/watertime` - Calculates the wait time between waters for a tree of a given height. * `/timetoheight` - Calculates how long it would take to go from `beginheight` to `endheight`. -* `/setping` - Guild members with the `Manage Roles` permission can run this command to set a Ready to Water reminder meaage and set the channel to send it in. Once this command has been run a new `Reset Ping` button will appear next time you refresh the `/compare` message. -* `/optout` - Disable automatic water reminder messages. * `/reset` - Removes your server's configuration from the database. * `/help` - Displays the bot's help page and links to each command. \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..ddbfebb --- /dev/null +++ b/TODO.md @@ -0,0 +1,27 @@ +## In Progress +☑ Switch `/setup` to ask for the tree and leaderboard channels +* Switch `/compare` to check for newer trees and leaderboards when run **and** on every refresh + +## Future Ideas +* Go through and comment the code + +## Variable Structures + +guildInfo = { + guildId: "", + treeName: "name", + treeHeight: 0, + treeMessageId: "", + treeChannelId: "", + leaderboardMessageId: "", + leaderboardChannelId: "", + reminderMessage: "", + reminderChannelId: "", + remindedStatus: 0, + reminderOptIn: 0, +} + +## Expected Behaviors + +* Run `/compare` before `/setup`: `/compare` will search the current channel for tree and leaderboard messages, then create a comparison embed. If it can't find `/tree` or `/top trees` messages, it'll return an error saying as much. +* Run `/compare` after `/setup`: ``/compare` will search the current channel for tree and leaderboard messages, then create a comparison embed. If it can't find `/tree` or `/top trees` messages, it'll just use old data silently (odds are `/compare` is being run from another channel, that's fine) diff --git a/data/strings.json b/data/strings.json index 70888ef..d6abb0f 100644 --- a/data/strings.json +++ b/data/strings.json @@ -4,10 +4,10 @@ }, "help": { "title": "Silvanus Help", - "info": "Silvanus is the ultimate Grow A Tree companion bot! Quickly compare your server's tree to others on the leaderboard with automatic calculation of tree height differences, active growth detection, watering time calculations, and more! Get started with and , then check out .\n\nImportant Note: Silvanus is only as up-to-date as your server's Tree and Tallest Trees messages. Make sure to refresh them before refreshing Silvanus' Compare message.\n\nFor the best experience we recommend the use of a single and message, otherwise make sure to run each time you run .", - "setup": "To begin analyzing your Tree, first you must set up the reference messages.\n\n1. Run in the channel(s) that contain your server's tree and leaderboard messages.\n2. Now simply run where you want your analysis to be visible.", + "info": "Silvanus is the ultimate Grow A Tree companion bot! Quickly compare your server's tree to others on the leaderboard with automatic calculation of tree height differences, active growth detection, watering time calculations, and more!\n\nImportant Note: Silvanus is only as up-to-date as your server's newest Tree and Tallest Trees messages. Make sure to refresh them before refreshing Silvanus' Compare message.", + "setup": "If your ``/tree`` and ``/top trees`` messages are in the same channel, simple run in that channel and you're good to go!\n\nOtherwise, run to set the proper channels for the bot to look in for the ``/tree`` and ``/top trees`` messages;", "permissions": "At a minimum, Silvanus requires permissions to `Send Messages` and `Send Messages in Threads` if applicable. If Analyzer is given permission to `Manage Messages`, the bot will delete the `.settree` and `.setranks` messages to reduce spam.", - "allCommands": " - Automatically detects and saves the location of your server's Tree and Tallest Trees messages. Run this command in the channel(s) that contain these messages.\n - Displays links to the current Tree and Tallest Trees messages configured in your server.\n - Sends a refreshable embed that calculcates the height difference between your tree and the trees currently displayed on your Tallest Trees message. There is also an Active Growth Indicator (`[+]`) and a water wait time calculation (`[20 mins]`)\n - Calculates the wait time between waters for a tree of a given height.\n - Calculates how long it would take to go from `beginheight` to `endheight`.\n - Guild members with the `Manage Roles` permission can run this command to set a Ready to Water reminder meaage and set the channel to send it in. Once this command has been run a new `Reset Ping` button will appear next time you refresh the `/compare` message.\n - Disable automatic water reminder messages.\n - Removes your server's configuration from the database.\n - Displays the bot's help page and links to each command." + "allCommands": " - You only need to run this command if your server has its ``/tree`` and ``/top trees`` messages in separate channels.\n - Displays links to the current Tree and Tallest Trees messages configured in your server.\n - Sends a refreshable embed that calculcates the height difference between your tree and the trees currently displayed on your Tallest Trees message. There is also an Active Growth Indicator (``[💧]``).\n - Guild members with the ``Manage Roles`` permission can run this command to set up automatic reminders when your tree is ready to be watered.\n Type a reminder message (including any ``@pings`` desired) and select a channel to send the reminders in.\n Once this command has been run a new ``Reset Ping`` button will appear next time you refresh the message.\n Click the ``Reset Ping`` button to be sent a reminder the next time the tree is ready to be watered.\n Use to disable reminder messages for your server.\n - Calculates the wait time between waters for a tree of a given height.\n - Calculates how long it would take to go from ``beginheight`` to ``endheight``.\n - Removes your server's configuration from the database.\n - Displays the bot's help page and links to each command." }, "commands": { "compare": "", @@ -37,8 +37,12 @@ }, "status": { "treeAndLeaderboard": "Tree and leaderboard messages were both found, setup is complete. Run to verify. Run to get started!", - "treeNoLeaderboard": "A tree message was found, but a leaderboard message was not. Please run this command again in the channel containing the leaderboard if you haven't done so already. Run to see if the message is set.", - "leaderboardNoTree": "A leaderboard message was found, but a tree message was not. Please run this command again in the channel containing the tree if you haven't done so already. Run to see if the message is set." + "missingMessage": "I was unable to find both a ``/tree`` and ``/top trees`` message in this channel. Please run to set up separate ``/tree`` and ``/top trees`` channels.", + "noneFound": "Unable to find a tree or leaderboard in the last 20 messages in this channel, make sure you've run ``/tree`` and/or ``/top trees`` recently.", + "missingLeaderboardMessage": "There was a problem finding the Tallest Trees message. Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run to set the ``/tree`` and ``/top trees`` channels.", + "missingLeaderboardChannel": "There was a problem finding the Tallest Trees channel, was it deleted? Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run to set the ``/tree`` and ``/top trees`` channels.", + "missingTreeMessage": "There was a problem finding the Tree message. Please make sure the ``/tree`` and ``/top trees`` messages are this channel, or run to set the ``/tree`` and ``/top trees`` channels.", + "missingTreeChannel": "There was a problem finding the Tree channel, was it deleted? Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run to set the ``/tree`` and ``/top trees`` channels." }, "temp": {} } \ No newline at end of file diff --git a/main.js b/main.js index a6d6fd8..933e4c7 100644 --- a/main.js +++ b/main.js @@ -35,9 +35,6 @@ client.once('ready', () => { client.channels.fetch(statusChannelId).then(channel => { channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete <@481933290912350209>`); }); - } else { - // Dev shit - fn.checkReady(client); } }); @@ -58,7 +55,9 @@ client.on('interactionCreate', async interaction => { } if (interaction.isButton() && interaction.component.customId == 'refresh') { - fn.refresh(interaction); + fn.refresh(interaction).catch(err => { + interaction.update(fn.builders.errorEmbed(err)); + }); } else if (interaction.isButton() && interaction.component.customId == 'resetping') { fn.resetPing(interaction); interaction.reply({ content: "Water Readiness Detection System: [ARMED]", ephemeral: true }); diff --git a/modules/_prepareStrings.js b/modules/_prepareStrings.js new file mode 100644 index 0000000..f9a7de2 --- /dev/null +++ b/modules/_prepareStrings.js @@ -0,0 +1,36 @@ +/* + + + + + + + + + + + +*/ + +const fs = require('fs'); +const replaceAll = require('string.prototype.replaceall'); + +function convertHelpString() { + const string = fs.readFileSync('./string.txt').toString(); + let newString = replaceAll(string, '\* ', ''); + newString = replaceAll(newString, '\n', '\\n'); + newString = replaceAll(newString, '\t', ' - '); + newString = replaceAll(newString, '`/setup`', ''); + newString = replaceAll(newString, '`/setupinfo`', ''); + newString = replaceAll(newString, '`/compare`', ''); + newString = replaceAll(newString, '`/setping`', ''); + newString = replaceAll(newString, '`/optout`', ''); + newString = replaceAll(newString, '`/watertime`', ''); + newString = replaceAll(newString, '`/timetoheight`', ''); + newString = replaceAll(newString, '`/reset`', ''); + newString = replaceAll(newString, '`/help`', ''); + newString = replaceAll(newString, '`', '``'); + return newString; +} + +module.exports = convertHelpString(); \ No newline at end of file diff --git a/modules/dbfn.js b/modules/dbfn.js index ca55da4..b4fabdd 100644 --- a/modules/dbfn.js +++ b/modules/dbfn.js @@ -108,7 +108,7 @@ module.exports = { return; } row = res[0]; - const guildInfo = { "guildId": row.guild_id, + const guildInfo = { "guildId": guildId, "treeName": row.tree_name, "treeHeight": row.tree_height, "treeMessageId": row.tree_message_id, diff --git a/modules/functions.js b/modules/functions.js index 848bf1f..b39a9d5 100644 --- a/modules/functions.js +++ b/modules/functions.js @@ -70,7 +70,7 @@ const functions = { .setColor(strings.embeds.color) .setTitle('Tree Growth Comparison') .setDescription(content) - .setFooter({text: `v${package.version} - ${strings.embeds.footer}`}); + .setFooter({ text: `v${package.version} - ${strings.embeds.footer}` }); const messageContents = { embeds: [embed], components: [refreshActionRow] }; return messageContents; }, @@ -79,9 +79,9 @@ const functions = { const embed = new EmbedBuilder() .setColor(strings.embeds.color) .setTitle('Water Reminder') - .setDescription(`${content}\n[Click Here To Go To Your Tree](https://discord.com/channels/${guildInfo.guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId})`) - .setFooter({text: `This message will self-destruct in 60 seconds...`}); - const messageContents = { embeds: [embed] }; + .setDescription(`[Click here to go to your Tree](https://discord.com/channels/${guildInfo.guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId})`) + .setFooter({ text: `This message will self-destruct in 60 seconds.` }); + const messageContents = { content: content, embeds: [embed] }; return messageContents; }, helpEmbed(content, private) { @@ -114,94 +114,91 @@ const functions = { } }, rankings: { - parse(interaction) { - return new Promise ((resolve, reject) => { - dbfn.getGuildInfo(interaction.guildId).then(res => { - const guildInfo = res.data; - if (guildInfo.guildId == "") { - reject(strings.error.noGuild); - return; - } - if (guildInfo.leaderboardMessageId != undefined) { - interaction.guild.channels.fetch(guildInfo.leaderboardChannelId).then(c => { - c.messages.fetch(guildInfo.leaderboardMessageId).then(leaderboardMessage => { - if ((leaderboardMessage.embeds.length == 0) || (leaderboardMessage.embeds[0].data.title != 'Tallest Trees' )) { - reject("This doesn't appear to be a valid ``/top trees`` message."); - return; - } - let lines = leaderboardMessage.embeds[0].data.description.split('\n'); - let leaderboard = { - "guildId": interaction.guildId, - "entries": [] - }; - for (let i = 0; i < 10; i++) { - // Breakdown each line separating it on each - - let breakdown = lines[i].split(' - '); - - // Check if the first part, the ranking, has these emojis to detect first second and third place - if (breakdown[0].includes('🥇')) { - breakdown[0] = '``#1 ``' - } else if (breakdown[0].includes('🥈')) { - breakdown[0] = '``#2 ``' - } else if (breakdown[0].includes('🥉')) { - breakdown[0] = '``#3 ``' - } - - // Clean off the excess and get just the number from the rank, make sure it's an int not string - let trimmedRank = parseInt(breakdown[0].slice(breakdown[0].indexOf('#') + 1, breakdown[0].lastIndexOf('``'))); - - // Clean off the excess and get just the tree name - let trimmedName = breakdown[1].slice(breakdown[1].indexOf('``') + 2); - trimmedName = trimmedName.slice(0, trimmedName.indexOf('``')); - - // Clean off the excess and get just the tree height, make sure it's a 1 decimal float - let trimmedHeight = parseFloat(breakdown[2].slice(0, breakdown[2].indexOf('ft'))).toFixed(1); - let isMyTree = false; - let isMaybeMyTree = false; - if (breakdown[2].includes('📍')) isMyTree = true; - if (breakdown[1].includes(guildInfo.treeName)) maybeMyTree = true; - - // "entries": [ { "treeHeight": 12, "treeRank": 34, "treeName": "name" }, ] } - - - leaderboard.entries.push({ - treeRank: trimmedRank, - treeName: trimmedName, - treeHeight: trimmedHeight, - hasPin: isMyTree - }); - } - - dbfn.uploadLeaderboard(leaderboard).then(res => { - console.log(res.status); - resolve(res.status); - }).catch(err => { - console.error(err); - reject(err); - return; - }); - }); - }); - } else { - reject("The leaderboardMessageId is undefined somehow"); - return; - } - }).catch(err => { - reject(err); + parse(interaction, guildInfo) { + return new Promise((resolve, reject) => { + if (guildInfo.guildId == "") { + reject(strings.error.noGuild); return; - }); + } + if (guildInfo.leaderboardMessageId != undefined) { + interaction.guild.channels.fetch(guildInfo.leaderboardChannelId).then(c => { + c.messages.fetch(guildInfo.leaderboardMessageId).then(leaderboardMessage => { + if ((leaderboardMessage.embeds.length == 0) || (leaderboardMessage.embeds[0].data.title != 'Tallest Trees')) { + reject("This doesn't appear to be a valid ``/top trees`` message."); + return; + } + let lines = leaderboardMessage.embeds[0].data.description.split('\n'); + let leaderboard = { + "guildId": interaction.guildId, + "entries": [] + }; + for (let i = 0; i < 10; i++) { + // Breakdown each line separating it on each - + let breakdown = lines[i].split(' - '); + + // Check if the first part, the ranking, has these emojis to detect first second and third place + if (breakdown[0].includes('🥇')) { + breakdown[0] = '``#1 ``' + } else if (breakdown[0].includes('🥈')) { + breakdown[0] = '``#2 ``' + } else if (breakdown[0].includes('🥉')) { + breakdown[0] = '``#3 ``' + } + + // Clean off the excess and get just the number from the rank, make sure it's an int not string + let trimmedRank = parseInt(breakdown[0].slice(breakdown[0].indexOf('#') + 1, breakdown[0].lastIndexOf('``'))); + + // Clean off the excess and get just the tree name + let trimmedName = breakdown[1].slice(breakdown[1].indexOf('``') + 2); + trimmedName = trimmedName.slice(0, trimmedName.indexOf('``')); + + // Clean off the excess and get just the tree height, make sure it's a 1 decimal float + let trimmedHeight = parseFloat(breakdown[2].slice(0, breakdown[2].indexOf('ft'))).toFixed(1); + let isMyTree = false; + let isMaybeMyTree = false; + if (breakdown[2].includes('📍')) isMyTree = true; + if (breakdown[1].includes(guildInfo.treeName)) maybeMyTree = true; + + // "entries": [ { "treeHeight": 12, "treeRank": 34, "treeName": "name" }, ] } + + + leaderboard.entries.push({ + treeRank: trimmedRank, + treeName: trimmedName, + treeHeight: trimmedHeight, + hasPin: isMyTree + }); + } + + dbfn.uploadLeaderboard(leaderboard).then(res => { + resolve(res.status); + }).catch(err => { + console.error(err); + reject(err); + return; + }); + }).catch(err => { + reject(strings.status.missingLeaderboardMessage); + console.error(err); + return; + }); + }).catch(err => { + reject(strings.status.missingLeaderboardChannel); + console.error(err); + return; + }); + } else { + reject("The leaderboardMessageId is undefined somehow"); + return; + } }); - + }, - async compare(interaction) { + async compare(interaction, guildInfo) { try { - // fetch the guild's settings from the database - const guildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); - const guildInfo = guildInfoResponse.data; // { "guildId": "123","treeName": "name","treeHeight": 123,"treeMessageId": "123","treeChannelId": "123","leaderboardMessageId": "123","leaderboardChannelId": "123"}; - // Get the most recent leaderboard listing from the database const getLeaderboardResponse = await dbfn.getLeaderboard(interaction.guildId); const leaderboard = getLeaderboardResponse.data; // [ { treeName: "Name", treeHeight: 1234.5, treeRank: 67 }, {...}, {...} ] - + // Prepare the beginning of the comparison message let comparisonReplyString = `Here\'s how your tree compares: \nCurrent Tree Height: ${guildInfo.treeHeight}ft\n\n`; // Iterate over the leaderboard entries, backwards @@ -210,7 +207,7 @@ const functions = { // Setup the status indicator, default to blank, we'll change it later let statusIndicator = ""; if ((leaderboardEntry.treeHeight % 1).toFixed(1) > 0) statusIndicator += "``[💧]``"; - + // Get the data for this tree from 24 hours ago // const get24hTreeResponse = await dbfn.get24hTree(interaction.guildId, leaderboardEntry.treeName); // const dayAgoTree = get24hTreeResponse.data; @@ -220,14 +217,14 @@ const functions = { // Get the 24h watering time for this tree // const totalWaterTime = await functions.timeToHeight(dayAgoTree.treeHeight, leaderboardEntry.treeHeight); // statusIndicator += `${totalWaterTime}]\`\``; - + // Determine if this tree is the guild's tree if (leaderboardEntry.hasPin) { comparisonReplyString += `#${leaderboardEntry.treeRank} - This is your tree`; } else { // If it's another guild's tree // Calculate the current height difference const currentHeightDifference = guildInfo.treeHeight - leaderboardEntry.treeHeight; - + if (currentHeightDifference > 0) { // Guild Tree is taller than the leaderboard tree comparisonReplyString += `#${leaderboardEntry.treeRank} - ${currentHeightDifference.toFixed(1)}ft taller`; } else { @@ -240,70 +237,209 @@ const functions = { } return comparisonReplyString; } catch (err) { - console.error(err); - return 'An error occurred while comparing the trees.'; + throw err; } } }, tree: { - parse(interaction) { + parse(interaction, guildInfo) { let input; return new Promise((resolve, reject) => { - dbfn.getGuildInfo(interaction.guildId).then(res => { - const guildInfo = res.data; - guildInfo.guildId = interaction.guildId; - if (guildInfo == undefined) { - reject(`The guild entry hasn't been created yet. [${interaction.guildId || interaction.commandGuildId}]`); - return; - } - if (guildInfo.treeMessageId != "Run /setup where your tree is.") { - interaction.guild.channels.fetch(guildInfo.treeChannelId).then(c => { - c.messages.fetch(guildInfo.treeMessageId).then(m => { - if ( (m.embeds.length == 0) || !(m.embeds[0].data.description.includes('Your tree is')) ) { - reject("This doesn't appear to be a valid ``/tree`` message."); - return; - } - input = m.embeds[0].data.description; - let treeName = m.embeds[0].data.title; - let lines = input.split('\n'); - guildInfo.treeHeight = parseFloat(lines[0].slice(lines[0].indexOf('is') + 3, lines[0].indexOf('ft'))).toFixed(1); - guildInfo.treeName = treeName; - dbfn.setTreeInfo(guildInfo).then(res => { - resolve("The reference tree message has been saved/updated."); - }); - }); - }) - } else { - console.error('treeMessageId undefined'); - reject("There was an unknown error while setting the tree message."); - return; - } - }).catch(err => { - reject(err); - console.error(err); + if (guildInfo == undefined) { + reject(`The guild entry hasn't been created yet. [${interaction.guildId || interaction.commandGuildId}]`); return; - }); + } + if (guildInfo.treeMessageId != "Run /setup where your tree is.") { + interaction.guild.channels.fetch(guildInfo.treeChannelId).then(c => { + c.messages.fetch(guildInfo.treeMessageId).then(m => { + if ((m.embeds.length == 0) || !(m.embeds[0].data.description.includes('Your tree is'))) { + reject("This doesn't appear to be a valid ``/tree`` message."); + return; + } + input = m.embeds[0].data.description; + let treeName = m.embeds[0].data.title; + let lines = input.split('\n'); + guildInfo.treeHeight = parseFloat(lines[0].slice(lines[0].indexOf('is') + 3, lines[0].indexOf('ft'))).toFixed(1); + guildInfo.treeName = treeName; + dbfn.setTreeInfo(guildInfo).then(res => { + resolve("The reference tree message has been saved/updated."); + }); + }).catch(err => { + reject(strings.status.missingTreeMessage); + console.error(err); + return; + }); + }).catch(err => { + reject(strings.status.missingTreeChannel); + console.error(err); + return; + }); + } else { + console.error('treeMessageId undefined'); + reject("There was an unknown error while setting the tree message."); + return; + } }); } }, - refresh(interaction) { - functions.rankings.parse(interaction).then(r1 => { - functions.tree.parse(interaction).then(r2 => { - functions.rankings.compare(interaction).then(async res => { - const refreshActionRow = await functions.builders.refreshAction(interaction.guildId); - const embed = functions.builders.comparisonEmbed(res, refreshActionRow) - interaction.update(embed); - }).catch(err => { - console.error(err); - }); - }).catch(e => { - console.error(e); - interaction.reply(functions.builders.errorEmbed(e)); - }); - }).catch(e => { - console.error(e); - interaction.reply(functions.builders.errorEmbed(e)); - }); + messages: { + async find(interaction, guildInfo) { + try { + let response = { status: "Incomplete", data: undefined, code: 0 }; + // If the tree channel ID and leaderboard channel ID are both set + if (guildInfo.treeChannelId != "" || guildInfo.leaderboardChannelId != "") { + // If one us unset, we'll set it to the current channel just to check + if (guildInfo.treeChannelId == "") { + guildInfo.treeChannelId = `${guildInfo.leaderboardChannelId}`; + } else if (guildInfo.leaderboardChannelId == "") { + guildInfo.leaderboardChannelId = `${guildInfo.treeChannelId}`; + } + let treeFound, leaderboardFound = false; + // If these values have already been set in the database, we don't want to report that they weren't found + // they'll still get updated later if applicable. + treeFound = (guildInfo.treeMessageId != ""); + leaderboardFound = (guildInfo.leaderboardMessageId != ""); + // If the Tree and Leaderboard messages are in the same channel + if (guildInfo.treeChannelId == guildInfo.leaderboardChannelId) { + // Fetch the tree channel so we can get the most recent messages + const treeChannel = await interaction.guild.channels.fetch(guildInfo.treeChannelId); + // Fetch the last 20 messages in the channel + const treeChannelMessageCollection = await treeChannel.messages.fetch({ limit: 20 }); + // Create a basic array of [Message, Message, ...] from the Collection + const treeChannelMessages = Array.from(treeChannelMessageCollection.values()); + // Iterate over the Messages in reverse order (newest messages first) + for (let i = treeChannelMessages.length - 1; i >= 0; i--) { + let treeChannelMessage = treeChannelMessages[i]; + + if (this.isTree(treeChannelMessage)) { // This is a tree message + // Set the tree message ID + guildInfo.treeMessageId = treeChannelMessage.id; + // Parse out the tree name + input = treeChannelMessage.embeds[0].data.description; + let treeName = treeChannelMessage.embeds[0].data.title; + // And tree height + let lines = input.split('\n'); + guildInfo.treeHeight = parseFloat(lines[0].slice(lines[0].indexOf('is') + 3, lines[0].indexOf('ft'))).toFixed(1); + guildInfo.treeName = treeName; + // Upload the found messages to the database + await dbfn.setTreeInfo(guildInfo); + // Let the end of the function know we found a tree message and successfully uploaded it + treeFound = true; + } else if (this.isLeaderboard(treeChannelMessage)) { // This is a leaderboard message + // Set the leaderboard message ID + guildInfo.leaderboardMessageId = treeChannelMessage.id; + // Upload it to the database + await dbfn.setLeaderboardInfo(guildInfo); + // Let the end of the function know we found a leaderboard message and successfully uploaded it + leaderboardFound = true; + } + } + } else { // If the tree and leaderboard are in different channels + // Fetch the tree channel so we can get the most recent messages + const treeChannel = await interaction.guild.channels.fetch(guildInfo.treeChannelId); + // Fetch the last 20 messages in the tree channel + const treeChannelMessageCollection = await treeChannel.messages.fetch({ limit: 20 }); + // Create an Array like [Message, Message, ...] from the Collection + const treeChannelMessages = Array.from(treeChannelMessageCollection.values()); + // Iterate over the Array of Messages in reverse order (newest messages first) + for (let i = treeChannelMessages.length - 1; i >= 0; i--) { + let treeChannelMessage = treeChannelMessages[i]; + + if (this.isTree(treeChannelMessage)) { // This is a tree message + // Set the tree message ID + guildInfo.treeMessageId = treeChannelMessage.id; + // Parse out the tree name + input = treeChannelMessage.embeds[0].data.description; + let treeName = treeChannelMessage.embeds[0].data.title; + // And tree height + let lines = input.split('\n'); + guildInfo.treeHeight = parseFloat(lines[0].slice(lines[0].indexOf('is') + 3, lines[0].indexOf('ft'))).toFixed(1); + guildInfo.treeName = treeName; + // Upload the found messages to the database + await dbfn.setTreeInfo(guildInfo); + // Let the end of the function know we found a tree message and successfully uploaded it + treeFound = true; + } + } + + // Fetch the tree channel so we can get the most recent messages + const leaderboardChannel = await interaction.guild.channels.fetch(guildInfo.leaderboardChannelId); + // Fetch the last 20 messages in the leaderboard channel + const leaderboardChannelMessageCollection = await leaderboardChannel.messages.fetch({ limit: 20 }); + // Create an Array like [Message, Message, ...] from the Collection + const leaderboardChannelMessages = Array.from(leaderboardChannelMessageCollection.values()); + // Iterate over the Array of Messages in reverse order (newest messages first) + for (let i = leaderboardChannelMessages.length - 1; i >= 0; i--) { + let leaderboardChannelMessage = leaderboardChannelMessages[i]; + + if (this.isLeaderboard(leaderboardChannelMessage)) { // This is a leaderboard message + // Set the leaderboard message ID + guildInfo.leaderboardMessageId = leaderboardChannelMessage.id; + // Upload it to the database + await dbfn.setLeaderboardInfo(guildInfo); + // Let the end of the function know we've found a leaderboard + leaderboardFound = true; + } + } + } + + // await dbfn.setGuildInfo(guildInfo); + // Bundle guildInfo into the response + const getGuildInfoResponse = await dbfn.getGuildInfo(guildInfo.guildId); + response.data = getGuildInfoResponse.data; + + // Set the response status, this is only used as a response to /setup + if (treeFound && leaderboardFound) { // we found both the tree and leaderboard + response.status = strings.status.treeAndLeaderboard; + response.code = 1; + } else if (treeFound || leaderboardFound) { // Only found the tree + response.status = strings.status.missingMessage; + response.code = 2; + } else { // Didn't find any + response.status = strings.status.noneFound; + response.code = 3; + } + return response; + } else { // This should only ever occur if some weird database stuff happens + response.status = "It looks like this channel doesn't contain both your ``/tree`` and ``/top trees`` messages, please run ``/setup``"; + return response; + } + + } catch (err) { + throw "Problem checking messages: " + err; + } + }, + isTree(message) { + if (message.embeds.length > 0) { + return message.embeds[0].data.description.includes("Your tree is"); + } + }, + isLeaderboard(message) { + if (message.embeds.length > 0) { + return message.embeds[0].data.title == "Tallest Trees"; + } + } + }, + async refresh(interaction) { + const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); + let guildInfo = getGuildInfoResponse.data; + 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); + // Build the Action Row that will contain the Refresh and Reset Ping buttons + const compareActionRow = await this.builders.refreshAction(interaction.guildId); + + const embed = this.builders.comparisonEmbed(comparedRankings, compareActionRow); + await interaction.update(embed); + } else { + await interaction.update(this.builders.errorEmbed(findMessagesResponse.status)); + } }, reset(guildId) { return new Promise((resolve, reject) => { @@ -348,13 +484,13 @@ const functions = { // 86400 sec in day let units = " secs"; - if ( 60 < time && time <= 3600 ) { // Minutes + if (60 < time && time <= 3600) { // Minutes time = parseFloat(time / 60).toFixed(1); units = " mins"; - } else if ( 3600 < time && time <= 86400 ) { + } else if (3600 < time && time <= 86400) { time = parseFloat(time / 3600).toFixed(1); units = " hrs"; - } else if ( 86400 < time ) { + } else if (86400 < time) { time = parseFloat(time / 86400).toFixed(1); units = " days"; } @@ -371,7 +507,7 @@ const functions = { reminderChannel.send(reminderEmbed).then(async m => { await dbfn.setRemindedStatus(guildId, 1); if (m.deletable) { - setTimeout(function() { + setTimeout(function () { m.delete(); }, 60000); } @@ -391,7 +527,7 @@ const functions = { const guilds = getOptedInGuildsResponse.data; guilds.forEach(async guildInfo => { const { guildId, treeChannelId, treeMessageId, remindedStatus } = guildInfo; - + if (remindedStatus == 0) { const guild = await client.guilds.fetch(guildId); const treeChannel = await guild.channels.fetch(treeChannelId); @@ -399,13 +535,13 @@ const functions = { const readyToWater = treeMessage.embeds[0].description.includes('Ready to be watered'); if (readyToWater) { // console.log("Ready to water"); - this.sendReminder(guildInfo, guild); - this.sleep(5000).then(() => { + await this.sendReminder(guildInfo, guild); + await this.sleep(5000).then(() => { this.checkReady(client); }); } else { // console.log("Not ready to water"); - this.sleep(5000).then(() => { + await this.sleep(5000).then(() => { this.checkReady(client); }); } @@ -413,13 +549,13 @@ const functions = { }); } else { // console.log(getOptedInGuildsResponse.status); - this.sleep(5000).then(() => { + await this.sleep(5000).then(() => { this.checkReady(client); }); } - } catch(err) { + } catch (err) { console.error(err); - this.sleep(30000).then(() => { + await this.sleep(30000).then(() => { this.checkReady(client); }); } diff --git a/modules/string.txt b/modules/string.txt new file mode 100644 index 0000000..25a734c --- /dev/null +++ b/modules/string.txt @@ -0,0 +1,3 @@ +If your `/tree` and `/top trees` messages are in the same channel, simple run `/compare` in that channel and you're good to go! + +Otherwise, run `/setup` to set the proper channels for the bot to look in for the `/tree` and `/top trees` messages; \ No newline at end of file diff --git a/package.json b/package.json index f749387..abcefce 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "dependencies": { "discord.js": "^14.7.1", "dotenv": "^16.0.3", - "mysql": "^2.18.1" + "mysql": "^2.18.1", + "string-replace-all": "^2.0.0", + "string.prototype.replaceall": "^1.0.7" } } diff --git a/slash-commands/DEV_dumptree.js b/slash-commands/DEV_dumptree.js deleted file mode 100644 index 8301c66..0000000 --- a/slash-commands/DEV_dumptree.js +++ /dev/null @@ -1,20 +0,0 @@ -const { SlashCommandBuilder } = require('discord.js'); -const dbfn = require('../modules/dbfn.js'); -const fn = require('../modules/functions.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('dumptree') - .setDescription('Dump the contents of the tree message to console'), - async execute(interaction) { - await interaction.deferReply({ ephemeral: true }); - const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); - const { treeMessageId, treeChannelId } = getGuildInfoResponse.data; - interaction.guild.channels.fetch(treeChannelId).then(treeChannel => { - treeChannel.messages.fetch(treeMessageId).then(treeMessage => { - interaction.editReply("done"); - console.log(JSON.stringify(treeMessage.embeds[0])); - }); - }); - }, -}; \ No newline at end of file diff --git a/slash-commands/commands.js b/slash-commands/commands.js new file mode 100644 index 0000000..06f906f --- /dev/null +++ b/slash-commands/commands.js @@ -0,0 +1,20 @@ +const { SlashCommandBuilder, messageLink } = require('discord.js'); +const fn = require('../modules/functions.js'); +const strings = require('../data/strings.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('commands') + .setDescription('Get a list of all my commands') + .addStringOption(o => + o.setName('private') + .setDescription('Should the response be sent privately?') + .setRequired(true) + .addChoices( + { name: "True", value: "true" }, + { name: "False", value: "false" })), + execute(interaction) { + const helpEmbed = fn.builders.helpEmbed(`**All Commands**\n${strings.help.allCommands}`, interaction.options.getString('private')); + interaction.reply(helpEmbed); + }, +}; \ No newline at end of file diff --git a/slash-commands/compare.js b/slash-commands/compare.js index 7268c1c..738c1ec 100644 --- a/slash-commands/compare.js +++ b/slash-commands/compare.js @@ -1,4 +1,5 @@ const { SlashCommandBuilder } = require('discord.js'); +const dbfn = require('../modules/dbfn.js'); const fn = require('../modules/functions.js'); module.exports = { @@ -6,19 +7,71 @@ module.exports = { .setName('compare') .setDescription('See how your tree compares to other trees!'), async execute(interaction) { - interaction.deferReply().then(() => { - fn.rankings.compare(interaction).then(async res => { - const refreshActionRow = await fn.builders.refreshAction(interaction.guildId); - const embed = fn.builders.comparisonEmbed(res, refreshActionRow); - interaction.editReply(embed).catch(err => { + try { + await interaction.deferReply(); + // Get the guildInfo from the database + dbfn.getGuildInfo(interaction.guildId).then(async getGuildInfoResponse => { + let guildInfo = getGuildInfoResponse.data; + // Find the most recent tree and leaderboard messages in their respective channels + const findMessagesResponse = await fn.messages.find(interaction, guildInfo); + if (findMessagesResponse.code == 1) { + guildInfo = findMessagesResponse.data; + // 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); + // Build the Action Row that will contain the Refresh and Reset Ping buttons + const compareActionRow = await fn.builders.refreshAction(interaction.guildId); + + const embed = fn.builders.comparisonEmbed(comparedRankings, compareActionRow); + await interaction.editReply(embed); + } else { + await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); + } + + }).catch(async err => { // If we fail to fetch the guild's info from the database + // If the error is because the guild hasn't been setup yet, set it up + if (err === "There is no database entry for your guild yet. Try running /setup") { + // Create a basic guildInfo with blank data + let guildInfo = { + guildId: `${interaction.guildId}`, + treeName: "", + treeHeight: 0, + treeMessageId: "", + treeChannelId: `${interaction.channelId}`, // Use this interaction channel for the initial channel IDs + leaderboardMessageId: "", + leaderboardChannelId: `${interaction.channelId}`, + reminderMessage: "", + reminderChannelId: "", + remindedStatus: 0, + reminderOptIn: 0, + } + // 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); + // Build the Action Row that will contain the Refresh and Reset Ping buttons + const compareActionRow = await fn.builders.refreshAction(interaction.guildId); + + + const embed = fn.builders.comparisonEmbed(comparedRankings, compareActionRow); + await interaction.editReply(embed); + } else { + await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); + } + + } else { + await interaction.editReply(fn.builders.errorEmbed("An unknown error occurred while running the compare command.")); console.error(err); - }); - }).catch(err => { - interaction.editReply(fn.builders.errorEmbed(err)).catch(err => { - console.error(err); - }); + } + }); + } catch (err) { + interaction.editReply(fn.builders.errorEmbed(err)).catch(err => { console.error(err); }); - }) + console.error(err); + } }, }; \ No newline at end of file diff --git a/slash-commands/help.js b/slash-commands/help.js index 6319426..40a9141 100644 --- a/slash-commands/help.js +++ b/slash-commands/help.js @@ -14,7 +14,7 @@ module.exports = { { name: "True", value: "true" }, { name: "False", value: "false" })), execute(interaction) { - const helpEmbed = fn.builders.helpEmbed(`${strings.help.info}\n\n**Setup**\n${strings.help.setup}\n\n**All Commands**\n${strings.help.allCommands}\n\n**Support Server**\n${strings.urls.supportServer}`, interaction.options.getString('private')); + const helpEmbed = fn.builders.helpEmbed(`${strings.help.info}\n\n**Setup**\n${strings.help.setup}\n\nSee for a list of all my commands\n\n**Support Server**\n${strings.urls.supportServer}`, interaction.options.getString('private')); interaction.reply(helpEmbed); }, }; \ No newline at end of file diff --git a/slash-commands/setup.js b/slash-commands/setup.js index 1a95bed..cbd1350 100644 --- a/slash-commands/setup.js +++ b/slash-commands/setup.js @@ -6,62 +6,32 @@ const dbfn = require('../modules/dbfn.js'); module.exports = { data: new SlashCommandBuilder() .setName('setup') - .setDescription('Attempt automatic configuration of the bot.'), - execute(interaction) { - interaction.deferReply({ ephemeral: true }).then(function () { - /*const guildInfo = { "guildId": "123", - "treeName": "name", - "treeHeight": 123, - "treeMessageId": "123", - "treeChannelId": "123", - "leaderboardMessageId": "123", - "leaderboardChannelId": "123" - };*/ - const guildInfo = { "guildId": interaction.guildId, - "treeName": "name", - "treeHeight": 123, - "treeMessageId": "123", - "treeChannelId": "123", - "leaderboardMessageId": "123", - "leaderboardChannelId": "123" - }; - interaction.channel.messages.fetch({ limit: 20 }).then(function (msgs) { - let treeFound = false; - let leaderboardFound = false; - msgs.reverse().forEach(msg => { - if (msg.embeds.length > 0) { - if (msg.embeds[0].data.description.includes("Your tree is")) { - treeFound = true; - guildInfo.treeName = msg.embeds[0].title; - guildInfo.treeChannelId = msg.channelId; - guildInfo.treeMessageId = msg.id; - } else if (msg.embeds[0].data.title == "Tallest Trees") { - leaderboardFound = true; - guildInfo.leaderboardChannelId = msg.channelId; - guildInfo.leaderboardMessageId = msg.id; - } - } - }); - if (treeFound && !(leaderboardFound)) { - dbfn.setTreeInfo(guildInfo).then(res => { - interaction.editReply(fn.builders.embed(strings.status.treeNoLeaderboard)); - }).catch(err => { - console.error(err); - }); - } else if (!(treeFound) && leaderboardFound) { - dbfn.setLeaderboardInfo(guildInfo).then(res => { - interaction.editReply(fn.builders.embed(strings.status.leaderboardNoTree)); - }).catch(err => { - console.error(err); - }); - } else if (treeFound && leaderboardFound) { - dbfn.setGuildInfo(guildInfo).then(res => { - interaction.editReply(fn.builders.embed(strings.status.treeAndLeaderboard)); - }).catch(err => { - console.error(err); - }); - } - }); - }); + .setDescription('Attempt automatic configuration of the bot.') + .addChannelOption(o => + o.setName('treechannel') + .setDescription('What channel is your tree in?') + .setRequired(true)) + .addChannelOption(o => + o.setName('leaderboardchannel') + .setDescription('If your leaderboard isn\'t in the same channel, where is it?') + .setRequired(false)), + async execute(interaction) { + await interaction.deferReply({ ephemeral: true }); + /**/ + let guildInfo = { + "guildId": interaction.guildId, + "treeName": "", + "treeHeight": 0, + "treeMessageId": "", + "treeChannelId": `${interaction.options.getChannel('treechannel').id }`, + "leaderboardMessageId": "", + "leaderboardChannelId": `${interaction.options.getChannel('leaderboardchannel').id || interaction.options.getChannel('treechannel').id }`, + "reminderMessage": "", + "reminderChannelId": "", + "remindedStatus": 0, + "reminderOptIn": 0, + }; + const findMessagesResponse = await fn.messages.find(interaction, guildInfo); + interaction.editReply(findMessagesResponse.status); }, }; \ No newline at end of file