modified: consent-plugin/pom.xml

modified:   consent-plugin/src/main/java/de/simolzimol/mclogger/consent/ConsentConfig.java
	modified:   consent-plugin/src/main/java/de/simolzimol/mclogger/consent/ConsentPlugin.java
	modified:   consent-plugin/src/main/java/de/simolzimol/mclogger/consent/commands/ConsentCommand.java
	modified:   consent-plugin/src/main/java/de/simolzimol/mclogger/consent/listeners/ConsentListener.java
	modified:   consent-plugin/src/main/java/de/simolzimol/mclogger/consent/util/MessageUtil.java
	modified:   consent-plugin/src/main/resources/config.yml
	modified:   consent-plugin/src/main/resources/plugin.yml
This commit is contained in:
SimolZimol
2026-04-17 14:18:51 +02:00
parent aa8dfdcdbf
commit c2b645ed58
8 changed files with 134 additions and 10 deletions

View File

@@ -27,11 +27,11 @@
</repositories>
<dependencies>
<!-- Paper API 1.21 -->
<!-- Paper API 1.17+ (lowest supported; ensures no 1.18+-only APIs are used) -->
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<version>1.17.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>

View File

@@ -164,4 +164,13 @@ public class ConsentConfig {
"<gray>Policy: <aqua>{policy_url}</aqua>"
);
}
/**
* When {@code true}, kick messages are forwarded to the Velocity / BungeeCord
* proxy via the {@code BungeeCord} plugin-messaging channel so the player is
* removed from the entire network rather than just the current server.
*/
public boolean isVelocityKick() {
return cfg.getBoolean("consent.velocity-kick", false);
}
}

View File

@@ -4,7 +4,12 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.command.PluginCommand;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import de.simolzimol.mclogger.consent.commands.ConsentCommand;
@@ -59,6 +64,9 @@ public class ConsentPlugin extends JavaPlugin {
return;
}
// Register plugin-messaging channel for Velocity / BungeeCord network kick
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
// Register event listeners
ConsentListener listener = new ConsentListener(this);
getServer().getPluginManager().registerEvents(listener, this);
@@ -107,4 +115,31 @@ public class ConsentPlugin extends JavaPlugin {
public Set<UUID> getPendingConsent() {
return pendingConsent;
}
/**
* Kicks a player from the current server, or from the entire
* Velocity / BungeeCord network when {@code velocity-kick: true} is set
* in the config. Falls back to a normal kick if the proxy message fails.
*/
public void networkKick(Player player, Component reason) {
if (consentConfig.isVelocityKick()) {
try {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("KickPlayer");
out.writeUTF(player.getName());
out.writeUTF(LegacyComponentSerializer.legacySection().serialize(reason));
player.sendPluginMessage(this, "BungeeCord", out.toByteArray());
} catch (Exception e) {
getLogger().warning("[MCConsent] Velocity/BungeeCord network kick failed: " + e.getMessage());
player.kick(reason);
return;
}
// Fallback: if the proxy never kicks, remove the player after 1 s
getServer().getScheduler().runTaskLater(this, () -> {
if (player.isOnline()) player.kick(reason);
}, 20L);
} else {
player.kick(reason);
}
}
}

View File

@@ -113,7 +113,7 @@ public class ConsentCommand implements CommandExecutor, TabCompleter {
plugin.getPendingConsent().remove(player.getUniqueId());
String msg = MessageUtil.replace(cfg.getMsgDeclined(), player, cfg);
player.kick(MM.deserialize(msg));
plugin.networkKick(player, MM.deserialize(msg));
}
private void handleStatus(CommandSender sender, ConsentConfig cfg) {

View File

@@ -1,20 +1,29 @@
package de.simolzimol.mclogger.consent.listeners;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import de.simolzimol.mclogger.consent.ConsentConfig;
import de.simolzimol.mclogger.consent.ConsentConfig.EnforcementMode;
@@ -93,9 +102,12 @@ public class ConsentListener implements Listener {
EnforcementMode mode = cfg.getEnforcementMode();
// Send the join prompt
String prompt = MessageUtil.replace(cfg.getMsgJoinPrompt(), player, cfg);
player.sendMessage(MiniMessage.miniMessage().deserialize(prompt));
// Open the policy book 1 tick after join (openBook can't be called during join)
String promptMini = MessageUtil.replace(cfg.getMsgJoinPrompt(), player, cfg);
Bukkit.getScheduler().runTaskLater(plugin, () -> {
if (!player.isOnline()) return;
openPolicyBook(player, promptMini);
}, 1L);
if (mode == EnforcementMode.HOLD) {
plugin.getPendingConsent().add(player.getUniqueId());
@@ -177,6 +189,28 @@ public class ConsentListener implements Listener {
event.getPlayer().sendMessage(MiniMessage.miniMessage().deserialize(msg));
}
// ─────────────────────────────────────────────────────────
// HOLD-mode: block block-break / place / interact
// ─────────────────────────────────────────────────────────
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockBreak(BlockBreakEvent event) {
if (!isHoldPending(event.getPlayer())) return;
event.setCancelled(true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockPlace(BlockPlaceEvent event) {
if (!isHoldPending(event.getPlayer())) return;
event.setCancelled(true);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onInteract(PlayerInteractEvent event) {
if (!isHoldPending(event.getPlayer())) return;
event.setCancelled(true);
}
// ─────────────────────────────────────────────────────────
// Internal helpers
// ─────────────────────────────────────────────────────────
@@ -193,6 +227,40 @@ public class ConsentListener implements Listener {
return cfg.getExemptWorlds().contains(worldName);
}
/**
* Opens a written-book GUI with the policy prompt so the player can read
* the full text including a clickable URL, without cluttering chat.
* A short plain-chat hint is also sent so players know why the book opened.
*/
private void openPolicyBook(Player player, String promptMini) {
MiniMessage mm = MiniMessage.miniMessage();
ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
BookMeta meta = (BookMeta) Objects.requireNonNull(book.getItemMeta());
meta.title(mm.deserialize("<bold><blue>Privacy Policy</blue></bold>"));
meta.author(Component.text("MCLogger"));
// First page: the configured join-prompt text (supports MiniMessage incl. click URLs)
Component page1 = mm.deserialize(promptMini);
meta.pages(List.of(page1));
book.setItemMeta(meta);
player.openBook(book);
// Also send a brief chat message so the book title isn't the only indicator
String hint = MessageUtil.replace(
plugin.getConsentConfig().getMsgJoinPrompt(), player, plugin.getConsentConfig());
player.sendMessage(mm.deserialize(hint));
}
/**
* Kicks a player from the server or if velocity-kick is enabled from
* the entire network. Delegates to {@link ConsentPlugin#networkKick}.
*/
void doKick(Player player, Component reason) {
plugin.networkKick(player, reason);
}
/**
* Sends a MiniMessage-formatted message to the player with a cooldown to
* avoid flooding the chat (e.g. when the player spams movement).
@@ -241,7 +309,7 @@ public class ConsentListener implements Listener {
plugin.getPendingConsent().remove(uuid);
String kickMsg = MessageUtil.replace(
plugin.getConsentConfig().getMsgGraceKick(), p, plugin.getConsentConfig());
p.kick(MiniMessage.miniMessage().deserialize(kickMsg));
doKick(p, MiniMessage.miniMessage().deserialize(kickMsg));
}, (long) graceSec * 20);
}

View File

@@ -16,7 +16,9 @@ public final class MessageUtil {
* <p>Supported placeholders:
* <ul>
* <li>{@code {player}} player's display name</li>
* <li>{@code {policy_url}} configured policy URL</li>
* <li>{@code {url}} configured policy URL</li>
* <li>{@code {policy_url}} configured policy URL (alias for {url})</li>
* <li>{@code {command}} consent command name (e.g. "consent")</li>
* <li>{@code {version}} policy version string</li>
* </ul>
*/
@@ -24,7 +26,9 @@ public final class MessageUtil {
if (template == null) return "";
return template
.replace("{player}", player.getName())
.replace("{url}", cfg.getPolicyUrl())
.replace("{policy_url}", cfg.getPolicyUrl())
.replace("{command}", cfg.getCommand())
.replace("{version}", cfg.getPolicyVersion());
}
}

View File

@@ -1,7 +1,7 @@
# ============================================================
# MCConsent Paper Plugin Configuration
# Author: SimolZimol
# Docs: https://github.com/SimolZimol/MCLogger
# Website: https://log.devanturas.net
# ============================================================
# ── Global switch ────────────────────────────────────────────
@@ -55,6 +55,14 @@ consent:
# in plugin.yml with aliases /privacy and /pp).
command: "consent"
# ── Velocity / BungeeCord network kick ──────────────────────
# Set to true if this server runs behind a Velocity or BungeeCord
# proxy. When enabled, kick messages are forwarded via the
# BungeeCord plugin-messaging channel so the player is removed
# from the entire network instead of just this server.
# Requires the proxy to have the plugin-messaging channel enabled.
velocity-kick: false
# ── Grace period (HOLD mode only) ───────────────────────────
# How many seconds a player has to accept before being kicked.
# Set to 0 to wait forever (player can take as long as they like).

View File

@@ -1,7 +1,7 @@
name: MCConsent
version: '1.0.0'
main: de.simolzimol.mclogger.consent.ConsentPlugin
api-version: '1.20'
api-version: '1.17'
description: Privacy Policy consent enforcement for Minecraft servers
author: SimolZimol