Setup automatic water reminders
This commit is contained in:
parent
9b2ad681b8
commit
a8ceecff9a
6
main.js
6
main.js
@ -34,6 +34,9 @@ client.once('ready', () => {
|
||||
client.channels.fetch(statusChannelId).then(channel => {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
|
133
modules/dbfn.js
133
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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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 => {
|
||||
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);
|
||||
});
|
||||
}, 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);
|
||||
}
|
||||
};
|
||||
|
||||
|
20
slash-commands/DEV_dumptree.js
Normal file
20
slash-commands/DEV_dumptree.js
Normal 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]));
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
@ -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);
|
||||
});
|
||||
|
20
slash-commands/optout.js
Normal file
20
slash-commands/optout.js
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
@ -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) {
|
||||
try {
|
||||
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.");
|
||||
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));
|
||||
}
|
||||
},
|
||||
};
|
18
slash-commands/setping.js.bak
Normal file
18
slash-commands/setping.js.bak
Normal 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.");
|
||||
},
|
||||
};
|
@ -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));
|
||||
}
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user