Improvements & fixes to AbstractSyncModule

This commit is contained in:
Vankka 2024-06-24 02:28:30 +03:00
parent 72758ff888
commit 654bfb5907
No known key found for this signature in database
GPG Key ID: 62E48025ED4E7EBB
18 changed files with 409 additions and 90 deletions

View File

@ -1,3 +1,26 @@
/*
* This file is part of the DiscordSRV API, licensed under the MIT License
* Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.discordsrv.api.punishment; package com.discordsrv.api.punishment;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;

View File

@ -70,6 +70,9 @@ public class BukkitBanModule extends AbstractModule<BukkitDiscordSRV> implements
} }
return entryFuture.thenApply(ban -> { return entryFuture.thenApply(ban -> {
if (ban == null) {
return null;
}
Date expiration = ban.getExpiration(); Date expiration = ban.getExpiration();
return new Punishment(expiration != null ? expiration.toInstant() : null, ban.getReason(), ban.getSource()); return new Punishment(expiration != null ? expiration.toInstant() : null, ban.getReason(), ban.getSource());
}); });

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.bansync; package com.discordsrv.common.bansync;
import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent;
@ -71,7 +89,7 @@ public class BanSyncModule extends AbstractSyncModule<DiscordSRV, BanSyncConfig,
} }
@Override @Override
public String logName() { public String logFileName() {
return "bansync"; return "bansync";
} }
@ -91,8 +109,10 @@ public class BanSyncModule extends AbstractSyncModule<DiscordSRV, BanSyncConfig,
} }
@Override @Override
protected boolean isActive(Punishment state) { protected @Nullable ISyncResult doesStateMatch(Punishment one, Punishment two) {
return state != null; boolean oneActive = one != null;
boolean twoActive = two != null;
return (oneActive == twoActive) ? GenericSyncResults.both(oneActive) : null;
} }
private PunishmentEvent upsertEvent(long userId, boolean newState) { private PunishmentEvent upsertEvent(long userId, boolean newState) {
@ -211,7 +231,7 @@ public class BanSyncModule extends AbstractSyncModule<DiscordSRV, BanSyncConfig,
} }
@Override @Override
protected CompletableFuture<ISyncResult> applyDiscord(BanSyncConfig config, long userId, Punishment state) { protected CompletableFuture<ISyncResult> applyDiscord(BanSyncConfig config, long userId, Punishment newState) {
if (config.direction == SyncDirection.DISCORD_TO_MINECRAFT) { if (config.direction == SyncDirection.DISCORD_TO_MINECRAFT) {
return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION); return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION);
} }
@ -228,9 +248,10 @@ public class BanSyncModule extends AbstractSyncModule<DiscordSRV, BanSyncConfig,
} }
UserSnowflake snowflake = UserSnowflake.fromId(userId); UserSnowflake snowflake = UserSnowflake.fromId(userId);
if (state != null) { if (newState != null) {
return guild.ban(snowflake, config.discordMessageHoursToDelete, TimeUnit.HOURS) return guild.ban(snowflake, config.discordMessageHoursToDelete, TimeUnit.HOURS)
.reason(discordSRV.placeholderService().replacePlaceholders(config.discordBanReasonFormat, state)) .reason(discordSRV.placeholderService().replacePlaceholders(config.discordBanReasonFormat,
newState))
.submit() .submit()
.thenApply(v -> GenericSyncResults.ADD_DISCORD); .thenApply(v -> GenericSyncResults.ADD_DISCORD);
} else { } else {
@ -242,7 +263,7 @@ public class BanSyncModule extends AbstractSyncModule<DiscordSRV, BanSyncConfig,
} }
@Override @Override
protected CompletableFuture<ISyncResult> applyGame(BanSyncConfig config, UUID playerUUID, Punishment state) { protected CompletableFuture<ISyncResult> applyGame(BanSyncConfig config, UUID playerUUID, Punishment newState) {
if (config.direction == SyncDirection.MINECRAFT_TO_DISCORD) { if (config.direction == SyncDirection.MINECRAFT_TO_DISCORD) {
return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION); return CompletableFuture.completedFuture(GenericSyncResults.WRONG_DIRECTION);
} }
@ -252,9 +273,11 @@ public class BanSyncModule extends AbstractSyncModule<DiscordSRV, BanSyncConfig,
return CompletableFuture.completedFuture(BanSyncResult.NO_PUNISHMENT_INTEGRATION); return CompletableFuture.completedFuture(BanSyncResult.NO_PUNISHMENT_INTEGRATION);
} }
if (state != null) { if (newState != null) {
String reason = discordSRV.placeholderService().replacePlaceholders(config.gameBanReasonFormat, state); String reason = discordSRV.placeholderService().replacePlaceholders(config.gameBanReasonFormat,
String punisher = discordSRV.placeholderService().replacePlaceholders(config.gamePunisherFormat, state); newState);
String punisher = discordSRV.placeholderService().replacePlaceholders(config.gamePunisherFormat,
newState);
return bans.addBan(playerUUID, null, reason, punisher) return bans.addBan(playerUUID, null, reason, punisher)
.thenApply(v -> GenericSyncResults.ADD_GAME); .thenApply(v -> GenericSyncResults.ADD_GAME);
} else { } else {

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.bansync.enums; package com.discordsrv.common.bansync.enums;
import com.discordsrv.common.sync.cause.ISyncCause; import com.discordsrv.common.sync.cause.ISyncCause;

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.bansync.enums; package com.discordsrv.common.bansync.enums;
import com.discordsrv.common.sync.result.ISyncResult; import com.discordsrv.common.sync.result.ISyncResult;

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.config.main; package com.discordsrv.common.config.main;
import com.discordsrv.common.config.main.generic.AbstractSyncConfig; import com.discordsrv.common.config.main.generic.AbstractSyncConfig;

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.config.main.generic; package com.discordsrv.common.config.main.generic;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;

View File

@ -66,7 +66,7 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, GroupSyncCon
} }
@Override @Override
public String logName() { public String logFileName() {
return "groupsync"; return "groupsync";
} }
@ -86,8 +86,11 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, GroupSyncCon
} }
@Override @Override
protected boolean isActive(Boolean state) { protected @Nullable ISyncResult doesStateMatch(Boolean one, Boolean two) {
return state; if (one == two) {
return GenericSyncResults.both(one);
}
return null;
} }
@Override @Override
@ -154,8 +157,8 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, GroupSyncCon
groupChanged(player, groupName, serverContext, cause, false); groupChanged(player, groupName, serverContext, cause, false);
} }
private void roleChanged(long userId, long roleId, boolean state) { private void roleChanged(long userId, long roleId, boolean newState) {
if (checkExpectation(expectedDiscordChanges, userId, roleId, state)) { if (checkExpectation(expectedDiscordChanges, userId, roleId, newState)) {
return; return;
} }
@ -165,7 +168,7 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, GroupSyncCon
return; return;
} }
discordChanged(GroupSyncCause.DISCORD_ROLE_CHANGE, Someone.of(userId), roleId, state); discordChanged(GroupSyncCause.DISCORD_ROLE_CHANGE, Someone.of(userId), roleId, newState);
} }
private void groupChanged( private void groupChanged(
@ -173,7 +176,7 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, GroupSyncCon
String groupName, String groupName,
Set<String> serverContext, Set<String> serverContext,
GroupSyncCause cause, GroupSyncCause cause,
Boolean state boolean state
) { ) {
if (cause.isDiscordSRVCanCause() && checkExpectation(expectedMinecraftChanges, playerUUID, groupName, state)) { if (cause.isDiscordSRVCanCause() && checkExpectation(expectedMinecraftChanges, playerUUID, groupName, state)) {
return; return;
@ -247,7 +250,7 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, GroupSyncCon
} }
@Override @Override
public CompletableFuture<ISyncResult> applyDiscord(GroupSyncConfig.PairConfig config, long userId, Boolean state) { public CompletableFuture<ISyncResult> applyDiscord(GroupSyncConfig.PairConfig config, long userId, Boolean newState) {
DiscordRole role = discordSRV.discordAPI().getRoleById(config.roleId); DiscordRole role = discordSRV.discordAPI().getRoleById(config.roleId);
if (role == null) { if (role == null) {
return CompletableFutureUtil.failed(new SyncFail(GroupSyncResult.ROLE_DOESNT_EXIST)); return CompletableFutureUtil.failed(new SyncFail(GroupSyncResult.ROLE_DOESNT_EXIST));
@ -255,11 +258,11 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, GroupSyncCon
Map<Long, Boolean> expected = expectedDiscordChanges.get(userId, key -> new ConcurrentHashMap<>()); Map<Long, Boolean> expected = expectedDiscordChanges.get(userId, key -> new ConcurrentHashMap<>());
if (expected != null) { if (expected != null) {
expected.put(config.roleId, state); expected.put(config.roleId, newState);
} }
return role.getGuild().retrieveMemberById(userId) return role.getGuild().retrieveMemberById(userId)
.thenCompose(member -> state .thenCompose(member -> newState
? member.addRole(role).thenApply(v -> (ISyncResult) GenericSyncResults.ADD_DISCORD) ? member.addRole(role).thenApply(v -> (ISyncResult) GenericSyncResults.ADD_DISCORD)
: member.removeRole(role).thenApply(v -> GenericSyncResults.REMOVE_DISCORD) : member.removeRole(role).thenApply(v -> GenericSyncResults.REMOVE_DISCORD)
).whenComplete((r, t) -> { ).whenComplete((r, t) -> {
@ -271,14 +274,14 @@ public class GroupSyncModule extends AbstractSyncModule<DiscordSRV, GroupSyncCon
} }
@Override @Override
public CompletableFuture<ISyncResult> applyGame(GroupSyncConfig.PairConfig config, UUID playerUUID, Boolean state) { public CompletableFuture<ISyncResult> applyGame(GroupSyncConfig.PairConfig config, UUID playerUUID, Boolean newState) {
Map<String, Boolean> expected = expectedMinecraftChanges.get(playerUUID, key -> new ConcurrentHashMap<>()); Map<String, Boolean> expected = expectedMinecraftChanges.get(playerUUID, key -> new ConcurrentHashMap<>());
if (expected != null) { if (expected != null) {
expected.put(config.groupName, state); expected.put(config.groupName, newState);
} }
CompletableFuture<ISyncResult> future = CompletableFuture<ISyncResult> future =
state newState
? addGroup(playerUUID, config).thenApply(v -> GenericSyncResults.ADD_GAME) ? addGroup(playerUUID, config).thenApply(v -> GenericSyncResults.ADD_GAME)
: removeGroup(playerUUID, config).thenApply(v -> GenericSyncResults.REMOVE_GAME); : removeGroup(playerUUID, config).thenApply(v -> GenericSyncResults.REMOVE_GAME);
return future.exceptionally(t -> { return future.exceptionally(t -> {

View File

@ -178,8 +178,8 @@ public class LuckPermsIntegration extends PluginIntegration<DiscordSRV> implemen
InheritanceNode node = InheritanceNode.builder(group).context(contexts).build(); InheritanceNode node = InheritanceNode.builder(group).context(contexts).build();
DataMutateResult result = function.apply(user.data(), node); DataMutateResult result = function.apply(user.data(), node);
if (result != DataMutateResult.SUCCESS) { if (!result.wasSuccessful()) {
return CompletableFutureUtil.failed(new MessageException(result.name())); return CompletableFutureUtil.failed(new MessageException("Group mutate failed: " + result.name()));
} }
return luckPerms.getUserManager().saveUser(user); return luckPerms.getUserManager().saveUser(user);

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.someone; package com.discordsrv.common.someone;
import com.discordsrv.api.discord.entity.DiscordUser; import com.discordsrv.api.discord.entity.DiscordUser;

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.sync; package com.discordsrv.common.sync;
import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.DiscordSRVApi;
@ -17,11 +35,12 @@ import com.discordsrv.common.sync.enums.SyncDirection;
import com.discordsrv.common.sync.result.GenericSyncResults; 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 com.discordsrv.common.sync.result.ISyncResult;
import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable;
import java.time.Duration; import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap; 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;
@ -51,17 +70,17 @@ public abstract class AbstractSyncModule<
super(discordSRV, new NamedLogger(discordSRV, loggerName)); super(discordSRV, new NamedLogger(discordSRV, loggerName));
} }
public abstract String syncName(); protected abstract String syncName();
public abstract String logName(); protected abstract String logFileName();
public abstract String gameTerm(); protected abstract String gameTerm();
public abstract String discordTerm(); protected abstract String discordTerm();
/** /**
* Returns a list of all in use synchronizables. * Returns a list of all in use synchronizables.
* @return a list of configurations for synchronizables * @return a list of configurations for synchronizables
*/ */
public abstract List<C> configs(); protected abstract List<C> configs();
@Override @Override
public void reload(Consumer<DiscordSRVApi.ReloadResult> resultConsumer) { public void reload(Consumer<DiscordSRVApi.ReloadResult> resultConsumer) {
@ -131,12 +150,11 @@ public abstract class AbstractSyncModule<
} }
/** /**
* Check if the provided state is active or inactive, should this not match for the state of the two sides, synchronization will occur. * Checks if the given new and current state are the same, basically meaning that no update is necessary.
* * @return the result stating the states are the same, otherwise {@code null} to state they are not
* @param state the state
* @return {@code true} indicating the provided state is "active"
*/ */
protected abstract boolean isActive(S state); @Nullable
protected abstract ISyncResult doesStateMatch(S one, S two);
/** /**
* Gets the current state of the provided config for the specified user on Discord. * Gets the current state of the provided config for the specified user on Discord.
@ -161,18 +179,18 @@ public abstract class AbstractSyncModule<
* *
* @param config the configuration for the synchronizable * @param config the configuration for the synchronizable
* @param userId the Discord user id * @param userId the Discord user id
* @param state the state to apply * @param newState the newState to apply
* @return a future with the result of the synchronization * @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 newState);
protected CompletableFuture<ISyncResult> applyDiscordIfNot(C config, long userId, S state) { protected CompletableFuture<ISyncResult> applyDiscordIfDoesNotMatch(C config, long userId, S newState) {
return getDiscord(config, userId).thenCompose(value -> { return getDiscord(config, userId).thenCompose(currentState -> {
boolean actualValue; ISyncResult result = doesStateMatch(newState, currentState);
if ((actualValue = isActive(state)) == isActive(value)) { if (result != null) {
return CompletableFuture.completedFuture(actualValue ? GenericSyncResults.BOTH_TRUE : GenericSyncResults.BOTH_FALSE); return CompletableFuture.completedFuture(result);
} else { } else {
return applyDiscord(config, userId, state); return applyDiscord(config, userId, newState);
} }
}); });
} }
@ -182,23 +200,23 @@ public abstract class AbstractSyncModule<
* *
* @param config the configuration for the synchronizable * @param config the configuration for the synchronizable
* @param playerUUID the Minecraft player {@link UUID} * @param playerUUID the Minecraft player {@link UUID}
* @param state the state to apply * @param newState the newState to apply
* @return a future with the result of the synchronization * @return a future with the result of the synchronization
*/ */
protected abstract CompletableFuture<ISyncResult> applyGame(C config, UUID playerUUID, S state); protected abstract CompletableFuture<ISyncResult> applyGame(C config, UUID playerUUID, S newState);
protected CompletableFuture<ISyncResult> applyGameIfNot(C config, UUID playerUUID, S state) { protected CompletableFuture<ISyncResult> applyGameIfDoesNotMatch(C config, UUID playerUUID, S newState) {
return getGame(config, playerUUID).thenCompose(value -> { return getGame(config, playerUUID).thenCompose(currentState -> {
boolean active; ISyncResult result = doesStateMatch(currentState, newState);
if ((active = isActive(state)) == isActive(value)) { if (result != null) {
return CompletableFuture.completedFuture(active ? GenericSyncResults.BOTH_TRUE : GenericSyncResults.BOTH_FALSE); return CompletableFuture.completedFuture(result);
} else { } else {
return applyGame(config, playerUUID, state); return applyGame(config, playerUUID, newState);
} }
}); });
} }
protected CompletableFuture<SyncSummary<C>> discordChanged(ISyncCause cause, Someone someone, D discordId, S state) { protected CompletableFuture<SyncSummary<C>> discordChanged(ISyncCause cause, Someone someone, D discordId, S newState) {
List<C> gameConfigs = configsForDiscord.get(discordId); List<C> gameConfigs = configsForDiscord.get(discordId);
if (gameConfigs == null) { if (gameConfigs == null) {
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
@ -206,10 +224,10 @@ public abstract class AbstractSyncModule<
return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> { return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> {
if (resolved == null) { if (resolved == null) {
return new SyncSummary<C>(cause, someone).fail(GenericSyncResults.NOT_LINKED); return new SyncSummary<>(this, cause, someone).fail(GenericSyncResults.NOT_LINKED);
} }
SyncSummary<C> summary = new SyncSummary<>(cause, resolved); SyncSummary<C> summary = new SyncSummary<>(this, cause, resolved);
for (C config : gameConfigs) { for (C config : gameConfigs) {
SyncDirection direction = config.direction; SyncDirection direction = config.direction;
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) { if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
@ -218,7 +236,7 @@ public abstract class AbstractSyncModule<
continue; continue;
} }
summary.appendResult(config, applyGameIfNot(config, resolved.playerUUID(), state)); summary.appendResult(config, applyGameIfDoesNotMatch(config, resolved.playerUUID(), newState));
// If the sync is bidirectional, also sync anything else linked to the same Minecraft id // If the sync is bidirectional, also sync anything else linked to the same Minecraft id
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) { if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
@ -231,11 +249,11 @@ public abstract class AbstractSyncModule<
} }
for (C gameConfig : discordConfigs) { for (C gameConfig : discordConfigs) {
if (gameConfig.discordId() == discordId) { if (Objects.equals(gameConfig.discordId(), discordId)) {
continue; continue;
} }
summary.appendResult(gameConfig, applyDiscordIfNot(gameConfig, resolved.userId(), state)); summary.appendResult(gameConfig, applyDiscordIfDoesNotMatch(gameConfig, resolved.userId(), newState));
} }
} }
return summary; return summary;
@ -246,7 +264,7 @@ public abstract class AbstractSyncModule<
}); });
} }
protected CompletableFuture<SyncSummary<C>> gameChanged(ISyncCause cause, Someone someone, G gameId, S state) { protected CompletableFuture<SyncSummary<C>> gameChanged(ISyncCause cause, Someone someone, G gameId, S newState) {
List<C> discordConfigs = configsForGame.get(gameId); List<C> discordConfigs = configsForGame.get(gameId);
if (discordConfigs == null) { if (discordConfigs == null) {
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
@ -254,10 +272,10 @@ public abstract class AbstractSyncModule<
return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> { return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> {
if (resolved == null) { if (resolved == null) {
return new SyncSummary<C>(cause, someone).fail(GenericSyncResults.NOT_LINKED); return new SyncSummary<>(this, cause, someone).fail(GenericSyncResults.NOT_LINKED);
} }
SyncSummary<C> summary = new SyncSummary<>(cause, resolved); SyncSummary<C> summary = new SyncSummary<>(this, cause, resolved);
for (C config : discordConfigs) { for (C config : discordConfigs) {
SyncDirection direction = config.direction; SyncDirection direction = config.direction;
if (direction == SyncDirection.DISCORD_TO_MINECRAFT) { if (direction == SyncDirection.DISCORD_TO_MINECRAFT) {
@ -266,7 +284,7 @@ public abstract class AbstractSyncModule<
continue; continue;
} }
summary.appendResult(config, applyDiscordIfNot(config, resolved.userId(), state)); summary.appendResult(config, applyDiscordIfDoesNotMatch(config, resolved.userId(), newState));
// If the sync is bidirectional, also sync anything else linked to the same Discord id // If the sync is bidirectional, also sync anything else linked to the same Discord id
if (direction == SyncDirection.MINECRAFT_TO_DISCORD) { if (direction == SyncDirection.MINECRAFT_TO_DISCORD) {
@ -279,11 +297,11 @@ public abstract class AbstractSyncModule<
} }
for (C gameConfig : gameConfigs) { for (C gameConfig : gameConfigs) {
if (gameConfig.gameId() == gameId) { if (Objects.equals(gameConfig.gameId(), gameId)) {
continue; continue;
} }
summary.appendResult(gameConfig, applyGameIfNot(gameConfig, resolved.playerUUID(), state)); summary.appendResult(gameConfig, applyGameIfDoesNotMatch(gameConfig, resolved.playerUUID(), newState));
} }
} }
return summary; return summary;
@ -297,11 +315,11 @@ public abstract class AbstractSyncModule<
public CompletableFuture<SyncSummary<C>> resyncAll(ISyncCause cause, Someone someone) { public CompletableFuture<SyncSummary<C>> resyncAll(ISyncCause cause, Someone someone) {
return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> { return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> {
if (resolved == null) { if (resolved == null) {
return new SyncSummary<C>(cause, someone).fail(GenericSyncResults.NOT_LINKED); return new SyncSummary<>(this, cause, someone).fail(GenericSyncResults.NOT_LINKED);
} }
SyncSummary<C> summary = new SyncSummary<>(cause, resolved); SyncSummary<C> summary = new SyncSummary<>(this, cause, resolved);
List<C> configs = configs(); Set<C> configs = syncs.keySet();
for (C config : configs) { for (C config : configs) {
summary.appendResult(config, resync(config, resolved)); summary.appendResult(config, resync(config, resolved));
@ -317,10 +335,10 @@ public abstract class AbstractSyncModule<
protected CompletableFuture<SyncSummary<C>> resync(ISyncCause cause, C config, Someone someone) { protected CompletableFuture<SyncSummary<C>> resync(ISyncCause cause, C config, Someone someone) {
return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> { return someone.withLinkedAccounts(discordSRV).thenApply(resolved -> {
if (resolved == null) { if (resolved == null) {
return new SyncSummary<C>(cause, someone).fail(GenericSyncResults.NOT_LINKED); return new SyncSummary<>(this, cause, someone).fail(GenericSyncResults.NOT_LINKED);
} }
return new SyncSummary<C>(cause, resolved) return new SyncSummary<C>(this, cause, resolved)
.appendResult(config, resync(config, resolved)); .appendResult(config, resync(config, resolved));
}).whenComplete((summary, t) -> { }).whenComplete((summary, t) -> {
if (summary != null) { if (summary != null) {
@ -340,10 +358,9 @@ public abstract class AbstractSyncModule<
S gameState = gameGet.join(); S gameState = gameGet.join();
S discordState = discordGet.join(); S discordState = discordGet.join();
boolean bothState; ISyncResult alreadyInSyncResult = doesStateMatch(gameState, discordState);
if ((bothState = (gameState != null)) == (discordState != null)) { if (alreadyInSyncResult != null) {
// Already in sync return CompletableFuture.completedFuture(alreadyInSyncResult);
return CompletableFuture.completedFuture((ISyncResult) (bothState ? GenericSyncResults.BOTH_TRUE : GenericSyncResults.BOTH_FALSE));
} }
SyncSide side = config.tieBreaker; SyncSide side = config.tieBreaker;
@ -392,45 +409,71 @@ public abstract class AbstractSyncModule<
private String formatResults(SyncSummary<C> summary, List<String> results) { private String formatResults(SyncSummary<C> summary, List<String> results) {
int count = results.size(); int count = results.size();
return summary.who().toString() return summary.who() + " (sync cause: " + summary.cause() + ")"
+ (count == 1 ? ": " : "\n") + (count == 1 ? ": " : "\n")
+ String.join("\n", results); + String.join("\n", results);
} }
private void logSummary(SyncSummary<C> summary) { private void logSummary(SyncSummary<C> summary) {
summary.resultFuture().whenComplete((results, t) -> { summary.resultFuture().whenComplete((results, t) -> {
Throwable throwableToLog = null;
if (t != null) { if (t != null) {
logger().error("Failed to " + syncName() + " " + summary.who(), t); while (t instanceof CompletionException) {
return; t = t.getCause();
}
if (t instanceof SyncFail) {
SyncFail fail = (SyncFail) t;
summary.fail(fail.getResult());
throwableToLog = fail.getCause();
} else {
logger().error("Failed to " + syncName() + " " + summary.who() + " (sync cause: " + summary.cause() + ")", t);
return;
}
} }
ISyncResult allFailReason = summary.allFailReason(); ISyncResult allFailReason = summary.allFailReason();
if (allFailReason != null) { if (allFailReason != null) {
String reason = allFailReason.format(gameTerm(), discordTerm()); String reason = allFailReason.format(gameTerm(), discordTerm());
logger().debug("Failed to " + syncName() + " " + summary.who() + ": " + reason); String message = "Failed to " + syncName() + " " + summary.who() + " (sync cause: " + summary.cause() + "): " + reason;
if (!allFailReason.isSuccess()) {
logger().error(message, throwableToLog);
} else {
logger().debug(message, throwableToLog);
}
return; return;
} }
List<String> logResults = new ArrayList<>(); Map<ISyncResult, List<String>> groupedResults = new LinkedHashMap<>();
List<String> auditResults = new ArrayList<>();
for (Map.Entry<C, ISyncResult> entry : results.entrySet()) { for (Map.Entry<C, ISyncResult> entry : results.entrySet()) {
C config = entry.getKey(); C config = entry.getKey();
ISyncResult result = entry.getValue(); ISyncResult result = entry.getValue();
String log = config.describe(); groupedResults.computeIfAbsent(result, key -> new ArrayList<>()).add(config.describe());
if (StringUtils.isEmpty(log)) { }
log += ": ";
}
log += result.format(gameTerm(), discordTerm());
logResults.add(log); List<String> successResults = new ArrayList<>();
List<String> failResults = new ArrayList<>();
for (Map.Entry<ISyncResult, List<String>> entry : groupedResults.entrySet()) {
ISyncResult result = entry.getKey();
String line = result.format(gameTerm(), discordTerm())
+ ": [" + String.join(", ", entry.getValue()) + "]";
if (result.isSuccess()) { if (result.isSuccess()) {
auditResults.add(log); successResults.add(line);
} else {
failResults.add(line);
} }
} }
logger().debug(formatResults(summary, logResults)); boolean anySuccess = !successResults.isEmpty();
discordSRV.logger().writeLogForCurrentDay(logName(), formatResults(summary, auditResults)); boolean anyFail = !failResults.isEmpty();
String partially = anySuccess && anyFail ? " partially" : "";
if (anySuccess) {
logger().debug(syncName() + partially + " succeeded for " + formatResults(summary, successResults));
}
if (anyFail) {
logger().error(syncName() + partially + " failed for " + formatResults(summary, failResults));
}
discordSRV.logger().writeLogForCurrentDay(logFileName(), formatResults(summary, successResults));
}); });
} }

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.sync; package com.discordsrv.common.sync;
import com.discordsrv.common.sync.result.ISyncResult; import com.discordsrv.common.sync.result.ISyncResult;

View File

@ -1,5 +1,24 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.sync; package com.discordsrv.common.sync;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.generic.AbstractSyncConfig; import com.discordsrv.common.config.main.generic.AbstractSyncConfig;
import com.discordsrv.common.future.util.CompletableFutureUtil; import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.someone.Someone; import com.discordsrv.common.someone.Someone;
@ -9,16 +28,19 @@ import com.discordsrv.common.sync.result.ISyncResult;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class SyncSummary<C extends AbstractSyncConfig<C, ?, ?>> { public class SyncSummary<C extends AbstractSyncConfig<C, ?, ?>> {
private final AbstractSyncModule<? extends DiscordSRV, C, ?, ?, ?> syncModule;
private final ISyncCause cause; private final ISyncCause cause;
private final Someone who; private final Someone who;
private ISyncResult allFailReason; private ISyncResult allFailReason;
private final Map<C, CompletableFuture<ISyncResult>> results = new ConcurrentHashMap<>(); private final Map<C, CompletableFuture<ISyncResult>> results = new ConcurrentHashMap<>();
public SyncSummary(ISyncCause cause, Someone who) { public SyncSummary(AbstractSyncModule<? extends DiscordSRV, C, ?, ?, ?> syncModule, ISyncCause cause, Someone who) {
this.syncModule = syncModule;
this.cause = cause; this.cause = cause;
this.who = who; this.who = who;
} }
@ -55,7 +77,25 @@ public class SyncSummary<C extends AbstractSyncConfig<C, ?, ?>> {
.thenApply((__) -> { .thenApply((__) -> {
Map<C, ISyncResult> results = new HashMap<>(); Map<C, ISyncResult> results = new HashMap<>();
for (Map.Entry<C, CompletableFuture<ISyncResult>> entry : this.results.entrySet()) { for (Map.Entry<C, CompletableFuture<ISyncResult>> entry : this.results.entrySet()) {
results.put(entry.getKey(), entry.getValue().join()); results.put(entry.getKey(), entry.getValue().exceptionally(t -> {
while (t instanceof CompletionException) {
t = t.getCause();
}
Throwable throwableToLog = t;
ISyncResult result = null;
if (t instanceof SyncFail) {
throwableToLog = t.getCause();
result = ((SyncFail) t).getResult();
}
if (throwableToLog != null) {
syncModule.logger().error(
"Error in " + syncModule.syncName() + " "
+ entry.getKey().describe() + " for " + who()
+ " (sync cause: " + cause() + ")", throwableToLog);
}
return result;
}).join());
} }
return results; return results;
}); });

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.sync.cause; package com.discordsrv.common.sync.cause;
public enum GenericSyncCauses implements ISyncCause { public enum GenericSyncCauses implements ISyncCause {

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.sync.cause; package com.discordsrv.common.sync.cause;
public interface ISyncCause { public interface ISyncCause {

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.sync.result; package com.discordsrv.common.sync.result;
public enum GenericSyncResults implements ISyncResult { public enum GenericSyncResults implements ISyncResult {
@ -18,6 +36,10 @@ public enum GenericSyncResults implements ISyncResult {
; ;
public static GenericSyncResults both(boolean value) {
return value ? BOTH_TRUE : BOTH_FALSE;
}
private final String message; private final String message;
private final boolean success; private final boolean success;

View File

@ -1,3 +1,21 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 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.sync.result; package com.discordsrv.common.sync.result;
import com.discordsrv.api.placeholder.util.Placeholders; import com.discordsrv.api.placeholder.util.Placeholders;