Group synchronization

This commit is contained in:
Vankka 2022-01-23 16:11:35 +02:00
parent f5acc00f8a
commit 634c123937
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
40 changed files with 1564 additions and 81 deletions

View File

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

View File

@ -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<DiscordRole> 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<Void> addRole(@NotNull DiscordRole role);
/**
* Takes the given role from this member.
* @param role the role to take
* @return a future
*/
CompletableFuture<Void> removeRole(@NotNull DiscordRole role);
/**
* Gets the effective name of this Discord server member.
* @return the Discord server member's effective name

View File

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

View File

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

View File

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

View File

@ -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<DiscordRole> roles;
public AbstractDiscordMemberRoleChangeEvent(DiscordGuildMember member, List<DiscordRole> roles) {
super(member);
this.roles = roles;
}
public List<DiscordRole> getRoles() {
return roles;
}
}

View File

@ -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<DiscordRole> roles) {
super(member, roles);
}
}

View File

@ -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<DiscordRole> roles) {
super(member, roles);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<BukkitDiscordSRV>
implements PermissionDataProvider.Permissions,
PermissionDataProvider.Groups,
PermissionDataProvider.PrefixAndSuffix {
public class VaultIntegration extends PluginIntegration<BukkitDiscordSRV> implements PermissionDataProvider.Basic {
private Permission permission;
private Chat chat;
@ -100,13 +99,11 @@ public class VaultIntegration extends PluginIntegration<BukkitDiscordSRV>
}
private <T> CompletableFuture<T> unsupported(@Nullable Object vault) {
CompletableFuture<T> 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 <T> CompletableFuture<T> supply(CheckedSupplier<T> supplier, boolean async) {
@ -131,7 +128,7 @@ public class VaultIntegration extends PluginIntegration<BukkitDiscordSRV>
}
@Override
public CompletableFuture<Boolean> hasGroup(UUID player, String groupName) {
public CompletableFuture<Boolean> hasGroup(UUID player, String groupName, boolean includeInherited) {
if (permission == null || !permission.isEnabled() || !permission.hasGroupSupport()) {
return unsupported(permission);
}

View File

@ -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<C extends MainConfig, CC extends Connec
// DiscordSRV
private final DiscordSRVLogger logger;
private final ModuleManager moduleManager;
private ChannelConfigHelper channelConfig;
private ModuleManager moduleManager;
private DiscordConnectionManager discordConnectionManager;
// Internal
@ -94,6 +96,7 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
public AbstractDiscordSRV() {
ApiInstanceUtil.setInstance(this);
this.logger = new DiscordSRVLogger(this);
this.moduleManager = new ModuleManager(this);
}
protected final void load() {
@ -116,6 +119,11 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
return eventBus;
}
@Override
public LinkingBackend linkingBackend() {
return getModule(LinkingBackend.class);
}
@Override
public @NotNull PlaceholderServiceImpl placeholderService() {
return placeholderService;
@ -306,12 +314,10 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
// Register PlayerProvider listeners
playerProvider().subscribe();
// Register modules
moduleManager = new ModuleManager(this);
registerModule(ChannelUpdaterModule::new);
registerModule(GlobalChannelLookupModule::new);
registerModule(DiscordAPIEventModule::new);
registerModule(GroupSyncModule::new);
registerModule(LuckPermsIntegration::new);
registerModule(DiscordChatMessageModule::new);
registerModule(DiscordMessageMirroringModule::new);

View File

@ -29,7 +29,7 @@ import com.discordsrv.common.console.Console;
import com.discordsrv.common.discord.api.DiscordAPIImpl;
import com.discordsrv.common.discord.connection.DiscordConnectionManager;
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.PlaceholderServiceImpl;
import com.discordsrv.common.player.provider.AbstractPlayerProvider;
import com.discordsrv.common.scheduler.Scheduler;
@ -37,6 +37,7 @@ import com.discordsrv.common.logging.Logger;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.Locale;
@ -84,6 +85,7 @@ public interface DiscordSRV extends DiscordSRVApi {
DiscordConnectionManager discordConnectionManager();
// Modules
@Nullable
<T extends Module> T getModule(Class<T> moduleType);
void registerModule(AbstractModule<?> module);
void unregisterModule(AbstractModule<?> module);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<PairConfig> 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 + '}';
}
}
}

View File

@ -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<ChannelUpdaterConfig> channelUpdaters = new ArrayList<>(Collections.singletonList(new ChannelUpdaterConfig()));
@Comment("Configuration options for group-role synchronization")
public GroupSyncConfig groupSync = new GroupSyncConfig();
}

View File

@ -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<DiscordSRV> {
public DiscordAPIEventModule(DiscordSRV discordSRV) {
@ -59,4 +67,20 @@ public class DiscordAPIEventModule extends AbstractModule<DiscordSRV> {
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())
));
}
}

View File

@ -386,7 +386,7 @@ public class DiscordAPIImpl implements DiscordAPI {
public @NotNull Optional<DiscordRole> 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<Long, WebhookClient> {

View File

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

View File

@ -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<DiscordRole> getRoles() {
List<DiscordRole> roles = new ArrayList<>();
for (Role role : guild.getRoles()) {
roles.add(new DiscordRoleImpl(role));
roles.add(new DiscordRoleImpl(discordSRV, role));
}
return roles;
}

View File

@ -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<DiscordRole> 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<Void> addRole(@NotNull DiscordRole role) {
return discordSRV.discordAPI().mapExceptions(() ->
guild.getAsJDAGuild().addRoleToMember(member, role.getAsJDARole()).submit()
);
}
@Override
public CompletableFuture<Void> removeRole(@NotNull DiscordRole role) {
return discordSRV.discordAPI().mapExceptions(() ->
guild.getAsJDAGuild().removeRoleFromMember(member, role.getAsJDARole()).submit()
);
}
@Override
public @NotNull String getEffectiveServerAvatarUrl() {
return member.getEffectiveAvatarUrl();

View File

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

View File

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

View File

@ -16,28 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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() {}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 <T> CompletableFuture<T> failed(Throwable throwable) {
CompletableFuture<T> future = new CompletableFuture<>();
future.completeExceptionally(throwable);
return future;
}
@SuppressWarnings("unchecked")
public static <T> CompletableFuture<Set<T>> combine(Collection<CompletableFuture<T>> futures) {
return combine(futures.toArray(new CompletableFuture[0]));
}
public static <T> CompletableFuture<Set<T>> combine(CompletableFuture<T>[] futures) {
CompletableFuture<Set<T>> future = new CompletableFuture<>();
CompletableFuture.allOf(futures).whenComplete((v, t) -> {
if (t != null) {
future.completeExceptionally(t);
return;
}
Set<T> results = new HashSet<>();
for (CompletableFuture<T> aFuture : futures) {
results.add(aFuture.join());
}
future.complete(results);
});
return future;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<DiscordSRV> {
private final Map<GroupSyncConfig.PairConfig, Future<?>> pairs = new LinkedHashMap<>();
private final Map<String, List<GroupSyncConfig.PairConfig>> groupsToPairs = new ConcurrentHashMap<>();
private final Map<Long, List<GroupSyncConfig.PairConfig>> rolesToPairs = new ConcurrentHashMap<>();
private final Cache<Long, Map<Long, Boolean>> expectedDiscordChanges;
private final Cache<UUID, Map<String, Boolean>> 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<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> pairs
) {
CompletableFutureUtil.combine(pairs.values()).whenComplete((v, t) -> {
SynchronizationSummary summary = new SynchronizationSummary(player, cause);
for (Map.Entry<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> 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<Boolean> 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<Void> 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<Void> 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<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> 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<GroupSyncResult> 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<GroupSyncResult> 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<Void> 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<String> serverContext, GroupSyncCause cause) {
groupChanged(player, groupName, serverContext, cause, false);
}
public void groupRemoved(UUID player, String groupName, @Nullable Set<String> serverContext, GroupSyncCause cause) {
groupChanged(player, groupName, serverContext, cause, true);
}
// Internal handling of changes
private <T, R> boolean checkExpectation(Cache<T, Map<R, Boolean>> 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<R, Boolean> 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<GroupSyncConfig.PairConfig> 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<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> 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<GroupSyncConfig.PairConfig> 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<String> serverContext, GroupSyncCause cause, boolean remove) {
if (cause.isDiscordSRVCanCause() && checkExpectation(expectedMinecraftChanges, player, groupName, remove)) {
return;
}
List<GroupSyncConfig.PairConfig> pairs = groupsToPairs.get(groupName);
if (pairs == null) {
return;
}
Long userId = getLinkedAccount(player);
if (userId == null) {
return;
}
PermissionDataProvider.Groups permissionProvider = getPermissionProvider();
Map<GroupSyncConfig.PairConfig, CompletableFuture<GroupSyncResult>> 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<String> 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<GroupSyncConfig.PairConfig> 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<GroupSyncResult> modifyGroupState(UUID player, GroupSyncConfig.PairConfig config, boolean remove) {
String groupName = config.groupName;
Map<String, Boolean> expected = expectedMinecraftChanges.get(player, key -> new ConcurrentHashMap<>());
if (expected != null) {
expected.put(groupName, remove);
}
CompletableFuture<GroupSyncResult> 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<GroupSyncResult> 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<Long, Boolean> expected = expectedDiscordChanges.get(userId, key -> new ConcurrentHashMap<>());
if (expected != null) {
expected.put(roleId, remove);
}
boolean hasRole = member.hasRole(role);
CompletableFuture<GroupSyncResult> 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<GroupSyncResult> 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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.groupsync;
import com.discordsrv.common.config.main.GroupSyncConfig;
import com.discordsrv.common.groupsync.enums.GroupSyncCause;
import com.discordsrv.common.groupsync.enums.GroupSyncResult;
import java.util.*;
public class SynchronizationSummary {
private final EnumMap<GroupSyncResult, Set<GroupSyncConfig.PairConfig>> 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<GroupSyncResult, Set<GroupSyncConfig.PairConfig>> entry : pairs.entrySet()) {
message.append(count == 1 ? ": " : "\n")
.append(entry.getKey().toString())
.append(": ")
.append(entry.getValue().toString());
}
return message.toString();
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.groupsync.enums;
public enum GroupSyncDirection {
MINECRAFT_TO_DISCORD,
DISCORD_TO_MINECRAFT,
BIDIRECTIONAL
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.groupsync.enums;
public enum GroupSyncSide {
MINECRAFT,
DISCORD
}

View File

@ -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<DiscordSRV> implements PermissionDataProvider.All {
private LuckPerms luckPerms;
private final List<EventSubscription<?>> subscriptions = new ArrayList<>();
public LuckPermsIntegration(DiscordSRV discordSRV) {
super(discordSRV);
@ -57,10 +76,20 @@ public class LuckPermsIntegration extends PluginIntegration<DiscordSRV> 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 <E extends LuckPermsEvent> void subscribe(Class<E> clazz, Consumer<E> 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<DiscordSRV> implemen
}
@Override
public CompletableFuture<Boolean> hasGroup(UUID player, String groupName) {
public Set<String> getDefaultServerContext() {
return luckPerms.getContextManager().getStaticContext().getValues(DefaultContextKeys.SERVER_KEY);
}
@Override
public CompletableFuture<Boolean> 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<Void> addGroup(UUID player, String groupName) {
return groupMutate(player, groupName, NodeMap::add);
public CompletableFuture<Void> addGroup(UUID player, String groupName, String serverContext) {
return groupMutate(player, groupName, serverContext, NodeMap::add);
}
@Override
public CompletableFuture<Void> removeGroup(UUID player, String groupName) {
return groupMutate(player, groupName, NodeMap::remove);
public CompletableFuture<Void> removeGroup(UUID player, String groupName, String serverContext) {
return groupMutate(player, groupName, serverContext, NodeMap::remove);
}
private CompletableFuture<Void> groupMutate(UUID player, String groupName, BiFunction<NodeMap, Node, DataMutateResult> function) {
private CompletableFuture<Void> groupMutate(UUID player, String groupName, String serverContext, BiFunction<NodeMap, Node, DataMutateResult> function) {
Group group = luckPerms.getGroupManager().getGroup(groupName);
if (group == null) {
CompletableFuture<Void> 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<DiscordSRV> implemen
public CompletableFuture<String> 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<String> serverContext = inheritanceNode.getContexts().getValues(DefaultContextKeys.SERVER_KEY);
groupUpdate((User) holder, groupName, serverContext, remove, false);
}
private void groupUpdate(User user, String groupName, Set<String> 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);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Boolean> hasGroup(UUID player, String groupName);
CompletableFuture<Boolean> hasGroup(UUID player, String groupName, boolean includeInherited);
CompletableFuture<Void> addGroup(UUID player, String groupName);
CompletableFuture<Void> removeGroup(UUID player, String groupName);
}
@ -46,4 +51,27 @@ public interface PermissionDataProvider extends Module {
CompletableFuture<String> getMeta(UUID player, String key);
}
interface GroupsContext extends Groups {
Set<String> getDefaultServerContext();
CompletableFuture<Boolean> hasGroup(UUID player, String groupName, boolean includeInherited, @Nullable String serverContext);
CompletableFuture<Void> addGroup(UUID player, String groupName, @Nullable String serverContext);
CompletableFuture<Void> removeGroup(UUID player, String groupName, @Nullable String serverContext);
@Override
default CompletableFuture<Boolean> hasGroup(UUID player, String groupName, boolean includeInherited) {
return hasGroup(player, groupName, includeInherited, null);
}
@Override
default CompletableFuture<Void> addGroup(UUID player, String groupName) {
return addGroup(player, groupName, null);
}
@Override
default CompletableFuture<Void> removeGroup(UUID player, String groupName) {
return removeGroup(player, groupName, null);
}
}
}