mirror of
https://github.com/DiscordSRV/Ascension.git
synced 2024-11-25 12:25:15 +01:00
AbstractSyncModule
This commit is contained in:
parent
53e216ea2a
commit
eec194491e
@ -1,6 +1,7 @@
|
||||
package com.discordsrv.api.module.type;
|
||||
|
||||
import com.discordsrv.api.module.Module;
|
||||
import com.discordsrv.api.punishment.Punishment;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@ -21,29 +22,4 @@ public interface PunishmentModule extends Module {
|
||||
CompletableFuture<Void> addMute(@NotNull UUID playerUUID, @Nullable Instant until, @Nullable String reason, @NotNull String punisher);
|
||||
CompletableFuture<Void> removeMute(@NotNull UUID playerUUID);
|
||||
}
|
||||
|
||||
class Punishment {
|
||||
|
||||
private final Instant until;
|
||||
private final String reason;
|
||||
private final String punisher;
|
||||
|
||||
public Punishment(@Nullable Instant until, @Nullable String reason, @Nullable String punisher) {
|
||||
this.until = until;
|
||||
this.reason = reason;
|
||||
this.punisher = punisher;
|
||||
}
|
||||
|
||||
public Instant until() {
|
||||
return until;
|
||||
}
|
||||
|
||||
public String reason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public String punisher() {
|
||||
return punisher;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
package com.discordsrv.api.punishment;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class Punishment {
|
||||
|
||||
private final Instant until;
|
||||
private final String reason;
|
||||
private final String punisher;
|
||||
|
||||
public Punishment(@Nullable Instant until, @Nullable String reason, @Nullable String punisher) {
|
||||
this.until = until;
|
||||
this.reason = reason;
|
||||
this.punisher = punisher;
|
||||
}
|
||||
|
||||
public Instant until() {
|
||||
return until;
|
||||
}
|
||||
|
||||
public String reason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public String punisher() {
|
||||
return punisher;
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package com.discordsrv.bukkit.ban;
|
||||
|
||||
import com.discordsrv.api.module.type.PunishmentModule;
|
||||
import com.discordsrv.api.punishment.Punishment;
|
||||
import com.discordsrv.bukkit.BukkitDiscordSRV;
|
||||
import com.discordsrv.common.bansync.BanSyncModule;
|
||||
import com.discordsrv.common.module.type.AbstractModule;
|
||||
@ -34,13 +35,13 @@ public class BukkitBanModule extends AbstractModule<BukkitDiscordSRV> implements
|
||||
|
||||
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));
|
||||
getBan(player.getUniqueId())
|
||||
.whenComplete((punishment, t) -> module.notifyBanned(discordSRV.playerProvider().player(player), punishment));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Punishment> getBan(@NotNull UUID playerUUID) {
|
||||
public CompletableFuture<com.discordsrv.api.punishment.Punishment> getBan(@NotNull UUID playerUUID) {
|
||||
CompletableFuture<BanEntry> entryFuture;
|
||||
if (PaperBanList.IS_AVAILABLE) {
|
||||
entryFuture = CompletableFuture.completedFuture(PaperBanList.getBanEntry(discordSRV.server(), playerUUID));
|
||||
@ -52,7 +53,7 @@ public class BukkitBanModule extends AbstractModule<BukkitDiscordSRV> implements
|
||||
|
||||
return entryFuture.thenApply(ban -> {
|
||||
Date expiration = ban.getExpiration();
|
||||
return new PunishmentModule.Punishment(expiration != null ? expiration.toInstant() : null, ban.getReason(), ban.getSource());
|
||||
return new Punishment(expiration != null ? expiration.toInstant() : null, ban.getReason(), ban.getSource());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import com.discordsrv.api.event.events.message.receive.game.GameChatMessageRecei
|
||||
import com.discordsrv.api.module.type.NicknameModule;
|
||||
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.bukkit.player.BukkitPlayer;
|
||||
import com.discordsrv.common.component.util.ComponentUtil;
|
||||
@ -75,7 +76,7 @@ public class EssentialsXIntegration
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Punishment> getMute(@NotNull UUID playerUUID) {
|
||||
public CompletableFuture<com.discordsrv.api.punishment.Punishment> getMute(@NotNull UUID playerUUID) {
|
||||
return getUser(playerUUID).thenApply(user -> new Punishment(Instant.ofEpochMilli(user.getMuteTimeout()), user.getMuteReason(), null));
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadedEvent;
|
||||
import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent;
|
||||
import com.discordsrv.api.module.Module;
|
||||
import com.discordsrv.common.api.util.ApiInstanceUtil;
|
||||
import com.discordsrv.common.bansync.BanSyncModule;
|
||||
import com.discordsrv.common.bootstrap.IBootstrap;
|
||||
import com.discordsrv.common.channel.ChannelConfigHelper;
|
||||
import com.discordsrv.common.channel.ChannelLockingModule;
|
||||
@ -565,6 +566,7 @@ public abstract class AbstractDiscordSRV<
|
||||
placeholderService().addGlobalContext(UUIDUtil.class);
|
||||
|
||||
// Modules
|
||||
registerModule(BanSyncModule::new);
|
||||
registerModule(ConsoleModule::new);
|
||||
registerModule(ChannelLockingModule::new);
|
||||
registerModule(TimedUpdaterModule::new);
|
||||
|
@ -1,34 +1,69 @@
|
||||
package com.discordsrv.common.bansync;
|
||||
|
||||
import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent;
|
||||
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.api.punishment.Punishment;
|
||||
import com.discordsrv.common.DiscordSRV;
|
||||
import com.discordsrv.common.bansync.enums.BanSyncResult;
|
||||
import com.discordsrv.common.config.main.BanSyncConfig;
|
||||
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 com.discordsrv.common.sync.enums.SyncDirection;
|
||||
import com.discordsrv.common.sync.enums.SyncSide;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.audit.ActionType;
|
||||
import net.dv8tion.jda.api.audit.AuditLogEntry;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
import net.dv8tion.jda.api.entities.UserSnowflake;
|
||||
import net.dv8tion.jda.api.events.guild.GuildAuditLogEntryCreateEvent;
|
||||
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.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class BanSyncModule extends AbstractModule<DiscordSRV> {
|
||||
|
||||
private final Map<Long, PunishmentEvent> events = new ConcurrentHashMap<>();
|
||||
|
||||
public BanSyncModule(DiscordSRV discordSRV) {
|
||||
super(discordSRV);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
if (discordSRV.config().banSync.serverId == 0) {
|
||||
//return false;
|
||||
}
|
||||
|
||||
return super.isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<DiscordGatewayIntent> requiredIntents() {
|
||||
return Collections.singleton(DiscordGatewayIntent.GUILD_MODERATION);
|
||||
}
|
||||
|
||||
private Punishment punishment(Guild.Ban ban) {
|
||||
return ban != null ? new Punishment(null, ban.getReason(), ban.getUser().getName()) : null;
|
||||
}
|
||||
|
||||
private CompletableFuture<Long> lookupLinkedAccount(UUID player) {
|
||||
return discordSRV.profileManager().lookupProfile(player)
|
||||
.thenApply(Profile::userId);
|
||||
@ -39,70 +74,203 @@ public class BanSyncModule extends AbstractModule<DiscordSRV> {
|
||||
.thenApply(Profile::playerUUID);
|
||||
}
|
||||
|
||||
public void notifyBanned(IPlayer player, @Nullable String reason) {
|
||||
playerBanChange(player.uniqueId(), true);
|
||||
public void notifyBanned(IPlayer player, @Nullable Punishment punishment) {
|
||||
playerBanChange(player.uniqueId(), true, punishment);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPlayerConnected(PlayerConnectedEvent event) {
|
||||
playerBanChange(event.player().uniqueId(), false);
|
||||
playerBanChange(event.player().uniqueId(), false, null);
|
||||
}
|
||||
|
||||
private PunishmentEvent upsertEvent(long userId, boolean newState) {
|
||||
return events.computeIfAbsent(userId, key -> new PunishmentEvent(userId, newState));
|
||||
}
|
||||
|
||||
private class PunishmentEvent {
|
||||
|
||||
private final long userId;
|
||||
private final boolean newState;
|
||||
private final Future<?> future;
|
||||
|
||||
public PunishmentEvent(long userId, boolean newState) {
|
||||
this.userId = userId;
|
||||
this.newState = newState;
|
||||
|
||||
// Run in 5s if an audit log event doesn't arrive
|
||||
this.future = discordSRV.scheduler().runLater(() -> applyPunishment(null), Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
public void applyPunishment(@Nullable Punishment punishment) {
|
||||
if (!future.cancel(false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
userBanChange(userId, newState, punishment);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onGuildBan(GuildBanEvent event) {
|
||||
userBanChange(event.getUser().getIdLong(), true);
|
||||
upsertEvent(event.getUser().getIdLong(), true);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onGuildUnban(GuildUnbanEvent event) {
|
||||
userBanChange(event.getUser().getIdLong(), false);
|
||||
upsertEvent(event.getUser().getIdLong(), false);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onGuildAuditLogEntryCreate(GuildAuditLogEntryCreateEvent event) {
|
||||
AuditLogEntry entry = event.getEntry();
|
||||
ActionType actionType = entry.getType();
|
||||
if (actionType != ActionType.BAN && actionType != ActionType.UNBAN) {
|
||||
return;
|
||||
}
|
||||
|
||||
long punisherId = entry.getUserIdLong();
|
||||
User punisher = event.getJDA().getUserById(punisherId);
|
||||
String punishmentName = punisher != null ? punisher.getName() : Long.toUnsignedString(punisherId);
|
||||
|
||||
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);
|
||||
} else {
|
||||
upsertEvent(bannedUserId, false).applyPunishment(punishment);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onAccountLinked(AccountLinkedEvent event) {
|
||||
|
||||
BanSyncConfig config = discordSRV.config().banSync;
|
||||
if (config.resyncUponLinking) {
|
||||
resync(event.getPlayerUUID(), event.getUserId());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onAccountUnlinked(AccountUnlinkedEvent event) {
|
||||
|
||||
private CompletableFuture<Guild.@Nullable Ban> getBan(Guild guild, long userId) {
|
||||
return guild.retrieveBan(UserSnowflake.fromId(userId)).submit().exceptionally(t -> {
|
||||
if (t instanceof ErrorResponseException && ((ErrorResponseException) t).getErrorResponse() == ErrorResponse.UNKNOWN_BAN) {
|
||||
return null;
|
||||
}
|
||||
throw (RuntimeException) t;
|
||||
});
|
||||
}
|
||||
|
||||
private void playerBanChange(UUID player, boolean newState) {
|
||||
lookupLinkedAccount(player).thenApply(userId -> {
|
||||
public CompletableFuture<BanSyncResult> resync(UUID playerUUID) {
|
||||
return lookupLinkedAccount(playerUUID).thenCompose(userId -> {
|
||||
if (userId == null) {
|
||||
// Unlinked
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: configurable reason format
|
||||
return changeUserBanState(userId, newState, null);
|
||||
return resync(playerUUID, userId);
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<BanSyncResult> changeUserBanState(long userId, boolean newState, @Nullable String reason) {
|
||||
public CompletableFuture<BanSyncResult> resync(long userId) {
|
||||
return lookupLinkedAccount(userId).thenCompose(playerUUID -> {
|
||||
if (playerUUID == null) {
|
||||
// Unlinked
|
||||
return null;
|
||||
}
|
||||
|
||||
return resync(playerUUID, userId);
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<BanSyncResult> resync(UUID playerUUID, long userId) {
|
||||
return doResync(playerUUID, userId).whenComplete((r, t) -> {
|
||||
String label = playerUUID + ":" + Long.toUnsignedString(userId);
|
||||
if (t != null) {
|
||||
logger().error("Failed to update ban state for " + label, t);
|
||||
} else {
|
||||
logger().debug("Updated " + label + " ban state: " + r);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<BanSyncResult> doResync(UUID playerUUID, long userId) {
|
||||
BanSyncConfig config = discordSRV.config().banSync;
|
||||
|
||||
SyncSide side = config.tieBreaker;
|
||||
if (side == null) {
|
||||
return CompletableFuture.completedFuture(BanSyncResult.INVALID_CONFIG);
|
||||
}
|
||||
|
||||
switch (side) {
|
||||
case DISCORD:
|
||||
JDA jda = discordSRV.jda();
|
||||
if (jda == null) {
|
||||
return CompletableFuture.completedFuture(BanSyncResult.NO_DISCORD_CONNECTION);
|
||||
}
|
||||
|
||||
Guild guild = jda.getGuildById(0L); // TODO: config
|
||||
Guild guild = jda.getGuildById(config.serverId);
|
||||
if (guild == null) {
|
||||
// Server doesn't exist
|
||||
return CompletableFuture.completedFuture(BanSyncResult.GUILD_DOESNT_EXIST);
|
||||
}
|
||||
|
||||
return getBan(guild, userId)
|
||||
.thenCompose(ban -> changePlayerBanState(playerUUID, ban != null, punishment(ban)));
|
||||
case MINECRAFT:
|
||||
PunishmentModule.Bans bans = discordSRV.getModule(PunishmentModule.Bans.class);
|
||||
if (bans == null) {
|
||||
return CompletableFuture.completedFuture(BanSyncResult.NO_PUNISHMENT_INTEGRATION);
|
||||
}
|
||||
|
||||
return bans.getBan(playerUUID)
|
||||
.thenCompose(punishment -> changeUserBanState(userId, punishment != null, punishment));
|
||||
default:
|
||||
throw new IllegalStateException("Missing side " + side.name());
|
||||
}
|
||||
}
|
||||
|
||||
private void playerBanChange(UUID playerUUID, boolean newState, @Nullable Punishment punishment) {
|
||||
lookupLinkedAccount(playerUUID).thenCompose(userId -> {
|
||||
if (userId == null) {
|
||||
// Unlinked
|
||||
return null;
|
||||
}
|
||||
|
||||
return changeUserBanState(userId, newState, punishment).whenComplete((r, t) -> {
|
||||
if (t != null) {
|
||||
logger().error("Failed to update ban state for " + Long.toUnsignedString(userId), t);
|
||||
} else {
|
||||
logger().debug("Updated " + Long.toUnsignedString(userId) + " ban state: " + r);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<BanSyncResult> changeUserBanState(long userId, boolean newState, @Nullable Punishment punishment) {
|
||||
BanSyncConfig config = discordSRV.config().banSync;
|
||||
if (config.direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||
return CompletableFuture.completedFuture(BanSyncResult.WRONG_DIRECTION);
|
||||
}
|
||||
|
||||
JDA jda = discordSRV.jda();
|
||||
if (jda == null) {
|
||||
return CompletableFuture.completedFuture(BanSyncResult.NO_DISCORD_CONNECTION);
|
||||
}
|
||||
|
||||
Guild guild = jda.getGuildById(config.serverId);
|
||||
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 -> {
|
||||
return getBan(guild, userId).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);
|
||||
return guild.ban(snowflake, config.discordMessageHoursToDelete, TimeUnit.HOURS)
|
||||
.reason(discordSRV.placeholderService().replacePlaceholders(config.discordBanReasonFormat, punishment))
|
||||
.submit()
|
||||
.thenApply(v -> BanSyncResult.BAN_USER);
|
||||
} else {
|
||||
// Already unbanned
|
||||
return CompletableFuture.completedFuture(BanSyncResult.ALREADY_IN_SYNC);
|
||||
@ -112,35 +280,49 @@ public class BanSyncModule extends AbstractModule<DiscordSRV> {
|
||||
// Already banned
|
||||
return CompletableFuture.completedFuture(BanSyncResult.ALREADY_IN_SYNC);
|
||||
} else {
|
||||
return guild.unban(snowflake).reason(reason).submit().thenApply(v -> BanSyncResult.UNBAN_USER);
|
||||
return guild.unban(snowflake)
|
||||
.reason(discordSRV.placeholderService().replacePlaceholders(config.discordUnbanReasonFormat, punishment))
|
||||
.submit()
|
||||
.thenApply(v -> BanSyncResult.UNBAN_USER);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void userBanChange(long userId, boolean newState) {
|
||||
lookupLinkedAccount(userId).thenApply(playerUUID -> {
|
||||
public void userBanChange(long userId, boolean newState, @Nullable Punishment punishment) {
|
||||
lookupLinkedAccount(userId).thenCompose(playerUUID -> {
|
||||
if (playerUUID == null) {
|
||||
// Unlinked
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: configurable reason format
|
||||
return changePlayerBanState(playerUUID, newState, null);
|
||||
return changePlayerBanState(playerUUID, newState, punishment).whenComplete((r, t) -> {
|
||||
if (t != null) {
|
||||
logger().error("Failed to update ban state for " + playerUUID, t);
|
||||
} else {
|
||||
logger().debug("Updated " + playerUUID + " ban state: " + r);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<BanSyncResult> changePlayerBanState(UUID playerUUID, boolean newState, @Nullable String reason) {
|
||||
private CompletableFuture<BanSyncResult> changePlayerBanState(UUID playerUUID, boolean newState, @Nullable Punishment punishment) {
|
||||
BanSyncConfig config = discordSRV.config().banSync;
|
||||
if (config.direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
||||
return CompletableFuture.completedFuture(BanSyncResult.WRONG_DIRECTION);
|
||||
}
|
||||
|
||||
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) {
|
||||
return bans.getBan(playerUUID).thenCompose(existingPunishment -> {
|
||||
if (existingPunishment == null) {
|
||||
if (newState) {
|
||||
return bans.addBan(playerUUID, null, reason, "DiscordSRV")
|
||||
String reason = discordSRV.placeholderService().replacePlaceholders(config.gameBanReasonFormat, punishment);
|
||||
String punisher = discordSRV.placeholderService().replacePlaceholders(config.gamePunisherFormat, punishment);
|
||||
return bans.addBan(playerUUID, null, reason, punisher)
|
||||
.thenApply(v -> BanSyncResult.BAN_PLAYER);
|
||||
} else {
|
||||
// Already unbanned
|
||||
@ -157,6 +339,4 @@ public class BanSyncModule extends AbstractModule<DiscordSRV> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -3,18 +3,28 @@ package com.discordsrv.common.bansync.enums;
|
||||
public enum BanSyncResult {
|
||||
|
||||
// Success, actioned
|
||||
BAN_USER,
|
||||
BAN_PLAYER,
|
||||
UNBAN_USER,
|
||||
UNBAN_PLAYER,
|
||||
BAN_USER("Ban user"),
|
||||
BAN_PLAYER("Ban player"),
|
||||
UNBAN_USER("Unban user"),
|
||||
UNBAN_PLAYER("Unban player"),
|
||||
|
||||
// Nothing done
|
||||
ALREADY_IN_SYNC,
|
||||
WRONG_DIRECTION,
|
||||
ALREADY_IN_SYNC("Already in sync"),
|
||||
WRONG_DIRECTION("Wrong direction"),
|
||||
|
||||
// Error
|
||||
NO_PUNISHMENT_INTEGRATION,
|
||||
NO_DISCORD_CONNECTION,
|
||||
GUILD_DOESNT_EXIST
|
||||
NO_PUNISHMENT_INTEGRATION("No punishment integration"),
|
||||
NO_DISCORD_CONNECTION("No Discord connection"),
|
||||
GUILD_DOESNT_EXIST("Guild doesn't exist"),
|
||||
INVALID_CONFIG("Invalid config");
|
||||
|
||||
private final String prettyResult;
|
||||
|
||||
BanSyncResult(String prettyResult) {
|
||||
this.prettyResult = prettyResult;
|
||||
}
|
||||
|
||||
public String prettyResult() {
|
||||
return prettyResult;
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,13 @@ import com.discordsrv.common.command.combined.abstraction.CommandExecution;
|
||||
import com.discordsrv.common.command.combined.abstraction.GameCommandExecution;
|
||||
import com.discordsrv.common.command.combined.abstraction.Text;
|
||||
import com.discordsrv.common.command.game.abstraction.GameCommand;
|
||||
import com.discordsrv.common.config.main.GroupSyncConfig;
|
||||
import com.discordsrv.common.future.util.CompletableFutureUtil;
|
||||
import com.discordsrv.common.groupsync.GroupSyncModule;
|
||||
import com.discordsrv.common.groupsync.enums.GroupSyncCause;
|
||||
import com.discordsrv.common.groupsync.enums.GroupSyncResult;
|
||||
import com.discordsrv.common.permission.Permission;
|
||||
import com.discordsrv.common.player.IPlayer;
|
||||
import com.discordsrv.common.sync.ISyncResult;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
|
||||
import java.util.*;
|
||||
@ -78,15 +79,19 @@ public class ResyncCommand extends CombinedCommand {
|
||||
execution.runAsync(() -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
CompletableFutureUtil.combine(resyncOnlinePlayers(module))
|
||||
.whenComplete((results, t) -> {
|
||||
EnumMap<GroupSyncResult, AtomicInteger> resultCounts = new EnumMap<>(GroupSyncResult.class);
|
||||
int total = 0;
|
||||
for (List<GroupSyncResult> result : results) {
|
||||
for (GroupSyncResult singleResult : result) {
|
||||
total++;
|
||||
resultCounts.computeIfAbsent(singleResult, key -> new AtomicInteger(0)).getAndIncrement();
|
||||
CompletableFutureUtil.combine(resyncOnlinePlayers(module)).thenCompose(result -> {
|
||||
List<CompletableFuture<ISyncResult>> results = new ArrayList<>();
|
||||
for (Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>> map : result) {
|
||||
results.addAll(map.values());
|
||||
}
|
||||
return CompletableFutureUtil.combine(results);
|
||||
}).whenComplete((results, t) -> {
|
||||
Map<ISyncResult, AtomicInteger> resultCounts = new HashMap<>();
|
||||
int total = 0;
|
||||
|
||||
for (ISyncResult result : results) {
|
||||
total++;
|
||||
resultCounts.computeIfAbsent(result, key -> new AtomicInteger(0)).getAndIncrement();
|
||||
}
|
||||
String resultHover = resultCounts.entrySet().stream()
|
||||
.map(entry -> entry.getKey().toString() + ": " + entry.getValue().get())
|
||||
@ -111,8 +116,8 @@ public class ResyncCommand extends CombinedCommand {
|
||||
});
|
||||
}
|
||||
|
||||
private List<CompletableFuture<List<GroupSyncResult>>> resyncOnlinePlayers(GroupSyncModule module) {
|
||||
List<CompletableFuture<List<GroupSyncResult>>> futures = new ArrayList<>();
|
||||
private List<CompletableFuture<Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>>>> resyncOnlinePlayers(GroupSyncModule module) {
|
||||
List<CompletableFuture<Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>>>> futures = new ArrayList<>();
|
||||
for (IPlayer player : discordSRV.playerProvider().allPlayers()) {
|
||||
futures.add(module.resync(player.uniqueId(), GroupSyncCause.COMMAND));
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
package com.discordsrv.common.config.main;
|
||||
|
||||
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<BanSyncConfig, Void, Long> {
|
||||
|
||||
@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%";
|
||||
|
||||
@Comment("The punisher applied when creating new bans in Minecraft")
|
||||
public String gamePunisherFormat = "@%user_effective_server_name%";
|
||||
|
||||
@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'%";
|
||||
|
||||
@Comment("The reason applied when removing bans in Discord")
|
||||
public String discordUnbanReasonFormat = "Unbanned in Minecraft";
|
||||
|
||||
@Comment("The amount of hours to delete Discord messages, when syncing bans from Minecraft to Discord")
|
||||
public int discordMessageHoursToDelete = 0;
|
||||
|
||||
@Comment("Resync upon linking")
|
||||
public boolean resyncUponLinking = true;
|
||||
|
||||
@Override
|
||||
public boolean isSet() {
|
||||
return serverId != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void gameId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long discordId() {
|
||||
return serverId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameAs(BanSyncConfig config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String describe() {
|
||||
return "Ban sync";
|
||||
}
|
||||
}
|
@ -18,10 +18,9 @@
|
||||
|
||||
package com.discordsrv.common.config.main;
|
||||
|
||||
import com.discordsrv.common.DiscordSRV;
|
||||
import com.discordsrv.common.config.configurate.annotation.Constants;
|
||||
import com.discordsrv.common.sync.enums.SyncDirection;
|
||||
import com.discordsrv.common.sync.enums.SyncSide;
|
||||
import com.discordsrv.common.config.main.generic.AbstractSyncConfig;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
|
||||
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||||
|
||||
@ -30,11 +29,13 @@ import java.util.*;
|
||||
@ConfigSerializable
|
||||
public class GroupSyncConfig {
|
||||
|
||||
@Comment("Group-Role pairs for group synchronization")
|
||||
@Comment("Group-Role pairs for group synchronization\n"
|
||||
+ "\n"
|
||||
+ "If you are not using LuckPerms and want to use Minecraft -> Discord synchronization, you must specify timed synchronization")
|
||||
public List<PairConfig> pairs = new ArrayList<>(Collections.singletonList(new PairConfig()));
|
||||
|
||||
@ConfigSerializable
|
||||
public static class PairConfig {
|
||||
public static class PairConfig extends AbstractSyncConfig<PairConfig, String, Long> {
|
||||
|
||||
@Comment("The case-sensitive group name from your permissions plugin")
|
||||
public String groupName = "";
|
||||
@ -42,69 +43,43 @@ public class GroupSyncConfig {
|
||||
@Comment("The Discord role id")
|
||||
public Long roleId = 0L;
|
||||
|
||||
@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 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")
|
||||
public TimerConfig timer = new TimerConfig();
|
||||
|
||||
@ConfigSerializable
|
||||
public static class TimerConfig {
|
||||
|
||||
@Comment("If timed synchronization of this group-role pair is enabled")
|
||||
public boolean enabled = true;
|
||||
|
||||
@Comment("The amount of minutes between cycles")
|
||||
public int cycleTime = 5;
|
||||
}
|
||||
|
||||
@Comment("Decides which side takes priority when using timed synchronization or the resync command\n"
|
||||
+ "Valid options: %1, %2")
|
||||
@Constants.Comment({"minecraft", "discord"})
|
||||
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")
|
||||
@Constants.Comment({"server", "global"})
|
||||
public String serverContext = "global";
|
||||
|
||||
public boolean isTheSameAs(PairConfig config) {
|
||||
return groupName.equals(config.groupName) && Objects.equals(roleId, config.roleId);
|
||||
public boolean isSet() {
|
||||
return roleId != 0 && StringUtils.isNotEmpty(groupName);
|
||||
}
|
||||
|
||||
public boolean validate(DiscordSRV discordSRV) {
|
||||
String label = "Group synchronization (" + groupName + ":" + Long.toUnsignedString(roleId) + ")";
|
||||
boolean invalidTieBreaker, invalidDirection = false;
|
||||
if ((invalidTieBreaker = (tieBreaker == null)) || (invalidDirection = (direction == null))) {
|
||||
if (invalidTieBreaker) {
|
||||
discordSRV.logger().error(label + " has invalid tie-breaker: " + tieBreaker
|
||||
+ ", should be one of " + Arrays.toString(SyncSide.values()));
|
||||
@Override
|
||||
public String gameId() {
|
||||
return makeGameId(groupName, serverContext != null ? Collections.singleton(serverContext) : null);
|
||||
}
|
||||
if (invalidDirection) {
|
||||
discordSRV.logger().error(label + " has invalid direction: " + direction
|
||||
+ ", should be one of " + Arrays.toString(SyncDirection.values()));
|
||||
|
||||
@Override
|
||||
public Long discordId() {
|
||||
return roleId;
|
||||
}
|
||||
return false;
|
||||
} else if (direction != SyncDirection.BIDIRECTIONAL) {
|
||||
boolean 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 + ")");
|
||||
tieBreaker = opposite; // Fix the config
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@Override
|
||||
public boolean isSameAs(PairConfig config) {
|
||||
return groupName.equals(config.groupName) && Objects.equals(roleId, config.roleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PairConfig{" + groupName + direction.arrow() + Long.toUnsignedString(roleId) + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public String describe() {
|
||||
return "Group sync (" + groupName + ":" + Long.toUnsignedString(roleId) + ")";
|
||||
}
|
||||
|
||||
public static String makeGameId(String groupName, Set<String> serverContext) {
|
||||
return groupName + (serverContext != null ? String.join(" ", serverContext) : "");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -81,6 +81,9 @@ public abstract class MainConfig implements Config {
|
||||
@Comment("Configuration options for group-role synchronization")
|
||||
public GroupSyncConfig groupSync = new GroupSyncConfig();
|
||||
|
||||
@Comment("Configuration options for ban synchronization")
|
||||
public BanSyncConfig banSync = new BanSyncConfig();
|
||||
|
||||
@Comment("In-game command configuration")
|
||||
public GameCommandConfig gameCommand = new GameCommandConfig();
|
||||
|
||||
|
@ -0,0 +1,72 @@
|
||||
package com.discordsrv.common.config.main.generic;
|
||||
|
||||
import com.discordsrv.common.DiscordSRV;
|
||||
import com.discordsrv.common.config.configurate.annotation.Constants;
|
||||
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;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@ConfigSerializable
|
||||
public abstract class AbstractSyncConfig<C extends AbstractSyncConfig<C, G, D>, G, D> {
|
||||
|
||||
@Comment("The direction to synchronize in.\n"
|
||||
+ "Valid options: %1, %2, %3")
|
||||
@Constants.Comment({"bidirectional", "minecraft_to_discord", "discord_to_minecraft"})
|
||||
public SyncDirection direction = SyncDirection.BIDIRECTIONAL;
|
||||
|
||||
@Comment("Timed resynchronization")
|
||||
public TimerConfig timer = new TimerConfig();
|
||||
|
||||
@ConfigSerializable
|
||||
public static class TimerConfig {
|
||||
|
||||
@Comment("If timed synchronization is enabled")
|
||||
public boolean enabled = true;
|
||||
|
||||
@Comment("The amount of minutes between timed synchronization cycles")
|
||||
public int cycleTime = 5;
|
||||
}
|
||||
|
||||
@Comment("Decides which side takes priority when using timed synchronization or the resync command and there are differences\n"
|
||||
+ "Valid options: %1, %2")
|
||||
@Constants.Comment({"minecraft", "discord"})
|
||||
public SyncSide tieBreaker = SyncSide.MINECRAFT;
|
||||
|
||||
public abstract boolean isSet();
|
||||
|
||||
public abstract G gameId();
|
||||
public abstract D discordId();
|
||||
public abstract boolean isSameAs(C otherConfig);
|
||||
|
||||
public abstract String describe();
|
||||
|
||||
public boolean validate(DiscordSRV discordSRV) {
|
||||
String label = describe();
|
||||
boolean invalidTieBreaker, invalidDirection = false;
|
||||
if ((invalidTieBreaker = (tieBreaker == null)) || (invalidDirection = (direction == null))) {
|
||||
if (invalidTieBreaker) {
|
||||
discordSRV.logger().error(label + " has invalid tie-breaker: " + tieBreaker
|
||||
+ ", should be one of " + Arrays.toString(SyncSide.values()));
|
||||
}
|
||||
if (invalidDirection) {
|
||||
discordSRV.logger().error(label + " has invalid direction: " + direction
|
||||
+ ", should be one of " + Arrays.toString(SyncDirection.values()));
|
||||
}
|
||||
return false;
|
||||
} else if (direction != SyncDirection.BIDIRECTIONAL) {
|
||||
boolean 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 + ")"
|
||||
);
|
||||
tieBreaker = opposite; // Fix the config
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -389,6 +389,9 @@ public class JDAConnectionManager implements DiscordConnectionManager {
|
||||
// We don't use MDC
|
||||
jdaBuilder.setContextEnabled(false);
|
||||
|
||||
// Enable event passthrough
|
||||
jdaBuilder.setEventPassthrough(true);
|
||||
|
||||
// Custom event manager to forward to the DiscordSRV event bus & block using JDA's event listeners
|
||||
jdaBuilder.setEventManager(new EventManagerProxy(new JDAEventManager(discordSRV), discordSRV.scheduler().forkJoinPool()));
|
||||
|
||||
|
@ -1,24 +1,5 @@
|
||||
/*
|
||||
* This file is part of DiscordSRV, licensed under the GPLv3 License
|
||||
* Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.discordsrv.common.groupsync;
|
||||
|
||||
import com.discordsrv.api.DiscordSRVApi;
|
||||
import com.discordsrv.api.discord.entity.guild.DiscordRole;
|
||||
import com.discordsrv.api.event.bus.Subscribe;
|
||||
import com.discordsrv.api.event.events.discord.member.role.DiscordMemberRoleAddEvent;
|
||||
@ -31,50 +12,52 @@ 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.sync.enums.SyncDirection;
|
||||
import com.discordsrv.common.groupsync.enums.GroupSyncResult;
|
||||
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;
|
||||
import com.discordsrv.common.profile.Profile;
|
||||
import com.discordsrv.common.sync.AbstractSyncModule;
|
||||
import com.discordsrv.common.sync.ISyncResult;
|
||||
import com.discordsrv.common.sync.SyncFail;
|
||||
import com.discordsrv.common.sync.enums.SyncResults;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class GroupSyncModule extends AbstractModule<DiscordSRV> {
|
||||
|
||||
private final Map<GroupSyncConfig.PairConfig, Future<?>> pairs = new LinkedHashMap<>();
|
||||
private final Map<String, List<GroupSyncConfig.PairConfig>> groupsToPairs = new ConcurrentHashMap<>();
|
||||
private final Map<Long, List<GroupSyncConfig.PairConfig>> rolesToPairs = new ConcurrentHashMap<>();
|
||||
public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long, GroupSyncConfig.PairConfig, Boolean> {
|
||||
|
||||
private final Cache<Long, Map<Long, Boolean>> expectedDiscordChanges;
|
||||
private final Cache<UUID, Map<String, Boolean>> expectedMinecraftChanges;
|
||||
|
||||
public GroupSyncModule(DiscordSRV discordSRV) {
|
||||
super(discordSRV, new NamedLogger(discordSRV, "GROUP_SYNC"));
|
||||
super(discordSRV, "GROUP_SYNC");
|
||||
|
||||
this.expectedDiscordChanges = discordSRV.caffeineBuilder()
|
||||
.expireAfterWrite(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
this.expectedMinecraftChanges = discordSRV.caffeineBuilder()
|
||||
.expireAfterWrite(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupSyncConfig.PairConfig> configs() {
|
||||
return discordSRV.config().groupSync.pairs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isTrue(Boolean state) {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
boolean any = false;
|
||||
for (GroupSyncConfig.PairConfig pair : discordSRV.config().groupSync.pairs) {
|
||||
if (pair.roleId != 0 && StringUtils.isNotEmpty(pair.groupName)) {
|
||||
if (pair.isSet()) {
|
||||
any = true;
|
||||
break;
|
||||
}
|
||||
@ -86,67 +69,11 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
|
||||
return super.isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Consumer<DiscordSRVApi.ReloadResult> resultConsumer) {
|
||||
synchronized (pairs) {
|
||||
pairs.values().forEach(future -> {
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
});
|
||||
pairs.clear();
|
||||
groupsToPairs.clear();
|
||||
rolesToPairs.clear();
|
||||
|
||||
GroupSyncConfig config = discordSRV.config().groupSync;
|
||||
for (GroupSyncConfig.PairConfig pair : config.pairs) {
|
||||
String groupName = pair.groupName;
|
||||
long roleId = pair.roleId;
|
||||
if (StringUtils.isEmpty(groupName) || roleId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!pair.validate(discordSRV)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean failed = false;
|
||||
for (GroupSyncConfig.PairConfig pairConfig : config.pairs) {
|
||||
if (pairConfig != pair && pair.isTheSameAs(pairConfig)) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (failed) {
|
||||
discordSRV.logger().error("Duplicate group synchronization pair: " + groupName + " to " + roleId);
|
||||
continue;
|
||||
}
|
||||
|
||||
Future<?> future = null;
|
||||
GroupSyncConfig.PairConfig.TimerConfig timer = pair.timer;
|
||||
if (timer != null && timer.enabled) {
|
||||
int cycleTime = timer.cycleTime;
|
||||
future = discordSRV.scheduler().runAtFixedRate(
|
||||
() -> resyncPair(pair, GroupSyncCause.TIMER),
|
||||
Duration.ofMinutes(cycleTime),
|
||||
Duration.ofMinutes(cycleTime)
|
||||
);
|
||||
}
|
||||
|
||||
pairs.put(pair, future);
|
||||
groupsToPairs.computeIfAbsent(groupName, key -> new ArrayList<>()).add(pair);
|
||||
rolesToPairs.computeIfAbsent(roleId, key -> new ArrayList<>()).add(pair);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug
|
||||
|
||||
@Subscribe
|
||||
public void onDebugGenerate(DebugGenerateEvent event) {
|
||||
StringBuilder builder = new StringBuilder("Active pairs:");
|
||||
|
||||
for (Map.Entry<GroupSyncConfig.PairConfig, Future<?>> entry : pairs.entrySet()) {
|
||||
for (Map.Entry<GroupSyncConfig.PairConfig, Future<?>> entry : syncs.entrySet()) {
|
||||
GroupSyncConfig.PairConfig pair = entry.getKey();
|
||||
builder.append("\n- ").append(pair)
|
||||
.append(" (tie-breaker: ").append(pair.tieBreaker)
|
||||
@ -171,13 +98,29 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
|
||||
}
|
||||
|
||||
private void logSummary(
|
||||
UUID player,
|
||||
UUID playerUUID,
|
||||
GroupSyncCause cause,
|
||||
Map<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> pairs
|
||||
CompletableFuture<Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>>> future
|
||||
) {
|
||||
future.whenComplete((result, t) -> {
|
||||
if (t != null) {
|
||||
// TODO: "not linked" doesn't need to be a error/ other errors that don't need to be errors?
|
||||
logger().error("Failed to sync groups (" + cause + ") for " + playerUUID, t);
|
||||
return;
|
||||
}
|
||||
|
||||
logSummary(playerUUID, cause, result);
|
||||
});
|
||||
}
|
||||
|
||||
private void logSummary(
|
||||
UUID playerUUID,
|
||||
GroupSyncCause cause,
|
||||
Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>> pairs
|
||||
) {
|
||||
CompletableFutureUtil.combine(pairs.values()).whenComplete((v, t) -> {
|
||||
GroupSyncSummary summary = new GroupSyncSummary(player, cause);
|
||||
for (Map.Entry<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> entry : pairs.entrySet()) {
|
||||
GroupSyncSummary summary = new GroupSyncSummary(playerUUID, cause);
|
||||
for (Map.Entry<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>> entry : pairs.entrySet()) {
|
||||
summary.add(entry.getKey(), entry.getValue().join());
|
||||
}
|
||||
|
||||
@ -191,507 +134,245 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
|
||||
});
|
||||
}
|
||||
|
||||
// Linked account helper methods
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Permission data helper methods
|
||||
|
||||
private PermissionModule.Groups getPermissionProvider() {
|
||||
PermissionModule.GroupsContext groupsContext = discordSRV.getModule(PermissionModule.GroupsContext.class);
|
||||
return groupsContext == null ? discordSRV.getModule(PermissionModule.Groups.class) : groupsContext;
|
||||
}
|
||||
|
||||
public boolean noPermissionProvider() {
|
||||
PermissionModule.Groups groups = getPermissionProvider();
|
||||
return groups == null || !groups.isEnabled();
|
||||
}
|
||||
|
||||
private boolean supportsOffline() {
|
||||
return getPermissionProvider().supportsOffline();
|
||||
}
|
||||
|
||||
private CompletableFuture<Boolean> hasGroup(
|
||||
UUID player,
|
||||
String groupName,
|
||||
@Nullable String serverContext
|
||||
) {
|
||||
PermissionModule.Groups permissionProvider = getPermissionProvider();
|
||||
if (permissionProvider instanceof PermissionModule.GroupsContext) {
|
||||
return ((PermissionModule.GroupsContext) permissionProvider)
|
||||
.hasGroup(player, groupName, false, serverContext != null ? Collections.singleton(serverContext) : null);
|
||||
} else {
|
||||
return permissionProvider.hasGroup(player, groupName, false);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> addGroup(
|
||||
UUID player,
|
||||
String groupName,
|
||||
@Nullable String serverContext
|
||||
) {
|
||||
PermissionModule.Groups permissionProvider = getPermissionProvider();
|
||||
if (permissionProvider instanceof PermissionModule.GroupsContext) {
|
||||
return ((PermissionModule.GroupsContext) permissionProvider)
|
||||
.addGroup(player, groupName, Collections.singleton(serverContext));
|
||||
} else {
|
||||
return permissionProvider.addGroup(player, groupName);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> removeGroup(
|
||||
UUID player,
|
||||
String groupName,
|
||||
@Nullable String serverContext
|
||||
) {
|
||||
PermissionModule.Groups permissionProvider = getPermissionProvider();
|
||||
if (permissionProvider instanceof PermissionModule.GroupsContext) {
|
||||
return ((PermissionModule.GroupsContext) permissionProvider)
|
||||
.removeGroup(player, groupName, Collections.singleton(serverContext));
|
||||
} else {
|
||||
return permissionProvider.removeGroup(player, groupName);
|
||||
}
|
||||
}
|
||||
|
||||
// Resync user
|
||||
|
||||
public CompletableFuture<List<GroupSyncResult>> resync(UUID player, GroupSyncCause cause) {
|
||||
return lookupLinkedAccount(player).thenCompose(userId -> {
|
||||
if (userId == null) {
|
||||
return CompletableFuture.completedFuture(Collections.emptyList());
|
||||
}
|
||||
|
||||
return CompletableFutureUtil.combine(resync(player, userId, cause));
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<List<GroupSyncResult>> resync(long userId, GroupSyncCause cause) {
|
||||
return lookupLinkedAccount(userId).thenCompose(player -> {
|
||||
if (player == null) {
|
||||
return CompletableFuture.completedFuture(Collections.emptyList());
|
||||
}
|
||||
|
||||
return CompletableFutureUtil.combine(resync(player, userId, cause));
|
||||
});
|
||||
}
|
||||
|
||||
public Collection<CompletableFuture<GroupSyncResult>> resync(UUID player, long userId, GroupSyncCause cause) {
|
||||
if (noPermissionProvider()) {
|
||||
return Collections.singletonList(CompletableFuture.completedFuture(GroupSyncResult.NO_PERMISSION_PROVIDER));
|
||||
} else if (discordSRV.playerProvider().player(player) == null && !supportsOffline()) {
|
||||
return Collections.singletonList(CompletableFuture.completedFuture(GroupSyncResult.PERMISSION_PROVIDER_NO_OFFLINE_SUPPORT));
|
||||
}
|
||||
|
||||
Map<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> futures = new LinkedHashMap<>();
|
||||
for (GroupSyncConfig.PairConfig pair : pairs.keySet()) {
|
||||
futures.put(pair, resyncPair(pair, player, userId));
|
||||
}
|
||||
|
||||
logSummary(player, cause, futures);
|
||||
return futures.values();
|
||||
}
|
||||
|
||||
private void resyncPair(GroupSyncConfig.PairConfig pair, GroupSyncCause cause) {
|
||||
if (noPermissionProvider()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (IPlayer player : discordSRV.playerProvider().allPlayers()) {
|
||||
UUID uuid = player.uniqueId();
|
||||
lookupLinkedAccount(uuid).whenComplete((userId, t) -> {
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
resyncPair(pair, uuid, userId).whenComplete((result, t2) -> logger().debug(
|
||||
new GroupSyncSummary(uuid, cause, pair, result).toString()
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<GroupSyncResult> resyncPair(GroupSyncConfig.PairConfig pair, UUID player, long userId) {
|
||||
DiscordRole role = discordSRV.discordAPI().getRoleById(pair.roleId);
|
||||
if (role == null) {
|
||||
return CompletableFuture.completedFuture(GroupSyncResult.ROLE_DOESNT_EXIST);
|
||||
}
|
||||
|
||||
if (!role.getGuild().getSelfMember().canInteract(role)) {
|
||||
return CompletableFuture.completedFuture(GroupSyncResult.ROLE_CANNOT_INTERACT);
|
||||
}
|
||||
|
||||
return role.getGuild().retrieveMemberById(userId).thenCompose(member -> {
|
||||
if (member == null) {
|
||||
return CompletableFuture.completedFuture(GroupSyncResult.NOT_A_GUILD_MEMBER);
|
||||
}
|
||||
|
||||
boolean hasRole = member.hasRole(role);
|
||||
String groupName = pair.groupName;
|
||||
CompletableFuture<GroupSyncResult> resultFuture = new CompletableFuture<>();
|
||||
|
||||
hasGroup(player, groupName, pair.serverContext).whenComplete((hasGroup, t) -> {
|
||||
if (t != null) {
|
||||
discordSRV.logger().error("Failed to check if player " + player + " has group " + groupName, t);
|
||||
resultFuture.complete(GroupSyncResult.PERMISSION_BACKEND_FAIL_CHECK);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasRole == hasGroup) {
|
||||
resultFuture.complete(hasRole ? GroupSyncResult.BOTH_TRUE : GroupSyncResult.BOTH_FALSE);
|
||||
// We're all good
|
||||
return;
|
||||
}
|
||||
|
||||
SyncSide side = pair.tieBreaker;
|
||||
SyncDirection direction = pair.direction;
|
||||
CompletableFuture<Void> future;
|
||||
GroupSyncResult result;
|
||||
if (hasRole) {
|
||||
if (side == SyncSide.DISCORD) {
|
||||
// Has role, add group
|
||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
||||
resultFuture.complete(GroupSyncResult.WRONG_DIRECTION);
|
||||
return;
|
||||
}
|
||||
|
||||
result = GroupSyncResult.ADD_GROUP;
|
||||
future = addGroup(player, groupName, pair.serverContext);
|
||||
} else {
|
||||
// Doesn't have group, remove role
|
||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||
resultFuture.complete(GroupSyncResult.WRONG_DIRECTION);
|
||||
return;
|
||||
}
|
||||
|
||||
result = GroupSyncResult.REMOVE_ROLE;
|
||||
future = member.removeRole(role);
|
||||
}
|
||||
} else {
|
||||
if (side == SyncSide.DISCORD) {
|
||||
// Doesn't have role, remove group
|
||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
||||
resultFuture.complete(GroupSyncResult.WRONG_DIRECTION);
|
||||
return;
|
||||
}
|
||||
|
||||
result = GroupSyncResult.REMOVE_GROUP;
|
||||
future = removeGroup(player, groupName, pair.serverContext);
|
||||
} else {
|
||||
// Has group, add role
|
||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||
resultFuture.complete(GroupSyncResult.WRONG_DIRECTION);
|
||||
return;
|
||||
}
|
||||
|
||||
result = GroupSyncResult.ADD_ROLE;
|
||||
future = member.addRole(role);
|
||||
}
|
||||
}
|
||||
future.whenComplete((v, t2) -> {
|
||||
if (t2 != null) {
|
||||
discordSRV.logger().error("Failed to " + result + " to " + player + "/" + Long.toUnsignedString(userId), t2);
|
||||
resultFuture.complete(GroupSyncResult.UPDATE_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
resultFuture.complete(result);
|
||||
});
|
||||
});
|
||||
|
||||
return resultFuture;
|
||||
});
|
||||
}
|
||||
|
||||
// Listeners & methods to indicate something changed
|
||||
|
||||
@Subscribe
|
||||
public void onPlayerConnected(PlayerConnectedEvent event) {
|
||||
resync(event.player().uniqueId(), GroupSyncCause.GAME_JOIN);
|
||||
UUID playerUUID = event.player().uniqueId();
|
||||
logSummary(playerUUID, GroupSyncCause.GAME_JOIN, resyncAll(playerUUID));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onDiscordMemberRoleAdd(DiscordMemberRoleAddEvent event) {
|
||||
event.getRoles().forEach(role -> roleChanged(event.getMember().getUser().getId(), role.getId(), false));
|
||||
event.getRoles().forEach(role -> roleChanged(event.getMember().getUser().getId(), role.getId(), true));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onDiscordMemberRoleRemove(DiscordMemberRoleRemoveEvent event) {
|
||||
event.getRoles().forEach(role -> roleChanged(event.getMember().getUser().getId(), role.getId(), true));
|
||||
event.getRoles().forEach(role -> roleChanged(event.getMember().getUser().getId(), role.getId(), false));
|
||||
}
|
||||
|
||||
public void groupAdded(UUID player, String groupName, @Nullable Set<String> serverContext, GroupSyncCause cause) {
|
||||
groupChanged(player, groupName, serverContext, cause, false);
|
||||
}
|
||||
|
||||
public void groupRemoved(UUID player, String groupName, @Nullable Set<String> serverContext, GroupSyncCause cause) {
|
||||
groupChanged(player, groupName, serverContext, cause, true);
|
||||
}
|
||||
|
||||
// Internal handling of changes
|
||||
public void groupRemoved(UUID player, String groupName, @Nullable Set<String> serverContext, GroupSyncCause cause) {
|
||||
groupChanged(player, groupName, serverContext, cause, false);
|
||||
}
|
||||
|
||||
private <T, R> boolean checkExpectation(Cache<T, Map<R, Boolean>> expectations, T key, R mapKey, boolean remove) {
|
||||
private void roleChanged(long userId, long roleId, boolean state) {
|
||||
if (checkExpectation(expectedDiscordChanges, userId, roleId, state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PermissionModule.Groups permissionProvider = getPermissionProvider();
|
||||
if (permissionProvider == null) {
|
||||
logger().debug("No permission provider");
|
||||
return;
|
||||
}
|
||||
|
||||
lookupLinkedAccount(userId).whenComplete((playerUUID, t) -> {
|
||||
if (playerUUID == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!permissionProvider.supportsOffline() && discordSRV.playerProvider().player(playerUUID) == null) {
|
||||
logger().debug("Not running sync for " + playerUUID + ": permission provider does not support offline operations");
|
||||
return;
|
||||
}
|
||||
|
||||
logSummary(playerUUID, GroupSyncCause.DISCORD_ROLE_CHANGE, discordChanged(userId, playerUUID, roleId, state));
|
||||
});
|
||||
}
|
||||
|
||||
private void groupChanged(
|
||||
UUID playerUUID,
|
||||
String groupName,
|
||||
Set<String> serverContext,
|
||||
GroupSyncCause cause,
|
||||
Boolean state
|
||||
) {
|
||||
if (cause.isDiscordSRVCanCause() && checkExpectation(expectedMinecraftChanges, playerUUID, groupName, state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PermissionModule.Groups permissionProvider = getPermissionProvider();
|
||||
if (permissionProvider == null) {
|
||||
logger().debug("No permission provider");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!permissionProvider.supportsOffline() && discordSRV.playerProvider().player(playerUUID) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
lookupLinkedAccount(playerUUID).whenComplete((userId, t) -> {
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
logSummary(playerUUID, cause, gameChanged(userId, playerUUID, context(groupName, serverContext), state));
|
||||
});
|
||||
}
|
||||
|
||||
private PermissionModule.Groups getPermissionProvider() {
|
||||
PermissionModule.GroupsContext groupsContext = discordSRV.getModule(PermissionModule.GroupsContext.class);
|
||||
return groupsContext != null ? groupsContext : discordSRV.getModule(PermissionModule.Groups.class);
|
||||
}
|
||||
|
||||
public boolean noPermissionProvider() {
|
||||
return getPermissionProvider() != null;
|
||||
}
|
||||
|
||||
private <T, R> boolean checkExpectation(Cache<T, Map<R, Boolean>> expectations, T key, R mapKey, boolean newState) {
|
||||
// Check if we were expecting the change (when we add/remove something due to synchronization),
|
||||
// if we did expect the change, we won't trigger a synchronization since we just synchronized what was needed
|
||||
Map<R, Boolean> expected = expectations.getIfPresent(key);
|
||||
if (expected != null && Objects.equals(expected.get(mapKey), remove)) {
|
||||
if (expected != null && Objects.equals(expected.get(mapKey), newState)) {
|
||||
expected.remove(mapKey);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void roleChanged(long userId, long roleId, boolean remove) {
|
||||
if (noPermissionProvider()) {
|
||||
return;
|
||||
}
|
||||
// Resync
|
||||
|
||||
if (checkExpectation(expectedDiscordChanges, userId, roleId, remove)) {
|
||||
return;
|
||||
}
|
||||
|
||||
lookupLinkedAccount(userId).whenComplete((player, t) -> {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
roleChanged(userId, player, roleId, remove);
|
||||
});
|
||||
}
|
||||
|
||||
private void roleChanged(long userId, UUID player, long roleId, boolean remove) {
|
||||
List<GroupSyncConfig.PairConfig> pairs = rolesToPairs.get(roleId);
|
||||
if (pairs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PermissionModule.Groups permissionProvider = getPermissionProvider();
|
||||
if (permissionProvider == null) {
|
||||
discordSRV.logger().warning("No supported permission plugin available to perform group sync");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> futures = new LinkedHashMap<>();
|
||||
for (GroupSyncConfig.PairConfig pair : pairs) {
|
||||
SyncDirection direction = pair.direction;
|
||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
||||
// Not going Discord -> Minecraft
|
||||
futures.put(pair, CompletableFuture.completedFuture(GroupSyncResult.WRONG_DIRECTION));
|
||||
continue;
|
||||
}
|
||||
|
||||
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 == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<GroupSyncConfig.PairConfig> groupPairs = groupsToPairs.get(pair.groupName);
|
||||
if (groupPairs == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (GroupSyncConfig.PairConfig groupPair : groupPairs) {
|
||||
if (groupPair.roleId == roleId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
futures.put(groupPair, modifyRoleState(userId, groupPair, remove));
|
||||
}
|
||||
}
|
||||
logSummary(player, GroupSyncCause.DISCORD_ROLE_CHANGE, futures);
|
||||
}
|
||||
|
||||
private void groupChanged(
|
||||
UUID player,
|
||||
String groupName,
|
||||
@Nullable Set<String> serverContext,
|
||||
GroupSyncCause cause,
|
||||
boolean remove
|
||||
) {
|
||||
if (noPermissionProvider()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cause.isDiscordSRVCanCause() && checkExpectation(expectedMinecraftChanges, player, groupName, remove)) {
|
||||
return;
|
||||
}
|
||||
|
||||
lookupLinkedAccount(player).whenComplete((userId, t) -> {
|
||||
@Override
|
||||
protected void resyncTimer(GroupSyncConfig.PairConfig config) {
|
||||
for (IPlayer player : discordSRV.playerProvider().allPlayers()) {
|
||||
UUID playerUUID = player.uniqueId();
|
||||
lookupLinkedAccount(playerUUID)
|
||||
.whenComplete((userId, t) -> {
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
groupChanged(player, userId, groupName, serverContext, cause, remove);
|
||||
logSummary(playerUUID, GroupSyncCause.TIMER, resync(config, playerUUID, userId).thenApply(result -> {
|
||||
Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>> map = new HashMap<>();
|
||||
map.put(config, CompletableFuture.completedFuture(result));
|
||||
return map;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private void groupChanged(
|
||||
UUID player,
|
||||
long userId,
|
||||
String groupName,
|
||||
@Nullable Set<String> serverContext,
|
||||
GroupSyncCause cause,
|
||||
boolean remove
|
||||
) {
|
||||
List<GroupSyncConfig.PairConfig> pairs = groupsToPairs.get(groupName);
|
||||
if (pairs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PermissionModule.Groups permissionProvider = getPermissionProvider();
|
||||
Map<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> futures = new LinkedHashMap<>();
|
||||
for (GroupSyncConfig.PairConfig pair : pairs) {
|
||||
SyncDirection direction = pair.direction;
|
||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||
// Not going Minecraft -> Discord
|
||||
futures.put(pair, CompletableFuture.completedFuture(GroupSyncResult.WRONG_DIRECTION));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we're in the right context
|
||||
String context = pair.serverContext;
|
||||
if (permissionProvider instanceof PermissionModule.GroupsContext) {
|
||||
if (StringUtils.isEmpty(context)) {
|
||||
// Use the default server context of the server
|
||||
Set<String> defaultValues = ((PermissionModule.GroupsContext) permissionProvider)
|
||||
.getDefaultServerContext();
|
||||
if (!Objects.equals(serverContext, defaultValues)) {
|
||||
continue;
|
||||
}
|
||||
} else if (context.equals("global")) {
|
||||
// No server context
|
||||
if (serverContext != null && !serverContext.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Server context has to match the specified
|
||||
if (serverContext == null
|
||||
|| serverContext.size() != 1
|
||||
|| !serverContext.iterator().next().equals(context)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 == SyncDirection.MINECRAFT_TO_DISCORD) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long roleId = pair.roleId;
|
||||
List<GroupSyncConfig.PairConfig> rolePairs = rolesToPairs.get(roleId);
|
||||
if (rolePairs == null || rolePairs.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (GroupSyncConfig.PairConfig rolePair : rolePairs) {
|
||||
if (rolePair.groupName.equals(groupName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
futures.put(rolePair, modifyGroupState(player, rolePair, remove));
|
||||
}
|
||||
}
|
||||
logSummary(player, cause, futures);
|
||||
}
|
||||
|
||||
private CompletableFuture<GroupSyncResult> modifyGroupState(UUID player, GroupSyncConfig.PairConfig config, boolean remove) {
|
||||
String groupName = config.groupName;
|
||||
|
||||
Map<String, Boolean> expected = expectedMinecraftChanges.get(player, key -> new ConcurrentHashMap<>());
|
||||
if (expected != null) {
|
||||
expected.put(groupName, remove);
|
||||
}
|
||||
|
||||
CompletableFuture<GroupSyncResult> future = new CompletableFuture<>();
|
||||
String serverContext = config.serverContext;
|
||||
hasGroup(player, groupName, serverContext).thenCompose(hasGroup -> {
|
||||
if (remove && hasGroup) {
|
||||
return removeGroup(player, groupName, serverContext).thenApply(v -> GroupSyncResult.REMOVE_GROUP);
|
||||
} else if (!remove && !hasGroup) {
|
||||
return addGroup(player, groupName, serverContext).thenApply(v -> GroupSyncResult.ADD_GROUP);
|
||||
} else {
|
||||
// Nothing to do
|
||||
return CompletableFuture.completedFuture(GroupSyncResult.ALREADY_IN_SYNC);
|
||||
}
|
||||
}).whenComplete((result, t) -> {
|
||||
if (t != null) {
|
||||
if (expected != null) {
|
||||
// Failed, remove expectation
|
||||
expected.remove(groupName);
|
||||
}
|
||||
|
||||
future.complete(GroupSyncResult.UPDATE_FAILED);
|
||||
discordSRV.logger().error("Failed to add group " + groupName + " to " + player, t);
|
||||
return;
|
||||
}
|
||||
|
||||
future.complete(result);
|
||||
});
|
||||
public CompletableFuture<Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>>> resync(UUID playerUUID, GroupSyncCause cause) {
|
||||
CompletableFuture<Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>>> future = resyncAll(playerUUID);
|
||||
logSummary(playerUUID, cause, future);
|
||||
return future;
|
||||
}
|
||||
|
||||
private CompletableFuture<GroupSyncResult> modifyRoleState(long userId, GroupSyncConfig.PairConfig config, boolean remove) {
|
||||
long roleId = config.roleId;
|
||||
DiscordRole role = discordSRV.discordAPI().getRoleById(roleId);
|
||||
@Override
|
||||
public CompletableFuture<Boolean> getDiscord(GroupSyncConfig.PairConfig config, long userId) {
|
||||
DiscordRole role = discordSRV.discordAPI().getRoleById(config.roleId);
|
||||
if (role == null) {
|
||||
return CompletableFuture.completedFuture(GroupSyncResult.ROLE_DOESNT_EXIST);
|
||||
return CompletableFutureUtil.failed(new SyncFail(GroupSyncResult.ROLE_DOESNT_EXIST));
|
||||
}
|
||||
|
||||
if (!role.getGuild().getSelfMember().canInteract(role)) {
|
||||
return CompletableFuture.completedFuture(GroupSyncResult.ROLE_CANNOT_INTERACT);
|
||||
return CompletableFutureUtil.failed(new SyncFail(GroupSyncResult.ROLE_CANNOT_INTERACT));
|
||||
}
|
||||
|
||||
return role.getGuild().retrieveMemberById(userId).thenCompose(member -> {
|
||||
return role.getGuild().retrieveMemberById(userId).thenApply(member -> {
|
||||
if (member == null) {
|
||||
return CompletableFuture.completedFuture(GroupSyncResult.NOT_A_GUILD_MEMBER);
|
||||
throw new SyncFail(GroupSyncResult.NOT_A_GUILD_MEMBER);
|
||||
}
|
||||
|
||||
return member.hasRole(role);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> getGame(GroupSyncConfig.PairConfig config, UUID playerUUID) {
|
||||
PermissionModule.Groups permissionProvider = getPermissionProvider();
|
||||
CompletableFuture<Boolean> future;
|
||||
if (permissionProvider instanceof PermissionModule.GroupsContext) {
|
||||
future = ((PermissionModule.GroupsContext) permissionProvider)
|
||||
.hasGroup(playerUUID, config.groupName, false, config.serverContext != null ? Collections.singleton(config.serverContext) : null);
|
||||
} else {
|
||||
future = permissionProvider.hasGroup(playerUUID, config.groupName, false);
|
||||
}
|
||||
|
||||
return future.exceptionally(t -> {
|
||||
throw new SyncFail(GroupSyncResult.PERMISSION_BACKEND_FAILED, t);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ISyncResult> applyDiscord(GroupSyncConfig.PairConfig config, long userId, Boolean state) {
|
||||
DiscordRole role = discordSRV.discordAPI().getRoleById(config.roleId);
|
||||
if (role == null) {
|
||||
return CompletableFutureUtil.failed(new SyncFail(GroupSyncResult.ROLE_DOESNT_EXIST));
|
||||
}
|
||||
|
||||
Map<Long, Boolean> expected = expectedDiscordChanges.get(userId, key -> new ConcurrentHashMap<>());
|
||||
if (expected != null) {
|
||||
expected.put(roleId, remove);
|
||||
expected.put(config.roleId, state);
|
||||
}
|
||||
|
||||
boolean hasRole = member.hasRole(role);
|
||||
CompletableFuture<GroupSyncResult> future;
|
||||
if (remove && hasRole) {
|
||||
future = member.removeRole(role).thenApply(v -> GroupSyncResult.REMOVE_ROLE);
|
||||
} else if (!remove && !hasRole) {
|
||||
future = member.addRole(role).thenApply(v -> GroupSyncResult.ADD_ROLE);
|
||||
} else {
|
||||
if (expected != null) {
|
||||
// Nothing needed to be changed, remove expectation
|
||||
expected.remove(roleId);
|
||||
}
|
||||
return CompletableFuture.completedFuture(GroupSyncResult.ALREADY_IN_SYNC);
|
||||
}
|
||||
|
||||
CompletableFuture<GroupSyncResult> resultFuture = new CompletableFuture<>();
|
||||
future.whenComplete((result, t) -> {
|
||||
return role.getGuild().retrieveMemberById(userId)
|
||||
.thenCompose(member -> state
|
||||
? member.addRole(role).thenApply(v -> (ISyncResult) SyncResults.ADD_DISCORD)
|
||||
: member.removeRole(role).thenApply(v -> SyncResults.REMOVE_DISCORD)
|
||||
).whenComplete((r, t) -> {
|
||||
if (t != null) {
|
||||
if (expected != null) {
|
||||
// Failed, remove expectation
|
||||
expected.remove(roleId);
|
||||
//noinspection DataFlowIssue
|
||||
expected.remove(config.roleId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resultFuture.complete(GroupSyncResult.UPDATE_FAILED);
|
||||
discordSRV.logger().error("Failed to give/take role " + role + " to/from " + member, t);
|
||||
return;
|
||||
@Override
|
||||
public CompletableFuture<ISyncResult> applyGame(GroupSyncConfig.PairConfig config, UUID playerUUID, Boolean state) {
|
||||
Map<String, Boolean> expected = expectedMinecraftChanges.get(playerUUID, key -> new ConcurrentHashMap<>());
|
||||
if (expected != null) {
|
||||
expected.put(config.groupName, state);
|
||||
}
|
||||
resultFuture.complete(result);
|
||||
});
|
||||
return resultFuture;
|
||||
|
||||
CompletableFuture<ISyncResult> future =
|
||||
state
|
||||
? addGroup(playerUUID, config).thenApply(v -> SyncResults.ADD_GAME)
|
||||
: removeGroup(playerUUID, config).thenApply(v -> SyncResults.REMOVE_GAME);
|
||||
return future.exceptionally(t -> {
|
||||
//noinspection DataFlowIssue
|
||||
expected.remove(config.groupName);
|
||||
throw new SyncFail(GroupSyncResult.PERMISSION_BACKEND_FAILED, t);
|
||||
});
|
||||
}
|
||||
|
||||
private Set<String> context(GroupSyncConfig.PairConfig config) {
|
||||
return config.serverContext != null ? Collections.singleton(config.serverContext) : null;
|
||||
}
|
||||
|
||||
private String context(String groupName, Set<String> serverContext) {
|
||||
if (serverContext == null || serverContext.isEmpty()) {
|
||||
return GroupSyncConfig.PairConfig.makeGameId(groupName, Collections.singleton("global"));
|
||||
}
|
||||
if (serverContext.size() == 1 && serverContext.iterator().next().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return GroupSyncConfig.PairConfig.makeGameId(groupName, serverContext);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> addGroup(UUID player, GroupSyncConfig.PairConfig config) {
|
||||
PermissionModule.Groups permissionProvider = getPermissionProvider();
|
||||
String groupName = config.groupName;
|
||||
if (permissionProvider instanceof PermissionModule.GroupsContext) {
|
||||
return ((PermissionModule.GroupsContext) permissionProvider)
|
||||
.addGroup(player, groupName, context(config));
|
||||
} else {
|
||||
return permissionProvider.addGroup(player, groupName);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> removeGroup(UUID player, GroupSyncConfig.PairConfig config) {
|
||||
PermissionModule.Groups permissionProvider = getPermissionProvider();
|
||||
String groupName = config.groupName;
|
||||
if (permissionProvider instanceof PermissionModule.GroupsContext) {
|
||||
return ((PermissionModule.GroupsContext) permissionProvider)
|
||||
.removeGroup(player, groupName, context(config));
|
||||
} else {
|
||||
return permissionProvider.removeGroup(player, groupName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,17 +20,17 @@ package com.discordsrv.common.groupsync;
|
||||
|
||||
import com.discordsrv.common.config.main.GroupSyncConfig;
|
||||
import com.discordsrv.common.groupsync.enums.GroupSyncCause;
|
||||
import com.discordsrv.common.groupsync.enums.GroupSyncResult;
|
||||
import com.discordsrv.common.sync.ISyncResult;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class GroupSyncSummary {
|
||||
|
||||
private final EnumMap<GroupSyncResult, Set<GroupSyncConfig.PairConfig>> pairs = new EnumMap<>(GroupSyncResult.class);
|
||||
private final Map<ISyncResult, Set<GroupSyncConfig.PairConfig>> pairs = new HashMap<>();
|
||||
private final UUID player;
|
||||
private final GroupSyncCause cause;
|
||||
|
||||
public GroupSyncSummary(UUID player, GroupSyncCause cause, GroupSyncConfig.PairConfig config, GroupSyncResult result) {
|
||||
public GroupSyncSummary(UUID player, GroupSyncCause cause, GroupSyncConfig.PairConfig config, ISyncResult result) {
|
||||
this(player, cause);
|
||||
add(config, result);
|
||||
}
|
||||
@ -40,12 +40,12 @@ public class GroupSyncSummary {
|
||||
this.cause = cause;
|
||||
}
|
||||
|
||||
public void add(GroupSyncConfig.PairConfig config, GroupSyncResult result) {
|
||||
public void add(GroupSyncConfig.PairConfig config, ISyncResult result) {
|
||||
pairs.computeIfAbsent(result, key -> new LinkedHashSet<>()).add(config);
|
||||
}
|
||||
|
||||
public boolean anySuccess() {
|
||||
for (GroupSyncResult result : pairs.keySet()) {
|
||||
for (ISyncResult result : pairs.keySet()) {
|
||||
if (result.isSuccess()) {
|
||||
return true;
|
||||
}
|
||||
@ -59,7 +59,7 @@ public class GroupSyncSummary {
|
||||
StringBuilder message = new StringBuilder(
|
||||
"Group synchronization (of " + count + " pair" + (count == 1 ? "" : "s") + ") for " + player + " (" + cause + ")");
|
||||
|
||||
for (Map.Entry<GroupSyncResult, Set<GroupSyncConfig.PairConfig>> entry : pairs.entrySet()) {
|
||||
for (Map.Entry<ISyncResult, Set<GroupSyncConfig.PairConfig>> entry : pairs.entrySet()) {
|
||||
message.append(count == 1 ? ": " : "\n")
|
||||
.append(entry.getKey().toString())
|
||||
.append(": ")
|
||||
|
@ -18,25 +18,15 @@
|
||||
|
||||
package com.discordsrv.common.groupsync.enums;
|
||||
|
||||
public enum GroupSyncResult {
|
||||
import com.discordsrv.common.sync.ISyncResult;
|
||||
|
||||
// Something happened
|
||||
ADD_GROUP("Success (group add)", true),
|
||||
REMOVE_GROUP("Success (group remove)", true),
|
||||
ADD_ROLE("Success (role add)", true),
|
||||
REMOVE_ROLE("Success (role remove)", true),
|
||||
|
||||
// Nothing done
|
||||
ALREADY_IN_SYNC("Already in sync"),
|
||||
WRONG_DIRECTION("Wrong direction"),
|
||||
BOTH_TRUE("Both sides true"),
|
||||
BOTH_FALSE("Both sides false"),
|
||||
public enum GroupSyncResult implements ISyncResult {
|
||||
|
||||
// Errors
|
||||
ROLE_DOESNT_EXIST("Role doesn't exist"),
|
||||
ROLE_CANNOT_INTERACT("Bot doesn't have a role above the synced role (cannot interact)"),
|
||||
NOT_A_GUILD_MEMBER("User is not part of the server the role is in"),
|
||||
PERMISSION_BACKEND_FAIL_CHECK("Failed to check group status, error printed"),
|
||||
PERMISSION_BACKEND_FAILED("Failed to check group status, error printed"),
|
||||
UPDATE_FAILED("Failed to modify role/group, error printed"),
|
||||
NO_PERMISSION_PROVIDER("No permission provider"),
|
||||
PERMISSION_PROVIDER_NO_OFFLINE_SUPPORT("Permission provider doesn't support offline players"),
|
||||
|
@ -0,0 +1,296 @@
|
||||
package com.discordsrv.common.sync;
|
||||
|
||||
import com.discordsrv.api.DiscordSRVApi;
|
||||
import com.discordsrv.common.DiscordSRV;
|
||||
import com.discordsrv.common.config.main.GroupSyncConfig;
|
||||
import com.discordsrv.common.config.main.generic.AbstractSyncConfig;
|
||||
import com.discordsrv.common.future.util.CompletableFutureUtil;
|
||||
import com.discordsrv.common.logging.NamedLogger;
|
||||
import com.discordsrv.common.module.type.AbstractModule;
|
||||
import com.discordsrv.common.profile.Profile;
|
||||
import com.discordsrv.common.sync.enums.SyncDirection;
|
||||
import com.discordsrv.common.sync.enums.SyncResults;
|
||||
import com.discordsrv.common.sync.enums.SyncSide;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends AbstractSyncConfig<C, G, D>, S> extends AbstractModule<DT> {
|
||||
|
||||
protected final Map<C, Future<?>> syncs = new LinkedHashMap<>();
|
||||
private final Map<G, List<C>> configsForGame = new ConcurrentHashMap<>();
|
||||
private final Map<D, List<C>> configsForDiscord = new ConcurrentHashMap<>();
|
||||
|
||||
public AbstractSyncModule(DT discordSRV, String loggerName) {
|
||||
super(discordSRV, new NamedLogger(discordSRV, loggerName));
|
||||
}
|
||||
|
||||
public abstract List<C> configs();
|
||||
|
||||
@Override
|
||||
public void reload(Consumer<DiscordSRVApi.ReloadResult> resultConsumer) {
|
||||
synchronized (syncs) {
|
||||
syncs.values().forEach(future -> {
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
});
|
||||
syncs.clear();
|
||||
configsForGame.clear();
|
||||
configsForDiscord.clear();
|
||||
|
||||
for (C config : configs()) {
|
||||
if (!config.isSet() || !config.validate(discordSRV)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean failed = false;
|
||||
for (C existingConfig : syncs.keySet()) {
|
||||
if (existingConfig.isSameAs(config)) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (failed) {
|
||||
discordSRV.logger().error("Duplicate " + config.describe());
|
||||
continue;
|
||||
}
|
||||
|
||||
Future<?> future = null;
|
||||
GroupSyncConfig.PairConfig.TimerConfig timer = config.timer;
|
||||
if (timer != null && timer.enabled) {
|
||||
int cycleTime = timer.cycleTime;
|
||||
future = discordSRV.scheduler().runAtFixedRate(
|
||||
() -> resyncTimer(config),
|
||||
Duration.ofMinutes(cycleTime),
|
||||
Duration.ofMinutes(cycleTime)
|
||||
);
|
||||
}
|
||||
|
||||
syncs.put(config, future);
|
||||
|
||||
G game = config.gameId();
|
||||
if (game != null) {
|
||||
configsForGame.computeIfAbsent(game, key -> new ArrayList<>()).add(config);
|
||||
}
|
||||
|
||||
D discord = config.discordId();
|
||||
if (discord != null) {
|
||||
configsForDiscord.computeIfAbsent(discord, key -> new ArrayList<>()).add(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected CompletableFuture<Long> lookupLinkedAccount(UUID player) {
|
||||
return discordSRV.profileManager().lookupProfile(player)
|
||||
.thenApply(Profile::userId)
|
||||
.thenApply(userId -> {
|
||||
if (userId == null) {
|
||||
throw new SyncFail(SyncResults.NOT_LINKED);
|
||||
}
|
||||
return userId;
|
||||
});
|
||||
}
|
||||
|
||||
protected CompletableFuture<UUID> lookupLinkedAccount(long userId) {
|
||||
return discordSRV.profileManager().lookupProfile(userId)
|
||||
.thenApply(Profile::playerUUID)
|
||||
.thenApply(playerUUID -> {
|
||||
if (playerUUID == null) {
|
||||
throw new SyncFail(SyncResults.NOT_LINKED);
|
||||
}
|
||||
return playerUUID;
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract boolean isTrue(S state);
|
||||
|
||||
protected abstract CompletableFuture<S> getDiscord(C config, long userId);
|
||||
protected abstract CompletableFuture<S> getGame(C config, UUID playerUUID);
|
||||
|
||||
protected abstract CompletableFuture<ISyncResult> applyDiscord(C config, long userId, S state);
|
||||
protected abstract CompletableFuture<ISyncResult> applyGame(C config, UUID playerUUID, S state);
|
||||
|
||||
protected CompletableFuture<ISyncResult> applyDiscordIfNot(C config, long userId, S state) {
|
||||
return getDiscord(config, userId).thenCompose(value -> {
|
||||
boolean actualValue;
|
||||
if ((actualValue = isTrue(state)) == isTrue(value)) {
|
||||
return CompletableFuture.completedFuture(actualValue ? SyncResults.BOTH_TRUE : SyncResults.BOTH_FALSE);
|
||||
} else {
|
||||
return applyDiscord(config, userId, state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected CompletableFuture<ISyncResult> applyGameIfNot(C config, UUID playerUUID, S state) {
|
||||
return getGame(config, playerUUID).thenCompose(value -> {
|
||||
boolean actualValue;
|
||||
if ((actualValue = isTrue(state)) == isTrue(value)) {
|
||||
return CompletableFuture.completedFuture(actualValue ? SyncResults.BOTH_TRUE : SyncResults.BOTH_FALSE);
|
||||
} else {
|
||||
return applyGame(config, playerUUID, state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected Map<C, CompletableFuture<ISyncResult>> discordChanged(long userId, UUID playerUUID, D discordId, S state) {
|
||||
List<C> gameConfigs = configsForDiscord.get(discordId);
|
||||
if (gameConfigs == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<C, CompletableFuture<ISyncResult>> futures = new LinkedHashMap<>();
|
||||
for (C config : gameConfigs) {
|
||||
SyncDirection direction = config.direction;
|
||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
||||
// Not going Discord -> Minecraft
|
||||
futures.put(config, CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION));
|
||||
continue;
|
||||
}
|
||||
|
||||
futures.put(config, applyGameIfNot(config, playerUUID, state));
|
||||
|
||||
// If the sync is bidirectional, also sync anything else linked to the same Minecraft id
|
||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<C> discordConfigs = configsForGame.get(config.gameId());
|
||||
if (discordConfigs == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (C gameConfig : discordConfigs) {
|
||||
if (gameConfig.discordId() == discordId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
futures.put(gameConfig, applyDiscordIfNot(gameConfig, userId, state));
|
||||
}
|
||||
}
|
||||
return futures;
|
||||
}
|
||||
|
||||
protected Map<C, CompletableFuture<ISyncResult>> gameChanged(long userId, UUID playerUUID, G gameId, S state) {
|
||||
List<C> discordConfigs = configsForGame.get(gameId);
|
||||
if (discordConfigs == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<C, CompletableFuture<ISyncResult>> futures = new LinkedHashMap<>();
|
||||
for (C config : discordConfigs) {
|
||||
SyncDirection direction = config.direction;
|
||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||
// Not going Minecraft -> Discord
|
||||
futures.put(config, CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION));
|
||||
continue;
|
||||
}
|
||||
|
||||
futures.put(config, applyDiscordIfNot(config, userId, state));
|
||||
|
||||
// If the sync is bidirectional, also sync anything else linked to the same Discord id
|
||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<C> gameConfigs = configsForDiscord.get(config.discordId());
|
||||
if (gameConfigs == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (C gameConfig : gameConfigs) {
|
||||
if (gameConfig.gameId() == gameId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
futures.put(gameConfig, applyGameIfNot(gameConfig, playerUUID, state));
|
||||
}
|
||||
}
|
||||
return futures;
|
||||
}
|
||||
|
||||
protected abstract void resyncTimer(C config);
|
||||
|
||||
protected CompletableFuture<Map<C, CompletableFuture<ISyncResult>>> resyncAll(UUID playerUUID) {
|
||||
return lookupLinkedAccount(playerUUID).thenApply(userId -> resyncAll(playerUUID, userId));
|
||||
}
|
||||
|
||||
protected CompletableFuture<Map<C, CompletableFuture<ISyncResult>>> resyncAll(long userId) {
|
||||
return lookupLinkedAccount(userId).thenApply(playerUUID -> resyncAll(playerUUID, userId));
|
||||
}
|
||||
|
||||
protected Map<C, CompletableFuture<ISyncResult>> resyncAll(UUID playerUUID, long userId) {
|
||||
List<C> configs = configs();
|
||||
|
||||
Map<C, CompletableFuture<ISyncResult>> results = new HashMap<>(configs.size());
|
||||
for (C config : configs) {
|
||||
results.put(config, resync(config, playerUUID, userId));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
protected CompletableFuture<ISyncResult> resync(C config, UUID playerUUID, long userId) {
|
||||
CompletableFuture<S> gameGet = getGame(config, playerUUID);
|
||||
CompletableFuture<S> discordGet = getDiscord(config, userId);
|
||||
|
||||
return CompletableFutureUtil.combine(gameGet, discordGet).thenCompose((__) -> {
|
||||
S gameState = gameGet.join();
|
||||
S discordState = discordGet.join();
|
||||
|
||||
boolean bothState;
|
||||
if ((bothState = (gameState != null)) == (discordState != null)) {
|
||||
// Already in sync
|
||||
return CompletableFuture.completedFuture((ISyncResult) (bothState ? SyncResults.BOTH_TRUE : SyncResults.BOTH_FALSE));
|
||||
}
|
||||
|
||||
SyncSide side = config.tieBreaker;
|
||||
SyncDirection direction = config.direction;
|
||||
if (discordState != null) {
|
||||
if (side == SyncSide.DISCORD) {
|
||||
// Has Discord, add game
|
||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
||||
return CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION);
|
||||
}
|
||||
|
||||
return applyGame(config, playerUUID, discordState).thenApply(v -> SyncResults.ADD_GAME);
|
||||
} else {
|
||||
// Missing game, remove Discord
|
||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||
return CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION);
|
||||
}
|
||||
|
||||
return applyDiscord(config, userId, null).thenApply(v -> SyncResults.REMOVE_DISCORD);
|
||||
}
|
||||
} else {
|
||||
if (side == SyncSide.DISCORD) {
|
||||
// Missing Discord, remove game
|
||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
||||
return CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION);
|
||||
}
|
||||
|
||||
return applyGame(config, playerUUID, null).thenApply(v -> SyncResults.REMOVE_GAME);
|
||||
} else {
|
||||
// Has game, add Discord
|
||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||
return CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION);
|
||||
}
|
||||
|
||||
return applyDiscord(config, userId, gameState).thenApply(v -> SyncResults.ADD_DISCORD);
|
||||
}
|
||||
}
|
||||
}).exceptionally(t -> {
|
||||
if (t instanceof SyncFail) {
|
||||
return ((SyncFail) t).getResult();
|
||||
} else {
|
||||
throw (RuntimeException) t;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.discordsrv.common.sync;
|
||||
|
||||
public interface ISyncResult {
|
||||
|
||||
boolean isSuccess();
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.discordsrv.common.sync;
|
||||
|
||||
public class SyncFail extends RuntimeException {
|
||||
|
||||
private final ISyncResult result;
|
||||
|
||||
public SyncFail(ISyncResult result) {
|
||||
this(result, null);
|
||||
}
|
||||
|
||||
public SyncFail(ISyncResult result, Throwable cause) {
|
||||
super(cause);
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public ISyncResult getResult() {
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.discordsrv.common.sync.enums;
|
||||
|
||||
import com.discordsrv.common.sync.ISyncResult;
|
||||
|
||||
public enum SyncResults implements ISyncResult {
|
||||
|
||||
// Change made
|
||||
ADD_GAME("Add game"),
|
||||
REMOVE_GAME("Remove game"),
|
||||
ADD_DISCORD("Add Discord"),
|
||||
REMOVE_DISCORD("Remove Discord"),
|
||||
|
||||
// Nothing happened
|
||||
BOTH_TRUE("Both sides true"),
|
||||
BOTH_FALSE("Both sides false"),
|
||||
WRONG_DIRECTION("Wrong direction"),
|
||||
|
||||
NOT_LINKED("Accounts not linked"),
|
||||
|
||||
;
|
||||
|
||||
private final String prettyResult;
|
||||
private final boolean success;
|
||||
|
||||
SyncResults(String prettyResult) {
|
||||
this(prettyResult, true);
|
||||
}
|
||||
|
||||
SyncResults(String prettyResult, boolean success) {
|
||||
this.prettyResult = prettyResult;
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return prettyResult;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user