Initial for ban sync

This commit is contained in:
Vankka 2024-04-06 17:00:13 +03:00
parent 50445503ed
commit 0a61eac1ee
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
9 changed files with 260 additions and 59 deletions

View File

@ -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<Punishment> getBan(@NotNull UUID playerUUID);
CompletableFuture<@Nullable Punishment> getBan(@NotNull UUID playerUUID);
CompletableFuture<Void> addBan(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable String reason, @NotNull String punisher);
CompletableFuture<Void> removeBan(@NotNull UUID playerUUID);
}
interface Mutes extends PunishmentModule {
@Nullable
CompletableFuture<Punishment> getMute(@NotNull UUID playerUUID);
CompletableFuture<@Nullable Punishment> getMute(@NotNull UUID playerUUID);
CompletableFuture<Void> addMute(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable String reason, @NotNull String punisher);
CompletableFuture<Void> removeMute(@NotNull UUID playerUUID);
}

View File

@ -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<BukkitDiscordSRV> implements PunishmentModule.Bans {
public class BukkitBanModule extends AbstractModule<BukkitDiscordSRV> 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

View File

@ -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<DiscordSRV> {
public BanSyncModule(DiscordSRV discordSRV) {
super(discordSRV);
}
private CompletableFuture<Long> lookupLinkedAccount(UUID player) {
return discordSRV.profileManager().lookupProfile(player)
.thenApply(Profile::userId);
}
private CompletableFuture<UUID> 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<BanSyncResult> 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<BanSyncResult> 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);
}
}
});
}
}

View File

@ -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
}

View File

@ -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) + '}';
}
}

View File

@ -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<DiscordSRV> {
Map<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> pairs
) {
CompletableFutureUtil.combine(pairs.values()).whenComplete((v, t) -> {
SynchronizationSummary summary = new SynchronizationSummary(player, cause);
GroupSyncSummary summary = new GroupSyncSummary(player, cause);
for (Map.Entry<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> entry : pairs.entrySet()) {
summary.add(entry.getKey(), entry.getValue().join());
}
@ -312,7 +312,7 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
}
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<DiscordSRV> {
return;
}
GroupSyncSide side = pair.tieBreaker;
GroupSyncDirection direction = pair.direction;
SyncSide side = pair.tieBreaker;
SyncDirection direction = pair.direction;
CompletableFuture<Void> 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<DiscordSRV> {
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<DiscordSRV> {
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<DiscordSRV> {
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<DiscordSRV> {
Map<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> 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<DiscordSRV> {
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<DiscordSRV> {
PermissionModule.Groups permissionProvider = getPermissionProvider();
Map<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> 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<DiscordSRV> {
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;
}

View File

@ -24,18 +24,18 @@ import com.discordsrv.common.groupsync.enums.GroupSyncResult;
import java.util.*;
public class SynchronizationSummary {
public class GroupSyncSummary {
private final EnumMap<GroupSyncResult, Set<GroupSyncConfig.PairConfig>> 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;
}

View File

@ -16,12 +16,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -16,9 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.groupsync.enums;
package com.discordsrv.common.sync.enums;
public enum GroupSyncSide {
public enum SyncSide {
MINECRAFT,
DISCORD