/* eslint-disable comma-dangle */ // dotenv for handling environment variables const dotenv = require('dotenv'); dotenv.config(); const isDev = process.env.isDev; const package = require('../package.json'); // filesystem const fs = require('fs'); // Discord.js const Discord = require('discord.js'); const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = Discord; const { GuildInfo } = require('./CustomClasses'); // Various imports from other files 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'); const functions = { // Functions for managing and creating Collections collectionBuilders: { // Create the collection of slash commands slashCommands(client) { if (!client.slashCommands) client.slashCommands = new Discord.Collection(); client.slashCommands.clear(); for (const file of slashCommandFiles) { const slashCommand = require(`../slash-commands/${file}`); if (slashCommand.data != undefined) { client.slashCommands.set(slashCommand.data.name, slashCommand); } } if (isDev) console.log('Slash Commands Collection Built'); }, async guildInfos(client) { const guildInfos = await dbfn.getAllGuildInfos(); if (!client.guildInfos) client.guildInfos = new Discord.Collection(); client.guildInfos.clear(); for (const guildInfo of guildInfos) { client.guildInfos.set(guildInfo.guildId, guildInfo); } return 'guildInfos Collection Built'; } }, builders: { 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 ); return refreshActionRow; }, treeRoleMenu() { return new ActionRowBuilder() .addComponents( this.buttons.waterPing(), this.buttons.fruitPing() ); }, buttons: { acceptRules() { return new ButtonBuilder() .setCustomId('acceptrules') .setLabel(`${strings.emoji.confirm} Accept Rules`) .setStyle(ButtonStyle.Primary); }, waterPing() { return new ButtonBuilder() .setCustomId('waterpingrole') .setLabel(strings.emoji.water) .setStyle(ButtonStyle.Primary); }, fruitPing() { return new ButtonBuilder() .setCustomId('fruitpingrole') .setLabel(strings.emoji.fruit) .setStyle(ButtonStyle.Primary); } } }, embeds: { treeRoleMenu(guildInfo) { const actionRow = functions.builders.actionRows.treeRoleMenu(); let tempStrings = strings.embeds.treeRoleMenu; let description = tempStrings[0] + tempStrings[1] + `<@&${guildInfo.waterRoleId}>` + tempStrings[2]; if (guildInfo.fruitRoleId != undefined) { description += tempStrings[3] + `<@&${guildInfo.fruitRoleId}>` + tempStrings[4]; } const embed = new EmbedBuilder() .setColor(strings.embeds.color) .setTitle(strings.embeds.roleMenuTitle) .setDescription(description) .setFooter({ text: strings.embeds.roleMenuFooter }); return { embeds: [embed], components: [actionRow] }; } }, comparisonEmbed(content, guildInfo) { // Create the embed using the content passed to this function const embed = new EmbedBuilder() .setColor(strings.embeds.color) .setTitle('Tallest Trees Comparison') .setDescription(content) .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('Notification Relay') .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) { const embed = new EmbedBuilder() .setColor(strings.embeds.color) .setTitle(strings.help.title) .setDescription(content) .setFooter({ text: `v${package.version} - ${strings.embeds.footer}` }); const privateBool = private == 'true'; const messageContents = { embeds: [embed], ephemeral: privateBool }; return messageContents; }, errorEmbed(content) { const embed = new EmbedBuilder() .setColor(0xFF0000) .setTitle('Error!') .setDescription("Error: " + content) .setFooter({ text: `v${package.version} - ${strings.embeds.footer}` }); const messageContents = { embeds: [embed], ephemeral: true }; return messageContents; }, embed(content) { const embed = new EmbedBuilder() .setColor(0x8888FF) .setTitle('Information') .setDescription(content) .setFooter({ text: `v${package.version} - ${strings.embeds.footer}` }); const messageContents = { embeds: [embed], ephemeral: true }; return messageContents; } }, rankings: { parse(interaction, guildInfo) { return new Promise((resolve, reject) => { if (guildInfo.guildId == "") { reject(strings.error.noGuild); return; } if (guildInfo.leaderboardMessageId != undefined) { interaction.guild.channels.fetch(guildInfo.leaderboardChannelId).then(c => { c.messages.fetch(guildInfo.leaderboardMessageId).then(leaderboardMessage => { if ((leaderboardMessage.embeds.length == 0) || (leaderboardMessage.embeds[0].data.title != 'Tallest Trees')) { reject("This doesn't appear to be a valid ``/top trees`` message."); return; } let lines = leaderboardMessage.embeds[0].data.description.split('\n'); let leaderboard = { "guildId": interaction.guildId, "entries": [] }; for (let i = 0; i < 10; i++) { // Breakdown each line separating it on each - let breakdown = lines[i].split(' - '); // Check if the first part, the ranking, has these emojis to detect first second and third place if (breakdown[0].includes('🥇')) { breakdown[0] = '``#1 ``' } else if (breakdown[0].includes('🥈')) { breakdown[0] = '``#2 ``' } else if (breakdown[0].includes('🥉')) { breakdown[0] = '``#3 ``' } // Clean off the excess and get just the number from the rank, make sure it's an int not string let trimmedRank = parseInt(breakdown[0].slice(breakdown[0].indexOf('#') + 1, breakdown[0].lastIndexOf('``'))); // Clean off the excess and get just the tree name let trimmedName = breakdown[1].slice(breakdown[1].indexOf('``') + 2); trimmedName = trimmedName.slice(0, trimmedName.indexOf('``')); // Clean off the excess and get just the tree height, make sure it's a 1 decimal float let trimmedHeight = parseFloat(breakdown[2].slice(0, breakdown[2].indexOf('ft'))).toFixed(1); let isMyTree = false; let isMaybeMyTree = false; if (breakdown[2].includes('📍')) isMyTree = true; if (breakdown[1].includes(guildInfo.treeName)) maybeMyTree = true; // "entries": [ { "treeHeight": 12, "treeRank": 34, "treeName": "name" }, ] } leaderboard.entries.push({ treeRank: trimmedRank, treeName: trimmedName, treeHeight: trimmedHeight, hasPin: isMyTree }); } dbfn.uploadLeaderboard(leaderboard).then(res => { resolve(res.status); }).catch(err => { console.error(err); reject(err); return; }); }).catch(err => { reject(strings.status.missingLeaderboardMessage); console.error(err); return; }); }).catch(err => { reject(strings.status.missingLeaderboardChannel); console.error(err); return; }); } else { reject("The leaderboardMessageId is undefined somehow"); return; } }); }, async compare(interaction, 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, guildInfo) { let input; return new Promise((resolve, reject) => { 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; } }); } }, 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); await functions.collectionBuilders.guildInfos(interaction.client); response.data = interaction.client.guildInfos.get(guildInfo.guildId); // 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"; } } }, buttonHandlers: { async fruitPing(interaction) { if (interaction.client.guildInfos.has(interaction.guildId)) { let guildInfo = interaction.client.guildInfos.get(interaction.guildId); const role = await functions.roles.fetchRole(interaction.guild, guildInfo.fruitRoleId); let status = "No Changes Made"; if (interaction.member.roles.cache.some(role => role.id == guildInfo.fruitRoleId)) { await functions.roles.takeRole(interaction.member, role); status = "Removed the fruit role."; } else { await functions.roles.giveRole(interaction.member, role); status = "Added the fruit role."; } return functions.builders.embed(status); } else { throw "Guild doesn't exist in database!"; } }, async waterPing(interaction) { if (interaction.client.guildInfos.has(interaction.guildId)) { let guildInfo = interaction.client.guildInfos.get(interaction.guildId); let status = "No Changes Made"; const role = await functions.roles.fetchRole(interaction.guild, guildInfo.waterRoleId); if (interaction.member.roles.cache.some(role => role.id == guildInfo.waterRoleId)) { await functions.roles.takeRole(interaction.member, role); status = "Removed the water role."; } else { await functions.roles.giveRole(interaction.member, role); status = "Added the water role."; } return functions.builders.embed(status); } else { throw "Guild doesn't exist in database!"; } } }, roles: { async fetchRole(guild, roleId) { return await guild.roles.fetch(roleId).catch(err => console.error("Error fetching the role: " + err + "\n" + roleId)); }, async giveRole(member, role) { await member.roles.add(role).catch(err => console.error("Error giving the role: " + err + "\n" + JSON.stringify(role))); }, async takeRole(member, role) { await member.roles.remove(role).catch(err => console.error("Error taking the role: " + err + "\n" + JSON.stringify(role))); } }, async refresh(interaction) { // const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); // let guildInfo = getGuildInfoResponse.data; let guildInfo = interaction.client.guildInfos.get(interaction.guild.id); 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).catch(err => console.error(err)); } else { await interaction.update(this.builders.errorEmbed(findMessagesResponse.status)); } }, reset(guildId) { return new Promise((resolve, reject) => { dbfn.deleteGuildInfo(guildId).then(res => { resolve(res); }).catch(err => { console.error(err); reject(err); return; }); }); }, getWaterTime(size) { return Math.floor(Math.pow(size * 0.07 + 5, 1.1)); // Seconds }, timeToHeight(beginHeight, destHeight) { 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; } // 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, message, channelId, guild) { const reminderChannel = await guild.channels.fetch(channelId); const reminderEmbed = functions.builders.reminderEmbed(message, guildInfo); await reminderChannel.send(reminderEmbed).catch(err => { console.error(err); }); }, async setupCollectors(client) { let guildInfos = client.guildInfos; guildInfos.set("collectors", []); await guildInfos.forEach(async guildInfo => { if (guildInfo.watchChannelId != "" && guildInfo instanceof GuildInfo) { const guild = await client.guilds.fetch(guildInfo.guildId); // console.log(guildInfo instanceof GuildInfo); const channel = await guild.channels.fetch(guildInfo.watchChannelId); const filter = message => { return message.author.id != process.env.BOTID && message.embeds != undefined; } const collector = channel.createMessageCollector({ filter }); collector.on('collect', message => { if (message.embeds.length == 0) return; console.log(message.embeds); if (message.embeds[0].data.description.includes(strings.notifications.water)) { this.sendReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild); } else if (message.embeds[0].data.description.includes(strings.notifications.fruit)) { this.sendReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild); } }); } }); } }; module.exports = functions;