Progress on abstracting sync modules

This commit is contained in:
Vankka 2024-05-01 21:58:02 +03:00
parent eec194491e
commit ccdcfe31bd
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
22 changed files with 714 additions and 561 deletions

View File

@ -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<DiscordSRV> {
public class BanSyncModule extends AbstractSyncModule<DiscordSRV, BanSyncConfig, Void, Long, Punishment> {
private final Map<Long, PunishmentEvent> 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<DiscordSRV> {
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);
}
private CompletableFuture<UUID> 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<BanSyncConfig> 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<DiscordSRV> {
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<DiscordSRV> {
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<DiscordSRV> {
});
}
public CompletableFuture<BanSyncResult> resync(UUID playerUUID) {
return lookupLinkedAccount(playerUUID).thenCompose(userId -> {
if (userId == null) {
// Unlinked
return null;
}
return resync(playerUUID, userId);
});
}
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);
@Override
protected CompletableFuture<Punishment> 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<BanSyncResult> changeUserBanState(long userId, boolean newState, @Nullable Punishment punishment) {
BanSyncConfig config = discordSRV.config().banSync;
@Override
protected CompletableFuture<Punishment> 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<ISyncResult> 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<DiscordSRV> {
}
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<BanSyncResult> changePlayerBanState(UUID playerUUID, boolean newState, @Nullable Punishment punishment) {
BanSyncConfig config = discordSRV.config().banSync;
@Override
protected CompletableFuture<ISyncResult> 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<DiscordSRV> {
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);
}
}
}

View File

@ -0,0 +1,8 @@
package com.discordsrv.common.bansync.enums;
import com.discordsrv.common.sync.cause.ISyncCause;
public enum BanSyncCause implements ISyncCause {
PLAYER_BANNED
}

View File

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

View File

@ -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<CompletableFuture<ISyncResult>> results = new ArrayList<>();
for (Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>> map : result) {
results.addAll(map.values());
List<CompletableFuture<? extends SyncSummary<?>>> futures = resyncOnlinePlayers(module);
CompletableFutureUtil.combineGeneric(futures).thenCompose(result -> {
List<CompletableFuture<?>> 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<ISyncResult, AtomicInteger> resultCounts = new HashMap<>();
int total = 0;
List<ISyncResult> results = new ArrayList<>();
for (CompletableFuture<? extends SyncSummary<?>> 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<CompletableFuture<Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>>>> resyncOnlinePlayers(GroupSyncModule module) {
List<CompletableFuture<Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>>>> futures = new ArrayList<>();
private List<CompletableFuture<? extends SyncSummary<?>>> resyncOnlinePlayers(AbstractSyncModule<?, ?, ?, ?, ?> module) {
List<CompletableFuture<? extends SyncSummary<?>>> 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;
}
}

View File

@ -50,6 +50,6 @@ public class BanSyncConfig extends AbstractSyncConfig<BanSyncConfig, Void, Long>
@Override
public String describe() {
return "Ban sync";
return Long.toUnsignedString(serverId);
}
}

View File

@ -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<String> serverContext) {

View File

@ -9,6 +9,12 @@ import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.Arrays;
/**
* A configuration for a synchronizable.
* @param <C> the implementation type
* @param <G> the game identifier
* @param <D> the Discord identifier
*/
@ConfigSerializable
public abstract class AbstractSyncConfig<C extends AbstractSyncConfig<C, G, D>, G, D> {
@ -43,8 +49,8 @@ public abstract class AbstractSyncConfig<C extends AbstractSyncConfig<C, G, D>,
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) {

View File

@ -46,6 +46,11 @@ public final class CompletableFutureUtil {
return combine(futures.toArray(new CompletableFuture[0]));
}
@SuppressWarnings("unchecked")
public static <T> CompletableFuture<List<T>> combineGeneric(Collection<CompletableFuture<? extends T>> futures) {
return combine(futures.toArray(new CompletableFuture[0]));
}
@SafeVarargs
public static <T> CompletableFuture<List<T>> combine(CompletableFuture<T>... futures) {
CompletableFuture<List<T>> future = new CompletableFuture<>();

View File

@ -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<DiscordSRV, String, Long, GroupSyncConfig.PairConfig, Boolean> {
public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, GroupSyncConfig.PairConfig, String, Long, Boolean> {
private final Cache<Long, Map<Long, Boolean>> expectedDiscordChanges;
private final Cache<UUID, Map<String, Boolean>> expectedMinecraftChanges;
@ -43,13 +42,33 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long
.build();
}
@Override
public String syncName() {
return "Group sync";
}
@Override
public String logName() {
return "groupsync";
}
@Override
public String gameTerm() {
return "group";
}
@Override
public String discordTerm() {
return "role";
}
@Override
public List<GroupSyncConfig.PairConfig> 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<DiscordSRV, String, Long
event.addFile(new TextDebugFile("group-sync.txt", builder));
}
private void logSummary(
UUID playerUUID,
GroupSyncCause cause,
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(playerUUID, cause);
for (Map.Entry<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>> 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<DiscordSRV, String, Long
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));
});
discordChanged(GroupSyncCause.DISCORD_ROLE_CHANGE, Someone.of(userId), roleId, state);
}
private void groupChanged(
@ -202,17 +167,7 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long
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));
});
gameChanged(cause, Someone.of(playerUUID), context(groupName, serverContext), state);
}
private PermissionModule.Groups getPermissionProvider() {
@ -237,31 +192,6 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long
// Resync
@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;
}
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;
}));
});
}
}
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;
}
@Override
public CompletableFuture<Boolean> getDiscord(GroupSyncConfig.PairConfig config, long userId) {
DiscordRole role = discordSRV.discordAPI().getRoleById(config.roleId);
@ -312,8 +242,8 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long
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)
? 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<DiscordSRV, String, Long
CompletableFuture<ISyncResult> 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);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ISyncResult, Set<GroupSyncConfig.PairConfig>> 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<ISyncResult, Set<GroupSyncConfig.PairConfig>> entry : pairs.entrySet()) {
message.append(count == 1 ? ": " : "\n")
.append(entry.getKey().toString())
.append(": ")
.append(entry.getValue().toString());
}
return message.toString();
}
}

View File

@ -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"),

View File

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

View File

@ -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<Someone.@Nullable Resolved> 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();
}
}
}

View File

@ -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<DT extends DiscordSRV, G, D, C extends AbstractSyncConfig<C, G, D>, S> extends AbstractModule<DT> {
/**
* Abstraction for synchronization between Minecraft and Discord.
*
* @param <DT> the DiscordSRV type
* @param <C> the configuration type for a single synchronizable
* @param <G> the identifier for the game object to be synced
* @param <D> the identifier for the Discord object to be synced
* @param <S> state of synchronization on Minecraft/Discord
*/
public abstract class AbstractSyncModule<
DT extends DiscordSRV,
C extends AbstractSyncConfig<C, G, D>,
G,
D,
S
> extends AbstractModule<DT> {
protected final Map<C, Future<?>> syncs = new LinkedHashMap<>();
private final Map<G, List<C>> configsForGame = new ConcurrentHashMap<>();
@ -29,6 +51,16 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
super(discordSRV, new NamedLogger(discordSRV, loggerName));
}
public abstract String syncName();
public abstract String logName();
public abstract String gameTerm();
public abstract String discordTerm();
/**
* Returns a list of all in use synchronizables.
* @return a list of configurations for synchronizables
*/
public abstract List<C> configs();
@Override
@ -43,8 +75,9 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
configsForGame.clear();
configsForDiscord.clear();
String syncName = syncName();
for (C config : configs()) {
if (!config.isSet() || !config.validate(discordSRV)) {
if (!config.isSet() || !config.validate(syncName, discordSRV)) {
continue;
}
@ -56,7 +89,7 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
}
}
if (failed) {
discordSRV.logger().error("Duplicate " + config.describe());
discordSRV.logger().error("Duplicate " + syncName + " (" + config.describe() + ")");
continue;
}
@ -86,155 +119,220 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
}
}
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;
});
private void resyncTimer(C config) {
for (IPlayer player : discordSRV.playerProvider().allPlayers()) {
resync(GenericSyncCauses.TIMER, config, Someone.of(player.uniqueId()));
}
}
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;
});
@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<S> 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<S> 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<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);
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<ISyncResult> applyGame(C config, UUID playerUUID, S 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);
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<C, CompletableFuture<ISyncResult>> discordChanged(long userId, UUID playerUUID, D discordId, S state) {
protected CompletableFuture<SyncSummary<C>> discordChanged(ISyncCause cause, Someone someone, D discordId, S state) {
List<C> gameConfigs = configsForDiscord.get(discordId);
if (gameConfigs == null) {
return Collections.emptyMap();
return CompletableFuture.completedFuture(null);
}
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;
return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> {
if (resolved == null) {
return new SyncSummary<C>(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<C> discordConfigs = configsForGame.get(config.gameId());
if (discordConfigs == null) {
continue;
}
for (C gameConfig : discordConfigs) {
if (gameConfig.discordId() == discordId) {
SyncSummary<C> 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<C> 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<C, CompletableFuture<ISyncResult>> gameChanged(long userId, UUID playerUUID, G gameId, S state) {
protected CompletableFuture<SyncSummary<C>> gameChanged(ISyncCause cause, Someone someone, G gameId, S state) {
List<C> discordConfigs = configsForGame.get(gameId);
if (discordConfigs == null) {
return Collections.emptyMap();
return CompletableFuture.completedFuture(null);
}
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;
return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> {
if (resolved == null) {
return new SyncSummary<C>(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<C> gameConfigs = configsForDiscord.get(config.discordId());
if (gameConfigs == null) {
continue;
}
for (C gameConfig : gameConfigs) {
if (gameConfig.gameId() == gameId) {
SyncSummary<C> 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<C> 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<SyncSummary<C>> resyncAll(ISyncCause cause, Someone someone) {
return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> {
if (resolved == null) {
return new SyncSummary<C>(cause, someone).fail(GenericSyncResults.NOT_LINKED);
}
protected CompletableFuture<Map<C, CompletableFuture<ISyncResult>>> resyncAll(UUID playerUUID) {
return lookupLinkedAccount(playerUUID).thenApply(userId -> resyncAll(playerUUID, userId));
SyncSummary<C> summary = new SyncSummary<>(cause, resolved);
List<C> configs = configs();
for (C config : configs) {
summary.appendResult(config, resync(config, resolved));
}
return summary;
}).whenComplete((summary, t) -> {
if (summary != null) {
logSummary(summary);
}
});
}
protected CompletableFuture<Map<C, CompletableFuture<ISyncResult>>> resyncAll(long userId) {
return lookupLinkedAccount(userId).thenApply(playerUUID -> resyncAll(playerUUID, userId));
protected CompletableFuture<SyncSummary<C>> resync(ISyncCause cause, C config, Someone someone) {
return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> {
if (resolved == null) {
return new SyncSummary<C>(cause, someone).fail(GenericSyncResults.NOT_LINKED);
}
return new SyncSummary<C>(cause, resolved)
.appendResult(config, resync(config, resolved));
}).whenComplete((summary, t) -> {
if (summary != null) {
logSummary(summary);
}
});
}
protected Map<C, CompletableFuture<ISyncResult>> resyncAll(UUID playerUUID, long userId) {
List<C> configs = configs();
private CompletableFuture<ISyncResult> resync(C config, Someone.Resolved resolved) {
UUID playerUUID = resolved.playerUUID();
long userId = resolved.userId();
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);
@ -245,7 +343,7 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
boolean bothState;
if ((bothState = (gameState != null)) == (discordState != null)) {
// Already in sync
return CompletableFuture.completedFuture((ISyncResult) (bothState ? SyncResults.BOTH_TRUE : SyncResults.BOTH_FALSE));
return CompletableFuture.completedFuture((ISyncResult) (bothState ? GenericSyncResults.BOTH_TRUE : GenericSyncResults.BOTH_FALSE));
}
SyncSide side = config.tieBreaker;
@ -254,33 +352,33 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
if (side == SyncSide.DISCORD) {
// Has Discord, add game
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
return CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION);
return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION);
}
return applyGame(config, playerUUID, discordState).thenApply(v -> 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<DT extends DiscordSRV, G, D, C extends
});
}
private String formatResults(SyncSummary<C> summary, List<String> results) {
int count = results.size();
return summary.who().toString()
+ (count == 1 ? ": " : "\n")
+ String.join("\n", results);
}
private void logSummary(SyncSummary<C> 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<String> logResults = new ArrayList<>();
List<String> auditResults = new ArrayList<>();
for (Map.Entry<C, ISyncResult> 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));
});
}
}

View File

@ -1,7 +0,0 @@
package com.discordsrv.common.sync;
public interface ISyncResult {
boolean isSuccess();
}

View File

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

View File

@ -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<C extends AbstractSyncConfig<C, ?, ?>> {
private final ISyncCause cause;
private final Someone who;
private ISyncResult allFailReason;
private final Map<C, CompletableFuture<ISyncResult>> 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<C> fail(ISyncResult genericFail) {
this.allFailReason = genericFail;
return this;
}
public ISyncResult allFailReason() {
return allFailReason;
}
public SyncSummary<C> appendResult(C config, ISyncResult result) {
return appendResult(config, CompletableFuture.completedFuture(result));
}
public SyncSummary<C> appendResult(C config, CompletableFuture<ISyncResult> result) {
this.results.put(config, result);
return this;
}
public CompletableFuture<Map<C, ISyncResult>> resultFuture() {
return CompletableFutureUtil.combine(results.values())
.exceptionally(t -> null)
.thenApply((__) -> {
Map<C, ISyncResult> results = new HashMap<>();
for (Map.Entry<C, CompletableFuture<ISyncResult>> entry : this.results.entrySet()) {
results.put(entry.getKey(), entry.getValue().join());
}
return results;
});
}
}

View File

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

View File

@ -0,0 +1,4 @@
package com.discordsrv.common.sync.cause;
public interface ISyncCause {
}

View File

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

View File

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

View File

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