fix blacklist and add static create command, change node commands

This commit is contained in:
Rene Kievits
2024-09-16 06:47:19 +02:00
parent 9fd47761b0
commit a065618cdb
12 changed files with 261 additions and 112 deletions

View File

@@ -4,3 +4,5 @@ node_modules
tests/ tests/
npm-debug.log npm-debug.log
.DS_Store .DS_Store
.env.test
.env.example

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
BOT_TOKEN=
CLIENT_ID=

View File

@@ -1,7 +0,0 @@
BOT_TOKEN=
CLIENT_ID=
DB_NAME=
DB_PORT=
DB_HOST=
DB_USER=
DB_PASS=

5
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.env .env.prod
.env.test
node_modules/ node_modules/
package-lock.json package-lock.json
src/database/config/ src/database/config/config.json
src/database/migrations/ src/database/migrations/
src/database/seeders/ src/database/seeders/

View File

@@ -15,6 +15,10 @@ Its best to run the bot in a docker container
- Blacklist a player with a given reason - Blacklist a player with a given reason
- Updates a global message - Updates a global message
- Can check against individual players - 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 ## 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 - 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) - 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 - Write easy to understand documentation and a HOWTO
- Blacklist
- Limit command usage to admins, maybe go away from slash commands?

View File

@@ -14,17 +14,14 @@ export default [
'indent': ['error', 2], 'indent': ['error', 2],
'linebreak-style': ['error', 'unix'], 'linebreak-style': ['error', 'unix'],
'comma-dangle': ['error', 'always-multiline'], 'comma-dangle': ['error', 'always-multiline'],
'no-unused-vars': ['error', {
vars: 'all',
args: 'after-used',
ignoreRestSiblings: true,
caughtErrors: 'none',
argsIgnorePattern: '_',
}],
}, },
}, },
{ {
files: ['**/*.js'], files: ['**/*.js'],
env:{
'node': true,
'commonjs': true,
},
languageOptions: { languageOptions: {
sourceType: 'commonjs', sourceType: 'commonjs',
ecmaVersion: 2021, ecmaVersion: 2021,
@@ -52,7 +49,7 @@ export default [
}, },
{ {
languageOptions: { languageOptions: {
globals: globals.browser, globals: globals.node,
}, },
}, },
pluginJs.configs.recommended, pluginJs.configs.recommended,

View File

@@ -2,13 +2,16 @@
"dependencies": { "dependencies": {
"discord.js": "^14.15.3", "discord.js": "^14.15.3",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"dotenv-cli": "^7.4.2",
"jest": "^29.7.0", "jest": "^29.7.0",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"pg": "^8.12.0", "pg": "^8.12.0",
"sequelize": "^6.37.3" "sequelize": "^6.37.3"
}, },
"scripts": { "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": { "devDependencies": {
"@eslint/js": "^9.10.0", "@eslint/js": "^9.10.0",

View File

@@ -0,0 +1,23 @@
{
"development": {
"username": "",
"password": "",
"database": "",
"host": "",
"dialect": ""
},
"test": {
"username": "",
"password": "",
"database": "",
"host": "",
"dialect": ""
},
"production": {
"username": "",
"password": "",
"database": "",
"host": "",
"dialect": ""
}
}

View File

@@ -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() require('dotenv').config()
const { REST } = require('@discordjs/rest') const { REST } = require('@discordjs/rest')
const { SlashCommandBuilder } = require('@discordjs/builders') const { SlashCommandBuilder } = require('@discordjs/builders')
@@ -22,11 +22,23 @@ const {
handleBirthdayDelete, handleBirthdayDelete,
} = require('../features/birthday') } = require('../features/birthday')
const {
handleStaticAdd,
handleStaticGet,
handleStaticDelete,
handleStaticUpdateName,
handleStaticUpdateUser,
handleStaticUpdateUsers,
handleStaticUpdateSize,
} = require('../features/static')
const { startBirthdayCheckCron } = require('../tasks/checkBirthday') const { startBirthdayCheckCron } = require('../tasks/checkBirthday')
const { startEventCheckCron } = require('../tasks/eventReminder') const { startEventCheckCron } = require('../tasks/eventReminder')
const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN) const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN)
let server = null
const client = new Client({ const client = new Client({
intents: [ intents: [
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
@@ -44,89 +56,170 @@ client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return if (!interaction.isCommand()) return
switch (interaction.commandName) { switch (interaction.commandName) {
case 'blacklist': case 'blacklist': {
const reportedUser = interaction.options.getString('player') const reportedUser = interaction.options.getString('player')
const reason = interaction.options.getString('reason') const reason = interaction.options.getString('reason')
const reportedByUser = interaction.user.username const reportedByUser = interaction.user.username
const res = await handleBlacklistAdd(reportedUser, reason, reportedByUser) const res = await handleBlacklistAdd(reportedUser, reason, reportedByUser)
if (res) { if (res) {
if (res.name === reportedUser) if (res.name === reportedUser)
interaction.reply({ content: 'This user has already been reported', ephemeral: true }) interaction.reply({ content: 'This user has already been reported', ephemeral: true })
else { else {
interaction.reply({ content: `Player ** ${reportedUser}** had been successfully reported for ${reason}`, ephemeral: true }) interaction.reply({ content: `Player ** ${reportedUser}** had been successfully reported for ${reason}`, ephemeral: true })
updateGlobalMessage(interaction) 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 const day = parseInt(match[1], 10)
case 'blacklist-check-player': const month = parseInt(match[2], 10)
const player = interaction.options.getString('player') const year = parseInt(match[3], 10)
const reason2 = await handleBlacklistCheck(player) // Validates dd.mm ae legit, year doesnt matter for the birthday
reason2 ? const isValidDate = (day, month, year) => {
await interaction.reply({ content: `** ${reason2.name}** is blacklisted for: ** ${reason2.reason || 'No reason provided.'}**`, ephemeral: true }) : if (month < 1 || month > 12) return false
await interaction.reply({ content: `** ${player}** is not blacklisted.`, ephemeral: true })
break const daysInMonth = [31, ((year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
case 'birthday': return day > 0 && day <= daysInMonth[month - 1]
const user = interaction.user.username }
const birthday = interaction.options.getString('birthday')
// Matches format xx.xx.xxxx, later dd.mm.yyyy if (!isValidDate(day, month, year)) {
const match = birthday.match(/^(\d{2})\.(\d{2})\.(\d{4})$/) await interaction.reply({ content: 'Invalid date. Please enter a valid birthday as dd.mm.yyyy.', ephemeral: true })
if (!match) { return
await interaction.reply({ content: 'Invalid date format. Please use 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') .setName('birthday-delete')
.setDescription('Delete your birthday, nobody will know when your birthday arrives :('), .setDescription('Delete your birthday, nobody will know when your birthday arrives :('),
new SlashCommandBuilder() new SlashCommandBuilder()
.setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator)
.setName('blacklist') .setName('blacklist')
.setDescription('Add a player to a blacklist with a reason') .setDescription('Add a player to a blacklist with a reason')
.addStringOption(option => .addStringOption(option =>
@@ -186,6 +280,24 @@ const connectDiscord = async () => {
.setDescription('The in-game name of the player') .setDescription('The in-game name of the player')
.setRequired(true) .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()) ].map(command => command.toJSON())
await rest.put( await rest.put(

View File

@@ -61,7 +61,6 @@ const createBlacklistEmbeds = (playerEntries, maxChars = 30) => {
if (fieldCount > 0) { if (fieldCount > 0) {
embeds.push(embed) embeds.push(embed)
} }
return embeds return embeds
} }
@@ -79,15 +78,17 @@ const updateGlobalMessage = async (client) => {
return 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())) 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) for (const embed of embeds)
await targetChannel.send({ embeds: [embed] }) await targetChannel.send({ embeds: [embed] })
@@ -96,7 +97,6 @@ const updateGlobalMessage = async (client) => {
} }
} }
module.exports = { module.exports = {
handleBlacklistAdd, handleBlacklistAdd,
handleBlacklistCheck, handleBlacklistCheck,

View File

@@ -1,7 +1,7 @@
const { CreateStatic, ReadStatic, DeleteStatic } = require('../database/staticdb') const { CreateStatic, ReadStatic, DeleteStatic } = require('../database/staticdb')
const handleStaticAdd = async (name, creator, members, size) => { 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) const result = await CreateStatic(name, creator, members, size)
@@ -24,8 +24,25 @@ const handleStaticDelete = async (name) => {
return result return result
} }
const handleStaticUpdateName = async (newName) => {
}
const handleStaticUpdateUser = async (user, action) => {
}
const handleStaticUpdateUsers = async (users, action) => {
}
const handleStaticUpdateSize = async (newSize) => {
}
module.exports = { module.exports = {
handleStaticAdd, handleStaticAdd,
handleStaticGet, handleStaticGet,
handleStaticDelete, handleStaticDelete,
handleStaticUpdateName,
handleStaticUpdateUser,
handleStaticUpdateUsers,
handleStaticUpdateSize
} }

View File

@@ -1,12 +1,9 @@
const { Sequelize } = require('sequelize')
const { connectDiscord } = require('./discord/discordClient') const { connectDiscord } = require('./discord/discordClient')
require('dotenv').config();
(async () => {
void (async () => {
try { try {
connectDiscord() connectDiscord()
} catch (error) { } catch (error) {
console.error('Error initializing application:', error) console.error('Error initializing application:', error)
} }