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
This commit is contained in:
Skylar Grant 2023-01-31 22:51:10 -05:00 committed by GitHub
parent e64fa099c1
commit 5ef905449d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1128 additions and 350 deletions

24
.github/workflows/docker-image-dev.yml vendored Normal file
View File

@ -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

View File

@ -1,8 +1,6 @@
name: Docker Image CI name: Silvanus Dockerization
on: on:
push:
branches: [ "main" ]
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ env.dev
env.prod env.prod
.DS_Store .DS_Store
data/guildInfo.json data/guildInfo.json
data/rawstring.txt
# Custom folders # Custom folders
# gifs/* # gifs/*

View File

@ -1,9 +1,7 @@
# Silvanus # 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. 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.
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.
Silvanus is not affiliated with Grow A Tree or Limbo Labs. 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) [Join Discord Server](https://discord.gg/g5JRGn7PxU)
## Setup ## 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 ## Permissions
Silvanus requires permissions to `Send Messages` and `Send Messages in Threads` if applicable. Silvanus requires permissions to `Send Messages` and `Send Messages in Threads` if applicable.
## Commands ## 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. * `/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]`) * `/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 (`[💧]`).
* `/watertime height` - Calculates the wait time between waters for a tree of a given height. * `/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. * `/reset` - Removes your server's configuration from the database.
* `/help` - Displays the bot's help page and links to each command. * `/help` - Displays the bot's help page and links to each command.

27
TODO.md Normal file
View File

@ -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)

View File

@ -4,10 +4,10 @@
}, },
"help": { "help": {
"title": "Silvanus 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 </help:1065346941166297129> and </setup:1065407649363005561>, then check out </compare:1065346941166297128>.\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 </tree:972648557796524032> and </top trees:1051840665362894950> message, otherwise make sure to run </setup:1065407649363005561> each time you run </compare:1065346941166297128>.", "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": "To begin analyzing your Tree, first you must set up the reference messages.\n\n1. Run </setup:1065407649363005561> in the channel(s) that contain your server's tree and leaderboard messages.\n2. Now simply run </compare:1065346941166297128> where you want your analysis to be visible.", "setup": "If your ``/tree`` and ``/top trees`` messages are in the same channel, simple run </compare:1065346941166297128> in that channel and you're good to go!\n\nOtherwise, run </setup:1065407649363005561> to set the proper channels for the bot to look in for the ``/tree`` and ``/top trees`` messages.\n\nUse </commands:1069501270454456331> 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.", "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": "</compare:1065346941166297128> | </setup:1065407649363005561> | </watertime:1066970330029113444> | </setupinfo:1065413032374706196> | </reset:1065412317052944476> | </help:1065346941166297129>" "allCommands": "</setup:1065407649363005561> - You only need to run this command if your server has its ``/tree`` and ``/top trees`` messages in separate channels.\n</setupinfo:1065413032374706196> - Displays links to the current Tree and Tallest Trees messages configured in your server.\n</compare:1065346941166297128> - 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</setping:1068373237995683902> - 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 </compare:1065346941166297128> message.\n Click the ``Reset Ping`` button to be sent a reminder the next time the tree is ready to be watered.\n Use </optout:1068753032801693758> to disable reminder messages for your server.\n</watertime:1066970330029113444> - Calculates the wait time between waters for a tree of a given height.\n</timetoheight:1067727254634889227> - Calculates how long it would take to go from ``beginheight`` to ``endheight``.\n</reset:1065412317052944476> - Removes your server's configuration from the database.\n</help:1065346941166297129> - Displays the bot's help page and links to each command."
}, },
"commands": { "commands": {
"compare": "</compare:1065346941166297128>", "compare": "</compare:1065346941166297128>",
@ -37,8 +37,12 @@
}, },
"status": { "status": {
"treeAndLeaderboard": "Tree and leaderboard messages were both found, setup is complete. Run </setupinfo:1065413032374706196> to verify. Run </compare:1065346941166297128> to get started!", "treeAndLeaderboard": "Tree and leaderboard messages were both found, setup is complete. Run </setupinfo:1065413032374706196> to verify. Run </compare:1065346941166297128> 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 </setupinfo:1065413032374706196> 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 </setup:1065407649363005561> to set up separate ``/tree`` and ``/top trees`` channels.",
"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 </setupinfo:1065413032374706196> to see if the message is set." "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 </setup:1065407649363005561> 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 </setup:1065407649363005561> 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 </setup:1065407649363005561> 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 </setup:1065407649363005561> to set the ``/tree`` and ``/top trees`` channels."
}, },
"temp": {} "temp": {}
} }

27
main.js
View File

@ -24,15 +24,19 @@ const client = new Client({
// Various imports // Various imports
const fn = require('./modules/functions.js'); const fn = require('./modules/functions.js');
const strings = require('./data/strings.json'); const strings = require('./data/strings.json');
const dbfn = require('./modules/dbfn.js');
const isDev = process.env.isDev; const isDev = process.env.isDev;
client.once('ready', () => { client.once('ready', () => {
fn.collections.slashCommands(client); fn.collections.slashCommands(client);
console.log('Ready!'); console.log('Ready!');
client.user.setActivity({ name: strings.activity.name, type: ActivityType.Watching }); client.user.setActivity({ name: strings.activity.name, type: ActivityType.Watching });
fn.checkReady(client);
if (isDev == 'false') {
client.channels.fetch(statusChannelId).then(channel => { client.channels.fetch(statusChannelId).then(channel => {
channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete`); channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete <@481933290912350209>`);
}); });
}
}); });
// slash-commands // slash-commands
@ -52,7 +56,26 @@ client.on('interactionCreate', async interaction => {
} }
if (interaction.isButton() && interaction.component.customId == 'refresh') { 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);
});
}
} }
}); });

View File

@ -0,0 +1,33 @@
/*
</setup:1065407649363005561>
</setupinfo:1065413032374706196>
</compare:1065346941166297128>
</setping:1068373237995683902>
</optout:1068753032801693758>
</watertime:1066970330029113444>
</timetoheight:1067727254634889227>
</reset:1065412317052944476>
</help:1065346941166297129>
</tree:972648557796524032>
</top trees:1051840665362894950>
*/
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`', '</setup:1065407649363005561>');
newString = replaceAll(newString, '`/setupinfo`', '</setupinfo:1065413032374706196>');
newString = replaceAll(newString, '`/compare`', '</compare:1065346941166297128>');
newString = replaceAll(newString, '`/setping`', '</setping:1068373237995683902>');
newString = replaceAll(newString, '`/optout`', '</optout:1068753032801693758>');
newString = replaceAll(newString, '`/watertime`', '</watertime:1066970330029113444>');
newString = replaceAll(newString, '`/timetoheight`', '</timetoheight:1067727254634889227>');
newString = replaceAll(newString, '`/reset`', '</reset:1065412317052944476>');
newString = replaceAll(newString, '`/help`', '</help:1065346941166297129>');
newString = replaceAll(newString, '`/commands`', '</commands:1069501270454456331>');
newString = replaceAll(newString, '`', '``');
fs.writeFileSync('./data/rawstring.txt', newString);
return "Done";

View File

@ -35,11 +35,11 @@ leaderboard
module.exports = { module.exports = {
createGuildTables(guildId) { createGuildTables(guildId) {
const db = mysql.createConnection({ const db = mysql.createConnection({
host : process.env.DBHOST, host: process.env.DBHOST,
user : process.env.DBUSER, user: process.env.DBUSER,
password : process.env.DBPASS, password: process.env.DBPASS,
database : process.env.DBNAME, database: process.env.DBNAME,
port : process.env.DBPORT port: process.env.DBPORT
}); });
db.connect((err) => { db.connect((err) => {
if (err) throw `Error connecting to the database: ${err.message}`; if (err) throw `Error connecting to the database: ${err.message}`;
@ -71,17 +71,17 @@ module.exports = {
}, },
getGuildInfo(guildId) { getGuildInfo(guildId) {
const db = mysql.createConnection({ const db = mysql.createConnection({
host : process.env.DBHOST, host: process.env.DBHOST,
user : process.env.DBUSER, user: process.env.DBUSER,
password : process.env.DBPASS, password: process.env.DBPASS,
database : process.env.DBNAME, database: process.env.DBNAME,
port : process.env.DBPORT port: process.env.DBPORT
}); });
db.connect((err) => { db.connect((err) => {
if (err) throw `Error connecting to the database: ${err.message}`; if (err) throw `Error connecting to the database: ${err.message}`;
}); });
// Get a server's tree information from the database // 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)}`; 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 } // 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) => { return new Promise((resolve, reject) => {
db.query(selectGuildInfoQuery, (err, res) => { db.query(selectGuildInfoQuery, (err, res) => {
@ -97,7 +97,11 @@ module.exports = {
"treeMessageId": "123", "treeMessageId": "123",
"treeChannelId": "123", "treeChannelId": "123",
"leaderboardMessageId": "123", "leaderboardMessageId": "123",
"leaderboardChannelId": "123" "leaderboardChannelId": "123",
"reminderMessage": "Abc",
"reminderChannelId": "123",
"remindedStatus": 0,
"comparisonMessageId": "123"
};*/ };*/
if (res.length == 0) { if (res.length == 0) {
reject("There is no database entry for your guild yet. Try running /setup"); reject("There is no database entry for your guild yet. Try running /setup");
@ -105,13 +109,20 @@ module.exports = {
return; return;
} }
row = res[0]; row = res[0];
const guildInfo = { "guildId": row.guild_id, const guildInfo = {
"guildId": guildId,
"treeName": row.tree_name, "treeName": row.tree_name,
"treeHeight": row.tree_height, "treeHeight": row.tree_height,
"treeMessageId": row.tree_message_id, "treeMessageId": row.tree_message_id,
"treeChannelId": row.tree_channel_id, "treeChannelId": row.tree_channel_id,
"leaderboardMessageId": row.leaderboard_message_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(); db.end();
resolve({ "status": "Successfully fetched guild information", "data": guildInfo }); resolve({ "status": "Successfully fetched guild information", "data": guildInfo });
@ -120,11 +131,11 @@ module.exports = {
}, },
setGuildInfo(guildInfo) { setGuildInfo(guildInfo) {
const db = mysql.createConnection({ const db = mysql.createConnection({
host : process.env.DBHOST, host: process.env.DBHOST,
user : process.env.DBUSER, user: process.env.DBUSER,
password : process.env.DBPASS, password: process.env.DBPASS,
database : process.env.DBNAME, database: process.env.DBNAME,
port : process.env.DBPORT port: process.env.DBPORT
}); });
db.connect((err) => { db.connect((err) => {
if (err) throw `Error connecting to the database: ${err.message}`; if (err) throw `Error connecting to the database: ${err.message}`;
@ -149,11 +160,11 @@ module.exports = {
}, },
setTreeInfo(guildInfo) { setTreeInfo(guildInfo) {
const db = mysql.createConnection({ const db = mysql.createConnection({
host : process.env.DBHOST, host: process.env.DBHOST,
user : process.env.DBUSER, user: process.env.DBUSER,
password : process.env.DBPASS, password: process.env.DBPASS,
database : process.env.DBNAME, database: process.env.DBNAME,
port : process.env.DBPORT port: process.env.DBPORT
}); });
db.connect((err) => { db.connect((err) => {
if (err) throw `Error connecting to the database: ${err.message}`; if (err) throw `Error connecting to the database: ${err.message}`;
@ -178,11 +189,11 @@ module.exports = {
}, },
setLeaderboardInfo(guildInfo) { setLeaderboardInfo(guildInfo) {
const db = mysql.createConnection({ const db = mysql.createConnection({
host : process.env.DBHOST, host: process.env.DBHOST,
user : process.env.DBUSER, user: process.env.DBUSER,
password : process.env.DBPASS, password: process.env.DBPASS,
database : process.env.DBNAME, database: process.env.DBNAME,
port : process.env.DBPORT port: process.env.DBPORT
}); });
db.connect((err) => { db.connect((err) => {
if (err) throw `Error connecting to the database: ${err.message}`; if (err) throw `Error connecting to the database: ${err.message}`;
@ -207,11 +218,11 @@ module.exports = {
}, },
deleteGuildInfo(guildId) { deleteGuildInfo(guildId) {
const db = mysql.createConnection({ const db = mysql.createConnection({
host : process.env.DBHOST, host: process.env.DBHOST,
user : process.env.DBUSER, user: process.env.DBUSER,
password : process.env.DBPASS, password: process.env.DBPASS,
database : process.env.DBNAME, database: process.env.DBNAME,
port : process.env.DBPORT port: process.env.DBPORT
}); });
db.connect((err) => { db.connect((err) => {
if (err) throw `Error connecting to the database: ${err.message}`; if (err) throw `Error connecting to the database: ${err.message}`;
@ -236,11 +247,11 @@ module.exports = {
}, },
getLeaderboard(guildId) { getLeaderboard(guildId) {
const db = mysql.createConnection({ const db = mysql.createConnection({
host : process.env.DBHOST, host: process.env.DBHOST,
user : process.env.DBUSER, user: process.env.DBUSER,
password : process.env.DBPASS, password: process.env.DBPASS,
database : process.env.DBNAME, database: process.env.DBNAME,
port : process.env.DBPORT port: process.env.DBPORT
}); });
db.connect((err) => { db.connect((err) => {
if (err) throw `Error connecting to the database: ${err.message}`; if (err) throw `Error connecting to the database: ${err.message}`;
@ -272,11 +283,11 @@ module.exports = {
}, },
uploadLeaderboard(leaderboard) { uploadLeaderboard(leaderboard) {
const db = mysql.createConnection({ const db = mysql.createConnection({
host : process.env.DBHOST, host: process.env.DBHOST,
user : process.env.DBUSER, user: process.env.DBUSER,
password : process.env.DBPASS, password: process.env.DBPASS,
database : process.env.DBNAME, database: process.env.DBNAME,
port : process.env.DBPORT port: process.env.DBPORT
}); });
db.connect((err) => { db.connect((err) => {
if (err) throw `Error connecting to the database: ${err.message}`; if (err) throw `Error connecting to the database: ${err.message}`;
@ -300,5 +311,218 @@ module.exports = {
resolve({ "status": "Successfully uploaded the leaderboard", "data": res }); 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 });
});
});
} }
}; };

View File

@ -17,6 +17,7 @@ const config = require('../data/config.json');
const strings = require('../data/strings.json'); const strings = require('../data/strings.json');
const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => file.endsWith('.js')); const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => file.endsWith('.js'));
const dbfn = require('./dbfn.js'); const dbfn = require('./dbfn.js');
const { finished } = require('stream');
dbfn.createGuildTables().then(res => { dbfn.createGuildTables().then(res => {
console.log(res.status); console.log(res.status);
@ -41,27 +42,84 @@ const functions = {
} }
}, },
builders: { 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 // Create the button to go in the Action Row
const refreshButton = new ButtonBuilder() const refreshButton = new ButtonBuilder()
.setCustomId('refresh') .setCustomId('refresh')
.setLabel('Refresh') .setLabel('Refresh')
.setStyle(ButtonStyle.Primary); .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 // Create the Action Row with the Button in it, to be sent with the Embed
const refreshActionRow = new ActionRowBuilder() let refreshActionRow = new ActionRowBuilder()
.addComponents( .addComponents(
refreshButton refreshButton
); );
const getGuildInfoResponse = await dbfn.getGuildInfo(guildId);
const guildInfo = getGuildInfoResponse.data;
if (guildInfo.reminderMessage != "" && guildInfo.reminderChannelId != "") {
refreshActionRow.addComponents(resetPingButton);
}
return refreshActionRow; return refreshActionRow;
}, },
comparisonEmbed(content, refreshActionRow) { comparisonEmbed(content, guildInfo) {
// Create the embed using the content passed to this function // Create the embed using the content passed to this function
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(strings.embeds.color) .setColor(strings.embeds.color)
.setTitle('Tree Growth Comparison') .setTitle('Tallest Trees Comparison')
.setDescription(content) .setDescription(content)
.setFooter({text: `v${package.version} - ${strings.embeds.footer}`}); .setFooter({ text: `v${package.version} - ${strings.embeds.footer}` });
const messageContents = { embeds: [embed], components: [refreshActionRow] }; 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; return messageContents;
}, },
helpEmbed(content, private) { helpEmbed(content, private) {
@ -94,10 +152,8 @@ const functions = {
} }
}, },
rankings: { rankings: {
parse(interaction) { parse(interaction, guildInfo) {
return new Promise ((resolve, reject) => { return new Promise((resolve, reject) => {
dbfn.getGuildInfo(interaction.guildId).then(res => {
const guildInfo = res.data;
if (guildInfo.guildId == "") { if (guildInfo.guildId == "") {
reject(strings.error.noGuild); reject(strings.error.noGuild);
return; return;
@ -105,7 +161,7 @@ const functions = {
if (guildInfo.leaderboardMessageId != undefined) { if (guildInfo.leaderboardMessageId != undefined) {
interaction.guild.channels.fetch(guildInfo.leaderboardChannelId).then(c => { interaction.guild.channels.fetch(guildInfo.leaderboardChannelId).then(c => {
c.messages.fetch(guildInfo.leaderboardMessageId).then(leaderboardMessage => { c.messages.fetch(guildInfo.leaderboardMessageId).then(leaderboardMessage => {
if ((leaderboardMessage.embeds.length == 0) || (leaderboardMessage.embeds[0].data.title != 'Tallest Trees' )) { if ((leaderboardMessage.embeds.length == 0) || (leaderboardMessage.embeds[0].data.title != 'Tallest Trees')) {
reject("This doesn't appear to be a valid ``/top trees`` message."); reject("This doesn't appear to be a valid ``/top trees`` message.");
return; return;
} }
@ -153,78 +209,80 @@ const functions = {
} }
dbfn.uploadLeaderboard(leaderboard).then(res => { dbfn.uploadLeaderboard(leaderboard).then(res => {
console.log(res.status);
resolve(res.status); resolve(res.status);
}).catch(err => { }).catch(err => {
console.error(err); console.error(err);
reject(err); reject(err);
return; return;
}); });
}).catch(err => {
reject(strings.status.missingLeaderboardMessage);
console.error(err);
return;
}); });
}).catch(err => {
reject(strings.status.missingLeaderboardChannel);
console.error(err);
return;
}); });
} else { } else {
reject("The leaderboardMessageId is undefined somehow"); reject("The leaderboardMessageId is undefined somehow");
return; return;
} }
}).catch(err => {
reject(err);
return;
});
}); });
}, },
compare(interaction) { async compare(interaction, guildInfo) {
return new Promise((resolve, reject) => { try {
dbfn.getGuildInfo(interaction.guildId).then(res => { const getLeaderboardResponse = await dbfn.getLeaderboard(interaction.guildId);
const guildInfo = res.data; const leaderboard = getLeaderboardResponse.data; // [ { treeName: "Name", treeHeight: 1234.5, treeRank: 67 }, {...}, {...} ]
guildInfo.guildId = interaction.guildId;
let treeHeight = parseFloat(guildInfo.treeHeight).toFixed(1); // Prepare the beginning of the comparison message
dbfn.getLeaderboard(interaction.guildId).then(res => { let comparisonReplyString = `Here\'s how your tree compares: \nCurrent Tree Height: ${guildInfo.treeHeight}ft\n\n`;
const leaderboard = res.data; // 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 += "``[💧]``";
let replyString = 'Current Tree Height: ' + treeHeight + 'ft\n\n'; // Get the data for this tree from 24 hours ago
leaderboard.reverse().forEach(treeRanking => { // const get24hTreeResponse = await dbfn.get24hTree(interaction.guildId, leaderboardEntry.treeName);
let difference = parseFloat(treeRanking.treeHeight).toFixed(1) - treeHeight; // const dayAgoTree = get24hTreeResponse.data;
let decimal = (treeRanking.treeHeight % 1).toFixed(1); // const hist24hDifference = (leaderboardEntry.treeHeight - dayAgoTree.treeHeight).toFixed(1);
let growthIndicator = ""; // statusIndicator += `+${hist24hDifference}ft|`
if (decimal > 0) {
growthIndicator += "[+]"; // Get the 24h watering time for this tree
} // const totalWaterTime = await functions.timeToHeight(dayAgoTree.treeHeight, leaderboardEntry.treeHeight);
const absDifference = parseFloat(Math.abs(difference)).toFixed(1); // statusIndicator += `${totalWaterTime}]\`\``;
if (treeRanking.hasPin) {
replyString += "This is your tree. "; // Determine if this tree is the guild's tree
} else if ((treeRanking.treeHeight == treeHeight) && (treeRanking.treeName == guildInfo.treeName)) { if (leaderboardEntry.hasPin) {
replyString += "This might be your tree. Same height, same name. "; 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 { } else {
if (difference > 0) { comparisonReplyString += `#${leaderboardEntry.treeRank} - ${Math.abs(currentHeightDifference).toFixed(1)}ft shorter`;
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`; // Build a string using the current leaderboard entry and the historic entry from 24 hours ago
}); comparisonReplyString += `${statusIndicator}\n`;
resolve('Here\'s how your tree compares: \n' + replyString); // if (process.env.isDev == 'true') comparisonReplyString += `Current Height: ${leaderboardEntry.treeHeight} 24h Ago Height: ${dayAgoTree.treeHeight}\n`;
}).catch(err => { }
console.error(err); return comparisonReplyString;
}); } catch (err) {
}).catch(err => { throw err;
reject(err); }
return;
});
});
} }
}, },
tree: { tree: {
parse(interaction) { parse(interaction, guildInfo) {
let input; let input;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
dbfn.getGuildInfo(interaction.guildId).then(res => {
const guildInfo = res.data;
guildInfo.guildId = interaction.guildId;
if (guildInfo == undefined) { if (guildInfo == undefined) {
reject(`The guild entry hasn't been created yet. [${interaction.guildId || interaction.commandGuildId}]`); reject(`The guild entry hasn't been created yet. [${interaction.guildId || interaction.commandGuildId}]`);
return; return;
@ -232,7 +290,7 @@ const functions = {
if (guildInfo.treeMessageId != "Run /setup where your tree is.") { if (guildInfo.treeMessageId != "Run /setup where your tree is.") {
interaction.guild.channels.fetch(guildInfo.treeChannelId).then(c => { interaction.guild.channels.fetch(guildInfo.treeChannelId).then(c => {
c.messages.fetch(guildInfo.treeMessageId).then(m => { c.messages.fetch(guildInfo.treeMessageId).then(m => {
if ( (m.embeds.length == 0) || !(m.embeds[0].data.description.includes('Your tree is')) ) { 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."); reject("This doesn't appear to be a valid ``/tree`` message.");
return; return;
} }
@ -244,38 +302,183 @@ const functions = {
dbfn.setTreeInfo(guildInfo).then(res => { dbfn.setTreeInfo(guildInfo).then(res => {
resolve("The reference tree message has been saved/updated."); 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 { } else {
console.error('treeMessageId undefined'); console.error('treeMessageId undefined');
reject("There was an unknown error while setting the tree message."); reject("There was an unknown error while setting the tree message.");
return; return;
} }
}).catch(err => {
reject(err);
console.error(err);
return;
});
}); });
} }
}, },
refresh(interaction) { messages: {
functions.rankings.parse(interaction).then(r1 => { async find(interaction, guildInfo) {
functions.tree.parse(interaction).then(r2 => { try {
functions.rankings.compare(interaction).then(res => { let response = { status: "Incomplete", data: undefined, code: 0 };
const embed = functions.builders.comparisonEmbed(res, functions.builders.refreshAction()) // If the tree channel ID and leaderboard channel ID are both set
interaction.update(embed); if (guildInfo.treeChannelId != "" || guildInfo.leaderboardChannelId != "") {
}).catch(err => { // If one us unset, we'll set it to the current channel just to check
console.error(err); if (guildInfo.treeChannelId == "") {
}); guildInfo.treeChannelId = `${guildInfo.leaderboardChannelId}`;
}).catch(e => { } else if (guildInfo.leaderboardChannelId == "") {
console.error(e); guildInfo.leaderboardChannelId = `${guildInfo.treeChannelId}`;
interaction.reply(functions.builders.errorEmbed(e)); }
}); let treeFound, leaderboardFound = false;
}).catch(e => { // If these values have already been set in the database, we don't want to report that they weren't found
console.error(e); // they'll still get updated later if applicable.
interaction.reply(functions.builders.errorEmbed(e)); 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);
}); });
} else {
await interaction.update(this.builders.errorEmbed(findMessagesResponse.status));
}
}, },
reset(guildId) { reset(guildId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -304,27 +507,140 @@ const functions = {
}); });
}, },
getWaterTime(size) { 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)); // Seconds
return (Math.floor((Math.pow(size * 0.07 + 5, 1.1))) / 60).toFixed(2);
}, },
timeToHeight(interaction, destHeight) { timeToHeight(beginHeight, destHeight) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
dbfn.getGuildInfo(interaction.guildId).then(res => {
let guildInfo = res.data;
let currentHeight = parseInt(guildInfo.treeHeight);
let time = 0; let time = 0;
for (let i = currentHeight; i < destHeight; i++) { for (let i = beginHeight; i < destHeight; i++) {
const waterTime = parseFloat(functions.getWaterTime(i)); const waterTime = parseFloat(functions.getWaterTime(i));
console.log("Height: " + i + "Time: " + waterTime); // console.log("Height: " + i + "Time: " + waterTime);
time += waterTime; time += waterTime;
} }
resolve(time.toFixed(2));
// 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 => { }).catch(err => {
console.error(err); console.error(err);
reject(err);
return;
})
}); });
},
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("<t:") + 3;
const endWaterTimestamp = description.indexOf(":>");
// Split the description starting at "<t:" and ending 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);
} }
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "silvanus", "name": "silvanus",
"version": "1.1.1", "version": "1.1.4",
"description": "Grow A Tree Comparison Tool", "description": "Grow A Tree Comparison Tool",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@ -19,6 +19,8 @@
"dependencies": { "dependencies": {
"discord.js": "^14.7.1", "discord.js": "^14.7.1",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"mysql": "^2.18.1" "mysql": "^2.18.1",
"string-replace-all": "^2.0.0",
"string.prototype.replaceall": "^1.0.7"
} }
} }

View File

@ -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);
},
};

View File

@ -1,4 +1,5 @@
const { SlashCommandBuilder } = require('discord.js'); const { SlashCommandBuilder } = require('discord.js');
const dbfn = require('../modules/dbfn.js');
const fn = require('../modules/functions.js'); const fn = require('../modules/functions.js');
module.exports = { module.exports = {
@ -6,18 +7,69 @@ module.exports = {
.setName('compare') .setName('compare')
.setDescription('See how your tree compares to other trees!'), .setDescription('See how your tree compares to other trees!'),
async execute(interaction) { async execute(interaction) {
interaction.deferReply().then(() => { try {
fn.rankings.compare(interaction).then(res => { await interaction.deferReply();
const embed = fn.builders.comparisonEmbed(res, fn.builders.refreshAction()); // Get the guildInfo from the database
interaction.editReply(embed).catch(err => { dbfn.getGuildInfo(interaction.guildId).then(async getGuildInfoResponse => {
console.error(err); 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);
}); });
}).catch(err => { } 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 => { interaction.editReply(fn.builders.errorEmbed(err)).catch(err => {
console.error(err); console.error(err);
}); });
console.error(err); console.error(err);
}); }
})
}, },
}; };

View File

@ -14,7 +14,7 @@ module.exports = {
{ name: "True", value: "true" }, { name: "True", value: "true" },
{ name: "False", value: "false" })), { name: "False", value: "false" })),
execute(interaction) { 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 </commands:0> for a list of all my commands\n\n**Support Server**\n${strings.urls.supportServer}`, interaction.options.getString('private'));
interaction.reply(helpEmbed); interaction.reply(helpEmbed);
}, },
}; };

20
slash-commands/optout.js Normal file
View File

@ -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));
}
},
};

31
slash-commands/setping.js Normal file
View File

@ -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));
}
},
};

View File

@ -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.");
},
};

View File

@ -6,62 +6,32 @@ const dbfn = require('../modules/dbfn.js');
module.exports = { module.exports = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('setup') .setName('setup')
.setDescription('Attempt automatic configuration of the bot.'), .setDescription('Attempt automatic configuration of the bot.')
execute(interaction) { .addChannelOption(o =>
interaction.deferReply({ ephemeral: true }).then(function () { o.setName('treechannel')
/*const guildInfo = { "guildId": "123", .setDescription('What channel is your tree in?')
"treeName": "name", .setRequired(true))
"treeHeight": 123, .addChannelOption(o =>
"treeMessageId": "123", o.setName('leaderboardchannel')
"treeChannelId": "123", .setDescription('If your leaderboard isn\'t in the same channel, where is it?')
"leaderboardMessageId": "123", .setRequired(false)),
"leaderboardChannelId": "123" async execute(interaction) {
};*/ await interaction.deferReply({ ephemeral: true });
const guildInfo = { "guildId": interaction.guildId, /**/
"treeName": "name", let guildInfo = {
"treeHeight": 123, "guildId": interaction.guildId,
"treeMessageId": "123", "treeName": "",
"treeChannelId": "123", "treeHeight": 0,
"leaderboardMessageId": "123", "treeMessageId": "",
"leaderboardChannelId": "123" "treeChannelId": `${interaction.options.getChannel('treechannel').id }`,
"leaderboardMessageId": "",
"leaderboardChannelId": `${interaction.options.getChannel('leaderboardchannel').id || interaction.options.getChannel('treechannel').id }`,
"reminderMessage": "",
"reminderChannelId": "",
"remindedStatus": 0,
"reminderOptIn": 0,
}; };
interaction.channel.messages.fetch({ limit: 20 }).then(function (msgs) { const findMessagesResponse = await fn.messages.find(interaction, guildInfo);
let treeFound = false; interaction.editReply(findMessagesResponse.status);
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);
});
}
});
});
}, },
}; };

View File

@ -1,4 +1,5 @@
const { SlashCommandBuilder } = require('discord.js'); const { SlashCommandBuilder } = require('discord.js');
const dbfn = require('../modules/dbfn.js');
const fn = require('../modules/functions.js'); const fn = require('../modules/functions.js');
module.exports = { module.exports = {
@ -6,14 +7,19 @@ module.exports = {
.setName('timetoheight') .setName('timetoheight')
.setDescription('Calculate how long it would take to reach a given height') .setDescription('Calculate how long it would take to reach a given height')
.addStringOption(o => .addStringOption(o =>
o.setName('height') o.setName('beginheight')
.setDescription('Tree height in feet, numbers ONLY') .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)), .setRequired(true)),
async execute(interaction) { async execute(interaction) {
await interaction.deferReply(); await interaction.deferReply({ ephemeral: true });
const destTreeHeight = interaction.options.getString('height'); const beginHeight = interaction.options.getString('beginheight');
fn.timeToHeight(interaction, destTreeHeight).then(res => { const endHeight = interaction.options.getString('endheight');
interaction.editReply(`It will take you ${res} minutes to reach ${destTreeHeight}ft.`); 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 => { }).catch(err => {
interaction.editReply("Error: " + err); interaction.editReply("Error: " + err);
console.error(err); console.error(err);