diff --git a/build.gradle.kts b/build.gradle.kts index 3c73dbc..9197e8a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ import java.io.* plugins { java - id("com.github.johnrengelman.shadow") version "7.1.2" + id("com.gradleup.shadow") version "9.0.0-beta4" `maven-publish` id("xyz.jpenilla.run-velocity") version "2.0.0" id("io.papermc.hangar-publish-plugin") version "0.0.4" @@ -40,7 +40,7 @@ fun runCommand(command: String): String { val release = System.getenv("GRADLE_RELEASE").equals("true", ignoreCase = true) val gitHash = runCommand("git rev-parse --short HEAD") group = "com.oskarsmc" -version = "1.4.0" +version = "1.5.0" if (!release) { version = "$version-$gitHash-SNAPSHOT" @@ -61,6 +61,7 @@ tasks { relocate("org.bstats", "com.oskarsmc.message.relocated.bstats") relocate("cloud.commandframework", "com.oskarsmc.message.relocated.cloud") relocate("io.leangen.geantyref", "com.oskarsmc.message.relocated.geantyref") + archiveFileName.set("${project.name}-${project.version}.jar") } build { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e1bef7e..2f2958b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/com/oskarsmc/message/Message.java b/src/main/java/com/oskarsmc/message/Message.java index a0cb216..5c73bab 100644 --- a/src/main/java/com/oskarsmc/message/Message.java +++ b/src/main/java/com/oskarsmc/message/Message.java @@ -7,6 +7,7 @@ import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.TypeLiteral; +import com.oskarsmc.message.command.IgnoreCommand; import com.oskarsmc.message.command.MessageCommand; import com.oskarsmc.message.command.ReplyCommand; import com.oskarsmc.message.command.SocialSpyCommand; @@ -93,6 +94,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { injector.getInstance(MessageCommand.class); injector.getInstance(SocialSpyCommand.class); injector.getInstance(ReplyCommand.class); + injector.getInstance(IgnoreCommand.class); // Metrics injector.getInstance(MessageMetrics.class); diff --git a/src/main/java/com/oskarsmc/message/command/IgnoreCommand.java b/src/main/java/com/oskarsmc/message/command/IgnoreCommand.java new file mode 100644 index 0000000..16c5c41 --- /dev/null +++ b/src/main/java/com/oskarsmc/message/command/IgnoreCommand.java @@ -0,0 +1,119 @@ +package com.oskarsmc.message.command; + +import cloud.commandframework.Command; +import cloud.commandframework.minecraft.extras.RichDescription; +import cloud.commandframework.velocity.VelocityCommandManager; +import cloud.commandframework.velocity.arguments.PlayerArgument; +import com.google.inject.Inject; +import com.oskarsmc.message.configuration.MessageSettings; +import com.oskarsmc.message.logic.IgnoreManager; +import com.oskarsmc.message.util.DefaultPermission; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; +import java.util.UUID; + +/** + * /ignore command with subcommands: on, off, list, and toggle + * Player being ignored doesnt knows + */ +public final class IgnoreCommand { + private final IgnoreManager ignoreManager; + private final ProxyServer proxyServer; + + @Inject + public IgnoreCommand(@NotNull VelocityCommandManager commandManager, + @NotNull MessageSettings messageSettings, + @NotNull IgnoreManager ignoreManager, + @NotNull ProxyServer proxyServer) { + this.ignoreManager = ignoreManager; + this.proxyServer = proxyServer; + + Command.Builder builder = commandManager.commandBuilder("ignore", + messageSettings.ignoreAliases().toArray(new String[0])) + .senderType(Player.class) + .permission(new DefaultPermission("osmc.message.ignore")); + + // /ignore toggle + commandManager.command(builder + .argument(PlayerArgument.of("player"), RichDescription.translatable("oskarsmc.message.command.ignore.argument.player")) + .handler(context -> handleToggle((Player) context.getSender(), context.get("player"))) + ); + + // /ignore on + commandManager.command(builder + .literal("on") + .argument(PlayerArgument.of("player"), RichDescription.translatable("oskarsmc.message.command.ignore.argument.player")) + .handler(context -> handleOn((Player) context.getSender(), context.get("player"))) + ); + + // /ignore off + commandManager.command(builder + .literal("off") + .argument(PlayerArgument.of("player"), RichDescription.translatable("oskarsmc.message.command.ignore.argument.player")) + .handler(context -> handleOff((Player) context.getSender(), context.get("player"))) + ); + + // /ignore list + commandManager.command(builder + .literal("list") + .handler(context -> handleList((Player) context.getSender())) + ); + } + + private void handleToggle(Player ignorer, Player target) { + if (ignorer.getUniqueId().equals(target.getUniqueId())) { + ignorer.sendMessage(Component.translatable("oskarsmc.message.command.ignore.self-error", NamedTextColor.RED)); + return; + } + boolean nowIgnoring = ignoreManager.toggleIgnore(ignorer, target); + sendFeedback(ignorer, target, nowIgnoring); + } + + private void handleOn(Player ignorer, Player target) { + if (ignorer.getUniqueId().equals(target.getUniqueId())) { + ignorer.sendMessage(Component.translatable("oskarsmc.message.command.ignore.self-error", NamedTextColor.RED)); + return; + } + ignoreManager.ignore(ignorer, target); + sendFeedback(ignorer, target, true); + } + + private void handleOff(Player ignorer, Player target) { + if (ignorer.getUniqueId().equals(target.getUniqueId())) { + ignorer.sendMessage(Component.translatable("oskarsmc.message.command.ignore.self-error", NamedTextColor.RED)); + return; + } + ignoreManager.unignore(ignorer, target); + sendFeedback(ignorer, target, false); + } + + private void handleList(Player ignorer) { + Set ignored = ignoreManager.getIgnored(ignorer); + if (ignored.isEmpty()) { + ignorer.sendMessage(Component.translatable("oskarsmc.message.command.ignore.list.empty")); + return; + } + + ignorer.sendMessage(Component.translatable("oskarsmc.message.command.ignore.list.header")); + for (UUID uuid : ignored) { + String name = proxyServer.getPlayer(uuid) + .map(Player::getUsername) + .orElse(uuid.toString()); + ignorer.sendMessage(Component.translatable("oskarsmc.message.command.ignore.list.entry", Component.text(name))); + } + } + + private void sendFeedback(Player ignorer, Player target, boolean nowIgnoring) { + if (nowIgnoring) { + ignorer.sendMessage(Component.translatable("oskarsmc.message.command.ignore.on", Component.text(target.getUsername()))); + } else { + ignorer.sendMessage(Component.translatable("oskarsmc.message.command.ignore.off", Component.text(target.getUsername()))); + } + } +} diff --git a/src/main/java/com/oskarsmc/message/configuration/MessageSettings.java b/src/main/java/com/oskarsmc/message/configuration/MessageSettings.java index bfa5e8e..661c9e3 100644 --- a/src/main/java/com/oskarsmc/message/configuration/MessageSettings.java +++ b/src/main/java/com/oskarsmc/message/configuration/MessageSettings.java @@ -35,6 +35,7 @@ public final class MessageSettings { private List messageAlias; private List replyAlias; private List socialSpyAlias; + private List ignoreAlias; private boolean luckpermsIntegration; private boolean miniPlaceholdersIntegration; @@ -84,6 +85,7 @@ public MessageSettings(@DataDirectory @NotNull Path dataFolder, Logger logger) { this.messageAlias = toml.getList("aliases.message"); this.replyAlias = toml.getList("aliases.reply"); this.socialSpyAlias = toml.getList("aliases.socialspy"); + this.ignoreAlias = toml.getList("aliases.ignore"); // Exceptions this.customErrorHandlers = new HashMap<>(); @@ -277,6 +279,16 @@ public List replyAliases() { return replyAlias; } + /** + * Get the aliases of the ignore command. + * + * @return The aliases of the ignore command. + */ + @Pure + public List ignoreAliases() { + return ignoreAlias; + } + /** * Get all defined custom error handlers * diff --git a/src/main/java/com/oskarsmc/message/logic/IgnoreManager.java b/src/main/java/com/oskarsmc/message/logic/IgnoreManager.java new file mode 100644 index 0000000..e9e1484 --- /dev/null +++ b/src/main/java/com/oskarsmc/message/logic/IgnoreManager.java @@ -0,0 +1,102 @@ +package com.oskarsmc.message.logic; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages persistent player ignore lists (A ignores B = A never sees messages from B). + * Player B has no idea they are ignored. + */ +public final class IgnoreManager { + private static final Type MAP_TYPE = new TypeToken>>() {}.getType(); + + private final Path ignoresFile; + private final Gson gson = new Gson(); + private final Map> ignoredPlayers = new ConcurrentHashMap<>(); // ignorer UUID → set of ignored UUIDs + + public IgnoreManager(Path dataFolder) { + this.ignoresFile = dataFolder.resolve("ignores.json"); + loadIgnores(); + } + + private void loadIgnores() { + if (!Files.exists(ignoresFile)) return; + try { + Map> loaded = gson.fromJson(Files.readString(ignoresFile), MAP_TYPE); + if (loaded != null) ignoredPlayers.putAll(loaded); + } catch (IOException e) { + // silent fail - file will be recreated on first save + } + } + + private void saveIgnores() { + try { + Files.writeString(ignoresFile, gson.toJson(ignoredPlayers)); + } catch (IOException ignored) { + } + } + + /** + * Toggle ignore status for the given player. + * @return true if the target is now ignored + */ + public boolean toggleIgnore(@NotNull Player ignorer, @NotNull Player target) { + UUID ignorerId = ignorer.getUniqueId(); + UUID targetId = target.getUniqueId(); + + Set set = ignoredPlayers.computeIfAbsent(ignorerId, k -> ConcurrentHashMap.newKeySet()); + + if (set.remove(targetId)) { + saveIgnores(); + return false; // no longer ignoring + } else { + set.add(targetId); + saveIgnores(); + return true; // now ignoring + } + } + + public void ignore(@NotNull Player ignorer, @NotNull Player target) { + UUID ignorerId = ignorer.getUniqueId(); + UUID targetId = target.getUniqueId(); + ignoredPlayers.computeIfAbsent(ignorerId, k -> ConcurrentHashMap.newKeySet()).add(targetId); + saveIgnores(); + } + + public void unignore(@NotNull Player ignorer, @NotNull Player target) { + UUID ignorerId = ignorer.getUniqueId(); + UUID targetId = target.getUniqueId(); + Set set = ignoredPlayers.get(ignorerId); + if (set != null) { + set.remove(targetId); + saveIgnores(); + } + } + + /** + * Check if the recipient has ignored the sender (silent check). + */ + public boolean isIgnored(@NotNull Player recipient, @NotNull CommandSource sender) { + if (!(sender instanceof Player senderPlayer)) return false; + Set ignored = ignoredPlayers.get(recipient.getUniqueId()); + return ignored != null && ignored.contains(senderPlayer.getUniqueId()); + } + + /** + * Get all players the given ignorer is ignoring (for /ignore list). + */ + public @NotNull Set getIgnored(@NotNull Player ignorer) { + Set set = ignoredPlayers.get(ignorer.getUniqueId()); + return set != null ? Collections.unmodifiableSet(set) : Collections.emptySet(); + } +} diff --git a/src/main/java/com/oskarsmc/message/logic/MessageHandler.java b/src/main/java/com/oskarsmc/message/logic/MessageHandler.java index fb1ba59..5074cb0 100644 --- a/src/main/java/com/oskarsmc/message/logic/MessageHandler.java +++ b/src/main/java/com/oskarsmc/message/logic/MessageHandler.java @@ -28,6 +28,7 @@ public final class MessageHandler { private final MessageSettings messageSettings; private final MiniMessage miniMessage = MiniMessage.miniMessage(); private final ConcurrentHashMap conversations = new ConcurrentHashMap<>(); + private final IgnoreManager ignoreManager; /** * Conversation Watchers */ @@ -38,8 +39,9 @@ public final class MessageHandler { * * @param messageSettings Message Settings */ - public MessageHandler(MessageSettings messageSettings) { + public MessageHandler(MessageSettings messageSettings, IgnoreManager ignoreManager) { this.messageSettings = messageSettings; + this.ignoreManager = ignoreManager; } /** @@ -102,11 +104,19 @@ public void handleMessageEvent(@NotNull MessageEvent event) { Component receiverMessage = miniMessage.deserialize(messageSettings.messageReceivedMiniMessage(), placeholders); event.sender().sendMessage(senderMessage); - event.recipient().sendMessage(receiverMessage); - if (event.sender() instanceof Player player) { - conversations.remove(event.recipient()); - conversations.put(event.recipient(), player); + boolean isIgnored = false; + if (event.sender() instanceof Player senderPlayer) { + isIgnored = ignoreManager.isIgnored(event.recipient(), senderPlayer); + } + + if (!isIgnored) { + event.recipient().sendMessage(receiverMessage); + + if (event.sender() instanceof Player player) { + conversations.remove(event.recipient()); + conversations.put(event.recipient(), player); + } } Component socialSpyComponent = miniMessage.deserialize(messageSettings.messageSocialSpyMiniMessage(), placeholders); diff --git a/src/main/java/com/oskarsmc/message/util/MessageModule.java b/src/main/java/com/oskarsmc/message/util/MessageModule.java index abbe251..ebe2085 100644 --- a/src/main/java/com/oskarsmc/message/util/MessageModule.java +++ b/src/main/java/com/oskarsmc/message/util/MessageModule.java @@ -3,15 +3,20 @@ import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.Singleton; +import com.oskarsmc.message.command.IgnoreCommand; import com.oskarsmc.message.command.MessageCommand; import com.oskarsmc.message.command.ReplyCommand; import com.oskarsmc.message.command.SocialSpyCommand; import com.oskarsmc.message.configuration.MessageSettings; +import com.oskarsmc.message.logic.IgnoreManager; import com.oskarsmc.message.logic.MessageHandler; import com.oskarsmc.message.logic.MessageMetrics; +import com.velocitypowered.api.plugin.annotation.DataDirectory; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; + /** * The message guice module. */ @@ -34,12 +39,20 @@ protected void configure() { bind(ReplyCommand.class).in(Singleton.class); bind(SocialSpyCommand.class).in(Singleton.class); bind(MessageMetrics.class).in(Singleton.class); + bind(IgnoreCommand.class).in(Singleton.class); } @Contract(" -> new") @Singleton @Provides - private @NotNull MessageHandler provideHandler() { - return new MessageHandler(messageSettings); + private @NotNull MessageHandler provideHandler(IgnoreManager ignoreManager) { + return new MessageHandler(messageSettings, ignoreManager); + } + + @Singleton + @Provides + private @NotNull IgnoreManager provideIgnoreManager(@DataDirectory Path dataDirectory) { + return new IgnoreManager(dataDirectory); } + } diff --git a/src/main/resources/config.toml b/src/main/resources/config.toml index f872f40..2902e3a 100644 --- a/src/main/resources/config.toml +++ b/src/main/resources/config.toml @@ -28,6 +28,7 @@ message-socialspy = "[SocialSpy] [ message = ["msg", "tell"] reply = ["r"] socialspy = ["ss"] +ignore = ["ignore", "ig"] # Please don't touch this [developer-info] diff --git a/src/main/resources/translations.json b/src/main/resources/translations.json index 197e9f6..1e10cef 100644 --- a/src/main/resources/translations.json +++ b/src/main/resources/translations.json @@ -8,17 +8,33 @@ "oskarsmc.message.command.common.argument.message-description": "The message to send to the player.", "oskarsmc.message.command.socialspy.on": "SocialSpy enabled.", "oskarsmc.message.command.socialspy.off": "SocialSpy disabled.", - "oskarsmc.message.command.common.self-sending-error": "You cannot send messages to yourself." + "oskarsmc.message.command.common.self-sending-error": "You cannot send messages to yourself.", + + "oskarsmc.message.command.ignore.argument.player": "The player to toggle ignore for.", + "oskarsmc.message.command.ignore.on": "You are now ignoring {0}.", + "oskarsmc.message.command.ignore.off": "You are no longer ignoring {0}.", + "oskarsmc.message.command.ignore.self-error": "You cannot ignore yourself.", + "oskarsmc.message.command.ignore.list.header": "Players you are currently ignoring:", + "oskarsmc.message.command.ignore.list.empty": "You are not ignoring anyone.", + "oskarsmc.message.command.ignore.list.entry": "• {0}" } }, { "language-tag": "es-ES", "translations": { "oskarsmc.message.command.message.argument.player-argument": "El jugador al cual enviar el mensaje.", - "oskarsmc.message.command.common.argument.message-description": "El mensaje que se enviara al jugador.", + "oskarsmc.message.command.common.argument.message-description": "El mensaje que se enviará al jugador.", "oskarsmc.message.command.socialspy.on": "SocialSpy habilitado.", "oskarsmc.message.command.socialspy.off": "SocialSpy deshabilitado.", - "oskarsmc.message.command.common.self-sending-error": "No puedes enviarte mensajes a ti mismo." + "oskarsmc.message.command.common.self-sending-error": "No puedes enviarte mensajes a ti mismo.", + + "oskarsmc.message.command.ignore.argument.player": "El jugador para (des)ignorar.", + "oskarsmc.message.command.ignore.on": "Ahora estás ignorando a {0}.", + "oskarsmc.message.command.ignore.off": "Ya no estás ignorando a {0}.", + "oskarsmc.message.command.ignore.self-error": "No puedes ignorarte a ti mismo.", + "oskarsmc.message.command.ignore.list.header": "Jugadores que estás ignorando actualmente:", + "oskarsmc.message.command.ignore.list.empty": "No estás ignorando a nadie.", + "oskarsmc.message.command.ignore.list.entry": "• {0}" } } ]