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 87dc02e0..a2cb3f63 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 @@ -1,5 +1,6 @@ package com.discordsrv.api.module.type; +import com.discordsrv.api.module.Module; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -7,18 +8,16 @@ import java.time.Instant; import java.util.UUID; import java.util.concurrent.CompletableFuture; -public interface PunishmentModule { +public interface PunishmentModule extends Module { interface Bans extends PunishmentModule { - @Nullable - CompletableFuture getBan(@NotNull UUID playerUUID); + CompletableFuture<@Nullable Punishment> getBan(@NotNull UUID playerUUID); CompletableFuture addBan(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable String reason, @NotNull String punisher); CompletableFuture removeBan(@NotNull UUID playerUUID); } interface Mutes extends PunishmentModule { - @Nullable - CompletableFuture getMute(@NotNull UUID playerUUID); + CompletableFuture<@Nullable Punishment> getMute(@NotNull UUID playerUUID); CompletableFuture addMute(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable String reason, @NotNull String punisher); CompletableFuture removeMute(@NotNull UUID playerUUID); } 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 b8e69a7d..55767402 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/ban/BukkitBanModule.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/ban/BukkitBanModule.java @@ -2,10 +2,15 @@ package com.discordsrv.bukkit.ban; import com.discordsrv.api.module.type.PunishmentModule; import com.discordsrv.bukkit.BukkitDiscordSRV; -import com.discordsrv.common.logging.NamedLogger; +import com.discordsrv.common.bansync.BanSyncModule; import com.discordsrv.common.module.type.AbstractModule; 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.Listener; +import org.bukkit.event.player.PlayerKickEvent; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -14,10 +19,24 @@ import java.util.Date; import java.util.UUID; import java.util.concurrent.CompletableFuture; -public class BukkitBanModule extends AbstractModule implements PunishmentModule.Bans { +public class BukkitBanModule extends AbstractModule implements Listener, PunishmentModule.Bans { public BukkitBanModule(BukkitDiscordSRV discordSRV) { - super(discordSRV, new NamedLogger(discordSRV, "BUKKIT_BAN")); + super(discordSRV); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerKick(PlayerKickEvent event) { + Player player = event.getPlayer(); + if (!player.isBanned()) { + return; + } + + BanSyncModule module = discordSRV.getModule(BanSyncModule.class); + if (module != null) { + getBan(player.getUniqueId()).thenApply(Punishment::reason) + .whenComplete((reason, t) -> module.notifyBanned(discordSRV.playerProvider().player(player), reason)); + } } @Override diff --git a/common/src/main/java/com/discordsrv/common/bansync/BanSyncModule.java b/common/src/main/java/com/discordsrv/common/bansync/BanSyncModule.java new file mode 100644 index 00000000..35454bcf --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/bansync/BanSyncModule.java @@ -0,0 +1,162 @@ +package com.discordsrv.common.bansync; + +import com.discordsrv.api.event.bus.Subscribe; +import com.discordsrv.api.event.events.linking.AccountLinkedEvent; +import com.discordsrv.api.event.events.linking.AccountUnlinkedEvent; +import com.discordsrv.api.module.type.PunishmentModule; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.bansync.enums.BanSyncResult; +import com.discordsrv.common.event.events.player.PlayerConnectedEvent; +import com.discordsrv.common.module.type.AbstractModule; +import com.discordsrv.common.player.IPlayer; +import com.discordsrv.common.profile.Profile; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.UserSnowflake; +import net.dv8tion.jda.api.events.guild.GuildBanEvent; +import net.dv8tion.jda.api.events.guild.GuildUnbanEvent; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.requests.ErrorResponse; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class BanSyncModule extends AbstractModule { + + public BanSyncModule(DiscordSRV discordSRV) { + super(discordSRV); + } + + private CompletableFuture lookupLinkedAccount(UUID player) { + return discordSRV.profileManager().lookupProfile(player) + .thenApply(Profile::userId); + } + + private CompletableFuture lookupLinkedAccount(long userId) { + return discordSRV.profileManager().lookupProfile(userId) + .thenApply(Profile::playerUUID); + } + + public void notifyBanned(IPlayer player, @Nullable String reason) { + playerBanChange(player.uniqueId(), true); + } + + @Subscribe + public void onPlayerConnected(PlayerConnectedEvent event) { + playerBanChange(event.player().uniqueId(), false); + } + + @Subscribe + public void onGuildBan(GuildBanEvent event) { + userBanChange(event.getUser().getIdLong(), true); + } + + @Subscribe + public void onGuildUnban(GuildUnbanEvent event) { + userBanChange(event.getUser().getIdLong(), false); + } + + @Subscribe + public void onAccountLinked(AccountLinkedEvent event) { + + } + + @Subscribe + public void onAccountUnlinked(AccountUnlinkedEvent event) { + + } + + private void playerBanChange(UUID player, boolean newState) { + lookupLinkedAccount(player).thenApply(userId -> { + if (userId == null) { + // Unlinked + return null; + } + + // TODO: configurable reason format + return changeUserBanState(userId, newState, null); + }); + } + + private CompletableFuture changeUserBanState(long userId, boolean newState, @Nullable String reason) { + JDA jda = discordSRV.jda(); + if (jda == null) { + return CompletableFuture.completedFuture(BanSyncResult.NO_DISCORD_CONNECTION); + } + + Guild guild = jda.getGuildById(0L); // TODO: config + if (guild == null) { + // Server doesn't exist + return CompletableFuture.completedFuture(BanSyncResult.GUILD_DOESNT_EXIST); + } + + UserSnowflake snowflake = UserSnowflake.fromId(userId); + return guild.retrieveBan(snowflake).submit().exceptionally(t -> { + if (t instanceof ErrorResponseException && ((ErrorResponseException) t).getErrorResponse() == ErrorResponse.UNKNOWN_BAN) { + return null; + } + throw (RuntimeException) t; + }).thenCompose(ban -> { + if (ban == null) { + if (newState) { + // TODO: configurable deletion timeframe + return guild.ban(snowflake, 0, TimeUnit.MILLISECONDS).reason(reason).submit().thenApply(v -> BanSyncResult.BAN_USER); + } else { + // Already unbanned + return CompletableFuture.completedFuture(BanSyncResult.ALREADY_IN_SYNC); + } + } else { + if (newState) { + // Already banned + return CompletableFuture.completedFuture(BanSyncResult.ALREADY_IN_SYNC); + } else { + return guild.unban(snowflake).reason(reason).submit().thenApply(v -> BanSyncResult.UNBAN_USER); + } + } + }); + } + + public void userBanChange(long userId, boolean newState) { + lookupLinkedAccount(userId).thenApply(playerUUID -> { + if (playerUUID == null) { + // Unlinked + return null; + } + + // TODO: configurable reason format + return changePlayerBanState(playerUUID, newState, null); + }); + } + + private CompletableFuture changePlayerBanState(UUID playerUUID, boolean newState, @Nullable String reason) { + PunishmentModule.Bans bans = discordSRV.getModule(PunishmentModule.Bans.class); + if (bans == null) { + return CompletableFuture.completedFuture(BanSyncResult.NO_PUNISHMENT_INTEGRATION); + } + + return bans.getBan(playerUUID) + .thenCompose(punishment -> { + if (punishment == null) { + if (newState) { + return bans.addBan(playerUUID, null, reason, "DiscordSRV") + .thenApply(v -> BanSyncResult.BAN_PLAYER); + } else { + // Already unbanned + return CompletableFuture.completedFuture(BanSyncResult.ALREADY_IN_SYNC); + } + } else { + if (newState) { + // Already banned + return CompletableFuture.completedFuture(BanSyncResult.ALREADY_IN_SYNC); + } else { + return bans.removeBan(playerUUID).thenApply(v -> BanSyncResult.UNBAN_PLAYER); + } + } + }); + } + + + +} diff --git a/common/src/main/java/com/discordsrv/common/bansync/enums/BanSyncResult.java b/common/src/main/java/com/discordsrv/common/bansync/enums/BanSyncResult.java new file mode 100644 index 00000000..245c2bae --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/bansync/enums/BanSyncResult.java @@ -0,0 +1,20 @@ +package com.discordsrv.common.bansync.enums; + +public enum BanSyncResult { + + // Success, actioned + BAN_USER, + BAN_PLAYER, + UNBAN_USER, + UNBAN_PLAYER, + + // Nothing done + ALREADY_IN_SYNC, + WRONG_DIRECTION, + + // Error + NO_PUNISHMENT_INTEGRATION, + NO_DISCORD_CONNECTION, + GUILD_DOESNT_EXIST + +} diff --git a/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java b/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java index 20647fb3..47573b1d 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java @@ -20,8 +20,8 @@ package com.discordsrv.common.config.main; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.configurate.annotation.Constants; -import com.discordsrv.common.groupsync.enums.GroupSyncDirection; -import com.discordsrv.common.groupsync.enums.GroupSyncSide; +import com.discordsrv.common.sync.enums.SyncDirection; +import com.discordsrv.common.sync.enums.SyncSide; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; @@ -45,7 +45,7 @@ public class GroupSyncConfig { @Comment("The direction this group-role pair will synchronize in.\n" + "Valid options: %1, %2, %3") @Constants.Comment({"bidirectional", "minecraft_to_discord", "discord_to_minecraft"}) - public GroupSyncDirection direction = GroupSyncDirection.BIDIRECTIONAL; + public SyncDirection direction = SyncDirection.BIDIRECTIONAL; @Comment("Timed resynchronization.\n" + "This is required if you're not using LuckPerms and want to use Minecraft to Discord synchronization") @@ -64,7 +64,7 @@ public class GroupSyncConfig { @Comment("Decides which side takes priority when using timed synchronization or the resync command\n" + "Valid options: %1, %2") @Constants.Comment({"minecraft", "discord"}) - public GroupSyncSide tieBreaker = GroupSyncSide.MINECRAFT; + public SyncSide tieBreaker = SyncSide.MINECRAFT; @Comment("The LuckPerms \"%1\" context value, used when adding, removing and checking the groups of players.\n" + "Make this blank (\"\") to use the current server's value, or \"%2\" to not use the context") @@ -81,17 +81,17 @@ public class GroupSyncConfig { if ((invalidTieBreaker = (tieBreaker == null)) || (invalidDirection = (direction == null))) { if (invalidTieBreaker) { discordSRV.logger().error(label + " has invalid tie-breaker: " + tieBreaker - + ", should be one of " + Arrays.toString(GroupSyncSide.values())); + + ", should be one of " + Arrays.toString(SyncSide.values())); } if (invalidDirection) { discordSRV.logger().error(label + " has invalid direction: " + direction - + ", should be one of " + Arrays.toString(GroupSyncDirection.values())); + + ", should be one of " + Arrays.toString(SyncDirection.values())); } return false; - } else if (direction != GroupSyncDirection.BIDIRECTIONAL) { + } else if (direction != SyncDirection.BIDIRECTIONAL) { boolean minecraft; - if ((direction == GroupSyncDirection.MINECRAFT_TO_DISCORD) != (minecraft = (tieBreaker == GroupSyncSide.MINECRAFT))) { - GroupSyncSide opposite = (minecraft ? GroupSyncSide.DISCORD : GroupSyncSide.MINECRAFT); + if ((direction == SyncDirection.MINECRAFT_TO_DISCORD) != (minecraft = (tieBreaker == SyncSide.MINECRAFT))) { + SyncSide opposite = (minecraft ? SyncSide.DISCORD : SyncSide.MINECRAFT); discordSRV.logger().warning(label + " with direction " + direction + " with tie-breaker " + tieBreaker + " (should be " + opposite + ")"); @@ -103,20 +103,7 @@ public class GroupSyncConfig { @Override public String toString() { - String arrow; - switch (direction) { - default: - case BIDIRECTIONAL: - arrow = "<->"; - break; - case DISCORD_TO_MINECRAFT: - arrow = "<-"; - break; - case MINECRAFT_TO_DISCORD: - arrow = "->"; - break; - } - return "PairConfig{" + groupName + arrow + roleId + '}'; + return "PairConfig{" + groupName + direction.arrow() + Long.toUnsignedString(roleId) + '}'; } } 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 8eb33ee4..36e6cdd5 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java +++ b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java @@ -31,9 +31,9 @@ import com.discordsrv.common.debug.file.TextDebugFile; import com.discordsrv.common.event.events.player.PlayerConnectedEvent; import com.discordsrv.common.future.util.CompletableFutureUtil; import com.discordsrv.common.groupsync.enums.GroupSyncCause; -import com.discordsrv.common.groupsync.enums.GroupSyncDirection; +import com.discordsrv.common.sync.enums.SyncDirection; import com.discordsrv.common.groupsync.enums.GroupSyncResult; -import com.discordsrv.common.groupsync.enums.GroupSyncSide; +import com.discordsrv.common.sync.enums.SyncSide; import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.module.type.AbstractModule; import com.discordsrv.common.player.IPlayer; @@ -176,7 +176,7 @@ public class GroupSyncModule extends AbstractModule { Map> pairs ) { CompletableFutureUtil.combine(pairs.values()).whenComplete((v, t) -> { - SynchronizationSummary summary = new SynchronizationSummary(player, cause); + GroupSyncSummary summary = new GroupSyncSummary(player, cause); for (Map.Entry> entry : pairs.entrySet()) { summary.add(entry.getKey(), entry.getValue().join()); } @@ -312,7 +312,7 @@ public class GroupSyncModule extends AbstractModule { } resyncPair(pair, uuid, userId).whenComplete((result, t2) -> logger().debug( - new SynchronizationSummary(uuid, cause, pair, result).toString() + new GroupSyncSummary(uuid, cause, pair, result).toString() )); }); } @@ -350,14 +350,14 @@ public class GroupSyncModule extends AbstractModule { return; } - GroupSyncSide side = pair.tieBreaker; - GroupSyncDirection direction = pair.direction; + SyncSide side = pair.tieBreaker; + SyncDirection direction = pair.direction; CompletableFuture future; GroupSyncResult result; if (hasRole) { - if (side == GroupSyncSide.DISCORD) { + if (side == SyncSide.DISCORD) { // Has role, add group - if (direction == GroupSyncDirection.MINECRAFT_TO_DISCORD) { + if (direction == SyncDirection.MINECRAFT_TO_DISCORD) { resultFuture.complete(GroupSyncResult.WRONG_DIRECTION); return; } @@ -366,7 +366,7 @@ public class GroupSyncModule extends AbstractModule { future = addGroup(player, groupName, pair.serverContext); } else { // Doesn't have group, remove role - if (direction == GroupSyncDirection.DISCORD_TO_MINECRAFT) { + if (direction == SyncDirection.DISCORD_TO_MINECRAFT) { resultFuture.complete(GroupSyncResult.WRONG_DIRECTION); return; } @@ -375,9 +375,9 @@ public class GroupSyncModule extends AbstractModule { future = member.removeRole(role); } } else { - if (side == GroupSyncSide.DISCORD) { + if (side == SyncSide.DISCORD) { // Doesn't have role, remove group - if (direction == GroupSyncDirection.MINECRAFT_TO_DISCORD) { + if (direction == SyncDirection.MINECRAFT_TO_DISCORD) { resultFuture.complete(GroupSyncResult.WRONG_DIRECTION); return; } @@ -386,7 +386,7 @@ public class GroupSyncModule extends AbstractModule { future = removeGroup(player, groupName, pair.serverContext); } else { // Has group, add role - if (direction == GroupSyncDirection.DISCORD_TO_MINECRAFT) { + if (direction == SyncDirection.DISCORD_TO_MINECRAFT) { resultFuture.complete(GroupSyncResult.WRONG_DIRECTION); return; } @@ -480,8 +480,8 @@ public class GroupSyncModule extends AbstractModule { Map> futures = new LinkedHashMap<>(); for (GroupSyncConfig.PairConfig pair : pairs) { - GroupSyncDirection direction = pair.direction; - if (direction == GroupSyncDirection.MINECRAFT_TO_DISCORD) { + SyncDirection direction = pair.direction; + if (direction == SyncDirection.MINECRAFT_TO_DISCORD) { // Not going Discord -> Minecraft futures.put(pair, CompletableFuture.completedFuture(GroupSyncResult.WRONG_DIRECTION)); continue; @@ -490,7 +490,7 @@ public class GroupSyncModule extends AbstractModule { futures.put(pair, modifyGroupState(player, pair, remove)); // If the sync is bidirectional, also add/remove any other roles that are linked to this group - if (direction == GroupSyncDirection.DISCORD_TO_MINECRAFT) { + if (direction == SyncDirection.DISCORD_TO_MINECRAFT) { continue; } @@ -550,8 +550,8 @@ public class GroupSyncModule extends AbstractModule { PermissionModule.Groups permissionProvider = getPermissionProvider(); Map> futures = new LinkedHashMap<>(); for (GroupSyncConfig.PairConfig pair : pairs) { - GroupSyncDirection direction = pair.direction; - if (direction == GroupSyncDirection.DISCORD_TO_MINECRAFT) { + SyncDirection direction = pair.direction; + if (direction == SyncDirection.DISCORD_TO_MINECRAFT) { // Not going Minecraft -> Discord futures.put(pair, CompletableFuture.completedFuture(GroupSyncResult.WRONG_DIRECTION)); continue; @@ -585,7 +585,7 @@ public class GroupSyncModule extends AbstractModule { futures.put(pair, modifyRoleState(userId, pair, remove)); // If the sync is bidirectional, also add/remove any other groups that are linked to this role - if (direction == GroupSyncDirection.MINECRAFT_TO_DISCORD) { + if (direction == SyncDirection.MINECRAFT_TO_DISCORD) { continue; } diff --git a/common/src/main/java/com/discordsrv/common/groupsync/SynchronizationSummary.java b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncSummary.java similarity index 90% rename from common/src/main/java/com/discordsrv/common/groupsync/SynchronizationSummary.java rename to common/src/main/java/com/discordsrv/common/groupsync/GroupSyncSummary.java index 23231591..62251ec2 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/SynchronizationSummary.java +++ b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncSummary.java @@ -24,18 +24,18 @@ import com.discordsrv.common.groupsync.enums.GroupSyncResult; import java.util.*; -public class SynchronizationSummary { +public class GroupSyncSummary { private final EnumMap> pairs = new EnumMap<>(GroupSyncResult.class); private final UUID player; private final GroupSyncCause cause; - public SynchronizationSummary(UUID player, GroupSyncCause cause, GroupSyncConfig.PairConfig config, GroupSyncResult result) { + public GroupSyncSummary(UUID player, GroupSyncCause cause, GroupSyncConfig.PairConfig config, GroupSyncResult result) { this(player, cause); add(config, result); } - public SynchronizationSummary(UUID player, GroupSyncCause cause) { + public GroupSyncSummary(UUID player, GroupSyncCause cause) { this.player = player; this.cause = cause; } diff --git a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncDirection.java b/common/src/main/java/com/discordsrv/common/sync/enums/SyncDirection.java similarity index 67% rename from common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncDirection.java rename to common/src/main/java/com/discordsrv/common/sync/enums/SyncDirection.java index ddd06003..c22a51b3 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncDirection.java +++ b/common/src/main/java/com/discordsrv/common/sync/enums/SyncDirection.java @@ -16,12 +16,26 @@ * along with this program. If not, see . */ -package com.discordsrv.common.groupsync.enums; +package com.discordsrv.common.sync.enums; -public enum GroupSyncDirection { +public enum SyncDirection { - MINECRAFT_TO_DISCORD, - DISCORD_TO_MINECRAFT, - BIDIRECTIONAL + MINECRAFT_TO_DISCORD("->"), + DISCORD_TO_MINECRAFT("<-"), + BIDIRECTIONAL("<->"); + + private final String arrow; + + SyncDirection(String arrow) { + this.arrow = arrow; + } + + /** + * Game on the left, Discord on the right. + * @return the arrow + */ + public String arrow() { + return arrow; + } } diff --git a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncSide.java b/common/src/main/java/com/discordsrv/common/sync/enums/SyncSide.java similarity index 91% rename from common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncSide.java rename to common/src/main/java/com/discordsrv/common/sync/enums/SyncSide.java index 177ccfc0..c6bf849a 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncSide.java +++ b/common/src/main/java/com/discordsrv/common/sync/enums/SyncSide.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.discordsrv.common.groupsync.enums; +package com.discordsrv.common.sync.enums; -public enum GroupSyncSide { +public enum SyncSide { MINECRAFT, DISCORD