Setup automatic water reminders

This commit is contained in:
Skylar Grant 2023-01-27 18:14:01 -05:00
parent 9b2ad681b8
commit a8ceecff9a
9 changed files with 305 additions and 60 deletions

View File

@ -34,6 +34,9 @@ client.once('ready', () => {
client.channels.fetch(statusChannelId).then(channel => { 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') { if (interaction.isButton() && interaction.component.customId == 'refresh') {
fn.refresh(interaction); fn.refresh(interaction);
} else if (interaction.isButton() && interaction.component.customId == 'resetping') {
fn.resetPing(interaction);
interaction.reply({ content: "Reset water readiness detection system.", ephemeral: true });
} }
}); });

View File

@ -81,7 +81,7 @@ module.exports = {
if (err) throw `Error connecting to the database: ${err.message}`; if (err) throw `Error connecting to the database: ${err.message}`;
}); });
// Get a server's tree information from the database // 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 } // 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) => { return new Promise((resolve, reject) => {
db.query(selectGuildInfoQuery, (err, res) => { db.query(selectGuildInfoQuery, (err, res) => {
@ -98,7 +98,9 @@ module.exports = {
"treeChannelId": "123", "treeChannelId": "123",
"leaderboardMessageId": "123", "leaderboardMessageId": "123",
"leaderboardChannelId": "123", "leaderboardChannelId": "123",
"pingRoleId": "123" "reminderMessage": "Abc",
"reminderChannelId": "123",
"remindedStatus": 0
};*/ };*/
if (res.length == 0) { if (res.length == 0) {
reject("There is no database entry for your guild yet. Try running /setup"); reject("There is no database entry for your guild yet. Try running /setup");
@ -113,7 +115,9 @@ module.exports = {
"treeChannelId": row.tree_channel_id, "treeChannelId": row.tree_channel_id,
"leaderboardMessageId": row.leaderboard_message_id, "leaderboardMessageId": row.leaderboard_message_id,
"leaderboardChannelId": row.leaderboard_channel_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(); db.end();
resolve({ "status": "Successfully fetched guild information", "data": guildInfo }); 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({ const db = mysql.createConnection({
host : process.env.DBHOST, host : process.env.DBHOST,
user : process.env.DBUSER, user : process.env.DBUSER,
@ -348,18 +352,131 @@ module.exports = {
if (err) throw `Error connecting to the database: ${err.message}`; if (err) throw `Error connecting to the database: ${err.message}`;
}); });
// Returns a Promise, resolve({ "status": "", "data": leaderboard }) // 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 } // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.query(insertPingRoleQuery, (err, res) => { db.query(insertReminderInfoQuery, (err, res) => {
if (err) { if (err) {
console.error(err); console.error(err);
db.end(); db.end();
reject("Error updating the ping role ID: " + err.message); reject("Error updating the reminder info: " + err.message);
return; return;
} }
db.end(); 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 });
}); });
}); });
} }

View File

@ -42,17 +42,26 @@ const functions = {
} }
}, },
builders: { builders: {
refreshAction() { async refreshAction(guildId) {
// Create the button to go in the Action Row // Create the button to go in the Action Row
const refreshButton = new ButtonBuilder() const refreshButton = new ButtonBuilder()
.setCustomId('refresh') .setCustomId('refresh')
.setLabel('Refresh') .setLabel('Refresh')
.setStyle(ButtonStyle.Primary); .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 // Create the Action Row with the Button in it, to be sent with the Embed
const refreshActionRow = new ActionRowBuilder() let refreshActionRow = new ActionRowBuilder()
.addComponents( .addComponents(
refreshButton refreshButton
); );
const getGuildInfoResponse = await dbfn.getGuildInfo(guildId);
const guildInfo = getGuildInfoResponse.data;
if (guildInfo.reminderMessage != "" && guildInfo.reminderChannelId != "") {
refreshActionRow.addComponents(resetPingButton);
}
return refreshActionRow; return refreshActionRow;
}, },
comparisonEmbed(content, refreshActionRow) { comparisonEmbed(content, refreshActionRow) {
@ -65,6 +74,16 @@ const functions = {
const messageContents = { embeds: [embed], components: [refreshActionRow] }; const messageContents = { embeds: [embed], components: [refreshActionRow] };
return messageContents; 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) { helpEmbed(content, private) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(strings.embeds.color) .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 // Build a string using the current leaderboard entry and the historic entry from 24 hours ago
comparisonReplyString += `\n${statusIndicator}\n`; 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; return comparisonReplyString;
} catch (err) { } catch (err) {
@ -270,8 +289,9 @@ const functions = {
refresh(interaction) { refresh(interaction) {
functions.rankings.parse(interaction).then(r1 => { functions.rankings.parse(interaction).then(r1 => {
functions.tree.parse(interaction).then(r2 => { functions.tree.parse(interaction).then(r2 => {
functions.rankings.compare(interaction).then(res => { functions.rankings.compare(interaction).then(async res => {
const embed = functions.builders.comparisonEmbed(res, functions.builders.refreshAction()) const refreshActionRow = await functions.builders.refreshAction(interaction.guildId);
const embed = functions.builders.comparisonEmbed(res, refreshActionRow)
interaction.update(embed); interaction.update(embed);
}).catch(err => { }).catch(err => {
console.error(err); console.error(err);
@ -341,17 +361,71 @@ const functions = {
resolve(time + units); resolve(time + units);
}); });
}, },
setReminder(interaction, time, pingRoleId) { sleep(ms) {
const reminderChannel = interaction.channel; return new Promise(resolve => setTimeout(resolve, ms));
setTimeout(function () { },
reminderChannel.send(`<@&${pingRoleId}>`).then(m => { 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) { if (m.deletable) {
setTimeout(function() { setTimeout(function() {
m.delete(); m.delete();
}, 60000); }, 60000);
} }
}).catch(err => {
console.error(err);
}); });
}, time); },
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);
});
}
},
resetPing(interaction) {
dbfn.setRemindedStatus(interaction.guildId, 0);
} }
}; };

View File

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

View File

@ -7,8 +7,9 @@ module.exports = {
.setDescription('See how your tree compares to other trees!'), .setDescription('See how your tree compares to other trees!'),
async execute(interaction) { async execute(interaction) {
interaction.deferReply().then(() => { interaction.deferReply().then(() => {
fn.rankings.compare(interaction).then(res => { fn.rankings.compare(interaction).then(async res => {
const embed = fn.builders.comparisonEmbed(res, fn.builders.refreshAction()); const refreshActionRow = await fn.builders.refreshAction(interaction.guildId);
const embed = fn.builders.comparisonEmbed(res, refreshActionRow);
interaction.editReply(embed).catch(err => { interaction.editReply(embed).catch(err => {
console.error(err); console.error(err);
}); });

20
slash-commands/optout.js Normal file
View File

@ -0,0 +1,20 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const dbfn = require('../modules/dbfn.js');
const fn = require('../modules/functions.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('optout')
.setDescription('Opt-out of automatic water reminders')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles),
async execute(interaction) {
try {
await interaction.deferReply({ ephemeral: true });
const setReminderOptInResponse = await dbfn.setReminderOptIn(interaction.guildId, 0);
interaction.editReply(setReminderOptInResponse.status);
} catch(err) {
console.error(err);
await interaction.editReply(fn.builders.errorEmbed(err));
}
},
};

View File

@ -1,18 +1,31 @@
const { SlashCommandBuilder } = require('discord.js'); const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const dbfn = require('../modules/dbfn.js'); const dbfn = require('../modules/dbfn.js');
const fn = require('../modules/functions.js'); const fn = require('../modules/functions.js');
module.exports = { module.exports = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('setping') .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) { async execute(interaction) {
try {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); const reminderMessage = interaction.options.getString('pingmsg');
const guildInfo = getGuildInfoResponse.data; const reminderChannel = interaction.options.getChannel('pingchannel');
const reminderTimeS = fn.getWaterTime(guildInfo.treeHeight); const setPingRoleResponse = await dbfn.setReminderInfo(interaction.guildId, reminderMessage, reminderChannel.id);
const reminderTimeMs = reminderTimeS * 1000; await dbfn.setReminderOptIn(interaction.guildId, 1);
fn.setReminder(interaction, reminderTimeMs, guildInfo.pingRoleId); interaction.editReply(setPingRoleResponse.status);
interaction.editReply("A reminder has been set."); } catch(err) {
console.error(err);
await interaction.editReply(fn.builders.errorEmbed(err));
}
}, },
}; };

View File

@ -0,0 +1,18 @@
const { SlashCommandBuilder } = require('discord.js');
const dbfn = require('../modules/dbfn.js');
const fn = require('../modules/functions.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('setping')
.setDescription('Run this command when you water your tree to have a reminder sent.'),
async execute(interaction) {
await interaction.deferReply({ ephemeral: true });
const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId);
const guildInfo = getGuildInfoResponse.data;
const reminderTimeS = fn.getWaterTime(guildInfo.treeHeight);
const reminderTimeMs = reminderTimeS * 1000;
fn.setReminder(interaction, reminderTimeMs, guildInfo.pingRoleId);
interaction.editReply("A reminder has been set.");
},
};

View File

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