diff --git a/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java b/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java index 6d7c7fde..1c37be85 100644 --- a/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java +++ b/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java @@ -27,6 +27,7 @@ import com.discordsrv.api.component.MinecraftComponentFactory; import com.discordsrv.api.discord.api.DiscordAPI; import com.discordsrv.api.discord.connection.DiscordConnectionDetails; import com.discordsrv.api.event.bus.EventBus; +import com.discordsrv.api.linking.LinkingBackend; import com.discordsrv.api.placeholder.PlaceholderService; import com.discordsrv.api.player.DiscordSRVPlayer; import com.discordsrv.api.player.IPlayerProvider; @@ -58,6 +59,8 @@ public interface DiscordSRVApi { @NotNull EventBus eventBus(); + LinkingBackend linkingBackend(); + /** * DiscordSRV's own placeholder service. * @return the {@link PlaceholderService} instance. diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuildMember.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuildMember.java index d4faf025..975ebdae 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuildMember.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuildMember.java @@ -33,6 +33,7 @@ import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; /** * A Discord server member. @@ -60,6 +61,27 @@ public interface DiscordGuildMember extends DiscordUser, Mentionable { @NotNull List getRoles(); + /** + * Checks if the member has the given role. + * @param role the role to check for + * @return {@code true} if the member has the role + */ + boolean hasRole(@NotNull DiscordRole role); + + /** + * Gives the given role to this member. + * @param role the role to give + * @return a future + */ + CompletableFuture addRole(@NotNull DiscordRole role); + + /** + * Takes the given role from this member. + * @param role the role to take + * @return a future + */ + CompletableFuture removeRole(@NotNull DiscordRole role); + /** * Gets the effective name of this Discord server member. * @return the Discord server member's effective name diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordRole.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordRole.java index ffdb550b..2a2e2b1b 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordRole.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordRole.java @@ -41,6 +41,13 @@ public interface DiscordRole extends Snowflake, Mentionable { */ Color DEFAULT_COLOR = new Color(0xFFFFFF); + /** + * The Discord server this role is from. + * @return the Discord server + */ + @NotNull + DiscordGuild getGuild(); + /** * Gets the name of the Discord role. * @return the role name diff --git a/api/src/main/java/com/discordsrv/api/discord/events/DiscordEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/DiscordEvent.java new file mode 100644 index 00000000..ceb3b006 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/events/DiscordEvent.java @@ -0,0 +1,29 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 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.discord.events; + +import com.discordsrv.api.event.events.Event; + +public interface DiscordEvent extends Event { +} diff --git a/api/src/main/java/com/discordsrv/api/discord/events/member/AbstractDiscordMemberEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/member/AbstractDiscordMemberEvent.java new file mode 100644 index 00000000..e515fac9 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/events/member/AbstractDiscordMemberEvent.java @@ -0,0 +1,40 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 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.discord.events.member; + +import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; +import com.discordsrv.api.discord.events.DiscordEvent; + +public abstract class AbstractDiscordMemberEvent implements DiscordEvent { + + private final DiscordGuildMember member; + + public AbstractDiscordMemberEvent(DiscordGuildMember member) { + this.member = member; + } + + public DiscordGuildMember getMember() { + return member; + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/events/member/role/AbstractDiscordMemberRoleChangeEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/member/role/AbstractDiscordMemberRoleChangeEvent.java new file mode 100644 index 00000000..101fe61d --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/events/member/role/AbstractDiscordMemberRoleChangeEvent.java @@ -0,0 +1,44 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 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.discord.events.member.role; + +import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; +import com.discordsrv.api.discord.api.entity.guild.DiscordRole; +import com.discordsrv.api.discord.events.member.AbstractDiscordMemberEvent; + +import java.util.List; + +public abstract class AbstractDiscordMemberRoleChangeEvent extends AbstractDiscordMemberEvent { + + private final List roles; + + public AbstractDiscordMemberRoleChangeEvent(DiscordGuildMember member, List roles) { + super(member); + this.roles = roles; + } + + public List getRoles() { + return roles; + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/events/member/role/DiscordMemberRoleAddEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/member/role/DiscordMemberRoleAddEvent.java new file mode 100644 index 00000000..8defe4db --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/events/member/role/DiscordMemberRoleAddEvent.java @@ -0,0 +1,36 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 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.discord.events.member.role; + +import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; +import com.discordsrv.api.discord.api.entity.guild.DiscordRole; + +import java.util.List; + +public class DiscordMemberRoleAddEvent extends AbstractDiscordMemberRoleChangeEvent { + + public DiscordMemberRoleAddEvent(DiscordGuildMember member, List roles) { + super(member, roles); + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/events/member/role/DiscordMemberRoleRemoveEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/member/role/DiscordMemberRoleRemoveEvent.java new file mode 100644 index 00000000..acaa31d9 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/events/member/role/DiscordMemberRoleRemoveEvent.java @@ -0,0 +1,36 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 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.discord.events.member.role; + +import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; +import com.discordsrv.api.discord.api.entity.guild.DiscordRole; + +import java.util.List; + +public class DiscordMemberRoleRemoveEvent extends AbstractDiscordMemberRoleChangeEvent { + + public DiscordMemberRoleRemoveEvent(DiscordGuildMember member, List roles) { + super(member, roles); + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/events/AbstractDiscordMessageEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/message/AbstractDiscordMessageEvent.java similarity index 94% rename from api/src/main/java/com/discordsrv/api/discord/events/AbstractDiscordMessageEvent.java rename to api/src/main/java/com/discordsrv/api/discord/events/message/AbstractDiscordMessageEvent.java index 95ce46e1..25a65b12 100644 --- a/api/src/main/java/com/discordsrv/api/discord/events/AbstractDiscordMessageEvent.java +++ b/api/src/main/java/com/discordsrv/api/discord/events/message/AbstractDiscordMessageEvent.java @@ -21,18 +21,18 @@ * SOFTWARE. */ -package com.discordsrv.api.discord.events; +package com.discordsrv.api.discord.events.message; import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordThreadChannel; -import com.discordsrv.api.event.events.Event; +import com.discordsrv.api.discord.events.DiscordEvent; import org.jetbrains.annotations.NotNull; import java.util.Optional; -public abstract class AbstractDiscordMessageEvent implements Event { +public abstract class AbstractDiscordMessageEvent implements DiscordEvent { private final DiscordMessageChannel channel; diff --git a/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageDeleteEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/message/DiscordMessageDeleteEvent.java similarity index 97% rename from api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageDeleteEvent.java rename to api/src/main/java/com/discordsrv/api/discord/events/message/DiscordMessageDeleteEvent.java index 9ed086f7..97ad8e21 100644 --- a/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageDeleteEvent.java +++ b/api/src/main/java/com/discordsrv/api/discord/events/message/DiscordMessageDeleteEvent.java @@ -21,7 +21,7 @@ * SOFTWARE. */ -package com.discordsrv.api.discord.events; +package com.discordsrv.api.discord.events.message; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; diff --git a/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageReceiveEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/message/DiscordMessageReceiveEvent.java similarity index 97% rename from api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageReceiveEvent.java rename to api/src/main/java/com/discordsrv/api/discord/events/message/DiscordMessageReceiveEvent.java index 7727862d..8d940547 100644 --- a/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageReceiveEvent.java +++ b/api/src/main/java/com/discordsrv/api/discord/events/message/DiscordMessageReceiveEvent.java @@ -21,7 +21,7 @@ * SOFTWARE. */ -package com.discordsrv.api.discord.events; +package com.discordsrv.api.discord.events.message; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; diff --git a/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageUpdateEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/message/DiscordMessageUpdateEvent.java similarity index 97% rename from api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageUpdateEvent.java rename to api/src/main/java/com/discordsrv/api/discord/events/message/DiscordMessageUpdateEvent.java index f737f1ea..f6d9e468 100644 --- a/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageUpdateEvent.java +++ b/api/src/main/java/com/discordsrv/api/discord/events/message/DiscordMessageUpdateEvent.java @@ -21,7 +21,7 @@ * SOFTWARE. */ -package com.discordsrv.api.discord.events; +package com.discordsrv.api.discord.events.message; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; diff --git a/api/src/main/java/com/discordsrv/api/linking/LinkingBackend.java b/api/src/main/java/com/discordsrv/api/linking/LinkingBackend.java new file mode 100644 index 00000000..b2afdedc --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/linking/LinkingBackend.java @@ -0,0 +1,39 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 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.linking; + +import com.discordsrv.api.module.type.Module; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +public interface LinkingBackend extends Module { + + @Nullable + UUID getLinkedAccount(long userId); + + @Nullable + Long getLinkedAccount(@NotNull UUID player); +} diff --git a/api/src/main/java/com/discordsrv/api/module/type/Module.java b/api/src/main/java/com/discordsrv/api/module/type/Module.java new file mode 100644 index 00000000..79bb950b --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/module/type/Module.java @@ -0,0 +1,48 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 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.module.type; + +public interface Module { + + default boolean isEnabled() { + return true; + } + + /** + * Returns the priority of this Module given the lookup type. + * @param type the type being looked up this could be an interface + * @return the priority of this module, higher is more important. Default is 0 + */ + @SuppressWarnings("unused") // API + default int priority(Class type) { + return 0; + } + + default void enable() { + reload(); + } + + default void disable() {} + default void reload() {} +} diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/integration/VaultIntegration.java b/bukkit/src/main/java/com/discordsrv/bukkit/integration/VaultIntegration.java index 03350e77..4a13cc6d 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/integration/VaultIntegration.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/integration/VaultIntegration.java @@ -19,7 +19,9 @@ package com.discordsrv.bukkit.integration; import com.discordsrv.bukkit.BukkitDiscordSRV; +import com.discordsrv.common.exception.MessageException; import com.discordsrv.common.function.CheckedSupplier; +import com.discordsrv.common.future.util.CompletableFutureUtil; import com.discordsrv.common.module.type.PermissionDataProvider; import com.discordsrv.common.module.type.PluginIntegration; import net.milkbowl.vault.chat.Chat; @@ -32,10 +34,7 @@ import org.jetbrains.annotations.Nullable; import java.util.UUID; import java.util.concurrent.CompletableFuture; -public class VaultIntegration extends PluginIntegration - implements PermissionDataProvider.Permissions, - PermissionDataProvider.Groups, - PermissionDataProvider.PrefixAndSuffix { +public class VaultIntegration extends PluginIntegration implements PermissionDataProvider.Basic { private Permission permission; private Chat chat; @@ -100,13 +99,11 @@ public class VaultIntegration extends PluginIntegration } private CompletableFuture unsupported(@Nullable Object vault) { - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(new RuntimeException( + return CompletableFutureUtil.failed(new MessageException( vault != null - ? "Vault backend " + vault.getClass().getName() + " unable to complete request" - : "No vault backend available" + ? "Vault backend " + vault.getClass().getName() + " unable to complete request" + : "No vault backend available" )); - return future; } private CompletableFuture supply(CheckedSupplier supplier, boolean async) { @@ -131,7 +128,7 @@ public class VaultIntegration extends PluginIntegration } @Override - public CompletableFuture hasGroup(UUID player, String groupName) { + public CompletableFuture hasGroup(UUID player, String groupName, boolean includeInherited) { if (permission == null || !permission.isEnabled() || !permission.hasGroupSupport()) { return unsupported(permission); } diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index 34bf090f..13a9bfa1 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -22,6 +22,7 @@ import com.discordsrv.api.discord.connection.DiscordConnectionDetails; import com.discordsrv.api.event.bus.EventBus; import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadEvent; import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; +import com.discordsrv.api.linking.LinkingBackend; import com.discordsrv.common.api.util.ApiInstanceUtil; import com.discordsrv.common.channel.ChannelConfigHelper; import com.discordsrv.common.channel.ChannelUpdaterModule; @@ -39,6 +40,7 @@ import com.discordsrv.common.discord.details.DiscordConnectionDetailsImpl; import com.discordsrv.common.event.bus.EventBusImpl; import com.discordsrv.common.function.CheckedFunction; import com.discordsrv.common.function.CheckedRunnable; +import com.discordsrv.common.groupsync.GroupSyncModule; import com.discordsrv.common.integration.LuckPermsIntegration; import com.discordsrv.common.logging.Logger; import com.discordsrv.common.logging.adapter.DependencyLoggerAdapter; @@ -50,7 +52,7 @@ import com.discordsrv.common.messageforwarding.game.JoinMessageModule; import com.discordsrv.common.messageforwarding.game.LeaveMessageModule; import com.discordsrv.common.module.ModuleManager; import com.discordsrv.common.module.type.AbstractModule; -import com.discordsrv.common.module.type.Module; +import com.discordsrv.api.module.type.Module; import com.discordsrv.common.placeholder.ComponentResultStringifier; import com.discordsrv.common.placeholder.PlaceholderServiceImpl; import com.discordsrv.common.placeholder.context.GlobalTextHandlingContext; @@ -84,8 +86,8 @@ public abstract class AbstractDiscordSRV T getModule(Class moduleType); void registerModule(AbstractModule module); void unregisterModule(AbstractModule module); diff --git a/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java b/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java new file mode 100644 index 00000000..9874b153 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/main/GroupSyncConfig.java @@ -0,0 +1,134 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.config.main; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.groupsync.enums.GroupSyncDirection; +import com.discordsrv.common.groupsync.enums.GroupSyncSide; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.*; + +@ConfigSerializable +public class GroupSyncConfig { + + @Comment("Group-Role pairs for group synchronization") + public List pairs = new ArrayList<>(Collections.singletonList(new PairConfig())); + + @ConfigSerializable + public static class PairConfig { + + @Comment("The case-sensitive group name from your permissions plugin") + public String groupName = ""; + + @Comment("The Discord role id") + public Long roleId = 0L; + + @Comment("The direction this group-role pair will synchronize in.\n" + + "Valid options: BIDIRECTIONAL, MINECRAFT_TO_DISCORD, DISCORD_TO_MINECRAFT") + public String direction = GroupSyncDirection.BIDIRECTIONAL.name(); + + public GroupSyncDirection direction() { + try { + return GroupSyncDirection.valueOf(direction); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + @Comment("Timed resynchronization. This is required if you're not using LuckPerms") + public TimerConfig timer = new TimerConfig(); + + @ConfigSerializable + public static class TimerConfig { + + @Comment("If timed synchronization of this group-role pair is enabled") + public boolean enabled = true; + + @Comment("The amount of minutes between cycles") + public int cycleTime = 5; + } + + @Comment("Decides which side takes priority when using timed synchronization or the resync command\n" + + "Valid options: MINECRAFT, DISCORD") + public String tieBreaker = GroupSyncSide.MINECRAFT.name(); + + public GroupSyncSide tieBreaker() { + try { + return GroupSyncSide.valueOf(tieBreaker); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + @Comment("The LuckPerms \"server\" context value, used when adding, removing and checking the groups of players.\n" + + "Make this blank (\"\") to use the current server's value, or \"global\" to not use the context") + public String serverContext = "global"; + + public boolean isTheSameAs(PairConfig config) { + return groupName.equals(config.groupName) && Objects.equals(roleId, config.roleId); + } + + public boolean validate(DiscordSRV discordSRV) { + String label = "Group synchronization (" + groupName + ":" + Long.toUnsignedString(roleId) + ")"; + boolean invalidTieBreaker, invalidDirection = false; + if ((invalidTieBreaker = (tieBreaker() == null)) || (invalidDirection = (direction == null))) { + if (invalidTieBreaker) { + discordSRV.logger().error(label + " has invalid tie-breaker: " + tieBreaker + + ", should be one of " + Arrays.toString(GroupSyncSide.values())); + } + if (invalidDirection) { + discordSRV.logger().error(label + " has invalid direction: " + direction + + ", should be one of " + Arrays.toString(GroupSyncDirection.values())); + } + return false; + } else if (direction() != GroupSyncDirection.BIDIRECTIONAL) { + boolean minecraft; + if ((direction() == GroupSyncDirection.MINECRAFT_TO_DISCORD) != (minecraft = (tieBreaker() == GroupSyncSide.MINECRAFT))) { + String opposite = (minecraft ? GroupSyncSide.DISCORD : GroupSyncSide.MINECRAFT).name(); + discordSRV.logger().warning(label + " with direction " + + direction + " with tie-breaker " + + tieBreaker + " (should be " + opposite + ")"); + tieBreaker = opposite; // Fix the config + } + } + return true; + } + + @Override + public String toString() { + String arrow; + switch (direction()) { + default: + case BIDIRECTIONAL: + arrow = "<->"; + break; + case DISCORD_TO_MINECRAFT: + arrow = "<-"; + break; + case MINECRAFT_TO_DISCORD: + arrow = "->"; + break; + } + return "PairConfig{" + groupName + arrow + roleId + '}'; + } + } + +} diff --git a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java index 8351ef3b..57dd945e 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java @@ -23,6 +23,7 @@ import com.discordsrv.common.config.annotation.DefaultOnly; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.config.main.channels.base.ChannelConfig; import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; import java.util.*; @@ -43,4 +44,7 @@ public class MainConfig implements Config { }}; public List channelUpdaters = new ArrayList<>(Collections.singletonList(new ChannelUpdaterConfig())); + + @Comment("Configuration options for group-role synchronization") + public GroupSyncConfig groupSync = new GroupSyncConfig(); } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIEventModule.java b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIEventModule.java index 7f49ebfd..b13ed08b 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIEventModule.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIEventModule.java @@ -18,18 +18,26 @@ package com.discordsrv.common.discord.api; -import com.discordsrv.api.discord.events.DiscordMessageDeleteEvent; -import com.discordsrv.api.discord.events.DiscordMessageReceiveEvent; -import com.discordsrv.api.discord.events.DiscordMessageUpdateEvent; +import com.discordsrv.api.discord.events.member.role.DiscordMemberRoleAddEvent; +import com.discordsrv.api.discord.events.member.role.DiscordMemberRoleRemoveEvent; +import com.discordsrv.api.discord.events.message.DiscordMessageDeleteEvent; +import com.discordsrv.api.discord.events.message.DiscordMessageReceiveEvent; +import com.discordsrv.api.discord.events.message.DiscordMessageUpdateEvent; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.discord.api.entity.channel.DiscordMessageChannelImpl; +import com.discordsrv.common.discord.api.entity.guild.DiscordGuildMemberImpl; +import com.discordsrv.common.discord.api.entity.guild.DiscordRoleImpl; import com.discordsrv.common.discord.api.entity.message.ReceivedDiscordMessageImpl; import com.discordsrv.common.module.type.AbstractModule; +import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent; +import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent; import net.dv8tion.jda.api.events.message.MessageDeleteEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageUpdateEvent; +import java.util.stream.Collectors; + public class DiscordAPIEventModule extends AbstractModule { public DiscordAPIEventModule(DiscordSRV discordSRV) { @@ -59,4 +67,20 @@ public class DiscordAPIEventModule extends AbstractModule { event.getMessageIdLong() )); } + + @Subscribe + public void onGuildMemberRoleAdd(GuildMemberRoleAddEvent event) { + discordSRV.eventBus().publish(new DiscordMemberRoleAddEvent( + new DiscordGuildMemberImpl(discordSRV, event.getMember()), + event.getRoles().stream().map(role -> new DiscordRoleImpl(discordSRV, role)).collect(Collectors.toList()) + )); + } + + @Subscribe + public void onGuildMemberRoleRemove(GuildMemberRoleRemoveEvent event) { + discordSRV.eventBus().publish(new DiscordMemberRoleRemoveEvent( + new DiscordGuildMemberImpl(discordSRV, event.getMember()), + event.getRoles().stream().map(role -> new DiscordRoleImpl(discordSRV, role)).collect(Collectors.toList()) + )); + } } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java index a10a1f9a..3fe86f86 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java @@ -386,7 +386,7 @@ public class DiscordAPIImpl implements DiscordAPI { public @NotNull Optional getRoleById(long id) { return discordSRV.jda() .map(jda -> jda.getRoleById(id)) - .map(DiscordRoleImpl::new); + .map(role -> new DiscordRoleImpl(discordSRV, role)); } private class WebhookCacheLoader implements AsyncCacheLoader { diff --git a/common/src/main/java/com/discordsrv/common/discord/api/entity/DiscordUserImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/entity/DiscordUserImpl.java index e30b08a9..dcd9f19f 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/entity/DiscordUserImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/entity/DiscordUserImpl.java @@ -31,9 +31,9 @@ import java.util.concurrent.CompletableFuture; public class DiscordUserImpl implements DiscordUser { - private final DiscordSRV discordSRV; - private final User user; - private final boolean self; + protected final DiscordSRV discordSRV; + protected final User user; + protected final boolean self; public DiscordUserImpl(DiscordSRV discordSRV, User user) { this.discordSRV = discordSRV; diff --git a/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordGuildImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordGuildImpl.java index 69bb8d51..42825181 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordGuildImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordGuildImpl.java @@ -80,14 +80,14 @@ public class DiscordGuildImpl implements DiscordGuild { return Optional.empty(); } - return Optional.of(new DiscordRoleImpl(role)); + return Optional.of(new DiscordRoleImpl(discordSRV, role)); } @Override public @NotNull List getRoles() { List roles = new ArrayList<>(); for (Role role : guild.getRoles()) { - roles.add(new DiscordRoleImpl(role)); + roles.add(new DiscordRoleImpl(discordSRV, role)); } return roles; } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordGuildMemberImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordGuildMemberImpl.java index 5cee4474..7d7b9a72 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordGuildMemberImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordGuildMemberImpl.java @@ -37,6 +37,7 @@ import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGuildMember { @@ -52,7 +53,7 @@ public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGu List roles = new ArrayList<>(); for (Role role : member.getRoles()) { - roles.add(new DiscordRoleImpl(role)); + roles.add(new DiscordRoleImpl(discordSRV, role)); } this.roles = roles; this.color = new Color(member.getColorRaw()); @@ -73,6 +74,25 @@ public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGu return roles; } + @Override + public boolean hasRole(@NotNull DiscordRole role) { + return roles.stream().anyMatch(role::equals); + } + + @Override + public CompletableFuture addRole(@NotNull DiscordRole role) { + return discordSRV.discordAPI().mapExceptions(() -> + guild.getAsJDAGuild().addRoleToMember(member, role.getAsJDARole()).submit() + ); + } + + @Override + public CompletableFuture removeRole(@NotNull DiscordRole role) { + return discordSRV.discordAPI().mapExceptions(() -> + guild.getAsJDAGuild().removeRoleFromMember(member, role.getAsJDARole()).submit() + ); + } + @Override public @NotNull String getEffectiveServerAvatarUrl() { return member.getEffectiveAvatarUrl(); diff --git a/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordRoleImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordRoleImpl.java index ad57ce60..46905ef0 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordRoleImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordRoleImpl.java @@ -19,17 +19,23 @@ package com.discordsrv.common.discord.api.entity.guild; import com.discordsrv.api.color.Color; +import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; import com.discordsrv.api.discord.api.entity.guild.DiscordRole; +import com.discordsrv.common.DiscordSRV; import net.dv8tion.jda.api.entities.Role; import org.jetbrains.annotations.NotNull; +import java.util.Objects; + public class DiscordRoleImpl implements DiscordRole { private final Role role; + private final DiscordGuild guild; private final Color color; - public DiscordRoleImpl(Role role) { + public DiscordRoleImpl(DiscordSRV discordSRV, Role role) { this.role = role; + this.guild = new DiscordGuildImpl(discordSRV, role.getGuild()); this.color = new Color(role.getColorRaw()); } @@ -38,6 +44,11 @@ public class DiscordRoleImpl implements DiscordRole { return role.getIdLong(); } + @Override + public @NotNull DiscordGuild getGuild() { + return guild; + } + @Override public @NotNull String getName() { return role.getName(); @@ -67,4 +78,17 @@ public class DiscordRoleImpl implements DiscordRole { public String toString() { return "ServerRole:" + getName() + "(" + Long.toUnsignedString(getId()) + ")"; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DiscordRoleImpl that = (DiscordRoleImpl) o; + return Objects.equals(role.getId(), that.role.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(role.getId()); + } } diff --git a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java index 55a149e5..78a184f8 100644 --- a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java +++ b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java @@ -184,7 +184,7 @@ public class JDAConnectionManager implements DiscordConnectionManager { } else if (o instanceof Member) { converted = new DiscordGuildMemberImpl(discordSRV, (Member) o); } else if (o instanceof Role) { - converted = new DiscordRoleImpl((Role) o); + converted = new DiscordRoleImpl(discordSRV, (Role) o); } else if (o instanceof ReceivedMessage) { converted = ReceivedDiscordMessageImpl.fromJDA(discordSRV, (Message) o); } else if (o instanceof User) { diff --git a/common/src/main/java/com/discordsrv/common/module/type/Module.java b/common/src/main/java/com/discordsrv/common/exception/MessageException.java similarity index 59% rename from common/src/main/java/com/discordsrv/common/module/type/Module.java rename to common/src/main/java/com/discordsrv/common/exception/MessageException.java index 702f48a6..13ebf68f 100644 --- a/common/src/main/java/com/discordsrv/common/module/type/Module.java +++ b/common/src/main/java/com/discordsrv/common/exception/MessageException.java @@ -16,28 +16,15 @@ * along with this program. If not, see . */ -package com.discordsrv.common.module.type; +package com.discordsrv.common.exception; -public interface Module { +import com.discordsrv.common.exception.util.ExceptionUtil; - default boolean isEnabled() { - return true; +public class MessageException extends RuntimeException { + + @SuppressWarnings("ThrowableNotThrown") + public MessageException(String message) { + super(message); + ExceptionUtil.minifyException(this); } - - /** - * Returns the priority of this Module given the lookup type. - * @param type the type being looked up this could be an interface - * @return the priority of this module, higher is more important. Default is 0 - */ - @SuppressWarnings("unused") // API - default int priority(Class type) { - return 0; - } - - default void enable() { - reload(); - } - - default void disable() {} - default void reload() {} } diff --git a/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java b/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java new file mode 100644 index 00000000..e3c25791 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java @@ -0,0 +1,60 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.future.util; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public final class CompletableFutureUtil { + + private CompletableFutureUtil() {} + + /** + * Same as {@link CompletableFuture#completedFuture(Object)} but for failing. + */ + public static CompletableFuture failed(Throwable throwable) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(throwable); + return future; + } + + @SuppressWarnings("unchecked") + public static CompletableFuture> combine(Collection> futures) { + return combine(futures.toArray(new CompletableFuture[0])); + } + + public static CompletableFuture> combine(CompletableFuture[] futures) { + CompletableFuture> future = new CompletableFuture<>(); + CompletableFuture.allOf(futures).whenComplete((v, t) -> { + if (t != null) { + future.completeExceptionally(t); + return; + } + + Set results = new HashSet<>(); + for (CompletableFuture aFuture : futures) { + results.add(aFuture.join()); + } + future.complete(results); + }); + return future; + } +} diff --git a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java new file mode 100644 index 00000000..ab41e20c --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java @@ -0,0 +1,563 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.groupsync; + +import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; +import com.discordsrv.api.discord.api.entity.guild.DiscordRole; +import com.discordsrv.api.discord.events.member.role.DiscordMemberRoleAddEvent; +import com.discordsrv.api.discord.events.member.role.DiscordMemberRoleRemoveEvent; +import com.discordsrv.api.event.bus.Subscribe; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.config.main.GroupSyncConfig; +import com.discordsrv.common.future.util.CompletableFutureUtil; +import com.discordsrv.common.groupsync.enums.GroupSyncCause; +import com.discordsrv.common.groupsync.enums.GroupSyncDirection; +import com.discordsrv.common.groupsync.enums.GroupSyncResult; +import com.discordsrv.common.groupsync.enums.GroupSyncSide; +import com.discordsrv.common.module.type.AbstractModule; +import com.discordsrv.common.module.type.PermissionDataProvider; +import com.discordsrv.common.player.IPlayer; +import com.github.benmanes.caffeine.cache.Cache; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class GroupSyncModule extends AbstractModule { + + private final Map> pairs = new LinkedHashMap<>(); + private final Map> groupsToPairs = new ConcurrentHashMap<>(); + private final Map> rolesToPairs = new ConcurrentHashMap<>(); + + private final Cache> expectedDiscordChanges; + private final Cache> expectedMinecraftChanges; + + public GroupSyncModule(DiscordSRV discordSRV) { + super(discordSRV); + this.expectedDiscordChanges = discordSRV.caffeineBuilder() + .expireAfterWrite(30, TimeUnit.SECONDS) + .build(); + + this.expectedMinecraftChanges = discordSRV.caffeineBuilder() + .expireAfterWrite(30, TimeUnit.SECONDS) + .build(); + } + + @Override + public void reload() { + synchronized (pairs) { + pairs.values().forEach(future -> future.cancel(false)); + pairs.clear(); + groupsToPairs.clear(); + rolesToPairs.clear(); + + GroupSyncConfig config = discordSRV.config().groupSync; + for (GroupSyncConfig.PairConfig pair : config.pairs) { + String groupName = pair.groupName; + long roleId = pair.roleId; + if (StringUtils.isEmpty(groupName) || roleId == 0) { + continue; + } + + if (!pair.validate(discordSRV)) { + continue; + } + + boolean failed = false; + for (GroupSyncConfig.PairConfig pairConfig : config.pairs) { + if (pairConfig != pair && pair.isTheSameAs(pairConfig)) { + failed = true; + break; + } + } + if (failed) { + discordSRV.logger().error("Duplicate group synchronization pair: " + groupName + " to " + roleId); + continue; + } + + Future future = null; + GroupSyncConfig.PairConfig.TimerConfig timer = pair.timer; + if (timer != null && timer.enabled) { + int cycleTime = timer.cycleTime; + future = discordSRV.scheduler().runAtFixedRate( + () -> resyncPair(pair, GroupSyncCause.TIMER), + cycleTime, + cycleTime, + TimeUnit.MINUTES + ); + } + + pairs.put(pair, future); + groupsToPairs.computeIfAbsent(groupName, key -> new ArrayList<>()).add(pair); + rolesToPairs.computeIfAbsent(roleId, key -> new ArrayList<>()).add(pair); + } + } + } + + private void logSummary( + UUID player, + GroupSyncCause cause, + Map> pairs + ) { + CompletableFutureUtil.combine(pairs.values()).whenComplete((v, t) -> { + SynchronizationSummary summary = new SynchronizationSummary(player, cause); + for (Map.Entry> entry : pairs.entrySet()) { + summary.add(entry.getKey(), entry.getValue().join()); + } + discordSRV.logger().debug(summary.toString()); + }); + } + + // Linked account helper methods + + private Long getLinkedAccount(UUID player) { + return discordSRV.linkingBackend().getLinkedAccount(player); + } + + private UUID getLinkedAccount(long userId) { + return discordSRV.linkingBackend().getLinkedAccount(userId); + } + + // Permission data helper methods + + private PermissionDataProvider.Groups getPermissionProvider() { + PermissionDataProvider.GroupsContext groupsContext = discordSRV.getModule(PermissionDataProvider.GroupsContext.class); + return groupsContext == null ? discordSRV.getModule(PermissionDataProvider.Groups.class) : groupsContext; + } + + private CompletableFuture hasGroup( + UUID player, + String groupName, + @Nullable String serverContext + ) { + PermissionDataProvider.Groups permissionProvider = getPermissionProvider(); + if (permissionProvider instanceof PermissionDataProvider.GroupsContext) { + return ((PermissionDataProvider.GroupsContext) permissionProvider) + .hasGroup(player, groupName, false, serverContext); + } else { + return permissionProvider.hasGroup(player, groupName, false); + } + } + + private CompletableFuture addGroup( + UUID player, + String groupName, + @Nullable String serverContext + ) { + PermissionDataProvider.Groups permissionProvider = getPermissionProvider(); + if (permissionProvider instanceof PermissionDataProvider.GroupsContext) { + return ((PermissionDataProvider.GroupsContext) permissionProvider) + .addGroup(player, groupName, serverContext); + } else { + return permissionProvider.addGroup(player, groupName); + } + } + + private CompletableFuture removeGroup( + UUID player, + String groupName, + @Nullable String serverContext + ) { + PermissionDataProvider.Groups permissionProvider = getPermissionProvider(); + if (permissionProvider instanceof PermissionDataProvider.GroupsContext) { + return ((PermissionDataProvider.GroupsContext) permissionProvider) + .removeGroup(player, groupName, serverContext); + } else { + return permissionProvider.removeGroup(player, groupName); + } + } + + // Resync user + + public void resync(UUID player, GroupSyncCause cause) { + Long userId = getLinkedAccount(player); + if (userId == null) { + return; + } + + resync(player, userId, cause); + } + + public void resync(long userId, GroupSyncCause cause) { + UUID player = getLinkedAccount(userId); + if (player == null) { + return; + } + + resync(player, userId, cause); + } + + public void resync(UUID player, long userId, GroupSyncCause cause) { + Map> futures = new LinkedHashMap<>(); + for (GroupSyncConfig.PairConfig pair : pairs.keySet()) { + futures.put(pair, resyncPair(pair, player, userId)); + } + + logSummary(player, cause, futures); + } + + private void resyncPair(GroupSyncConfig.PairConfig pair, GroupSyncCause cause) { + for (IPlayer player : discordSRV.playerProvider().allPlayers()) { + UUID uuid = player.uniqueId(); + Long userId = getLinkedAccount(uuid); + if (userId == null) { + continue; + } + + resyncPair(pair, uuid, userId) + .whenComplete((result, t) -> discordSRV.logger().debug( + new SynchronizationSummary(uuid, cause, pair, result).toString())); + } + } + + private CompletableFuture resyncPair(GroupSyncConfig.PairConfig pair, UUID player, long userId) { + DiscordRole role = discordSRV.discordAPI().getRoleById(pair.roleId).orElse(null); + if (role == null) { + return CompletableFuture.completedFuture(GroupSyncResult.ROLE_DOESNT_EXIST); + } + + DiscordGuildMember member = role.getGuild().getMemberById(userId).orElse(null); + if (member == null) { + return CompletableFuture.completedFuture(GroupSyncResult.NOT_A_GUILD_MEMBER); + } + + boolean hasRole = member.hasRole(role); + String groupName = pair.groupName; + CompletableFuture resultFuture = new CompletableFuture<>(); + + hasGroup(player, groupName, pair.serverContext).whenComplete((hasGroup, t) -> { + if (t != null) { + discordSRV.logger().error("Failed to check if player " + player + " has group " + groupName, t); + resultFuture.complete(GroupSyncResult.PERMISSION_BACKEND_FAIL_CHECK); + return; + } + + if (hasRole == hasGroup) { + resultFuture.complete(hasRole ? GroupSyncResult.BOTH_TRUE : GroupSyncResult.BOTH_FALSE); + // We're all good + return; + } + + GroupSyncSide side = pair.tieBreaker(); + GroupSyncDirection direction = pair.direction(); + CompletableFuture future; + GroupSyncResult result; + if (hasRole) { + if (side == GroupSyncSide.DISCORD) { + // Has role, add group + if (direction == GroupSyncDirection.MINECRAFT_TO_DISCORD) { + resultFuture.complete(GroupSyncResult.WRONG_DIRECTION); + return; + } + + result = GroupSyncResult.ADD_GROUP; + future = addGroup(player, groupName, pair.serverContext); + } else { + // Doesn't have group, remove role + if (direction == GroupSyncDirection.DISCORD_TO_MINECRAFT) { + resultFuture.complete(GroupSyncResult.WRONG_DIRECTION); + return; + } + + result = GroupSyncResult.REMOVE_ROLE; + future = member.removeRole(role); + } + } else { + if (side == GroupSyncSide.DISCORD) { + // Doesn't have role, remove group + if (direction == GroupSyncDirection.MINECRAFT_TO_DISCORD) { + resultFuture.complete(GroupSyncResult.WRONG_DIRECTION); + return; + } + + result = GroupSyncResult.REMOVE_GROUP; + future = removeGroup(player, groupName, pair.serverContext); + } else { + // Has group, add role + if (direction == GroupSyncDirection.DISCORD_TO_MINECRAFT) { + resultFuture.complete(GroupSyncResult.WRONG_DIRECTION); + return; + } + + result = GroupSyncResult.ADD_ROLE; + future = member.addRole(role); + } + } + future.whenComplete((v, t2) -> { + if (t2 != null) { + discordSRV.logger().error("Failed to " + result + " to " + player + "/" + Long.toUnsignedString(userId), t2); + resultFuture.complete(GroupSyncResult.UPDATE_FAILED); + return; + } + + resultFuture.complete(result); + }); + }); + + return resultFuture; + } + + // Listeners & methods to indicate something changed + + @Subscribe + public void onDiscordMemberRoleAdd(DiscordMemberRoleAddEvent event) { + event.getRoles().forEach(role -> roleChanged(event.getMember().getId(), role.getId(), false)); + } + + @Subscribe + public void onDiscordMemberRoleRemove(DiscordMemberRoleRemoveEvent event) { + event.getRoles().forEach(role -> roleChanged(event.getMember().getId(), role.getId(), true)); + } + + public void groupAdded(UUID player, String groupName, @Nullable Set serverContext, GroupSyncCause cause) { + groupChanged(player, groupName, serverContext, cause, false); + } + + public void groupRemoved(UUID player, String groupName, @Nullable Set serverContext, GroupSyncCause cause) { + groupChanged(player, groupName, serverContext, cause, true); + } + + // Internal handling of changes + + private boolean checkExpectation(Cache> expectations, T key, R mapKey, boolean remove) { + // Check if we were expecting the change (when we add/remove something due to synchronization), + // if we did expect the change, we won't trigger a synchronization since we just synchronized what was needed + Map expected = expectations.getIfPresent(key); + if (expected != null && Objects.equals(expected.get(mapKey), remove)) { + expected.remove(mapKey); + return true; + } + return false; + } + + private void roleChanged(long userId, long roleId, boolean remove) { + if (checkExpectation(expectedDiscordChanges, userId, roleId, remove)) { + return; + } + + List pairs = rolesToPairs.get(roleId); + if (pairs == null) { + return; + } + + PermissionDataProvider.Groups permissionProvider = getPermissionProvider(); + if (permissionProvider == null) { + discordSRV.logger().warning("No supported permission plugin available to perform group sync"); + return; + } + + UUID player = getLinkedAccount(userId); + if (player == null) { + return; + } + + Map> futures = new LinkedHashMap<>(); + for (GroupSyncConfig.PairConfig pair : pairs) { + GroupSyncDirection direction = pair.direction(); + if (direction == GroupSyncDirection.MINECRAFT_TO_DISCORD) { + // Not going Discord -> Minecraft + futures.put(pair, CompletableFuture.completedFuture(GroupSyncResult.WRONG_DIRECTION)); + continue; + } + + futures.put(pair, modifyGroupState(player, pair, remove)); + + // If the sync is bidirectional, also add/remove any other roles that are linked to this group + if (direction == GroupSyncDirection.DISCORD_TO_MINECRAFT) { + continue; + } + + List groupPairs = groupsToPairs.get(pair.groupName); + if (groupPairs == null) { + continue; + } + + for (GroupSyncConfig.PairConfig groupPair : groupPairs) { + if (groupPair.roleId == roleId) { + continue; + } + + futures.put(groupPair, modifyRoleState(userId, groupPair, remove)); + } + } + logSummary(player, GroupSyncCause.DISCORD_ROLE_CHANGE, futures); + } + + private void groupChanged(UUID player, String groupName, @Nullable Set serverContext, GroupSyncCause cause, boolean remove) { + if (cause.isDiscordSRVCanCause() && checkExpectation(expectedMinecraftChanges, player, groupName, remove)) { + return; + } + + List pairs = groupsToPairs.get(groupName); + if (pairs == null) { + return; + } + + Long userId = getLinkedAccount(player); + if (userId == null) { + return; + } + + PermissionDataProvider.Groups permissionProvider = getPermissionProvider(); + Map> futures = new LinkedHashMap<>(); + for (GroupSyncConfig.PairConfig pair : pairs) { + GroupSyncDirection direction = pair.direction(); + if (direction == GroupSyncDirection.DISCORD_TO_MINECRAFT) { + // Not going Minecraft -> Discord + futures.put(pair, CompletableFuture.completedFuture(GroupSyncResult.WRONG_DIRECTION)); + continue; + } + + // Check if we're in the right context + String context = pair.serverContext; + if (permissionProvider instanceof PermissionDataProvider.GroupsContext) { + if (StringUtils.isEmpty(context)) { + // Use the default server context of the server + Set defaultValues = ((PermissionDataProvider.GroupsContext) permissionProvider) + .getDefaultServerContext(); + if (!Objects.equals(serverContext, defaultValues)) { + continue; + } + } else if (context.equals("global")) { + // No server context + if (serverContext != null && !serverContext.isEmpty()) { + continue; + } + } else { + // Server context has to match the specified + if (serverContext == null + || serverContext.size() != 1 + || !serverContext.iterator().next().equals(context)) { + continue; + } + } + } + + futures.put(pair, modifyRoleState(userId, pair, remove)); + + // If the sync is bidirectional, also add/remove any other groups that are linked to this role + if (direction == GroupSyncDirection.MINECRAFT_TO_DISCORD) { + continue; + } + + long roleId = pair.roleId; + List rolePairs = rolesToPairs.get(roleId); + if (rolePairs == null || rolePairs.isEmpty()) { + continue; + } + + for (GroupSyncConfig.PairConfig rolePair : rolePairs) { + if (rolePair.groupName.equals(groupName)) { + continue; + } + + futures.put(rolePair, modifyGroupState(player, rolePair, remove)); + } + } + logSummary(player, cause, futures); + } + + private CompletableFuture modifyGroupState(UUID player, GroupSyncConfig.PairConfig config, boolean remove) { + String groupName = config.groupName; + + Map expected = expectedMinecraftChanges.get(player, key -> new ConcurrentHashMap<>()); + if (expected != null) { + expected.put(groupName, remove); + } + + CompletableFuture future = new CompletableFuture<>(); + String serverContext = config.serverContext; + hasGroup(player, groupName, serverContext).thenCompose(hasGroup -> { + if (remove && hasGroup) { + return removeGroup(player, groupName, serverContext).thenApply(v -> GroupSyncResult.REMOVE_GROUP); + } else if (!remove && !hasGroup) { + return addGroup(player, groupName, serverContext).thenApply(v -> GroupSyncResult.ADD_GROUP); + } else { + // Nothing to do + return CompletableFuture.completedFuture(GroupSyncResult.ALREADY_IN_SYNC); + } + }).whenComplete((result, t) -> { + if (t != null) { + if (expected != null) { + // Failed, remove expectation + expected.remove(groupName); + } + + future.complete(GroupSyncResult.UPDATE_FAILED); + discordSRV.logger().error("Failed to add group " + groupName + " to " + player, t); + return; + } + + future.complete(result); + }); + return future; + } + + private CompletableFuture modifyRoleState(long userId, GroupSyncConfig.PairConfig config, boolean remove) { + long roleId = config.roleId; + DiscordRole role = discordSRV.discordAPI().getRoleById(roleId).orElse(null); + if (role == null) { + return CompletableFuture.completedFuture(GroupSyncResult.ROLE_DOESNT_EXIST); + } + + DiscordGuildMember member = role.getGuild().getMemberById(userId).orElse(null); + if (member == null) { + return CompletableFuture.completedFuture(GroupSyncResult.NOT_A_GUILD_MEMBER); + } + + Map expected = expectedDiscordChanges.get(userId, key -> new ConcurrentHashMap<>()); + if (expected != null) { + expected.put(roleId, remove); + } + + boolean hasRole = member.hasRole(role); + CompletableFuture future; + if (remove && hasRole) { + future = member.removeRole(role).thenApply(v -> GroupSyncResult.REMOVE_ROLE); + } else if (!remove && !hasRole) { + future = member.addRole(role).thenApply(v -> GroupSyncResult.ADD_ROLE); + } else { + if (expected != null) { + // Nothing needed to be changed, remove expectation + expected.remove(roleId); + } + return CompletableFuture.completedFuture(GroupSyncResult.ALREADY_IN_SYNC); + } + + CompletableFuture resultFuture = new CompletableFuture<>(); + future.whenComplete((result, t) -> { + if (t != null) { + if (expected != null) { + // Failed, remove expectation + expected.remove(roleId); + } + + resultFuture.complete(GroupSyncResult.UPDATE_FAILED); + discordSRV.logger().error("Failed to give/take role " + role + " to/from " + member, t); + return; + } + resultFuture.complete(result); + }); + return resultFuture; + } +} diff --git a/common/src/main/java/com/discordsrv/common/groupsync/SynchronizationSummary.java b/common/src/main/java/com/discordsrv/common/groupsync/SynchronizationSummary.java new file mode 100644 index 00000000..1b3dba75 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/groupsync/SynchronizationSummary.java @@ -0,0 +1,61 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.groupsync; + +import com.discordsrv.common.config.main.GroupSyncConfig; +import com.discordsrv.common.groupsync.enums.GroupSyncCause; +import com.discordsrv.common.groupsync.enums.GroupSyncResult; + +import java.util.*; + +public class SynchronizationSummary { + + private final EnumMap> pairs = new EnumMap<>(GroupSyncResult.class); + private final UUID player; + private final GroupSyncCause cause; + + public SynchronizationSummary(UUID player, GroupSyncCause cause, GroupSyncConfig.PairConfig config, GroupSyncResult result) { + this(player, cause); + add(config, result); + } + + public SynchronizationSummary(UUID player, GroupSyncCause cause) { + this.player = player; + this.cause = cause; + } + + public void add(GroupSyncConfig.PairConfig config, GroupSyncResult result) { + pairs.computeIfAbsent(result, key -> new LinkedHashSet<>()).add(config); + } + + @Override + public String toString() { + int count = pairs.size(); + StringBuilder message = new StringBuilder( + "Group synchronization (of " + count + " pairs) for " + player + " (" + cause + ")"); + + for (Map.Entry> entry : pairs.entrySet()) { + message.append(count == 1 ? ": " : "\n") + .append(entry.getKey().toString()) + .append(": ") + .append(entry.getValue().toString()); + } + return message.toString(); + } +} diff --git a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncCause.java b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncCause.java new file mode 100644 index 00000000..786bdd27 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncCause.java @@ -0,0 +1,53 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.groupsync.enums; + +public enum GroupSyncCause { + + API("API"), + COMMAND("Command"), + GAME_JOIN("Joined game"), + LINK("Linked account"), + TIMER("Timed synchronization"), + DISCORD_ROLE_CHANGE("Discord role changed", true), + LUCKPERMS_NODE_CHANGE("LuckPerms node changed", true), + LUCKPERMS_TRACK("LuckPerms track promotion/demotion"), + ; + + private final String prettyCause; + private final boolean discordSRVCanCause; + + GroupSyncCause(String prettyCause) { + this(prettyCause, false); + } + + GroupSyncCause(String prettyCause, boolean discordSRVCanCause) { + this.prettyCause = prettyCause; + this.discordSRVCanCause = discordSRVCanCause; + } + + public boolean isDiscordSRVCanCause() { + return discordSRVCanCause; + } + + @Override + public String toString() { + return prettyCause; + } +} diff --git a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncDirection.java b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncDirection.java new file mode 100644 index 00000000..18792555 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncDirection.java @@ -0,0 +1,27 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.groupsync.enums; + +public enum GroupSyncDirection { + + MINECRAFT_TO_DISCORD, + DISCORD_TO_MINECRAFT, + BIDIRECTIONAL + +} diff --git a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncResult.java b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncResult.java new file mode 100644 index 00000000..f3a4be53 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncResult.java @@ -0,0 +1,53 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.groupsync.enums; + +public enum GroupSyncResult { + + // Something happened + ADD_GROUP("Success (group add)"), + REMOVE_GROUP("Success (group remove)"), + ADD_ROLE("Success (role add)"), + REMOVE_ROLE("Success (role remove)"), + + // Nothing done + ALREADY_IN_SYNC("Already in sync"), + WRONG_DIRECTION("Wrong direction"), + BOTH_TRUE("Both sides true"), + BOTH_FALSE("Both sides false"), + + // Errors + ROLE_DOESNT_EXIST("Role doesn't exist"), + NOT_A_GUILD_MEMBER("User is not part of the server the role is in"), + PERMISSION_BACKEND_FAIL_CHECK("Failed to check group status, error printed"), + UPDATE_FAILED("Failed to modify role/group, error printed"), + + ; + + final String prettyResult; + + GroupSyncResult(String prettyResult) { + this.prettyResult = prettyResult; + } + + @Override + public String toString() { + return prettyResult; + } +} diff --git a/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncSide.java b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncSide.java new file mode 100644 index 00000000..1ac6a517 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/groupsync/enums/GroupSyncSide.java @@ -0,0 +1,26 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.groupsync.enums; + +public enum GroupSyncSide { + + MINECRAFT, + DISCORD + +} diff --git a/common/src/main/java/com/discordsrv/common/integration/LuckPermsIntegration.java b/common/src/main/java/com/discordsrv/common/integration/LuckPermsIntegration.java index efdabeea..68827976 100644 --- a/common/src/main/java/com/discordsrv/common/integration/LuckPermsIntegration.java +++ b/common/src/main/java/com/discordsrv/common/integration/LuckPermsIntegration.java @@ -19,25 +19,44 @@ package com.discordsrv.common.integration; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.exception.MessageException; +import com.discordsrv.common.future.util.CompletableFutureUtil; +import com.discordsrv.common.groupsync.GroupSyncModule; +import com.discordsrv.common.groupsync.enums.GroupSyncCause; import com.discordsrv.common.module.type.PermissionDataProvider; import com.discordsrv.common.module.type.PluginIntegration; import net.luckperms.api.LuckPerms; import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.DefaultContextKeys; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.context.MutableContextSet; +import net.luckperms.api.event.EventSubscription; +import net.luckperms.api.event.LuckPermsEvent; +import net.luckperms.api.event.node.NodeAddEvent; +import net.luckperms.api.event.node.NodeClearEvent; +import net.luckperms.api.event.node.NodeRemoveEvent; +import net.luckperms.api.event.user.track.UserTrackEvent; +import net.luckperms.api.model.PermissionHolder; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.NodeMap; import net.luckperms.api.model.group.Group; import net.luckperms.api.model.user.User; import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.query.QueryMode; import net.luckperms.api.query.QueryOptions; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; +import java.util.function.Consumer; public class LuckPermsIntegration extends PluginIntegration implements PermissionDataProvider.All { private LuckPerms luckPerms; + private final List> subscriptions = new ArrayList<>(); public LuckPermsIntegration(DiscordSRV discordSRV) { super(discordSRV); @@ -57,10 +76,20 @@ public class LuckPermsIntegration extends PluginIntegration implemen @Override public void enable() { luckPerms = LuckPermsProvider.get(); + subscribe(NodeAddEvent.class, this::onNodeAdd); + subscribe(NodeRemoveEvent.class, this::onNodeRemove); + subscribe(NodeClearEvent.class, this::onNodeClear); + subscribe(UserTrackEvent.class, this::onUserTrack); + } + + private void subscribe(Class clazz, Consumer method) { + subscriptions.add(luckPerms.getEventBus().subscribe(clazz, method)); } @Override public void disable() { + subscriptions.forEach(EventSubscription::close); + subscriptions.clear(); luckPerms = null; } @@ -74,41 +103,72 @@ public class LuckPermsIntegration extends PluginIntegration implemen } @Override - public CompletableFuture hasGroup(UUID player, String groupName) { + public Set getDefaultServerContext() { + return luckPerms.getContextManager().getStaticContext().getValues(DefaultContextKeys.SERVER_KEY); + } + + @Override + public CompletableFuture hasGroup(UUID player, String groupName, boolean includeInherited, String serverContext) { return user(player).thenApply(user -> { - for (Group inheritedGroup : user.getInheritedGroups(QueryOptions.defaultContextualOptions())) { - if (inheritedGroup.getName().equalsIgnoreCase(groupName)) { - return true; + MutableContextSet context = luckPerms.getContextManager().getStaticContext().mutableCopy(); + if (serverContext != null) { + context.removeAll(DefaultContextKeys.SERVER_KEY); + if (!serverContext.equals("global")) { + context.add(DefaultContextKeys.SERVER_KEY, serverContext); } } - return false; + + return ( + includeInherited + ? user.getInheritedGroups(QueryOptions.builder(QueryMode.CONTEXTUAL).context(context).build()) + .stream() + .map(Group::getName) + : user.getNodes(NodeType.INHERITANCE) + .stream() + .filter(node -> node.getContexts().isSatisfiedBy(context)) + .map(InheritanceNode::getGroupName) + ).anyMatch(name -> name.equalsIgnoreCase(groupName)); }); } @Override - public CompletableFuture addGroup(UUID player, String groupName) { - return groupMutate(player, groupName, NodeMap::add); + public CompletableFuture addGroup(UUID player, String groupName, String serverContext) { + return groupMutate(player, groupName, serverContext, NodeMap::add); } @Override - public CompletableFuture removeGroup(UUID player, String groupName) { - return groupMutate(player, groupName, NodeMap::remove); + public CompletableFuture removeGroup(UUID player, String groupName, String serverContext) { + return groupMutate(player, groupName, serverContext, NodeMap::remove); } - private CompletableFuture groupMutate(UUID player, String groupName, BiFunction function) { + private CompletableFuture groupMutate(UUID player, String groupName, String serverContext, BiFunction function) { Group group = luckPerms.getGroupManager().getGroup(groupName); if (group == null) { - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(new RuntimeException("Group does not exist")); - return future; + return CompletableFutureUtil.failed(new MessageException("Group does not exist")); } - - return user(player).thenApply(user -> { - DataMutateResult result = function.apply(user.data(), InheritanceNode.builder(group).build()); - if (result != DataMutateResult.SUCCESS) { - throw new RuntimeException(result.name()); + return user(player).thenCompose(user -> { + ContextSet contexts; + if (serverContext != null) { + if (!serverContext.equals("global")) { + contexts = ImmutableContextSet.of(DefaultContextKeys.SERVER_KEY, serverContext); + } else { + contexts = ImmutableContextSet.empty(); + } + } else { + MutableContextSet contextSet = MutableContextSet.create(); + for (String value : getDefaultServerContext()) { + contextSet.add(DefaultContextKeys.SERVER_KEY, value); + } + contexts = contextSet; } - return null; + + InheritanceNode node = InheritanceNode.builder(group).context(contexts).build(); + DataMutateResult result = function.apply(user.data(), node); + if (result != DataMutateResult.SUCCESS) { + return CompletableFutureUtil.failed(new MessageException(result.name())); + } + + return luckPerms.getUserManager().saveUser(user); }); } @@ -132,4 +192,53 @@ public class LuckPermsIntegration extends PluginIntegration implemen public CompletableFuture getMeta(UUID player, String key) throws UnsupportedOperationException { return user(player).thenApply(user -> user.getCachedData().getMetaData().getMetaValue(key)); } + + private void onNodeAdd(NodeAddEvent event) { + nodeUpdate(event.getTarget(), event.getNode(), false); + } + + private void onNodeRemove(NodeRemoveEvent event) { + nodeUpdate(event.getTarget(), event.getNode(), true); + } + + private void onNodeClear(NodeClearEvent event) { + PermissionHolder target = event.getTarget(); + for (Node node : event.getNodes()) { + nodeUpdate(target, node, true); + } + } + + private void onUserTrack(UserTrackEvent event) { + User user = event.getUser(); + event.getGroupFrom().ifPresent(group -> groupUpdate(user, group, Collections.emptySet(), true, true)); + event.getGroupTo().ifPresent(group -> groupUpdate(user, group, Collections.emptySet(), false, true)); + } + + private void nodeUpdate(PermissionHolder holder, Node node, boolean remove) { + if (!(holder instanceof User) || node.getType() != NodeType.INHERITANCE) { + return; + } + + InheritanceNode inheritanceNode = NodeType.INHERITANCE.cast(node); + String groupName = inheritanceNode.getGroupName(); + Set serverContext = inheritanceNode.getContexts().getValues(DefaultContextKeys.SERVER_KEY); + + groupUpdate((User) holder, groupName, serverContext, remove, false); + } + + private void groupUpdate(User user, String groupName, Set serverContext, boolean remove, boolean track) { + GroupSyncModule module = discordSRV.getModule(GroupSyncModule.class); + if (module == null || !module.isEnabled()) { + return; + } + + GroupSyncCause cause = track ? GroupSyncCause.LUCKPERMS_TRACK : GroupSyncCause.LUCKPERMS_NODE_CHANGE; + UUID uuid = user.getUniqueId(); + if (remove) { + module.groupRemoved(uuid, groupName, serverContext, cause); + } else { + module.groupAdded(uuid, groupName, serverContext, cause); + } + } + } diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java index 0acb4759..276068fc 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java @@ -25,7 +25,7 @@ import com.discordsrv.api.discord.api.entity.DiscordUser; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; -import com.discordsrv.api.discord.events.DiscordMessageReceiveEvent; +import com.discordsrv.api.discord.events.message.DiscordMessageReceiveEvent; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.message.receive.discord.DiscordChatMessageProcessingEvent; import com.discordsrv.api.placeholder.util.Placeholders; diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java index 9c735ccd..62ca14d6 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java @@ -27,8 +27,8 @@ import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; -import com.discordsrv.api.discord.events.DiscordMessageDeleteEvent; -import com.discordsrv.api.discord.events.DiscordMessageUpdateEvent; +import com.discordsrv.api.discord.events.message.DiscordMessageDeleteEvent; +import com.discordsrv.api.discord.events.message.DiscordMessageUpdateEvent; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.message.receive.discord.DiscordChatMessageProcessingEvent; import com.discordsrv.common.DiscordSRV; diff --git a/common/src/main/java/com/discordsrv/common/module/ModuleManager.java b/common/src/main/java/com/discordsrv/common/module/ModuleManager.java index d1bd97dc..dcec3bc9 100644 --- a/common/src/main/java/com/discordsrv/common/module/ModuleManager.java +++ b/common/src/main/java/com/discordsrv/common/module/ModuleManager.java @@ -24,7 +24,7 @@ import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadEvent; import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.module.type.AbstractModule; -import com.discordsrv.common.module.type.Module; +import com.discordsrv.api.module.type.Module; import java.util.Map; import java.util.Set; diff --git a/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java b/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java index 4acf6d77..d1035a18 100644 --- a/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java +++ b/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java @@ -20,6 +20,7 @@ package com.discordsrv.common.module.type; import com.discordsrv.api.event.events.Cancellable; import com.discordsrv.api.event.events.Processable; +import com.discordsrv.api.module.type.Module; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.event.util.EventUtil; diff --git a/common/src/main/java/com/discordsrv/common/module/type/PermissionDataProvider.java b/common/src/main/java/com/discordsrv/common/module/type/PermissionDataProvider.java index f864a12d..9908605c 100644 --- a/common/src/main/java/com/discordsrv/common/module/type/PermissionDataProvider.java +++ b/common/src/main/java/com/discordsrv/common/module/type/PermissionDataProvider.java @@ -18,6 +18,10 @@ package com.discordsrv.common.module.type; +import com.discordsrv.api.module.type.Module; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -25,10 +29,11 @@ public interface PermissionDataProvider extends Module { boolean supportsOffline(); - interface All extends Groups, Permissions, PrefixAndSuffix, Meta {} + interface Basic extends Groups, Permissions, PrefixAndSuffix {} + interface All extends Basic, Meta, GroupsContext {} interface Groups extends PermissionDataProvider { - CompletableFuture hasGroup(UUID player, String groupName); + CompletableFuture hasGroup(UUID player, String groupName, boolean includeInherited); CompletableFuture addGroup(UUID player, String groupName); CompletableFuture removeGroup(UUID player, String groupName); } @@ -46,4 +51,27 @@ public interface PermissionDataProvider extends Module { CompletableFuture getMeta(UUID player, String key); } + interface GroupsContext extends Groups { + + Set getDefaultServerContext(); + CompletableFuture hasGroup(UUID player, String groupName, boolean includeInherited, @Nullable String serverContext); + CompletableFuture addGroup(UUID player, String groupName, @Nullable String serverContext); + CompletableFuture removeGroup(UUID player, String groupName, @Nullable String serverContext); + + @Override + default CompletableFuture hasGroup(UUID player, String groupName, boolean includeInherited) { + return hasGroup(player, groupName, includeInherited, null); + } + + @Override + default CompletableFuture addGroup(UUID player, String groupName) { + return addGroup(player, groupName, null); + } + + @Override + default CompletableFuture removeGroup(UUID player, String groupName) { + return removeGroup(player, groupName, null); + } + } + }