Compare commits

...

28 Commits

Author SHA1 Message Date
Skylar Grant c99319ec75 Add guildInfos Collection 2023-02-05 20:59:43 -05:00
Skylar Grant c00d997c2b Add ability to get all guild informations 2023-02-05 20:54:56 -05:00
Skylar Grant b2fbb5012c Updates to water reminders 2023-01-31 22:30:20 -05:00
Skylar Grant 7dfe6a19af fix reminders 2023-01-31 17:23:26 -05:00
Skylar Grant 798b042c70 moar tweekz 2023-01-30 22:03:47 -05:00
Skylar Grant 1b0148a32f Adjust readiness detection system 2023-01-30 21:38:12 -05:00
Skylar Grant a1b9ea9fcb Tweak timings 2023-01-30 19:38:26 -05:00
Skylar Grant 73541fbec6 Changing the way reminders are deleted 2023-01-30 19:28:12 -05:00
Skylar Grant db7af90cae Documentation update 2023-01-30 19:27:59 -05:00
Skylar Grant f85bb536a7 Not meant to be uploaded 2023-01-30 01:13:15 -05:00
Skylar Grant 699652e97f Restructuring and new help messages 2023-01-30 01:12:26 -05:00
Skylar Grant c71154f1c9 Switch to ephemeral reply 2023-01-28 17:42:28 -05:00
Skylar Grant 08f721aa70 Forgot to allow checking in prod 2023-01-28 00:30:57 -05:00
Skylar Grant 5d8aaf85c2 Readability improvements 2023-01-28 00:03:42 -05:00
Skylar Grant a456610c7c Fix linebreaks 2023-01-28 00:02:31 -05:00
Skylar Grant d1e76692c3 Fix slash command link 2023-01-27 23:50:34 -05:00
Skylar Grant ae2bb9467b Documentation Time 2023-01-27 23:41:04 -05:00
Skylar Grant 0f83678bfa New water readiness system 2023-01-27 20:57:40 -05:00
Skylar Grant 431b244997 Improved workflows 2023-01-27 20:57:27 -05:00
Skylar Grant a8ceecff9a Setup automatic water reminders 2023-01-27 18:14:01 -05:00
Skylar Grant 9b2ad681b8 Add a ping reminder and setup command for it 2023-01-26 22:31:48 -05:00
Skylar Grant 112c671435 Changed startup message to ping me 2023-01-26 21:31:02 -05:00
Skylar Grant 74b65a1978 Add beginning height option 2023-01-26 21:30:52 -05:00
Skylar Grant a69bca9259 Add 24 hour observed growth 2023-01-26 21:30:41 -05:00
Skylar Grant f5daa186ac Trim decimals 2023-01-25 23:19:00 -05:00
Skylar Grant d6eac632e8 Fix some bugs 2023-01-25 23:17:07 -05:00
Skylar Grant 2627cc24b1 Fix float math 2023-01-25 23:14:40 -05:00
Skylar Grant 82c20dda68 Add 24h growth indicator 2023-01-25 23:11:49 -05:00
19 changed files with 1219 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:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

1
.gitignore vendored
View File

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

View File

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

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": {
"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>.",
"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.",
"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 </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.",
"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": {
"compare": "</compare:1065346941166297128>",
@ -37,8 +37,12 @@
},
"status": {
"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.",
"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."
"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.",
"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": {}
}

31
main.js
View File

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

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,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,107 @@ 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) {
},
getAllGuildInfos() {
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 selectAllGuildInfosQuery = `SELECT * FROM guild_info`;
// 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(selectAllGuildInfosQuery, (err, res) => {
if (err) {
console.error(err);
reject("Error fetching all guilds 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) {
reject("There are no guilds set up yet.");
db.end();
return;
}
let guildInfos = new Array();
for (let i = 0; i < res.length; i++) {
let row = res[i];
let guildInfo = {
"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,
"reminderOptIn": row.reminder_optin,
"comparisonMessageId": row.comparison_message_id,
"comparisonChannelId": row.comparison_channel_id
};
guildInfos.push(guildInfo);
}
db.end();
resolve({ "status": "Successfully fetched all guilds information", "data": guildInfos });
});
});
},
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
});
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 +224,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 +253,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 +282,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 +312,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 +347,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 +378,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 });
});
});
}
};

View File

@ -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);
@ -29,39 +30,120 @@ const functions = {
collections: {
// Create the collection of slash commands
slashCommands(client) {
// Create the Collection if it doesn't already exist
if (!client.slashCommands) client.slashCommands = new Discord.Collection();
// Empty the Collection to make sure there's no duplication of data
client.slashCommands.clear();
// Iterate over every *.js file in ./slash-commands
for (const file of slashCommandFiles) {
// Require the file so we can reference it and save it to the Collection
const slashCommand = require(`../slash-commands/${file}`);
// Make sure that the file has a data export, otherwise it's not valid
if (slashCommand.data != undefined) {
// Set a new key, value pair in the Collection
// The Key is the command name, the Value is the command itself
client.slashCommands.set(slashCommand.data.name, slashCommand);
}
}
if (isDev) console.log('Slash Commands Collection Built');
},
async guildInfos(client) {
// Build a Collection of all guilds and their guildInfo from the database
try {
// Create the collection if it doesn't already exist
if (!client.guildInfos) client.guildInfos = new Discord.Collection();
// Empty the collection to prevent duplication of data if the Collection already existed
client.guildInfos.clear();
const getAllGuildInfosResponse = await dbfn.getAllGuildInfos();
const guildInfos = getAllGuildInfosResponse.data;
for (const guildInfo of guildInfos) {
client.guildInfos.set(guildInfo.guildId, guildInfo);
}
if (isDev) console.log("guildInfos Collection Built");
} catch(err) {
throw "There was a problem getting all guilds information: " + err;
}
}
},
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 +176,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 +531,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("<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",
"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"
}
}

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

View File

@ -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 </commands:0> for a list of all my commands\n\n**Support Server**\n${strings.urls.supportServer}`, interaction.options.getString('private'));
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 = {
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);
},
};

View File

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