mirror of
https://github.com/DiscordSRV/Ascension.git
synced 2025-02-07 00:11:36 +01:00
Progress on abstracting sync modules
This commit is contained in:
parent
eec194491e
commit
ccdcfe31bd
@ -6,14 +6,18 @@ import com.discordsrv.api.event.events.linking.AccountLinkedEvent;
|
|||||||
import com.discordsrv.api.module.type.PunishmentModule;
|
import com.discordsrv.api.module.type.PunishmentModule;
|
||||||
import com.discordsrv.api.punishment.Punishment;
|
import com.discordsrv.api.punishment.Punishment;
|
||||||
import com.discordsrv.common.DiscordSRV;
|
import com.discordsrv.common.DiscordSRV;
|
||||||
|
import com.discordsrv.common.bansync.enums.BanSyncCause;
|
||||||
import com.discordsrv.common.bansync.enums.BanSyncResult;
|
import com.discordsrv.common.bansync.enums.BanSyncResult;
|
||||||
import com.discordsrv.common.config.main.BanSyncConfig;
|
import com.discordsrv.common.config.main.BanSyncConfig;
|
||||||
import com.discordsrv.common.event.events.player.PlayerConnectedEvent;
|
import com.discordsrv.common.future.util.CompletableFutureUtil;
|
||||||
import com.discordsrv.common.module.type.AbstractModule;
|
|
||||||
import com.discordsrv.common.player.IPlayer;
|
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.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.JDA;
|
||||||
import net.dv8tion.jda.api.audit.ActionType;
|
import net.dv8tion.jda.api.audit.ActionType;
|
||||||
import net.dv8tion.jda.api.audit.AuditLogEntry;
|
import net.dv8tion.jda.api.audit.AuditLogEntry;
|
||||||
@ -29,27 +33,24 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Collection;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
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<>();
|
private final Map<Long, PunishmentEvent> events = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public BanSyncModule(DiscordSRV discordSRV) {
|
public BanSyncModule(DiscordSRV discordSRV) {
|
||||||
super(discordSRV);
|
super(discordSRV, "BAN_SYNC");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
if (discordSRV.config().banSync.serverId == 0) {
|
if (discordSRV.config().banSync.serverId == 0) {
|
||||||
//return false;
|
//return false; // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isEnabled();
|
return super.isEnabled();
|
||||||
@ -60,27 +61,38 @@ public class BanSyncModule extends AbstractModule<DiscordSRV> {
|
|||||||
return Collections.singleton(DiscordGatewayIntent.GUILD_MODERATION);
|
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) {
|
public void notifyBanned(IPlayer player, @Nullable Punishment punishment) {
|
||||||
playerBanChange(player.uniqueId(), true, punishment);
|
gameChanged(BanSyncCause.PLAYER_BANNED, Someone.of(player.uniqueId()), null, punishment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Override
|
||||||
public void onPlayerConnected(PlayerConnectedEvent event) {
|
public String syncName() {
|
||||||
playerBanChange(event.player().uniqueId(), false, null);
|
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) {
|
private PunishmentEvent upsertEvent(long userId, boolean newState) {
|
||||||
@ -106,7 +118,15 @@ public class BanSyncModule extends AbstractModule<DiscordSRV> {
|
|||||||
return;
|
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) {
|
public void onAccountLinked(AccountLinkedEvent event) {
|
||||||
BanSyncConfig config = discordSRV.config().banSync;
|
BanSyncConfig config = discordSRV.config().banSync;
|
||||||
if (config.resyncUponLinking) {
|
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) {
|
@Override
|
||||||
return lookupLinkedAccount(playerUUID).thenCompose(userId -> {
|
protected CompletableFuture<Punishment> getDiscord(BanSyncConfig config, long userId) {
|
||||||
if (userId == null) {
|
JDA jda = discordSRV.jda();
|
||||||
// Unlinked
|
if (jda == null) {
|
||||||
return null;
|
return CompletableFutureUtil.failed(new SyncFail(BanSyncResult.NO_DISCORD_CONNECTION));
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (side) {
|
Guild guild = jda.getGuildById(config.serverId);
|
||||||
case DISCORD:
|
if (guild == null) {
|
||||||
JDA jda = discordSRV.jda();
|
// Server doesn't exist
|
||||||
if (jda == null) {
|
return CompletableFutureUtil.failed(new SyncFail(BanSyncResult.GUILD_DOESNT_EXIST));
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return getBan(guild, userId).thenApply(this::punishment);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void playerBanChange(UUID playerUUID, boolean newState, @Nullable Punishment punishment) {
|
private Punishment punishment(Guild.Ban ban) {
|
||||||
lookupLinkedAccount(playerUUID).thenCompose(userId -> {
|
return ban != null ? new Punishment(null, ban.getReason(), ban.getUser().getName()) : null;
|
||||||
if (userId == null) {
|
|
||||||
// Unlinked
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return changeUserBanState(userId, newState, punishment).whenComplete((r, t) -> {
|
|
||||||
if (t != null) {
|
|
||||||
logger().error("Failed to update ban state for " + Long.toUnsignedString(userId), t);
|
|
||||||
} else {
|
|
||||||
logger().debug("Updated " + Long.toUnsignedString(userId) + " ban state: " + r);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<BanSyncResult> changeUserBanState(long userId, boolean newState, @Nullable Punishment punishment) {
|
@Override
|
||||||
BanSyncConfig config = discordSRV.config().banSync;
|
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) {
|
if (config.direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||||
return CompletableFuture.completedFuture(BanSyncResult.WRONG_DIRECTION);
|
return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
JDA jda = discordSRV.jda();
|
JDA jda = discordSRV.jda();
|
||||||
@ -264,52 +228,23 @@ public class BanSyncModule extends AbstractModule<DiscordSRV> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UserSnowflake snowflake = UserSnowflake.fromId(userId);
|
UserSnowflake snowflake = UserSnowflake.fromId(userId);
|
||||||
return getBan(guild, userId).thenCompose(ban -> {
|
if (state != null) {
|
||||||
if (ban == null) {
|
return guild.ban(snowflake, config.discordMessageHoursToDelete, TimeUnit.HOURS)
|
||||||
if (newState) {
|
.reason(discordSRV.placeholderService().replacePlaceholders(config.discordBanReasonFormat, state))
|
||||||
return guild.ban(snowflake, config.discordMessageHoursToDelete, TimeUnit.HOURS)
|
.submit()
|
||||||
.reason(discordSRV.placeholderService().replacePlaceholders(config.discordBanReasonFormat, punishment))
|
.thenApply(v -> GenericSyncResults.ADD_DISCORD);
|
||||||
.submit()
|
} else {
|
||||||
.thenApply(v -> BanSyncResult.BAN_USER);
|
return guild.unban(snowflake)
|
||||||
} else {
|
.reason(discordSRV.placeholderService().replacePlaceholders(config.discordUnbanReasonFormat))
|
||||||
// Already unbanned
|
.submit()
|
||||||
return CompletableFuture.completedFuture(BanSyncResult.ALREADY_IN_SYNC);
|
.thenApply(v -> GenericSyncResults.REMOVE_DISCORD);
|
||||||
}
|
}
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void userBanChange(long userId, boolean newState, @Nullable Punishment punishment) {
|
@Override
|
||||||
lookupLinkedAccount(userId).thenCompose(playerUUID -> {
|
protected CompletableFuture<ISyncResult> applyGame(BanSyncConfig config, UUID playerUUID, Punishment state) {
|
||||||
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;
|
|
||||||
if (config.direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
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);
|
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 CompletableFuture.completedFuture(BanSyncResult.NO_PUNISHMENT_INTEGRATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bans.getBan(playerUUID).thenCompose(existingPunishment -> {
|
if (state != null) {
|
||||||
if (existingPunishment == null) {
|
String reason = discordSRV.placeholderService().replacePlaceholders(config.gameBanReasonFormat, state);
|
||||||
if (newState) {
|
String punisher = discordSRV.placeholderService().replacePlaceholders(config.gamePunisherFormat, state);
|
||||||
String reason = discordSRV.placeholderService().replacePlaceholders(config.gameBanReasonFormat, punishment);
|
return bans.addBan(playerUUID, null, reason, punisher)
|
||||||
String punisher = discordSRV.placeholderService().replacePlaceholders(config.gamePunisherFormat, punishment);
|
.thenApply(v -> GenericSyncResults.ADD_GAME);
|
||||||
return bans.addBan(playerUUID, null, reason, punisher)
|
} else {
|
||||||
.thenApply(v -> BanSyncResult.BAN_PLAYER);
|
return bans.removeBan(playerUUID).thenApply(v -> GenericSyncResults.REMOVE_GAME);
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.discordsrv.common.bansync.enums;
|
||||||
|
|
||||||
|
import com.discordsrv.common.sync.cause.ISyncCause;
|
||||||
|
|
||||||
|
public enum BanSyncCause implements ISyncCause {
|
||||||
|
|
||||||
|
PLAYER_BANNED
|
||||||
|
}
|
@ -1,30 +1,29 @@
|
|||||||
package com.discordsrv.common.bansync.enums;
|
package com.discordsrv.common.bansync.enums;
|
||||||
|
|
||||||
public enum BanSyncResult {
|
import com.discordsrv.common.sync.result.ISyncResult;
|
||||||
|
|
||||||
// Success, actioned
|
public enum BanSyncResult implements ISyncResult {
|
||||||
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"),
|
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
NO_PUNISHMENT_INTEGRATION("No punishment integration"),
|
NO_PUNISHMENT_INTEGRATION("No punishment integration"),
|
||||||
NO_DISCORD_CONNECTION("No Discord connection"),
|
NO_DISCORD_CONNECTION("No Discord connection"),
|
||||||
GUILD_DOESNT_EXIST("Guild doesn't exist"),
|
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) {
|
BanSyncResult(String format) {
|
||||||
this.prettyResult = prettyResult;
|
this.format = format;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String prettyResult() {
|
@Override
|
||||||
return prettyResult;
|
public boolean isSuccess() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFormat() {
|
||||||
|
return format;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.GameCommandExecution;
|
||||||
import com.discordsrv.common.command.combined.abstraction.Text;
|
import com.discordsrv.common.command.combined.abstraction.Text;
|
||||||
import com.discordsrv.common.command.game.abstraction.GameCommand;
|
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.future.util.CompletableFutureUtil;
|
||||||
import com.discordsrv.common.groupsync.GroupSyncModule;
|
import com.discordsrv.common.groupsync.GroupSyncModule;
|
||||||
import com.discordsrv.common.groupsync.enums.GroupSyncCause;
|
|
||||||
import com.discordsrv.common.permission.Permission;
|
import com.discordsrv.common.permission.Permission;
|
||||||
import com.discordsrv.common.player.IPlayer;
|
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 net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@ -79,16 +81,29 @@ public class ResyncCommand extends CombinedCommand {
|
|||||||
execution.runAsync(() -> {
|
execution.runAsync(() -> {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
CompletableFutureUtil.combine(resyncOnlinePlayers(module)).thenCompose(result -> {
|
List<CompletableFuture<? extends SyncSummary<?>>> futures = resyncOnlinePlayers(module);
|
||||||
List<CompletableFuture<ISyncResult>> results = new ArrayList<>();
|
CompletableFutureUtil.combineGeneric(futures).thenCompose(result -> {
|
||||||
for (Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>> map : result) {
|
List<CompletableFuture<?>> results = new ArrayList<>();
|
||||||
results.addAll(map.values());
|
for (SyncSummary<?> summary : result) {
|
||||||
|
results.add(summary.resultFuture());
|
||||||
}
|
}
|
||||||
return CompletableFutureUtil.combine(results);
|
return CompletableFutureUtil.combineGeneric(results);
|
||||||
}).whenComplete((results, t) -> {
|
}).whenComplete((__, t) -> {
|
||||||
Map<ISyncResult, AtomicInteger> resultCounts = new HashMap<>();
|
Map<ISyncResult, AtomicInteger> resultCounts = new HashMap<>();
|
||||||
int total = 0;
|
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) {
|
for (ISyncResult result : results) {
|
||||||
total++;
|
total++;
|
||||||
resultCounts.computeIfAbsent(result, key -> new AtomicInteger(0)).getAndIncrement();
|
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) {
|
private List<CompletableFuture<? extends SyncSummary<?>>> resyncOnlinePlayers(AbstractSyncModule<?, ?, ?, ?, ?> module) {
|
||||||
List<CompletableFuture<Map<GroupSyncConfig.PairConfig, CompletableFuture<ISyncResult>>>> futures = new ArrayList<>();
|
List<CompletableFuture<? extends SyncSummary<?>>> summaries = new ArrayList<>();
|
||||||
for (IPlayer player : discordSRV.playerProvider().allPlayers()) {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,6 @@ public class BanSyncConfig extends AbstractSyncConfig<BanSyncConfig, Void, Long>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String describe() {
|
public String describe() {
|
||||||
return "Ban sync";
|
return Long.toUnsignedString(serverId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,12 +69,12 @@ public class GroupSyncConfig {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "PairConfig{" + groupName + direction.arrow() + Long.toUnsignedString(roleId) + '}';
|
return "GroupSyncConfig$PairConfig{" + describe() + '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String describe() {
|
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) {
|
public static String makeGameId(String groupName, Set<String> serverContext) {
|
||||||
|
@ -9,6 +9,12 @@ import org.spongepowered.configurate.objectmapping.meta.Comment;
|
|||||||
|
|
||||||
import java.util.Arrays;
|
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
|
@ConfigSerializable
|
||||||
public abstract class AbstractSyncConfig<C extends AbstractSyncConfig<C, G, D>, G, D> {
|
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 abstract String describe();
|
||||||
|
|
||||||
public boolean validate(DiscordSRV discordSRV) {
|
public boolean validate(String syncName, DiscordSRV discordSRV) {
|
||||||
String label = describe();
|
String label = syncName + " (" + describe() + ")";
|
||||||
boolean invalidTieBreaker, invalidDirection = false;
|
boolean invalidTieBreaker, invalidDirection = false;
|
||||||
if ((invalidTieBreaker = (tieBreaker == null)) || (invalidDirection = (direction == null))) {
|
if ((invalidTieBreaker = (tieBreaker == null)) || (invalidDirection = (direction == null))) {
|
||||||
if (invalidTieBreaker) {
|
if (invalidTieBreaker) {
|
||||||
|
@ -46,6 +46,11 @@ public final class CompletableFutureUtil {
|
|||||||
return combine(futures.toArray(new CompletableFuture[0]));
|
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
|
@SafeVarargs
|
||||||
public static <T> CompletableFuture<List<T>> combine(CompletableFuture<T>... futures) {
|
public static <T> CompletableFuture<List<T>> combine(CompletableFuture<T>... futures) {
|
||||||
CompletableFuture<List<T>> future = new CompletableFuture<>();
|
CompletableFuture<List<T>> future = new CompletableFuture<>();
|
||||||
|
@ -9,15 +9,14 @@ import com.discordsrv.common.DiscordSRV;
|
|||||||
import com.discordsrv.common.config.main.GroupSyncConfig;
|
import com.discordsrv.common.config.main.GroupSyncConfig;
|
||||||
import com.discordsrv.common.debug.DebugGenerateEvent;
|
import com.discordsrv.common.debug.DebugGenerateEvent;
|
||||||
import com.discordsrv.common.debug.file.TextDebugFile;
|
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.future.util.CompletableFutureUtil;
|
||||||
import com.discordsrv.common.groupsync.enums.GroupSyncCause;
|
import com.discordsrv.common.groupsync.enums.GroupSyncCause;
|
||||||
import com.discordsrv.common.groupsync.enums.GroupSyncResult;
|
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.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.SyncFail;
|
||||||
import com.discordsrv.common.sync.enums.SyncResults;
|
import com.discordsrv.common.sync.result.GenericSyncResults;
|
||||||
import com.github.benmanes.caffeine.cache.Cache;
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@ -27,7 +26,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
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<Long, Map<Long, Boolean>> expectedDiscordChanges;
|
||||||
private final Cache<UUID, Map<String, Boolean>> expectedMinecraftChanges;
|
private final Cache<UUID, Map<String, Boolean>> expectedMinecraftChanges;
|
||||||
@ -43,13 +42,33 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long
|
|||||||
.build();
|
.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
|
@Override
|
||||||
public List<GroupSyncConfig.PairConfig> configs() {
|
public List<GroupSyncConfig.PairConfig> configs() {
|
||||||
return discordSRV.config().groupSync.pairs;
|
return discordSRV.config().groupSync.pairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isTrue(Boolean state) {
|
protected boolean isActive(Boolean state) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,51 +116,8 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long
|
|||||||
event.addFile(new TextDebugFile("group-sync.txt", builder));
|
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
|
// 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
|
@Subscribe
|
||||||
public void onDiscordMemberRoleAdd(DiscordMemberRoleAddEvent event) {
|
public void onDiscordMemberRoleAdd(DiscordMemberRoleAddEvent event) {
|
||||||
event.getRoles().forEach(role -> roleChanged(event.getMember().getUser().getId(), role.getId(), true));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lookupLinkedAccount(userId).whenComplete((playerUUID, t) -> {
|
discordChanged(GroupSyncCause.DISCORD_ROLE_CHANGE, Someone.of(userId), roleId, state);
|
||||||
if (playerUUID == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!permissionProvider.supportsOffline() && discordSRV.playerProvider().player(playerUUID) == null) {
|
|
||||||
logger().debug("Not running sync for " + playerUUID + ": permission provider does not support offline operations");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logSummary(playerUUID, GroupSyncCause.DISCORD_ROLE_CHANGE, discordChanged(userId, playerUUID, roleId, state));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void groupChanged(
|
private void groupChanged(
|
||||||
@ -202,17 +167,7 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!permissionProvider.supportsOffline() && discordSRV.playerProvider().player(playerUUID) == null) {
|
gameChanged(cause, Someone.of(playerUUID), context(groupName, serverContext), state);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lookupLinkedAccount(playerUUID).whenComplete((userId, t) -> {
|
|
||||||
if (userId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logSummary(playerUUID, cause, gameChanged(userId, playerUUID, context(groupName, serverContext), state));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PermissionModule.Groups getPermissionProvider() {
|
private PermissionModule.Groups getPermissionProvider() {
|
||||||
@ -237,31 +192,6 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long
|
|||||||
|
|
||||||
// Resync
|
// 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
|
@Override
|
||||||
public CompletableFuture<Boolean> getDiscord(GroupSyncConfig.PairConfig config, long userId) {
|
public CompletableFuture<Boolean> getDiscord(GroupSyncConfig.PairConfig config, long userId) {
|
||||||
DiscordRole role = discordSRV.discordAPI().getRoleById(config.roleId);
|
DiscordRole role = discordSRV.discordAPI().getRoleById(config.roleId);
|
||||||
@ -312,8 +242,8 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long
|
|||||||
|
|
||||||
return role.getGuild().retrieveMemberById(userId)
|
return role.getGuild().retrieveMemberById(userId)
|
||||||
.thenCompose(member -> state
|
.thenCompose(member -> state
|
||||||
? member.addRole(role).thenApply(v -> (ISyncResult) SyncResults.ADD_DISCORD)
|
? member.addRole(role).thenApply(v -> (ISyncResult) GenericSyncResults.ADD_DISCORD)
|
||||||
: member.removeRole(role).thenApply(v -> SyncResults.REMOVE_DISCORD)
|
: member.removeRole(role).thenApply(v -> GenericSyncResults.REMOVE_DISCORD)
|
||||||
).whenComplete((r, t) -> {
|
).whenComplete((r, t) -> {
|
||||||
if (t != null) {
|
if (t != null) {
|
||||||
//noinspection DataFlowIssue
|
//noinspection DataFlowIssue
|
||||||
@ -331,8 +261,8 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, String, Long
|
|||||||
|
|
||||||
CompletableFuture<ISyncResult> future =
|
CompletableFuture<ISyncResult> future =
|
||||||
state
|
state
|
||||||
? addGroup(playerUUID, config).thenApply(v -> SyncResults.ADD_GAME)
|
? addGroup(playerUUID, config).thenApply(v -> GenericSyncResults.ADD_GAME)
|
||||||
: removeGroup(playerUUID, config).thenApply(v -> SyncResults.REMOVE_GAME);
|
: removeGroup(playerUUID, config).thenApply(v -> GenericSyncResults.REMOVE_GAME);
|
||||||
return future.exceptionally(t -> {
|
return future.exceptionally(t -> {
|
||||||
//noinspection DataFlowIssue
|
//noinspection DataFlowIssue
|
||||||
expected.remove(config.groupName);
|
expected.remove(config.groupName);
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,13 +18,10 @@
|
|||||||
|
|
||||||
package com.discordsrv.common.groupsync.enums;
|
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),
|
DISCORD_ROLE_CHANGE("Discord role changed", true),
|
||||||
LUCKPERMS_NODE_CHANGE("LuckPerms node changed", true),
|
LUCKPERMS_NODE_CHANGE("LuckPerms node changed", true),
|
||||||
LUCKPERMS_TRACK("LuckPerms track promotion/demotion"),
|
LUCKPERMS_TRACK("LuckPerms track promotion/demotion"),
|
||||||
|
@ -18,39 +18,30 @@
|
|||||||
|
|
||||||
package com.discordsrv.common.groupsync.enums;
|
package com.discordsrv.common.groupsync.enums;
|
||||||
|
|
||||||
import com.discordsrv.common.sync.ISyncResult;
|
import com.discordsrv.common.sync.result.ISyncResult;
|
||||||
|
|
||||||
public enum GroupSyncResult implements ISyncResult {
|
public enum GroupSyncResult implements ISyncResult {
|
||||||
|
|
||||||
// Errors
|
// Error
|
||||||
ROLE_DOESNT_EXIST("Role doesn't exist"),
|
ROLE_DOESNT_EXIST("Role doesn't exist"),
|
||||||
ROLE_CANNOT_INTERACT("Bot doesn't have a role above the synced role (cannot interact)"),
|
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"),
|
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"),
|
PERMISSION_BACKEND_FAILED("Failed to interact with permission backend, 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"),
|
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|
||||||
final String prettyResult;
|
private final String format;
|
||||||
final boolean success;
|
|
||||||
|
|
||||||
GroupSyncResult(String prettyResult) {
|
GroupSyncResult(String format) {
|
||||||
this(prettyResult, false);
|
this.format = format;
|
||||||
}
|
|
||||||
|
|
||||||
GroupSyncResult(String prettyResult, boolean success) {
|
|
||||||
this.prettyResult = prettyResult;
|
|
||||||
this.success = success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSuccess() {
|
public boolean isSuccess() {
|
||||||
return success;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String getFormat() {
|
||||||
return prettyResult;
|
return format;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
107
common/src/main/java/com/discordsrv/common/someone/Someone.java
Normal file
107
common/src/main/java/com/discordsrv/common/someone/Someone.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,23 @@
|
|||||||
package com.discordsrv.common.sync;
|
package com.discordsrv.common.sync;
|
||||||
|
|
||||||
import com.discordsrv.api.DiscordSRVApi;
|
import com.discordsrv.api.DiscordSRVApi;
|
||||||
|
import com.discordsrv.api.event.bus.Subscribe;
|
||||||
import com.discordsrv.common.DiscordSRV;
|
import com.discordsrv.common.DiscordSRV;
|
||||||
import com.discordsrv.common.config.main.GroupSyncConfig;
|
import com.discordsrv.common.config.main.GroupSyncConfig;
|
||||||
import com.discordsrv.common.config.main.generic.AbstractSyncConfig;
|
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.future.util.CompletableFutureUtil;
|
||||||
import com.discordsrv.common.logging.NamedLogger;
|
import com.discordsrv.common.logging.NamedLogger;
|
||||||
import com.discordsrv.common.module.type.AbstractModule;
|
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.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.enums.SyncSide;
|
||||||
|
import com.discordsrv.common.sync.result.ISyncResult;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@ -19,7 +26,22 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.function.Consumer;
|
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<>();
|
protected final Map<C, Future<?>> syncs = new LinkedHashMap<>();
|
||||||
private final Map<G, List<C>> configsForGame = new ConcurrentHashMap<>();
|
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));
|
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();
|
public abstract List<C> configs();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -43,8 +75,9 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
|
|||||||
configsForGame.clear();
|
configsForGame.clear();
|
||||||
configsForDiscord.clear();
|
configsForDiscord.clear();
|
||||||
|
|
||||||
|
String syncName = syncName();
|
||||||
for (C config : configs()) {
|
for (C config : configs()) {
|
||||||
if (!config.isSet() || !config.validate(discordSRV)) {
|
if (!config.isSet() || !config.validate(syncName, discordSRV)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +89,7 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failed) {
|
if (failed) {
|
||||||
discordSRV.logger().error("Duplicate " + config.describe());
|
discordSRV.logger().error("Duplicate " + syncName + " (" + config.describe() + ")");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,155 +119,220 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<Long> lookupLinkedAccount(UUID player) {
|
private void resyncTimer(C config) {
|
||||||
return discordSRV.profileManager().lookupProfile(player)
|
for (IPlayer player : discordSRV.playerProvider().allPlayers()) {
|
||||||
.thenApply(Profile::userId)
|
resync(GenericSyncCauses.TIMER, config, Someone.of(player.uniqueId()));
|
||||||
.thenApply(userId -> {
|
}
|
||||||
if (userId == null) {
|
|
||||||
throw new SyncFail(SyncResults.NOT_LINKED);
|
|
||||||
}
|
|
||||||
return userId;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<UUID> lookupLinkedAccount(long userId) {
|
@Subscribe
|
||||||
return discordSRV.profileManager().lookupProfile(userId)
|
public void onPlayerConnected(PlayerConnectedEvent event) {
|
||||||
.thenApply(Profile::playerUUID)
|
resyncAll(GenericSyncCauses.GAME_JOIN, Someone.of(event.player()));
|
||||||
.thenApply(playerUUID -> {
|
|
||||||
if (playerUUID == null) {
|
|
||||||
throw new SyncFail(SyncResults.NOT_LINKED);
|
|
||||||
}
|
|
||||||
return playerUUID;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
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);
|
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> 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) {
|
protected CompletableFuture<ISyncResult> applyDiscordIfNot(C config, long userId, S state) {
|
||||||
return getDiscord(config, userId).thenCompose(value -> {
|
return getDiscord(config, userId).thenCompose(value -> {
|
||||||
boolean actualValue;
|
boolean actualValue;
|
||||||
if ((actualValue = isTrue(state)) == isTrue(value)) {
|
if ((actualValue = isActive(state)) == isActive(value)) {
|
||||||
return CompletableFuture.completedFuture(actualValue ? SyncResults.BOTH_TRUE : SyncResults.BOTH_FALSE);
|
return CompletableFuture.completedFuture(actualValue ? GenericSyncResults.BOTH_TRUE : GenericSyncResults.BOTH_FALSE);
|
||||||
} else {
|
} else {
|
||||||
return applyDiscord(config, userId, state);
|
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) {
|
protected CompletableFuture<ISyncResult> applyGameIfNot(C config, UUID playerUUID, S state) {
|
||||||
return getGame(config, playerUUID).thenCompose(value -> {
|
return getGame(config, playerUUID).thenCompose(value -> {
|
||||||
boolean actualValue;
|
boolean active;
|
||||||
if ((actualValue = isTrue(state)) == isTrue(value)) {
|
if ((active = isActive(state)) == isActive(value)) {
|
||||||
return CompletableFuture.completedFuture(actualValue ? SyncResults.BOTH_TRUE : SyncResults.BOTH_FALSE);
|
return CompletableFuture.completedFuture(active ? GenericSyncResults.BOTH_TRUE : GenericSyncResults.BOTH_FALSE);
|
||||||
} else {
|
} else {
|
||||||
return applyGame(config, playerUUID, state);
|
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);
|
List<C> gameConfigs = configsForDiscord.get(discordId);
|
||||||
if (gameConfigs == null) {
|
if (gameConfigs == null) {
|
||||||
return Collections.emptyMap();
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<C, CompletableFuture<ISyncResult>> futures = new LinkedHashMap<>();
|
return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> {
|
||||||
for (C config : gameConfigs) {
|
if (resolved == null) {
|
||||||
SyncDirection direction = config.direction;
|
return new SyncSummary<C>(cause, someone).fail(GenericSyncResults.NOT_LINKED);
|
||||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
|
||||||
// Not going Discord -> Minecraft
|
|
||||||
futures.put(config, CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION));
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
futures.put(config, applyGameIfNot(config, playerUUID, state));
|
SyncSummary<C> summary = new SyncSummary<>(cause, resolved);
|
||||||
|
for (C config : gameConfigs) {
|
||||||
// If the sync is bidirectional, also sync anything else linked to the same Minecraft id
|
SyncDirection direction = config.direction;
|
||||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
||||||
continue;
|
// Not going Discord -> Minecraft
|
||||||
}
|
summary.appendResult(config, GenericSyncResults.WRONG_DIRECTION);
|
||||||
|
|
||||||
List<C> discordConfigs = configsForGame.get(config.gameId());
|
|
||||||
if (discordConfigs == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (C gameConfig : discordConfigs) {
|
|
||||||
if (gameConfig.discordId() == discordId) {
|
|
||||||
continue;
|
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 summary;
|
||||||
return futures;
|
}).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);
|
List<C> discordConfigs = configsForGame.get(gameId);
|
||||||
if (discordConfigs == null) {
|
if (discordConfigs == null) {
|
||||||
return Collections.emptyMap();
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<C, CompletableFuture<ISyncResult>> futures = new LinkedHashMap<>();
|
return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> {
|
||||||
for (C config : discordConfigs) {
|
if (resolved == null) {
|
||||||
SyncDirection direction = config.direction;
|
return new SyncSummary<C>(cause, someone).fail(GenericSyncResults.NOT_LINKED);
|
||||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
|
||||||
// Not going Minecraft -> Discord
|
|
||||||
futures.put(config, CompletableFuture.completedFuture(SyncResults.WRONG_DIRECTION));
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
futures.put(config, applyDiscordIfNot(config, userId, state));
|
SyncSummary<C> summary = new SyncSummary<>(cause, resolved);
|
||||||
|
for (C config : discordConfigs) {
|
||||||
// If the sync is bidirectional, also sync anything else linked to the same Discord id
|
SyncDirection direction = config.direction;
|
||||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
||||||
continue;
|
// Not going Minecraft -> Discord
|
||||||
}
|
summary.appendResult(config, GenericSyncResults.WRONG_DIRECTION);
|
||||||
|
|
||||||
List<C> gameConfigs = configsForDiscord.get(config.discordId());
|
|
||||||
if (gameConfigs == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (C gameConfig : gameConfigs) {
|
|
||||||
if (gameConfig.gameId() == gameId) {
|
|
||||||
continue;
|
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 summary;
|
||||||
return futures;
|
}).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) {
|
SyncSummary<C> summary = new SyncSummary<>(cause, resolved);
|
||||||
return lookupLinkedAccount(playerUUID).thenApply(userId -> resyncAll(playerUUID, userId));
|
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) {
|
protected CompletableFuture<SyncSummary<C>> resync(ISyncCause cause, C config, Someone someone) {
|
||||||
return lookupLinkedAccount(userId).thenApply(playerUUID -> resyncAll(playerUUID, userId));
|
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) {
|
private CompletableFuture<ISyncResult> resync(C config, Someone.Resolved resolved) {
|
||||||
List<C> configs = configs();
|
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> gameGet = getGame(config, playerUUID);
|
||||||
CompletableFuture<S> discordGet = getDiscord(config, userId);
|
CompletableFuture<S> discordGet = getDiscord(config, userId);
|
||||||
|
|
||||||
@ -245,7 +343,7 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
|
|||||||
boolean bothState;
|
boolean bothState;
|
||||||
if ((bothState = (gameState != null)) == (discordState != null)) {
|
if ((bothState = (gameState != null)) == (discordState != null)) {
|
||||||
// Already in sync
|
// 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;
|
SyncSide side = config.tieBreaker;
|
||||||
@ -254,33 +352,33 @@ public abstract class AbstractSyncModule<DT extends DiscordSRV, G, D, C extends
|
|||||||
if (side == SyncSide.DISCORD) {
|
if (side == SyncSide.DISCORD) {
|
||||||
// Has Discord, add game
|
// Has Discord, add game
|
||||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
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 {
|
} else {
|
||||||
// Missing game, remove Discord
|
// Missing game, remove Discord
|
||||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
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 {
|
} else {
|
||||||
if (side == SyncSide.DISCORD) {
|
if (side == SyncSide.DISCORD) {
|
||||||
// Missing Discord, remove game
|
// Missing Discord, remove game
|
||||||
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
|
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 {
|
} else {
|
||||||
// Has game, add Discord
|
// Has game, add Discord
|
||||||
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
|
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 -> {
|
}).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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package com.discordsrv.common.sync;
|
|
||||||
|
|
||||||
public interface ISyncResult {
|
|
||||||
|
|
||||||
boolean isSuccess();
|
|
||||||
|
|
||||||
}
|
|
@ -1,5 +1,7 @@
|
|||||||
package com.discordsrv.common.sync;
|
package com.discordsrv.common.sync;
|
||||||
|
|
||||||
|
import com.discordsrv.common.sync.result.ISyncResult;
|
||||||
|
|
||||||
public class SyncFail extends RuntimeException {
|
public class SyncFail extends RuntimeException {
|
||||||
|
|
||||||
private final ISyncResult result;
|
private final ISyncResult result;
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.discordsrv.common.sync.cause;
|
||||||
|
|
||||||
|
public interface ISyncCause {
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user