From 5ef905449dcb69f04cc77585b4da776cbd00e4b6 Mon Sep 17 00:00:00 2001 From: Skylar Grant Date: Tue, 31 Jan 2023 22:51:10 -0500 Subject: [PATCH] Dev-v1.1.4 (#9) * Add 24h growth indicator * Fix float math * Fix some bugs * Trim decimals * Add 24 hour observed growth * Add beginning height option * Changed startup message to ping me * Add a ping reminder and setup command for it * Setup automatic water reminders * Improved workflows * New water readiness system * Documentation Time * Fix slash command link * Fix linebreaks * Readability improvements * Forgot to allow checking in prod * Switch to ephemeral reply * Restructuring and new help messages * Not meant to be uploaded * Documentation update * Changing the way reminders are deleted * Tweak timings * Adjust readiness detection system * moar tweekz * fix reminders * Updates to water reminders --- .github/workflows/docker-image-dev.yml | 24 + .github/workflows/docker-image.yml | 4 +- .gitignore | 1 + README.md | 25 +- TODO.md | 27 + data/strings.json | 14 +- main.js | 31 +- modules/_prepareStrings.js | 33 ++ modules/dbfn.js | 346 +++++++++--- modules/functions.js | 702 ++++++++++++++++++------- package.json | 6 +- slash-commands/commands.js | 20 + slash-commands/compare.js | 72 ++- slash-commands/help.js | 2 +- slash-commands/optout.js | 20 + slash-commands/setping.js | 31 ++ slash-commands/setping.js.bak | 18 + slash-commands/setup.js | 84 +-- slash-commands/timetoheight.js | 18 +- 19 files changed, 1128 insertions(+), 350 deletions(-) create mode 100644 .github/workflows/docker-image-dev.yml create mode 100644 TODO.md create mode 100644 modules/_prepareStrings.js create mode 100644 slash-commands/commands.js create mode 100644 slash-commands/optout.js create mode 100644 slash-commands/setping.js create mode 100644 slash-commands/setping.js.bak diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml new file mode 100644 index 0000000..d9a73c1 --- /dev/null +++ b/.github/workflows/docker-image-dev.yml @@ -0,0 +1,24 @@ +name: Silvanus-Dev Dockerization + +on: + push: + branches: [ "*-dev" ] + +env: + DHUB_UNAME: ${{ secrets.DHUB_UNAME }} + DHUB_PWORD: ${{ secrets.DHUB_PWORD }} + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag v0idf1sh/silvanus-dev + - name: Log into Docker Hub + run: docker login -u $DHUB_UNAME -p $DHUB_PWORD + - name: Push image to Docker Hub + run: docker push v0idf1sh/silvanus-dev diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 272d3e9..b400d25 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,8 +1,6 @@ -name: Docker Image CI +name: Silvanus Dockerization on: - push: - branches: [ "main" ] pull_request: branches: [ "main" ] diff --git a/.gitignore b/.gitignore index b7a3af7..b9e44c7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ env.dev env.prod .DS_Store data/guildInfo.json +data/rawstring.txt # Custom folders # gifs/* diff --git a/README.md b/README.md index 0abea42..017087a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # Silvanus -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 /help and /setup, then check out /compare. +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! -Important 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. - -For the best experience we recommend the use of a single /tree and /top trees message, otherwise make sure to run /setup each time you run /compare. +Important 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. Silvanus is not affiliated with Grow A Tree or Limbo Labs. @@ -14,15 +12,26 @@ Silvanus is not affiliated with Grow A Tree or Limbo Labs. [Join Discord Server](https://discord.gg/g5JRGn7PxU) ## Setup -To begin analyzing your Tree, first you must set up the reference messages.\n\n1. Run `/setup` in the channel(s) that contain your server's tree and leaderboard messages.\n2. Now simply run `/compare` where you want your analysis to be visible. + +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. + +Use `/commands` to view a description of all my commands. ## Permissions 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`. * `/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 1d2c13b..afb5fcc 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.\n\nUse to view a description of all my commands.", "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": " | | | | | " + "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 ea56db2..fef604a 100644 --- a/main.js +++ b/main.js @@ -24,15 +24,19 @@ const client = new Client({ // Various imports const fn = require('./modules/functions.js'); const strings = require('./data/strings.json'); +const dbfn = require('./modules/dbfn.js'); const isDev = process.env.isDev; client.once('ready', () => { fn.collections.slashCommands(client); console.log('Ready!'); client.user.setActivity({ name: strings.activity.name, type: ActivityType.Watching }); - client.channels.fetch(statusChannelId).then(channel => { - channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete`); - }); + fn.checkReady(client); + if (isDev == 'false') { + client.channels.fetch(statusChannelId).then(channel => { + channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete <@481933290912350209>`); + }); + } }); // slash-commands @@ -52,7 +56,26 @@ client.on('interactionCreate', async interaction => { } if (interaction.isButton() && interaction.component.customId == 'refresh') { - fn.refresh(interaction); + // console.log(JSON.stringify(interaction)); + await fn.refresh(interaction).catch(err => { + interaction.channel.send(fn.builders.errorEmbed(err)); + }); + } else if (interaction.isButton() && interaction.component.customId == 'resetping') { + await fn.resetPing(interaction); + await fn.refresh(interaction).catch(err => { + interaction.channel.send(fn.builders.errorEmbed(err)); + }); + } else if (interaction.isButton() && interaction.component.customId == 'deleteping') { + if (interaction.message.deletable) { + await dbfn.setRemindedStatus(interaction.guildId, 0); + await dbfn.getGuildInfo(interaction.guildId).then(async res => { + const guildInfo = res.data; + await fn.refreshComparisonMessage(interaction.client, guildInfo); + }); + await interaction.message.delete().catch(err => { + console.error(err); + }); + } } }); diff --git a/modules/_prepareStrings.js b/modules/_prepareStrings.js new file mode 100644 index 0000000..1c26722 --- /dev/null +++ b/modules/_prepareStrings.js @@ -0,0 +1,33 @@ +/* + + + + + + + + + + + +*/ + +const fs = require('fs'); +const replaceAll = require('string.prototype.replaceall'); +const string = fs.readFileSync('./data/rawstring.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, '`/commands`', ''); +newString = replaceAll(newString, '`', '``'); +fs.writeFileSync('./data/rawstring.txt', newString); +return "Done"; \ No newline at end of file diff --git a/modules/dbfn.js b/modules/dbfn.js index 296fb3f..5eac3e4 100644 --- a/modules/dbfn.js +++ b/modules/dbfn.js @@ -35,16 +35,16 @@ leaderboard module.exports = { createGuildTables(guildId) { const db = mysql.createConnection({ - host : process.env.DBHOST, - user : process.env.DBUSER, - password : process.env.DBPASS, - database : process.env.DBNAME, - port : process.env.DBPORT + 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}`; }); - // Create the guild-information and rank-information tables to be used. + // Create the guild-information and rank-information tables to be used. const createGuildInfoTableQuery = "CREATE TABLE IF NOT EXISTS guild_info(guild_id VARCHAR(50) NOT NULL, tree_name VARCHAR(100) NOT NULL DEFAULT 'Run /setup where your tree is.', tree_height INT(10) NOT NULL DEFAULT 0, tree_message_id VARCHAR(50) NOT NULL DEFAULT 'Run /setup where your tree is.', tree_channel_id VARCHAR(50) NOT NULL DEFAULT 'Run /setup where your tree is.', leaderboard_message_id VARCHAR(50) NOT NULL DEFAULT 'Run /setup where your leaderboard is.', leaderboard_channel_id VARCHAR(50) NOT NULL DEFAULT 'Run /setup where your leaderboard is.', CONSTRAINT guild_pk PRIMARY KEY (guild_id))"; const createLeaderboardTableQuery = "CREATE TABLE IF NOT EXISTS leaderboard(id INT(10) NOT NULL AUTO_INCREMENT,guild_id VARCHAR(50) NOT NULL,tree_name VARCHAR(100) NOT NULL,tree_rank INT(10) NOT NULL,tree_height INT(10) NOT NULL DEFAULT 1,has_pin TINYINT(1) NOT NULL DEFAULT 0,timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT id_pk PRIMARY KEY(id))"; // TODO run the queries, then add a call to this function at the beginning of main.js or functions.js @@ -68,20 +68,20 @@ module.exports = { }); }); }); - }, - getGuildInfo(guildId) { + }, + getGuildInfo(guildId) { const db = mysql.createConnection({ - host : process.env.DBHOST, - user : process.env.DBUSER, - password : process.env.DBPASS, - database : process.env.DBNAME, - port : process.env.DBPORT + 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}`; }); - // Get a server's tree information from the database - const selectGuildInfoQuery = `SELECT tree_name, tree_height, tree_message_id, tree_channel_id, leaderboard_message_id, leaderboard_channel_id FROM guild_info WHERE guild_id = ${db.escape(guildId)}`; + // Get a server's tree information from the database + const selectGuildInfoQuery = `SELECT * FROM guild_info WHERE guild_id = ${db.escape(guildId)}`; // TODO run this query and return a promise then structure the output into a GuildInfo object. resolve with { "status": , "data": guildInfo } return new Promise((resolve, reject) => { db.query(selectGuildInfoQuery, (err, res) => { @@ -97,7 +97,11 @@ module.exports = { "treeMessageId": "123", "treeChannelId": "123", "leaderboardMessageId": "123", - "leaderboardChannelId": "123" + "leaderboardChannelId": "123", + "reminderMessage": "Abc", + "reminderChannelId": "123", + "remindedStatus": 0, + "comparisonMessageId": "123" };*/ if (res.length == 0) { reject("There is no database entry for your guild yet. Try running /setup"); @@ -105,33 +109,40 @@ 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, "treeChannelId": row.tree_channel_id, "leaderboardMessageId": row.leaderboard_message_id, - "leaderboardChannelId": row.leaderboard_channel_id + "leaderboardChannelId": row.leaderboard_channel_id, + "reminderMessage": row.ping_role_id, + "reminderChannelId": row.ping_channel_id, + "remindedStatus": row.reminded_status, + "reminderOptIn": row.reminder_optin, + "comparisonMessageId": row.comparison_message_id, + "comparisonChannelId": row.comparison_channel_id }; db.end(); resolve({ "status": "Successfully fetched guild information", "data": guildInfo }); }); }); - }, - setGuildInfo(guildInfo) { + }, + setGuildInfo(guildInfo) { const db = mysql.createConnection({ - host : process.env.DBHOST, - user : process.env.DBUSER, - password : process.env.DBPASS, - database : process.env.DBNAME, - port : process.env.DBPORT + 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": null }) // guildInfo = { "guildId": "123", "treeName": "name", "treeHeight": 123, "treeMessageId": "123", "treeChannelId": "123", "leaderboardMessageId": "123", "leaderboardChannelId": "123"} - // Set a server's tree information in the database + // Set a server's tree information in the database const insertGuildInfoQuery = `INSERT INTO guild_info (guild_id, tree_name, tree_height, tree_message_id, tree_channel_id, leaderboard_message_id, leaderboard_channel_id) VALUES (${db.escape(guildInfo.guildId)}, ${db.escape(guildInfo.treeName)}, ${db.escape(guildInfo.treeHeight)},${db.escape(guildInfo.treeMessageId)}, ${db.escape(guildInfo.treeChannelId)}, ${db.escape(guildInfo.leaderboardMessageId)}, ${db.escape(guildInfo.leaderboardChannelId)}) ON DUPLICATE KEY UPDATE tree_name = ${db.escape(guildInfo.treeName)},tree_height = ${db.escape(guildInfo.treeHeight)},tree_message_id = ${db.escape(guildInfo.treeMessageId)},tree_channel_id = ${db.escape(guildInfo.treeChannelId)},leaderboard_message_id = ${db.escape(guildInfo.leaderboardMessageId)},leaderboard_channel_id = ${db.escape(guildInfo.leaderboardChannelId)}`; // TODO run this query and return a promise, then resolve with { "status": , "data": null } return new Promise((resolve, reject) => { @@ -146,21 +157,21 @@ module.exports = { resolve({ "status": "Successfully set the guild information", "data": null }); }); }); - }, - setTreeInfo(guildInfo) { + }, + setTreeInfo(guildInfo) { const db = mysql.createConnection({ - host : process.env.DBHOST, - user : process.env.DBUSER, - password : process.env.DBPASS, - database : process.env.DBNAME, - port : process.env.DBPORT + 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": null }) // guildInfo = { "guildId": "123", "treeName": "name", "treeHeight": 123, "treeMessageId": "123", "treeChannelId": "123", "leaderboardMessageId": "123", "leaderboardChannelId": "123"} - // Set a server's tree information in the database) + // Set a server's tree information in the database) const insertGuildInfoQuery = `INSERT INTO guild_info (guild_id, tree_name, tree_height, tree_message_id, tree_channel_id) VALUES (${db.escape(guildInfo.guildId)}, ${db.escape(guildInfo.treeName)}, ${db.escape(guildInfo.treeHeight)},${db.escape(guildInfo.treeMessageId)}, ${db.escape(guildInfo.treeChannelId)}) ON DUPLICATE KEY UPDATE tree_name = ${db.escape(guildInfo.treeName)},tree_height = ${db.escape(guildInfo.treeHeight)},tree_message_id = ${db.escape(guildInfo.treeMessageId)},tree_channel_id = ${db.escape(guildInfo.treeChannelId)}`; // TODO run this query and return a promise, then resolve with { "status": , "data": null } return new Promise((resolve, reject) => { @@ -175,21 +186,21 @@ module.exports = { resolve({ "status": "Successfully set the guild information", "data": null }); }); }); - }, - setLeaderboardInfo(guildInfo) { + }, + setLeaderboardInfo(guildInfo) { const db = mysql.createConnection({ - host : process.env.DBHOST, - user : process.env.DBUSER, - password : process.env.DBPASS, - database : process.env.DBNAME, - port : process.env.DBPORT + 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": null }) // guildInfo = { "guildId": "123", "treeName": "name", "treeHeight": 123, "treeMessageId": "123", "treeChannelId": "123", "leaderboardMessageId": "123", "leaderboardChannelId": "123"} - // Set a server's tree information in the database + // Set a server's tree information in the database const insertGuildInfoQuery = `INSERT INTO guild_info (guild_id, leaderboard_message_id, leaderboard_channel_id) VALUES (${db.escape(guildInfo.guildId)}, ${db.escape(guildInfo.leaderboardMessageId)}, ${db.escape(guildInfo.leaderboardChannelId)}) ON DUPLICATE KEY UPDATE leaderboard_message_id = ${db.escape(guildInfo.leaderboardMessageId)},leaderboard_channel_id = ${db.escape(guildInfo.leaderboardChannelId)}`; // TODO run this query and return a promise, then resolve with { "status": , "data": null } return new Promise((resolve, reject) => { @@ -204,21 +215,21 @@ module.exports = { resolve({ "status": "Successfully set the guild information", "data": null }); }); }); - }, + }, deleteGuildInfo(guildId) { const db = mysql.createConnection({ - host : process.env.DBHOST, - user : process.env.DBUSER, - password : process.env.DBPASS, - database : process.env.DBNAME, - port : process.env.DBPORT + 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": null }) // guildInfo = { "guildId": "123", "treeName": "name", "treeHeight": 123, "treeMessageId": "123", "treeChannelId": "123", "leaderboardMessageId": "123", "leaderboardChannelId": "123"} - // Set a server's tree information in the database + // Set a server's tree information in the database const deleteGuildInfoQuery = `DELETE FROM guild_info WHERE guild_id = ${db.escape(guildId)}`; // TODO run this query and return a promise, then resolve with { "status": , "data": null } return new Promise((resolve, reject) => { @@ -234,13 +245,13 @@ module.exports = { }); }); }, - getLeaderboard(guildId) { + getLeaderboard(guildId) { const db = mysql.createConnection({ - host : process.env.DBHOST, - user : process.env.DBUSER, - password : process.env.DBPASS, - database : process.env.DBNAME, - port : process.env.DBPORT + 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}`; @@ -269,14 +280,14 @@ module.exports = { resolve({ "status": "Successfully fetched leaderboard.", "data": leaderboard }); }); }); - }, + }, uploadLeaderboard(leaderboard) { const db = mysql.createConnection({ - host : process.env.DBHOST, - user : process.env.DBUSER, - password : process.env.DBPASS, - database : process.env.DBNAME, - port : process.env.DBPORT + 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}`; @@ -300,5 +311,218 @@ 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 }); + }); + }); + }, + setReminderInfo(guildId, reminderMessage, reminderChannelId) { + 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 insertReminderInfoQuery = `UPDATE guild_info SET ping_role_id = ${db.escape(reminderMessage)}, ping_channel_id = ${db.escape(reminderChannelId)} WHERE guild_id = ${db.escape(guildId)}`; + // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } + return new Promise((resolve, reject) => { + db.query(insertReminderInfoQuery, (err, res) => { + if (err) { + console.error(err); + db.end(); + reject("Error updating the reminder info: " + err.message); + return; + } + db.end(); + resolve({ "status": `Successfully set the reminder message to "${reminderMessage}" in <#${reminderChannelId}>`, "data": res }); + }); + }); + }, + setRemindedStatus(guildId, remindedStatus) { + 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 setRemindedStatusQuery = `UPDATE guild_info SET reminded_status = ${db.escape(remindedStatus)} WHERE guild_id = ${db.escape(guildId)}`; + // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } + return new Promise((resolve, reject) => { + db.query(setRemindedStatusQuery, (err, res) => { + if (err) { + console.error(err); + db.end(); + reject("Error updating the reminded status: " + err.message); + return; + } + db.end(); + resolve({ "status": `Successfully set the reminded status to ${remindedStatus}`, "data": res }); + // console.log("Boop: " + remindedStatus); + }); + }); + }, + setReminderOptIn(guildId, optIn) { + 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 setReminderOptInQuery = `UPDATE guild_info SET reminder_optin = ${db.escape(optIn)} WHERE guild_id = ${db.escape(guildId)}`; + // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } + return new Promise((resolve, reject) => { + db.query(setReminderOptInQuery, (err, res) => { + if (err) { + console.error(err); + db.end(); + reject("Error updating the reminder opt-in status: " + err.message); + return; + } + db.end(); + resolve({ "status": `Successfully set the reminder opt-in status to ${optIn}`, "data": res }); + }); + }); + }, + getOptedInGuilds() { + 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}`; + }); + // Get a server's tree information from the database + const getOptedInGuildsQuery = `SELECT * FROM guild_info WHERE reminder_optin = 1`; + // TODO run this query and return a promise then structure the output into a GuildInfo object. resolve with { "status": , "data": guildInfo } + return new Promise((resolve, reject) => { + db.query(getOptedInGuildsQuery, (err, res) => { + if (err) { + console.error(err); + reject("Error fetching guild information: " + err.message); + db.end(); + return; + } + /*const guildInfo = { "guildId": "123", + "treeName": "name", + "treeHeight": 123, + "treeMessageId": "123", + "treeChannelId": "123", + "leaderboardMessageId": "123", + "leaderboardChannelId": "123", + "reminderMessage": "Abc", + "reminderChannelId": "123", + "remindedStatus": 0, + "comparisonMessageId": "123" + };*/ + if (res.length == 0) { + resolve({ "status": "No servers have opted in yet" }); + db.end(); + return; + } + row = res[0]; + let guilds = []; + res.forEach(row => { + guilds.push({ + "guildId": row.guild_id, + "treeName": row.tree_name, + "treeHeight": row.tree_height, + "treeMessageId": row.tree_message_id, + "treeChannelId": row.tree_channel_id, + "leaderboardMessageId": row.leaderboard_message_id, + "leaderboardChannelId": row.leaderboard_channel_id, + "reminderMessage": row.ping_role_id, + "reminderChannelId": row.ping_channel_id, + "remindedStatus": row.reminded_status, + "comparisonMessageId": row.comparison_message_id, + "comparisonChannelId": row.comparison_channel_id + }); + }); + db.end(); + resolve({ "status": "Successfully fetched guild information", "data": guilds }); + }); + }); + }, + setComparisonMessage(comparisonMessage, guildId) { + 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 setComparisonMessageQuery = `UPDATE guild_info SET comparison_message_id = ${db.escape(comparisonMessage.id)}, comparison_channel_id = ${db.escape(comparisonMessage.channel.id)} WHERE guild_id = ${db.escape(guildId)}`; + // console.log(JSON.stringify(comparisonMessage)); + // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } + return new Promise((resolve, reject) => { + db.query(setComparisonMessageQuery, (err, res) => { + if (err) { + console.error(err); + db.end(); + reject("Error updating the comparison message ID: " + err.message); + return; + } + db.end(); + resolve({ "status": `Successfully set the comparison message ID: ${comparisonMessage}`, "data": res }); + }); + }); } }; \ No newline at end of file diff --git a/modules/functions.js b/modules/functions.js index 91e4e46..90d2e97 100644 --- a/modules/functions.js +++ b/modules/functions.js @@ -17,6 +17,7 @@ const config = require('../data/config.json'); const strings = require('../data/strings.json'); const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => file.endsWith('.js')); const dbfn = require('./dbfn.js'); +const { finished } = require('stream'); dbfn.createGuildTables().then(res => { console.log(res.status); @@ -41,27 +42,84 @@ const functions = { } }, builders: { - refreshAction() { + actionRows: { + reminderActionRow() { + const deleteButton = new ButtonBuilder() + .setCustomId('deleteping') + .setEmoji('♻️') + .setStyle(ButtonStyle.Danger); + const actionRow = new ActionRowBuilder() + .addComponents(deleteButton); + return actionRow; + }, + comparisonActionRow(guildInfo) { + // console.log(guildInfo); + // Create the button to go in the Action Row + const refreshButton = new ButtonBuilder() + .setCustomId('refresh') + .setEmoji('🔄') + .setStyle(ButtonStyle.Primary); + // Create the Action Row with the Button in it, to be sent with the Embed + let refreshActionRow = new ActionRowBuilder() + .addComponents( + refreshButton + ); + if (guildInfo.reminderOptIn == 1 && guildInfo.remindedStatus == 1) { + const resetPingButton = new ButtonBuilder() + .setCustomId('resetping') + .setLabel('Reset Ping') + .setStyle(ButtonStyle.Secondary); + refreshActionRow.addComponents(resetPingButton); + } else if (guildInfo.reminderOptIn == 1 && guildInfo.remindedStatus == 0) { + const resetPingButton = new ButtonBuilder() + .setCustomId('resetping') + .setLabel('[Armed]') + .setStyle(ButtonStyle.Secondary); + refreshActionRow.addComponents(resetPingButton); + } + return refreshActionRow; + } + }, + async refreshAction(guildId) { // Create the button to go in the Action Row const refreshButton = new ButtonBuilder() .setCustomId('refresh') .setLabel('Refresh') .setStyle(ButtonStyle.Primary); + const resetPingButton = new ButtonBuilder() + .setCustomId('resetping') + .setLabel('Reset Ping') + .setStyle(ButtonStyle.Secondary); // Create the Action Row with the Button in it, to be sent with the Embed - const refreshActionRow = new ActionRowBuilder() + let refreshActionRow = new ActionRowBuilder() .addComponents( refreshButton ); + const getGuildInfoResponse = await dbfn.getGuildInfo(guildId); + const guildInfo = getGuildInfoResponse.data; + if (guildInfo.reminderMessage != "" && guildInfo.reminderChannelId != "") { + refreshActionRow.addComponents(resetPingButton); + } return refreshActionRow; }, - comparisonEmbed(content, refreshActionRow) { + comparisonEmbed(content, guildInfo) { // Create the embed using the content passed to this function const embed = new EmbedBuilder() .setColor(strings.embeds.color) - .setTitle('Tree Growth Comparison') + .setTitle('Tallest Trees Comparison') .setDescription(content) - .setFooter({text: `v${package.version} - ${strings.embeds.footer}`}); - const messageContents = { embeds: [embed], components: [refreshActionRow] }; + .setFooter({ text: `v${package.version} - ${strings.embeds.footer}` }); + const messageContents = { embeds: [embed], components: [this.actionRows.comparisonActionRow(guildInfo)] }; + return messageContents; + }, + reminderEmbed(content, guildInfo) { + // Create the embed using the content passed to this function + const embed = new EmbedBuilder() + .setColor(strings.embeds.color) + .setTitle('Water Reminder') + .setDescription(`[Click here to go to your Tree](https://discord.com/channels/${guildInfo.guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId})`) + .setFooter({ text: `Click ♻️ to delete this message` }); + const messageContents = { content: content, embeds: [embed], components: [this.actionRows.reminderActionRow()] }; return messageContents; }, helpEmbed(content, private) { @@ -94,188 +152,333 @@ 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); - return; - }); - }); - - }, - compare(interaction) { + parse(interaction, guildInfo) { return new Promise((resolve, reject) => { - dbfn.getGuildInfo(interaction.guildId).then(res => { - const guildInfo = res.data; - guildInfo.guildId = interaction.guildId; - - let treeHeight = parseFloat(guildInfo.treeHeight).toFixed(1); - dbfn.getLeaderboard(interaction.guildId).then(res => { - const leaderboard = res.data; - - let replyString = 'Current Tree Height: ' + treeHeight + 'ft\n\n'; - leaderboard.reverse().forEach(treeRanking => { - let difference = parseFloat(treeRanking.treeHeight).toFixed(1) - treeHeight; - let decimal = (treeRanking.treeHeight % 1).toFixed(1); - let growthIndicator = ""; - if (decimal > 0) { - growthIndicator += "[+]"; - } - const absDifference = parseFloat(Math.abs(difference)).toFixed(1); - if (treeRanking.hasPin) { - replyString += "This is your tree. "; - } else if ((treeRanking.treeHeight == treeHeight) && (treeRanking.treeName == guildInfo.treeName)) { - replyString += "This might be your tree. Same height, same name. "; - } else { - if (difference > 0) { - replyString += `#${treeRanking.treeRank} - ${absDifference}ft${growthIndicator} shorter `; - } else if (difference < 0) { - replyString += `#${treeRanking.treeRank} - ${absDifference}ft${growthIndicator} taller `; - } else if (difference == 0) { - replyString += `#${treeRanking.treeRank} - Same Height${growthIndicator} `; - } - } - replyString += `[${functions.getWaterTime(treeRanking.treeHeight)} mins]\n`; - }); - resolve('Here\'s how your tree compares: \n' + replyString); - }).catch(err => { - console.error(err); - }); - }).catch(err => { - reject(err); + 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, guildInfo) { + try { + 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 + for (let i = leaderboard.length - 1; i >= 0; i--) { + const leaderboardEntry = leaderboard[i]; + // 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; + // const hist24hDifference = (leaderboardEntry.treeHeight - dayAgoTree.treeHeight).toFixed(1); + // statusIndicator += `+${hist24hDifference}ft|` + + // 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 { + comparisonReplyString += `#${leaderboardEntry.treeRank} - ${Math.abs(currentHeightDifference).toFixed(1)}ft shorter`; + } + } + // Build a string using the current leaderboard entry and the historic entry from 24 hours ago + comparisonReplyString += `${statusIndicator}\n`; + // if (process.env.isDev == 'true') comparisonReplyString += `Current Height: ${leaderboardEntry.treeHeight} 24h Ago Height: ${dayAgoTree.treeHeight}\n`; + } + return comparisonReplyString; + } catch (err) { + 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(res => { - const embed = functions.builders.comparisonEmbed(res, functions.builders.refreshAction()) - interaction.update(embed); - }).catch(err => { - console.error(err); - }); - }).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); + + const embed = this.builders.comparisonEmbed(comparedRankings, guildInfo); + await interaction.update(embed).then(async interactionResponse => { + // console.log(interactionResponse.interaction.message); + await dbfn.setComparisonMessage(interactionResponse.interaction.message, interaction.guildId); }); - }).catch(e => { - console.error(e); - interaction.reply(functions.builders.errorEmbed(e)); - }); + } else { + await interaction.update(this.builders.errorEmbed(findMessagesResponse.status)); + } }, reset(guildId) { return new Promise((resolve, reject) => { @@ -304,27 +507,140 @@ const functions = { }); }, getWaterTime(size) { - const seconds = Math.floor(Math.pow(size * 0.07 + 5, 1.1)); - return (Math.floor((Math.pow(size * 0.07 + 5, 1.1))) / 60).toFixed(2); + return Math.floor(Math.pow(size * 0.07 + 5, 1.1)); // Seconds }, - timeToHeight(interaction, destHeight) { + timeToHeight(beginHeight, destHeight) { return new Promise((resolve, reject) => { - dbfn.getGuildInfo(interaction.guildId).then(res => { - let guildInfo = res.data; - let currentHeight = parseInt(guildInfo.treeHeight); - let time = 0; - for (let i = currentHeight; i < destHeight; i++) { - const waterTime = parseFloat(functions.getWaterTime(i)); - console.log("Height: " + i + "Time: " + waterTime); - time += waterTime; - } - resolve(time.toFixed(2)); - }).catch(err => { - console.error(err); - reject(err); - return; - }) + let time = 0; + for (let i = beginHeight; i < destHeight; i++) { + const waterTime = parseFloat(functions.getWaterTime(i)); + // console.log("Height: " + i + "Time: " + waterTime); + time += waterTime; + } + + // 60 secs in min + // 3600 secs in hr + // 86400 sec in day + + let units = " secs"; + if (60 < time && time <= 3600) { // Minutes + time = parseFloat(time / 60).toFixed(1); + units = " mins"; + } else if (3600 < time && time <= 86400) { + time = parseFloat(time / 3600).toFixed(1); + units = " hrs"; + } else if (86400 < time) { + time = parseFloat(time / 86400).toFixed(1); + units = " days"; + } + resolve(time + units); }); + }, + sleep(ms) { + // console.log(`Begin Sleep: ${new Date(Date.now()).getSeconds()}`); + return new Promise(resolve => { + setTimeout(function () { + resolve(); + // console.log(`End Sleep: ${new Date(Date.now()).getSeconds()}`); + }, ms); + }); + }, + async sendReminder(guildInfo, guild) { + const { guildId, reminderChannelId, reminderMessage } = guildInfo; + const reminderChannel = await guild.channels.fetch(reminderChannelId); + const reminderEmbed = functions.builders.reminderEmbed(reminderMessage, guildInfo); + await reminderChannel.send(reminderEmbed).then(async m => { + const setRemindedStatusReponse = await dbfn.setRemindedStatus(guildId, 1); + return 1; + }).catch(err => { + console.error(err); + }); + }, + async setReminder(interaction, ms) { + setTimeout(this.sendReminder(interaction), ms); + }, + async checkReady(client) { // Check if the guilds trees are ready to water + // let time = new Date(Date.now()); + // console.log("Ready check " + time.getSeconds()); + try { + // Get the guildInfos for each guild that is opted in and waiting to send a reminder + const getOptedInGuildsResponse = await dbfn.getOptedInGuilds(); + // getOptedInGuilds will return this if it gets an empty set from the database + if (getOptedInGuildsResponse.status != "No servers have opted in yet") { + // Get the Array of Guilds from the response + const guilds = getOptedInGuildsResponse.data; + // Iterate over the Array + for (let i = 0; i < guilds.length; i++) { + // console.log(`iter: ${i}`); + // Save the 'old' guild info that came from getOptedInGuilds + const oldGuildInfo = guilds[i]; + // Get up-to-date guildInfo from the database, probably unnecessary and redundant + const getGuildInfoResponse = await dbfn.getGuildInfo(oldGuildInfo.guildId); + // Save the new guildInfo so we can reference its remindedStatus + const guildInfo = getGuildInfoResponse.data; + const { guildId, treeChannelId, treeMessageId, remindedStatus } = guildInfo; + // console.log(`${guildInfo.treeName}: ${remindedStatus}`); + // Double check the remindedStatus to prevent double pings + if (remindedStatus == 0) { + // Fetch the guild + const guild = await client.guilds.fetch(guildId); + // Fetch the tree channel + const treeChannel = await guild.channels.fetch(treeChannelId); + // Fetch the tree message + const treeMessage = await treeChannel.messages.fetch(treeMessageId); + // Get the description from the embed of the tree message + const description = treeMessage.embeds[0].description; + // Default to not being ready to water + let readyToWater = false; + // Obviously if the tree says it's Ready to be watered, it's ready + if (description.includes("Ready to be watered")) { + readyToWater = true; + // But sometimes the tree doesn't refresh the embed, in that case we'll do a secondary check using the + // timestamp included in the embed. + } else { + const beginWaterTimestamp = description.indexOf(""); + // Split the description starting at "" to get just the numerical timestamp + const waterTimestamp = parseInt(description.slice(beginWaterTimestamp, endWaterTimestamp)); + // The Discord timestamp is in seconds, not ms so we need to divide by 1000 + const nowTimestamp = (Date.now() / 1000); + readyToWater = (nowTimestamp > waterTimestamp); + } + + if (readyToWater) { + // Send the reminder message + await this.sendReminder(guildInfo, guild); + guildInfo.remindedStatus = 1; + await this.refreshComparisonMessage(client, guildInfo); + } + } + } + await this.sleep(5000); + this.checkReady(client); + } else { + // console.log(getOptedInGuildsResponse.status); + await this.sleep(5000); + this.checkReady(client); + } + } catch (err) { + console.error(err); + await this.sleep(30000); + this.checkReady(client); + } + }, + async refreshComparisonMessage(client, guildInfo) { + if (guildInfo.comparisonChannelId != "" && guildInfo.comparisonMessageId != "") { + const guild = await client.guilds.fetch(guildInfo.guildId); + const comparisonChannel = await guild.channels.fetch(guildInfo.comparisonChannelId); + const comparisonMessage = await comparisonChannel.messages.fetch(guildInfo.comparisonMessageId); + const embed = comparisonMessage.embeds[0]; + const actionRow = this.builders.actionRows.comparisonActionRow(guildInfo); + await comparisonMessage.edit({ components: [actionRow] }); + return; + } + }, + async resetPing(interaction) { + await dbfn.setRemindedStatus(interaction.guildId, 0); } }; diff --git a/package.json b/package.json index 9576e95..abcefce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silvanus", - "version": "1.1.1", + "version": "1.1.4", "description": "Grow A Tree Comparison Tool", "main": "main.js", "scripts": { @@ -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/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 37864f8..e631f86 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,18 +7,69 @@ module.exports = { .setName('compare') .setDescription('See how your tree compares to other trees!'), async execute(interaction) { - interaction.deferReply().then(() => { - fn.rankings.compare(interaction).then(res => { - const embed = fn.builders.comparisonEmbed(res, fn.builders.refreshAction()); - 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); + + const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo); + await interaction.editReply(embed).then(async message => { + await dbfn.setComparisonMessage(message, interaction.guildId); + }); + } 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); + const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo); + await interaction.editReply(embed).then(async message => { + await dbfn.setComparisonMessage(message.id, interaction.guildId); + }); + } 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/optout.js b/slash-commands/optout.js new file mode 100644 index 0000000..1d1cf37 --- /dev/null +++ b/slash-commands/optout.js @@ -0,0 +1,20 @@ +const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); +const dbfn = require('../modules/dbfn.js'); +const fn = require('../modules/functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('optout') + .setDescription('Opt-out of automatic water reminders') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles), + async execute(interaction) { + try { + await interaction.deferReply({ ephemeral: true }); + const setReminderOptInResponse = await dbfn.setReminderOptIn(interaction.guildId, 0); + interaction.editReply(setReminderOptInResponse.status); + } catch(err) { + console.error(err); + await interaction.editReply(fn.builders.errorEmbed(err)); + } + }, +}; \ No newline at end of file diff --git a/slash-commands/setping.js b/slash-commands/setping.js new file mode 100644 index 0000000..8674e5f --- /dev/null +++ b/slash-commands/setping.js @@ -0,0 +1,31 @@ +const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); +const dbfn = require('../modules/dbfn.js'); +const fn = require('../modules/functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('setping') + .setDescription('Opt-in to automatic water reminders') + .addStringOption(o => + o.setName('pingmsg') + .setDescription('The message to send for a water reminder') + .setRequired(true)) + .addChannelOption(o => + o.setName('pingchannel') + .setDescription('The channel to send the water reminder in') + .setRequired(true)) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles), + async execute(interaction) { + try { + await interaction.deferReply({ ephemeral: true }); + const reminderMessage = interaction.options.getString('pingmsg'); + const reminderChannel = interaction.options.getChannel('pingchannel'); + const setPingRoleResponse = await dbfn.setReminderInfo(interaction.guildId, reminderMessage, reminderChannel.id); + await dbfn.setReminderOptIn(interaction.guildId, 1); + interaction.editReply(setPingRoleResponse.status); + } catch(err) { + console.error(err); + await interaction.editReply(fn.builders.errorEmbed(err)); + } + }, +}; \ No newline at end of file diff --git a/slash-commands/setping.js.bak b/slash-commands/setping.js.bak new file mode 100644 index 0000000..4d13791 --- /dev/null +++ b/slash-commands/setping.js.bak @@ -0,0 +1,18 @@ +const { SlashCommandBuilder } = require('discord.js'); +const dbfn = require('../modules/dbfn.js'); +const fn = require('../modules/functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('setping') + .setDescription('Run this command when you water your tree to have a reminder sent.'), + async execute(interaction) { + await interaction.deferReply({ ephemeral: true }); + const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); + const guildInfo = getGuildInfoResponse.data; + const reminderTimeS = fn.getWaterTime(guildInfo.treeHeight); + const reminderTimeMs = reminderTimeS * 1000; + fn.setReminder(interaction, reminderTimeMs, guildInfo.pingRoleId); + interaction.editReply("A reminder has been set."); + }, +}; \ 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 diff --git a/slash-commands/timetoheight.js b/slash-commands/timetoheight.js index 6b62a57..f065c86 100644 --- a/slash-commands/timetoheight.js +++ b/slash-commands/timetoheight.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,14 +7,19 @@ module.exports = { .setName('timetoheight') .setDescription('Calculate how long it would take to reach a given height') .addStringOption(o => - o.setName('height') - .setDescription('Tree height in feet, numbers ONLY') + o.setName('beginheight') + .setDescription('Begining tree height in feet, numbers ONLY') + .setRequired(true)) + .addStringOption(o => + o.setName('endheight') + .setDescription('Ending tree height in feet, numbers ONLY') .setRequired(true)), async execute(interaction) { - await interaction.deferReply(); - const destTreeHeight = interaction.options.getString('height'); - fn.timeToHeight(interaction, destTreeHeight).then(res => { - interaction.editReply(`It will take you ${res} minutes to reach ${destTreeHeight}ft.`); + await interaction.deferReply({ ephemeral: true }); + const beginHeight = interaction.options.getString('beginheight'); + const endHeight = interaction.options.getString('endheight'); + fn.timeToHeight(beginHeight, endHeight).then(res => { + interaction.editReply(`It will take a tree that is ${beginHeight}ft tall ${res} to reach ${endHeight}ft.`); }).catch(err => { interaction.editReply("Error: " + err); console.error(err);