diff --git a/common/src/main/java/com/discordsrv/common/bansync/BanSyncModule.java b/common/src/main/java/com/discordsrv/common/bansync/BanSyncModule.java index a741000b..47c4d148 100644 --- a/common/src/main/java/com/discordsrv/common/bansync/BanSyncModule.java +++ b/common/src/main/java/com/discordsrv/common/bansync/BanSyncModule.java @@ -6,14 +6,18 @@ import com.discordsrv.api.event.events.linking.AccountLinkedEvent; import com.discordsrv.api.module.type.PunishmentModule; import com.discordsrv.api.punishment.Punishment; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.bansync.enums.BanSyncCause; 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.future.util.CompletableFutureUtil; import com.discordsrv.common.player.IPlayer; -import com.discordsrv.common.profile.Profile; +import com.discordsrv.common.someone.Someone; +import com.discordsrv.common.sync.AbstractSyncModule; +import com.discordsrv.common.sync.SyncFail; +import com.discordsrv.common.sync.cause.GenericSyncCauses; +import com.discordsrv.common.sync.result.ISyncResult; import com.discordsrv.common.sync.enums.SyncDirection; -import com.discordsrv.common.sync.enums.SyncSide; +import com.discordsrv.common.sync.result.GenericSyncResults; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.audit.ActionType; import net.dv8tion.jda.api.audit.AuditLogEntry; @@ -29,27 +33,24 @@ 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.*; 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 { +public class BanSyncModule extends AbstractSyncModule { private final Map events = new ConcurrentHashMap<>(); public BanSyncModule(DiscordSRV discordSRV) { - super(discordSRV); + super(discordSRV, "BAN_SYNC"); } @Override public boolean isEnabled() { if (discordSRV.config().banSync.serverId == 0) { - //return false; + //return false; // TODO } return super.isEnabled(); @@ -60,27 +61,38 @@ public class BanSyncModule extends AbstractModule { 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 lookupLinkedAccount(UUID player) { - return discordSRV.profileManager().lookupProfile(player) - .thenApply(Profile::userId); - } - - private CompletableFuture lookupLinkedAccount(long userId) { - return discordSRV.profileManager().lookupProfile(userId) - .thenApply(Profile::playerUUID); - } - public void notifyBanned(IPlayer player, @Nullable Punishment punishment) { - playerBanChange(player.uniqueId(), true, punishment); + gameChanged(BanSyncCause.PLAYER_BANNED, Someone.of(player.uniqueId()), null, punishment); } - @Subscribe - public void onPlayerConnected(PlayerConnectedEvent event) { - playerBanChange(event.player().uniqueId(), false, null); + @Override + public String syncName() { + return "Ban sync"; + } + + @Override + public String logName() { + return "bansync"; + } + + @Override + public String gameTerm() { + return "ban"; + } + + @Override + public String discordTerm() { + return "ban"; + } + + @Override + public List configs() { + return Collections.singletonList(discordSRV.config().banSync); + } + + @Override + protected boolean isActive(Punishment state) { + return state != null; } private PunishmentEvent upsertEvent(long userId, boolean newState) { @@ -106,7 +118,15 @@ public class BanSyncModule extends AbstractModule { return; } - userBanChange(userId, newState, punishment); + if (newState && punishment == null) { + punishment = new Punishment(null, null, null); + } + gameChanged( + GenericSyncCauses.LINK, + Someone.of(userId), + null, + punishment + ); } } @@ -147,7 +167,7 @@ public class BanSyncModule extends AbstractModule { public void onAccountLinked(AccountLinkedEvent event) { BanSyncConfig config = discordSRV.config().banSync; if (config.resyncUponLinking) { - resync(event.getPlayerUUID(), event.getUserId()); + resyncAll(GenericSyncCauses.LINK, Someone.of(event.getPlayerUUID(), event.getUserId())); } } @@ -160,96 +180,40 @@ public class BanSyncModule extends AbstractModule { }); } - public CompletableFuture resync(UUID playerUUID) { - return lookupLinkedAccount(playerUUID).thenCompose(userId -> { - if (userId == null) { - // Unlinked - return null; - } - - return resync(playerUUID, userId); - }); - } - - public CompletableFuture resync(long userId) { - return lookupLinkedAccount(userId).thenCompose(playerUUID -> { - if (playerUUID == null) { - // Unlinked - return null; - } - - return resync(playerUUID, userId); - }); - } - - public CompletableFuture 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 doResync(UUID playerUUID, long userId) { - BanSyncConfig config = discordSRV.config().banSync; - - SyncSide side = config.tieBreaker; - if (side == null) { - return CompletableFuture.completedFuture(BanSyncResult.INVALID_CONFIG); + @Override + protected CompletableFuture getDiscord(BanSyncConfig config, long userId) { + JDA jda = discordSRV.jda(); + if (jda == null) { + return CompletableFutureUtil.failed(new SyncFail(BanSyncResult.NO_DISCORD_CONNECTION)); } - switch (side) { - case DISCORD: - 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); - } - - 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()); + Guild guild = jda.getGuildById(config.serverId); + if (guild == null) { + // Server doesn't exist + return CompletableFutureUtil.failed(new SyncFail(BanSyncResult.GUILD_DOESNT_EXIST)); } + + return getBan(guild, userId).thenApply(this::punishment); } - 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 Punishment punishment(Guild.Ban ban) { + return ban != null ? new Punishment(null, ban.getReason(), ban.getUser().getName()) : null; } - private CompletableFuture changeUserBanState(long userId, boolean newState, @Nullable Punishment punishment) { - BanSyncConfig config = discordSRV.config().banSync; + @Override + protected CompletableFuture getGame(BanSyncConfig config, UUID playerUUID) { + PunishmentModule.Bans bans = discordSRV.getModule(PunishmentModule.Bans.class); + if (bans == null) { + return CompletableFutureUtil.failed(new SyncFail(BanSyncResult.NO_PUNISHMENT_INTEGRATION)); + } + + return bans.getBan(playerUUID); + } + + @Override + protected CompletableFuture applyDiscord(BanSyncConfig config, long userId, Punishment state) { if (config.direction == SyncDirection.DISCORD_TO_MINECRAFT) { - return CompletableFuture.completedFuture(BanSyncResult.WRONG_DIRECTION); + return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION); } JDA jda = discordSRV.jda(); @@ -264,52 +228,23 @@ public class BanSyncModule extends AbstractModule { } UserSnowflake snowflake = UserSnowflake.fromId(userId); - return getBan(guild, userId).thenCompose(ban -> { - if (ban == null) { - if (newState) { - 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); - } - } else { - if (newState) { - // Already banned - return CompletableFuture.completedFuture(BanSyncResult.ALREADY_IN_SYNC); - } else { - return guild.unban(snowflake) - .reason(discordSRV.placeholderService().replacePlaceholders(config.discordUnbanReasonFormat, punishment)) - .submit() - .thenApply(v -> BanSyncResult.UNBAN_USER); - } - } - }); + if (state != null) { + return guild.ban(snowflake, config.discordMessageHoursToDelete, TimeUnit.HOURS) + .reason(discordSRV.placeholderService().replacePlaceholders(config.discordBanReasonFormat, state)) + .submit() + .thenApply(v -> GenericSyncResults.ADD_DISCORD); + } else { + return guild.unban(snowflake) + .reason(discordSRV.placeholderService().replacePlaceholders(config.discordUnbanReasonFormat)) + .submit() + .thenApply(v -> GenericSyncResults.REMOVE_DISCORD); + } } - public void userBanChange(long userId, boolean newState, @Nullable Punishment punishment) { - lookupLinkedAccount(userId).thenCompose(playerUUID -> { - if (playerUUID == null) { - // Unlinked - return 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 changePlayerBanState(UUID playerUUID, boolean newState, @Nullable Punishment punishment) { - BanSyncConfig config = discordSRV.config().banSync; + @Override + protected CompletableFuture applyGame(BanSyncConfig config, UUID playerUUID, Punishment state) { if (config.direction == SyncDirection.MINECRAFT_TO_DISCORD) { - return CompletableFuture.completedFuture(BanSyncResult.WRONG_DIRECTION); + return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION); } PunishmentModule.Bans bans = discordSRV.getModule(PunishmentModule.Bans.class); @@ -317,26 +252,14 @@ public class BanSyncModule extends AbstractModule { return CompletableFuture.completedFuture(BanSyncResult.NO_PUNISHMENT_INTEGRATION); } - return bans.getBan(playerUUID).thenCompose(existingPunishment -> { - if (existingPunishment == null) { - if (newState) { - 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 - 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); - } - } - }); + if (state != null) { + String reason = discordSRV.placeholderService().replacePlaceholders(config.gameBanReasonFormat, state); + String punisher = discordSRV.placeholderService().replacePlaceholders(config.gamePunisherFormat, state); + return bans.addBan(playerUUID, null, reason, punisher) + .thenApply(v -> GenericSyncResults.ADD_GAME); + } else { + return bans.removeBan(playerUUID).thenApply(v -> GenericSyncResults.REMOVE_GAME); + } } } diff --git a/common/src/main/java/com/discordsrv/common/bansync/enums/BanSyncCause.java b/common/src/main/java/com/discordsrv/common/bansync/enums/BanSyncCause.java new file mode 100644 index 00000000..ba4abb2a --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/bansync/enums/BanSyncCause.java @@ -0,0 +1,8 @@ +package com.discordsrv.common.bansync.enums; + +import com.discordsrv.common.sync.cause.ISyncCause; + +public enum BanSyncCause implements ISyncCause { + + PLAYER_BANNED +} diff --git a/common/src/main/java/com/discordsrv/common/bansync/enums/BanSyncResult.java b/common/src/main/java/com/discordsrv/common/bansync/enums/BanSyncResult.java index a445ce19..f0bffc80 100644 --- a/common/src/main/java/com/discordsrv/common/bansync/enums/BanSyncResult.java +++ b/common/src/main/java/com/discordsrv/common/bansync/enums/BanSyncResult.java @@ -1,30 +1,29 @@ package com.discordsrv.common.bansync.enums; -public enum BanSyncResult { +import com.discordsrv.common.sync.result.ISyncResult; - // Success, actioned - BAN_USER("Ban user"), - BAN_PLAYER("Ban player"), - UNBAN_USER("Unban user"), - UNBAN_PLAYER("Unban player"), - - // Nothing done - ALREADY_IN_SYNC("Already in sync"), - WRONG_DIRECTION("Wrong direction"), +public enum BanSyncResult implements ISyncResult { // Error NO_PUNISHMENT_INTEGRATION("No punishment integration"), NO_DISCORD_CONNECTION("No Discord connection"), GUILD_DOESNT_EXIST("Guild doesn't exist"), - INVALID_CONFIG("Invalid config"); + INVALID_CONFIG("Invalid config"), + ; - private final String prettyResult; + private final String format; - BanSyncResult(String prettyResult) { - this.prettyResult = prettyResult; + BanSyncResult(String format) { + this.format = format; } - public String prettyResult() { - return prettyResult; + @Override + public boolean isSuccess() { + return false; + } + + @Override + public String getFormat() { + return format; } } diff --git a/common/src/main/java/com/discordsrv/common/command/combined/commands/ResyncCommand.java b/common/src/main/java/com/discordsrv/common/command/combined/commands/ResyncCommand.java index f44c7407..ea81a078 100644 --- a/common/src/main/java/com/discordsrv/common/command/combined/commands/ResyncCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/combined/commands/ResyncCommand.java @@ -8,13 +8,15 @@ 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.permission.Permission; import com.discordsrv.common.player.IPlayer; -import com.discordsrv.common.sync.ISyncResult; +import com.discordsrv.common.someone.Someone; +import com.discordsrv.common.sync.AbstractSyncModule; +import com.discordsrv.common.sync.SyncSummary; +import com.discordsrv.common.sync.cause.GenericSyncCauses; +import com.discordsrv.common.sync.result.ISyncResult; import net.kyori.adventure.text.format.NamedTextColor; import java.util.*; @@ -79,16 +81,29 @@ public class ResyncCommand extends CombinedCommand { execution.runAsync(() -> { long startTime = System.currentTimeMillis(); - CompletableFutureUtil.combine(resyncOnlinePlayers(module)).thenCompose(result -> { - List> results = new ArrayList<>(); - for (Map> map : result) { - results.addAll(map.values()); + List>> futures = resyncOnlinePlayers(module); + CompletableFutureUtil.combineGeneric(futures).thenCompose(result -> { + List> results = new ArrayList<>(); + for (SyncSummary summary : result) { + results.add(summary.resultFuture()); } - return CompletableFutureUtil.combine(results); - }).whenComplete((results, t) -> { + return CompletableFutureUtil.combineGeneric(results); + }).whenComplete((__, t) -> { Map resultCounts = new HashMap<>(); int total = 0; + List results = new ArrayList<>(); + for (CompletableFuture> future : futures) { + SyncSummary summary = future.join(); + ISyncResult allFailResult = summary.allFailReason(); + if (allFailResult != null) { + results.add(allFailResult); + continue; + } + + results.addAll(summary.resultFuture().join().values()); + } + for (ISyncResult result : results) { total++; resultCounts.computeIfAbsent(result, key -> new AtomicInteger(0)).getAndIncrement(); @@ -116,11 +131,11 @@ public class ResyncCommand extends CombinedCommand { }); } - private List>>> resyncOnlinePlayers(GroupSyncModule module) { - List>>> futures = new ArrayList<>(); + private List>> resyncOnlinePlayers(AbstractSyncModule module) { + List>> summaries = new ArrayList<>(); for (IPlayer player : discordSRV.playerProvider().allPlayers()) { - futures.add(module.resync(player.uniqueId(), GroupSyncCause.COMMAND)); + summaries.add(module.resyncAll(GenericSyncCauses.COMMAND, Someone.of(player))); } - return futures; + return summaries; } } diff --git a/common/src/main/java/com/discordsrv/common/config/main/BanSyncConfig.java b/common/src/main/java/com/discordsrv/common/config/main/BanSyncConfig.java index f6276794..6a1c484e 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/BanSyncConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/BanSyncConfig.java @@ -50,6 +50,6 @@ public class BanSyncConfig extends AbstractSyncConfig @Override public String describe() { - return "Ban sync"; + return Long.toUnsignedString(serverId); } } diff --git a/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java b/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java index cb0ccf48..392d7510 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java @@ -69,12 +69,12 @@ public class GroupSyncConfig { @Override public String toString() { - return "PairConfig{" + groupName + direction.arrow() + Long.toUnsignedString(roleId) + '}'; + return "GroupSyncConfig$PairConfig{" + describe() + '}'; } @Override public String describe() { - return "Group sync (" + groupName + ":" + Long.toUnsignedString(roleId) + ")"; + return groupName + direction.arrow() + Long.toUnsignedString(roleId); } public static String makeGameId(String groupName, Set serverContext) { diff --git a/common/src/main/java/com/discordsrv/common/config/main/generic/AbstractSyncConfig.java b/common/src/main/java/com/discordsrv/common/config/main/generic/AbstractSyncConfig.java index 7e5c38df..15d6396f 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/generic/AbstractSyncConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/generic/AbstractSyncConfig.java @@ -9,6 +9,12 @@ import org.spongepowered.configurate.objectmapping.meta.Comment; import java.util.Arrays; +/** + * A configuration for a synchronizable. + * @param the implementation type + * @param the game identifier + * @param the Discord identifier + */ @ConfigSerializable public abstract class AbstractSyncConfig, G, D> { @@ -43,8 +49,8 @@ public abstract class AbstractSyncConfig, public abstract String describe(); - public boolean validate(DiscordSRV discordSRV) { - String label = describe(); + public boolean validate(String syncName, DiscordSRV discordSRV) { + String label = syncName + " (" + describe() + ")"; boolean invalidTieBreaker, invalidDirection = false; if ((invalidTieBreaker = (tieBreaker == null)) || (invalidDirection = (direction == null))) { if (invalidTieBreaker) { diff --git a/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java b/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java index a15f24e6..0795d9ce 100644 --- a/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java +++ b/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java @@ -46,6 +46,11 @@ public final class CompletableFutureUtil { return combine(futures.toArray(new CompletableFuture[0])); } + @SuppressWarnings("unchecked") + public static CompletableFuture> combineGeneric(Collection> futures) { + return combine(futures.toArray(new CompletableFuture[0])); + } + @SafeVarargs public static CompletableFuture> combine(CompletableFuture... futures) { CompletableFuture> future = new CompletableFuture<>(); diff --git a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java index 5a73f54d..962f14ba 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java +++ b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java @@ -9,15 +9,14 @@ import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.main.GroupSyncConfig; import com.discordsrv.common.debug.DebugGenerateEvent; 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.GroupSyncResult; -import com.discordsrv.common.player.IPlayer; +import com.discordsrv.common.someone.Someone; import com.discordsrv.common.sync.AbstractSyncModule; -import com.discordsrv.common.sync.ISyncResult; +import com.discordsrv.common.sync.result.ISyncResult; import com.discordsrv.common.sync.SyncFail; -import com.discordsrv.common.sync.enums.SyncResults; +import com.discordsrv.common.sync.result.GenericSyncResults; import com.github.benmanes.caffeine.cache.Cache; import org.jetbrains.annotations.Nullable; @@ -27,7 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -public class GroupSyncModule extends AbstractSyncModule { +public class GroupSyncModule extends AbstractSyncModule { private final Cache> expectedDiscordChanges; private final Cache> expectedMinecraftChanges; @@ -43,13 +42,33 @@ public class GroupSyncModule extends AbstractSyncModule configs() { return discordSRV.config().groupSync.pairs; } @Override - protected boolean isTrue(Boolean state) { + protected boolean isActive(Boolean state) { return state; } @@ -97,51 +116,8 @@ public class GroupSyncModule extends AbstractSyncModule>> 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> pairs - ) { - CompletableFutureUtil.combine(pairs.values()).whenComplete((v, t) -> { - GroupSyncSummary summary = new GroupSyncSummary(playerUUID, cause); - for (Map.Entry> entry : pairs.entrySet()) { - summary.add(entry.getKey(), entry.getValue().join()); - } - - String finalSummary = summary.toString(); - logger().debug(finalSummary); - - if (summary.anySuccess()) { - // If anything was changed as a result of synchronization, log to file - discordSRV.logger().writeLogForCurrentDay("groupsync", finalSummary); - } - }); - } - // Listeners & methods to indicate something changed - @Subscribe - public void onPlayerConnected(PlayerConnectedEvent event) { - 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(), true)); @@ -171,18 +147,7 @@ public class GroupSyncModule extends AbstractSyncModule { - 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)); - }); + discordChanged(GroupSyncCause.DISCORD_ROLE_CHANGE, Someone.of(userId), roleId, state); } private void groupChanged( @@ -202,17 +167,7 @@ public class GroupSyncModule extends AbstractSyncModule { - if (userId == null) { - return; - } - - logSummary(playerUUID, cause, gameChanged(userId, playerUUID, context(groupName, serverContext), state)); - }); + gameChanged(cause, Someone.of(playerUUID), context(groupName, serverContext), state); } private PermissionModule.Groups getPermissionProvider() { @@ -237,31 +192,6 @@ public class GroupSyncModule extends AbstractSyncModule { - if (userId == null) { - return; - } - - logSummary(playerUUID, GroupSyncCause.TIMER, resync(config, playerUUID, userId).thenApply(result -> { - Map> map = new HashMap<>(); - map.put(config, CompletableFuture.completedFuture(result)); - return map; - })); - }); - } - } - - public CompletableFuture>> resync(UUID playerUUID, GroupSyncCause cause) { - CompletableFuture>> future = resyncAll(playerUUID); - logSummary(playerUUID, cause, future); - return future; - } - @Override public CompletableFuture getDiscord(GroupSyncConfig.PairConfig config, long userId) { DiscordRole role = discordSRV.discordAPI().getRoleById(config.roleId); @@ -312,8 +242,8 @@ public class GroupSyncModule extends AbstractSyncModule state - ? member.addRole(role).thenApply(v -> (ISyncResult) SyncResults.ADD_DISCORD) - : member.removeRole(role).thenApply(v -> SyncResults.REMOVE_DISCORD) + ? member.addRole(role).thenApply(v -> (ISyncResult) GenericSyncResults.ADD_DISCORD) + : member.removeRole(role).thenApply(v -> GenericSyncResults.REMOVE_DISCORD) ).whenComplete((r, t) -> { if (t != null) { //noinspection DataFlowIssue @@ -331,8 +261,8 @@ public class GroupSyncModule extends AbstractSyncModule future = state - ? addGroup(playerUUID, config).thenApply(v -> SyncResults.ADD_GAME) - : removeGroup(playerUUID, config).thenApply(v -> SyncResults.REMOVE_GAME); + ? addGroup(playerUUID, config).thenApply(v -> GenericSyncResults.ADD_GAME) + : removeGroup(playerUUID, config).thenApply(v -> GenericSyncResults.REMOVE_GAME); return future.exceptionally(t -> { //noinspection DataFlowIssue expected.remove(config.groupName); diff --git a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncSummary.java b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncSummary.java deleted file mode 100644 index f67b8e17..00000000 --- a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncSummary.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 . - */ - -package com.discordsrv.common.groupsync; - -import com.discordsrv.common.config.main.GroupSyncConfig; -import com.discordsrv.common.groupsync.enums.GroupSyncCause; -import com.discordsrv.common.sync.ISyncResult; - -import java.util.*; - -public class GroupSyncSummary { - - private final Map> pairs = new HashMap<>(); - private final UUID player; - private final GroupSyncCause cause; - - public GroupSyncSummary(UUID player, GroupSyncCause cause, GroupSyncConfig.PairConfig config, ISyncResult result) { - this(player, cause); - add(config, result); - } - - public GroupSyncSummary(UUID player, GroupSyncCause cause) { - this.player = player; - this.cause = cause; - } - - public void add(GroupSyncConfig.PairConfig config, ISyncResult result) { - pairs.computeIfAbsent(result, key -> new LinkedHashSet<>()).add(config); - } - - public boolean anySuccess() { - for (ISyncResult result : pairs.keySet()) { - if (result.isSuccess()) { - return true; - } - } - return false; - } - - @Override - public String toString() { - int count = pairs.size(); - StringBuilder message = new StringBuilder( - "Group synchronization (of " + count + " pair" + (count == 1 ? "" : "s") + ") for " + player + " (" + cause + ")"); - - for (Map.Entry> entry : pairs.entrySet()) { - message.append(count == 1 ? ": " : "\n") - .append(entry.getKey().toString()) - .append(": ") - .append(entry.getValue().toString()); - } - return message.toString(); - } -} diff --git a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncCause.java b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncCause.java index 2af9a2c6..88481545 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncCause.java +++ b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncCause.java @@ -18,13 +18,10 @@ package com.discordsrv.common.groupsync.enums; -public enum GroupSyncCause { +import com.discordsrv.common.sync.cause.ISyncCause; + +public enum GroupSyncCause implements ISyncCause { - API("API"), - COMMAND("Command"), - GAME_JOIN("Joined game"), - LINK("Linked account"), - TIMER("Timed synchronization"), DISCORD_ROLE_CHANGE("Discord role changed", true), LUCKPERMS_NODE_CHANGE("LuckPerms node changed", true), LUCKPERMS_TRACK("LuckPerms track promotion/demotion"), diff --git a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncResult.java b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncResult.java index eda98ff2..e3ed05db 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncResult.java +++ b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncResult.java @@ -18,39 +18,30 @@ package com.discordsrv.common.groupsync.enums; -import com.discordsrv.common.sync.ISyncResult; +import com.discordsrv.common.sync.result.ISyncResult; public enum GroupSyncResult implements ISyncResult { - // Errors + // Error 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_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"), + PERMISSION_BACKEND_FAILED("Failed to interact with permission backend, error printed"), ; - final String prettyResult; - final boolean success; + private final String format; - GroupSyncResult(String prettyResult) { - this(prettyResult, false); - } - - GroupSyncResult(String prettyResult, boolean success) { - this.prettyResult = prettyResult; - this.success = success; + GroupSyncResult(String format) { + this.format = format; } public boolean isSuccess() { - return success; + return false; } @Override - public String toString() { - return prettyResult; + public String getFormat() { + return format; } } diff --git a/common/src/main/java/com/discordsrv/common/someone/Someone.java b/common/src/main/java/com/discordsrv/common/someone/Someone.java new file mode 100644 index 00000000..6711d069 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/someone/Someone.java @@ -0,0 +1,107 @@ +package com.discordsrv.common.someone; + +import com.discordsrv.api.discord.entity.DiscordUser; +import com.discordsrv.api.player.DiscordSRVPlayer; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.profile.Profile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class Someone { + + public static Someone.Resolved of(@NotNull DiscordSRVPlayer player, @NotNull DiscordUser user) { + return of(player.uniqueId(), user.getId()); + } + + public static Someone.Resolved of(@NotNull UUID playerUUID, long userId) { + return new Someone.Resolved(playerUUID, userId); + } + + public static Someone of(@NotNull DiscordSRVPlayer player) { + return of(player.uniqueId()); + } + + public static Someone of(@NotNull UUID playerUUID) { + return new Someone(playerUUID, null); + } + + public static Someone of(@NotNull DiscordUser user) { + return of(user.getId()); + } + + public static Someone of(long userId) { + return new Someone(null, userId); + } + + private final UUID playerUUID; + private final Long userId; + + private Someone(@Nullable UUID playerUUID, @Nullable Long userId) { + this.playerUUID = playerUUID; + this.userId = userId; + } + + @NotNull + public CompletableFuture<@NotNull Profile> profile(DiscordSRV discordSRV) { + if (playerUUID != null) { + return discordSRV.profileManager().lookupProfile(playerUUID); + } else if (userId != null) { + return discordSRV.profileManager().lookupProfile(userId); + } else { + throw new IllegalStateException("Cannot have Someone instance without either a Player UUID or User Id"); + } + } + + @NotNull + public CompletableFuture withLinkedAccounts(DiscordSRV discordSRV) { + if (playerUUID != null && userId != null) { + return CompletableFuture.completedFuture(of(playerUUID, userId)); + } + + return profile(discordSRV).thenApply(profile -> { + UUID playerUUID = profile.playerUUID(); + Long userId = profile.userId(); + if (playerUUID == null || userId == null) { + return null; + } + return of(playerUUID, userId); + }); + } + + @Nullable + public UUID playerUUID() { + return playerUUID; + } + + @Nullable + public Long userId() { + return userId; + } + + @Override + public String toString() { + return playerUUID != null ? playerUUID.toString() : Objects.requireNonNull(userId).toString(); + } + + @SuppressWarnings("DataFlowIssue") + public static class Resolved extends Someone { + + private Resolved(@NotNull UUID playerUUID, @NotNull Long userId) { + super(playerUUID, userId); + } + + @Override + public @NotNull UUID playerUUID() { + return super.playerUUID(); + } + + @Override + public @NotNull Long userId() { + return super.userId(); + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/sync/AbstractSyncModule.java b/common/src/main/java/com/discordsrv/common/sync/AbstractSyncModule.java index d147108e..6a5f367d 100644 --- a/common/src/main/java/com/discordsrv/common/sync/AbstractSyncModule.java +++ b/common/src/main/java/com/discordsrv/common/sync/AbstractSyncModule.java @@ -1,16 +1,23 @@ package com.discordsrv.common.sync; import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.main.GroupSyncConfig; import com.discordsrv.common.config.main.generic.AbstractSyncConfig; +import com.discordsrv.common.event.events.player.PlayerConnectedEvent; 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.player.IPlayer; +import com.discordsrv.common.someone.Someone; +import com.discordsrv.common.sync.cause.GenericSyncCauses; +import com.discordsrv.common.sync.cause.ISyncCause; import com.discordsrv.common.sync.enums.SyncDirection; -import com.discordsrv.common.sync.enums.SyncResults; +import com.discordsrv.common.sync.result.GenericSyncResults; import com.discordsrv.common.sync.enums.SyncSide; +import com.discordsrv.common.sync.result.ISyncResult; +import org.apache.commons.lang3.StringUtils; import java.time.Duration; import java.util.*; @@ -19,7 +26,22 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.function.Consumer; -public abstract class AbstractSyncModule
, S> extends AbstractModule
{ +/** + * Abstraction for synchronization between Minecraft and Discord. + * + * @param
the DiscordSRV type + * @param the configuration type for a single synchronizable + * @param the identifier for the game object to be synced + * @param the identifier for the Discord object to be synced + * @param state of synchronization on Minecraft/Discord + */ +public abstract class AbstractSyncModule< + DT extends DiscordSRV, + C extends AbstractSyncConfig, + G, + D, + S +> extends AbstractModule
{ protected final Map> syncs = new LinkedHashMap<>(); private final Map> configsForGame = new ConcurrentHashMap<>(); @@ -29,6 +51,16 @@ public abstract class AbstractSyncModule
configs(); @Override @@ -43,8 +75,9 @@ public abstract class AbstractSyncModule
lookupLinkedAccount(UUID player) { - return discordSRV.profileManager().lookupProfile(player) - .thenApply(Profile::userId) - .thenApply(userId -> { - if (userId == null) { - throw new SyncFail(SyncResults.NOT_LINKED); - } - return userId; - }); + private void resyncTimer(C config) { + for (IPlayer player : discordSRV.playerProvider().allPlayers()) { + resync(GenericSyncCauses.TIMER, config, Someone.of(player.uniqueId())); + } } - protected CompletableFuture lookupLinkedAccount(long userId) { - return discordSRV.profileManager().lookupProfile(userId) - .thenApply(Profile::playerUUID) - .thenApply(playerUUID -> { - if (playerUUID == null) { - throw new SyncFail(SyncResults.NOT_LINKED); - } - return playerUUID; - }); + @Subscribe + public void onPlayerConnected(PlayerConnectedEvent event) { + resyncAll(GenericSyncCauses.GAME_JOIN, Someone.of(event.player())); } - protected abstract boolean isTrue(S state); + /** + * Check if the provided state is active or inactive, should this not match for the state of the two sides, synchronization will occur. + * + * @param state the state + * @return {@code true} indicating the provided state is "active" + */ + protected abstract boolean isActive(S state); + /** + * Gets the current state of the provided config for the specified user on Discord. + * + * @param config the configuration for the synchronizable + * @param userId the Discord user id + * @return a future for the state on Discord + */ protected abstract CompletableFuture getDiscord(C config, long userId); + + /** + * Gets the current state of the provided config for the specified player on Minecraft. + * + * @param config the configuration for the synchronizable + * @param playerUUID the Minecraft player {@link UUID} + * @return a future for the state on Minecraft + */ protected abstract CompletableFuture getGame(C config, UUID playerUUID); + /** + * Applies the provided state for the provided config for the provided Discord user. + * + * @param config the configuration for the synchronizable + * @param userId the Discord user id + * @param state the state to apply + * @return a future with the result of the synchronization + */ protected abstract CompletableFuture applyDiscord(C config, long userId, S state); - protected abstract CompletableFuture applyGame(C config, UUID playerUUID, S state); protected CompletableFuture 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); + if ((actualValue = isActive(state)) == isActive(value)) { + return CompletableFuture.completedFuture(actualValue ? GenericSyncResults.BOTH_TRUE : GenericSyncResults.BOTH_FALSE); } else { return applyDiscord(config, userId, state); } }); } + /** + * Applies the provided state for the provided config for the provided Minecraft player. + * + * @param config the configuration for the synchronizable + * @param playerUUID the Minecraft player {@link UUID} + * @param state the state to apply + * @return a future with the result of the synchronization + */ + protected abstract CompletableFuture applyGame(C config, UUID playerUUID, S state); + protected CompletableFuture 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); + boolean active; + if ((active = isActive(state)) == isActive(value)) { + return CompletableFuture.completedFuture(active ? GenericSyncResults.BOTH_TRUE : GenericSyncResults.BOTH_FALSE); } else { return applyGame(config, playerUUID, state); } }); } - protected Map> discordChanged(long userId, UUID playerUUID, D discordId, S state) { + protected CompletableFuture> discordChanged(ISyncCause cause, Someone someone, D discordId, S state) { List gameConfigs = configsForDiscord.get(discordId); if (gameConfigs == null) { - return Collections.emptyMap(); + return CompletableFuture.completedFuture(null); } - Map> 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; + return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> { + if (resolved == null) { + return new SyncSummary(cause, someone).fail(GenericSyncResults.NOT_LINKED); } - 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 discordConfigs = configsForGame.get(config.gameId()); - if (discordConfigs == null) { - continue; - } - - for (C gameConfig : discordConfigs) { - if (gameConfig.discordId() == discordId) { + SyncSummary summary = new SyncSummary<>(cause, resolved); + for (C config : gameConfigs) { + SyncDirection direction = config.direction; + if (direction == SyncDirection.MINECRAFT_TO_DISCORD) { + // Not going Discord -> Minecraft + summary.appendResult(config, GenericSyncResults.WRONG_DIRECTION); continue; } - futures.put(gameConfig, applyDiscordIfNot(gameConfig, userId, state)); + summary.appendResult(config, applyGameIfNot(config, resolved.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 discordConfigs = configsForGame.get(config.gameId()); + if (discordConfigs == null) { + continue; + } + + for (C gameConfig : discordConfigs) { + if (gameConfig.discordId() == discordId) { + continue; + } + + summary.appendResult(gameConfig, applyDiscordIfNot(gameConfig, resolved.userId(), state)); + } } - } - return futures; + return summary; + }).whenComplete((summary, t) -> { + if (summary != null) { + logSummary(summary); + } + }); } - protected Map> gameChanged(long userId, UUID playerUUID, G gameId, S state) { + protected CompletableFuture> gameChanged(ISyncCause cause, Someone someone, G gameId, S state) { List discordConfigs = configsForGame.get(gameId); if (discordConfigs == null) { - return Collections.emptyMap(); + return CompletableFuture.completedFuture(null); } - Map> 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; + return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> { + if (resolved == null) { + return new SyncSummary(cause, someone).fail(GenericSyncResults.NOT_LINKED); } - 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 gameConfigs = configsForDiscord.get(config.discordId()); - if (gameConfigs == null) { - continue; - } - - for (C gameConfig : gameConfigs) { - if (gameConfig.gameId() == gameId) { + SyncSummary summary = new SyncSummary<>(cause, resolved); + for (C config : discordConfigs) { + SyncDirection direction = config.direction; + if (direction == SyncDirection.DISCORD_TO_MINECRAFT) { + // Not going Minecraft -> Discord + summary.appendResult(config, GenericSyncResults.WRONG_DIRECTION); continue; } - futures.put(gameConfig, applyGameIfNot(gameConfig, playerUUID, state)); + summary.appendResult(config, applyDiscordIfNot(config, resolved.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 gameConfigs = configsForDiscord.get(config.discordId()); + if (gameConfigs == null) { + continue; + } + + for (C gameConfig : gameConfigs) { + if (gameConfig.gameId() == gameId) { + continue; + } + + summary.appendResult(gameConfig, applyGameIfNot(gameConfig, resolved.playerUUID(), state)); + } } - } - return futures; + return summary; + }).whenComplete((summary, t) -> { + if (summary != null) { + logSummary(summary); + } + }); } - protected abstract void resyncTimer(C config); + public CompletableFuture> resyncAll(ISyncCause cause, Someone someone) { + return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> { + if (resolved == null) { + return new SyncSummary(cause, someone).fail(GenericSyncResults.NOT_LINKED); + } - protected CompletableFuture>> resyncAll(UUID playerUUID) { - return lookupLinkedAccount(playerUUID).thenApply(userId -> resyncAll(playerUUID, userId)); + SyncSummary summary = new SyncSummary<>(cause, resolved); + List configs = configs(); + + for (C config : configs) { + summary.appendResult(config, resync(config, resolved)); + } + return summary; + }).whenComplete((summary, t) -> { + if (summary != null) { + logSummary(summary); + } + }); } - protected CompletableFuture>> resyncAll(long userId) { - return lookupLinkedAccount(userId).thenApply(playerUUID -> resyncAll(playerUUID, userId)); + protected CompletableFuture> resync(ISyncCause cause, C config, Someone someone) { + return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> { + if (resolved == null) { + return new SyncSummary(cause, someone).fail(GenericSyncResults.NOT_LINKED); + } + + return new SyncSummary(cause, resolved) + .appendResult(config, resync(config, resolved)); + }).whenComplete((summary, t) -> { + if (summary != null) { + logSummary(summary); + } + }); } - protected Map> resyncAll(UUID playerUUID, long userId) { - List configs = configs(); + private CompletableFuture resync(C config, Someone.Resolved resolved) { + UUID playerUUID = resolved.playerUUID(); + long userId = resolved.userId(); - Map> results = new HashMap<>(configs.size()); - for (C config : configs) { - results.put(config, resync(config, playerUUID, userId)); - } - return results; - } - - protected CompletableFuture resync(C config, UUID playerUUID, long userId) { CompletableFuture gameGet = getGame(config, playerUUID); CompletableFuture discordGet = getDiscord(config, userId); @@ -245,7 +343,7 @@ public abstract class AbstractSyncModule
SyncResults.ADD_GAME); + return applyGame(config, playerUUID, discordState).thenApply(v -> GenericSyncResults.ADD_GAME); } else { // Missing game, remove Discord if (direction == SyncDirection.DISCORD_TO_MINECRAFT) { - return CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION); + return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION); } - return applyDiscord(config, userId, null).thenApply(v -> SyncResults.REMOVE_DISCORD); + return applyDiscord(config, userId, null).thenApply(v -> GenericSyncResults.REMOVE_DISCORD); } } else { if (side == SyncSide.DISCORD) { // Missing Discord, remove game if (direction == SyncDirection.MINECRAFT_TO_DISCORD) { - return CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION); + return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION); } - return applyGame(config, playerUUID, null).thenApply(v -> SyncResults.REMOVE_GAME); + return applyGame(config, playerUUID, null).thenApply(v -> GenericSyncResults.REMOVE_GAME); } else { // Has game, add Discord if (direction == SyncDirection.DISCORD_TO_MINECRAFT) { - return CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION); + return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION); } - return applyDiscord(config, userId, gameState).thenApply(v -> SyncResults.ADD_DISCORD); + return applyDiscord(config, userId, gameState).thenApply(v -> GenericSyncResults.ADD_DISCORD); } } }).exceptionally(t -> { @@ -292,5 +390,48 @@ public abstract class AbstractSyncModule
summary, List results) { + int count = results.size(); + return summary.who().toString() + + (count == 1 ? ": " : "\n") + + String.join("\n", results); + } + + private void logSummary(SyncSummary summary) { + summary.resultFuture().whenComplete((results, t) -> { + if (t != null) { + logger().error("Failed to " + syncName() + " " + summary.who(), t); + return; + } + + ISyncResult allFailReason = summary.allFailReason(); + if (allFailReason != null) { + String reason = allFailReason.format(gameTerm(), discordTerm()); + logger().debug("Failed to " + syncName() + " " + summary.who() + ": " + reason); + return; + } + + List logResults = new ArrayList<>(); + List auditResults = new ArrayList<>(); + for (Map.Entry entry : results.entrySet()) { + C config = entry.getKey(); + ISyncResult result = entry.getValue(); + + String log = config.describe(); + if (StringUtils.isEmpty(log)) { + log += ": "; + } + log += result.format(gameTerm(), discordTerm()); + + logResults.add(log); + if (result.isSuccess()) { + auditResults.add(log); + } + } + + logger().debug(formatResults(summary, logResults)); + discordSRV.logger().writeLogForCurrentDay(logName(), formatResults(summary, auditResults)); + }); + } } diff --git a/common/src/main/java/com/discordsrv/common/sync/ISyncResult.java b/common/src/main/java/com/discordsrv/common/sync/ISyncResult.java deleted file mode 100644 index fb972f64..00000000 --- a/common/src/main/java/com/discordsrv/common/sync/ISyncResult.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.discordsrv.common.sync; - -public interface ISyncResult { - - boolean isSuccess(); - -} diff --git a/common/src/main/java/com/discordsrv/common/sync/SyncFail.java b/common/src/main/java/com/discordsrv/common/sync/SyncFail.java index 24b6bc55..a93461ad 100644 --- a/common/src/main/java/com/discordsrv/common/sync/SyncFail.java +++ b/common/src/main/java/com/discordsrv/common/sync/SyncFail.java @@ -1,5 +1,7 @@ package com.discordsrv.common.sync; +import com.discordsrv.common.sync.result.ISyncResult; + public class SyncFail extends RuntimeException { private final ISyncResult result; diff --git a/common/src/main/java/com/discordsrv/common/sync/SyncSummary.java b/common/src/main/java/com/discordsrv/common/sync/SyncSummary.java new file mode 100644 index 00000000..62c2252a --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/sync/SyncSummary.java @@ -0,0 +1,63 @@ +package com.discordsrv.common.sync; + +import com.discordsrv.common.config.main.generic.AbstractSyncConfig; +import com.discordsrv.common.future.util.CompletableFutureUtil; +import com.discordsrv.common.someone.Someone; +import com.discordsrv.common.sync.cause.ISyncCause; +import com.discordsrv.common.sync.result.ISyncResult; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +public class SyncSummary> { + + private final ISyncCause cause; + private final Someone who; + private ISyncResult allFailReason; + private final Map> results = new ConcurrentHashMap<>(); + + public SyncSummary(ISyncCause cause, Someone who) { + this.cause = cause; + this.who = who; + } + + public ISyncCause cause() { + return cause; + } + + public Someone who() { + return who; + } + + public SyncSummary fail(ISyncResult genericFail) { + this.allFailReason = genericFail; + return this; + } + + public ISyncResult allFailReason() { + return allFailReason; + } + + public SyncSummary appendResult(C config, ISyncResult result) { + return appendResult(config, CompletableFuture.completedFuture(result)); + } + + public SyncSummary appendResult(C config, CompletableFuture result) { + this.results.put(config, result); + return this; + } + + public CompletableFuture> resultFuture() { + return CompletableFutureUtil.combine(results.values()) + .exceptionally(t -> null) + .thenApply((__) -> { + Map results = new HashMap<>(); + for (Map.Entry> entry : this.results.entrySet()) { + results.put(entry.getKey(), entry.getValue().join()); + } + return results; + }); + } +} diff --git a/common/src/main/java/com/discordsrv/common/sync/cause/GenericSyncCauses.java b/common/src/main/java/com/discordsrv/common/sync/cause/GenericSyncCauses.java new file mode 100644 index 00000000..c0951a24 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/sync/cause/GenericSyncCauses.java @@ -0,0 +1,23 @@ +package com.discordsrv.common.sync.cause; + +public enum GenericSyncCauses implements ISyncCause { + + API("API"), + COMMAND("Command"), + GAME_JOIN("Joined game"), + LINK("Linked account"), + TIMER("Timed synchronization"), + + ; + + private final String prettyCause; + + GenericSyncCauses(String prettyCause) { + this.prettyCause = prettyCause; + } + + @Override + public String toString() { + return prettyCause; + } +} diff --git a/common/src/main/java/com/discordsrv/common/sync/cause/ISyncCause.java b/common/src/main/java/com/discordsrv/common/sync/cause/ISyncCause.java new file mode 100644 index 00000000..59a7d140 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/sync/cause/ISyncCause.java @@ -0,0 +1,4 @@ +package com.discordsrv.common.sync.cause; + +public interface ISyncCause { +} diff --git a/common/src/main/java/com/discordsrv/common/sync/enums/SyncResults.java b/common/src/main/java/com/discordsrv/common/sync/enums/SyncResults.java deleted file mode 100644 index 232df6ee..00000000 --- a/common/src/main/java/com/discordsrv/common/sync/enums/SyncResults.java +++ /dev/null @@ -1,43 +0,0 @@ -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; - } -} diff --git a/common/src/main/java/com/discordsrv/common/sync/result/GenericSyncResults.java b/common/src/main/java/com/discordsrv/common/sync/result/GenericSyncResults.java new file mode 100644 index 00000000..fd456eb0 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/sync/result/GenericSyncResults.java @@ -0,0 +1,42 @@ +package com.discordsrv.common.sync.result; + +public enum GenericSyncResults implements ISyncResult { + + // Success, actioned + ADD_DISCORD("Add %d"), + REMOVE_DISCORD("Remove %d"), + ADD_GAME("Add %g"), + REMOVE_GAME("Remove %g"), + + // Success, Nothing done + BOTH_TRUE("Both true"), + BOTH_FALSE("Both false"), + WRONG_DIRECTION("Wrong direction"), + + // Error + NOT_LINKED("Accounts not linked"), + + ; + + private final String message; + private final boolean success; + + GenericSyncResults(String message) { + this(message, true); + } + + GenericSyncResults(String message, boolean success) { + this.message = message; + this.success = success; + } + + @Override + public boolean isSuccess() { + return success; + } + + @Override + public String getFormat() { + return message; + } +} diff --git a/common/src/main/java/com/discordsrv/common/sync/result/ISyncResult.java b/common/src/main/java/com/discordsrv/common/sync/result/ISyncResult.java new file mode 100644 index 00000000..67daf121 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/sync/result/ISyncResult.java @@ -0,0 +1,17 @@ +package com.discordsrv.common.sync.result; + +import com.discordsrv.api.placeholder.util.Placeholders; + +public interface ISyncResult { + + boolean isSuccess(); + String getFormat(); + + default String format(String gameTerm, String discordTerm) { + return new Placeholders(getFormat()) + .replace("%g", gameTerm) + .replace("%d", discordTerm) + .toString(); + } + +}