Compare commits

...

22 Commits

Author SHA1 Message Date
Skylar Grant
51dbff6f28
Catch errors 2023-03-11 14:47:54 -05:00
Skylar Grant
dbd66c248c Testing new tree update detection 2023-03-10 21:16:32 -05:00
Skylar Grant
3eaa7db561 Improvements to timetoheights calculations display 2023-02-26 16:11:05 -05:00
Skylar Grant
a0822cb002 Versioning 2023-02-26 12:41:48 -05:00
Skylar Grant
03202db21d Stricter check 2023-02-26 12:30:55 -05:00
Skylar Grant
15a1093aa3 Add privacy switch 2023-02-26 12:26:52 -05:00
Skylar Grant
b880503059 Some more logging 2023-02-26 12:18:44 -05:00
Skylar Grant
915ca4bf7c Add percentage modifiers to timetoheight 2023-02-26 12:09:24 -05:00
Skylar Grant
f4ecbf9ba3 Rate limits 2023-02-26 12:09:04 -05:00
Skylar Grant
291db33692 Stop checking ratelimits 2023-02-21 18:41:47 -05:00
Skylar Grant
1b0269c304 Documentation update 2023-02-20 15:17:37 -05:00
Skylar Grant
b2ff3030f9 Stop deleting commands 2023-02-20 15:15:08 -05:00
Skylar Grant
555d0436f8 Fix /setup compare 2023-02-20 15:14:57 -05:00
Skylar Grant
c2fe7bfa9a Documentation update 2023-02-20 15:14:41 -05:00
Skylar Grant
d3a04921d6 CI Update 2023-02-20 15:14:13 -05:00
Skylar Grant
3127305c03 Removed update event 2023-02-20 14:25:39 -05:00
Skylar Grant
989d26af83 Self-destructing water relay 2023-02-20 14:14:54 -05:00
Skylar Grant
30c43ee0cf Error update 2023-02-20 14:07:50 -05:00
Skylar Grant
6fe4456513 Bugfixes in notifications and update to relay 2023-02-20 14:07:38 -05:00
Skylar Grant
3912ccf40e Versioning 2023-02-19 21:46:01 -05:00
Skylar Grant
38a70d5457 Start collector when the command is run 2023-02-19 21:44:58 -05:00
Skylar Grant
53ccfa31d2 Disappearing fruit pings 2023-02-19 21:44:46 -05:00
15 changed files with 473 additions and 166 deletions

View File

@ -21,9 +21,3 @@ jobs:
run: docker login -u $DHUB_UNAME -p $DHUB_PWORD
- name: Push image to Docker Hub v0idf1sh/silvanus-dev
run: docker push v0idf1sh/silvanus-dev
- name: Set up a skeleton .env file
run: echo "TOKEN=${{secrets.DEVTOKEN}}" > .env && echo "BOTID=${{ secrets.CLIENTID }}" >> .env
- name: Install modules
run: npm i
- name: Refresh commands with Discord
run: node modules/_deploy-global.js

View File

@ -21,9 +21,3 @@ jobs:
run: docker login -u $DHUB_UNAME -p $DHUB_PWORD
- name: Push image to Docker Hub v0idf1sh/silvanus
run: docker push v0idf1sh/silvanus
- name: Set up a skeleton .env file
run: echo "TOKEN=${{secrets.PRODTOKEN}}" > .env && echo "clientId=${{ secrets.PRODCLIENTID }}" >> .env
- name: Install modules
run: npm i
- name: Refresh commands with Discord
run: node modules/_deploy-global.js

View File

@ -13,34 +13,24 @@ Silvanus is not affiliated with Grow A Tree or Limbo Labs.
## Setup
If your `/tree` and `/top trees` messages are in the same channel, simple run `/compare` in that channel and you're good to go!
If your `/tree` and `/top trees` messages are in the same channel, simply 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.
Otherwise, run `/setup compare` 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. If you plan to use the Role Menu Silvanus will also need permission to `Manage Roles`
Silvanus requires permissions to `Send Messages` and `Send Messages in Threads` if applicable. If you plan to use the Role Menu Silvanus will also need permission to `Manage Roles`.
## Commands
* `/setup` - You only need to run this command if your server has its `/tree` and `/top trees` messages in separate channels.
* `/setupinfo` - Displays your server's configuration information.
* `/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 (`[💧]`).
* `/notifications` - Guild members with the `Manage Roles` permission can run this command to set up automatic reminders when your tree is ready to be watered or when fruit is dropping.
* This feature relies on Grow A Tree's built-in Notification system. Refer to Grow A Tree for instructions on setting them up.
* `watchchannel`: Select the channel you've configured Grow A Tree to send notifications in.
* `watermessage`: This option sets the message to send when the tree is ready to be watered. This can include `@pings`, links, etc.
* `pingchannel`: Select the channel you want Silvanus to forward the notifications to.
* `fruitmessage`: Optional: This sets the message to send when the tree is dropping fruit. If not set, the `watermessage` will be used instead.
* `/rolemenu` - Creates a menu for users to give themselves Water and Fruit pingable roles.
* Requires `Manage Roles` permission to run.
* `waterrole`: Select the role to give users when they select the Water button
* `fruitrole`: Optional: Select the role to give users when they select the Fruit button
* If this option isn't set, no Fruit Role will be available for self-assignment.
* `/watertime` - Calculates the wait time between waters for a tree of a given height.
* `height`: The height in feet to calculate for.
* `/timetoheight` - Calculates how long it would take to grow to a height
* `endheight`: The destination height, in feet.
* `beginheight`: Optional: The starting height, in feet. If this option isn't set, the current height of your tree will be used insead.
* `/reset` - Removes your server's configuration from the database.
* `/help` - Displays the bot's help page and links to each command.
* `/compare` - Compare your tree to others on the leaderboard
* `/relay set` - Setup a Notification Relay for the first time
* `/relay update` - Update an already configured Notification Relay
* `/relay disable` - Disable the Notification Relay
* `/rolemenu` - Send a self-assignable role menu for relay pings
* `/watertime` - Calculates the time between waters for a tree of a given height
* `/timetoheight` - Calculates how long it would take a tree to grow to a given height
* `/setup compare` - Set the channels to use with `/compare`
* `/setup view` - View your server's configuration
* `/setup reset` - Delete your server's configuration
* `/help` - Displays the bot's help page

View File

@ -5,9 +5,9 @@
"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!\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.",
"setup": "If your </tree:972648557796524032> and </top trees:1051840665362894950> messages are in the same channel, simply run </compare:1065346941166297128> in that channel and you're good to go!\n\nOtherwise, run </setup compare:1065407649363005561> to set the proper channels for the bot to look in for the </tree:972648557796524032> and </top trees:1051840665362894950> messages.\n\nUse </commands:1077058896469966888> 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": "</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 your server's configuration information.\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</notifications:0> - Guild members with the ``Manage Roles`` permission can run this command to set up automatic reminders when your tree is ready to be watered or when fruit is dropping.\n This feature relies on Grow A Tree's built-in Notification system. Refer to Grow A Tree for instructions on setting them up.\n ``watchchannel``: Select the channel you've configured Grow A Tree to send notifications in.\n ``watermessage``: This option sets the message to send when the tree is ready to be watered. This can include ``@pings``, links, etc.\n ``pingchannel``: Select the channel you want Silvanus to forward the notifications to.\n ``fruitmessage``: Optional: This sets the message to send when the tree is dropping fruit. If not set, the ``watermessage`` will be used instead.\n</rolemenu:0> - Creates a menu for users to give themselves Water and Fruit pingable roles.\n Requires ``Manage Roles`` permission to run.\n ``waterrole``: Select the role to give users when they select the Water button\n ``fruitrole``: Optional: Select the role to give users when they select the Fruit button\n If this option isn't set, no Fruit Role will be available for self-assignment.\n</watertime:1066970330029113444> - Calculates the wait time between waters for a tree of a given height.\n ``height``: The height in feet to calculate for.\n</timetoheight:1067727254634889227> - Calculates how long it would take to grow to a height\n ``endheight``: The destination height, in feet.\n ``beginheight``: Optional: The starting height, in feet. If this option isn't set, the current height of your tree will be used insead.\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."
"allCommands": "</compare:1077058896469966889> - Compare your tree to others on the leaderboard\n</relay set:1077322799032578152> - Setup a Notification Relay for the first time\n</relay update:1077322799032578152> - Update an already configured Notification Relay\n</relay disable:1077322799032578152> - Disable the Notification Relay\n</rolemenu:1077058896469966892> - Send a self-assignable role menu for relay pings\n</watertime:1077058896469966895> - Calculates the time between waters for a tree of a given height\n</timetoheight:1077058896469966894> - Calculates how long it would take a tree to grow to a given height\n</setup compare:1077058896469966893> - Set the channels to use with </compare:1077058896469966889>\n</setup view:1077058896469966893> - View your server's configuration\n</setup reset:1077058896469966893> - Delete your server's configuration\n</help:1077058896469966890> - Displays the bot's help page"
},
"commands": {
"compare": "</compare:1065346941166297128>",
@ -70,5 +70,8 @@
"water": "is ready to be watered again!",
"fruit": "Fruit is appearing!"
},
"ids": {
"growATree": "972637072991068220"
},
"temp": {}
}

33
main.js
View File

@ -28,9 +28,10 @@ const dbfn = require('./modules/dbfn.js');
const isDev = process.env.DEBUG;
client.once('ready', async () => {
await fn.collectionBuilders.slashCommands(client);
fn.collectionBuilders.slashCommands(client);
await fn.collectionBuilders.guildInfos(client);
await fn.setupCollectors(client);
await fn.collectionBuilders.messageCollectors(client);
// checkRateLimits();
console.log('Ready!');
client.user.setActivity({ name: strings.activity.name, type: ActivityType.Watching });
if (isDev == 'false') {
@ -85,6 +86,34 @@ client.on('interactionCreate', async interaction => {
}
});
client.on('messageUpdate', async message => {
await fn.messages.updateHandler(message).catch(e => console.error(e));
});
async function checkRateLimits(hi) {
const axios = require('axios');
// Make a GET request to the Discord API
await axios.get('https://discord.com/api/v10/users/@me', {
headers: {
'Authorization': `Bot ${token}`
}
}).then(response => {
// Get the rate limit headers
const remaining = response.headers['x-ratelimit-remaining'];
const reset = response.headers['x-ratelimit-reset'];
// Log the rate limit headers
console.log(`Remaining requests: ${remaining}`);
console.log(`Reset time (Unix epoch seconds): ${reset}`);
}).catch(error => {
console.error(error);
});
await fn.sleep(500).then(async () =>{
await checkRateLimits();
})
}
process.on('unhandledRejection', error => {
console.error('Unhandled promise rejection:', error);
});

View File

@ -39,12 +39,12 @@ module.exports = {
return this;
}
setTreeMessage(messageId, channelId) {
this.treeMessageId = messageId;
this.treeMessageId = messageId ? messageId : this.treeMessageId;
this.treeChannelId = channelId;
return this;
}
setLeaderboardMessage(messageId, channelId) {
this.leaderboardMessageId = messageId;
this.leaderboardMessageId = messageId ? messageId : this.leaderboardMessageId;
this.leaderboardChannelId = channelId;
return this;
}
@ -122,7 +122,7 @@ module.exports = {
case "setTreeMessage":
queryParts = [
`UPDATE guild_info SET tree_message_id = ${db.escape(this.treeMessageId)}, `,
`tree_channel_id = ${db.escape(this.treeChannelId)}, `,
`tree_channel_id = ${db.escape(this.treeChannelId)} `,
`WHERE guild_id = ${db.escape(this.guildId)}`
];
return queryParts.join('');
@ -130,7 +130,7 @@ module.exports = {
case "setLeaderboardMessage":
queryParts = [
`UPDATE guild_info SET leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `,
`leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}, `,
`leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)} `,
`WHERE guild_id = ${db.escape(this.guildId)}`
];
return queryParts.join('');
@ -153,7 +153,17 @@ module.exports = {
}
return queryParts.join('');
break;
default:
case "setTreeInfo":
queryParts = [
`INSERT INTO guild_info (`,
`guild_id, tree_name, tree_height`,
`) VALUES (`,
`${db.escape(this.guildId)}, ${db.escape(this.treeName)}, ${db.escape(this.treeHeight)}`,
`) ON DUPLICATE KEY UPDATE tree_name = ${db.escape(this.treeName)}, `,
`tree_height = ${db.escape(this.treeHeight)}`
];
return queryParts.join('');
default:
break;
}
}

View File

@ -54,6 +54,6 @@ async function uploadCommands() {
}
(async () => {
await deleteCommands();
// await deleteCommands();
await uploadCommands();
})();

View File

@ -1,34 +1,29 @@
/*
</setup:1065407649363005561>
</setupinfo:1065413032374706196>
</compare:1065346941166297128>
</setping:1068373237995683902>
</optout:1068753032801693758>
</watertime:1066970330029113444>
</timetoheight:1067727254634889227>
</reset:1065412317052944476>
</help:1065346941166297129>
</tree:972648557796524032>
</top trees:1051840665362894950>
*/
const commands = [
{raw: "`/compare`", clickable: "</compare:1077058896469966889>"},
{raw: "`/relay set`", clickable: "</relay set:1077322799032578152>"},
{raw: "`/relay update`", clickable: "</relay update:1077322799032578152>"},
{raw: "`/relay disable`", clickable: "</relay disable:1077322799032578152>"},
{raw: "`/rolemenu`", clickable: "</rolemenu:1077058896469966892>"},
{raw: "`/watertime`", clickable: "</watertime:1077058896469966895>"},
{raw: "`/timetoheight`", clickable: "</timetoheight:1077058896469966894>"},
{raw: "`/setup compare`", clickable: "</setup compare:1077058896469966893>"},
{raw: "`/setup view`", clickable: "</setup view:1077058896469966893>"},
{raw: "`/setup reset`", clickable: "</setup reset:1077058896469966893>"},
{raw: "`/help`", clickable: "</help:1077058896469966890>"},
{raw: "`/tree`", clickable: "</tree:972648557796524032>"},
{raw: "`/top trees`", clickable: "</top trees:1051840665362894950>"},
{raw: "`/commands`", clickable: "</commands:1077058896469966888>"}
];
const fs = require('fs');
const replaceAll = require('string.prototype.replaceall');
const path = "./modules/input.txt";
const string = fs.readFileSync(path).toString();
let newString = replaceAll(string, '\* ', '');
newString = replaceAll(newString, '\r\n', '\\n');
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, '`/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, '`/notifications`', '</notifications:0>');
newString = replaceAll(newString, '`/rolemenu`', '</rolemenu:0>');
commands.forEach(command => {
newString = replaceAll(newString, command.raw, command.clickable);
});
newString = replaceAll(newString, '`', '``');
fs.writeFileSync(path, newString);
return "Done";

View File

@ -43,6 +43,17 @@ const functions = {
client.guildInfos.set(guildInfo.guildId, guildInfo);
}
return 'guildInfos Collection Built';
},
async messageCollectors(client) {
// Create an empty collection for MessageCollectors
if (!client.messageCollectors) client.messageCollectors = new Discord.Collection();
client.messageCollectors.clear();
// Get all of the guild infos from the client
const { guildInfos, messageCollectors } = client;
// Iterate over each guild info
await guildInfos.forEach(async guildInfo => {
await functions.collectors.create(client, guildInfo);
});
}
},
builders: {
@ -112,6 +123,16 @@ const functions = {
.setDescription(description)
.setFooter({ text: strings.embeds.roleMenuFooter });
return { embeds: [embed], components: [actionRow] };
},
information(content, fields) {
const embed = new EmbedBuilder()
.setColor(strings.embeds.color)
.setTitle('Information')
.setDescription(content)
.setFooter({ text: `v${package.version} - ${strings.embeds.footer}` });
if (fields) embed.addFields(fields);
const messageContents = { embeds: [embed], ephemeral: true };
return messageContents;
}
},
comparisonEmbed(content, guildInfo) {
@ -303,7 +324,6 @@ const functions = {
},
tree: {
parse(interaction, guildInfo) {
let input;
return new Promise((resolve, reject) => {
if (guildInfo == undefined) {
reject(`The guild entry hasn't been created yet. [${interaction.guildId || interaction.commandGuildId}]`);
@ -316,6 +336,7 @@ const functions = {
reject("This doesn't appear to be a valid ``/tree`` message.");
return;
}
let input;
input = m.embeds[0].data.description;
let treeName = m.embeds[0].data.title;
let lines = input.split('\n');
@ -472,12 +493,61 @@ const functions = {
},
isTree(message) {
if (message.embeds.length > 0) {
return message.embeds[0].data.description.includes("Your tree is");
// Grab the description and title
const {description, title} = message.embeds[0].data;
// Make sure it's a tree message
if (description.includes("Your tree is")) {
// Grab the name
const treeName = title;
// Grab the tree's height
const indices = [description.indexOf("Your tree is ") + 13, description.indexOf("ft")];
const treeHeightStr = description.slice(indices[0], indices[1]);
const treeHeightFloat = parseFloat(treeHeightStr).toFixed(1);
// Return the info gathered
return {
treeName: treeName,
treeHeight: treeHeightFloat
};
}
} else {
return false;
}
},
isLeaderboard(message) {
if (message.embeds.length > 0) {
return message.embeds[0].data.title == "Tallest Trees";
} else {
return false;
}
},
async updateHandler(message) {
if (message.partial) {
message = await message.fetch().catch(e => console.error(e));
}
// Make sure the message is from Grow A Tree
if (message.author.id != strings.ids.growATree) return;
// Check and store the message types
const isLeaderboard = this.isLeaderboard(message);
const isTree = this.isTree(message);
// Check if the message is a leaderboard
if (isLeaderboard) {
// Need to actually handle this later
// console.log("I've seen a leaderboard update.");
} else if (isTree) { // Check if the message is a tree
// console.log(`I've seen a tree update: ${isTree.treeName}: ${isTree.treeHeight}ft`);
let guildInfo;
if (message.client.guildInfos.has(message.guildId)) {
guildInfo = message.client.guildInfos.get(message.guildId);
guildInfo.setName(isTree.treeName)
.setHeight(isTree.treeHeight);
} else {
guildInfo = new GuildInfo().setId(message.guildId)
.setName(isTree.treeName)
.setHeight(isTree.treeHeight);
}
const query = guildInfo.queryBuilder("setTreeInfo");
await dbfn.setGuildInfo(query);
}
}
},
@ -528,6 +598,59 @@ const functions = {
await member.roles.remove(role).catch(err => console.error("Error taking the role: " + err + "\n" + JSON.stringify(role)));
}
},
collectors: {
async create(client, guildInfo) {
// If a collector is already setup
if (client.messageCollectors.has(guildInfo.guildId)) {
// Close the collector
await this.end(client, guildInfo);
}
// Make sure guildInfo is what we expect, the watch channel isnt blank, and notifications are enabled
if (guildInfo instanceof GuildInfo && guildInfo.watchChannelId != "" && guildInfo.notificationsEnabled) {
// Fetch the Guild
const guild = await client.guilds.fetch(guildInfo.guildId);
// Fetch the Channel
const channel = await guild.channels.fetch(guildInfo.watchChannelId);
// Create the filter function
const filter = message => {
// Discard any messages sent by Silvanus
return message.author.id != process.env.BOTID;
}
// Create the collector
const collector = channel.createMessageCollector({ filter });
// Add the collector to the messageCollectors Collection
client.messageCollectors.set(guildInfo.guildId, collector);
collector.on('collect', message => {
// Check for manual relay use with "water ping" and "fruit ping"
if (message.content.toLowerCase().includes("water ping")) {
functions.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild);
return;
} else if (message.content.toLowerCase().includes("fruit ping")) {
functions.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild);
return;
}
// If the message doesn't contain an embed, we can ignore it
if (message.embeds == undefined) return;
if (message.embeds.length == 0) return;
// Check the description field of the embed to determine if it matches Grow A Tree's notification texts
if (message.embeds[0].data.description.includes(strings.notifications.water)) {
functions.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild);
} else if (message.embeds[0].data.description.includes(strings.notifications.fruit)) {
functions.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild);
}
});
}
},
async end(client, guildInfo) {
if (!client.messageCollectors) throw "No Message Collectors";
if (!client.messageCollectors.has(guildInfo.guildId)) throw "Guild doesn't have a Message Collector";
const collector = client.messageCollectors.get(guildInfo.guildId);
// Close the collector
await collector.stop();
// Remove the collector from the messageCollectors Collection
client.messageCollectors.delete(guildInfo.guildId);
}
},
async refresh(interaction) {
// const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId);
// let guildInfo = getGuildInfoResponse.data;
@ -585,31 +708,48 @@ const functions = {
}
return `${waterParts.value} ${waterParts.units}`;
},
timeToHeight(beginHeight, destHeight) {
timeToHeight(beginHeight, destHeight, efficiency, quality) {
return new Promise((resolve, reject) => {
let time = 0;
for (let i = beginHeight; i < destHeight; i++) {
const waterTime = parseFloat(functions.getWaterTime(i));
// console.log("Height: " + i + "Time: " + waterTime);
time += waterTime;
let oldTime = 0;
let compostAppliedCount = 0;
let totalWaterCount = 0;
if ((efficiency) && (quality)) {
for (let i = beginHeight; i < destHeight; i++) {
const randNum = Math.floor(Math.random() * 100);
const compostApplied = randNum <= efficiency;
if (compostApplied) {
let qualityPercent = quality / 100;
let waterTime = functions.getWaterTime(i);
let reductionTime = waterTime * qualityPercent;
let finalTime = waterTime - reductionTime;
compostAppliedCount++;
totalWaterCount++;
time += parseFloat(finalTime);
oldTime += waterTime;
} else {
totalWaterCount++;
let waterTime = parseFloat(functions.getWaterTime(i));
time += waterTime;
oldTime += waterTime;
}
}
} else {
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);
const readableWaterTime = this.parseWaterTime(time);
const savedTime = this.parseWaterTime(oldTime - time);
resolve({
time: readableWaterTime,
totalWaterCount: totalWaterCount ? totalWaterCount : undefined,
compostAppliedCount: compostAppliedCount ? compostAppliedCount : undefined,
average: totalWaterCount ? parseFloat((compostAppliedCount / totalWaterCount) * 100).toFixed(1) : undefined,
savedTime: savedTime
});
});
},
sleep(ms) {
@ -624,22 +764,34 @@ const functions = {
async sendWaterReminder(guildInfo, message, channelId, guild) {
const reminderChannel = await guild.channels.fetch(channelId);
const reminderEmbed = functions.builders.waterReminderEmbed(message, guildInfo);
await reminderChannel.send(reminderEmbed).catch(err => {
console.log(`Water Relay: ${guild.name}: ${guildInfo.treeName}`);
await reminderChannel.send(reminderEmbed).then(async m => {
if (!m.deletable) return;
await this.sleep(500).then(async () => {
await m.delete().catch(e => console.error(e));
});
}).catch(err => {
console.error(err);
});
},
async sendFruitReminder(guildInfo, message, channelId, guild) {
const reminderChannel = await guild.channels.fetch(channelId);
const reminderEmbed = functions.builders.fruitReminderEmbed(message, guildInfo);
await reminderChannel.send(reminderEmbed).catch(err => {
console.log(`Fruit Relay: ${guild.name}: ${guildInfo.treeName}`);
await reminderChannel.send(reminderEmbed).then(async m => {
if (!m.deletable) return;
await this.sleep(500).then(async () => {
await m.delete().catch(e => console.error(e));
});
}).catch(err => {
console.error(err);
});
},
async setupCollectors(client) {
let guildInfos = client.guildInfos;
guildInfos.set("collectors", []);
let collectorsArray = [];
await guildInfos.forEach(async guildInfo => {
if ( guildInfo instanceof GuildInfo && guildInfo.watchChannelId != "" && guildInfo.notificationsEnabled) {
if (guildInfo instanceof GuildInfo && guildInfo.watchChannelId != "" && guildInfo.notificationsEnabled) {
const guild = await client.guilds.fetch(guildInfo.guildId);
// console.log(guildInfo instanceof GuildInfo);
const channel = await guild.channels.fetch(guildInfo.watchChannelId);
@ -666,6 +818,36 @@ const functions = {
});
}
});
guildInfos.set("collectors", collectorsArray);
},
async setupCollector(channel, interaction) {
if (interaction.client.guildInfos.has(interaction.guildId)) {
let collectors = interaction.client.guildInfos.get('collectors');
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
const filter = message => {
return message.author.id != process.env.BOTID;
}
const collector = channel.createMessageCollector({ filter });
collectors.push(collector);
collector.on('collect', message => {
if (message.content.toLowerCase().includes("water ping")) {
this.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild);
return;
} else if (message.content.toLowerCase().includes("fruit ping")) {
this.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild);
return;
}
if (message.embeds == undefined) return;
if (message.embeds.length == 0) return;
if (message.embeds[0].data.description.includes(strings.notifications.water)) {
this.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild);
} else if (message.embeds[0].data.description.includes(strings.notifications.fruit)) {
this.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild);
}
});
} else {
throw "Guild doesn't exist in database!";
}
}
};

View File

@ -0,0 +1,21 @@
information(content, fields) {
const embed = new EmbedBuilder()
.setColor(strings.embeds.color)
.setTitle('Information')
.setDescription(content)
.setFooter({ text: `v${package.version} - ${strings.embeds.footer}` });
if (fields) embed.addFields(fields);
const messageContents = { embeds: [embed], ephemeral: true };
return messageContents;
}
replyContent = `I estimate that a tree with ${efficiency}% Composter Efficiency and ${quality}% Compost Quality growing from ${beginHeight}ft to ${endHeight}ft will take ${time}`;
replyFields = [
{ name: `Start Height:`, value: `**${beginHeight}ft**`, inline: true },
{ name: `End Height:`, value: `**${endHeight}**`, inline: true },
{ name: `Efficiency:`, value: `**${efficiency}%**`, inline: true },
{ name: `Quality:`, value: `**${quality}%**`, inline: true },
{ name: `Summary`, value: `Compost Applied **${compostAppliedCount}** times, out of **${totalWaterCount}** waterings, for an average of **${average}%**` }
];
const reply = information(replyContent, replyFields);

View File

@ -1,6 +1,6 @@
{
"name": "silvanus",
"version": "1.2.1",
"version": "1.2.3",
"description": "Grow A Tree Companion Bot",
"main": "main.js",
"scripts": {
@ -17,6 +17,7 @@
},
"homepage": "https://github.com/voidf1sh/silvanus#readme",
"dependencies": {
"axios": "^1.3.3",
"discord.js": "^14.7.1",
"dotenv": "^16.0.3",
"mysql": "^2.18.1",

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

View File

@ -6,7 +6,7 @@ const strings = require('../data/strings.json');
module.exports = {
data: new SlashCommandBuilder()
.setName('notifications')
.setName('relay')
.setDescription('A notification relay for improved water and fruit notifications')
.addSubcommand(sc =>
sc.setName('set')
@ -67,75 +67,115 @@ module.exports = {
const subcommand = interaction.options.getSubcommand();
// if (process.env.DEBUG) console.log(`${typeof subcommand}: ${subcommand}`);
switch (subcommand) {
// Set all components for the first time
case "set":
// If there is already a guildInfo object for this server
if (interaction.client.guildInfos.has(interaction.guildId)) {
const watchChannel = interaction.options.getChannel('watchchannel');
const waterMessage = interaction.options.getString('watermessage');
const fruitMessage = interaction.options.getString('fruitmessage') ? interaction.options.getString('fruitmessage') : interaction.options.getString('watermessage');
const reminderChannel = interaction.options.getChannel('pingchannel');
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
guildInfo.setReminders(waterMessage, fruitMessage, reminderChannel.id, watchChannel.id, true);
let query = guildInfo.queryBuilder("setReminders");
await dbfn.setGuildInfo(query);
const replyParts = [
`I'll watch <#${watchChannel.id}> for Grow A Tree Notifications and relay them to <#${reminderChannel.id}>.`,
`Water Message: ${waterMessage}`
];
if (fruitMessage != "") replyParts.push(`Fruit Message: ${fruitMessage}`);
await interaction.editReply(replyParts.join("\n")).catch(e => console.error(e));
fn.collectionBuilders.guildInfos(interaction.client);
} else {
// Get options from the interaction
const watchChannel = interaction.options.getChannel('watchchannel');
const waterMessage = interaction.options.getString('watermessage');
// If the fruit message is set, use it, otherwise default to the water message.
const fruitMessage = interaction.options.getString('fruitmessage') ? interaction.options.getString('fruitmessage') : interaction.options.getString('watermessage');
const reminderChannel = interaction.options.getChannel('pingchannel');
// Set the reminder configuration in the GuildInfo object
guildInfo.setReminders(waterMessage, fruitMessage, reminderChannel.id, watchChannel.id, true);
// Update the guildInfos Collection
interaction.client.guildInfos.set(interaction.guildId, guildInfo);
// Build a query to update the database
let query = guildInfo.queryBuilder("setReminders");
// Run the query
await dbfn.setGuildInfo(query);
// Set up a collector on the watch channel
fn.collectors.create(interaction.client, guildInfo);
// Compose a reply
const reply = [
`I'll watch <#${watchChannel.id}> for Grow A Tree Notifications and relay them to <#${reminderChannel.id}>.`,
`Water Message: ${waterMessage}`,
`Fruit Message: ${fruitMessage}`
].join("\n");
// Send the reply
await interaction.editReply(fn.builders.embed(reply)).catch(e => console.error(e));
} else {
// Get options from the interaction
const watchChannel = interaction.options.getChannel('watchchannel');
const waterMessage = interaction.options.getString('watermessage');
// If the fruit message is set, use it. Otherwise default to the water message
const fruitMessage = interaction.options.getString('fruitmessage') ? interaction.options.getString('fruitmessage') : interaction.options.getString('watermessage');
const reminderChannel = interaction.options.getChannel('pingchannel');
// Create a new GuildInfo object
let guildInfo = new GuildInfo()
.setId(interaction.guildId)
// Set the reminder configuration
.setReminders(waterMessage, fruitMessage, reminderChannel.id, watchChannel.id, true);
// Update the guildInfos Collection
interaction.client.guildInfos.set(interaction.guildId, guildInfo);
// Build a query to update the database
let query = guildInfo.queryBuilder("setReminders");
// Run the query
await dbfn.setGuildInfo(query);
const replyParts = [
// Create a messageCollector on the watch channel
fn.collectors.create(interaction.client, guildInfo);
// Compose a reply
const reply = [
`I'll watch <#${watchChannel.id}> for Grow A Tree Notifications and relay them to <#${reminderChannel.id}>.`,
`Water Message: ${waterMessage}`
];
if (fruitMessage != "") replyParts.push(`Fruit Message: ${fruitMessage}`);
await interaction.editReply(replyParts.join("\n")).catch(e => console.error(e));
fn.collectionBuilders.guildInfos(interaction.client);
`Water Message: ${waterMessage}`,
`Fruit Message: ${fruitMessage}`
].join("\n");
// Send the reply
await interaction.editReply(reply).catch(e => console.error(e));
}
break;
case "update":
case "update": // Update the relay configuration piecemeal
if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
// Get all possible options from the interaction
const inWatchChannel = interaction.options.getChannel('watchchannel');
const inWaterMessage = interaction.options.getString('watermessage');
const inFruitMessage = interaction.options.getString('fruitmessage');
const inReminderChannel = interaction.options.getChannel('pingchannel');
// Check if each option is set, if it is, use it. Otherwise use what was already set
const outWatchChannelId = inWatchChannel ? inWatchChannel.id : guildInfo.watchChannelId;
const outWaterMessage = inWaterMessage ? inWaterMessage : guildInfo.waterMessage;
const outFruitMessage = inFruitMessage ? inFruitMessage : guildInfo.fruitMessage;
const outReminderChannelId = inReminderChannel ? inReminderChannel.id : guildInfo.reminderChannelId;
// Update the relay configuration
guildInfo.setReminders(outWaterMessage, outFruitMessage, outReminderChannelId, outWatchChannelId, true);
// Update the guildInfos Collection
interaction.client.guildInfos.set(interaction.guildId, guildInfo);
// Build a query to update the database
let query = guildInfo.queryBuilder("setReminders");
// Run the query
await dbfn.setGuildInfo(query);
const replyParts = [
// Create a messageCollector on the watch channel
fn.collectors.create(interaction.client, guildInfo);
// Compose a reply
const reply = [
`I'll watch <#${outWatchChannelId}> for Grow A Tree Notifications and relay them to <#${outReminderChannelId}>.`,
`Water Message: ${outWaterMessage}`
];
if (outFruitMessage != "") replyParts.push(`Fruit Message: ${outFruitMessage}`);
await interaction.editReply(replyParts.join("\n")).catch(e => console.error(e));
fn.collectionBuilders.guildInfos(interaction.client);
`Water Message: ${outWaterMessage}`,
`Fruit Message: ${outFruitMessage}`
].join("\n");
// Send the reply
await interaction.editReply(reply).catch(e => console.error(e));
} else {
await interaction.editReply(fn.builders.errorEmbed("There is no existing notification relay to update!")).catch(e => console.error(e));
}
break;
case 'disable':
case 'disable': // Disable the relay
if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
// Update the relay config with all undefined except for `enabled` which is false
guildInfo.setReminders(undefined, undefined, undefined, undefined, false);
// Update the guildInfos Collection
interaction.client.guildInfos.set(interaction.guildId, guildInfo);
// Update the database
await dbfn.setGuildInfo(guildInfo.queryBuilder("setReminders")).catch(e => console.error(e));
await fn.collectionBuilders.guildInfos(interaction.client);
// Close the collector
await fn.collectors.end(interaction.client, guildInfo).catch(e => console.error(e));
// Reply confirming disabling of relay
await interaction.editReply(fn.builders.embed(strings.status.optout)).catch(e => console.error(e));
} else {
await interaction.editReply(fn.builders.errorEmbed("A notification relay has not been set up yet!")).catch(e => console.error(e));

View File

@ -18,8 +18,8 @@ module.exports = {
)
.addChannelOption(o =>
o.setName('leaderboardchannel')
.setDescription('If your leaderboard isn\'t in the same channel, where is it?')
.setRequired(false)
.setDescription('What channel is your leaderboard in?')
.setRequired(true)
)
)
.addSubcommand(sc =>
@ -41,12 +41,12 @@ module.exports = {
.setDescription('View your server\'s configuration'))
.addSubcommand(sc =>
sc.setName('reset')
.setDescription('Remove all server configuration from the database')
.addBooleanOption(o =>
o.setName('confirm')
.setDescription('WARNING THIS IS IRREVERSIBLE')
.setRequired(true)
)
.setDescription('Remove all server configuration from the database')
.addBooleanOption(o =>
o.setName('confirm')
.setDescription('WARNING THIS IS IRREVERSIBLE')
.setRequired(true)
)
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction) {
@ -54,15 +54,27 @@ module.exports = {
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
case "compare":
const treeChannel = interaction.options.getChannel('treechannel');
const leaderboardChannel = interaction.options.getChannel('leaderboardchannel');
if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
const findMessagesResponse = await fn.messages.find(interaction, guildInfo);
await interaction.editReply(findMessagesResponse.status).catch(e => console.error(e));
guildInfo.setTreeMessage(undefined, treeChannel.id);
guildInfo.setLeaderboardMessage(undefined, leaderboardChannel.id);
// Update the database
await dbfn.setGuildInfo(guildInfo.queryBuilder("setTreeMessage")).catch(e => console.error(e));
await dbfn.setGuildInfo(guildInfo.queryBuilder("setLeaderboardMessage")).catch(e => console.error(e));
const reply = `Tree Channel: <#${treeChannel.id}> | Leaderboard Channel: <#${leaderboardChannel.id}>`;
await interaction.editReply(fn.builders.embed(reply)).catch(e => console.error(e));
} else {
let guildInfo = new GuildInfo()
.setId(interaction.guildId);
const findMessagesResponse = await fn.messages.find(interaction, guildInfo);
await interaction.editReply(findMessagesResponse.status).catch(e => console.error(e));
.setId(interaction.guildId)
.setTreeMessage(undefined, treeChannel.id)
.setLeaderboardMessage(undefined, leaderboardChannel.id);
// Update the database
await dbfn.setGuildInfo(guildInfo.queryBuilder("setTreeMessage")).catch(e => console.error(e));
await dbfn.setGuildInfo(guildInfo.queryBuilder("setLeaderboardMessage")).catch(e => console.error(e));
const reply = `Tree Channel: <#${treeChannel.id}> | Leaderboard Channel: <#${leaderboardChannel.id}>`;
await interaction.editReply(fn.builders.embed(reply)).catch(e => console.error(e));
}
break;
case "rolemenu":
@ -114,7 +126,7 @@ module.exports = {
await interaction.editReply(fn.builders.embed("You must select 'true' to confirm setup reset. No changes have been made.")).catch(e => console.error(e));
}
} else {
throw "Guild doesn't exist in database!";
await interaction.editReply(fn.builders.errorEmbed("There is no configuration to delete.")).catch(e => console.error(e));
}
break;
default:

View File

@ -8,26 +8,62 @@ module.exports = {
.setDescription('Calculate how long it would take to reach a given height')
.addIntegerOption(o =>
o.setName('endheight')
.setDescription('Ending tree height in feet')
.setRequired(true))
.setDescription('Ending tree height in feet')
.setRequired(true)
)
.addIntegerOption(o =>
o.setName('beginheight')
.setDescription('Beginning tree height in feet')
.setRequired(false)),
.setDescription('Beginning tree height in feet')
.setRequired(false)
)
.addIntegerOption(o =>
o.setName('efficiency')
.setDescription('Composter efficiency percentage, rounded')
.setRequired(false)
)
.addIntegerOption(o =>
o.setName('quality')
.setDescription('Compost quality percentage, rounded')
.setRequired(false)
)
.addBooleanOption(o =>
o.setName('private')
.setDescription('Should the reply be visible only to you?')
.setRequired(false)
),
async execute(interaction) {
await interaction.deferReply({ ephemeral: true });
let beginHeight = interaction.options.getInteger('beginheight');
const private = interaction.options.getBoolean('private') != undefined ? interaction.options.getBoolean('private') : true;
await interaction.deferReply({ ephemeral: private });
const inBeginHeight = interaction.options.getInteger('beginheight');
const endHeight = interaction.options.getInteger('endheight');
if (!beginHeight) {
const efficiency = interaction.options.getInteger('efficiency') != undefined ? interaction.options.getInteger('efficiency') : 10;
const quality = interaction.options.getInteger('quality') != undefined ? interaction.options.getInteger('quality') : 5;
let beginHeight, replyContent;
if (!inBeginHeight) {
const guildInfo = interaction.client.guildInfos.get(interaction.guild.id);
beginHeight = guildInfo.treeHeight;
} else {
beginHeight = inBeginHeight;
}
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);
return;
});
},
const timeToHeightResults = await fn.timeToHeight(beginHeight, endHeight, efficiency, quality);
const { time, totalWaterCount, compostAppliedCount, average, savedTime } = timeToHeightResults;
let replyFields = undefined;
if (efficiency && quality) {
replyContent = `I estimate that a tree with ${efficiency}% Composter Efficiency and ${quality}% Compost Quality growing from ${beginHeight}ft to ${endHeight}ft will take ${time}`;
replyFields = [
{ name: `Height Gained:`, value: `${endHeight - beginHeight}ft`, inline: true},
{ name: `E/Q:`, value: `${efficiency}%/${quality}%`, inline: true},
{ name: `Compost Applied`, value: `${compostAppliedCount} times`, inline: true },
{ name: `Compost Average`, value: `${average}%`, inline: true },
{ name: `Saved Time`, value: savedTime, inline: true }
];
} else {
replyContent = `I estimate that a tree growing from ${beginHeight}ft to ${endHeight}ft will take ${time}`;
}
const reply = fn.builders.embeds.information(replyContent, replyFields)
await interaction.editReply(reply);
}
};