Compare commits

...

9 Commits

Author SHA1 Message Date
6f24616c81 Added alot of functions 2023-08-11 11:14:59 +02:00
0f54fbf07e Added minecraft to discord messages 2023-08-05 12:21:30 +02:00
1e72ecdf53 added username system 2023-08-05 11:16:18 +02:00
e5644462b9 Added whitelist function 2023-08-04 19:27:29 +02:00
1b89c4440b Added verifyminecraft function 2023-08-04 17:05:49 +02:00
cf968f198a Added database 2023-08-03 12:06:31 +02:00
b68cb0b219 added basic functions 2023-08-02 14:16:30 +02:00
0c1cc63b9b initialized discord bot 2023-08-02 13:26:48 +02:00
fbc9238e97 added eslint typescript 2023-06-28 14:19:07 +02:00
180 changed files with 3142 additions and 59994 deletions

View File

@ -1,59 +0,0 @@
name: Build and Deploy
on: [push]
jobs:
Deploy Web:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- uses: actions/checkout@v3
- name: Use Nodejs
uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
working-directory: ./web
- run: npm run build
working-directory: ./web
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v2
with:
registry: gitea.xeovalyte.dev
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- run: docker buildx build -t gitea.xeovalyte.dev/xeovalyte/polarcraft-web:latest --load --platform=linux/amd64 ./web
- run: docker push gitea.xeovalyte.dev/xeovalyte/polarcraft-web:latest
Deploy Discord Bot:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- uses: actions/checkout@v3
- name: Use Nodejs
uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
working-directory: ./discord-bot
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v2
with:
registry: gitea.xeovalyte.dev
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- run: docker buildx build -t gitea.xeovalyte.dev/xeovalyte/polarcraft-discord:latest --load --platform=linux/amd64 ./discord-bot
- run: docker push gitea.xeovalyte.dev/xeovalyte/polarcraft-discord:latest

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 984 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -1,2 +0,0 @@
.env
node_modules

View File

@ -1,11 +0,0 @@
FROM node:18-alpine
WORKDIR /usr/src/app
COPY . .
RUN npm install
EXPOSE 3000
CMD [ "node", "index.js" ]

View File

@ -1,173 +0,0 @@
const { SlashCommandBuilder } = require('discord.js');
const { QueryType } = require('discord-player');
module.exports = {
data: new SlashCommandBuilder()
.setName('music')
.setDescription('Play and configure music')
.addSubcommand(subcommand => subcommand
.setName('play')
.setDescription('Play music')
.addStringOption(option => option
.setName('link-or-query')
.setDescription('The song or link you want to play')
.setRequired(true)))
.addSubcommand(subcommand => subcommand
.setName('skip')
.setDescription('Skip current song'))
.addSubcommand(subcommand => subcommand
.setName('stop')
.setDescription('Stop current queue'))
.addSubcommand(subcommand => subcommand
.setName('pause')
.setDescription('Pause the current song'))
.addSubcommand(subcommand => subcommand
.setName('shuffle')
.setDescription('Shuffle the queue'))
.addSubcommand(subcommand => subcommand
.setName('resume')
.setDescription('Resume the current song'))
.addSubcommand(subcommand => subcommand
.setName('filter')
.setDescription('Apply a filter')
.addStringOption(option => option
.setName('type')
.setDescription('Type of filter')
.setRequired(true)
.addChoices(
{ name: 'Bassboost', value: 'bassboost_low' },
{ name: 'Nightcore', value: 'nightcore' },
)))
.addSubcommand(subcommand => subcommand
.setName('volume')
.setDescription('Set volume')
.addNumberOption(option => option
.setName('percentage')
.setDescription('The percentage of the volume between 1 and 100')
.setRequired(true))),
async execute({ interaction, createEmbed, client }) {
if (!interaction.member.voice.channelId) return await interaction.reply({ embeds: [createEmbed.basic('You are not in a voice channel!')], ephemeral: true });
if (interaction.guild.members.me.voice.channelId && interaction.member.voice.channelId !== interaction.guild.members.me.voice.channelId) {
return await interaction.reply({ embeds: [createEmbed.basic('You are not in my voice channel!')], ephemeral: true });
}
if (interaction.options.getSubcommand() === 'play') {
const query = interaction.options.getString('link-or-query');
const queue = client.player.createQueue(interaction.guild, {
ytdlOptions: {
filter: 'audioonly',
highWaterMark: 1 << 30,
dlChunkSize: 0,
},
metadata: {
channel: interaction.channel,
},
});
// verify vc connection
try {
if (!queue.connection) await queue.connect(interaction.member.voice.channel);
} catch {
queue.destroy();
return await interaction.reply({ embeds: [createEmbed.basic('Could not join your voice channel!')], ephemeral: true });
}
await interaction.reply({ embeds: [createEmbed.basic('Searching...')], ephemeral: true });
const searchResult = await client.player.search(query, {
requestedBy: interaction.user,
searchEngine: QueryType.AUTO,
});
if (!searchResult || !searchResult.tracks[0]) return await interaction.editReply({ embeds: [createEmbed.basic(`Track or playlist **${query}** not found!`)], ephemeral: true });
searchResult.playlist ? queue.addTracks(searchResult.tracks) : queue.addTrack(searchResult.tracks[0]);
if (!queue.playing) await queue.play();
if (!searchResult.playlist) await interaction.editReply({ embeds: [createEmbed.basic(`Adding track **${searchResult.tracks[0].title}**`)], ephemeral: false });
else await interaction.editReply({ embeds: [createEmbed.basic(`Adding **${searchResult.playlist.tracks.length}** tracks from playlist **${searchResult.playlist.title}**`)], ephemeral: false });
} else if (interaction.options.getSubcommand() === 'skip') {
const queue = client.player.getQueue(interaction.guild);
if (!queue || !queue.playing) return await interaction.reply({ embeds: [createEmbed.basic('No music is being played')] });
const currentTrack = queue.nowPlaying();
const success = queue.skip();
await interaction.reply({ embeds: [createEmbed.basic(success ? `Skipped **${currentTrack.title}**` : 'Something went wrong')] });
} else if (interaction.options.getSubcommand() === 'volume') {
const vol = interaction.options.getNumber('percentage');
const queue = client.player.getQueue(interaction.guild);
if (!queue || !queue.playing) return await interaction.reply({ embeds: [createEmbed.basic('No music is being played')] });
if (vol < 1 || vol > 100) return interaction.reply({ embeds: [createEmbed.basic('Volume must be between 1 and 100')] });
const success = queue.setVolume(vol);
await interaction.reply({ embeds: [createEmbed.basic(success ? `Volume set to **${vol}%**` : 'Something went wrong')] });
} else if (interaction.options.getSubcommand() === 'stop') {
const queue = client.player.getQueue(interaction.guild);
if (!queue || !queue.playing) return await interaction.reply({ embeds: [createEmbed.basic('No music is being played')] });
queue.destroy();
await interaction.reply({ embeds: [createEmbed.basic('Stopped the player')] });
} else if (interaction.options.getSubcommand() === 'pause') {
const queue = client.player.getQueue(interaction.guild);
if (!queue || !queue.playing) return await interaction.reply({ embeds: [createEmbed.basic('No music is being played')] });
queue.setPaused(true);
await interaction.reply({ embeds: [createEmbed.basic('Paused the player')] });
} else if (interaction.options.getSubcommand() === 'resume') {
const queue = client.player.getQueue(interaction.guild);
if (!queue || !queue.playing) return await interaction.reply({ embeds: [createEmbed.basic('No music is being played')] });
queue.setPaused(false);
await interaction.reply({ embeds: [createEmbed.basic('Resumed the player')] });
} else if (interaction.options.getSubcommand() === 'filter') {
const filter = interaction.options.getString('type');
const queue = client.player.getQueue(interaction.guild);
if (!queue || !queue.playing) return await interaction.reply({ embeds: [createEmbed.basic('No music is being played')] });
if (filter === 'bassboost_low') {
await queue.setFilters({
bassboost_low: !queue.getFiltersEnabled().includes('bassboost_low'),
normalizer2: !queue.getFiltersEnabled().includes('bassboost_low'),
});
}
if (filter === 'nightcore') {
await queue.setFilters({
nightcore: !queue.getFiltersEnabled().includes('nightcore'),
});
}
await interaction.reply({ embeds: [createEmbed.basic(queue.getFiltersEnabled().includes(filter) ? `Applied **${filter}** filter` : `Removed **${filter}** filter`)] });
} else if (interaction.options.getSubcommand() === 'shuffle') {
const queue = client.player.getQueue(interaction.guild);
if (!queue || !queue.playing) return await interaction.reply({ embeds: [createEmbed.basic('No music is being played')] });
const success = queue.shuffle();
await interaction.reply({ embeds: [createEmbed.basic(success ? 'The queue has been shuffled' : 'Something went wrong')] });
}
},
};

View File

@ -1,12 +0,0 @@
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),
async execute({ interaction, createEmbed, client }) {
const reply = await interaction.reply({ embeds: [createEmbed.basic(`Websocket heartbeat: **${client.ws.ping}ms**\n Roundtrip latency: **Pinging...**`)], fetchReply: true, ephemeral: true });
interaction.editReply({ embeds: [createEmbed.basic(`Websocket heartbeat: **${client.ws.ping}ms**\n Roundtrip latency: **${reply.createdTimestamp - interaction.createdTimestamp}ms**`)], emphemeral: true });
},
};

View File

@ -1,70 +0,0 @@
const { SlashCommandBuilder, PermissionsBitField, ChannelType, ButtonStyle, ButtonBuilder, ActionRowBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ticket')
.setDescription('Commands for managing a ticket')
.addSubcommand(subcommand => subcommand
.setName('create')
.setDescription('Create a ticket with you and moderators'))
.addSubcommand(subcommand => subcommand
.setName('close')
.setDescription('Close current ticket')),
async execute({ interaction, createEmbed }) {
if (interaction.options.getSubcommand() === 'create') {
await interaction.reply({ embeds: [createEmbed.basic('Creating ticket channel...')], fetchReply: true, ephemeral: true });
const ticketChannel = await interaction.member.guild.channels.create({
name: `ticket-${interaction.user.username}`,
type: ChannelType.GuildText,
parent: process.env.TICKET_CATEGORY_ID,
permissionOverwrites: [
{
id: interaction.guild.id,
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: process.env.MODERATOR_ROLE_ID,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: interaction.user.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
],
});
await ticketChannel.send({ embeds: [createEmbed.basic(`${interaction.user} created a ticket`)] });
interaction.editReply({ embeds: [createEmbed.basic(`${ticketChannel} has been created`)], emphemeral: true });
} else if (interaction.options.getSubcommand() === 'close') {
if (!interaction.channel.name.startsWith('ticket')) return interaction.reply({ embeds: [createEmbed.basic('You must execute this command inside a ticket channel')], emphemeral: true });
const cancelRow = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('cancel')
.setLabel('Cancel')
.setStyle(ButtonStyle.Danger),
);
const closeMessage = await interaction.reply({ embeds: [createEmbed.basic('Closing ticket in 10 seconds...')], components: [cancelRow] });
const collector = closeMessage.createMessageComponentCollector();
const timeout = setTimeout(() => {
try {
interaction.channel.delete();
} catch {}
}, 10000);
collector.on('collect', async () => {
clearTimeout(timeout);
await closeMessage.edit({ embeds: [createEmbed.basic('Ticket closing has been stopped')], components: [] });
});
}
},
};

View File

@ -1,17 +0,0 @@
const { Events } = require('discord.js');
module.exports = {
name: Events.MessageCreate,
async execute({ log }, message) {
if (message.channelId === process.env.MINECRAFT_CHANNEL_ID && !message.author.bot) {
await fetch(process.env.WEB_HOST + '/api/minecraft/message/chattominecraft', {
method: 'POST',
body: JSON.stringify({
content: message.content,
discordId: message.author.id,
}),
});
}
},
};

View File

@ -1,17 +0,0 @@
const { Events, EmbedBuilder } = require('discord.js');
module.exports = {
name: Events.GuildMemberAdd,
async execute({ client, log }, member) {
log.Info(`${member.user.username} has joined`);
const newMemberEmbed = new EmbedBuilder()
.setTitle(`${member.user.username} has joined!`)
.setDescription(`Welcome ${member} to the **Polarcraft** Discord server!`)
.setColor(process.env.EMBED_COLOR)
.setThumbnail(member.user.avatarURL());
const channel = await client.channels.cache.get(process.env.LOG_CHANNEL_ID);
channel.send({ embeds: [newMemberEmbed] });
},
};

View File

@ -1,15 +0,0 @@
const { Events, EmbedBuilder } = require('discord.js');
module.exports = {
name: Events.GuildMemberRemove,
async execute({ client, log }, member) {
log.Info(`${member.user.username} has left`);
const newMemberEmbed = new EmbedBuilder()
.setTitle(`${member.user.username} has left!`)
.setColor(process.env.EMBED_COLOR);
const channel = await client.channels.cache.get(process.env.LOG_CHANNEL_ID);
channel.send({ embeds: [newMemberEmbed] });
},
};

View File

@ -1,26 +0,0 @@
const { Events } = require('discord.js');
module.exports = {
name: Events.InteractionCreate,
async execute({ log, createEmbed, client }, interaction) {
if (interaction.isChatInputCommand()) {
const command = interaction.client.commands.get(interaction.commandName);
if (!command) {
return log.Error(`No command matching ${interaction.commandName} was found`);
} else {
log.Info(`${interaction.user.username} executed command ${interaction.commandName}`);
}
try {
await command.execute({ interaction, log, createEmbed, client });
} catch (error) {
log.Error(error);
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}
}
},
};

View File

@ -1,11 +0,0 @@
const { Events } = require('discord.js');
module.exports = {
name: Events.ClientReady,
once: true,
execute({ client, log }) {
log.Info(`Ready! Logged in as ${client.user.tag}`);
require('../functions/registerCommands.js').registerCommands(log);
},
};

View File

@ -1,12 +0,0 @@
const { EmbedBuilder } = require('discord.js');
const dotenv = require('dotenv');
dotenv.config();
const basic = (description) => {
return new EmbedBuilder()
.setColor(process.env.EMBED_COLOR)
.setDescription(description);
};
module.exports = { basic };

View File

@ -1,31 +0,0 @@
const { EmbedBuilder } = require('discord.js');
const registerEvents = ({ client }) => {
client.player.on('trackStart', (queue, track) => {
const nowPlayingEmbed = new EmbedBuilder()
.setTitle('Now Playing')
.setColor(process.env.EMBED_COLOR)
.setDescription(`[${track.title}](${track.url})`)
.addFields(
{ name: 'Requested By', value: `${track.requestedBy}`, inline: true },
{ name: 'Duration', value: track.duration, inline: true },
{ name: 'Queue', value: `${queue.tracks.length} song(s)`, inline: true },
{ name: 'Author', value: track.author, inline: true },
{ name: 'Source', value: track.source, inline: true },
{ name: 'Volume', value: `${queue.options.initialVolume}%`, inline: true },
{ name: 'Filters', value: queue.getFiltersEnabled().join('\n') || 'No filters active', inline: false },
)
.setThumbnail(track.thumbnail)
.setTimestamp();
queue.metadata.channel.send({ embeds: [nowPlayingEmbed] });
});
client.player.on('error', (queue, error) => {
console.log(error);
});
};
module.exports = { registerEvents };

View File

@ -1,48 +0,0 @@
const { REST, Routes } = require('discord.js');
const fs = require('node:fs');
const dotenv = require('dotenv');
dotenv.config();
const registerCommands = (log) => {
const commands = [];
// Grab all the command files from the commands directory you created earlier
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
for (const file of commandFiles) {
const command = require(`../commands/${file}`);
commands.push(command.data.toJSON());
}
// Construct and prepare an instance of the REST module
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
// and deploy your commands!
(async () => {
try {
log.Info(`Started refreshing ${commands.length} application (/) commands.`);
// The put method is used to fully refresh all commands in the guild with the current set
let data = null;
if (process.env.GUILD_ID !== 'production') {
data = await rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
{ body: commands },
);
} else {
data = await rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID),
{ body: commands },
);
}
log.Info(`Successfully reloaded ${data.length} application (/) commands.`);
} catch (error) {
// And of course, make sure you catch and log any errors!
log.Error(error);
}
})();
};
module.exports = { registerCommands };

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +0,0 @@
{
"type": "commonjs",
"name": "discord-bot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"lint": "eslint .",
"lint-fix": "eslint . --fix"
},
"author": "",
"license": "ISC",
"dependencies": {
"@discord-player/extractor": "^3.0.2",
"@discordjs/opus": "^0.8.0",
"chalk": "^4.1.2",
"discord-player": "^5.3.2",
"discord.js": "^14.7.1",
"distube": "^4.0.4",
"dotenv": "^16.0.3",
"eslint": "^8.29.0",
"express": "^4.18.2",
"ytdl-core": "^4.11.4"
}
}

View File

@ -1,38 +0,0 @@
const { WebhookClient, EmbedBuilder } = require('discord.js');
const express = require('express');
const router = express.Router();
const webhookClient = new WebhookClient({ url: process.env.MINECRAFT_WEBHOOK_URL });
router.post('/sendchatmessage', (req, res) => {
const { content, username, avatarURL } = req.body;
if (!username || !content || !avatarURL) return res.status(400).send({ error: 'Content, username and avatar_url are required' });
webhookClient.send({
content: content,
username: username,
avatarURL: avatarURL,
});
res.send({ data: 'Test data' });
});
router.post('/sendgamemessage', (req, res) => {
const { content, avatarURL } = req.body;
if (!content || !avatarURL) return res.status(400).send({ error: 'Content, username and avatar_url are required' });
const messageEmbed = new EmbedBuilder()
.setColor(process.env.EMBED_COLOR)
.setAuthor({ name: content, iconURL: avatarURL });
webhookClient.send({
embeds: [messageEmbed],
username: 'Server',
});
res.send({ data: 'Test data' });
});
module.exports = router;

View File

@ -1,145 +0,0 @@
const express = require('express');
const index = require('../index');
const { PermissionsBitField, ChannelType } = require('discord.js');
const router = express.Router();
router.post('/createchannels', async (req, res) => {
const { name, discordId } = req.body;
if (!name || !discordId) return res.status(400).send({ error: 'Name en discordId zijn vereist' });
try {
const guild = await index.client.guilds.fetch(process.env.GUILD_ID);
const category = await guild.channels.fetch(process.env.TEAM_CATEGORY_ID);
const member = await guild.members.fetch(discordId);
const textChannel = await guild.channels.create({
name: name,
type: ChannelType.GuildText,
parent: category,
permissionOverwrites: [
{
id: guild.id,
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: member.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
],
});
const voiceChannel = await guild.channels.create({
name: name,
type: ChannelType.GuildVoice,
parent: category,
permissionOverwrites: [
{
id: guild.id,
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: member.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
],
});
res.send({ textChannel, voiceChannel });
} catch (e) {
console.log(e);
return res.status(500).send({ error: 'Error tijdens het maken van discord channels' });
}
});
router.post('/deletechannels', async (req, res) => {
const { textChannelId, voiceChannelId } = req.body;
if (!textChannelId, !voiceChannelId) return res.status(400).send({ error: 'textChannelId en voiceChannelId zijn vereist' });
try {
const guild = await index.client.guilds.fetch(process.env.GUILD_ID);
const textChannel = await guild.channels.fetch(textChannelId);
const voiceChannel = await guild.channels.fetch(voiceChannelId);
await textChannel.delete();
await voiceChannel.delete();
res.send({ status: 'success' });
} catch (e) {
console.log(e);
return res.status(500).send({ error: 'Error tijdens het verwijderen van discord channels' });
}
});
router.post('/removeteammember', async (req, res) => {
const { textChannelId, voiceChannelId, discordId } = req.body;
if (!textChannelId, !voiceChannelId, !discordId) return res.status(400).send({ error: 'textChannelId, voiceChannelId en discordId zijn vereist' });
try {
const guild = await index.client.guilds.fetch(process.env.GUILD_ID);
const member = await guild.members.fetch(discordId);
const textChannel = await guild.channels.fetch(textChannelId);
const voiceChannel = await guild.channels.fetch(voiceChannelId);
await textChannel.permissionOverwrites.delete(member);
await voiceChannel.permissionOverwrites.delete(member);
res.send({ status: 'success' });
} catch (e) {
console.log(e);
return res.status(500).send({ error: 'Error tijdens het verwijderen van een team member' });
}
});
router.post('/addteammember', async (req, res) => {
const { textChannelId, voiceChannelId, discordId } = req.body;
if (!textChannelId, !voiceChannelId, !discordId) return res.status(400).send({ error: 'textChannelId, voiceChannelId en discordId zijn vereist' });
try {
const guild = await index.client.guilds.fetch(process.env.GUILD_ID);
const member = await guild.members.fetch(discordId);
const textChannel = await guild.channels.fetch(textChannelId);
const voiceChannel = await guild.channels.fetch(voiceChannelId);
await textChannel.permissionOverwrites.edit(member, { ViewChannel: true });
await voiceChannel.permissionOverwrites.edit(member, { ViewChannel: true });
res.send({ status: 'success' });
} catch (e) {
console.log(e);
return res.status(500).send({ error: 'Error tijdens het toevoegen van team member' });
}
});
router.post('/edit', async (req, res) => {
const { textChannelId, voiceChannelId, name } = req.body;
if (!textChannelId, !voiceChannelId, !name) return res.status(400).send({ error: 'textChannelId, voiceChannelId en name zijn vereist' });
try {
const guild = await index.client.guilds.fetch(process.env.GUILD_ID);
const textChannel = await guild.channels.fetch(textChannelId);
const voiceChannel = await guild.channels.fetch(voiceChannelId);
await textChannel.edit({ name: name });
await voiceChannel.edit({ name: name });
res.send({ status: 'success' });
} catch (e) {
console.log(e);
return res.status(500).send({ error: 'Error tijds het veranderen van Discord channel naam' });
}
});
module.exports = router;

View File

@ -1,47 +0,0 @@
const express = require('express');
const index = require('../index');
const router = express.Router();
router.post('/changenickname', async (req, res) => {
const { nickname, discordId } = req.body;
if (!nickname || !discordId) return res.status(400).send({ error: 'Nickname en discordId zijn vereist' });
const nick = nickname.length > 32 ? nickname.slice(0, 32) : nickname;
try {
const guild = await index.client.guilds.fetch(process.env.GUILD_ID);
const member = await guild.members.fetch(discordId);
await member.edit({ nick: nick });
} catch (e) {
console.log(e);
return res.status(500).send({ error: 'Error tijds het veranderen van de nickname' });
}
res.send({ status: 'success' });
});
router.post('/ban', async (req, res) => {
const { discordId, reason } = req.body;
if (!reason || !discordId) return res.status(400).send({ error: 'Reason en discordId zijn vereist' });
try {
const guild = await index.client.guilds.fetch(process.env.GUILD_ID);
const member = await guild.members.fetch(discordId);
await member.ban({ deleteMessageSeconds: 24 * 3600, reason: reason });
} catch (e) {
console.log(e);
return res.status(500).send({ error: 'Error tijds het bannen van gebruiker' });
}
res.send({ status: 'success' });
});
module.exports = router;

View File

@ -5,7 +5,8 @@
"es6": true
},
"parserOptions": {
"ecmaVersion": 2021
"ecmaVersion": 2021,
"sourceType": "module"
},
"rules": {
"arrow-spacing": ["warn", { "before": true, "after": true }],

View File

@ -0,0 +1,29 @@
const { SlashCommandBuilder } = require('discord.js');
const { simpleEmbed } = require('../functions/embeds.js');
const { Users } = require('../functions/models.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('adduser')
.setDescription('Add a user to the system')
.addUserOption(option => option
.setName('user')
.setDescription('The user to add')),
async execute(interaction) {
const user = interaction.options.getUser('user') ? interaction.options.getUser('user') : interaction.user;
try {
await Users.create({
id: user.id,
rawUsername: user.globalName,
});
await interaction.reply({ embeds: [simpleEmbed('Added user to the system')], ephemeral: true });
} catch (error) {
console.error(error);
await interaction.reply({ embeds: [simpleEmbed('There was an error while adding the user')], ephemeral: true });
}
},
};

View File

@ -0,0 +1,49 @@
const { SlashCommandBuilder } = require('discord.js');
const { simpleEmbed } = require('../functions/embeds.js');
const { Users } = require('../functions/models.js');
const { client } = require('../index.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Ban a user')
.setDefaultMemberPermissions(0)
.addUserOption(option => option
.setName('user')
.setDescription('The user to ban')
.setRequired(true))
.addStringOption(option => option
.setName('reason')
.setDescription('Why ban the user')
.setRequired(true)),
async execute(interaction) {
const member = interaction.options.getMember('user');
const reason = interaction.options.getString('reason');
try {
await member.ban({ reason });
} catch (error) {
console.error(error);
return await interaction.reply({ embeds: [simpleEmbed(`Error while banning ${member}`)], ephemeral: true });
}
const userInstance = await Users.findOne({ where: { id: member.id } });
if (!userInstance) return await interaction.reply({ embeds: [simpleEmbed('Error while getting user information')], ephemeral: true });
if (userInstance.minecraftUuid) {
await fetch(process.env.MINECRAFT_HOST + '/console', {
method: 'POST',
headers: {
'content-type': 'text/plain',
},
body: `ban ${userInstance.minecraftUuid} ${reason}`,
});
}
const channel = client.channels.cache.get(process.env.MOD_LOG_CHANNEL_ID);
await channel.send({ embeds: [simpleEmbed(`${interaction.user} banned ${member}, reason: **${reason}**`)] });
await interaction.reply({ embeds: [simpleEmbed(`Banned ${member}`)], ephemeral: true });
},
};

View File

@ -1,4 +1,5 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const { simpleEmbed } = require('../functions/embeds.js');
module.exports = {
data: new SlashCommandBuilder()
@ -10,15 +11,13 @@ module.exports = {
.setDescription('The amount of messages to clear')
.setRequired(true)),
async execute({ interaction, createEmbed, client }) {
async execute(interaction) {
const amount = interaction.options.getNumber('amount');
if (amount < 1 || amount > 100) return await interaction.reply({ embeds: [createEmbed.basic('The amount must be between 1-100')] });
if (amount < 1 || amount > 100) return await interaction.reply({ embeds: [simpleEmbed('The amount must be between 1-100')] });
const channel = client.channels.cache.get(interaction.channelId);
interaction.channel.bulkDelete(amount, true);
channel.bulkDelete(amount);
await interaction.reply({ embeds: [createEmbed.basic(`Cleared **${amount}** messages`)], ephemeral: true });
await interaction.reply({ embeds: [simpleEmbed(`Cleared **${amount}** messages`)], ephemeral: true });
},
};

View File

@ -0,0 +1,11 @@
const { SlashCommandBuilder } = require('discord.js');
const { simpleEmbed } = require('../functions/embeds.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('dennis')
.setDescription('Dennis.'),
async execute(interaction) {
await interaction.reply({ embeds: [simpleEmbed('Dennis is koel')], ephemeral: true });
},
};

View File

@ -0,0 +1,14 @@
const { SlashCommandBuilder } = require('discord.js');
const { simpleEmbed } = require('../functions/embeds.js');
const { client } = require('../index.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),
async execute(interaction) {
const reply = await interaction.reply({ embeds: [simpleEmbed(`Websocket heartbeat: **${client.ws.ping}ms**\n Roundtrip latency: **Pinging...**`)], fetchReply: true, emphemeral: true });
interaction.editReply({ embeds: [simpleEmbed(`Websocket heartbeat: **${client.ws.ping}ms**\n Roundtrip latency: **${reply.createdTimestamp - interaction.createdTimestamp}ms**`)] });
},
};

View File

@ -0,0 +1,31 @@
const { SlashCommandBuilder } = require('discord.js');
const { simpleEmbed } = require('../functions/embeds.js');
const { Minecraft, Users } = require('../functions/models.js');
const { applyUsername } = require('../functions/utils.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('removewhitelist')
.setDescription('Remove yourself from the whitelist'),
async execute(interaction) {
try {
const user = await Users.findOne({ where: { id: interaction.user.id } });
if (!user.minecraftUuid) return await interaction.reply({ embeds: [simpleEmbed('You are not whitelisted')], ephemeral: true });
await Minecraft.destroy({ where: { uuid: user.minecraftUuid } });
await applyUsername(user, interaction.member);
const role = await interaction.guild.roles.fetch(process.env.MINECRAFT_ROLE_ID);
await interaction.member.roles.remove(role);
await interaction.reply({ embeds: [simpleEmbed('Successfully removed you from the whitelist')], ephemeral: true });
} catch (error) {
console.error(error);
await interaction.reply({ embeds: [simpleEmbed('There was an error while removing you from the whitelist')], ephemeral: true });
}
},
};

View File

@ -0,0 +1,38 @@
const { SlashCommandBuilder } = require('discord.js');
const { simpleEmbed } = require('../functions/embeds.js');
const { Users } = require('../functions/models.js');
const { applyUsername } = require('../functions/utils.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('setusername')
.setDescription('Choose between Discord or Minecraft username')
.addStringOption(option => option
.setName('type')
.setDescription('Discord or Minecraft')
.setRequired(true)
.addChoices(
{ name: 'Discord', value: 'discord' },
{ name: 'Minecraft', value: 'minecraft' },
)),
async execute(interaction) {
const usernameType = interaction.options.getString('type');
try {
const user = await Users.findOne({ where: { id: interaction.user.id } });
user.useMinecraftUsername = usernameType === 'minecraft' ? true : false;
await user.save();
await applyUsername(user, interaction.member);
await interaction.reply({ embeds: [simpleEmbed('Successfully changed your username type')], ephemeral: true });
} catch (error) {
console.error(error);
await interaction.reply({ embeds: [simpleEmbed('There was an error while changing your username type')], ephemeral: true });
}
},
};

View File

@ -0,0 +1,49 @@
const { SlashCommandBuilder } = require('discord.js');
const { simpleEmbed } = require('../functions/embeds.js');
const { Users } = require('../functions/models.js');
const { client } = require('../index.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('suspend')
.setDescription('Suspend a user')
.setDefaultMemberPermissions(0)
.addUserOption(option => option
.setName('user')
.setDescription('The user to suspend')
.setRequired(true))
.addNumberOption(option => option
.setName('time')
.setDescription('The amount of time to suspend the user in minutes')
.setRequired(true))
.addStringOption(option => option
.setName('reason')
.setDescription('Why suspend the user')
.setRequired(true)),
async execute(interaction) {
const time = interaction.options.getNumber('time');
const member = interaction.options.getMember('user');
const reason = interaction.options.getString('reason');
await member.timeout(time * 60 * 1000, reason);
const user = await Users.findOne({ where: { id: member.id } });
if (!user) return await interaction.reply({ embeds: [simpleEmbed('Error while getting user information')], ephemeral: true });
if (user.minecraftUuid) {
await fetch(process.env.MINECRAFT_HOST + '/console', {
method: 'POST',
headers: {
'content-type': 'text/plain',
},
body: `tempban ${user.minecraftUuid} ${time}m ${reason}`,
});
}
const channel = client.channels.cache.get(process.env.MOD_LOG_CHANNEL_ID);
await channel.send({ embeds: [simpleEmbed(`${interaction.user} suspended ${member} for **${time}** minutes, reason: **${reason}**`)] });
await interaction.reply({ embeds: [simpleEmbed(`Suspended ${member} for **${time}** minutes`)], ephemeral: true });
},
};

208
discordbot/commands/team.js Normal file
View File

@ -0,0 +1,208 @@
const { SlashCommandBuilder, ModalBuilder, TextInputStyle, TextInputBuilder, ActionRowBuilder, ChannelType, PermissionsBitField, ButtonBuilder, ButtonStyle, ComponentType } = require('discord.js');
const { simpleEmbed } = require('../functions/embeds.js');
const { Users, Team } = require('../functions/models.js');
const { applyUsername } = require('../functions/utils.js');
const { client } = require('../index.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('team')
.setDescription('Team command')
.addSubcommand(subcommand => subcommand
.setName('create')
.setDescription('Create a team'))
.addSubcommand(subcommand => subcommand
.setName('invite')
.setDescription('Invite an user')
.addUserOption(option => option
.setName('user')
.setDescription('The user to invite')
.setRequired(true)))
.addSubcommand(subcommand => subcommand
.setName('leave')
.setDescription('Leave your team')),
async execute(interaction) {
const user = await Users.findOne({ where: { id: interaction.user.id } });
if (!user) return await interaction.reply({ embeds: [simpleEmbed('Error while getting user information')], ephemeral: true });
if (interaction.options.getSubcommand() === 'create') {
if (user.teamId) return await interaction.reply({ embeds: [simpleEmbed('You are already in a team')], ephemeral: true });
const modal = new ModalBuilder()
.setCustomId('teamCreate')
.setTitle('Create team');
const nameInput = new TextInputBuilder()
.setCustomId('nameInput')
.setLabel('Team name')
.setMinLength(3)
.setMaxLength(16)
.setStyle(TextInputStyle.Short);
const colorInput = new TextInputBuilder()
.setCustomId('colorInput')
.setLabel('Team color (hex)')
.setStyle(TextInputStyle.Short)
.setPlaceholder('#00ff00');
const firstActionRow = new ActionRowBuilder().addComponents(nameInput);
const secondActionRow = new ActionRowBuilder().addComponents(colorInput);
modal.addComponents(firstActionRow, secondActionRow);
await interaction.showModal(modal);
try {
const filter = (filterInteraction) => filterInteraction.customId === 'teamCreate';
const modalInteraction = await interaction.awaitModalSubmit({ filter, time: 10 * 60 * 1000 });
const teamName = modalInteraction.fields.getTextInputValue('nameInput');
const teamColor = modalInteraction.fields.getTextInputValue('colorInput');
if (!/^[a-zA-Z0-9]+$/.test(teamName)) return await modalInteraction.reply({ embeds: [simpleEmbed('Team name can only include alphanumeric characters')], ephemeral: true });
if (!/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(teamColor)) return await modalInteraction.reply({ embeds: [simpleEmbed('Team color must be a valid hex code. [Color Picker](https://htmlcolorcodes.com/color-picker/)')], ephemeral: true });
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const category = await guild.channels.fetch(process.env.TEAM_CATEGORY_ID);
const textChannel = await guild.channels.create({
name: teamName,
type: ChannelType.GuildText,
parent: category,
permissionOverwrites: [
{
id: guild.id,
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: interaction.member.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
],
});
const voiceChannel = await guild.channels.create({
name: teamName,
type: ChannelType.GuildVoice,
parent: category,
permissionOverwrites: [
{
id: guild.id,
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: interaction.member.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
],
});
const team = await Team.create({
name: teamName,
color: teamColor,
textChannelId: textChannel.id,
voiceChannelId: voiceChannel.id,
});
await team.addMembers([ user ]);
await applyUsername(user, interaction.member);
await modalInteraction.reply({ embeds: [simpleEmbed(`Successfully created team **${teamName}**`)], ephemeral: true });
} catch (error) {
console.error(error);
}
} else if (interaction.options.getSubcommand() === 'leave') {
if (!user.teamId) return await interaction.reply({ embeds: [simpleEmbed('You are not in a team')], ephemeral: true });
const team = await user.getTeam();
await team.removeMember(user);
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const textChannel = await guild.channels.fetch(team.textChannelId);
const voiceChannel = await guild.channels.fetch(team.voiceChannelId);
if (await team.countMembers() <= 0) {
await textChannel.delete();
await voiceChannel.delete();
await team.destroy();
} else {
await textChannel.permissionOverwrites.delete(interaction.member);
await voiceChannel.permissionOverwrites.delete(interaction.member);
}
await applyUsername(user, interaction.member);
await interaction.reply({ embeds: [simpleEmbed('Successfully left team')], ephemeral: true });
} else if (interaction.options.getSubcommand() === 'invite') {
if (!user.teamId) return await interaction.reply({ embeds: [simpleEmbed('You are not in a team')], ephemeral: true });
const invitedUser = interaction.options.getUser('user');
if (interaction.user.id === invitedUser.id) return await interaction.reply({ embeds: [simpleEmbed('You cannot invite yourself')], ephemeral: true });
const invitedUserInstance = await Users.findOne({ where: { id: invitedUser.id } });
if (invitedUserInstance.teamId) return await interaction.reply({ embeds: [simpleEmbed('The user you are trying already is in a team')], ephemeral: true });
const team = await user.getTeam();
const acceptButton = new ButtonBuilder()
.setCustomId('accept')
.setLabel('Accept')
.setStyle(ButtonStyle.Success);
const denyButton = new ButtonBuilder()
.setCustomId('deny')
.setLabel('Deny')
.setStyle(ButtonStyle.Danger);
const row = new ActionRowBuilder()
.addComponents(acceptButton, denyButton);
const inviteMessage = await invitedUser.send({
embeds: [simpleEmbed(`**${user.rawUsername}** invited you to join team **${team.name}**`)],
components: [row],
});
await interaction.reply({ embeds: [simpleEmbed(`Successfully invited ${invitedUser} to your team`)], ephemeral: true });
const collector = inviteMessage.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60 * 60 * 1000 });
collector.on('collect', async i => {
try {
if (i.customId === 'accept') {
await invitedUserInstance.reload();
if (invitedUserInstance.teamId) return await i.message.edit({ embeds: [simpleEmbed('You are already in a team')], components: [] });
await team.addMember(i.user.id);
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const member = await guild.members.fetch(i.user.id);
const textChannel = await guild.channels.fetch(team.textChannelId);
const voiceChannel = await guild.channels.fetch(team.voiceChannelId);
await textChannel.permissionOverwrites.edit(member, { ViewChannel: true });
await voiceChannel.permissionOverwrites.edit(member, { ViewChannel: true });
await applyUsername(invitedUserInstance, member);
await i.message.edit({ embeds: [simpleEmbed(`Successfully joined team **${team.name}**`)], components: [] });
} else if (i.customId === 'deny') {
await i.message.edit({ embeds: [simpleEmbed(`Successfully denied the request to join team **${team.name}**`)], components: [] });
}
} catch (error) {
console.error(error);
}
});
collector.on('end', async () => {
inviteMessage.edit({ embeds: [simpleEmbed('Confirmation not received within 1 hour, cancelling')], components: [] });
});
}
},
};

View File

@ -0,0 +1,51 @@
const { SlashCommandBuilder } = require('discord.js');
const { simpleEmbed } = require('../functions/embeds.js');
const { Minecraft, Users } = require('../functions/models.js');
const { applyUsername } = require('../functions/utils.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('whitelist')
.setDescription('Whitelist yourself on the Minecraft server')
.addStringOption(option => option
.setName('code')
.setDescription('6 digit code from Minecraft')
.setRequired(true)),
async execute(interaction) {
const code = interaction.options.getString('code');
if (code.length !== 6) return await interaction.reply({ embeds: [simpleEmbed('The code must be 6 digits long')], ephemeral: true });
const user = await Users.findOne({ where: { id: interaction.user.id } });
if (!user) return await interaction.reply({ embeds: [simpleEmbed('There was an error while finding the user')], ephemeral: true });
if (user.minecraftUuid) return await interaction.reply({ embeds: [simpleEmbed('You are already whitelisted')], ephemeral: true });
try {
const minecraftCol = await Minecraft.findOne({ where: { code: code } });
if (!minecraftCol) return await interaction.reply({ embeds: [simpleEmbed('The code was not linked with a Minecraft account')], ephemeral: true });
if (minecraftCol.whitelisted) return await interaction.reply({ embeds: [simpleEmbed('Minecraft account already whitelisted')], ephemeral: true });
minecraftCol.whitelisted = true;
delete minecraftCol.code;
user.minecraftUuid = minecraftCol.uuid;
await minecraftCol.save();
await user.save();
await applyUsername(user, interaction.member);
const role = await interaction.guild.roles.fetch(process.env.MINECRAFT_ROLE_ID);
await interaction.member.roles.add(role);
await interaction.reply({ embeds: [simpleEmbed('You are successfully whitelisted')], ephemeral: true });
} catch (error) {
console.error(error);
}
},
};

BIN
discordbot/database.sqlite Normal file

Binary file not shown.

View File

@ -0,0 +1,26 @@
const { Events, EmbedBuilder } = require('discord.js');
const { client } = require('../index.js');
const { Users } = require('../functions/models');
module.exports = {
name: Events.GuildMemberAdd,
async execute(member) {
const addMemberEmbed = new EmbedBuilder()
.setTitle(`${member.user.globalName} has joined!`)
.setDescription(`Welcome ${member} to the **Polarcraft** Discord server!`)
.setColor(process.env.EMBED_COLOR)
.setThumbnail(member.user.avatarURL());
const channel = client.channels.cache.get(process.env.LOG_CHANNEL_ID);
await channel.send({ embeds: [addMemberEmbed] });
try {
await Users.create({
id: member.user.id,
});
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') return;
console.error(error);
}
},
};

View File

@ -0,0 +1,14 @@
const { Events, EmbedBuilder } = require('discord.js');
const { client } = require('../index.js');
module.exports = {
name: Events.GuildMemberRemove,
async execute(member) {
const removeMemberEmbed = new EmbedBuilder()
.setTitle(`${member.user.globalName} has left!`)
.setColor(process.env.EMBED_COLOR);
const channel = client.channels.cache.get(process.env.LOG_CHANNEL_ID);
channel.send({ embeds: [removeMemberEmbed] });
},
};

View File

@ -0,0 +1,23 @@
const { Events } = require('discord.js');
module.exports = {
name: Events.InteractionCreate,
async execute(interaction) {
if (interaction.isChatInputCommand() || interaction.isUserContextMenuCommand()) {
const command = interaction.client.commands.get(interaction.commandName);
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
try {
await command.execute(interaction);
} catch (error) {
console.error(`Error executing ${interaction.commandName}`);
console.error(error);
}
}
},
};

View File

@ -0,0 +1,35 @@
const { Events } = require('discord.js');
const { Users } = require('../functions/models.js');
module.exports = {
name: Events.MessageCreate,
async execute(message) {
if (message.channelId === process.env.MINECRAFT_CHANNEL_ID && !message.author.bot) {
try {
const user = await Users.findOne({ where: { id: message.author.id } });
if (!user) return;
const team = await user.getTeam();
let tellraw;
if (!team) {
tellraw = `tellraw @a ["",{"text":"DC","color":"gray"},{"text":" ${user.rawUsername} > ${message.content}"}]`;
} else {
tellraw = `tellraw @a ["",{"text":"DC ","color":"dark_gray"},{"text":"[","color":"gray"},{"text":"${team.name}","color":"${team.color}"},{"text":"]","color":"gray"},{"text":" ${user.rawUsername} "},{"text":"> ${message.content}"}]`;
}
await fetch(process.env.MINECRAFT_HOST + '/console', {
method: 'POST',
headers: {
'content-type': 'text/plain',
},
body: tellraw,
});
} catch (error) {
console.error(error);
}
}
},
};

View File

@ -0,0 +1,17 @@
const { Events } = require('discord.js');
const deployCommands = require('../functions/deployCommands');
const { Users, Team, Minecraft } = require('../functions/models');
module.exports = {
name: Events.ClientReady,
once: true,
execute(client) {
Users.sync();
Team.sync();
Minecraft.sync();
console.log(`Ready! Logged in as ${client.user.tag}`);
deployCommands();
},
};

View File

@ -0,0 +1,45 @@
const { REST, Routes } = require('discord.js');
const fs = require('node:fs');
const path = require('node:path');
module.exports = function deployCommands() {
const commands = [];
const commandsPath = path.join(__dirname, '../commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
if ('data' in command && 'execute' in command) {
commands.push(command.data.toJSON());
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
(async () => {
try {
console.log(`Started refreshing ${commands.length} application (/) commands.`);
let data;
if (process.env.GUILD_ID === 'production') {
data = await rest.put(
Routes.applicationCommands(process.env.DISCORD_APPLICATION_ID),
{ body: commands },
);
} else {
data = await rest.put(
Routes.applicationGuildCommands(process.env.DISCORD_APPLICATION_ID, process.env.GUILD_ID),
{ body: commands },
);
}
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
} catch (error) {
console.error(error);
}
})();
};

View File

@ -0,0 +1,9 @@
const { EmbedBuilder } = require('discord.js');
const simpleEmbed = (content) => {
return new EmbedBuilder()
.setColor(process.env.EMBED_COLOR)
.setDescription(content);
};
module.exports = { simpleEmbed };

View File

@ -0,0 +1,73 @@
const { PartialWebhookMixin } = require('discord.js');
const { sequelize } = require('../index');
const Sequelize = require('sequelize');
const Users = sequelize.define('users', {
id: {
type: Sequelize.STRING,
primaryKey: true,
unique: true,
},
useMinecraftUsername: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
rawUsername: {
type: Sequelize.STRING,
},
moderator: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
admin: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
});
const Team = sequelize.define('teams', {
name: {
type: Sequelize.STRING,
unique: true,
validate: {
len: [3, 16],
isAlphanumeric: true,
},
},
color: {
type: Sequelize.STRING,
validate: {
is: /^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
},
},
textChannelId: {
type: Sequelize.STRING,
},
voiceChannelId: {
type: Sequelize.STRING,
},
});
const Minecraft = sequelize.define('minecraft', {
uuid: {
type: Sequelize.UUID,
unique: true,
primaryKey: true,
},
whitelisted: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
code: {
type: Sequelize.STRING,
unique: true,
},
});
Minecraft.hasOne(Users);
Users.belongsTo(Minecraft);
Team.hasMany(Users, { as: 'members' });
Users.belongsTo(Team);
module.exports = { Users, Team, Minecraft };

View File

@ -0,0 +1,29 @@
const applyUsername = async (user, member) => {
await user.reload();
let rawUsername = member.user.globalName;
if (user.useMinecraftUsername && user.minecraftUuid) {
const response = await fetch(`https://sessionserver.mojang.com/session/minecraft/profile/${user.minecraftUuid}`);
const minecraftProfile = await response.json();
rawUsername = minecraftProfile.name;
}
user.rawUsername = rawUsername;
const username = await getUsername(user);
await user.save();
await member.setNickname(username.slice(0, 32));
return username;
};
const getUsername = async (user) => {
const team = await user.getTeam();
return team ? user.rawUsername + ' [' + team.name + ']' : user.rawUsername;
};
module.exports = { applyUsername, getUsername };

View File

@ -1,45 +1,29 @@
const chalk = require('chalk');
const { Client, GatewayIntentBits, Collection } = require('discord.js');
const { Player } = require('discord-player');
const express = require('express');
const Sequelize = require('sequelize');
const dotenv = require('dotenv');
const fs = require('node:fs');
const path = require('node:path');
const dotenv = require('dotenv');
const express = require('express');
const createEmbed = require('./functions/createEmbed.js');
dotenv.config();
const log = {
Info: (message) => console.log(chalk.blue('INFO'), message),
Error: (message) => console.log(chalk.red('ERROR'), message),
Warn: (message) => console.log(chalk.hex('#FFA500'), message),
};
// Register client and music events
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] });
client.player = new Player(client);
require('./functions/player.js').registerEvents({ client, createEmbed });
// Express
const app = express();
app.use(express.json());
const minecraftRoute = require('./routes/Minecraft');
const userRoute = require('./routes/User');
const teamRoute = require('./routes/Team');
app.use('/minecraft', minecraftRoute);
app.use('/user', userRoute);
app.use('/team', teamRoute);
app.listen('4000', () => {
log.Info('Express app running');
// Configure database
const sequelize = new Sequelize('polarcraft', 'user', process.env.DATABSE_PASSWORD, {
host: 'localhost',
dialect: 'sqlite',
logging: false,
// SQLite only
storage: 'database.sqlite',
});
exports.sequelize = sequelize;
// Configure Discord
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] });
exports.client = client;
// Command handling
client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
@ -52,12 +36,10 @@ for (const file of commandFiles) {
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
} else {
log.Warn(`The command at ${filePath} is missing a required "data" or "execute" property.`);
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
// Event handling
const eventsPath = path.join(__dirname, 'events');
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
@ -65,12 +47,26 @@ for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if (event.once) {
client.once(event.name, (...args) => event.execute({ client, log, createEmbed }, ...args));
client.once(event.name, (...args) => event.execute(...args));
} else {
client.on(event.name, (...args) => event.execute({ client, log, createEmbed }, ...args));
client.on(event.name, (...args) => event.execute(...args));
}
}
// Configure express
const app = express();
app.use(express.json());
const verifyMinecraftRoute = require('./routes/verifyminecraft');
const messageRoute = require('./routes/message');
app.use('/verifyminecraft', verifyMinecraftRoute);
app.use('/message', messageRoute);
app.listen('3000', () => {
console.log('Express app is running');
});
client.login(process.env.DISCORD_TOKEN);
module.exports.client = client;

File diff suppressed because it is too large Load Diff

View File

@ -2,21 +2,20 @@
"name": "discordbot",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"main": "index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js",
"build": "tsup src/index.ts --minify"
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"author": "Xeovalyte",
"license": "ISC",
"dependencies": {
"discord.js": "^14.11.0",
"dotenv": "^16.3.1"
"discord.js": "^14.12.1",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"sequelize": "^6.32.1",
"sqlite3": "^5.1.6"
},
"devDependencies": {
"tsup": "^7.1.0",
"tsx": "^3.12.7",
"typescript": "^5.1.3"
"eslint": "^8.46.0"
}
}

View File

@ -0,0 +1,56 @@
const express = require('express');
const { WebhookClient, EmbedBuilder } = require('discord.js');
const { getUsername } = require('../functions/utils.js');
const { Users } = require('../functions/models.js');
const router = express.Router();
const webhookClient = new WebhookClient({ url: process.env.MINECRAFT_WEBHOOK_URL });
router.post('/player', async (req, res) => {
const { content, uuid } = req.body;
if (!uuid || !content) return res.status(400).send({ errorMessage: 'uuid and content are required' });
try {
const user = await Users.findOne({ where: { minecraftUUID: uuid } });
const username = await getUsername(user);
webhookClient.send({
content,
username,
avatarURL: 'https://api.mineatar.io/face/' + uuid + '?scale=8',
});
res.send({ status: 'ok' });
} catch (error) {
console.log(error);
res.status(500).send({ errorMessage: 'Error while sending player message' });
}
});
router.post('/game', async (req, res) => {
const { content, uuid } = req.body;
if (!uuid || !content) return res.status(400).send({ errorMessage: 'uuid and content are required' });
try {
const messageEmbed = new EmbedBuilder()
.setColor(process.env.EMBED_COLOR)
.setAuthor({ name: content, iconURL: 'https://api.mineatar.io/face/' + uuid + '?scale=8' });
webhookClient.send({
embeds: [messageEmbed],
username: 'Server',
});
res.send({ status: 'ok' });
} catch (error) {
console.log(error);
res.status(500).send({ errorMessage: 'Error while sending player message' });
}
});
module.exports = router;

View File

@ -0,0 +1,47 @@
const express = require('express');
const { Minecraft, Users } = require('../functions/models.js');
const router = express.Router();
router.post('/', async (req, res) => {
const { uuid } = req.body;
if (!uuid) return res.status(400).send({ errorMessage: 'UUID is required' });
try {
const user = await Users.findOne({ where: { minecraftUUID: uuid } });
if (!user) {
const minecraftCol = await Minecraft.findOrCreate({
where: { uuid },
defaults: {
code: generateCode().toString(),
},
});
return res.send({ code: minecraftCol[0].code, whitelisted: minecraftCol[0].whitelisted });
}
const team = await user.getTeam();
let username;
if (!team) {
username = user.rawUsername;
} else {
username = '<gray>[</gray>' + `<color:${team.color}>${team.name}</color>` + '<gray>] </gray>' + user.rawUsername;
}
res.send({ whitelisted: true, username, rawUsername: user.rawUsername });
} catch (error) {
console.log(error);
res.status(500).send({ errorMessage: 'Error while verifing minecraft uuid' });
}
});
const generateCode = () => {
return Math.floor(100000 + Math.random() * 900000);
};
module.exports = router;

View File

@ -1,10 +0,0 @@
import { CommandInteraction, SlashCommandBuilder, Client } from "discord.js";
export const data = new SlashCommandBuilder()
.setName("ping")
.setDescription("Replies with Pong!");
export async function execute({ interaction, client, createEmbed }: { interaction: CommandInteraction, client: Client, createEmbed: Function }) {
const reply = await interaction.reply({ embeds: [createEmbed(`Websocket heartbeat: **${client.ws.ping}ms**\n Roundtrip latency: **Pinging...**`)], fetchReply: true, ephemeral: true });
interaction.editReply({ embeds: [createEmbed(`Websocket heartbeat: **${client.ws.ping}ms**\n Roundtrip latency: **${reply.createdTimestamp - interaction.createdTimestamp}ms**`)] });
}

View File

@ -1,20 +0,0 @@
import { ColorResolvable } from "discord.js";
import dotenv from "dotenv";
dotenv.config();
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_GUILD_ID } = process.env;
if (!DISCORD_TOKEN || !DISCORD_CLIENT_ID) {
throw new Error("Missing environment variables");
}
const EMBED_COLOR: ColorResolvable = '#0080ff'
export const config = {
DISCORD_TOKEN,
DISCORD_CLIENT_ID,
DISCORD_GUILD_ID,
EMBED_COLOR
};

View File

@ -1,23 +0,0 @@
import { Events, Client, BaseInteraction } from 'discord.js'
export const name = Events.InteractionCreate
export const execute = async ({ client, createEmbed }: { client: Client, createEmbed: Function }, interaction: BaseInteraction) => {
if (interaction.isChatInputCommand()) {
const command = interaction.client.commands.get(interaction.commandName);
if (!command) {
return console.error(`No command matching ${interaction.commandName} was found`);
} else {
console.info(`${interaction.user.username} executed command ${interaction.commandName}`);
}
try {
await command.execute({ interaction, client, createEmbed });
} catch (error) {
console.error(error);
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}
}
}

View File

@ -1,12 +0,0 @@
import { Events, Client } from 'discord.js'
import registerCommands from '../registerCommands'
export const name = Events.ClientReady
export const once = true
export const execute = async ({ client }: { client: Client }) => {
console.log(`Ready! Logged in as ${client.user.tag}`)
registerCommands()
}

View File

@ -1,45 +0,0 @@
import { Client, GatewayIntentBits, Collection, EmbedBuilder } from 'discord.js'
import fs from 'node:fs'
import path from 'node:path'
import { config } from './config'
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent]
})
const createEmbed = (description: string) => {
return new EmbedBuilder()
.setColor(config.EMBED_COLOR)
.setDescription(description);
};
client.commands = new Collection()
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.ts'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
} else {
console.error(`The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
const eventsPath = path.join(__dirname, 'events');
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.ts'));
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if (event.once) {
client.once(event.name, (...args) => event.execute({ client, createEmbed }, ...args));
} else {
client.on(event.name, (...args) => event.execute({ client, createEmbed }, ...args));
}
}
client.login(config.DISCORD_TOKEN);

View File

@ -1,54 +0,0 @@
import fs from 'node:fs'
import path from 'node:path'
import { REST, Routes } from 'discord.js'
import { config } from './config'
type Command = {
data: any
}
export default async () => {
const commands = []
const commandsPath = path.join(__dirname, 'commands')
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('ts'))
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file)
const command = await import(filePath) as Command
if ('data' in command && 'execute' in command) {
commands.push(command.data.toJSON())
} else {
console.error(`The command at ${filePath} is missing a required "data" or "execute" property`)
}
}
const rest = new REST().setToken(config.DISCORD_TOKEN)
try {
console.log(`Started refreshing ${commands.length} application (/) commands.`);
// The put method is used to fully refresh all commands in the guild with the current set
if (config.DISCORD_GUILD_ID) {
const data: any = await rest.put(
Routes.applicationGuildCommands(config.DISCORD_CLIENT_ID, config.DISCORD_GUILD_ID),
{ body: commands },
);
console.log(`Successfully reloaded ${data.length} guild (/) commands.`);
} else {
const data: any = await rest.put(
Routes.applicationCommands(config.DISCORD_CLIENT_ID),
{ body: commands },
);
console.log(`Successfully reloaded ${data.length} global (/) commands.`);
}
} catch (error) {
// And of course, make sure you catch and log any errors!
console.error(error);
}
}

View File

@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist",
"removeComments": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"strictNullChecks": true,
"skipLibCheck": true
}
}

View File

@ -1,7 +0,0 @@
import { Collection } from 'discord.js'
declare module 'discord.js' {
export interface Client {
commands: Collection<unknown, any>
}
}

View File

@ -1,17 +0,0 @@
{
"root": true,
"extends": ["@nuxt/eslint-config", "plugin:tailwindcss/recommended"],
"rules": {
"vue/max-attributes-per-line": ["error", {
"singleline": {
"max": 4
},
"multiline": {
"max": 2
}
}],
"vue/multi-word-component-names": "off",
"vue/singleline-html-element-content-newline": "off",
"no-undef": "off"
}
}

9
web/.gitignore vendored
View File

@ -1,9 +0,0 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
dist
.DS_Store

View File

@ -1,2 +0,0 @@
shamefully-hoist=true
strict-peer-dependencies=false

View File

@ -1,9 +0,0 @@
FROM node:18-alpine
WORKDIR /usr/src/app
COPY .output .
EXPOSE 3000
CMD [ "node", "server/index.mjs" ]

View File

@ -1,42 +0,0 @@
# Nuxt 3 Minimal Starter
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# yarn
yarn install
# npm
npm install
# pnpm
pnpm install
```
## Development Server
Start the development server on `http://localhost:3000`
```bash
npm run dev
```
## Production
Build the application for production:
```bash
npm run build
```
Locally preview production build:
```bash
npm run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View File

@ -1,5 +0,0 @@
<template>
<div class="h-screen w-full overflow-y-auto">
<NuxtLayout />
</div>
</template>

View File

@ -1,17 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.sidebar-item {
@apply font-medium text-lg w-full px-4 py-2 rounded hover:bg-white hover:bg-opacity-5 hover:cursor-pointer flex items-center
}
.highlight {
@apply bg-primary bg-opacity-20 px-2 rounded
}
}
.router-link-active {
background-color: rgba(0, 0, 0, 20%)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

View File

@ -1,43 +0,0 @@
<template>
<div class="flex w-full flex-col items-center text-primary">
<div v-if="!user.minecraft.uuid" class="flex flex-col items-center">
<p class="mb-10 max-w-xl text-sm sm:text-base">
Je bent momenteel niet gewhitelist. Om toegang te krijgen tot de Minecraft server moet je in Minecraft naar de
server met het ip <span class="highlight">play.polarcraft.xeovalyte.com</span> gaan. Vervolgens krijg je een code
te zien, vul deze code hieronder in.
</p>
<div class="flex gap-2">
<Input v-model="code">Code</Input>
<Button @click="submitCode">Submit</Button>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
middleware: ["auth"]
})
const user = useState('user')
const code = ref('')
const submitCode = async () => {
try {
const response = await $fetch('/api/minecraft/whitelist', {
method: 'POST',
body: {
code: code.value
}
})
user.value.minecraft.uuid = response.uuid
user.value.minecraft.username = response.username
useToast().success('Succesvol gewhitelist')
} catch (e) {
console.log(e);
useToast().error(e.statusMessage)
}
}
</script>

View File

@ -1,19 +0,0 @@
<template>
<div class="flex items-center border-b border-b-gray-700 bg-neutral-800 px-5">
<h1 class="text-2xl font-bold text-primary">Polarcraft</h1>
<Button outline class="ml-auto" @click="logout">Logout</Button>
</div>
</template>
<script setup>
const logout = async () => {
try {
await useFetch('/api/auth/logout')
navigateTo('/login')
} catch (err) {
console.log(err)
alert(err.statusMessage)
}
}
</script>

View File

@ -1,35 +0,0 @@
<template>
<div class="flex flex-col gap-1 bg-neutral-800 px-2 pb-14 pt-5 text-gray-300">
<NuxtLink to="/" class="sidebar-item">
<Icon size="1.5em" name="ph:house" class="mr-3" />
Home
</NuxtLink>
<NuxtLink to="/team" class="sidebar-item">
<Icon size="1.5em" name="ph:users-three" class="mr-3" />
Team
</NuxtLink>
<NuxtLink to="/player-store" class="sidebar-item">
<Icon size="1.5em" name="ph:storefront" class="mr-3" />
Player stores
</NuxtLink>
<NuxtLink to="/map" class="sidebar-item">
<Icon size="1.5em" name="ph:map-trifold" class="mr-3" />
Map
</NuxtLink>
<div v-if="user && user.role.admin" class="mt-auto space-y-1">
<h2 class="ml-2 text-gray-400">Moderation</h2>
<NuxtLink to="/mod/users" class="sidebar-item">
<Icon size="1.5em" name="ph:users-three" class="mr-3" />
Users
</NuxtLink>
<NuxtLink to="/mod/server" class="sidebar-item">
<Icon size="1.5em" name="ph:cpu" class="mr-3" />
Server Control
</NuxtLink>
</div>
</div>
</template>
<script setup>
const user = useState('user')
</script>

View File

@ -1,180 +0,0 @@
<template>
<Modal v-if="modalOpen" title="Invite team member" @close="modalOpen = false" @submit="modalOpen = false">
<div>
<h2 class="text-lg font-bold text-primary">Users</h2>
<div class="h-48 space-y-3 overflow-y-auto">
<div v-for="unaffiliatedUser in unaffilatedUsers" :key="unaffiliatedUser._id.toString()" class="flex items-center rounded bg-neutral-700 px-3 py-1 text-gray-200">
{{ unaffiliatedUser.username }}
<Button v-if="!unaffiliatedUser.teamInvites.includes(user.team.id)" class="ml-auto" @click="inviteUser(unaffiliatedUser)">Invite</Button>
<Button v-else class="ml-auto" type="danger" @click="cancelInvite(unaffiliatedUser)">Cancel Invite</Button>
</div>
</div>
</div>
</Modal>
<Modal v-if="editTeamModal.open" title="Edit team" @close="editTeamModal.open = false" @submit="editTeam">
<Input v-model="editTeamModal.name" background-class="bg-neutral-800" class="w-full max-w-sm">Naam / Prefix</Input>
<Colorpicker v-model="editTeamModal.color" input-background-class="bg-neutral-800" class="w-full max-w-sm" />
</Modal>
<div class="mx-auto my-10 max-w-2xl">
<h2 class="mb-2 text-xl font-bold text-primary">Team Information</h2>
<div class="rounded border-[1px] border-primary px-5 py-2 text-primary">
<table class="w-full table-auto">
<tbody class="divide-y divide-gray-700">
<tr>
<td class="py-3">Name</td>
<td class="font-bold">{{ team.name }}</td>
</tr>
<tr>
<td class="py-3">Color</td>
<td class="font-bold">{{ team.color }}</td>
</tr>
<tr>
<td class="py-3">ID</td>
<td class="font-bold">{{ team._id }}</td>
</tr>
</tbody>
</table>
</div>
<div class="mb-10 mt-5 flex justify-center gap-x-3">
<Button type="danger" @click="leaveTeam">Leave Team</Button>
<Button v-if="user.team.admin" @click="openTeamModal">Edit Team</Button>
</div>
<h2 class="mb-2 text-xl font-bold text-primary">Team Members</h2>
<div class="space-y-5 rounded border-[1px] border-primary p-5 text-primary">
<div v-for="teamMember in teamMembers" :key="teamMember._id" class="flex h-12 items-center rounded bg-neutral-800 px-5 font-bold text-gray-200">
{{ teamMember.username }}
<span v-if="teamMember.team.admin" class="ml-3 text-sm text-gray-400">Admin</span>
<div v-if="user.team.admin" class="ml-auto">
<Button v-if="!teamMember.team.admin && teamMember._id !== user._id" @click="promoteUser(teamMember)">Promote</Button>
<Button v-if="teamMember.team.admin && teamMember._id !== user._id" @click="demoteUser(teamMember)">Demote</Button>
</div>
</div>
<div v-if="user.team.admin" class="rounded border-2 border-dashed border-neutral-500 bg-neutral-800 p-3 text-center font-bold text-gray-200 hover:cursor-pointer hover:bg-neutral-700" @click="modalOpen = true">
Invite new member
</div>
</div>
</div>
</template>
<script setup>
const user = useState('user');
const { data: team } = await useFetch('/api/team');
const { data: teamMembers } = await useFetch('/api/team/members');
const { data: unaffilatedUsers } = await useFetch('/api/team/unaffiliated')
const modalOpen = ref(false)
const editTeamModal = ref({
open: false,
name: '',
color: '',
})
const openTeamModal = () => {
editTeamModal.value.name = team.value.name
editTeamModal.value.color = team.value.color
editTeamModal.value.open = true
}
const leaveTeam = async () => {
try {
await $fetch('/api/team/leave');
user.value.team = null;
useToast().success('Succesvol team verlaten')
} catch (e) {
console.log(e);
useToast().error(e.statusMessage)
}
}
const editTeam = async () => {
try {
await $fetch('/api/team/edit', {
method: 'POST',
body: {
name: editTeamModal.value.name,
color: editTeamModal.value.color,
}
});
team.value.name = editTeamModal.value.name
team.value.color = editTeamModal.value.color
useToast().success('Team is succesvol bewerkt')
editTeamModal.value.open = false
} catch (e) {
console.log(e);
useToast().error(e.statusMessage)
}
}
const inviteUser = async (usr) => {
try {
await $fetch('/api/team/invite', {
method: 'POST',
body: { id: usr._id }
});
usr.teamInvites.push(user.value.team.id);
useToast().success(`Invited ${usr.username} succesvol`)
} catch (e) {
console.log(e);
useToast().error(e.statusMessage)
}
}
const cancelInvite = async (usr) => {
try {
await $fetch('/api/team/cancelinvite', {
method: 'POST',
body: { id: usr._id }
});
usr.teamInvites = usr.teamInvites.filter(a => a !== user.value.team.id);
useToast().success('Succesvol invite geannuleerd')
} catch (e) {
console.log(e);
useToast().error(e.statusMessage)
}
}
const promoteUser = async (usr) => {
try {
await $fetch('/api/team/promote', {
method: 'POST',
body: { userId: usr._id }
});
usr.team.admin = true
console.log(usr)
console.log(teamMembers)
useToast().success('Succesvol gebruiker gepromoveerd')
} catch (e) {
console.log(e);
useToast().error(e.statusMessage)
}
}
const demoteUser = async (usr) => {
try {
await $fetch('/api/team/demote', {
method: 'POST',
body: { userId: usr._id }
});
usr.team.admin = false
useToast().success('Succesvol gebruiker gedegradeerd')
} catch (e) {
console.log(e);
useToast().error(e.statusMessage)
}
}
</script>

View File

@ -1,84 +0,0 @@
<template>
<p class="my-10 text-center text-gray-300">
Je zit momenteel niet in een team. Maak een team aan of wacht tot dat je geinvite wordt
</p>
<div class="mb-5 flex w-full justify-center gap-10">
<span
class="font-bold text-primary hover:cursor-pointer" :class="{ 'underline underline-offset-4': !createTeam.show }"
@click="createTeam.show = false"
>
Team Invites
</span>
<span
class="font-bold text-primary hover:cursor-pointer" :class="{ 'underline underline-offset-4': createTeam.show }"
@click="createTeam.show = true"
>
Create Team
</span>
</div>
<div v-if="!createTeam.show" class="text-center text-gray-300">
<h2 v-if="!user.teamInvites.length">You don't have any team invites</h2>
<div v-else class="mx-auto flex max-w-lg flex-col">
<div v-for="team in filteredTeams" :key="team._id" class="flex items-center rounded bg-neutral-800 px-3 py-1 text-left" @click="acceptInvite(team)">
<span>
{{ team.name }}
</span>
<Button class="ml-auto">Accept</Button>
</div>
</div>
</div>
<div v-else class="flex w-full flex-col items-center gap-5">
<Input v-model="createTeam.name" class="w-full max-w-sm">Naam</Input>
<Colorpicker v-model="createTeam.color" class="w-full max-w-sm" />
<Button @click="handleCreateTeam">Create Team</Button>
</div>
</template>
<script setup>
const user = useState('user')
const teams = await $fetch('/api/team/all')
const filteredTeams = computed(() => {
return teams.filter(a => user.value.teamInvites.includes(a._id));
})
const createTeam = ref({
show: false,
name: '',
color: '',
})
const acceptInvite = async (team) => {
try {
const response = await $fetch('/api/team/acceptinvite', {
method: 'POST',
body: { teamId: team._id }
})
user.value.team = { id: response._id, admin: false }
useToast().success('Succesvol lid geworden van het team')
} catch (e) {
console.log(e);
useToast().error(e.statusMessage)
}
}
const handleCreateTeam = async () => {
try {
const response = await $fetch('/api/team/create', {
method: 'POST',
body: { teamName: createTeam.value.name, teamColor: createTeam.value.color }
})
user.value.team = { id: response.insertedId.toString(), admin: true }
useToast().success('Succesvol team gemaakt')
} catch (e) {
console.log(e);
useToast().error(e.statusMessage)
}
}
</script>

View File

@ -1,3 +0,0 @@
<template>
<NuxtPage />
</template>

View File

@ -1,20 +0,0 @@
<template>
<div class="h-full bg-neutral-900">
<div v-if="user" class="hidden h-full grid-cols-desktoplayout grid-rows-desktoplayout sm:grid">
<LayoutNavbar class="col-span-2" />
<LayoutSidebar v-if="user.minecraft.uuid" class="" />
<div class="overflow-y-auto px-10" :class="{ 'col-span-2': !user.minecraft.uuid }">
<NuxtPage />
</div>
</div>
<div v-if="user" class="h-full sm:hidden">
<div class="overflow-y-auto p-2">
<NuxtPage />
</div>
</div>
</div>
</template>
<script setup>
const user = useState('user')
</script>

View File

@ -1,17 +0,0 @@
export default defineNuxtRouteMiddleware(async (to) => {
if (process.server) return;
try {
const user = await $fetch('/api/auth/user')
useState('user', () => user)
if (to.meta.moderator && !user.role.moderator) return navigateTo('/')
} catch (err) {
console.log(err)
useState('user', () => null)
return navigateTo('/login')
}
})

View File

@ -1,40 +0,0 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
extends: 'node_modules/@xeovalyte/nuxt-xvui',
devtools: true,
ssr: false,
modules: [
'@nuxtjs/tailwindcss',
'@xeovalyte/nuxt-xvtoast',
'nuxt-icon',
'@nuxtjs/tailwindcss',
'@vueuse/nuxt',
'@nuxt/image-edge'
],
app: {
head: {
link: [
{ rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
{ rel: "icon", sizes: "32x32", type: "image/png", href: "/favicon-32x32.png" },
{ rel: "icon", sizes: "16x16", type: "image/png", href: "/favicon-16x16.png" },
{ rel: "manifest", href: "/site.webmanifest" },
]
}
},
runtimeConfig: {
discordId: '',
discordSecret: '',
discordHost: '',
jwtSecret: '',
dbUrl: '',
mineckerHost: '',
mineckerApiKey: '',
rconPassword: '',
rconPort: '25575',
rconHost: 'localhost',
redirectURI: 'http://localhost:3000/api/auth',
public: {
redirectUrl: 'https://discord.com/api/oauth2/authorize?client_id=1052974736432443432&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth&response_type=code&scope=identify',
}
}
})

25302
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +0,0 @@
{
"name": "nuxt-app",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint .",
"lint-fix": "eslint . --fix"
},
"devDependencies": {
"@nuxt/devtools": "^0.5.0",
"@nuxt/image-edge": "^1.0.0-28059208.2abef1b",
"@nuxtjs/eslint-module": "^4.1.0",
"@nuxtjs/tailwindcss": "^6.7.0",
"@types/node": "^20",
"@vueuse/components": "^10.1.2",
"@vueuse/core": "^10.1.2",
"@vueuse/nuxt": "^10.1.2",
"eslint": "^8.41.0",
"eslint-plugin-tailwindcss": "^3.12.0",
"nuxt": "^3.5.0",
"nuxt-icon": "^0.4.1"
},
"dependencies": {
"@nuxt/eslint-config": "^0.1.1",
"@xeovalyte/nuxt-xvtoast": "^1.1.3",
"@xeovalyte/nuxt-xvui": "git+https://gitea.xeovalyte.dev/xeovalyte/nuxt-xvui.git",
"jsonwebtoken": "^9.0.0",
"minecraft-server-util": "^5.4.2",
"mongodb": "^5.5.0",
"socket.io-client": "^4.6.1"
}
}

View File

@ -1,120 +0,0 @@
<template>
<div class="flex h-full w-full flex-col text-primary">
<h1 class="mb-10 mt-20 text-center text-5xl font-bold">Welkom, {{ user.username }}</h1>
<Whitelist v-if="!user.minecraft.uuid" />
<div v-else class="flex w-full flex-wrap justify-evenly gap-10">
<div class="flex w-full max-w-xl flex-col gap-3">
<h2 class="text-xl font-bold text-primary">Discord Information <span v-if="!user.useMinecraftUsername" class="font-normal">(Default)</span></h2>
<div class="flex gap-3">
<img :src="'https://cdn.discordapp.com/avatars/' + user.discord.id + '/' + user.discord.avatarHash + '.png'" class="aspect-square w-24 rounded shadow">
<div class="flex w-full rounded border-2 border-primary p-4">
<ul class="my-auto">
<li>Username: <b>{{ user.discord.username }}</b></li>
<li>ID: <b>{{ user.discord.id }}</b></li>
</ul>
</div>
</div>
<div class="mt-2 flex w-full justify-center gap-4">
<Button @click="refreshDiscordUsername">
Refresh Username
</Button>
<Button v-if="user.useMinecraftUsername" @click="setDefaultUsername('discord')">
Set Default
</Button>
</div>
</div>
<div class="flex w-full max-w-xl flex-col gap-3">
<h2 class="text-xl font-bold text-primary">Minecraft Information <span v-if="user.useMinecraftUsername" class="font-normal">(Default)</span></h2>
<div class="flex gap-3">
<img :src="'https://api.mineatar.io/face/' + user.minecraft.uuid + '?scale=16'" class="aspect-square w-24 rounded shadow">
<div class="flex w-full rounded border-2 border-primary p-4">
<ul class="my-auto">
<li>Username: <b>{{ user.minecraft.username }}</b></li>
<li>UUID: <b>{{ user.minecraft.uuid }}</b></li>
</ul>
</div>
</div>
<div class="mt-2 flex w-full justify-center gap-4">
<Button type="danger" @click="removeWhitelist">
Remove from whitelist
</Button>
<Button @click="refreshMinecraftUsername">
Refresh Username
</Button>
<Button v-if="!user.useMinecraftUsername" @click="setDefaultUsername('minecraft')">
Set Default
</Button>
</div>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
middleware: ["auth"],
})
useHead({ title: 'Polarcraft' })
const user = useState('user')
const refreshMinecraftUsername = async () => {
try {
const response = await $fetch('/api/minecraft/refreshusername')
user.value.minecraft.username = response.username
useToast().success('Gebruikersnaam is ververst')
} catch (e) {
console.log(e)
useToast().error(e.statusMessage)
}
}
const refreshDiscordUsername = async () => {
try {
const response = await $fetch('/api/discord/refreshusername')
user.value.discord.username = response.username
useToast().success('Gebruikersnaam is ververst')
} catch (e) {
console.log(e)
useToast().error(e.statusMessage)
}
}
const removeWhitelist = async () => {
try {
await $fetch('/api/minecraft/removewhitelist')
user.value.minecraft.uuid = null
user.value.minecraft.username = null
useToast().success('Minecraft is niet meer gekoppeld')
} catch (e) {
console.log(e)
useToast().error(e.statusMessage)
}
}
const setDefaultUsername = async (type) => {
try {
const response = await $fetch('/api/auth/user/setdefaultusername', {
method: 'POST',
body: { type: type }
})
user.value.username = response.username
user.value.useMinecraftUsername = type === 'discord' ? false : true
useToast().success(`${ type === 'discord' ? 'Discord' : 'Minecraft' } is nu de standaard gebruikersnaam`)
} catch (e) {
console.log(e)
useToast().error(e.statusMessage)
}
}
</script>

View File

@ -1,26 +0,0 @@
<template>
<div class="flex h-screen w-full flex-col items-center justify-center bg-transparent px-2 text-primary">
<h1 class="mb-5 text-center text-3xl font-bold sm:text-5xl">Polarcraft S5</h1>
<p class="mb-10 max-w-xl text-sm sm:text-base">
<b>Welkom bij Polarcraft seizoen 5!</b> Start door in te loggen met Discord en vervolgens je account te koppelen met Minecraft. Als je problemen hebt maak dan in Discord een ticket aan door het <span class="highlight">/ticket</span> commando uit te voeren.
</p>
<Button @click="navigateTo(config.public.redirectUrl, { external: true })">
Log in with Discord
<Icon size="1.6em" name="ic:baseline-discord" />
</Button>
<div class="absolute left-0 top-0 -z-10 h-screen w-full overflow-hidden">
<nuxt-img src="/pictures/diamond_wall.webp" class="h-full w-full scale-105 object-cover blur-[1px] brightness-75" />
</div>
</div>
</template>
<script setup>
definePageMeta({
layout: 'blank'
})
useHead({ title: 'Login | Polarcraft' })
const config = useRuntimeConfig()
</script>

View File

@ -1,13 +0,0 @@
<template>
<div class="-mx-10 h-full">
<iframe src="https://squaremap-demo.jpenilla.xyz/" class="h-full w-full" />
</div>
</template>
<script setup>
definePageMeta({
middleware: ["auth"]
})
useHead({ title: 'Map | Polarcraft' })
</script>

View File

@ -1,14 +0,0 @@
<template>
<div class="mt-5">
Server Control
</div>
</template>
<script setup>
definePageMeta({
middleware: ["auth"],
moderator: true
})
useHead({ title: 'Server Control | Polarcraft' })
</script>

View File

@ -1,78 +0,0 @@
<template>
<div class="mt-5">
<Modal v-if="actionModal.open" :title="actionModal.title" @click="actionModal.func">
<Select v-model="actionModal.reason" :options="[{ name: 'Griefing', value: 'griefing' }]">Reason</Select>
</Modal>
<h1 class="text-2xl font-bold text-primary">
Users
</h1>
<div class="mx-auto my-10 max-w-4xl">
<h2 class="mb-2 text-xl font-bold text-primary">User Actions</h2>
<div class="w-full rounded border-[1px] border-primary px-3 py-1 text-left">
<table class="w-full table-auto divide-y divide-neutral-500 rounded">
<tr class="border-b-2 border-primary text-gray-200">
<th class="py-1">Username</th>
<th class="py-1">Team</th>
<th class="py-1">MC Linked</th>
<th class="py-1">Actions</th>
</tr>
<tr v-for="user in users" :key="user._id" class="text-gray-200">
<td class="py-1">{{ user.username }}</td>
<td class="py-1">{{ user.team ? teams.filter(a => a._id === user.team.id)[0].name : '-' }}</td>
<td class="py-1">{{ user.minecraft.uuid ? 'True ' : 'False' }}</td>
<td class="space-x-2 py-1">
<span v-if="currentUser.role.admin" class="rounded bg-red-700 px-2 text-red-300 hover:cursor-pointer hover:bg-red-800" @click="openModal(user, 'ban', ban(user))">Ban</span>
<span class="rounded bg-orange-700 px-2 text-orange-300 hover:cursor-pointer hover:bg-orange-800">Suspend</span>
<span class="rounded bg-yellow-700 px-2 text-yellow-300 hover:cursor-pointer hover:bg-yellow-800">Warn</span>
</td>
</tr>
</table>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
middleware: ["auth"],
moderator: true
})
useHead({ title: 'Users | Polarcraft' })
const currentUser = useState('user')
const { data: users } = useFetch('/api/auth/getusers')
const { data: teams } = useFetch('/api/team/all')
const actionModal = ref({
open: false,
title: '',
reason: '',
userId: '',
func: null,
})
const openModal = (user, type, func) => {
if (type === 'ban') actionModal.value.title = 'Ban User'
if (type === 'suspend') actionModal.value.title = 'Suspend User'
if (type === 'warn') actionModal.value.title = 'Warn User'
actionModal.value.func = func
actionModal.value.userId = user._id;
actionModal.value.open = true
}
const ban = async (user) => {
try {
const response = await $fetch(`/api/auth/user/${user._id}/ban`, {
reason: "reason"
})
user = response;
} catch (e) {
console.log(e);
useToast().error('Error banning user')
}
}
</script>

View File

@ -1,158 +0,0 @@
<template>
<div class="mt-5 text-primary">
<Modal
v-if="storeModal.open"
title="Create new store"
:delete="!storeModal.new"
@submit="submitModal"
@delete="deleteStore"
@close="storeModal.open = false"
>
<Input v-model="storeModal.name" background-class="bg-neutral-800">Store name</Input>
<div class="flex gap-x-5">
<Input v-model="storeModal.coords.x" background-class="bg-neutral-800">Coordinate X</Input>
<Input v-model="storeModal.coords.z" background-class="bg-neutral-800">Coordinate Z</Input>
</div>
<h2 class="font-bold">Items</h2>
<div class="max-h-56 space-y-3 overflow-scroll">
<div v-for="item, index in storeModal.items" :key="item.name" class="flex gap-2">
<Input v-model="item.displayName" background-class="bg-neutral-800">Name</Input>
<Input v-model="item.price" background-class="bg-neutral-800">Price per quantity</Input>
<Button type="danger" @click="storeModal.items.splice(index, 1)"><Icon size="1.5em" name="ph:trash" class="-my-2" /></Button>
</div>
</div>
<Button class="" @click="storeModal.items.push({ displayName: '', price: '' })">Add Item</Button>
</Modal>
<h1 class="pb-10 text-2xl font-bold text-primary">
Player Stores
</h1>
<div class="fixed bottom-10 right-10">
<Button class="fixed" @click="openStoreModal(true)">
Create Store
</Button>
</div>
<div class="flex flex-wrap gap-10">
<div v-for="store in stores" :key="store._id.toString()" class="h-min w-96 rounded-lg bg-neutral-800 text-gray-200 shadow relative">
<div v-if="store.ownerId === user._id" class="absolute right-5 top-5 hover:cursor-pointer" @click="openStoreModal(false, store)">
<Icon name="ph:pencil-simple" size="1.6em" />
</div>
<div class="p-5 pb-3">
<div class="flex items-center">
<img :src="'https://api.mineatar.io/face/' + store.owner.minecraft.uuid + '?scale=3'" class="mr-2 aspect-square rounded">
{{ store.owner.username }}
</div>
<h2 class="text-2xl font-bold text-primary">
{{ store.name }}
<span class="text-sm font-medium">({{ store.coords.x }}, {{ store.coords.z }})</span>
</h2>
</div>
<h2 class="ml-3 text-xl font-bold">Items</h2>
<div class="divide-y divide-neutral-600 rounded-lg bg-neutral-700 px-4 pt-2 pb-2">
<div v-for="item in store.items" :key="item.displayname" class="py-1">
<b>{{ item.displayName }}</b> for <b>{{ item.price }}</b>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
middleware: ["auth"]
})
const user = useState('user')
useHead({ title: 'Player Stores | Polarcraft' })
const { data: stores } = await useFetch('/api/store');
const storeModal = ref({
open: false,
name: '',
new: false,
id: '',
coords: {
x: '',
z: '',
},
items: [
{ displayName: '', price: '' }
],
})
const openStoreModal = (newStore, store) => {
storeModal.value.open = true;
if (newStore === true) {
storeModal.value.name = '';
storeModal.value.items = [
{ displayName: '', price: '' }
];
storeModal.value.coords = { x: '', z: '' }
storeModal.value.new = true
} else {
storeModal.value.new = false
storeModal.value.items = []
store.items.forEach(item => {
storeModal.value.items.push(item);
})
storeModal.value.name = store.name
storeModal.value.id = store._id
storeModal.value.coords.x = store.coords.x
storeModal.value.coords.z = store.coords.z
}
}
const submitModal = async () => {
try {
const store = await $fetch('/api/store', {
method: 'POST',
body: { name: storeModal.value.name, coords: storeModal.value.coords, items: storeModal.value.items, id: storeModal.value.id || null }
})
if (storeModal.value.id) {
const oldStore = stores.value.filter(a => a._id === storeModal.value.id)[0]
oldStore.name = storeModal.value.name;
oldStore.coords = storeModal.value.coords;
oldStore.items = storeModal.value.items;
} else {
store.owner = user
stores.value.push(store)
}
storeModal.value.open = false
useToast().success('Succesvol store aangemaakt')
} catch (e) {
console.log(e)
useToast().error(e.statusMessage)
}
}
const deleteStore = async () => {
try {
await $fetch('/api/store', {
method: 'DELETE',
body: { id: storeModal.value.id }
})
stores.value = stores.value.filter(a => a._id !== storeModal.value.id)
storeModal.value.open = false
useToast().success('Succesvol store verwijderd')
} catch (e) {
console.log(e)
useToast().error(e.statusMessage)
}
}
</script>

View File

@ -1,19 +0,0 @@
<template>
<div class="mt-5">
<h1 class="text-2xl font-bold text-primary">
Team
</h1>
<TeamNone v-if="!user.team" />
<TeamDefault v-else />
</div>
</template>
<script setup>
const user = useState('user')
definePageMeta({
middleware: ["auth"]
})
useHead({ title: 'Team | Polarcraft' })
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

View File

@ -1 +0,0 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -1,13 +0,0 @@
export default defineEventHandler(async (event) => {
const user = await getAuth(event)
if (!user.role.moderator) return createError({ statusCode: 403, statusMessage: 'Gebruiker heeft geen toegang tot alle gebruikers' })
const usersColl = db.collection('users');
const cursor = usersColl.find()
const users = await cursor.toArray()
return users
});

View File

@ -1,52 +0,0 @@
export default defineEventHandler(async (event) => {
const { code } = getQuery(event)
const config = useRuntimeConfig()
if (!code) return sendRedirect(event, '/', 302)
try {
const tokenResponseData = await $fetch('https://discord.com/api/oauth2/token', {
method: 'POST',
body: new URLSearchParams({
client_id: config.discordId,
client_secret: config.discordSecret,
code: code,
grant_type: 'authorization_code',
redirect_uri: config.redirectURI,
scope: 'identify',
}).toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
const userResult = await $fetch('https://discord.com/api/users/@me', {
headers: {
authorization: `Bearer ${tokenResponseData.access_token}`
}
})
const coll = db.collection('users')
const doc = {
discord: {
id: userResult.id,
username: userResult.username,
avatarHash: userResult.avatar || null
},
}
await coll.updateOne({ 'discord.id': userResult.id }, { $set: doc, $setOnInsert: { minecraft: { uuid: null, username: null }, teamInvites: [], role: {} } }, { upsert: true })
const token = createToken(tokenResponseData.access_token, tokenResponseData.refresh_token, tokenResponseData.expires_in, userResult.id )
setCookie(event, 'jwt', token, { httpOnly: true, maxAge: tokenResponseData.expires_in * 1000 })
} catch (e) {
console.log(e)
throw createError({ statusCode: 500, statusMessage: 'Error tijdens het genereren van JWT token'})
}
return sendRedirect(event, '/', 302)
});

View File

@ -1,25 +0,0 @@
import { ObjectId } from "mongodb";
export default defineEventHandler(async (event) => {
const { reason } = await readBody(event)
const currentUser = await getAuth(event)
const userId = event.context.params.id;
if (!reason) return createError({ statusCode: 400, statusMessage: 'Reason is vereist' })
if (!currentUser.role.admin) return createError({ statusCode: 403, statusMessage: 'Geen toegang om gebruiker te bannen' })
const usersColl = db.collection('users')
const user = usersColl.findOneAndUpdate({ _id: new ObjectId(userId)}, { $set: { banned: { reason: reason, date: new Date() } } })
if (!user.value) return createError({ statusCode: 500, statusMessage: 'Error tijdens het updaten van de gebruiker' })
await $fetch(config.discordHost + '/user/ban', {
method: 'POST',
body: { reason: reason, discordId: user.discord.id }
})
await sendRconCommand(`ban ${user.value.minecraft.uuid} ${reason}`)
return user
});

View File

@ -1,5 +0,0 @@
export default defineEventHandler(async (event) => {
const user = await getAuth(event)
return user
});

View File

@ -1,16 +0,0 @@
import { ObjectId } from "mongodb";
import { applyUsername } from "~/server/utils/auth";
export default defineEventHandler(async (event) => {
const { type } = await readBody(event)
const auth = await getAuth(event)
const usersColl = db.collection('users')
const username = type === 'discord' ? auth.discord.username : auth.minecraft.username
const newUser = await usersColl.findOneAndUpdate({ _id: new ObjectId(auth._id) }, { $set: { username: username, useMinecraftUsername: type === 'discord' ? false : true } }, { returnDocument: 'after' })
await applyUsername(newUser.value)
return newUser.value
});

Some files were not shown because too many files have changed in this diff Show More