From a065618cdbcb9183a02486129d8abfd590304422 Mon Sep 17 00:00:00 2001 From: Rene Kievits Date: Mon, 16 Sep 2024 06:47:19 +0200 Subject: [PATCH] fix blacklist and add static create command, change node commands --- .dockerignore | 2 + .env.example | 2 + .env_EXAMPLE | 7 - .gitignore | 5 +- README.md | 6 +- eslint.config.mjs | 13 +- package.json | 5 +- src/database/config/config.example.json | 23 ++ src/discord/discordClient.js | 266 +++++++++++++++++------- src/features/blacklist.js | 18 +- src/features/static.js | 19 +- src/index.js | 7 +- 12 files changed, 261 insertions(+), 112 deletions(-) create mode 100644 .env.example delete mode 100644 .env_EXAMPLE create mode 100644 src/database/config/config.example.json diff --git a/.dockerignore b/.dockerignore index 63a65e7..657f04c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,5 @@ node_modules tests/ npm-debug.log .DS_Store +.env.test +.env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bc5a4e5 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +BOT_TOKEN= +CLIENT_ID= diff --git a/.env_EXAMPLE b/.env_EXAMPLE deleted file mode 100644 index 8a256cc..0000000 --- a/.env_EXAMPLE +++ /dev/null @@ -1,7 +0,0 @@ -BOT_TOKEN= -CLIENT_ID= -DB_NAME= -DB_PORT= -DB_HOST= -DB_USER= -DB_PASS= diff --git a/.gitignore b/.gitignore index 2a57f54..8d4fc3c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -.env +.env.prod +.env.test node_modules/ package-lock.json -src/database/config/ +src/database/config/config.json src/database/migrations/ src/database/seeders/ diff --git a/README.md b/README.md index 42e3cf5..b4549b4 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ Its best to run the bot in a docker container - Blacklist a player with a given reason - Updates a global message - Can check against individual players +- Event Reminder + - Reminds roles for ingame events 15min ahead of time +- Role assignment + - Assign your role by reacting to a message ## TODO @@ -32,5 +36,3 @@ Its best to run the bot in a docker container - Command that asks every user to confirm a set date and time for the static - Make it always repeat on the same time, or just once (have to re create every time) - Write easy to understand documentation and a HOWTO -- Blacklist - - Limit command usage to admins, maybe go away from slash commands? diff --git a/eslint.config.mjs b/eslint.config.mjs index d3d9d61..1bc2a72 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,17 +14,14 @@ export default [ 'indent': ['error', 2], 'linebreak-style': ['error', 'unix'], 'comma-dangle': ['error', 'always-multiline'], - 'no-unused-vars': ['error', { - vars: 'all', - args: 'after-used', - ignoreRestSiblings: true, - caughtErrors: 'none', - argsIgnorePattern: '_', - }], }, }, { files: ['**/*.js'], + env:{ + 'node': true, + 'commonjs': true, + }, languageOptions: { sourceType: 'commonjs', ecmaVersion: 2021, @@ -52,7 +49,7 @@ export default [ }, { languageOptions: { - globals: globals.browser, + globals: globals.node, }, }, pluginJs.configs.recommended, diff --git a/package.json b/package.json index 9c5ac1c..2ddaa75 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,16 @@ "dependencies": { "discord.js": "^14.15.3", "dotenv": "^16.4.5", + "dotenv-cli": "^7.4.2", "jest": "^29.7.0", "node-cron": "^3.0.3", "pg": "^8.12.0", "sequelize": "^6.37.3" }, "scripts": { - "test": "jest" + "test": "jest", + "dev": "NODE_ENV=development dotenv -e .env.test node src/index.js", + "prod": "NODE_ENV=production dotenv -e .env.prod node src/index.js" }, "devDependencies": { "@eslint/js": "^9.10.0", diff --git a/src/database/config/config.example.json b/src/database/config/config.example.json new file mode 100644 index 0000000..4ac88de --- /dev/null +++ b/src/database/config/config.example.json @@ -0,0 +1,23 @@ +{ + "development": { + "username": "", + "password": "", + "database": "", + "host": "", + "dialect": "" + }, + "test": { + "username": "", + "password": "", + "database": "", + "host": "", + "dialect": "" + }, + "production": { + "username": "", + "password": "", + "database": "", + "host": "", + "dialect": "" + } +} diff --git a/src/discord/discordClient.js b/src/discord/discordClient.js index 8e5600c..f3f07f8 100644 --- a/src/discord/discordClient.js +++ b/src/discord/discordClient.js @@ -1,4 +1,4 @@ -const { Client, GatewayIntentBits, Partials, Routes } = require('discord.js') +const { Client, GatewayIntentBits, Partials, Routes, PermissionFlagsBits, PermissionsBitField, ChannelFlags, ChannelManager, ChannelFlagsBitField, ChannelType } = require('discord.js') require('dotenv').config() const { REST } = require('@discordjs/rest') const { SlashCommandBuilder } = require('@discordjs/builders') @@ -22,11 +22,23 @@ const { handleBirthdayDelete, } = require('../features/birthday') +const { + handleStaticAdd, + handleStaticGet, + handleStaticDelete, + handleStaticUpdateName, + handleStaticUpdateUser, + handleStaticUpdateUsers, + handleStaticUpdateSize, +} = require('../features/static') + const { startBirthdayCheckCron } = require('../tasks/checkBirthday') const { startEventCheckCron } = require('../tasks/eventReminder') const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN) +let server = null + const client = new Client({ intents: [ GatewayIntentBits.Guilds, @@ -44,89 +56,170 @@ client.on('interactionCreate', async interaction => { if (!interaction.isCommand()) return switch (interaction.commandName) { - case 'blacklist': - const reportedUser = interaction.options.getString('player') - const reason = interaction.options.getString('reason') - const reportedByUser = interaction.user.username + case 'blacklist': { + const reportedUser = interaction.options.getString('player') + const reason = interaction.options.getString('reason') + const reportedByUser = interaction.user.username - const res = await handleBlacklistAdd(reportedUser, reason, reportedByUser) - if (res) { - if (res.name === reportedUser) - interaction.reply({ content: 'This user has already been reported', ephemeral: true }) - else { - interaction.reply({ content: `Player ** ${reportedUser}** had been successfully reported for ${reason}`, ephemeral: true }) - updateGlobalMessage(interaction) + const res = await handleBlacklistAdd(reportedUser, reason, reportedByUser) + if (res) { + if (res.name === reportedUser) + interaction.reply({ content: 'This user has already been reported', ephemeral: true }) + else { + interaction.reply({ content: `Player ** ${reportedUser}** had been successfully reported for ${reason}`, ephemeral: true }) + updateGlobalMessage(client) + } + } else + interaction.reply({ content: 'ERROR trying to add the player to the blacklist, please contact @Crylia', ephemeral: true }) + + break + } case 'blacklist-check-player': { + const player = interaction.options.getString('player') + + const reason2 = await handleBlacklistCheck(player) + reason2 ? + 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 }) + + break + } case 'birthday': { + const user = interaction.user.username + const birthday = interaction.options.getString('birthday') + + // Matches format xx.xx.xxxx, later dd.mm.yyyy + const match = birthday.match(/^(\d{2})\.(\d{2})\.(\d{4})$/) + if (!match) { + await interaction.reply({ content: 'Invalid date format. Please use dd.mm.yyyy.', ephemeral: true }) + return } - } else - interaction.reply({ content: 'ERROR trying to add the player to the blacklist, please contact @Crylia', ephemeral: true }) - break - case 'blacklist-check-player': - const player = interaction.options.getString('player') + const day = parseInt(match[1], 10) + const month = parseInt(match[2], 10) + const year = parseInt(match[3], 10) - const reason2 = await handleBlacklistCheck(player) - reason2 ? - 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 }) + // Validates dd.mm ae legit, year doesnt matter for the birthday + const isValidDate = (day, month, year) => { + if (month < 1 || month > 12) return false - break - case 'birthday': - const user = interaction.user.username - const birthday = interaction.options.getString('birthday') + const daysInMonth = [31, ((year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + return day > 0 && day <= daysInMonth[month - 1] + } - // Matches format xx.xx.xxxx, later dd.mm.yyyy - const match = birthday.match(/^(\d{2})\.(\d{2})\.(\d{4})$/) - if (!match) { - await interaction.reply({ content: 'Invalid date format. Please use dd.mm.yyyy.', ephemeral: true }) - return + if (!isValidDate(day, month, year)) { + await interaction.reply({ content: 'Invalid date. Please enter a valid birthday as dd.mm.yyyy.', ephemeral: true }) + return + } + + await handleBirthdayCheck(user, birthday).length > 0 ? + await handleBirthdayUpdate(user, birthday) : + handleBirthdayAdd(user, birthday) ? + await interaction.reply({ content: `Set ${birthday} as your birthday.Everyone will be notified once the day arrives!`, ephemeral: true }) : + await interaction.reply({ content: 'Something went wrong when setting / updating your birthday, please contact @crylia', ephemeral: true }) + break + } case 'birthday-check': { + const birthdayCheck = await handleBirthdayCheck(interaction.user.username) + birthdayCheck ? + await interaction.reply({ content: `Your birthday is currently set to ${new Date(birthdayCheck[0].date).toLocaleDateString('de-DE')}.`, ephemeral: true }) : + await interaction.reply({ content: 'You don\'t have a birthday set. Use the`birthday` command to set one.', ephemeral: true }) + break + } case 'birthday-delete': { + await handleBirthdayDelete(interaction.user.username) ? + await interaction.reply({ content: 'Your birthday has been deleted.', ephemeral: true }) : + await interaction.reply({ content: 'You don\'t have a birthday set.', ephemeral: true }) + break + } case 'static-create': { + const static_name = interaction.options.getString('name') + const static_size = interaction.options.getString('size') + let static_members = [interaction.user.username] + + try { + const static_role = await interaction.guild.roles.create({ + name: static_name, + color: 'BLUE', + }) + + interaction.member.roles.add(static_role) + + for (const username of (interaction.options.getString('members')).split(',').map(name => name.trim())) { + const member = interaction.guild.members.cache.find(member => member.user.username === username) + + if (member) { + static_members.push(member) + member.roles.add(static_role) + } else + console.log(`WARNING: Creating static: ${static_name} member named ${username} not found`) + } + + let category = interaction.guild.channels.cache.find(channel => channel.name === 'Statics' && channel.type === ChannelType.GuildCategory) + + if (!category) { + console.log(`ERROR: Creating static, couldn't find category Statics, ABBORTING`) + + interaction.guild.roles.remove(static_role) + } + + const static_text_channel = await interaction.guild.channels.create({ + name: static_name, + type: ChannelType.GuildText, + parent: category.id, + permissionOverwrites: [ + { + id: interaction.guild.id, // @everyone role + deny: [PermissionsBitField.Flags.ViewChannel] // Deny view for everyone + }, + { + id: static_role.id, // Allow view for the static role + allow: [PermissionsBitField.Flags.ViewChannel], + } + ] + }) + + const static_voice_channel = await interaction.guild.channels.create({ + name: static_name, + type: ChannelType.GuildVoice, + parent: category.id, + permissionOverwrites: [ + { + id: interaction.guild.id, // @everyone role + deny: [PermissionsBitField.Flags.ViewChannel] // Deny view for everyone + }, + { + id: static_role.id, // Allow view for the static role + allow: [PermissionsBitField.Flags.ViewChannel], + } + ], + }) + + const res = handleStaticAdd(static_name, static_members[0], static_members, static_size, static_role, static_text_channel, static_voice_channel) + + if (res) { + interaction.reply({ + content: `Static ${static_name} created. Current members are ${static_members}.`, + ephemeral: true + }) + } else + interaction.reply({ + content: `Error creating static, please contact @Crylia for help`, + ephemeral: true + }) + } catch (error) { + console.error('Error creating static or assigning roles:', error) + interaction.reply({ + content: 'An error occurred while creating the static. Please try again or contact an admin.', + ephemeral: true, + }) + } + break + } case 'static-delete': { + + break + + } case 'static-show': { + + break + } default: { + break } - - const day = parseInt(match[1], 10) - const month = parseInt(match[2], 10) - const year = parseInt(match[3], 10) - - // Validates dd.mm ae legit, year doesnt matter for the birthday - const isValidDate = (day, month, year) => { - if (month < 1 || month > 12) return false - - const daysInMonth = [31, ((year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - return day > 0 && day <= daysInMonth[month - 1] - } - - if (!isValidDate(day, month, year)) { - await interaction.reply({ content: 'Invalid date. Please enter a valid birthday as dd.mm.yyyy.', ephemeral: true }) - return - } - - await handleBirthdayCheck(user, birthday).length > 0 ? - await handleBirthdayUpdate(user, birthday) : - handleBirthdayAdd(user, birthday) ? - await interaction.reply({ content: `Set ${birthday} as your birthday.Everyone will be notified once the day arrives!`, ephemeral: true }) : - await interaction.reply({ content: 'Something went wrong when setting / updating your birthday, please contact @crylia', ephemeral: true }) - break - case 'birthday-check': - const birthdayCheck = await handleBirthdayCheck(interaction.user.username) - birthdayCheck ? - await interaction.reply({ content: `Your birthday is currently set to ${new Date(birthdayCheck[0].date).toLocaleDateString('de-DE')}.`, ephemeral: true }) : - await interaction.reply({ content: 'You don\'t have a birthday set. Use the`birthday` command to set one.', ephemeral: true }) - break - case 'birthday-delete': - await handleBirthdayDelete(interaction.user.username) ? - await interaction.reply({ content: 'Your birthday has been deleted.', ephemeral: true }) : - await interaction.reply({ content: 'You don\'t have a birthday set.', ephemeral: true }) - break - case 'static-create': - - break - case 'static-delete': - - break - - case 'static-show': - - break - default: - break } }) @@ -166,6 +259,7 @@ const connectDiscord = async () => { .setName('birthday-delete') .setDescription('Delete your birthday, nobody will know when your birthday arrives :('), new SlashCommandBuilder() + .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator) .setName('blacklist') .setDescription('Add a player to a blacklist with a reason') .addStringOption(option => @@ -186,6 +280,24 @@ const connectDiscord = async () => { .setDescription('The in-game name of the player') .setRequired(true) ), + new SlashCommandBuilder() + .setName('static-create') + .setDescription('Create a new static with a voice and text channel just for your members.') + .addStringOption(option => + option.setName('name') + .setDescription('Name of the static, the voice and test channel will be named after this.') + .setRequired(true) + ) + .addStringOption(option => + option.setName('size') + .setDescription('Number of members in the static.') + .setRequired(false) + ) + .addStringOption(option => + option.setName('members') + .setDescription('Optionally assign members here by a comma seperated list (user1,user2,user3...).') + .setRequired(false) + ) ].map(command => command.toJSON()) await rest.put( diff --git a/src/features/blacklist.js b/src/features/blacklist.js index 93bd285..251b392 100644 --- a/src/features/blacklist.js +++ b/src/features/blacklist.js @@ -61,7 +61,6 @@ const createBlacklistEmbeds = (playerEntries, maxChars = 30) => { if (fieldCount > 0) { embeds.push(embed) } - return embeds } @@ -79,15 +78,17 @@ const updateGlobalMessage = async (client) => { return } - const messages = await targetChannel.messages.fetch({ limit: 100 }) + const embeds = createBlacklistEmbeds( + (await handleBlacklistShow()).map( + entry => [entry.name, entry.reason] + ) + ) + + // The message count will increase/decrese with a maximum of 1, so pulling the new page + // count + 1 will guarantee that all previous embeds will be deleted + const messages = await targetChannel.messages.fetch({ limit: embeds.length + 1 }) await Promise.all(messages.map(msg => msg.delete())) - const blacklistEntries = await handleBlacklistShow() - - const playerEntries = blacklistEntries.map(entry => [entry.name, entry.reason]) - - const embeds = createBlacklistEmbeds(playerEntries) - for (const embed of embeds) await targetChannel.send({ embeds: [embed] }) @@ -96,7 +97,6 @@ const updateGlobalMessage = async (client) => { } } - module.exports = { handleBlacklistAdd, handleBlacklistCheck, diff --git a/src/features/static.js b/src/features/static.js index 0677914..88bc8fc 100644 --- a/src/features/static.js +++ b/src/features/static.js @@ -1,7 +1,7 @@ const { CreateStatic, ReadStatic, DeleteStatic } = require('../database/staticdb') const handleStaticAdd = async (name, creator, members, size) => { - if (!name || !createor || !members || !size) return false + if (!name || !creator || !members || !size) return false const result = await CreateStatic(name, creator, members, size) @@ -24,8 +24,25 @@ const handleStaticDelete = async (name) => { return result } +const handleStaticUpdateName = async (newName) => { + +} +const handleStaticUpdateUser = async (user, action) => { + +} +const handleStaticUpdateUsers = async (users, action) => { + +} +const handleStaticUpdateSize = async (newSize) => { + +} + module.exports = { handleStaticAdd, handleStaticGet, handleStaticDelete, + handleStaticUpdateName, + handleStaticUpdateUser, + handleStaticUpdateUsers, + handleStaticUpdateSize } diff --git a/src/index.js b/src/index.js index bcda9df..3bf5700 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,9 @@ -const { Sequelize } = require('sequelize') const { connectDiscord } = require('./discord/discordClient') -require('dotenv').config(); -(async () => { + +void (async () => { try { - connectDiscord() - } catch (error) { console.error('Error initializing application:', error) }