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

View File

@@ -164,4 +164,13 @@ public class ConsentConfig {
"<gray>Policy: <aqua>{policy_url}</aqua>" "<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.UUID;
import java.util.concurrent.ConcurrentHashMap; 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.command.PluginCommand;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import de.simolzimol.mclogger.consent.commands.ConsentCommand; import de.simolzimol.mclogger.consent.commands.ConsentCommand;
@@ -59,6 +64,9 @@ public class ConsentPlugin extends JavaPlugin {
return; return;
} }
// Register plugin-messaging channel for Velocity / BungeeCord network kick
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
// Register event listeners // Register event listeners
ConsentListener listener = new ConsentListener(this); ConsentListener listener = new ConsentListener(this);
getServer().getPluginManager().registerEvents(listener, this); getServer().getPluginManager().registerEvents(listener, this);
@@ -107,4 +115,31 @@ public class ConsentPlugin extends JavaPlugin {
public Set<UUID> getPendingConsent() { public Set<UUID> getPendingConsent() {
return pendingConsent; 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()); plugin.getPendingConsent().remove(player.getUniqueId());
String msg = MessageUtil.replace(cfg.getMsgDeclined(), player, cfg); 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) { private void handleStatus(CommandSender sender, ConsentConfig cfg) {

View File

@@ -1,20 +1,29 @@
package de.simolzimol.mclogger.consent.listeners; package de.simolzimol.mclogger.consent.listeners;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; 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.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent; 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;
import de.simolzimol.mclogger.consent.ConsentConfig.EnforcementMode; import de.simolzimol.mclogger.consent.ConsentConfig.EnforcementMode;
@@ -93,9 +102,12 @@ public class ConsentListener implements Listener {
EnforcementMode mode = cfg.getEnforcementMode(); EnforcementMode mode = cfg.getEnforcementMode();
// Send the join prompt // Open the policy book 1 tick after join (openBook can't be called during join)
String prompt = MessageUtil.replace(cfg.getMsgJoinPrompt(), player, cfg); String promptMini = MessageUtil.replace(cfg.getMsgJoinPrompt(), player, cfg);
player.sendMessage(MiniMessage.miniMessage().deserialize(prompt)); Bukkit.getScheduler().runTaskLater(plugin, () -> {
if (!player.isOnline()) return;
openPolicyBook(player, promptMini);
}, 1L);
if (mode == EnforcementMode.HOLD) { if (mode == EnforcementMode.HOLD) {
plugin.getPendingConsent().add(player.getUniqueId()); plugin.getPendingConsent().add(player.getUniqueId());
@@ -177,6 +189,28 @@ public class ConsentListener implements Listener {
event.getPlayer().sendMessage(MiniMessage.miniMessage().deserialize(msg)); 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 // Internal helpers
// ───────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────
@@ -193,6 +227,40 @@ public class ConsentListener implements Listener {
return cfg.getExemptWorlds().contains(worldName); 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 * Sends a MiniMessage-formatted message to the player with a cooldown to
* avoid flooding the chat (e.g. when the player spams movement). * avoid flooding the chat (e.g. when the player spams movement).
@@ -241,7 +309,7 @@ public class ConsentListener implements Listener {
plugin.getPendingConsent().remove(uuid); plugin.getPendingConsent().remove(uuid);
String kickMsg = MessageUtil.replace( String kickMsg = MessageUtil.replace(
plugin.getConsentConfig().getMsgGraceKick(), p, plugin.getConsentConfig()); plugin.getConsentConfig().getMsgGraceKick(), p, plugin.getConsentConfig());
p.kick(MiniMessage.miniMessage().deserialize(kickMsg)); doKick(p, MiniMessage.miniMessage().deserialize(kickMsg));
}, (long) graceSec * 20); }, (long) graceSec * 20);
} }

View File

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

View File

@@ -1,7 +1,7 @@
# ============================================================ # ============================================================
# MCConsent Paper Plugin Configuration # MCConsent Paper Plugin Configuration
# Author: SimolZimol # Author: SimolZimol
# Docs: https://github.com/SimolZimol/MCLogger # Website: https://log.devanturas.net
# ============================================================ # ============================================================
# ── Global switch ──────────────────────────────────────────── # ── Global switch ────────────────────────────────────────────
@@ -55,6 +55,14 @@ consent:
# in plugin.yml with aliases /privacy and /pp). # in plugin.yml with aliases /privacy and /pp).
command: "consent" 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) ─────────────────────────── # ── Grace period (HOLD mode only) ───────────────────────────
# How many seconds a player has to accept before being kicked. # 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). # Set to 0 to wait forever (player can take as long as they like).

View File

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