first commit
This commit is contained in:
@@ -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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
@@ -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: <t:${Math.floor(new Date(request.created_at).getTime() / 1000)}:R>`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -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
|
||||||
@@ -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`);
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
+14
@@ -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 };
|
||||||
+13
@@ -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=<true|false>
|
||||||
|
PING_DOMAIN=<ping_url>
|
||||||
Reference in New Issue
Block a user