From 83d6fe525f9de709186402423e1e7af37056cb98 Mon Sep 17 00:00:00 2001 From: Patrick <147879351+WinniePatGG@users.noreply.github.com> Date: Fri, 1 May 2026 19:34:31 +0200 Subject: [PATCH] first commit --- LICENSE | 21 ++ README.md | 102 +++++++ bot.js | 711 ++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 72 +++++ minecraft_bridge.js | 81 +++++ package.json | 26 ++ pingTask.js | 14 + sample.env | 13 + 8 files changed, 1040 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bot.js create mode 100644 docker-compose.yml create mode 100644 minecraft_bridge.js create mode 100644 package.json create mode 100644 pingTask.js create mode 100644 sample.env diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..958ab94 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 WinniePatGG + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a68ab45 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# Discord Whitelist Bot + +### A **Discord.js** bot that handles **Minecraft whitelist requests** with admin review, RCON integration, and optional uptime pings. +### Make sure to have NPM installed. If not install on https://nodejs.org/en/download for your os +### If you need help: Join my discord Server and send me a dm https://discord.gg/nRgXUFSFfe +### Also feel free to create issues in the issues tab https://github.com/WinniePatGG/MinecraftWhitelistBot/issues + +------------------------------------------------------------------------ + +## Features + +- Whitelist requests from discord +- Admin review system +- RCON integration +- Optional uptime ping system +- Docker support +- Fully customizable via `.env` + +------------------------------------------------------------------------ + +## Environment Variables + + +| Variable | Description | +|---------------------------|-----------------------------------------------| +| `DISCORD_TOKEN` | Discord Bot Token | +| `GUILD_ID` | Discord Server ID | +| `PUBLIC_CHANNEL_ID` | Channel where users submit whitelist requests | +| `ADMIN_REVIEW_CHANNEL_ID` | Channel where admins review requests | +| `TEAM_ROLE_ID` | Role that gets pinged for new requests | +| `RCON_HOST` | IP address of the Minecraft server | +| `RCON_PORT` | RCON port (from server.properties) | +| `RCON_PASSWORD` | Password for RCON | +| `PING_ENABLED` | Enable/disable API pings | +| `PING_DOMAIN` | URL to ping (e.g. Uptime Kuma) | + +------------------------------------------------------------------------ + +## File Overview + +### `bot.js` + +Handles: +- Slash commands +- Request storage +- Embeds and UI +- Workflow logic + +### `minecraft_bridge.js` + +Manages communication with the Minecraft server via **RCON**. + +### `pingTask.js` + +Runs scheduled pings if enabled. + +------------------------------------------------------------------------ + +## Getting Started + +### Local Setup + + git clone https://github.com/WinniePatGG/MinecraftWhitelistBot.git + cd MinecraftWhitelistBot + npm install + npm run start:all + +Then run `/whitelist` in your configured Discord channel. + +------------------------------------------------------------------------ + +## Docker Deployment + + mkdir MinecraftWhitelistBot + mkdir MinecraftWhitelistBot/app + +Copy: +- `docker-compose.yml` → root folder +- `.env` → root folder +- All app files → `/app` + +Start: + + docker compose up -d + +------------------------------------------------------------------------ + +## Common Issues + +### - Error: SQLITE_ERROR: no such table: whitelist_requests + +Restart everything: + + npm run start:all + +### - Error [TokenInvalid]: An invalid token was provided. + +Set a valid Discord Bot Token in you .env file in the field `DISCORD_TOKEN` and restart + + npm run start:all + +------------------------------------------------------------------------ \ No newline at end of file diff --git a/bot.js b/bot.js new file mode 100644 index 0000000..4e824b7 --- /dev/null +++ b/bot.js @@ -0,0 +1,711 @@ +const { + Client, + GatewayIntentBits, + ActionRowBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + Events, + EmbedBuilder, + ButtonBuilder, + ButtonStyle, + PermissionsBitField +} = require('discord.js'); + +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); +const { startPingLoop } = require('./pingTask'); +require('dotenv').config({ quiet: true, path: path.join(__dirname, '.env') }); + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent + ] +}); + +const dbPath = path.join(__dirname, 'whitelist.db'); +const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error('Error opening database:', err); + } else { + console.log('Connected to database'); + initializeDatabase(); + } +}); + +function initializeDatabase() { + db.run(` + CREATE TABLE IF NOT EXISTS whitelist_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + discord_id TEXT NOT NULL, + discord_username TEXT NOT NULL, + minecraft_username TEXT NOT NULL, + status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'approved', 'rejected')), + minecraft_added BOOLEAN DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(discord_id, minecraft_username) + ) + `, (err) => { + if (err) { + console.error('Error creating table:', err); + } else { + console.log('Database initialization was successful'); + } + }); +} + +function dbRun(sql, params = []) { + return new Promise((resolve, reject) => { + db.run(sql, params, function(err) { + if (err) reject(err); + else resolve(this); + }); + }); +} + +function dbGet(sql, params = []) { + return new Promise((resolve, reject) => { + db.get(sql, params, (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); +} + +function dbAll(sql, params = []) { + return new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); +} + +client.on(Events.InteractionCreate, async interaction => { + if (interaction.isChatInputCommand()) { + if (interaction.commandName === 'whitelist') { + await handleWhitelistCommand(interaction); + } + } + + if (interaction.isButton()) { + if (interaction.customId === 'request_whitelist') { + await showWhitelistModal(interaction); + } else if (interaction.customId.startsWith('approve_')) { + await handleApproveButton(interaction); + } else if (interaction.customId.startsWith('deny_')) { + await handleDenyButton(interaction); + } + } + + if (interaction.isModalSubmit()) { + if (interaction.customId === 'whitelist_modal') { + await handleWhitelistSubmission(interaction); + } + } +}); + +client.on(Events.InteractionCreate, async interaction => { + if (!interaction.isChatInputCommand()) return; + + if (interaction.commandName === 'whitelist_approve') { + await handleApproveCommand(interaction); + } + + if (interaction.commandName === 'whitelist_deny') { + await handleDenyCommand(interaction); + } + + if (interaction.commandName === 'whitelist_list') { + await handleListCommand(interaction); + } + + if (interaction.commandName === 'whitelist_remove') { + await handleRemoveCommand(interaction); + } + + if (interaction.commandName === 'whitelist_stats') { + await handleStatsCommand(interaction); + } +}); + +async function handleWhitelistCommand(interaction) { + if (interaction.channelId !== process.env.PUBLIC_CHANNEL_ID) { + return await interaction.reply({ + content: '❌ Please use this command in the designated whitelist channel.', + flags: 64 + }); + } + + const embed = new EmbedBuilder() + .setTitle('🎮 Survival Server Whitelist') + .setDescription('Click the button below to request whitelist access to our Minecraft server!') + .setColor(0x00FF00) + .addFields( + { name: 'How it works', + value: '1. Click the "Request Whitelist" button\n' + + '2. Enter your Minecraft username\n' + + '3. Wait for admin approval' }, + { name: 'Rules', + value: '• Use your exact Minecraft username\n' + + '• One request per user\n' + + '• No offensive names allowed' } + ) + .setFooter({ text: 'Minecraft Server Whitelist System' }); + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('request_whitelist') + .setLabel('Request Whitelist') + .setStyle(ButtonStyle.Primary) + .setEmoji('🎮') + ); + + await interaction.reply({ embeds: [embed], components: [row] }); +} + +async function showWhitelistModal(interaction) { + if (interaction.channelId !== process.env.PUBLIC_CHANNEL_ID) { + return await interaction.reply({ + content: '❌ Please use this command in the designated whitelist channel.', + flags: 64 + }); + } + + const modal = new ModalBuilder() + .setCustomId('whitelist_modal') + .setTitle('Minecraft Whitelist Request'); + + const minecraftInput = new TextInputBuilder() + .setCustomId('minecraft_username') + .setLabel('What is your Minecraft username?') + .setStyle(TextInputStyle.Short) + .setMinLength(3) + .setMaxLength(16) + .setPlaceholder('Enter your exact Minecraft username') + .setRequired(true); + + const actionRow = new ActionRowBuilder().addComponents(minecraftInput); + modal.addComponents(actionRow); + + await interaction.showModal(modal); +} + +async function handleWhitelistSubmission(interaction) { + const minecraftUsername = interaction.fields.getTextInputValue('minecraft_username'); + const discordUser = interaction.user; + + if (!/^[a-zA-Z0-9_]{3,16}$/.test(minecraftUsername)) { + return await interaction.reply({ + content: '❌ Invalid Minecraft username! Usernames must be 3-16 characters long and contain only letters, numbers, and underscores.', + flags: 64 + }); + } + + try { + const existing = await dbGet( + 'SELECT * FROM whitelist_requests WHERE discord_id = ? AND (status = "pending" OR status = "approved")', + [discordUser.id] + ); + + if (existing) { + return await interaction.reply({ + content: '❌ You already have a pending or approved whitelist request!', + flags: 64 + }); + } + + const existingMinecraft = await dbGet( + 'SELECT * FROM whitelist_requests WHERE minecraft_username = ? AND status = "approved"', + [minecraftUsername] + ); + + if (existingMinecraft) { + return await interaction.reply({ + content: '❌ This Minecraft username is already whitelisted!', + flags: 64 + }); + } + + await dbRun( + 'INSERT INTO whitelist_requests (discord_id, discord_username, minecraft_username, status) VALUES (?, ?, ?, "pending")', + [discordUser.id, discordUser.tag, minecraftUsername] + ); + + const adminChannel = await client.channels.fetch(process.env.ADMIN_REVIEW_CHANNEL_ID); + if (adminChannel) { + const adminEmbed = new EmbedBuilder() + .setTitle('🆕 New Whitelist Request') + .setColor(0xFFFF00) + .addFields( + { name: 'Discord User', + value: `${discordUser.tag} (\`${discordUser.id}\`)`, + inline: true }, + { name: 'Minecraft Username', + value: `\`${minecraftUsername}\``, + inline: true }, + { name: 'Status', + value: '⏳ Pending', + inline: true } + ) + .setThumbnail(discordUser.displayAvatarURL()) + .setTimestamp(); + + const adminRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(`approve_${discordUser.id}_${minecraftUsername}`) + .setLabel('Approve') + .setStyle(3) + .setEmoji('✅'), + new ButtonBuilder() + .setCustomId(`deny_${discordUser.id}_${minecraftUsername}`) + .setLabel('Deny') + .setStyle(4) + .setEmoji('❌') + ); + + await adminChannel.send({content: `<@&${process.env.TEAM_ROLE_ID}>`, + embeds: [adminEmbed], + components: [adminRow] }); + } + + await interaction.reply({ + content: `✅ Whitelist request submitted for **${minecraftUsername}**! An admin will review your request shortly.`, + flags: 64 + }); + + } catch (error) { + console.error('Error submitting whitelist request:', error); + await interaction.reply({ + content: '❌ An error occurred while submitting your request. Please try again later.', + flags: 64 + }); + } +} + +async function handleApproveButton(interaction) { + if (interaction.channelId !== process.env.ADMIN_REVIEW_CHANNEL_ID) { + return await interaction.reply({ + content: '❌ This button can only be used in the admin review channel.', + flags: 64 + }); + } + + if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { + return await interaction.reply({ + content: '❌ You need administrator permissions to use this button.', + flags: 64 + }); + } + + const [_, discordId, minecraftUsername] = interaction.customId.split('_'); + + try { + await dbRun( + 'UPDATE whitelist_requests SET status = "approved" WHERE discord_id = ? AND minecraft_username = ?', + [discordId, minecraftUsername] + ); + + const originalEmbed = interaction.message.embeds[0]; + const updatedEmbed = EmbedBuilder.from(originalEmbed) + .setColor(0x00FF00) + .spliceFields(2, 1, { name: 'Status', + value: '✅ Approved', + inline: true }) + .setFooter({ text: `Approved by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL() }); + + await interaction.message.edit({ + embeds: [updatedEmbed], + components: [] + }); + + await interaction.reply({ + content: `✅ Approved whitelist request for **${minecraftUsername}**`, + flags: 64 + }); + + try { + const user = await client.users.fetch(discordId); + const notifyEmbed = new EmbedBuilder() + .setTitle('🎉 Whitelist Request Approved!') + .setDescription(`Your whitelist request for **${minecraftUsername}** has been approved! by ${interaction.user.tag}`) + .setColor(0x00FF00) + .addFields( + { name: 'Minecraft Username', + value: minecraftUsername }, + { name: 'Status', + value: '✅ Approved' }, + { name: 'Next Steps', + value: 'You can now join the Minecraft server!' } + ) + .setTimestamp(); + + await user.send({ embeds: [notifyEmbed] }); + } catch (dmError) { + console.log('Could not send DM to user'); + } + + } catch (error) { + console.error('Error approving whitelist:', error); + await interaction.reply({ + content: '❌ An error occurred while approving the whitelist request.', + flags: 64 + }); + } +} + +async function handleDenyButton(interaction) { + if (interaction.channelId !== process.env.ADMIN_REVIEW_CHANNEL_ID) { + return await interaction.reply({ + content: '❌ This button can only be used in the admin review channel.', + flags: 64 + }); + } + + if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { + return await interaction.reply({ + content: '❌ You need administrator permissions to use this button.', + flags: 64 + }); + } + + const [_, discordId, minecraftUsername] = interaction.customId.split('_'); + + try { + await dbRun( + 'UPDATE whitelist_requests SET status = "rejected" WHERE discord_id = ? AND minecraft_username = ?', + [discordId, minecraftUsername] + ); + + const originalEmbed = interaction.message.embeds[0]; + const updatedEmbed = EmbedBuilder.from(originalEmbed) + .setColor(0xFF0000) + .spliceFields(2, 1, { name: 'Status', + value: '❌ Denied', + inline: true }) + .setFooter({ text: `Denied by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL() }); + + await interaction.message.edit({ + embeds: [updatedEmbed], + components: [] + }); + + await interaction.reply({ + content: `❌ Denied whitelist request for **${minecraftUsername}**`, + flags: 64 + }); + + try { + const user = await client.users.fetch(discordId); + const notifyEmbed = new EmbedBuilder() + .setTitle('❌ Whitelist Request Denied') + .setDescription(`Your whitelist request for **${minecraftUsername}** has been denied by ${interaction.user.tag}.`) + .setColor(0xFF0000) + .addFields( + { name: 'Minecraft Username', value: minecraftUsername }, + { name: 'Status', value: '❌ Denied' }, + { name: 'Next Steps', value: 'Please contact an admin if you believe this is a mistake.' } + ) + .setTimestamp(); + + await user.send({ embeds: [notifyEmbed] }); + } catch (dmError) { + console.log('Could not send DM to user'); + } + + } catch (error) { + console.error('Error denying whitelist:', error); + await interaction.reply({ + content: '❌ An error occurred while denying the whitelist request.', + flags: 64 + }); + } +} + +async function handleApproveCommand(interaction) { + if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { + return await interaction.reply({ + content: '❌ You need administrator permissions to use this command.', + flags: 64 + }); + } + + const username = interaction.options.getString('username'); + + try { + const result = await dbRun( + 'UPDATE whitelist_requests SET status = "approved" WHERE minecraft_username = ?', + [username] + ); + + if (result.changes === 0) { + return await interaction.reply({ + content: `❌ No pending request found for username **${username}**`, + flags: 64 + }); + } + + await interaction.reply({ + content: `✅ Successfully approved whitelist request for **${username}**` + }); + + } catch (error) { + console.error('Error approving whitelist:', error); + await interaction.reply({ + content: '❌ An error occurred while approving the whitelist request.', + flags: 64 + }); + } +} + +async function handleDenyCommand(interaction) { + if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { + return await interaction.reply({ + content: '❌ You need administrator permissions to use this command.', + flags: 64 + }); + } + + const username = interaction.options.getString('username'); + + try { + const result = await dbRun( + 'UPDATE whitelist_requests SET status = "rejected" WHERE minecraft_username = ?', + [username] + ); + + if (result.changes === 0) { + return await interaction.reply({ + content: `❌ No pending request found for username **${username}**`, + flags: 64 + }); + } + + await interaction.reply({ + content: `❌ Successfully denied whitelist request for **${username}**` + }); + + } catch (error) { + console.error('Error denying whitelist:', error); + await interaction.reply({ + content: '❌ An error occurred while denying the whitelist request.', + flags: 64 + }); + } +} + +async function handleRemoveCommand(interaction) { + if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { + return await interaction.reply({ + content: '❌ You need administrator permissions to use this command.', + flags: 64 + }); + } + + const username = interaction.options.getString('username'); + + try { + const result = await dbRun( + 'DELETE FROM whitelist_requests WHERE minecraft_username = ?', + [username] + ); + + if (result.changes === 0) { + return await interaction.reply({ + content: `❌ No whitelist entry found for username **${username}**`, + flags: 64 + }); + } + + await interaction.reply({ + content: `🗑️ Successfully removed **${username}** from the whitelist database` + }); + + } catch (error) { + console.error('Error removing from whitelist:', error); + await interaction.reply({ + content: '❌ An error occurred while removing the whitelist entry.', + flags: 64 + }); + } +} + +async function handleListCommand(interaction) { + if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { + return await interaction.reply({ + content: '❌ You need administrator permissions to use this command.', + flags: 64 + }); + } + + try { + const requests = await dbAll( + 'SELECT * FROM whitelist_requests WHERE status = "pending" ORDER BY created_at DESC' + ); + + if (requests.length === 0) { + return await interaction.reply({ + content: 'No pending whitelist requests.', + flags: 64 + }); + } + + const embed = new EmbedBuilder() + .setTitle('📋 Pending Whitelist Requests') + .setColor(0xFFFF00); + + requests.forEach(request => { + embed.addFields({ + name: `Request #${request.id} - ${request.minecraft_username}`, + value: `Discord User: <@${request.discord_id}> (\`${request.discord_username}\`)\nSubmitted: ` + }); + }); + + await interaction.reply({ embeds: [embed], flags: 64 }); + + } catch (error) { + console.error('Error listing requests:', error); + await interaction.reply({ + content: '❌ An error occurred while fetching whitelist requests.', + flags: 64 + }); + } +} + +async function handleStatsCommand(interaction) { + if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { + return await interaction.reply({ + content: '❌ You need administrator permissions to use this command.', + flags: 64 + }); + } + + try { + const [pending, approved, rejected, total] = await Promise.all([ + dbGet('SELECT COUNT(*) as count FROM whitelist_requests WHERE status = "pending"'), + dbGet('SELECT COUNT(*) as count FROM whitelist_requests WHERE status = "approved"'), + dbGet('SELECT COUNT(*) as count FROM whitelist_requests WHERE status = "rejected"'), + dbGet('SELECT COUNT(*) as count FROM whitelist_requests') + ]); + + const embed = new EmbedBuilder() + .setTitle('📊 Whitelist Statistics') + .setColor(0x0099FF) + .addFields( + { name: '📥 Pending Requests', + value: pending.count.toString(), inline: true }, + { name: '✅ Approved', + value: approved.count.toString(), + inline: true }, + { name: '❌ Rejected', + value: rejected.count.toString(), + inline: true }, + { name: '📈 Total Requests', + value: total.count.toString(), + inline: true } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + } catch (error) { + console.error('Error getting stats:', error); + await interaction.reply({ + content: '❌ An error occurred while fetching statistics.', + flags: 64 + }); + } +} + +const { REST, Routes } = require('discord.js'); + +const commands = [ + { + name: 'whitelist', + description: 'Start the whitelist process for the Minecraft server' + }, + { + name: 'whitelist_approve', + description: 'Approve a whitelist request', + options: [ + { + name: 'username', + type: 3, + description: 'Minecraft username to approve', + required: true + } + ] + }, + { + name: 'whitelist_deny', + description: 'Deny a whitelist request', + options: [ + { + name: 'username', + type: 3, + description: 'Minecraft username to deny', + required: true + } + ] + }, + { + name: 'whitelist_remove', + description: 'Remove a username from the whitelist database', + options: [ + { + name: 'username', + type: 3, + description: 'Minecraft username to remove', + required: true + } + ] + }, + { + name: 'whitelist_list', + description: 'List all pending whitelist requests' + }, + { + name: 'whitelist_stats', + description: 'Show whitelist statistics' + } +]; + +const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); + +async function registerCommands() { + try { + console.log('Refreshing slash-commands'); + + await rest.put( + Routes.applicationGuildCommands(client.user.id, process.env.GUILD_ID), + { body: commands }, + ); + + console.log('Refreshed slash-commands'); + } catch (error) { + console.error('Error registering commands:', error); + } +} + +client.once(Events.ClientReady, async () => { + console.log(''); + console.log(`✅ Bot is online as ${client.user.tag}`); + console.log(`📢 Public channel: ${process.env.PUBLIC_CHANNEL_ID}`); + console.log(`🔧 Admin channel: ${process.env.ADMIN_REVIEW_CHANNEL_ID}`); + console.log(''); + await registerCommands(); + if (process.env.PING_ENABLED === 'true') { + startPingLoop(); + console.log('[PING] enabled.') + } else { + console.log('[PING] disabled.') + } +}); + +client.login(process.env.DISCORD_TOKEN); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..250eb26 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,72 @@ +services: + minecraft-whitelist-bot: + image: node:18-alpine + container_name: minecraft-whitelist-bot + restart: unless-stopped + working_dir: /app + volumes: + - ./app:/app + - bot-data:/app/data + - bot-logs:/app/logs + env_file: + - .env + environment: + - NODE_ENV=production + command: > + sh -c " + npm install && + node bot.js + " + labels: + - "com.centurylinklabs.watchtower.enable=true" + - "traefik.enable=false" + logging: + driver: json-file + options: + max-size: 10m + max-file: 3 + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:${PORT:-3000}', (res) => { if (res.statusCode !== 200) throw new Error() })"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + minecraft-bridge: + image: node:18-alpine + container_name: minecraft-whitelist-bridge + restart: unless-stopped + working_dir: /app + volumes: + - ./app:/app + - bot-data:/app/data + - bot-logs:/app/logs + env_file: + - .env + environment: + - NODE_ENV=production + command: > + sh -c " + npm install && + node minecraft_bridge.js + " + depends_on: + - minecraft-whitelist-bot + labels: + - "com.centurylinklabs.watchtower.enable=true" + - "traefik.enable=false" + logging: + driver: json-file + options: + max-size: 10m + max-file: 3 + +volumes: + bot-data: + driver: local + bot-logs: + driver: local + +networks: + default: + name: minecraft-whitelist-network diff --git a/minecraft_bridge.js b/minecraft_bridge.js new file mode 100644 index 0000000..1181071 --- /dev/null +++ b/minecraft_bridge.js @@ -0,0 +1,81 @@ +const { Rcon } = require('rcon-client'); +const sqlite3 = require('sqlite3'); +const path = require('path'); +require('dotenv').config({ quiet: true }); + +console.log('🔗 Starting Minecraft Bridge...'); +console.log('RCON Config:', { + host: process.env.RCON_HOST || 'NOT SET', + port: process.env.RCON_PORT || '25575', + hasPassword: !!process.env.RCON_PASSWORD +}); + +const dbPath = path.join(__dirname, 'whitelist.db'); +const db = new sqlite3.Database(dbPath); + +function dbAll(sql, params = []) { + return new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); +} + +function dbRun(sql, params = []) { + return new Promise((resolve, reject) => { + db.run(sql, params, function(err) { + if (err) reject(err); + else resolve(this); + }); + }); +} + +async function executeMinecraftCommand(command) { + const rcon = new Rcon({ + host: process.env.RCON_HOST, + port: process.env.RCON_PORT, + password: process.env.RCON_PASSWORD, + }); + + try { + await rcon.connect(); + const response = await rcon.send(command); + await rcon.end(); + return response; + } catch (error) { + console.error('RCON error:', error); + throw error; + } +} + +async function processApprovedUsers() { + try { + const approvedUsers = await dbAll( + 'SELECT * FROM whitelist_requests WHERE status = "approved" AND minecraft_added = 0' + ); + + for (const user of approvedUsers) { + try { + await executeMinecraftCommand(`whitelist add ${user.minecraft_username}`); + console.log(`✅ Added ${user.minecraft_username} to whitelist`); + + await dbRun( + 'UPDATE whitelist_requests SET minecraft_added = 1 WHERE id = ?', + [user.id] + ); + } catch (error) { + console.error(`Failed to add ${user.minecraft_username} to whitelist:`, error); + } + } + } catch (error) { + console.error('Database error:', error); + } +} + +setInterval(processApprovedUsers, 1000); + +processApprovedUsers(); + +console.log(`✅ Started Minecraft Bridge successfully!`); +console.log(`Waiting for changes in whitelist.db`); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb61a3c --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "minecraft-whitelist-bot", + "version": "1.0.0", + "description": "Discord bot for Minecraft server whitelist management", + "main": "bot.js", + "scripts": { + "start": "node bot.js", + "start:bridge": "node minecraft_bridge.js", + "start:all": "concurrently \"npm run start\" \"npm run start:bridge\"", + "dev": "nodemon bot.js" + }, + "dependencies": { + "discord.js": "^14.24.2", + "dotenv": "^17.2.3", + "rcon-client": "^4.2.5", + "sqlite3": "^5.1.7", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "nodemon": "^3.1.11", + "concurrently": "^9.2.1" + }, + "keywords": ["discord", "minecraft", "whitelist", "bot"], + "author": "WinniePatGG", + "license": "MIT" +} diff --git a/pingTask.js b/pingTask.js new file mode 100644 index 0000000..e7cd39a --- /dev/null +++ b/pingTask.js @@ -0,0 +1,14 @@ +require('dotenv').config(); +let pingUrl = (process.env.PING_DOMAIN || "").trim(); + +function startPingLoop() { + setInterval(async () => { + try { + await fetch(pingUrl); + } catch (err) { + console.error(`[PING] Error: ${err}`); + } + }, 20000); +} + +module.exports = { startPingLoop }; \ No newline at end of file diff --git a/sample.env b/sample.env new file mode 100644 index 0000000..0bed07d --- /dev/null +++ b/sample.env @@ -0,0 +1,13 @@ +DISCORD_TOKEN=BotToken +GUILD_ID=DiscordServerID + +PUBLIC_CHANNEL_ID=PublicChannelID +ADMIN_REVIEW_CHANNEL_ID=AdminChannelID +TEAM_ROLE_ID=TeamRoleID + +RCON_HOST=RconHost +RCON_PORT=RconPort +RCON_PASSWORD=RconPassword + +PING_ENABLED= +PING_DOMAIN= \ No newline at end of file