Add roles management per reaction on a message and event reminder with role ping

This commit is contained in:
crylia
2024-09-12 05:13:56 +02:00
parent 721b0e1853
commit 677e74764c
9 changed files with 507 additions and 131 deletions

View File

@@ -1,6 +1,5 @@
BOT_TOKEN= BOT_TOKEN=
CLIENT_ID= CLIENT_ID=
SHEET_ID=
DB_NAME= DB_NAME=
DB_PORT= DB_PORT=
DB_HOST= DB_HOST=

View File

@@ -1,6 +1,6 @@
# Tochi's Discord Bot # Tochi's Discord Bot
A simple discord bot for the Limited Edition legion in the game Aion Classic that interfaces with a PostgreSQL database A simple discord bot for the Limited Edition guild in the game Aion Classic that interfaces with a PostgreSQL database
The bot is made to run on a single server and will thus share the same database The bot is made to run on a single server and will thus share the same database

49
src/database/eventdb.js Normal file
View File

@@ -0,0 +1,49 @@
const { client } = require('./database')
const ReadEvents = async () => {
try {
const res = await client.query(`
SELECT * FROM event_times;`
)
return res.rows
} catch (error) {
console.error('Error reading event entries:', error)
return false
}
}
const GetEventRole = async () => {
try {
const res = await client.query(`
SELECT * FROM event_role_view;`
)
const rolesEventMap = new Map()
res.rows.forEach(row => rolesEventMap.set(row.event_name, row.role))
return rolesEventMap
} catch (error) {
console.error(error)
return false
}
}
const GetIconRole = async () => {
try {
const res = await client.query(`
SELECT role, icon_name FROM event_roles;`
)
const rolesEventMap = new Map()
res.rows.forEach(row => rolesEventMap.set(row.icon_name, row.role))
return rolesEventMap
} catch (error) {
console.error(error)
return false
}
}
module.exports = {
ReadEvents,
GetEventRole,
GetIconRole,
}

59
src/database/staticdb.js Normal file
View File

@@ -0,0 +1,59 @@
const { client } = require('./database')
const CreateStatic = async (name, creator, members, size) => {
try {
await client.query(`
INSERT INTO static (name, creator, size)
VALUES ($1, $2, $3)
RETURNING id;`,
[name, creator, size]
)
const staticId = result.rows[0].id;
const memberValues = members.map(member => `(${staticId}, '${member}')`).join(',')
await client.query(`
INSERT INTO static_members (static_id, member)
VALUES ${memberValues};`
)
return true
} catch (error) {
console.error('Error creating static entry:', error)
return false
}
}
const ReadStatic = async (name) => {
try {
const res = await client.query(`
SELECT * FROM static
WHERE name = $1;`,
[name]
)
return res.rows
} catch (error) {
console.error('Error reading static entry:', error)
return false
}
}
const DeleteStatic = async (name) => {
try {
await client.query(`
DELETE FROM static
WHERE name = $1;`,
[name]
)
} catch (error) {
console.error('Error deleting static entry:', error)
return false
}
}
module.exports = {
CreateStatic,
ReadStatic,
DeleteStatic,
}

View File

@@ -1,12 +1,18 @@
const { Client, GatewayIntentBits, EmbedBuilder, Partials, Routes } = require('discord.js') const { Client, GatewayIntentBits, EmbedBuilder, Partials, Routes } = require('discord.js')
const { REST } = require('@discordjs/rest') const { REST } = require('@discordjs/rest')
const { SlashCommandBuilder } = require('@discordjs/builders') const { SlashCommandBuilder } = require('@discordjs/builders')
const {
initReactionPerRole,
messageReactionAdd,
messageReactionRemove,
} = require('../features/reactionPerRole')
const { const {
handleBlacklistAdd, handleBlacklistAdd,
handleBlacklistCheck, handleBlacklistCheck,
handleBlacklistShow, handleBlacklistShow,
} = require('../features/blacklist') } = require('../features/blacklist')
const { const {
handleBirthdayAdd, handleBirthdayAdd,
handleBirthdayUpdate, handleBirthdayUpdate,
@@ -15,172 +21,97 @@ const {
} = require('../features/birthday') } = require('../features/birthday')
const { startBirthdayCheckCron } = require('../tasks/checkBirthday') const { startBirthdayCheckCron } = require('../tasks/checkBirthday')
const { startEventCheckCron } = require('../tasks/eventReminder')
const client = new Client({ const client = new Client({
intents: [ intents: [
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMembers,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessageReactions,
], ],
partials: [ partials: [
Partials.Channel, Partials.Channel,
] ]
}) })
const createBlacklistEmbeds = (entries) => { const createBlacklistEmbeds = (playerEntries, maxChars = 30) => {
const embeds = []; const embeds = []
let currentEmbed = new EmbedBuilder() let embed = new EmbedBuilder()
.setTitle('Blacklisted Players') .setColor('#0099ff')
.setColor('#EF9A9A'); .setTitle('Blacklist (Page 1)')
.setDescription('Players who have been blacklisted and the reasons.')
let currentEntries = []; let fieldCount = 0
let first = true; let pageIndex = 1
const breakTextIntoLines = (text, maxLength) => { playerEntries.forEach(([playerName, reason]) => {
const words = text.split(' '); let splitReason = []
const lines = []; while (reason.length > maxChars) {
let currentLine = ''; let splitIndex = reason.lastIndexOf(' ', maxChars)
if (splitIndex === -1) splitIndex = maxChars
splitReason.push(reason.substring(0, splitIndex))
reason = reason.substring(splitIndex + 1)
}
splitReason.push(reason)
for (let i = 0; i < words.length; i++) { const nameField = playerName || '\u200B'
if (words[i].length > maxLength) { const valueField = splitReason.join('\n').trim() || '\u200B'
const splitWord = words[i].match(new RegExp(`.{1,${maxLength}}`, 'g')) || [];
for (let j = 0; j < splitWord.length; j++) { if (valueField !== '\u200B') {
if (j === 0 && currentLine) { embed.addFields({ name: nameField, value: valueField, inline: true })
lines.push(currentLine.trim()); fieldCount++
currentLine = '';
}
lines.push(splitWord[j]);
}
} else if ((currentLine + words[i]).length <= maxLength) {
currentLine += words[i] + ' ';
} else {
lines.push(currentLine.trim());
currentLine = words[i] + ' ';
}
} }
if (currentLine) { if (fieldCount >= 25) {
lines.push(currentLine.trim()); embeds.push(embed)
pageIndex++
embed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle(`Blacklist (Page ${pageIndex})`)
.setDescription('Players who have been blacklisted and the reasons.')
fieldCount = 0
} }
})
return lines; if (fieldCount > 0) {
}; embeds.push(embed)
const addEntryToEmbed = (entriesGroup) => {
let playerNames = '';
let reasons = '';
let reportedBys = '';
for (let i = 0; i < entriesGroup.length; i++) {
const entry = entriesGroup[i];
const name = entry.name || 'Unknown';
const reason = entry.reason || 'No reason provided';
const nameLines = breakTextIntoLines(name, 20);
const reasonLines = breakTextIntoLines(reason, 30);
const maxLines = Math.max(nameLines.length, reasonLines.length);
for (let j = 0; j < maxLines; j++) {
playerNames += (j < nameLines.length ? nameLines[j] : '\u200B') + '\n';
reasons += (j < reasonLines.length ? reasonLines[j] : '\u200B') + '\n';
reportedBys += (j === 0 ? entry.reportedby || 'Unknown' : '\u200B') + '\n';
}
}
let newFields = [];
if (first) {
newFields = [
{ name: 'Player', value: playerNames, inline: true },
{ name: 'Reason', value: reasons, inline: true },
{ name: 'Reported By', value: reportedBys, inline: true }
];
first = false;
} else {
newFields = [
{ name: '\u200B', value: playerNames, inline: true },
{ name: '\u200B', value: reasons, inline: true },
{ name: '\u200B', value: reportedBys, inline: true }
];
}
for (const field of newFields) {
currentEmbed.addFields(field);
}
};
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const tempEntries = [...currentEntries, entry];
let tempPlayerNames = '';
let tempReasons = '';
let tempReportedBys = '';
for (let j = 0; j < tempEntries.length; j++) {
const name = tempEntries[j].name || 'Unknown';
const reason = tempEntries[j].reason || 'No reason provided';
const nameLines = breakTextIntoLines(name, 20);
const reasonLines = breakTextIntoLines(reason, 30);
const maxLines = Math.max(nameLines.length, reasonLines.length);
for (let k = 0; k < maxLines; k++) {
tempPlayerNames += (k < nameLines.length ? nameLines[k] : '\u200B') + '\n';
tempReasons += (k < reasonLines.length ? reasonLines[k] : '\u200B') + '\n';
tempReportedBys += (k === 0 ? tempEntries[j].reportedby || 'Unknown' : '\u200B') + '\n';
}
}
const exceedsLimit = (str) => str.length > 1024;
if (exceedsLimit(tempPlayerNames) || exceedsLimit(tempReasons) || exceedsLimit(tempReportedBys)) {
if (currentEntries.length > 0) {
addEntryToEmbed(currentEntries);
currentEntries = [];
}
}
currentEntries.push(entry);
} }
if (currentEntries.length > 0) { return embeds
addEntryToEmbed(currentEntries); }
}
embeds.push(currentEmbed);
return embeds;
};
const updateGlobalMessage = async () => { const updateGlobalMessage = async () => {
try { try {
let targetChannel = null; let targetChannel = null
for (const [_, oauthGuild] of await client.guilds.fetch()) { for (const [_, oauthGuild] of await client.guilds.fetch()) {
targetChannel = (await (await oauthGuild.fetch()).channels.fetch()).find(ch => ch.name === 'blacklist') targetChannel = (await (await oauthGuild.fetch()).channels.fetch()).find(ch => ch.name === 'blacklist')
break; break
} }
if (!targetChannel) { if (!targetChannel) {
console.error('Channel with name "blacklist" not found.'); console.error('Channel with name "blacklist" not found.')
return; return
} }
const messages = await targetChannel.messages.fetch({ limit: 100 }); const messages = await targetChannel.messages.fetch({ limit: 100 })
await Promise.all(messages.map(msg => msg.delete())); await Promise.all(messages.map(msg => msg.delete()))
const blacklistEntries = await handleBlacklistShow(); const blacklistEntries = await handleBlacklistShow()
const embeds = createBlacklistEmbeds(blacklistEntries);
const playerEntries = blacklistEntries.map(entry => [entry.name, entry.reason])
const embeds = createBlacklistEmbeds(playerEntries)
for (const embed of embeds) for (const embed of embeds)
await targetChannel.send({ embeds: [embed] }); await targetChannel.send({ embeds: [embed] })
} catch (error) { } catch (error) {
console.error('Error updating global message:', error); console.error('Error updating global message:', error)
} }
}; }
client.on('interactionCreate', async interaction => { client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return if (!interaction.isCommand()) return
@@ -208,7 +139,7 @@ client.on('interactionCreate', async interaction => {
const reason2 = await handleBlacklistCheck(player) const reason2 = await handleBlacklistCheck(player)
reason2 ? reason2 ?
await interaction.reply({ content: `** ${reason2.name}** is blacklisted for: ** ${reason2.reason || 'No reason provided.'}** (by ${reason2.reportedby || 'unknown'}).`, ephemeral: true }) : await interaction.reply({ content: `** ${reason2.name}** is blacklisted for: ** ${reason2.reason || 'No reason provided.'}**`, ephemeral: true }) :
await interaction.reply({ content: `** ${player}** is not blacklisted.`, ephemeral: true }) await interaction.reply({ content: `** ${player}** is not blacklisted.`, ephemeral: true })
break break
@@ -257,17 +188,38 @@ client.on('interactionCreate', async interaction => {
await interaction.reply({ content: "Your birthday has been deleted.", ephemeral: true }) : await interaction.reply({ content: "Your birthday has been deleted.", ephemeral: true }) :
await interaction.reply({ content: "You don't have a birthday set.", ephemeral: true }) await interaction.reply({ content: "You don't have a birthday set.", ephemeral: true })
break break
case 'static-create':
break
case 'static-delete':
break
case 'static-show':
break
default: default:
break break
} }
}) })
client.on('messageReactionAdd', async (reaction, user) => {
messageReactionAdd(user, reaction)
})
client.on('messageReactionRemove', async (reaction, user) => {
messageReactionRemove(user, reaction)
})
const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN) const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN)
client.once('ready', async () => { client.once('ready', async () => {
console.log(`Logged in as ${client.user.tag} `) console.log(`Logged in as ${client.user.tag} `)
startBirthdayCheckCron(client) startBirthdayCheckCron(client)
startEventCheckCron(client)
updateGlobalMessage() updateGlobalMessage()
initReactionPerRole(client)
}) })
const connectDiscord = async () => { const connectDiscord = async () => {

View File

@@ -0,0 +1,206 @@
const { GetIconRole } = require('../database/eventdb')
const cron = require('node-cron');
let reminderMessageID = null
let rolesMap = new Map()
const areMapsEqual = (map1, map2) => {
if (map1.size !== map2.size) return false
for (const [key, value] of map1)
if (map2.get(key) !== value) return false
return true
}
const editReactionMessage = async (client) => {
try {
for (const [_, oauthGuild] of await client.guilds.fetch()) {
const guild = await oauthGuild.fetch()
const rolesChannel = (await guild.channels.fetch()).find(ch => ch.name === 'roles')
if (!rolesChannel) {
console.log("Channel not found")
return
}
const message = await rolesChannel.messages.fetch(reminderMessageID)
const newRolesFromDB = await GetIconRole()
if (areMapsEqual(rolesMap, newRolesFromDB)) {
console.log(`No changes to the roles, quitting`)
return
}
rolesMap = newRolesFromDB
const currentLines = message.content.split('\n').slice(1)
const currentRoleMap = new Map()
currentLines.forEach(line => {
if (!line.trim()) return
const [icon, roleName] = line.split(': ')
const iconName = icon.match(/<:(\w+):\d+>/)[1]
currentRoleMap.set(iconName, roleName.replace(/`/g, ''))
})
const rolesToAdd = [], rolesToRemove = []
for (const [iconName, roleName] of rolesMap)
if (!currentRoleMap.has(iconName))
rolesToAdd.push({ icon: iconName, name: roleName })
for (const [iconName, roleName] of currentRoleMap)
if (!rolesMap.has(iconName))
rolesToRemove.push({ icon: iconName, name: roleName })
const emojis = await guild.emojis.fetch()
let updatedContent = 'React to get your roles!\n\n'
rolesMap.forEach((roleName, iconName) => {
const emoji = emojis.find(e => e.name === iconName)
if (emoji)
updatedContent += `${emoji}: \`${roleName}\`\n`
else
console.log(`Couldn't find emoji ${iconName}`)
})
await message.edit(updatedContent)
for (const role of rolesToAdd) {
const icon = emojis.find(e => e.name === role.icon)
if (icon)
await message.react(`${icon}`)
}
for (const role of rolesToRemove) {
const reaction = message.reactions.cache.find(r => r.emoji.name === role.icon)
if (!reaction) continue
await reaction.remove()
const roleToRemove = message.guild.roles.cache.find(r => r.name === role.name)
if (!roleToRemove) continue
const members = await message.guild.members.fetch();
const membersWithRole = members.filter(member => member.roles.cache.has(roleToRemove.id));
for (const member of membersWithRole.values())
await member.roles.remove(roleToRemove)
}
}
} catch (error) {
console.log(error)
}
}
const createReactionMessage = async (client) => {
try {
for (const [_, oauthGuild] of await client.guilds.fetch()) {
const guild = await oauthGuild.fetch()
const rolesChannel = (await guild.channels.fetch()).find(ch => ch.name === 'roles')
if (!rolesChannel) {
console.log("Channel not found")
return
}
const fetchedMessage = await rolesChannel.messages.fetch({ limit: 1 })
if (fetchedMessage.size > 0) {
reminderMessageID = fetchedMessage.first().id
console.log('Reminder MessageID: ', reminderMessageID)
return
}
const emojis = await guild.emojis.fetch()
let message = 'React to get your roles!\n\n'
for (const [iconName, roleName] of rolesMap) {
const emoji = emojis.find(e => e.name === iconName)
if (emoji)
message += `<:${emoji.name}:${emoji.id}> : \`${roleName}\`\n`
else
console.log(`Emoji for ${iconName} not found`)
}
const sentMessage = await rolesChannel.send(message)
for (const [iconName, _] of rolesMap) {
const emoji = emojis.find(e => e.name === iconName)
if (emoji) await sentMessage.react(emoji.id)
}
reminderMessageID = sentMessage.id
}
} catch (error) {
console.log(error)
}
}
const messageReactionAdd = async (user, reaction) => {
if (user.id === '1280557738530963506') return
const { message, emoji } = reaction
const guild = message.guild
const member = await guild.members.fetch(user.id)
if (!member) return
if (message.id === reminderMessageID) {
const roleMap = await GetIconRole()
const roleName = roleMap.get(emoji.name)
if (roleName) {
try {
const roles = await guild.roles.fetch()
const role = roles.find(r => r.name === roleName)
if (role && !member.roles.cache.has(role.id))
await member.roles.add(role)
} catch (error) {
console.error(`Error fetching role for ${roleName}`, error)
}
}
}
}
const messageReactionRemove = async (user, reaction) => {
const { message, emoji } = reaction
const guild = message.guild
const member = await guild.members.fetch(user.id)
if (!member) return
if (message.id === reminderMessageID) {
const roleMap = await GetIconRole()
const roleName = roleMap.get(emoji.name)
if (roleName) {
try {
const roles = await guild.roles.fetch()
const role = roles.find(r => r.name === roleName)
if (role && member.roles.cache.has(role.id))
await member.roles.remove(role)
} catch (error) {
console.error(`Error fetching role for ${roleName}`, error)
}
}
}
}
const initReactionPerRole = async (client) => {
rolesMap = await GetIconRole()
await createReactionMessage(client)
cron.schedule('0 * * * *', async () => {
await editReactionMessage(client)
})
}
module.exports = {
initReactionPerRole,
messageReactionAdd,
messageReactionRemove,
}

31
src/features/static.js Normal file
View File

@@ -0,0 +1,31 @@
const { CreateStatic, ReadStatic, DeleteStatic } = require('../database/staticdb')
const handleStaticAdd = async (name, creator, members, size) => {
if (!name || !createor || !members || !size) return false
const result = await CreateStatic(name, creator, members, size)
return result
}
const handleStaticGet = async (name) => {
if (!name) return false;
const result = await ReadStatic(name)
return result
}
const handleStaticDelete = async (name) => {
if (!name) return false
const result = await DeleteStatic(name)
return result
}
module.exports = {
handleStaticAdd,
handleStaticGet,
handleStaticDelete,
}

View File

@@ -0,0 +1,80 @@
const cron = require('node-cron');
const { ReadEvents, GetEventRole } = require('../database/eventdb')
const { EmbedBuilder } = require('discord.js')
let eventCache = []
const FetchEvents = async () => {
const res = await ReadEvents()
eventCache = res
}
const convertToISO = (time, date) => {
const utcDate = new Date(`${date}T${time}`);
utcDate.setHours(utcDate.getHours());
return utcDate.toISOString().split('.')[0];
};
const convertToUTC = (time) => {
const [hours, minutes, seconds] = time.split(':').map(Number);
const date = new Date();
date.setUTCHours(hours - 2, minutes, seconds);
return date.toISOString().split('.')[0];
};
const isReminderTime = (eventStartUTC) => {
const now = new Date();
const [eventHours, eventMinutes] = eventStartUTC.split('T')[1].split(':').map(Number);
const eventStartDateTime = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), eventHours, eventMinutes));
const reminderTime = new Date(eventStartDateTime.getTime() - 15 * 60 * 1000);
console.log(now, reminderTime, eventStartDateTime)
return now >= reminderTime && now < eventStartDateTime;
};
const getDayOfWeek = () => new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate())).toLocaleDateString('en-US', { weekday: 'long' });
const startEventCheckCron = async (client) => {
cron.schedule('45 * * * *', async () => {
console.log('Checking for events...')
if (!eventCache.length) {
await FetchEvents();
}
const rolesEventMap = await GetEventRole()
for (const [guildId, oauthGuild] of await client.guilds.fetch()) {
const guild = await oauthGuild.fetch()
const channel = (await guild.channels.fetch()).find(ch => ch.name === 'reminder')
if (!channel) continue
eventCache.forEach(event => {
let { schedule_id, event_name, start_time, end_time, day_of_week } = event;
console.log(!((!day_of_week || day_of_week === getDayOfWeek()) && isReminderTime(convertToUTC(start_time))))
// Abbort if its not time to send a reminder
if (!((!day_of_week || day_of_week === getDayOfWeek()) && isReminderTime(convertToUTC(start_time)))) return;
const timeComparisonLink = `https://www.timeanddate.com/worldclock/fixedtime.html?iso=${convertToISO(start_time, new Date().toISOString().split('T')[0])}&msg=${encodeURIComponent(event_name)}`;
console.log("Sending event...")
console.log(rolesEventMap.get(event_name))
const embed = new EmbedBuilder()
.setTitle(event_name)
.setDescription(`${event_name} starts at **${start_time} CEST** and ends at **${end_time} CEST**. \n<@${rolesEventMap.get(event_name)}>\n`)
.addFields(
{ name: 'Day of Week', value: day_of_week ? day_of_week : 'Daily', inline: true },
{ name: 'Compare Time', value: `[Click here to compare event time to your local time](${timeComparisonLink})`, inline: true }
)
.setFooter({ text: `Schedule ID: ${schedule_id}` })
.setColor('#00FF00');
channel.send({ embeds: [embed] }).catch(console.error);
});
}
});
}
module.exports = {
startEventCheckCron,
}

View File