From a757b988b0b041341ca9537256a447d9c398ec7d Mon Sep 17 00:00:00 2001 From: Skylar Grant Date: Sat, 11 Feb 2023 23:57:34 -0500 Subject: [PATCH] Classes, Collections, and new Notification Relay --- data/strings.json | 4 + main.js | 17 +-- modules/CustomClasses.js | 127 +++++++++++++++++++++ modules/_deploy-global.js | 2 +- modules/dbfn.js | 164 ++++++++++++++------------- modules/functions.js | 189 ++++++++------------------------ modules/testing.js | 124 +++++++++++++++++++++ slash-commands/compare.js | 66 ++++------- slash-commands/notifications.js | 49 +++++++++ slash-commands/setping.js | 31 ------ slash-commands/setping.js.bak | 18 --- slash-commands/setupinfo.js | 2 +- slash-commands/timetoheight.js | 11 +- 13 files changed, 475 insertions(+), 329 deletions(-) create mode 100644 modules/CustomClasses.js create mode 100644 modules/testing.js create mode 100644 slash-commands/notifications.js delete mode 100644 slash-commands/setping.js delete mode 100644 slash-commands/setping.js.bak diff --git a/data/strings.json b/data/strings.json index afb5fcc..c6f6084 100644 --- a/data/strings.json +++ b/data/strings.json @@ -44,5 +44,9 @@ "missingTreeMessage": "There was a problem finding the Tree message. Please make sure the ``/tree`` and ``/top trees`` messages are this channel, or run to set the ``/tree`` and ``/top trees`` channels.", "missingTreeChannel": "There was a problem finding the Tree channel, was it deleted? Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run to set the ``/tree`` and ``/top trees`` channels." }, + "notifications": { + "water": "is ready to be watered again!", + "fruit": "Fruit is appearing!" + }, "temp": {} } \ No newline at end of file diff --git a/main.js b/main.js index fef604a..92e8eed 100644 --- a/main.js +++ b/main.js @@ -27,11 +27,12 @@ const strings = require('./data/strings.json'); const dbfn = require('./modules/dbfn.js'); const isDev = process.env.isDev; -client.once('ready', () => { - fn.collections.slashCommands(client); +client.once('ready', async () => { + await fn.collectionBuilders.slashCommands(client); + await fn.collectionBuilders.guildInfos(client); + await fn.setupCollectors(client); console.log('Ready!'); client.user.setActivity({ name: strings.activity.name, type: ActivityType.Watching }); - fn.checkReady(client); if (isDev == 'false') { client.channels.fetch(statusChannelId).then(channel => { channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete <@481933290912350209>`); @@ -60,18 +61,8 @@ client.on('interactionCreate', async interaction => { await fn.refresh(interaction).catch(err => { interaction.channel.send(fn.builders.errorEmbed(err)); }); - } else if (interaction.isButton() && interaction.component.customId == 'resetping') { - await fn.resetPing(interaction); - await fn.refresh(interaction).catch(err => { - interaction.channel.send(fn.builders.errorEmbed(err)); - }); } else if (interaction.isButton() && interaction.component.customId == 'deleteping') { if (interaction.message.deletable) { - await dbfn.setRemindedStatus(interaction.guildId, 0); - await dbfn.getGuildInfo(interaction.guildId).then(async res => { - const guildInfo = res.data; - await fn.refreshComparisonMessage(interaction.client, guildInfo); - }); await interaction.message.delete().catch(err => { console.error(err); }); diff --git a/modules/CustomClasses.js b/modules/CustomClasses.js new file mode 100644 index 0000000..8bf12d3 --- /dev/null +++ b/modules/CustomClasses.js @@ -0,0 +1,127 @@ +const mysql = require('mysql'); +const db = mysql.createConnection({ + host: process.env.DBHOST, + user: process.env.DBUSER, + password: process.env.DBPASS, + database: process.env.DBNAME, + port: process.env.DBPORT +}); + +module.exports = { + GuildInfo: class { + constructor() { + this.guildId = ""; + this.treeName = ""; + this.treeHeight = 0; + this.treeMessageId = ""; + this.treeChannelId = ""; + this.leaderboardMessageId = ""; + this.leaderboardChannelId = ""; + this.waterMessage = ""; + this.fruitMessage = ""; + this.reminderChannelId = ""; + this.watchChannelId = ""; + } + + setId(id) { + this.guildId = id; + return this; + } + setName(name) { + this.treeName = name; + return this; + } + setHeight(height) { + this.treeHeight = height; + return this; + } + setTreeMessage(messageId, channelId) { + this.treeMessageId = messageId; + this.treeChannelId = channelId; + return this; + } + setLeaderboardMessage(messageId, channelId) { + this.leaderboardMessageId = messageId; + this.leaderboardChannelId = channelId; + return this; + } + setReminders(waterMessage, fruitMessage, reminderChannelId, watchChannelId) { + this.waterMessage = waterMessage; + this.fruitMessage = fruitMessage; + this.reminderChannelId = reminderChannelId; + this.watchChannelId = watchChannelId; + return this; + } + queryBuilder(query) { + let queryParts = []; + switch (query) { + case "setAll": + queryParts = [ + `INSERT INTO guild_info `, + `(guild_id, `, + `tree_name, `, + `tree_height, `, + `tree_message_id, `, + `tree_channel_id, `, + `leaderboard_message_id, `, + `leaderboard_channel_id, `, + `water_message, `, + `fruit_message, `, + `reminder_channel_id, `, + `watch_channel_id) `, + `VALUES (${db.escape(this.guildId)}, `, + `${db.escape(this.treeName)}, `, + `${db.escape(this.treeHeight)}, `, + `${db.escape(this.treeMessageId)}, `, + `${db.escape(this.treeChannelId)}, `, + `${db.escape(this.leaderboardMessageId)}, `, + `${db.escape(this.leaderboardChannelId)}, `, + `${db.escape(this.waterMessage)}, `, + `${db.escape(this.fruitMessage)}, `, + `${db.escape(this.reminderChannelId)}, `, + `${db.escape(this.watchChannelId)}) `, + `ON DUPLICATE KEY UPDATE tree_name = ${db.escape(this.treeName)}, `, + `tree_height = ${db.escape(this.treeHeight)}, `, + `tree_message_id = ${db.escape(this.treeMessageId)}, `, + `tree_channel_id = ${db.escape(this.treeChannelId)}, `, + `leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `, + `leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}, `, + `water_message = ${db.escape(this.waterMessage)}, `, + `fruit_message = ${db.escape(this.fruitMessage)}, `, + `reminder_channel_id = ${db.escape(this.reminderChannelId)}, `, + `watch_channel_id = ${db.escape(this.watchChannelId)})` + ]; + return queryParts.join(''); + break; + case "setReminders": + queryParts = [ + `UPDATE guild_info SET water_message = ${db.escape(this.waterMessage)}, `, + `fruit_message = ${db.escape(this.fruitMessage)}, `, + `reminder_channel_id = ${db.escape(this.reminderChannelId)}, `, + `watch_channel_id = ${db.escape(this.watchChannelId)} `, + `WHERE guild_id = ${db.escape(this.guildId)}` + ]; + return queryParts.join(''); + break; + case "setTreeMessage": + queryParts = [ + `UPDATE guild_info SET tree_message_id = ${db.escape(this.treeMessageId)}, `, + `tree_channel_id = ${db.escape(this.treeChannelId)}, `, + `WHERE guild_id = ${db.escape(this.guildId)}` + ]; + return queryParts.join(''); + break; + case "setLeaderboardMessage": + queryParts = [ + `UPDATE guild_info SET leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `, + `leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}, `, + `WHERE guild_id = ${db.escape(this.guildId)}` + ]; + return queryParts.join(''); + break; + default: + break; + } + } + } +} \ No newline at end of file diff --git a/modules/_deploy-global.js b/modules/_deploy-global.js index 60de0e4..b9ffee9 100644 --- a/modules/_deploy-global.js +++ b/modules/_deploy-global.js @@ -4,7 +4,7 @@ dotenv.config(); const { REST } = require('@discordjs/rest'); const { Routes } = require('discord-api-types/v9'); -const clientId = process.env.clientId; +const clientId = process.env.BOTID; const token = process.env.TOKEN; const fs = require('fs'); diff --git a/modules/dbfn.js b/modules/dbfn.js index 5eac3e4..e41e73f 100644 --- a/modules/dbfn.js +++ b/modules/dbfn.js @@ -2,6 +2,7 @@ const dotenv = require('dotenv'); dotenv.config(); const debugMode = process.env.DEBUG || true; const mysql = require('mysql'); +const { GuildInfo } = require('./CustomClasses.js'); /* Table Structures guild_info @@ -33,42 +34,6 @@ 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 - }); - db.connect((err) => { - if (err) throw `Error connecting to the database: ${err.message}`; - }); - // 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 - return new Promise((resolve, reject) => { - db.query(createGuildInfoTableQuery, (err) => { - if (err) { - reject("Error creating the guild_info table: " + err.message); - console.error("Offending query: " + createGuildInfoTableQuery); - db.end(); - return; - } - db.query(createLeaderboardTableQuery, (err) => { - if (err) { - reject("Error creating the leaderboard table: " + err.message); - console.error("Offending query: " + createLeaderboardTableQuery); - db.end(); - return; - } - resolve({ "status": "Successfully checked both tables.", "data": null }); - db.end(); - }); - }); - }); - }, getGuildInfo(guildId) { const db = mysql.createConnection({ host: process.env.DBHOST, @@ -91,45 +56,26 @@ module.exports = { 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 is no database entry for your guild yet. Try running /setup"); db.end(); return; } row = res[0]; - 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, - "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 - }; + const guildInfo = new GuildInfo() + .setId(row.guild_id) + .setName(row.tree_name) + .setHeight(row.tree_height) + .setTreeMessage(row.tree_message_id, row.tree_channel_id) + .setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id) + .setReminders(row.water_message, row.fruit_message, row.reminder_channel_id, row.watch_channel_id); + db.end(); - resolve({ "status": "Successfully fetched guild information", "data": guildInfo }); + resolve(guildInfo); }); }); }, - setGuildInfo(guildInfo) { + getAllGuildInfos() { const db = mysql.createConnection({ host: process.env.DBHOST, user: process.env.DBUSER, @@ -140,13 +86,53 @@ module.exports = { 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 - 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 } + // Get a server's tree information from the database + const query = '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(insertGuildInfoQuery, (err, res) => { + db.query(query, (err, res) => { + if (err) { + console.error(err); + reject("Error fetching all guild infos: " + err.message); + db.end(); + return; + } + if (res.length == 0) { + reject("There are no servers yet!"); + db.end(); + return; + } + let guildInfos = []; + for (let i = 0; i < res.length; i++) { + let row = res[i]; + guildInfos.push(new GuildInfo() + .setId(row.guild_id) + .setName(row.tree_name) + .setHeight(row.tree_height) + .setTreeMessage(row.tree_message_id, row.tree_channel_id) + .setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id) + .setReminders(row.water_message, row.fruit_message, row.reminder_channel_id, row.watch_channel_id) + ); + } + + db.end(); + resolve(guildInfos); + }); + }); + }, + setGuildInfo(query) { + 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}`; + }); + return new Promise((resolve, reject) => { + db.query(query, (err, res) => { if (err) { console.error(err); reject("Error setting the guild info: " + err.message); @@ -154,7 +140,7 @@ module.exports = { return; } db.end(); - resolve({ "status": "Successfully set the guild information", "data": null }); + resolve(); }); }); }, @@ -353,7 +339,7 @@ module.exports = { }); }); }, - setReminderInfo(guildId, reminderMessage, reminderChannelId) { + setReminderInfo(guildInfo) { const db = mysql.createConnection({ host: process.env.DBHOST, user: process.env.DBUSER, @@ -365,7 +351,7 @@ module.exports = { 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)}`; + const insertReminderInfoQuery = `UPDATE guild_info SET waterMessage = ${db.escape(guildInfo.waterMessage)}, ping_channel_id = ${db.escape(reminderChannelId)}, fruit_message = ${db.escape(guildInfo.fruitMessage)} 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) => { @@ -376,7 +362,7 @@ module.exports = { return; } db.end(); - resolve({ "status": `Successfully set the reminder message to "${reminderMessage}" in <#${reminderChannelId}>`, "data": res }); + resolve({ "status": `Your notification relay has been set up.\nWater Message: "${guildInfo.waterMessage}"\nFruit Message: "${guildInfo.fruitMessage}"\nRelay Channel: <#${guildInfo.reminderChannelId}>\nWatch Channel: <#${guildInfo.notificationChannelId}>`, "data": res }); }); }); }, @@ -524,5 +510,33 @@ module.exports = { resolve({ "status": `Successfully set the comparison message ID: ${comparisonMessage}`, "data": res }); }); }); + }, + setNotificationChannel(id, 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 query = `UPDATE guild_info SET notification_channel_id = ${db.escape(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(query, (err, res) => { + if (err) { + console.error(err); + db.end(); + reject("Error updating the notification channel ID: " + err.message); + return; + } + db.end(); + resolve({ "status": `Successfully set the notification channel ID: ${comparisonMessage}`, "data": res }); + }); + }); } }; \ No newline at end of file diff --git a/modules/functions.js b/modules/functions.js index 8cdae06..0c7dc1e 100644 --- a/modules/functions.js +++ b/modules/functions.js @@ -11,6 +11,7 @@ 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'); @@ -21,7 +22,7 @@ const { finished } = require('stream'); const functions = { // Functions for managing and creating Collections - collections: { + collectionBuilders: { // Create the collection of slash commands slashCommands(client) { if (!client.slashCommands) client.slashCommands = new Discord.Collection(); @@ -33,6 +34,15 @@ const functions = { } } 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: { @@ -58,44 +68,9 @@ const functions = { .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 - 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, guildInfo) { // Create the embed using the content passed to this function const embed = new EmbedBuilder() @@ -110,7 +85,7 @@ const functions = { // Create the embed using the content passed to this function const embed = new EmbedBuilder() .setColor(strings.embeds.color) - .setTitle('Water Reminder') + .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()] }; @@ -417,8 +392,9 @@ const functions = { // await dbfn.setGuildInfo(guildInfo); // Bundle guildInfo into the response - const getGuildInfoResponse = await dbfn.getGuildInfo(guildInfo.guildId); - response.data = getGuildInfoResponse.data; + // 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 @@ -453,8 +429,9 @@ const functions = { } }, async refresh(interaction) { - const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); - let guildInfo = getGuildInfoResponse.data; + // 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; @@ -466,10 +443,7 @@ const functions = { 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); - }); + await interaction.update(embed).catch(err => console.error(err)); } else { await interaction.update(this.builders.errorEmbed(findMessagesResponse.status)); } @@ -485,20 +459,12 @@ const functions = { }); }); }, - getInfo(guildId) { - return new Promise((resolve, reject) => { - dbfn.getGuildInfo(guildId).then(res => { - let guildInfo = res.data; - let guildInfoString = ""; - guildInfoString += `Tree Message: https://discord.com/channels/${guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId}\n`; - guildInfoString += `Rank Message: https://discord.com/channels/${guildId}/${guildInfo.leaderboardChannelId}/${guildInfo.leaderboardMessageId}\n`; - resolve(`Here is your servers setup info:\n${guildInfoString}`); - }).catch(err => { - console.error(err); - reject(err); - return; - }) - }); + getInfo(interaction) { + let guildInfo = interaction.client.guildInfos.get(interaction.guild.id); + let guildInfoString = ""; + guildInfoString += `Tree Message: https://discord.com/channels/${guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId}\n`; + guildInfoString += `Rank Message: https://discord.com/channels/${guildId}/${guildInfo.leaderboardChannelId}/${guildInfo.leaderboardMessageId}\n`; + return `Here is your servers setup info:\n${guildInfoString}`; }, getWaterTime(size) { return Math.floor(Math.pow(size * 0.07 + 5, 1.1)); // Seconds @@ -539,89 +505,13 @@ const functions = { }, 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 => { + 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 setReminder(interaction, ms) { - setTimeout(this.sendReminder(interaction), ms); - }, - async checkReady(client) { // Check if the guilds trees are ready to water - // let time = new Date(Date.now()); - // console.log("Ready check " + time.getSeconds()); - try { - // Get the guildInfos for each guild that is opted in and waiting to send a reminder - const getOptedInGuildsResponse = await dbfn.getOptedInGuilds(); - // getOptedInGuilds will return this if it gets an empty set from the database - if (getOptedInGuildsResponse.status != "No servers have opted in yet") { - // Get the Array of Guilds from the response - const guilds = getOptedInGuildsResponse.data; - // Iterate over the Array - for (let i = 0; i < guilds.length; i++) { - // console.log(`iter: ${i}`); - // Save the 'old' guild info that came from getOptedInGuilds - const oldGuildInfo = guilds[i]; - // Get up-to-date guildInfo from the database, probably unnecessary and redundant - const getGuildInfoResponse = await dbfn.getGuildInfo(oldGuildInfo.guildId); - // Save the new guildInfo so we can reference its remindedStatus - const guildInfo = getGuildInfoResponse.data; - const { guildId, treeChannelId, treeMessageId, remindedStatus } = guildInfo; - // console.log(`${guildInfo.treeName}: ${remindedStatus}`); - // Double check the remindedStatus to prevent double pings - if (remindedStatus == 0) { - // Fetch the guild - const guild = await client.guilds.fetch(guildId); - // Fetch the tree channel - const treeChannel = await guild.channels.fetch(treeChannelId); - // Fetch the tree message - const treeMessage = await treeChannel.messages.fetch(treeMessageId); - // Get the description from the embed of the tree message - const description = treeMessage.embeds[0].description; - // Default to not being ready to water - let readyToWater = false; - // Obviously if the tree says it's Ready to be watered, it's ready - if (description.includes("Ready to be watered")) { - readyToWater = true; - // But sometimes the tree doesn't refresh the embed, in that case we'll do a secondary check using the - // timestamp included in the embed. - } else { - const beginWaterTimestamp = description.indexOf(""); - // Split the description starting at "" to get just the numerical timestamp - const waterTimestamp = parseInt(description.slice(beginWaterTimestamp, endWaterTimestamp)); - // The Discord timestamp is in seconds, not ms so we need to divide by 1000 - const nowTimestamp = (Date.now() / 1000); - readyToWater = (nowTimestamp > waterTimestamp); - } - - if (readyToWater) { - // Send the reminder message - await this.sendReminder(guildInfo, guild); - guildInfo.remindedStatus = 1; - await this.refreshComparisonMessage(client, guildInfo); - } - } - } - await this.sleep(5000); - this.checkReady(client); - } else { - // console.log(getOptedInGuildsResponse.status); - await this.sleep(5000); - this.checkReady(client); - } - } catch (err) { - console.error(err); - await this.sleep(30000); - this.checkReady(client); - } - }, async refreshComparisonMessage(client, guildInfo) { if (guildInfo.comparisonChannelId != "" && guildInfo.comparisonMessageId != "") { const guild = await client.guilds.fetch(guildInfo.guildId); @@ -633,8 +523,25 @@ const functions = { return; } }, - async resetPing(interaction) { - await dbfn.setRemindedStatus(interaction.guildId, 0); + 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 => message.author.id != process.env.BOTID; + const collector = channel.createMessageCollector({ filter }); + collector.on('collect', message => { + if (message.content.includes(strings.notifications.water)) { + this.sendReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild); + } else if (message.content.includes(strings.notifications.fruit)) { + this.sendReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild); + } + }); + } + }); } }; diff --git a/modules/testing.js b/modules/testing.js new file mode 100644 index 0000000..47ec1f9 --- /dev/null +++ b/modules/testing.js @@ -0,0 +1,124 @@ +const GuildInfo = class { + constructor() { + this.guildId = ""; + this.treeName = ""; + this.treeHeight = 0; + this.treeMessageId = ""; + this.treeChannelId = ""; + this.leaderboardMessageId = ""; + this.leaderboardChannelId = ""; + this.waterMessage = ""; + this.fruitMessage = ""; + this.reminderChannelId = ""; + this.watchChannelId = ""; + } + + setId(id) { + this.guildId = id; + return this; + } + setName(name) { + this.treeName = name; + return this; + } + setHeight(height) { + this.treeHeight = height; + return this; + } + setTreeMessage(messageId, channelId) { + this.treeMessageId = messageId; + this.treeChannelId = channelId; + return this; + } + setLeaderboardMessage(messageId, channelId) { + this.leaderboardMessageId = messageId; + this.leaderboardChannelId = channelId; + return this; + } + setReminders(waterMessage, fruitMessage, reminderChannelId, watchChannelId) { + this.waterMessage = waterMessage; + this.fruitMessage = fruitMessage; + this.reminderChannelId = reminderChannelId; + this.watchChannelId = watchChannelId; + return this; + } + queryBuilder(query) { + let queryParts = []; + switch (query) { + case "setAll": + queryParts = [ + `INSERT INTO guild_info `, + `(guild_id, `, + `tree_name, `, + `tree_height, `, + `tree_message_id, `, + `tree_channel_id, `, + `leaderboard_message_id, `, + `leaderboard_channel_id, `, + `water_message, `, + `fruit_message, `, + `reminder_channel_id, `, + `watch_channel_id) `, + `VALUES (${db.escape(this.guildId)}, `, + `${db.escape(this.treeName)}, `, + `${db.escape(this.treeHeight)}, `, + `${db.escape(this.treeMessageId)}, `, + `${db.escape(this.treeChannelId)}, `, + `${db.escape(this.leaderboardMessageId)}, `, + `${db.escape(this.leaderboardChannelId)}, `, + `${db.escape(this.waterMessage)}, `, + `${db.escape(this.fruitMessage)}, `, + `${db.escape(this.reminderChannelId)}, `, + `${db.escape(this.watchChannelId)}) `, + `ON DUPLICATE KEY UPDATE tree_name = ${db.escape(this.treeName)}, `, + `tree_height = ${db.escape(this.treeHeight)}, `, + `tree_message_id = ${db.escape(this.treeMessageId)}, `, + `tree_channel_id = ${db.escape(this.treeChannelId)}, `, + `leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `, + `leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}, `, + `water_message = ${db.escape(this.waterMessage)}, `, + `fruit_message = ${db.escape(this.fruitMessage)}, `, + `reminder_channel_id = ${db.escape(this.reminderChannelId)}, `, + `watch_channel_id = ${db.escape(this.watchChannelId)})` + ]; + return queryParts.join(); + break; + case "setReminders": + queryParts = [ + `UPDATE guildInfo SET water_message = ${db.escape(this.waterMessage)}, `, + `fruit_message = ${db.escape(this.fruitMessage)}, `, + `reminder_channel_id = ${db.escape(this.reminderChannelId)}, `, + `watch_channel_id = ${db.escape(this.watchChannelId)} `, + `WHERE guild_id = ${db.escape(this.guildId)}` + ]; + return queryParts.join(); + break; + case "setTreeMessage": + queryParts = [ + `UPDATE guildInfo SET tree_message_id = ${db.escape(this.treeMessageId)}, `, + `tree_channel_id = ${db.escape(this.treeChannelId)}, `, + `WHERE guild_id = ${db.escape(this.guildId)}` + ]; + return queryParts.join(); + break; + case "setLeaderboardMessage": + queryParts = [ + `UPDATE guildInfo SET leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `, + `leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}, `, + `WHERE guild_id = ${db.escape(this.guildId)}` + ]; + return queryParts.join(); + break; + default: + break; + } + } +} + +new GuildInfo() + .setId(row.guild_id) + .setName(row.tree_name) + .setHeight(row.tree_height) + .setTreeMessage(row.tree_message_id, row.tree_channel_id) + .setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id) + .setReminders(row.water_message, row.fruit_message, row.reminder_channel_id); \ No newline at end of file diff --git a/slash-commands/compare.js b/slash-commands/compare.js index e631f86..c94d48f 100644 --- a/slash-commands/compare.js +++ b/slash-commands/compare.js @@ -1,6 +1,7 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, Guild } = require('discord.js'); const dbfn = require('../modules/dbfn.js'); const fn = require('../modules/functions.js'); +const { GuildInfo } = require('../modules/CustomClasses.js'); module.exports = { data: new SlashCommandBuilder() @@ -10,61 +11,40 @@ module.exports = { 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 + + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); const findMessagesResponse = await fn.messages.find(interaction, guildInfo); if (findMessagesResponse.code == 1) { - guildInfo = findMessagesResponse.data; + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); // 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); - }); + await interaction.editReply(embed).catch(err => console.error(err)); } else { await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); } - - }).catch(async err => { // If we fail to fetch the guild's info from the database - // If the error is because the guild hasn't been setup yet, set it up - if (err === "There is no database entry for your guild yet. Try running /setup") { - // Create a basic guildInfo with blank data - let guildInfo = { - guildId: `${interaction.guildId}`, - treeName: "", - treeHeight: 0, - treeMessageId: "", - treeChannelId: `${interaction.channelId}`, // Use this interaction channel for the initial channel IDs - leaderboardMessageId: "", - leaderboardChannelId: `${interaction.channelId}`, - reminderMessage: "", - reminderChannelId: "", - remindedStatus: 0, - reminderOptIn: 0, - } - // Using the above guildInfo, try to find the Grow A Tree messages - const findMessagesResponse = await fn.messages.find(interaction, guildInfo); - guildInfo = findMessagesResponse.data; - if (findMessagesResponse.code == 1) { - // Build the string that shows the comparison // TODO Move the string building section to fn.builders? - const comparedRankings = await fn.rankings.compare(interaction, guildInfo); - const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo); - await interaction.editReply(embed).then(async message => { - await dbfn.setComparisonMessage(message.id, interaction.guildId); - }); - } else { - await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); - } - + } else { + // Create a basic guildInfo with blank data + let guildInfo = new GuildInfo() + .setId(interaction.guildId) + .setTreeMessage("", interaction.channelId) + .setLeaderboardMessage("", interaction.channelId) + // 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).catch(err => console.error(err)); } else { - await interaction.editReply(fn.builders.errorEmbed("An unknown error occurred while running the compare command.")); - console.error(err); + await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); } - }); + } } catch (err) { interaction.editReply(fn.builders.errorEmbed(err)).catch(err => { console.error(err); diff --git a/slash-commands/notifications.js b/slash-commands/notifications.js new file mode 100644 index 0000000..ea75a78 --- /dev/null +++ b/slash-commands/notifications.js @@ -0,0 +1,49 @@ +const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); +const dbfn = require('../modules/dbfn.js'); +const fn = require('../modules/functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('notifications') + .setDescription('Setup a notification relay for improved water and fruit notifications') + .addChannelOption(o => + o + .setName('watchchannel') + .setDescription('The channel Grow A Tree sends your notifications in') + .setRequired(true)) + .addStringOption(o => + o + .setName('watermessage') + .setDescription('Message to send for water reminders') + .setRequired(true)) + .addChannelOption(o => + o + .setName('pingchannel') + .setDescription('The channel to send the water reminder in') + .setRequired(true)) + .addStringOption(o => + o + .setName('fruitmessage') + .setDescription("Message to send for fruit reminders") + .setRequired(false)) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles), + async execute(interaction) { + try { + await interaction.deferReply({ ephemeral: true }); + 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); + let query = guildInfo.queryBuilder("setReminders"); + console.log(query); + await dbfn.setGuildInfo(query); + await interaction.editReply(`I'll watch <#${watchChannel.id}> for Grow A Tree Notifications and relay them to <#${reminderChannel.id}>.`).catch(e => console.error(e)); + } + } catch (err) { + console.error("Error occurred while setting up a notification relay: " + err); + } + }, +}; \ No newline at end of file diff --git a/slash-commands/setping.js b/slash-commands/setping.js deleted file mode 100644 index 8674e5f..0000000 --- a/slash-commands/setping.js +++ /dev/null @@ -1,31 +0,0 @@ -const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); -const dbfn = require('../modules/dbfn.js'); -const fn = require('../modules/functions.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('setping') - .setDescription('Opt-in to automatic water reminders') - .addStringOption(o => - o.setName('pingmsg') - .setDescription('The message to send for a water reminder') - .setRequired(true)) - .addChannelOption(o => - o.setName('pingchannel') - .setDescription('The channel to send the water reminder in') - .setRequired(true)) - .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles), - async execute(interaction) { - try { - await interaction.deferReply({ ephemeral: true }); - const reminderMessage = interaction.options.getString('pingmsg'); - const reminderChannel = interaction.options.getChannel('pingchannel'); - const setPingRoleResponse = await dbfn.setReminderInfo(interaction.guildId, reminderMessage, reminderChannel.id); - await dbfn.setReminderOptIn(interaction.guildId, 1); - interaction.editReply(setPingRoleResponse.status); - } catch(err) { - console.error(err); - await interaction.editReply(fn.builders.errorEmbed(err)); - } - }, -}; \ No newline at end of file diff --git a/slash-commands/setping.js.bak b/slash-commands/setping.js.bak deleted file mode 100644 index 4d13791..0000000 --- a/slash-commands/setping.js.bak +++ /dev/null @@ -1,18 +0,0 @@ -const { SlashCommandBuilder } = require('discord.js'); -const dbfn = require('../modules/dbfn.js'); -const fn = require('../modules/functions.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('setping') - .setDescription('Run this command when you water your tree to have a reminder sent.'), - async execute(interaction) { - await interaction.deferReply({ ephemeral: true }); - const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); - const guildInfo = getGuildInfoResponse.data; - const reminderTimeS = fn.getWaterTime(guildInfo.treeHeight); - const reminderTimeMs = reminderTimeS * 1000; - fn.setReminder(interaction, reminderTimeMs, guildInfo.pingRoleId); - interaction.editReply("A reminder has been set."); - }, -}; \ No newline at end of file diff --git a/slash-commands/setupinfo.js b/slash-commands/setupinfo.js index 8597c94..b2adf67 100644 --- a/slash-commands/setupinfo.js +++ b/slash-commands/setupinfo.js @@ -7,7 +7,7 @@ module.exports = { .setDescription('View information about how the bot is set up in your server'), execute(interaction) { interaction.deferReply({ephemeral: true}).then(() => { - fn.getInfo(interaction.guildId).then(res => { + fn.getInfo(interaction).then(res => { const embed = fn.builders.embed(res); interaction.editReply(embed); }).catch(err => { diff --git a/slash-commands/timetoheight.js b/slash-commands/timetoheight.js index de0c3d7..bf23959 100644 --- a/slash-commands/timetoheight.js +++ b/slash-commands/timetoheight.js @@ -6,21 +6,20 @@ module.exports = { data: new SlashCommandBuilder() .setName('timetoheight') .setDescription('Calculate how long it would take to reach a given height') - .addStringOption(o => + .addIntegerOption(o => o.setName('endheight') .setDescription('Ending tree height in feet') .setRequired(true)) - .addStringOption(o => + .addIntegerOption(o => o.setName('beginheight') .setDescription('Beginning tree height in feet') .setRequired(false)), async execute(interaction) { await interaction.deferReply({ ephemeral: true }); - let beginHeight = interaction.options.getString('beginheight'); - const endHeight = interaction.options.getString('endheight'); + let beginHeight = interaction.options.getInteger('beginheight'); + const endHeight = interaction.options.getInteger('endheight'); if (!beginHeight) { - const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); - const guildInfo = getGuildInfoResponse.data; + const guildInfo = interaction.client.guildInfos.get(interaction.guild.id); beginHeight = guildInfo.treeHeight; } fn.timeToHeight(beginHeight, endHeight).then(res => {