diff --git a/api/src/main/java/com/discordsrv/api/module/type/PunishmentModule.java b/api/src/main/java/com/discordsrv/api/module/type/PunishmentModule.java index 353e39d4..86d64499 100644 --- a/api/src/main/java/com/discordsrv/api/module/type/PunishmentModule.java +++ b/api/src/main/java/com/discordsrv/api/module/type/PunishmentModule.java @@ -23,7 +23,9 @@ package com.discordsrv.api.module.type; +import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.api.module.Module; +import com.discordsrv.api.player.DiscordSRVPlayer; import com.discordsrv.api.punishment.Punishment; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -36,13 +38,14 @@ public interface PunishmentModule extends Module { interface Bans extends PunishmentModule { CompletableFuture<@Nullable Punishment> getBan(@NotNull UUID playerUUID); - CompletableFuture addBan(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable String reason, @NotNull String punisher); + CompletableFuture addBan(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable MinecraftComponent reason, @NotNull MinecraftComponent punisher); CompletableFuture removeBan(@NotNull UUID playerUUID); + CompletableFuture kickPlayer(@NotNull DiscordSRVPlayer player, @NotNull MinecraftComponent message); } interface Mutes extends PunishmentModule { CompletableFuture<@Nullable Punishment> getMute(@NotNull UUID playerUUID); - CompletableFuture addMute(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable String reason, @NotNull String punisher); + CompletableFuture addMute(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable MinecraftComponent reason, @NotNull MinecraftComponent punisher); CompletableFuture removeMute(@NotNull UUID playerUUID); } } diff --git a/api/src/main/java/com/discordsrv/api/punishment/Punishment.java b/api/src/main/java/com/discordsrv/api/punishment/Punishment.java index 8ce872d5..968ca982 100644 --- a/api/src/main/java/com/discordsrv/api/punishment/Punishment.java +++ b/api/src/main/java/com/discordsrv/api/punishment/Punishment.java @@ -23,31 +23,39 @@ package com.discordsrv.api.punishment; -import org.jetbrains.annotations.Nullable; +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.placeholder.annotation.Placeholder; +import com.discordsrv.api.placeholder.annotation.PlaceholderPrefix; import java.time.Instant; +@PlaceholderPrefix("punishment_") public class Punishment { - private final Instant until; - private final String reason; - private final String punisher; + public static final Punishment UNKNOWN = new Punishment(null, null, null); - public Punishment(@Nullable Instant until, @Nullable String reason, @Nullable String punisher) { + private final Instant until; + private final MinecraftComponent reason; + private final MinecraftComponent punisher; + + public Punishment(Instant until, MinecraftComponent reason, MinecraftComponent punisher) { this.until = until; this.reason = reason; this.punisher = punisher; } + @Placeholder(value = "until", relookup = "date") public Instant until() { return until; } - public String reason() { + @Placeholder("reason") + public MinecraftComponent reason() { return reason; } - public String punisher() { + @Placeholder("punisher") + public MinecraftComponent punisher() { return punisher; } } diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/ban/BukkitBanModule.java b/bukkit/src/main/java/com/discordsrv/bukkit/ban/BukkitBanModule.java index 81981b9e..99d4029a 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/ban/BukkitBanModule.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/ban/BukkitBanModule.java @@ -18,16 +18,21 @@ package com.discordsrv.bukkit.ban; +import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.api.module.type.PunishmentModule; +import com.discordsrv.api.player.DiscordSRVPlayer; import com.discordsrv.api.punishment.Punishment; import com.discordsrv.bukkit.BukkitDiscordSRV; import com.discordsrv.common.bansync.BanSyncModule; +import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.module.type.AbstractModule; +import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer; import org.bukkit.BanEntry; import org.bukkit.BanList; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerKickEvent; import org.jetbrains.annotations.NotNull; @@ -44,6 +49,16 @@ public class BukkitBanModule extends AbstractModule implements super(discordSRV); } + @Override + public void enable() { + discordSRV.server().getPluginManager().registerEvents(this, discordSRV.plugin()); + } + + @Override + public void disable() { + HandlerList.unregisterAll(this); + } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPlayerKick(PlayerKickEvent event) { Player player = event.getPlayer(); @@ -59,7 +74,7 @@ public class BukkitBanModule extends AbstractModule implements } @Override - public CompletableFuture getBan(@NotNull UUID playerUUID) { + public CompletableFuture getBan(@NotNull UUID playerUUID) { CompletableFuture entryFuture; if (PaperBanList.IS_AVAILABLE) { entryFuture = CompletableFuture.completedFuture(PaperBanList.getBanEntry(discordSRV.server(), playerUUID)); @@ -74,20 +89,26 @@ public class BukkitBanModule extends AbstractModule implements return null; } Date expiration = ban.getExpiration(); - return new Punishment(expiration != null ? expiration.toInstant() : null, ban.getReason(), ban.getSource()); + return new Punishment( + expiration != null ? expiration.toInstant() : null, + ComponentUtil.toAPI(BukkitComponentSerializer.legacy().deserialize(ban.getReason())), + ComponentUtil.toAPI(BukkitComponentSerializer.legacy().deserialize(ban.getSource())) + ); }); } @Override - public CompletableFuture addBan(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable String reason, @NotNull String punisher) { + public CompletableFuture addBan(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable MinecraftComponent reason, @NotNull MinecraftComponent punisher) { + String reasonLegacy = reason != null ? BukkitComponentSerializer.legacy().serialize(ComponentUtil.fromAPI(reason)) : null; + String punisherLegacy = BukkitComponentSerializer.legacy().serialize(ComponentUtil.fromAPI(punisher)); if (PaperBanList.IS_AVAILABLE) { - PaperBanList.addBan(discordSRV.server(), playerUUID, until, reason, punisher); + PaperBanList.addBan(discordSRV.server(), playerUUID, until, reasonLegacy, punisherLegacy); return CompletableFuture.completedFuture(null); } BanList banList = discordSRV.server().getBanList(BanList.Type.NAME); return discordSRV.playerProvider().lookupOfflinePlayer(playerUUID).thenApply(offlinePlayer -> { - banList.addBan(offlinePlayer.username(), reason, until != null ? Date.from(until) : null, punisher); + banList.addBan(offlinePlayer.username(), reasonLegacy, until != null ? Date.from(until) : null, punisherLegacy); return null; }); } @@ -105,4 +126,17 @@ public class BukkitBanModule extends AbstractModule implements return null; }); } + + @Override + public CompletableFuture kickPlayer(@NotNull DiscordSRVPlayer srvPlayer, @NotNull MinecraftComponent message) { + Player player = discordSRV.server().getPlayer(srvPlayer.uniqueId()); + if (player == null) { + return CompletableFuture.completedFuture(null); + } + + return discordSRV.scheduler().executeOnMainThread( + player, + () -> player.kickPlayer(BukkitComponentSerializer.legacy().serialize(ComponentUtil.fromAPI(message))) + ); + } } diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/integration/EssentialsXIntegration.java b/bukkit/src/main/java/com/discordsrv/bukkit/integration/EssentialsXIntegration.java index a86ec10d..0d28ea7a 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/integration/EssentialsXIntegration.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/integration/EssentialsXIntegration.java @@ -107,15 +107,20 @@ public class EssentialsXIntegration @Override public CompletableFuture getMute(@NotNull UUID playerUUID) { - return getUser(playerUUID).thenApply(user -> new Punishment(Instant.ofEpochMilli(user.getMuteTimeout()), user.getMuteReason(), null)); + return getUser(playerUUID).thenApply(user -> new Punishment( + Instant.ofEpochMilli(user.getMuteTimeout()), + ComponentUtil.toAPI(BukkitComponentSerializer.legacy().deserialize(user.getMuteReason())), + null + )); } @Override - public CompletableFuture addMute(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable String reason, @NotNull String punisher) { + public CompletableFuture addMute(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable MinecraftComponent reason, @NotNull MinecraftComponent punisher) { + String reasonLegacy = reason != null ? BukkitComponentSerializer.legacy().serialize(ComponentUtil.fromAPI(reason)) : null; return getUser(playerUUID).thenApply(user -> { user.setMuted(true); user.setMuteTimeout(until != null ? until.toEpochMilli() : 0); - user.setMuteReason(reason); + user.setMuteReason(reasonLegacy); return null; }); } diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java index 1c6f2ad6..0bf7c015 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java @@ -27,6 +27,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; @@ -54,9 +55,20 @@ public class BukkitPlayerProvider extends ServerPlayerProvider { +public class BanSyncModule extends AbstractSyncModule { private final Map events = new ConcurrentHashMap<>(); @@ -65,22 +68,13 @@ public class BanSyncModule extends AbstractSyncModule requiredIntents() { return Collections.singleton(DiscordGatewayIntent.GUILD_MODERATION); } public void notifyBanned(IPlayer player, @Nullable Punishment punishment) { - gameChanged(BanSyncCause.PLAYER_BANNED, Someone.of(player.uniqueId()), null, punishment); + gameChanged(BanSyncCause.PLAYER_BANNED, Someone.of(player.uniqueId()), Game.INSTANCE, punishment); } @Override @@ -95,12 +89,12 @@ public class BanSyncModule extends AbstractSyncModule new PunishmentEvent(userId, newState)); + private PunishmentEvent upsertEvent(long guildId, long userId, boolean newState) { + return events.computeIfAbsent(userId, key -> new PunishmentEvent(guildId, userId, newState)); } private class PunishmentEvent { + private final long guildId; private final long userId; private final boolean newState; private final Future future; - public PunishmentEvent(long userId, boolean newState) { + public PunishmentEvent(long guildId, long userId, boolean newState) { + this.guildId = guildId; this.userId = userId; this.newState = newState; @@ -139,25 +135,26 @@ public class BanSyncModule extends AbstractSyncModule configs = configsForDiscord.get(guildId); + BanSyncConfig config = configs.isEmpty() ? null : configs.get(0); + if (config == null) { + return; + } + + long punisherId = entry.getUserIdLong(); + + // This user should be cacheable as they just made an auditable action + User punisher = event.getJDA().getUserById(punisherId); + Member punisherMember = punisher != null ? guild.getMember(punisher) : null; + + MinecraftComponent punisherName = discordSRV.componentFactory().textBuilder(config.gamePunisherFormat) + .addContext(punisher, punisherMember) + .applyPlaceholderService() + .build(); - Punishment punishment = new Punishment(null, entry.getReason(), punishmentName); long bannedUserId = entry.getTargetIdLong(); // Apply punishments instantly when audit log events arrive. if (actionType == ActionType.BAN) { - upsertEvent(bannedUserId, true).applyPunishment(punishment); + upsertEvent(guildId, bannedUserId, true).applyPunishment(new Punishment( + null, + ComponentUtil.fromPlain(entry.getReason()), + punisherName + )); } else { - upsertEvent(bannedUserId, false).applyPunishment(punishment); + upsertEvent(guildId, bannedUserId, false).applyPunishment(null); } } @@ -217,7 +232,7 @@ public class BanSyncModule extends AbstractSyncModule GenericSyncResults.ADD_DISCORD); } else { @@ -274,15 +288,37 @@ public class BanSyncModule extends AbstractSyncModule { + IPlayer player = discordSRV.playerProvider().player(playerUUID); + if (player == null) { + return CompletableFuture.completedFuture(null); + } + + MinecraftComponent kickMessage = discordSRV.componentFactory() + .textBuilder(config.gameKickReason) + .addContext(newState) + .applyPlaceholderService() + .build(); + + return bans.kickPlayer(player, kickMessage); + }) .thenApply(v -> GenericSyncResults.ADD_GAME); } else { return bans.removeBan(playerUUID).thenApply(v -> GenericSyncResults.REMOVE_GAME); } } + public enum Game { + INSTANCE + } + } diff --git a/common/src/main/java/com/discordsrv/common/component/util/ComponentUtil.java b/common/src/main/java/com/discordsrv/common/component/util/ComponentUtil.java index 6feedb68..543dfdc5 100644 --- a/common/src/main/java/com/discordsrv/common/component/util/ComponentUtil.java +++ b/common/src/main/java/com/discordsrv/common/component/util/ComponentUtil.java @@ -54,6 +54,14 @@ public final class ComponentUtil { return component.asPlainString().isEmpty(); } + @Contract("null -> null") + public static MinecraftComponent fromPlain(@Nullable String plainText) { + if (plainText == null) { + return null; + } + return toAPI(Component.text(plainText)); + } + @Contract("null -> null") public static MinecraftComponent toAPI(Component component) { if (component == null) { diff --git a/common/src/main/java/com/discordsrv/common/config/main/BanSyncConfig.java b/common/src/main/java/com/discordsrv/common/config/main/BanSyncConfig.java index 05106f14..27a42d25 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/BanSyncConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/BanSyncConfig.java @@ -18,21 +18,25 @@ package com.discordsrv.common.config.main; +import com.discordsrv.common.bansync.BanSyncModule; import com.discordsrv.common.config.main.generic.AbstractSyncConfig; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; @ConfigSerializable -public class BanSyncConfig extends AbstractSyncConfig { +public class BanSyncConfig extends AbstractSyncConfig { @Comment("The id for the Discord server where the bans should be synced from/to") public long serverId = 0L; @Comment("The reason applied when creating new bans in Minecraft") - public String gameBanReasonFormat = "%reason%"; + public String gameBanReasonFormat = "%punishment_reason%"; @Comment("The punisher applied when creating new bans in Minecraft") - public String gamePunisherFormat = "@%user_effective_server_name%"; + public String gamePunisherFormat = "%user_color%@%user_name%"; + + @Comment("The kick reason when a ban is applied to a online player") + public String gameKickReason = "&cYou have been banned for &f%punishment_reason% &cby &f%punishment_punisher%"; @Comment("The reason applied when creating new bans in Discord") public String discordBanReasonFormat = "Banned by %punishment_punisher% in Minecraft for %punishment_reason%, ends: %punishment_until:'YYYY-MM-dd HH:mm:ss zzz'|text:'Never'%"; @@ -52,8 +56,8 @@ public class BanSyncConfig extends AbstractSyncConfig } @Override - public Void gameId() { - return null; + public BanSyncModule.Game gameId() { + return BanSyncModule.Game.INSTANCE; } @Override diff --git a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java index 8ae46c33..c73bebd4 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java +++ b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java @@ -93,22 +93,6 @@ public class GroupSyncModule extends AbstractSyncModule applyDiscord(GroupSyncConfig.PairConfig config, long userId, Boolean newState) { + boolean stateToApply = newState != null && newState; + DiscordRole role = discordSRV.discordAPI().getRoleById(config.roleId); if (role == null) { return CompletableFutureUtil.failed(new SyncFail(GroupSyncResult.ROLE_DOESNT_EXIST)); @@ -258,11 +244,11 @@ public class GroupSyncModule extends AbstractSyncModule expected = expectedDiscordChanges.get(userId, key -> new ConcurrentHashMap<>()); if (expected != null) { - expected.put(config.roleId, newState); + expected.put(config.roleId, stateToApply); } return role.getGuild().retrieveMemberById(userId) - .thenCompose(member -> newState + .thenCompose(member -> stateToApply ? member.addRole(role).thenApply(v -> (ISyncResult) GenericSyncResults.ADD_DISCORD) : member.removeRole(role).thenApply(v -> GenericSyncResults.REMOVE_DISCORD) ).whenComplete((r, t) -> { @@ -275,13 +261,15 @@ public class GroupSyncModule extends AbstractSyncModule applyGame(GroupSyncConfig.PairConfig config, UUID playerUUID, Boolean newState) { + boolean stateToApply = newState != null && newState; + Map expected = expectedMinecraftChanges.get(playerUUID, key -> new ConcurrentHashMap<>()); if (expected != null) { - expected.put(config.groupName, newState); + expected.put(config.groupName, stateToApply); } CompletableFuture future = - newState + stateToApply ? addGroup(playerUUID, config).thenApply(v -> GenericSyncResults.ADD_GAME) : removeGroup(playerUUID, config).thenApply(v -> GenericSyncResults.REMOVE_GAME); return future.exceptionally(t -> { diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java index 1d628930..123a4baa 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java @@ -209,7 +209,7 @@ public class DiscordChatMessageModule extends AbstractModule { Component messageComponent = DiscordSRVMinecraftRenderer.getWithContext(guild, chatConfig, () -> discordSRV.componentFactory().minecraftSerializer().serialize(finalMessage)); - if (discordSRV.componentFactory().plainSerializer().serialize(messageComponent).trim().isEmpty() && !attachments) { + if (ComponentUtil.isEmpty(messageComponent) && !attachments) { // Check empty-ness again after rendering return; } diff --git a/common/src/main/java/com/discordsrv/common/sync/AbstractSyncModule.java b/common/src/main/java/com/discordsrv/common/sync/AbstractSyncModule.java index 2ce8cd3e..b194e836 100644 --- a/common/src/main/java/com/discordsrv/common/sync/AbstractSyncModule.java +++ b/common/src/main/java/com/discordsrv/common/sync/AbstractSyncModule.java @@ -32,9 +32,10 @@ import com.discordsrv.common.someone.Someone; import com.discordsrv.common.sync.cause.GenericSyncCauses; import com.discordsrv.common.sync.cause.ISyncCause; import com.discordsrv.common.sync.enums.SyncDirection; -import com.discordsrv.common.sync.result.GenericSyncResults; import com.discordsrv.common.sync.enums.SyncSide; +import com.discordsrv.common.sync.result.GenericSyncResults; import com.discordsrv.common.sync.result.ISyncResult; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.time.Duration; @@ -63,8 +64,8 @@ public abstract class AbstractSyncModule< > extends AbstractModule
{ protected final Map> syncs = new LinkedHashMap<>(); - private final Map> configsForGame = new ConcurrentHashMap<>(); - private final Map> configsForDiscord = new ConcurrentHashMap<>(); + protected final Map> configsForGame = new ConcurrentHashMap<>(); + protected final Map> configsForDiscord = new ConcurrentHashMap<>(); public AbstractSyncModule(DT discordSRV, String loggerName) { super(discordSRV, new NamedLogger(discordSRV, loggerName)); @@ -82,6 +83,22 @@ public abstract class AbstractSyncModule< */ protected abstract List configs(); + @Override + public boolean isEnabled() { + boolean any = false; + for (C config : configs()) { + if (config.isSet()) { + any = true; + break; + } + } + if (!any) { + return false; + } + + return super.isEnabled(); + } + @Override public void reload(Consumer resultConsumer) { synchronized (syncs) { @@ -182,9 +199,9 @@ public abstract class AbstractSyncModule< * @param newState the newState to apply * @return a future with the result of the synchronization */ - protected abstract CompletableFuture applyDiscord(C config, long userId, S newState); + protected abstract CompletableFuture applyDiscord(C config, long userId, @Nullable S newState); - protected CompletableFuture applyDiscordIfDoesNotMatch(C config, long userId, S newState) { + protected CompletableFuture applyDiscordIfDoesNotMatch(C config, long userId, @Nullable S newState) { return getDiscord(config, userId).thenCompose(currentState -> { ISyncResult result = doesStateMatch(newState, currentState); if (result != null) { @@ -203,9 +220,9 @@ public abstract class AbstractSyncModule< * @param newState the newState to apply * @return a future with the result of the synchronization */ - protected abstract CompletableFuture applyGame(C config, UUID playerUUID, S newState); + protected abstract CompletableFuture applyGame(C config, UUID playerUUID, @Nullable S newState); - protected CompletableFuture applyGameIfDoesNotMatch(C config, UUID playerUUID, S newState) { + protected CompletableFuture applyGameIfDoesNotMatch(C config, UUID playerUUID, @Nullable S newState) { return getGame(config, playerUUID).thenCompose(currentState -> { ISyncResult result = doesStateMatch(currentState, newState); if (result != null) { @@ -216,7 +233,7 @@ public abstract class AbstractSyncModule< }); } - protected CompletableFuture> discordChanged(ISyncCause cause, Someone someone, D discordId, S newState) { + protected CompletableFuture> discordChanged(ISyncCause cause, Someone someone, D discordId, @Nullable S newState) { List gameConfigs = configsForDiscord.get(discordId); if (gameConfigs == null) { return CompletableFuture.completedFuture(null); @@ -264,7 +281,7 @@ public abstract class AbstractSyncModule< }); } - protected CompletableFuture> gameChanged(ISyncCause cause, Someone someone, G gameId, S newState) { + protected CompletableFuture> gameChanged(ISyncCause cause, Someone someone, @NotNull G gameId, @Nullable S newState) { List discordConfigs = configsForGame.get(gameId); if (discordConfigs == null) { return CompletableFuture.completedFuture(null);