mirror of
https://github.com/System-End/Discord-MC-Chat.git
synced 2026-04-19 19:45:14 +00:00
feat: implement per-server OP level resolution and sync_op_level_to_minecraft
Co-authored-by: Xujiayao <58985541+Xujiayao@users.noreply.github.com>
This commit is contained in:
parent
95095b3c32
commit
461bbe8d39
13 changed files with 430 additions and 53 deletions
|
|
@ -22,6 +22,7 @@ import com.xujiayao.discord_mc_chat.network.packets.commands.execute.ExecuteResp
|
|||
import com.xujiayao.discord_mc_chat.network.packets.commands.info.InfoRequestPacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.commands.info.InfoResponsePacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.commands.link.LinkResponsePacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.commands.link.OpSyncPacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.commands.unlink.UnlinkResponsePacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.misc.KeepAlivePacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.misc.LatencyPongPacket;
|
||||
|
|
@ -202,6 +203,10 @@ public class ClientHandler extends SimpleChannelInboundHandler<Packet> {
|
|||
// Handle unlink response from server - notify the player
|
||||
EventManager.post(new CoreEvents.UnlinkResponseEvent(p.minecraftUuid, p.success, p.discordName != null ? p.discordName : ""));
|
||||
}
|
||||
case OpSyncPacket p -> {
|
||||
// Handle OP sync from server - apply OP levels to Minecraft players
|
||||
EventManager.post(new CoreEvents.OpSyncEvent(p.opLevels));
|
||||
}
|
||||
case DisconnectPacket p -> {
|
||||
// If we receive a DisconnectPacket, it means the server explicitly rejected us.
|
||||
// In most cases (whitelist, auth fail, version mismatch), retrying immediately won't help.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.xujiayao.discord_mc_chat.network.packets.commands.console.ConsoleRequ
|
|||
import com.xujiayao.discord_mc_chat.network.packets.commands.console.ConsoleResponsePacket;
|
||||
import com.xujiayao.discord_mc_chat.server.discord.DiscordManager;
|
||||
import com.xujiayao.discord_mc_chat.server.discord.JdaCommandSender;
|
||||
import com.xujiayao.discord_mc_chat.server.discord.OpLevelResolver;
|
||||
import com.xujiayao.discord_mc_chat.utils.CryptUtils;
|
||||
import com.xujiayao.discord_mc_chat.utils.config.ConfigManager;
|
||||
import com.xujiayao.discord_mc_chat.utils.config.ModeManager;
|
||||
|
|
@ -229,8 +230,14 @@ public class ConsoleCommand implements Command {
|
|||
CompletableFuture<ConsoleResponsePacket> future = new CompletableFuture<>();
|
||||
pendingRequests.put(requestId, future);
|
||||
|
||||
// Send with the sender's OP level for Minecraft's own permission check on the client
|
||||
NetworkManager.sendPacketToClient(new ConsoleRequestPacket(requestId, sender.getOpLevel(), commandLine), serverName);
|
||||
// Resolve per-server OP level for the target client
|
||||
int opLevel = sender.getOpLevel();
|
||||
if (sender instanceof JdaCommandSender jdaSender) {
|
||||
opLevel = OpLevelResolver.resolveForServer(jdaSender.getMember(), jdaSender.getUser(), serverName);
|
||||
}
|
||||
|
||||
// Send with the sender's per-server OP level for Minecraft's own permission check on the client
|
||||
NetworkManager.sendPacketToClient(new ConsoleRequestPacket(requestId, opLevel, commandLine), serverName);
|
||||
|
||||
try {
|
||||
ConsoleResponsePacket response = future.get(CONSOLE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.xujiayao.discord_mc_chat.network.packets.commands.execute.ExecuteRequ
|
|||
import com.xujiayao.discord_mc_chat.network.packets.commands.execute.ExecuteResponsePacket;
|
||||
import com.xujiayao.discord_mc_chat.server.discord.DiscordManager;
|
||||
import com.xujiayao.discord_mc_chat.server.discord.JdaCommandSender;
|
||||
import com.xujiayao.discord_mc_chat.server.discord.OpLevelResolver;
|
||||
import com.xujiayao.discord_mc_chat.utils.CryptUtils;
|
||||
import com.xujiayao.discord_mc_chat.utils.config.ConfigManager;
|
||||
import com.xujiayao.discord_mc_chat.utils.i18n.I18nManager;
|
||||
|
|
@ -144,12 +145,18 @@ public class ExecuteCommand implements Command {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Resolve per-server OP level for the target client
|
||||
int opLevel = sender.getOpLevel();
|
||||
if (sender instanceof JdaCommandSender jdaSender) {
|
||||
opLevel = OpLevelResolver.resolveForServer(jdaSender.getMember(), jdaSender.getUser(), serverName);
|
||||
}
|
||||
|
||||
String requestId = CryptUtils.generateRandomString(16);
|
||||
CompletableFuture<ExecuteResponsePacket> future = new CompletableFuture<>();
|
||||
pendingRequests.put(requestId, future);
|
||||
|
||||
// Append sender's OP level credential to the packet for client-side edge authorization
|
||||
NetworkManager.sendPacketToClient(new ExecuteRequestPacket(requestId, sender.getOpLevel(), commandName, commandArgs), serverName);
|
||||
// Append sender's per-server OP level credential to the packet for client-side edge authorization
|
||||
NetworkManager.sendPacketToClient(new ExecuteRequestPacket(requestId, opLevel, commandName, commandArgs), serverName);
|
||||
|
||||
try {
|
||||
ExecuteResponsePacket response = future.get(EXECUTE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.xujiayao.discord_mc_chat.network.packets.commands.link;
|
||||
|
||||
import com.xujiayao.discord_mc_chat.network.packets.Packet;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Sent by Server to Client to synchronize OP levels for linked Minecraft accounts.
|
||||
* <p>
|
||||
* This packet contains a full mapping of Minecraft UUID to desired OP level.
|
||||
* The client should apply a full reset: de-op all players, then re-apply these levels.
|
||||
*
|
||||
* @author Xujiayao
|
||||
*/
|
||||
public class OpSyncPacket extends Packet {
|
||||
public Map<String, Integer> opLevels;
|
||||
|
||||
public OpSyncPacket(Map<String, Integer> opLevels) {
|
||||
this.opLevels = opLevels;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import com.xujiayao.discord_mc_chat.network.packets.misc.LatencyPingPacket;
|
|||
import com.xujiayao.discord_mc_chat.network.packets.misc.LatencyPongPacket;
|
||||
import com.xujiayao.discord_mc_chat.server.discord.DiscordManager;
|
||||
import com.xujiayao.discord_mc_chat.server.linking.LinkedAccountManager;
|
||||
import com.xujiayao.discord_mc_chat.server.linking.OpSyncManager;
|
||||
import com.xujiayao.discord_mc_chat.server.linking.VerificationCodeManager;
|
||||
import com.xujiayao.discord_mc_chat.utils.CryptUtils;
|
||||
import com.xujiayao.discord_mc_chat.utils.config.ConfigManager;
|
||||
|
|
@ -82,8 +83,11 @@ public class ServerHandler extends SimpleChannelInboundHandler<Packet> {
|
|||
case MinecraftEventPacket p -> {
|
||||
switch (p.type) {
|
||||
// Server events
|
||||
case SERVER_STARTED ->
|
||||
DiscordManager.clientBroadcast(clientName, "server.started", "server.start", false, p.placeholders);
|
||||
case SERVER_STARTED -> {
|
||||
DiscordManager.clientBroadcast(clientName, "server.started", "server.start", false, p.placeholders);
|
||||
// Trigger OP sync for this newly started client
|
||||
OpSyncManager.syncAll();
|
||||
}
|
||||
case SERVER_STOPPING ->
|
||||
DiscordManager.clientBroadcast(clientName, "server.stopped", "server.stop", false, p.placeholders);
|
||||
// Player events
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
package com.xujiayao.discord_mc_chat.server.discord;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.xujiayao.discord_mc_chat.commands.CommandManager;
|
||||
import com.xujiayao.discord_mc_chat.commands.impl.StatsCommand;
|
||||
import com.xujiayao.discord_mc_chat.network.NetworkManager;
|
||||
import com.xujiayao.discord_mc_chat.server.linking.LinkedAccountManager;
|
||||
import com.xujiayao.discord_mc_chat.utils.LogFileUtils;
|
||||
import com.xujiayao.discord_mc_chat.utils.config.ConfigManager;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.Role;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
|
|
@ -35,55 +32,25 @@ public class DiscordEventHandler extends ListenerAdapter {
|
|||
|
||||
/**
|
||||
* Resolves the OP Level credential for a Discord user based on config mappings.
|
||||
* <p>
|
||||
* Resolution order (highest wins):
|
||||
* 1. user_mappings: exact user ID or username match.
|
||||
* 2. role_mappings: iterate user's roles, take the highest mapped OP level.
|
||||
* 3. Account linking: linked MC account gets at least OP 0.
|
||||
* <p>
|
||||
* For standalone mode, each mapping entry has a top-level {@code op_level} for the DMCC Server itself.
|
||||
* For single_server mode, mappings use a flat {@code op_level}.
|
||||
*
|
||||
* @param member The Discord Member object (null if in DMs).
|
||||
* @param user The Discord User object.
|
||||
* @return The resolved OP level (-1 to 4).
|
||||
*/
|
||||
private int getOpLevel(Member member, User user) {
|
||||
int maxOp = -1;
|
||||
return OpLevelResolver.resolve(member, user);
|
||||
}
|
||||
|
||||
// Check exact user mappings first (highest priority)
|
||||
JsonNode userMappings = ConfigManager.getConfigNode("account_linking.op_sync.user_mappings");
|
||||
if (userMappings.isArray()) {
|
||||
for (JsonNode node : userMappings) {
|
||||
if (user.getId().equals(node.path("user").asText()) || user.getName().equals(node.path("user").asText())) {
|
||||
// Read the top-level op_level (used by standalone and single_server)
|
||||
maxOp = Math.max(maxOp, node.path("op_level").asInt(-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check role mappings if member exists (in a guild)
|
||||
if (member != null) {
|
||||
JsonNode roleMappings = ConfigManager.getConfigNode("account_linking.op_sync.role_mappings");
|
||||
if (roleMappings.isArray()) {
|
||||
for (Role role : member.getRoles()) {
|
||||
for (JsonNode node : roleMappings) {
|
||||
if (role.getId().equals(node.path("role").asText()) || role.getName().equals(node.path("role").asText())) {
|
||||
// Read the top-level op_level (used by standalone and single_server)
|
||||
maxOp = Math.max(maxOp, node.path("op_level").asInt(-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Account Linking: if user has linked MC accounts, they get at least OP 0
|
||||
List<String> linkedUuids = LinkedAccountManager.getMinecraftUuidsByDiscordId(user.getId());
|
||||
if (!linkedUuids.isEmpty() && maxOp < 0) {
|
||||
maxOp = 0;
|
||||
}
|
||||
|
||||
return maxOp;
|
||||
/**
|
||||
* Resolves the OP Level credential for a specific target server.
|
||||
*
|
||||
* @param member The Discord Member object (null if in DMs).
|
||||
* @param user The Discord User object.
|
||||
* @param serverName The target DMCC client server name.
|
||||
* @return The resolved OP level (-1 to 4).
|
||||
*/
|
||||
private int getOpLevelForServer(Member member, User user, String serverName) {
|
||||
return OpLevelResolver.resolveForServer(member, user, serverName);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -211,13 +178,21 @@ public class DiscordEventHandler extends ListenerAdapter {
|
|||
* Gets auto-complete choices for the 'command' parameter of the execute command.
|
||||
* Sends a real-time auto-complete request to connected clients with the current input and OP level,
|
||||
* so clients can provide DMCC command suggestions that the user is authorized to execute.
|
||||
* <p>
|
||||
* When a target server is selected, uses the per-server OP level for accurate suggestions.
|
||||
*
|
||||
* @param currentValue The current user input for filtering
|
||||
* @param event The auto-complete event to read other options
|
||||
* @return List of choices
|
||||
*/
|
||||
private List<Command.Choice> getExecuteCommandChoices(String currentValue, CommandAutoCompleteInteractionEvent event) {
|
||||
int opLevel = getOpLevel(event.getMember(), event.getUser());
|
||||
String target = event.getOption("at", OptionMapping::getAsString);
|
||||
int opLevel;
|
||||
if (target != null && !target.isBlank() && !"all_online_clients".equalsIgnoreCase(target)) {
|
||||
opLevel = getOpLevelForServer(event.getMember(), event.getUser(), target);
|
||||
} else {
|
||||
opLevel = getOpLevel(event.getMember(), event.getUser());
|
||||
}
|
||||
|
||||
if (currentValue.startsWith("/")) {
|
||||
currentValue = currentValue.substring(1);
|
||||
|
|
@ -237,13 +212,21 @@ public class DiscordEventHandler extends ListenerAdapter {
|
|||
* Gets auto-complete choices for the 'command' parameter of the console command.
|
||||
* Sends a real-time auto-complete request to connected clients with the current input and OP level,
|
||||
* so clients can provide Minecraft command suggestions via their Brigadier dispatcher.
|
||||
* <p>
|
||||
* When a target server is selected, uses the per-server OP level for accurate suggestions.
|
||||
*
|
||||
* @param currentValue The current user input for filtering
|
||||
* @param event The auto-complete event to read other options
|
||||
* @return List of choices
|
||||
*/
|
||||
private List<Command.Choice> getConsoleCommandChoices(String currentValue, CommandAutoCompleteInteractionEvent event) {
|
||||
int opLevel = getOpLevel(event.getMember(), event.getUser());
|
||||
String target = event.getOption("at", OptionMapping::getAsString);
|
||||
int opLevel;
|
||||
if (target != null && !target.isBlank() && !"all_online_clients".equalsIgnoreCase(target)) {
|
||||
opLevel = getOpLevelForServer(event.getMember(), event.getUser(), target);
|
||||
} else {
|
||||
opLevel = getOpLevel(event.getMember(), event.getUser());
|
||||
}
|
||||
|
||||
if (currentValue.startsWith("/")) {
|
||||
currentValue = currentValue.substring(1);
|
||||
|
|
|
|||
|
|
@ -195,6 +195,41 @@ public class DiscordManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a Discord User object by ID.
|
||||
*
|
||||
* @param discordId The Discord user ID.
|
||||
* @return The User object, or null if JDA is not available or user not found.
|
||||
*/
|
||||
public static net.dv8tion.jda.api.entities.User retrieveUser(String discordId) {
|
||||
if (jda == null) return null;
|
||||
try {
|
||||
return jda.retrieveUserById(discordId).complete();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a Discord Member object by user ID from the first mutual guild.
|
||||
*
|
||||
* @param discordId The Discord user ID.
|
||||
* @return The Member object, or null if JDA is not available or member not found.
|
||||
*/
|
||||
public static net.dv8tion.jda.api.entities.Member retrieveMember(String discordId) {
|
||||
if (jda == null) return null;
|
||||
try {
|
||||
for (var guild : jda.getGuilds()) {
|
||||
net.dv8tion.jda.api.entities.Member member = guild.retrieveMemberById(discordId).complete();
|
||||
if (member != null) {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to a Discord channel using the JDA bot.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.xujiayao.discord_mc_chat.server.discord;
|
|||
import com.xujiayao.discord_mc_chat.commands.CommandSender;
|
||||
import com.xujiayao.discord_mc_chat.commands.impl.LinkCommand;
|
||||
import com.xujiayao.discord_mc_chat.utils.i18n.I18nManager;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
import net.dv8tion.jda.api.utils.FileUpload;
|
||||
|
||||
|
|
@ -65,6 +67,24 @@ public class JdaCommandSender implements CommandSender, LinkCommand.DiscordUserC
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Discord Member object for this sender.
|
||||
*
|
||||
* @return The Member, or null if in DMs.
|
||||
*/
|
||||
public Member getMember() {
|
||||
return event.getMember();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Discord User object for this sender.
|
||||
*
|
||||
* @return The User.
|
||||
*/
|
||||
public User getUser() {
|
||||
return event.getUser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpLevel() {
|
||||
return opLevel;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
package com.xujiayao.discord_mc_chat.server.discord;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.xujiayao.discord_mc_chat.server.linking.LinkedAccountManager;
|
||||
import com.xujiayao.discord_mc_chat.utils.config.ConfigManager;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.Role;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Centralized OP level resolution for Discord users.
|
||||
* <p>
|
||||
* Resolves OP levels from config mappings with optional per-server granularity.
|
||||
* Used by both DiscordEventHandler (for command authorization) and the
|
||||
* sync_op_level_to_minecraft feature.
|
||||
*
|
||||
* @author Xujiayao
|
||||
*/
|
||||
public class OpLevelResolver {
|
||||
|
||||
/**
|
||||
* Resolves the top-level OP level for a Discord user (used by standalone/single_server).
|
||||
*
|
||||
* @param member The Discord Member object (null if in DMs).
|
||||
* @param user The Discord User object.
|
||||
* @return The resolved OP level (-1 to 4).
|
||||
*/
|
||||
public static int resolve(Member member, User user) {
|
||||
return resolveInternal(member, user, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the OP level for a Discord user targeting a specific DMCC client server.
|
||||
* <p>
|
||||
* In standalone mode, reads the per-server OP from the {@code servers} array in mappings.
|
||||
* Falls back to the top-level {@code op_level} if no server-specific entry exists.
|
||||
*
|
||||
* @param member The Discord Member object (null if in DMs).
|
||||
* @param user The Discord User object.
|
||||
* @param serverName The target DMCC client server name.
|
||||
* @return The resolved OP level (-1 to 4).
|
||||
*/
|
||||
public static int resolveForServer(Member member, User user, String serverName) {
|
||||
return resolveInternal(member, user, serverName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal OP level resolution logic.
|
||||
*
|
||||
* @param member The Discord Member object (null if in DMs).
|
||||
* @param user The Discord User object.
|
||||
* @param serverName If non-null, resolve per-server OP from the {@code servers} list.
|
||||
* @return The resolved OP level (-1 to 4).
|
||||
*/
|
||||
private static int resolveInternal(Member member, User user, String serverName) {
|
||||
int maxOp = -1;
|
||||
|
||||
// Check exact user mappings first (highest priority)
|
||||
JsonNode userMappings = ConfigManager.getConfigNode("account_linking.op_sync.user_mappings");
|
||||
if (userMappings.isArray()) {
|
||||
for (JsonNode node : userMappings) {
|
||||
if (user.getId().equals(node.path("user").asText()) || user.getName().equals(node.path("user").asText())) {
|
||||
maxOp = Math.max(maxOp, resolveOpFromNode(node, serverName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check role mappings if member exists (in a guild)
|
||||
if (member != null) {
|
||||
JsonNode roleMappings = ConfigManager.getConfigNode("account_linking.op_sync.role_mappings");
|
||||
if (roleMappings.isArray()) {
|
||||
for (Role role : member.getRoles()) {
|
||||
for (JsonNode node : roleMappings) {
|
||||
if (role.getId().equals(node.path("role").asText()) || role.getName().equals(node.path("role").asText())) {
|
||||
maxOp = Math.max(maxOp, resolveOpFromNode(node, serverName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Account Linking: if user has linked MC accounts, they get at least OP 0
|
||||
List<String> linkedUuids = LinkedAccountManager.getMinecraftUuidsByDiscordId(user.getId());
|
||||
if (!linkedUuids.isEmpty() && maxOp < 0) {
|
||||
maxOp = 0;
|
||||
}
|
||||
|
||||
return maxOp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the OP level from a mapping node, optionally for a specific server.
|
||||
*
|
||||
* @param node The mapping node (user_mappings or role_mappings entry).
|
||||
* @param serverName The target server name (null for top-level OP).
|
||||
* @return The OP level from the mapping.
|
||||
*/
|
||||
private static int resolveOpFromNode(JsonNode node, String serverName) {
|
||||
if (serverName != null) {
|
||||
JsonNode serversArray = node.path("servers");
|
||||
if (serversArray.isArray()) {
|
||||
for (JsonNode serverEntry : serversArray) {
|
||||
if (serverName.equals(serverEntry.path("server").asText())) {
|
||||
return serverEntry.path("op_level").asInt(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Top-level op_level (for standalone and single_server)
|
||||
return node.path("op_level").asInt(-1);
|
||||
}
|
||||
}
|
||||
|
|
@ -162,6 +162,7 @@ public class LinkedAccountManager {
|
|||
|
||||
LOGGER.info(I18nManager.getDmccTranslation("linking.manager.linked", discordName, discordId, minecraftName, minecraftUuid));
|
||||
save();
|
||||
OpSyncManager.syncAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +181,7 @@ public class LinkedAccountManager {
|
|||
removed.forEach(entry -> UUID_TO_DISCORD.remove(entry.minecraftUuid()));
|
||||
LOGGER.info(I18nManager.getDmccTranslation("linking.manager.unlinked_discord", count, discordName, discordId));
|
||||
save();
|
||||
OpSyncManager.syncAll();
|
||||
}
|
||||
|
||||
return count;
|
||||
|
|
@ -209,6 +211,7 @@ public class LinkedAccountManager {
|
|||
String discordName = resolveDiscordName(discordId);
|
||||
LOGGER.info(I18nManager.getDmccTranslation("linking.manager.unlinked_minecraft", minecraftName, minecraftUuid, discordName, discordId));
|
||||
save();
|
||||
OpSyncManager.syncAll();
|
||||
return discordId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
package com.xujiayao.discord_mc_chat.server.linking;
|
||||
|
||||
import com.xujiayao.discord_mc_chat.network.NetworkManager;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.commands.link.OpSyncPacket;
|
||||
import com.xujiayao.discord_mc_chat.server.discord.DiscordManager;
|
||||
import com.xujiayao.discord_mc_chat.server.discord.OpLevelResolver;
|
||||
import com.xujiayao.discord_mc_chat.utils.config.ConfigManager;
|
||||
import com.xujiayao.discord_mc_chat.utils.config.ModeManager;
|
||||
import com.xujiayao.discord_mc_chat.utils.events.CoreEvents;
|
||||
import com.xujiayao.discord_mc_chat.utils.events.EventManager;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.xujiayao.discord_mc_chat.Constants.LOGGER;
|
||||
|
||||
/**
|
||||
* Manages the synchronization of OP levels from Discord mappings to Minecraft servers.
|
||||
* <p>
|
||||
* When {@code sync_op_level_to_minecraft} is enabled, this class computes the desired OP level
|
||||
* for every linked Minecraft account based on the Discord user's mapping configuration,
|
||||
* then applies a full-reset sync to the Minecraft server(s).
|
||||
*
|
||||
* @author Xujiayao
|
||||
*/
|
||||
public class OpSyncManager {
|
||||
|
||||
/**
|
||||
* Performs a full OP level sync for all linked accounts.
|
||||
* <p>
|
||||
* In single_server mode, posts a CoreEvent to the local Minecraft server.
|
||||
* In standalone mode, sends OpSyncPackets to each connected client.
|
||||
*/
|
||||
public static void syncAll() {
|
||||
if (!Boolean.TRUE.equals(ConfigManager.getBoolean("account_linking.op_sync.sync_op_level_to_minecraft"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, List<LinkedAccountManager.LinkEntry>> allLinks = LinkedAccountManager.getAllLinks();
|
||||
|
||||
switch (ModeManager.getMode()) {
|
||||
case "single_server" -> {
|
||||
// Compute OP levels for all linked accounts using flat mappings
|
||||
Map<String, Integer> opLevels = new HashMap<>();
|
||||
for (Map.Entry<String, List<LinkedAccountManager.LinkEntry>> entry : allLinks.entrySet()) {
|
||||
String discordId = entry.getKey();
|
||||
int opLevel = resolveOpForDiscordUser(discordId, null);
|
||||
for (LinkedAccountManager.LinkEntry link : entry.getValue()) {
|
||||
opLevels.put(link.minecraftUuid(), Math.max(0, opLevel));
|
||||
}
|
||||
}
|
||||
EventManager.post(new CoreEvents.OpSyncEvent(opLevels));
|
||||
}
|
||||
case "standalone" -> {
|
||||
// For each connected client, compute per-server OP levels and send
|
||||
List<String> clients = NetworkManager.getConnectedClientNames();
|
||||
for (String clientName : clients) {
|
||||
Map<String, Integer> opLevels = new HashMap<>();
|
||||
for (Map.Entry<String, List<LinkedAccountManager.LinkEntry>> entry : allLinks.entrySet()) {
|
||||
String discordId = entry.getKey();
|
||||
int opLevel = resolveOpForDiscordUser(discordId, clientName);
|
||||
for (LinkedAccountManager.LinkEntry link : entry.getValue()) {
|
||||
opLevels.put(link.minecraftUuid(), Math.max(0, opLevel));
|
||||
}
|
||||
}
|
||||
NetworkManager.sendPacketToClient(new OpSyncPacket(opLevels), clientName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the OP level for a Discord user, optionally for a specific server.
|
||||
* <p>
|
||||
* Uses the JDA API to retrieve the user's guild member information for role-based resolution.
|
||||
*
|
||||
* @param discordId The Discord user ID.
|
||||
* @param serverName The target server name (null for top-level/single_server).
|
||||
* @return The resolved OP level (-1 to 4).
|
||||
*/
|
||||
private static int resolveOpForDiscordUser(String discordId, String serverName) {
|
||||
try {
|
||||
User user = DiscordManager.retrieveUser(discordId);
|
||||
if (user == null) {
|
||||
return -1;
|
||||
}
|
||||
Member member = DiscordManager.retrieveMember(discordId);
|
||||
if (serverName != null) {
|
||||
return OpLevelResolver.resolveForServer(member, user, serverName);
|
||||
} else {
|
||||
return OpLevelResolver.resolve(member, user);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to resolve OP level for Discord user {}: {}", discordId, e.getMessage());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.xujiayao.discord_mc_chat.utils.events;
|
|||
import com.xujiayao.discord_mc_chat.commands.CommandSender;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
|
|
@ -138,4 +139,17 @@ public class CoreEvents {
|
|||
String discordName
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Posted when OP levels should be synchronized to the Minecraft server.
|
||||
* <p>
|
||||
* This is a full-reset sync: the handler should clear all existing OP assignments
|
||||
* and reapply the provided mapping. Players not in the map should be de-opped (OP 0).
|
||||
*
|
||||
* @param opLevels A map of Minecraft UUID (as string) to the desired OP level (0-4).
|
||||
*/
|
||||
public record OpSyncEvent(
|
||||
Map<String, Integer> opLevels
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.xujiayao.discord_mc_chat.network.packets.commands.info.InfoResponsePa
|
|||
import com.xujiayao.discord_mc_chat.network.packets.events.MinecraftEventPacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.commands.link.LinkRequestPacket;
|
||||
import com.xujiayao.discord_mc_chat.server.linking.LinkedAccountManager;
|
||||
import com.xujiayao.discord_mc_chat.server.linking.OpSyncManager;
|
||||
import com.xujiayao.discord_mc_chat.server.linking.VerificationCodeManager;
|
||||
import com.xujiayao.discord_mc_chat.utils.EnvironmentUtils;
|
||||
import com.xujiayao.discord_mc_chat.utils.config.ConfigManager;
|
||||
|
|
@ -121,6 +122,11 @@ public class MinecraftEventHandler {
|
|||
|
||||
// Register info supplier after server is started
|
||||
NetworkManager.registerInfoSupplier(() -> buildInfoResponse(event.minecraftServer()));
|
||||
|
||||
// Trigger initial OP sync after server is ready (single_server mode)
|
||||
if ("single_server".equals(ModeManager.getMode())) {
|
||||
OpSyncManager.syncAll();
|
||||
}
|
||||
});
|
||||
|
||||
EventManager.register(MinecraftEvents.ServerStopping.class, event -> {
|
||||
|
|
@ -391,6 +397,63 @@ public class MinecraftEventHandler {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ===== OP Level Sync =====
|
||||
|
||||
EventManager.register(CoreEvents.OpSyncEvent.class, event -> {
|
||||
if (serverInstance == null) return;
|
||||
|
||||
serverInstance.execute(() -> {
|
||||
try {
|
||||
net.minecraft.server.players.PlayerList playerList = serverInstance.getPlayerList();
|
||||
net.minecraft.server.players.ServerOpList opList = playerList.getOps();
|
||||
|
||||
// Step 1: De-op all currently opped players
|
||||
// Copy the list to avoid ConcurrentModificationException
|
||||
List<net.minecraft.server.players.ServerOpListEntry> currentOps =
|
||||
new ArrayList<>(opList.getEntries());
|
||||
for (net.minecraft.server.players.ServerOpListEntry op : currentOps) {
|
||||
com.mojang.authlib.GameProfile profile = op.getUser();
|
||||
if (profile != null) {
|
||||
playerList.deop(profile);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Apply the new OP levels from the sync event
|
||||
for (Map.Entry<String, Integer> entry : event.opLevels().entrySet()) {
|
||||
String uuidStr = entry.getKey();
|
||||
int opLevel = entry.getValue();
|
||||
if (opLevel <= 0) continue; // OP 0 means no special permissions, skip
|
||||
|
||||
try {
|
||||
UUID uuid = UUID.fromString(uuidStr);
|
||||
com.mojang.authlib.GameProfile profile = serverInstance.getProfileCache()
|
||||
.get(uuid).orElse(null);
|
||||
if (profile == null) {
|
||||
// Create a minimal profile for offline players
|
||||
profile = new com.mojang.authlib.GameProfile(uuid, "");
|
||||
}
|
||||
// Add the OP entry with the exact desired level
|
||||
opList.add(new net.minecraft.server.players.ServerOpListEntry(
|
||||
profile, opLevel, opList.canBypassPlayerLimit(profile)));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Save the ops list
|
||||
try {
|
||||
opList.save();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Step 4: Update permission levels for online players
|
||||
for (ServerPlayer player : playerList.getPlayers()) {
|
||||
playerList.sendPlayerPermissionLevel(player);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static List<String> getSuggestionsForInput(String input, CommandSourceStack source) throws Exception {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue