From a8ceecff9a4d523e767dc94664af0d2f2c29732e Mon Sep 17 00:00:00 2001 From: Skylar Grant Date: Fri, 27 Jan 2023 18:14:01 -0500 Subject: [PATCH] Setup automatic water reminders --- main.js | 10 ++- modules/dbfn.js | 133 +++++++++++++++++++++++++++++++-- modules/functions.js | 104 ++++++++++++++++++++++---- slash-commands/DEV_dumptree.js | 20 +++++ slash-commands/compare.js | 5 +- slash-commands/optout.js | 20 +++++ slash-commands/setping.js | 31 +++++--- slash-commands/setping.js.bak | 18 +++++ slash-commands/setpingrole.js | 24 ------ 9 files changed, 305 insertions(+), 60 deletions(-) create mode 100644 slash-commands/DEV_dumptree.js create mode 100644 slash-commands/optout.js create mode 100644 slash-commands/setping.js.bak delete mode 100644 slash-commands/setpingrole.js diff --git a/main.js b/main.js index f556951..7ca988a 100644 --- a/main.js +++ b/main.js @@ -32,8 +32,11 @@ client.once('ready', () => { client.user.setActivity({ name: strings.activity.name, type: ActivityType.Watching }); if (isDev == 'false') { client.channels.fetch(statusChannelId).then(channel => { - channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete <@481933290912350209>`); - }); + channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete <@481933290912350209>`); + }); + } else { + // Dev shit + fn.checkReady(client); } }); @@ -55,6 +58,9 @@ client.on('interactionCreate', async interaction => { if (interaction.isButton() && interaction.component.customId == 'refresh') { fn.refresh(interaction); + } else if (interaction.isButton() && interaction.component.customId == 'resetping') { + fn.resetPing(interaction); + interaction.reply({ content: "Reset water readiness detection system.", ephemeral: true }); } }); diff --git a/modules/dbfn.js b/modules/dbfn.js index baca2c6..e70b63f 100644 --- a/modules/dbfn.js +++ b/modules/dbfn.js @@ -81,7 +81,7 @@ module.exports = { if (err) throw `Error connecting to the database: ${err.message}`; }); // Get a server's tree information from the database - const selectGuildInfoQuery = `SELECT tree_name, tree_height, tree_message_id, tree_channel_id, leaderboard_message_id, leaderboard_channel_id, ping_role_id FROM guild_info WHERE guild_id = ${db.escape(guildId)}`; + const selectGuildInfoQuery = `SELECT tree_name, tree_height, tree_message_id, tree_channel_id, leaderboard_message_id, leaderboard_channel_id, ping_role_id, ping_channel_id, reminded_status FROM guild_info WHERE guild_id = ${db.escape(guildId)}`; // TODO run this query and return a promise then structure the output into a GuildInfo object. resolve with { "status": , "data": guildInfo } return new Promise((resolve, reject) => { db.query(selectGuildInfoQuery, (err, res) => { @@ -98,7 +98,9 @@ module.exports = { "treeChannelId": "123", "leaderboardMessageId": "123", "leaderboardChannelId": "123", - "pingRoleId": "123" + "reminderMessage": "Abc", + "reminderChannelId": "123", + "remindedStatus": 0 };*/ if (res.length == 0) { reject("There is no database entry for your guild yet. Try running /setup"); @@ -113,7 +115,9 @@ module.exports = { "treeChannelId": row.tree_channel_id, "leaderboardMessageId": row.leaderboard_message_id, "leaderboardChannelId": row.leaderboard_channel_id, - "pingRoleId": row.ping_role_id + "pingRoleId": row.ping_role_id, + "pingChannelId": row.ping_channel_id, + "remindedStatus": row.reminded_status }; db.end(); resolve({ "status": "Successfully fetched guild information", "data": guildInfo }); @@ -336,7 +340,7 @@ module.exports = { }); }); }, - setPingRole(guildId, pingRoleId) { + setReminderInfo(guildId, reminderMessage, reminderChannelId) { const db = mysql.createConnection({ host : process.env.DBHOST, user : process.env.DBUSER, @@ -348,18 +352,131 @@ module.exports = { if (err) throw `Error connecting to the database: ${err.message}`; }); // Returns a Promise, resolve({ "status": "", "data": leaderboard }) - const insertPingRoleQuery = `UPDATE guild_info SET ping_role_id = ${db.escape(pingRoleId)} WHERE guild_id = ${db.escape(guildId)}`; + const insertReminderInfoQuery = `UPDATE guild_info SET ping_role_id = ${db.escape(reminderMessage)}, ping_channel_id = ${db.escape(reminderChannelId)} WHERE guild_id = ${db.escape(guildId)}`; // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } return new Promise((resolve, reject) => { - db.query(insertPingRoleQuery, (err, res) => { + db.query(insertReminderInfoQuery, (err, res) => { if (err) { console.error(err); db.end(); - reject("Error updating the ping role ID: " + err.message); + reject("Error updating the reminder info: " + err.message); return; } db.end(); - resolve({ "status": `Successfully set the ping role to <@&${pingRoleId}>.`, "data": res }); + resolve({ "status": `Successfully set the reminder message to "${reminderMessage}" in <#${reminderChannelId}>`, "data": res }); + }); + }); + }, + setRemindedStatus(guildId, remindedStatus) { + const db = mysql.createConnection({ + host : process.env.DBHOST, + user : process.env.DBUSER, + password : process.env.DBPASS, + database : process.env.DBNAME, + port : process.env.DBPORT + }); + db.connect((err) => { + if (err) throw `Error connecting to the database: ${err.message}`; + }); + // Returns a Promise, resolve({ "status": "", "data": leaderboard }) + const setRemindedStatusQuery = `UPDATE guild_info SET reminded_status = ${db.escape(remindedStatus)} WHERE guild_id = ${db.escape(guildId)}`; + // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } + return new Promise((resolve, reject) => { + db.query(setRemindedStatusQuery, (err, res) => { + if (err) { + console.error(err); + db.end(); + reject("Error updating the reminded status: " + err.message); + return; + } + db.end(); + resolve({ "status": `Successfully set the reminded status to ${remindedStatus}`, "data": res }); + }); + }); + }, + setReminderOptIn(guildId, optIn) { + const db = mysql.createConnection({ + host : process.env.DBHOST, + user : process.env.DBUSER, + password : process.env.DBPASS, + database : process.env.DBNAME, + port : process.env.DBPORT + }); + db.connect((err) => { + if (err) throw `Error connecting to the database: ${err.message}`; + }); + // Returns a Promise, resolve({ "status": "", "data": leaderboard }) + const setReminderOptInQuery = `UPDATE guild_info SET reminder_optin = ${db.escape(optIn)} WHERE guild_id = ${db.escape(guildId)}`; + // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } + return new Promise((resolve, reject) => { + db.query(setReminderOptInQuery, (err, res) => { + if (err) { + console.error(err); + db.end(); + reject("Error updating the reminder opt-in status: " + err.message); + return; + } + db.end(); + resolve({ "status": `Successfully set the reminder opt-in status to ${optIn}`, "data": res }); + }); + }); + }, + getOptedInGuilds() { + const db = mysql.createConnection({ + host : process.env.DBHOST, + user : process.env.DBUSER, + password : process.env.DBPASS, + database : process.env.DBNAME, + port : process.env.DBPORT + }); + db.connect((err) => { + if (err) throw `Error connecting to the database: ${err.message}`; + }); + // Get a server's tree information from the database + const getOptedInGuildsQuery = `SELECT guild_id, tree_name, tree_height, tree_message_id, tree_channel_id, leaderboard_message_id, leaderboard_channel_id, ping_role_id, ping_channel_id, reminded_status FROM guild_info WHERE reminder_optin = 1 AND reminded_status = 0`; + // TODO run this query and return a promise then structure the output into a GuildInfo object. resolve with { "status": , "data": guildInfo } + return new Promise((resolve, reject) => { + db.query(getOptedInGuildsQuery, (err, res) => { + if (err) { + console.error(err); + reject("Error fetching guild information: " + err.message); + db.end(); + return; + } + /*const guildInfo = { "guildId": "123", + "treeName": "name", + "treeHeight": 123, + "treeMessageId": "123", + "treeChannelId": "123", + "leaderboardMessageId": "123", + "leaderboardChannelId": "123", + "reminderMessage": "Abc", + "reminderChannelId": "123", + "remindedStatus": 0 + };*/ + if (res.length == 0) { + resolve({"status": "No servers have opted in yet"}); + db.end(); + return; + } + row = res[0]; + let guilds = []; + res.forEach(row => { + guilds.push({ + "guildId": row.guild_id, + "treeName": row.tree_name, + "treeHeight": row.tree_height, + "treeMessageId": row.tree_message_id, + "treeChannelId": row.tree_channel_id, + "leaderboardMessageId": row.leaderboard_message_id, + "leaderboardChannelId": row.leaderboard_channel_id, + "reminderMessage": row.ping_role_id, + "reminderChannelId": row.ping_channel_id, + "remindedStatus": row.reminded_status + }); + }); + db.end(); + resolve({ "status": "Successfully fetched guild information", "data": guilds }); }); }); } diff --git a/modules/functions.js b/modules/functions.js index 11667a9..950e8e0 100644 --- a/modules/functions.js +++ b/modules/functions.js @@ -42,17 +42,26 @@ const functions = { } }, builders: { - refreshAction() { + async refreshAction(guildId) { // Create the button to go in the Action Row const refreshButton = new ButtonBuilder() .setCustomId('refresh') .setLabel('Refresh') .setStyle(ButtonStyle.Primary); + const resetPingButton = new ButtonBuilder() + .setCustomId('resetping') + .setLabel('Reset Ping') + .setStyle(ButtonStyle.Secondary); // Create the Action Row with the Button in it, to be sent with the Embed - const refreshActionRow = new ActionRowBuilder() + let refreshActionRow = new ActionRowBuilder() .addComponents( refreshButton ); + const getGuildInfoResponse = await dbfn.getGuildInfo(guildId); + const guildInfo = getGuildInfoResponse.data; + if (guildInfo.reminderMessage != "" && guildInfo.reminderChannelId != "") { + refreshActionRow.addComponents(resetPingButton); + } return refreshActionRow; }, comparisonEmbed(content, refreshActionRow) { @@ -65,6 +74,16 @@ const functions = { const messageContents = { embeds: [embed], components: [refreshActionRow] }; return messageContents; }, + reminderEmbed(content, guildInfo) { + // Create the embed using the content passed to this function + const embed = new EmbedBuilder() + .setColor(strings.embeds.color) + .setTitle('Water Reminder') + .setDescription(`${content}\n[Your Tree](https://discord.com/channels/${guildInfo.guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId})`) + .setFooter({text: `This message will self-destruct in 60 seconds...`}); + const messageContents = { embeds: [embed] }; + return messageContents; + }, helpEmbed(content, private) { const embed = new EmbedBuilder() .setColor(strings.embeds.color) @@ -217,7 +236,7 @@ const functions = { } // Build a string using the current leaderboard entry and the historic entry from 24 hours ago comparisonReplyString += `\n${statusIndicator}\n`; - if (process.env.isDev == 'true') comparisonReplyString += `Current Height: ${leaderboardEntry.treeHeight} 24h Ago Height: ${dayAgoTree.treeHeight}\n`; + // if (process.env.isDev == 'true') comparisonReplyString += `Current Height: ${leaderboardEntry.treeHeight} 24h Ago Height: ${dayAgoTree.treeHeight}\n`; } return comparisonReplyString; } catch (err) { @@ -270,8 +289,9 @@ const functions = { refresh(interaction) { functions.rankings.parse(interaction).then(r1 => { functions.tree.parse(interaction).then(r2 => { - functions.rankings.compare(interaction).then(res => { - const embed = functions.builders.comparisonEmbed(res, functions.builders.refreshAction()) + functions.rankings.compare(interaction).then(async res => { + const refreshActionRow = await functions.builders.refreshAction(interaction.guildId); + const embed = functions.builders.comparisonEmbed(res, refreshActionRow) interaction.update(embed); }).catch(err => { console.error(err); @@ -341,17 +361,71 @@ const functions = { resolve(time + units); }); }, - setReminder(interaction, time, pingRoleId) { - const reminderChannel = interaction.channel; - setTimeout(function () { - reminderChannel.send(`<@&${pingRoleId}>`).then(m => { - if (m.deletable) { - setTimeout(function() { - m.delete(); - }, 60000); - } + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + }, + async sendReminder(guildInfo, guild) { + const { guildId, reminderChannelId, reminderMessage } = guildInfo; + const reminderChannel = await guild.channels.fetch(reminderChannelId); + const reminderEmbed = functions.builders.reminderEmbed(reminderMessage, guildInfo); + reminderChannel.send(reminderEmbed).then(async m => { + await dbfn.setRemindedStatus(guildId, 1); + if (m.deletable) { + setTimeout(function() { + m.delete(); + }, 60000); + } + }).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 + // TODO This is hard coded for the dev server, need to change it to lookup each server and iterate over them + // Would also be helpful to have an opt-in or opt-out ability for water checks + try { + const getOptedInGuildsResponse = await dbfn.getOptedInGuilds(); + if (getOptedInGuildsResponse.status != "No servers have opted in yet") { + const guilds = getOptedInGuildsResponse.data; + guilds.forEach(async guildInfo => { + const { guildId, treeChannelId, treeMessageId, remindedStatus } = guildInfo; + + if (remindedStatus == 0) { + const guild = await client.guilds.fetch(guildId); + const treeChannel = await guild.channels.fetch(treeChannelId); + const treeMessage = await treeChannel.messages.fetch(treeMessageId); + const readyToWater = treeMessage.embeds[0].description.includes('Ready to be watered'); + if (readyToWater) { + console.log("Ready to water"); + this.sendReminder(guildInfo, guild); + this.sleep(5000).then(() => { + this.checkReady(client); + }); + } else { + console.log("Not ready to water"); + this.sleep(5000).then(() => { + this.checkReady(client); + }); + } + } + }); + } else { + // console.log(getOptedInGuildsResponse.status); + this.sleep(5000).then(() => { + this.checkReady(client); + }); + } + } catch(err) { + console.error(err); + this.sleep(30000).then(() => { + this.checkReady(client); }); - }, time); + } + }, + resetPing(interaction) { + dbfn.setRemindedStatus(interaction.guildId, 0); } }; diff --git a/slash-commands/DEV_dumptree.js b/slash-commands/DEV_dumptree.js new file mode 100644 index 0000000..8301c66 --- /dev/null +++ b/slash-commands/DEV_dumptree.js @@ -0,0 +1,20 @@ +const { SlashCommandBuilder } = require('discord.js'); +const dbfn = require('../modules/dbfn.js'); +const fn = require('../modules/functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('dumptree') + .setDescription('Dump the contents of the tree message to console'), + async execute(interaction) { + await interaction.deferReply({ ephemeral: true }); + const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); + const { treeMessageId, treeChannelId } = getGuildInfoResponse.data; + interaction.guild.channels.fetch(treeChannelId).then(treeChannel => { + treeChannel.messages.fetch(treeMessageId).then(treeMessage => { + interaction.editReply("done"); + console.log(JSON.stringify(treeMessage.embeds[0])); + }); + }); + }, +}; \ No newline at end of file diff --git a/slash-commands/compare.js b/slash-commands/compare.js index 37864f8..7268c1c 100644 --- a/slash-commands/compare.js +++ b/slash-commands/compare.js @@ -7,8 +7,9 @@ module.exports = { .setDescription('See how your tree compares to other trees!'), async execute(interaction) { interaction.deferReply().then(() => { - fn.rankings.compare(interaction).then(res => { - const embed = fn.builders.comparisonEmbed(res, fn.builders.refreshAction()); + fn.rankings.compare(interaction).then(async res => { + const refreshActionRow = await fn.builders.refreshAction(interaction.guildId); + const embed = fn.builders.comparisonEmbed(res, refreshActionRow); interaction.editReply(embed).catch(err => { console.error(err); }); diff --git a/slash-commands/optout.js b/slash-commands/optout.js new file mode 100644 index 0000000..1d1cf37 --- /dev/null +++ b/slash-commands/optout.js @@ -0,0 +1,20 @@ +const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); +const dbfn = require('../modules/dbfn.js'); +const fn = require('../modules/functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('optout') + .setDescription('Opt-out of automatic water reminders') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles), + async execute(interaction) { + try { + await interaction.deferReply({ ephemeral: true }); + const setReminderOptInResponse = await dbfn.setReminderOptIn(interaction.guildId, 0); + interaction.editReply(setReminderOptInResponse.status); + } catch(err) { + console.error(err); + await interaction.editReply(fn.builders.errorEmbed(err)); + } + }, +}; \ No newline at end of file diff --git a/slash-commands/setping.js b/slash-commands/setping.js index 4d13791..8674e5f 100644 --- a/slash-commands/setping.js +++ b/slash-commands/setping.js @@ -1,18 +1,31 @@ -const { SlashCommandBuilder } = require('discord.js'); +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('Run this command when you water your tree to have a reminder sent.'), + .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) { - 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."); + 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 new file mode 100644 index 0000000..4d13791 --- /dev/null +++ b/slash-commands/setping.js.bak @@ -0,0 +1,18 @@ +const { SlashCommandBuilder } = require('discord.js'); +const dbfn = require('../modules/dbfn.js'); +const fn = require('../modules/functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('setping') + .setDescription('Run this command when you water your tree to have a reminder sent.'), + async execute(interaction) { + await interaction.deferReply({ ephemeral: true }); + const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); + const guildInfo = getGuildInfoResponse.data; + const reminderTimeS = fn.getWaterTime(guildInfo.treeHeight); + const reminderTimeMs = reminderTimeS * 1000; + fn.setReminder(interaction, reminderTimeMs, guildInfo.pingRoleId); + interaction.editReply("A reminder has been set."); + }, +}; \ No newline at end of file diff --git a/slash-commands/setpingrole.js b/slash-commands/setpingrole.js deleted file mode 100644 index a79fc44..0000000 --- a/slash-commands/setpingrole.js +++ /dev/null @@ -1,24 +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('setpingrole') - .setDescription('Set the role to ping when you run /setping') - .addRoleOption(o => - o.setName('pingrole') - .setDescription('The role to ping') - .setRequired(true)), - async execute(interaction) { - try { - await interaction.deferReply({ ephemeral: true }); - const pingRole = interaction.options.getRole('pingrole'); - const setPingRoleResponse = await dbfn.setPingRole(interaction.guildId, pingRole.id); - interaction.editReply(setPingRoleResponse.status); - } catch(err) { - console.error(err); - await interaction.editReply(fn.builders.errorEmbed(err)); - } - }, -}; \ No newline at end of file