diff --git a/api/src/main/java/com/discordsrv/api/color/Color.java b/api/src/main/java/com/discordsrv/api/color/Color.java
index 87d2d28f..f85a5ad3 100644
--- a/api/src/main/java/com/discordsrv/api/color/Color.java
+++ b/api/src/main/java/com/discordsrv/api/color/Color.java
@@ -34,6 +34,8 @@ public class Color {
* Discord's blurple color (Discord branding).
*/
public static final Color BLURPLE = new Color(0x5865F2);
+ public static final Color WHITE = new Color(0xFFFFFF);
+ public static final Color BLACK = new Color(0);
private final int rgb;
diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/guild/DiscordGuild.java b/api/src/main/java/com/discordsrv/api/discord/entity/guild/DiscordGuild.java
index 4eefd857..ddf3449a 100644
--- a/api/src/main/java/com/discordsrv/api/discord/entity/guild/DiscordGuild.java
+++ b/api/src/main/java/com/discordsrv/api/discord/entity/guild/DiscordGuild.java
@@ -32,6 +32,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
/**
* A Discord server.
@@ -53,6 +54,14 @@ public interface DiscordGuild extends JDAEntity, Snowflake {
@Placeholder("server_member_count")
int getMemberCount();
+ /**
+ * Retrieves a Discord guild member from Discord by id.
+ * @param id the id for the Discord guild member
+ * @return a future for the Discord guild member
+ */
+ @NotNull
+ CompletableFuture retrieveMemberById(long id);
+
/**
* Gets a Discord guild member by id from the cache, the provided entity can be cached and will not update if it changes on Discord.
* @param id the id for the Discord guild member
diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/guild/DiscordGuildMember.java b/api/src/main/java/com/discordsrv/api/discord/entity/guild/DiscordGuildMember.java
index c5f2dd70..ee0addcc 100644
--- a/api/src/main/java/com/discordsrv/api/discord/entity/guild/DiscordGuildMember.java
+++ b/api/src/main/java/com/discordsrv/api/discord/entity/guild/DiscordGuildMember.java
@@ -32,6 +32,7 @@ import net.dv8tion.jda.api.entities.Member;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.time.OffsetDateTime;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -115,4 +116,27 @@ public interface DiscordGuildMember extends JDAEntity, Mentionable {
@Placeholder("user_color")
Color getColor();
+ /**
+ * Gets the time the member joined the server.
+ * @return the time the member joined the server
+ */
+ @NotNull
+ OffsetDateTime getTimeJoined();
+
+ /**
+ * Time the member started boosting.
+ * @return the time the member started boosting or {@code null}
+ */
+ @Nullable
+ OffsetDateTime getTimeBoosted();
+
+ /**
+ * If the Discord server member is boosted.
+ * @return {@code true} if this Discord server member is boosting
+ */
+ @Placeholder("user_isboosting")
+ default boolean isBoosting() {
+ return getTimeBoosted() != null;
+ }
+
}
diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java
index 149d1de0..c62d29fc 100644
--- a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java
+++ b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java
@@ -31,6 +31,7 @@ import com.discordsrv.bukkit.listener.BukkitDeathListener;
import com.discordsrv.bukkit.listener.BukkitStatusMessageListener;
import com.discordsrv.bukkit.player.BukkitPlayerProvider;
import com.discordsrv.bukkit.plugin.BukkitPluginManager;
+import com.discordsrv.bukkit.requiredlinking.BukkitRequiredLinkingModule;
import com.discordsrv.bukkit.scheduler.BukkitScheduler;
import com.discordsrv.common.ServerDiscordSRV;
import com.discordsrv.common.command.game.handler.ICommandHandler;
@@ -222,6 +223,7 @@ public class BukkitDiscordSRV extends ServerDiscordSRV.
+ */
+
+package com.discordsrv.bukkit.config.main;
+
+import com.discordsrv.common.config.main.linking.ServerRequiredLinkingConfig;
+import org.bukkit.event.EventPriority;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.meta.Comment;
+
+public class BukkitRequiredLinkingConfig extends ServerRequiredLinkingConfig {
+
+ public KickOptions kick = new KickOptions();
+
+ @ConfigSerializable
+ public static class KickOptions {
+
+ @Comment("The event to use for kick.\n"
+ + "Available events: AsyncPlayerPreLoginEvent (preferred), PlayerLoginEvent")
+ public String event = "AsyncPlayerPreLoginEvent";
+
+ @Comment("The event priority to use for the kick")
+ public String priority = EventPriority.NORMAL.name();
+
+ }
+}
diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/integration/chat/LunaChatIntegration.java b/bukkit/src/main/java/com/discordsrv/bukkit/integration/chat/LunaChatIntegration.java
index 041dd949..d9a9fafd 100644
--- a/bukkit/src/main/java/com/discordsrv/bukkit/integration/chat/LunaChatIntegration.java
+++ b/bukkit/src/main/java/com/discordsrv/bukkit/integration/chat/LunaChatIntegration.java
@@ -1,3 +1,21 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
package com.discordsrv.bukkit.integration.chat;
import com.discordsrv.api.channel.GameChannel;
diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/requiredlinking/BukkitRequiredLinkingModule.java b/bukkit/src/main/java/com/discordsrv/bukkit/requiredlinking/BukkitRequiredLinkingModule.java
new file mode 100644
index 00000000..eaf1ed1c
--- /dev/null
+++ b/bukkit/src/main/java/com/discordsrv/bukkit/requiredlinking/BukkitRequiredLinkingModule.java
@@ -0,0 +1,130 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.bukkit.requiredlinking;
+
+import com.discordsrv.bukkit.BukkitDiscordSRV;
+import com.discordsrv.bukkit.config.main.BukkitRequiredLinkingConfig;
+import com.discordsrv.common.config.main.linking.RequirementsConfig;
+import com.discordsrv.common.linking.requirelinking.ServerRequireLinkingModule;
+import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer;
+import net.kyori.adventure.text.Component;
+import org.bukkit.event.Event;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
+import org.bukkit.event.player.PlayerLoginEvent;
+
+import java.util.UUID;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+// TODO: implement freeze
+public class BukkitRequiredLinkingModule extends ServerRequireLinkingModule implements Listener {
+
+ public BukkitRequiredLinkingModule(BukkitDiscordSRV discordSRV) {
+ super(discordSRV);
+ }
+
+ @Override
+ public RequirementsConfig config() {
+ return discordSRV.config().requiredLinking.requirements;
+ }
+
+ @Override
+ public void enable() {
+ super.enable();
+
+ register(PlayerLoginEvent.class, this::handle);
+ register(AsyncPlayerPreLoginEvent.class, this::handle);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void register(Class eventType, BiConsumer eventConsumer) {
+ for (EventPriority priority : EventPriority.values()) {
+ if (priority == EventPriority.MONITOR) {
+ continue;
+ }
+
+ discordSRV.server().getPluginManager().registerEvent(
+ eventType,
+ this,
+ priority,
+ (listener, event) -> eventConsumer.accept((T) event, priority),
+ discordSRV.plugin(),
+ true
+ );
+ }
+ }
+
+ @SuppressWarnings("deprecation") // Component is relocated so using it here is inconvenient
+ private void handle(AsyncPlayerPreLoginEvent event, EventPriority priority) {
+ handle(
+ "AsyncPlayerPreLoginEvent",
+ priority,
+ event.getUniqueId(),
+ () -> event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED ? event.getLoginResult().name() : null,
+ text -> event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, text)
+ );
+ }
+
+ @SuppressWarnings("deprecation") // Component is relocated so using it here is inconvenient
+ private void handle(PlayerLoginEvent event, EventPriority priority) {
+ handle(
+ "PlayerLoginEvent",
+ priority,
+ event.getPlayer().getUniqueId(),
+ () -> event.getResult() != PlayerLoginEvent.Result.ALLOWED ? event.getResult().name() : null,
+ text -> event.disallow(PlayerLoginEvent.Result.KICK_OTHER, text)
+ );
+ }
+
+ private void handle(
+ String eventType,
+ EventPriority priority,
+ UUID playerUUID,
+ Supplier alreadyBlocked,
+ Consumer disallow
+ ) {
+ BukkitRequiredLinkingConfig config = discordSRV.config().requiredLinking;
+ if (!config.enabled || !config.action.equalsIgnoreCase("KICK")
+ || !eventType.equals(config.kick.event) || !priority.name().equals(config.kick.priority)) {
+ return;
+ }
+
+ String blockType = alreadyBlocked.get();
+ if (blockType != null) {
+ discordSRV.logger().debug(playerUUID + " is already blocked for " + eventType + "/" + priority + " (" + blockType + ")");
+ return;
+ }
+
+ Component kickReason = getKickReason(playerUUID).join();
+ if (kickReason != null) {
+ disallow.accept(BukkitComponentSerializer.legacy().serialize(kickReason));
+ }
+ }
+
+ @Override
+ public void disable() {
+ super.disable();
+
+ HandlerList.unregisterAll(this);
+ }
+}
diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java
index fecf7562..5aa65fea 100644
--- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java
+++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java
@@ -33,7 +33,7 @@ import com.discordsrv.common.command.game.GameCommandModule;
import com.discordsrv.common.component.ComponentFactory;
import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.connection.UpdateConfig;
-import com.discordsrv.common.config.main.LinkedAccountConfig;
+import com.discordsrv.common.config.main.linking.LinkedAccountConfig;
import com.discordsrv.common.config.main.MainConfig;
import com.discordsrv.common.config.manager.ConnectionConfigManager;
import com.discordsrv.common.config.manager.MainConfigManager;
diff --git a/common/src/main/java/com/discordsrv/common/channel/GlobalChannel.java b/common/src/main/java/com/discordsrv/common/channel/GlobalChannel.java
index 4baa038c..999b2e02 100644
--- a/common/src/main/java/com/discordsrv/common/channel/GlobalChannel.java
+++ b/common/src/main/java/com/discordsrv/common/channel/GlobalChannel.java
@@ -1,3 +1,21 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
package com.discordsrv.common.channel;
import com.discordsrv.api.channel.GameChannel;
diff --git a/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ResyncCommand.java b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ResyncCommand.java
index f410aa78..9d9bbf09 100644
--- a/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ResyncCommand.java
+++ b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ResyncCommand.java
@@ -35,7 +35,6 @@ import net.kyori.adventure.text.format.NamedTextColor;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
-import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@@ -75,7 +74,7 @@ public class ResyncCommand implements GameCommandExecutor {
.whenComplete((results, t) -> {
EnumMap resultCounts = new EnumMap<>(GroupSyncResult.class);
int total = 0;
- for (Set result : results) {
+ for (List result : results) {
for (GroupSyncResult singleResult : result) {
total++;
resultCounts.computeIfAbsent(singleResult, key -> new AtomicInteger(0)).getAndIncrement();
@@ -103,8 +102,8 @@ public class ResyncCommand implements GameCommandExecutor {
});
}
- private List>> resyncOnlinePlayers(GroupSyncModule module) {
- List>> futures = new ArrayList<>();
+ private List>> resyncOnlinePlayers(GroupSyncModule module) {
+ List>> futures = new ArrayList<>();
for (IPlayer player : discordSRV.playerProvider().allPlayers()) {
futures.add(module.resync(player.uniqueId(), GroupSyncCause.COMMAND));
}
diff --git a/common/src/main/java/com/discordsrv/common/config/annotation/Order.java b/common/src/main/java/com/discordsrv/common/config/annotation/Order.java
index 132c472a..68a09e52 100644
--- a/common/src/main/java/com/discordsrv/common/config/annotation/Order.java
+++ b/common/src/main/java/com/discordsrv/common/config/annotation/Order.java
@@ -31,5 +31,9 @@ import java.lang.annotation.Target;
@Target(ElementType.FIELD)
public @interface Order {
+ /**
+ * Lowest to highest.
+ * @return the order value of the option
+ */
int value();
}
diff --git a/common/src/main/java/com/discordsrv/common/config/connection/MinecraftAuthConfig.java b/common/src/main/java/com/discordsrv/common/config/connection/MinecraftAuthConfig.java
index e1ae7e30..52c430d1 100644
--- a/common/src/main/java/com/discordsrv/common/config/connection/MinecraftAuthConfig.java
+++ b/common/src/main/java/com/discordsrv/common/config/connection/MinecraftAuthConfig.java
@@ -26,7 +26,11 @@ public class MinecraftAuthConfig {
@Comment("If minecraftauth.me connections are allowed for Discord linking (requires linked-accounts.provider to be \"auto\" or \"minecraftauth\").\n"
+ "Requires a connection to: minecraftauth.me\n"
- + "Privacy Policy: https://minecraftauth.me/privacy.txt")
+ + "Privacy Policy: https://minecraftauth.me/privacy")
public boolean allow = true;
+ @Comment("minecraftauth.me token for checking subscription, following and membership statuses for required linking\n"
+ + "You can get the token from https://minecraftauth.me/api/token whilst logged in (please keep in mind that the token resets every time you visit that page)")
+ public String token = "";
+
}
diff --git a/common/src/main/java/com/discordsrv/common/config/connection/UpdateConfig.java b/common/src/main/java/com/discordsrv/common/config/connection/UpdateConfig.java
index f73f023f..86768b79 100644
--- a/common/src/main/java/com/discordsrv/common/config/connection/UpdateConfig.java
+++ b/common/src/main/java/com/discordsrv/common/config/connection/UpdateConfig.java
@@ -75,7 +75,7 @@ public class UpdateConfig {
@Setting(value = "force")
@Comment("If the security check needs to be completed for DiscordSRV to enable,\n"
- + "if the security check fails, DiscordSRV will be disabled if this option is set to true")
+ + "if the security check cannot be performed, DiscordSRV will be disabled if this option is set to true")
public boolean force = false;
}
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 0c70bb8d..169e1f16 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.Config;
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 com.discordsrv.common.config.main.linking.LinkedAccountConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
diff --git a/common/src/main/java/com/discordsrv/common/config/main/PluginIntegrationConfig.java b/common/src/main/java/com/discordsrv/common/config/main/PluginIntegrationConfig.java
index d59ce6e6..e7cfea4a 100644
--- a/common/src/main/java/com/discordsrv/common/config/main/PluginIntegrationConfig.java
+++ b/common/src/main/java/com/discordsrv/common/config/main/PluginIntegrationConfig.java
@@ -1,3 +1,21 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
package com.discordsrv.common.config.main;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
diff --git a/common/src/main/java/com/discordsrv/common/config/main/LinkedAccountConfig.java b/common/src/main/java/com/discordsrv/common/config/main/linking/LinkedAccountConfig.java
similarity index 96%
rename from common/src/main/java/com/discordsrv/common/config/main/LinkedAccountConfig.java
rename to common/src/main/java/com/discordsrv/common/config/main/linking/LinkedAccountConfig.java
index d5a6d6b5..d67555bd 100644
--- a/common/src/main/java/com/discordsrv/common/config/main/LinkedAccountConfig.java
+++ b/common/src/main/java/com/discordsrv/common/config/main/linking/LinkedAccountConfig.java
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package com.discordsrv.common.config.main;
+package com.discordsrv.common.config.main.linking;
import com.discordsrv.common.config.connection.ConnectionConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
diff --git a/common/src/main/java/com/discordsrv/common/config/main/linking/ProxyRequiredLinkingConfig.java b/common/src/main/java/com/discordsrv/common/config/main/linking/ProxyRequiredLinkingConfig.java
new file mode 100644
index 00000000..ddef51ed
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/config/main/linking/ProxyRequiredLinkingConfig.java
@@ -0,0 +1,37 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.config.main.linking;
+
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@ConfigSerializable
+public class ProxyRequiredLinkingConfig extends RequiredLinkingConfig {
+
+ public TargetRequirementConfig proxyRequirements = new TargetRequirementConfig();
+
+ public Map serverRequirements = new HashMap() {{
+ put("example", new TargetRequirementConfig());
+ }};
+
+ @ConfigSerializable
+ public static class TargetRequirementConfig extends RequirementsConfig {}
+}
diff --git a/common/src/main/java/com/discordsrv/common/config/main/linking/RequiredLinkingConfig.java b/common/src/main/java/com/discordsrv/common/config/main/linking/RequiredLinkingConfig.java
new file mode 100644
index 00000000..91fab017
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/config/main/linking/RequiredLinkingConfig.java
@@ -0,0 +1,31 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.config.main.linking;
+
+import com.discordsrv.common.config.annotation.Order;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.meta.Comment;
+
+@ConfigSerializable
+public abstract class RequiredLinkingConfig {
+
+ @Comment("If required linking is enabled")
+ @Order(-10)
+ public boolean enabled = false;
+}
diff --git a/common/src/main/java/com/discordsrv/common/config/main/linking/RequirementsConfig.java b/common/src/main/java/com/discordsrv/common/config/main/linking/RequirementsConfig.java
new file mode 100644
index 00000000..764b64a0
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/config/main/linking/RequirementsConfig.java
@@ -0,0 +1,59 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.config.main.linking;
+
+import com.discordsrv.common.config.annotation.DefaultOnly;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.meta.Comment;
+import org.spongepowered.configurate.objectmapping.meta.Setting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@ConfigSerializable
+public class RequirementsConfig {
+
+ @Setting("bypass-uuids")
+ @Comment("A list of uuids that are allowed to bypass these requirements")
+ @DefaultOnly
+ public List bypassUUIDs = new ArrayList<>(Collections.singletonList("6c983d46-0631-48b8-9baf-5e33eb5ffec4"));
+
+ @Comment("Requirements players must meet to be pass requirements\n"
+ + "Only one option has to pass, for example [\"TwitchSubscriber()\", \"DiscordRole(...)\"] allows twitch subscribers and users with the specified role to play\n"
+ + "while [\"TwitchSubscriber() && DiscordRole(...)\"] only allows twitch subscribers with the specified role to play\n"
+ + "\n"
+ + "Valid values are:\n"
+ + "DiscordServer(Server ID)\n"
+ + "DiscordBoosting(Server ID)\n"
+ + "DiscordRole(Role ID)\n"
+ + "The following are available if you're using MinecraftAuth.me for linked accounts:\n"
+ + "PatreonSubscriber() or PatreonSubscriber(Tier Title)\n"
+ + "GlimpseSubscriber() or GlimpseSubscriber(Level Name)\n"
+ + "TwitchFollower()\n"
+ + "TwitchSubscriber() or TwitchSubscriber(Tier)\n"
+ + "YouTubeSubscriber()\n"
+ + "YouTubeMember() or YouTubeMember(Tier)\n"
+ + "\n"
+ + "The following operators are available:\n"
+ + "&& = and, for example: \"DiscordServer(...) && TwitchFollower()\"\n"
+ + "|| = or, for example \"DiscordBoosting(...) || YouTubeMember()\n"
+ + "You can also use brackets () to clear ambiguity, for example: \"DiscordServer(...) && (TwitchSubscriber() || PatreonSubscriber())\"")
+ public List requirements = new ArrayList<>();
+}
diff --git a/common/src/main/java/com/discordsrv/common/config/main/linking/ServerRequiredLinkingConfig.java b/common/src/main/java/com/discordsrv/common/config/main/linking/ServerRequiredLinkingConfig.java
new file mode 100644
index 00000000..39da0acb
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/config/main/linking/ServerRequiredLinkingConfig.java
@@ -0,0 +1,36 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.config.main.linking;
+
+import com.discordsrv.common.config.annotation.Order;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.meta.Comment;
+import org.spongepowered.configurate.objectmapping.meta.Setting;
+
+@ConfigSerializable
+public class ServerRequiredLinkingConfig extends RequiredLinkingConfig {
+
+ @Comment("How the player should be blocked from joining the server.\nAvailable options: KICK, FREEZE")
+ public String action = "KICK";
+
+ @Setting(nodeFromParent = true)
+ @Order(10)
+ public RequirementsConfig requirements = new RequirementsConfig();
+
+}
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 ee49f36b..a187f4b8 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
@@ -29,6 +29,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
+import java.util.concurrent.CompletableFuture;
public class DiscordGuildImpl implements DiscordGuild {
@@ -55,6 +56,14 @@ public class DiscordGuildImpl implements DiscordGuild {
return guild.getMemberCount();
}
+ @Override
+ public @NotNull CompletableFuture retrieveMemberById(long id) {
+ return discordSRV.discordAPI().mapExceptions(() -> guild.retrieveMemberById(id)
+ .submit()
+ .thenApply(member -> new DiscordGuildMemberImpl(discordSRV, member))
+ );
+ }
+
@Override
public @Nullable DiscordGuildMember getMemberById(long id) {
Member member = guild.getMemberById(id);
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 85a29ffe..73d7a4cb 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
@@ -34,6 +34,7 @@ import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -110,6 +111,16 @@ public class DiscordGuildMemberImpl implements DiscordGuildMember {
return color;
}
+ @Override
+ public @NotNull OffsetDateTime getTimeJoined() {
+ return member.getTimeJoined();
+ }
+
+ @Override
+ public @Nullable OffsetDateTime getTimeBoosted() {
+ return member.getTimeBoosted();
+ }
+
//
// Placeholders
//
diff --git a/common/src/main/java/com/discordsrv/common/discord/api/entity/message/ReceivedDiscordMessageClusterImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/entity/message/ReceivedDiscordMessageClusterImpl.java
index cab7c900..bace4108 100644
--- a/common/src/main/java/com/discordsrv/common/discord/api/entity/message/ReceivedDiscordMessageClusterImpl.java
+++ b/common/src/main/java/com/discordsrv/common/discord/api/entity/message/ReceivedDiscordMessageClusterImpl.java
@@ -24,17 +24,15 @@ import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.future.util.CompletableFutureUtil;
import org.jetbrains.annotations.NotNull;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
import java.util.concurrent.CompletableFuture;
public class ReceivedDiscordMessageClusterImpl implements ReceivedDiscordMessageCluster {
private final Set messages;
- public ReceivedDiscordMessageClusterImpl(Set messages) {
- this.messages = messages;
+ public ReceivedDiscordMessageClusterImpl(Collection messages) {
+ this.messages = new HashSet<>(messages);
}
@Override
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
index 004fa1bf..eaf580c3 100644
--- a/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java
+++ b/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java
@@ -18,9 +18,9 @@
package com.discordsrv.common.future.util;
+import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
public final class CompletableFutureUtil {
@@ -37,19 +37,20 @@ public final class CompletableFutureUtil {
}
@SuppressWarnings("unchecked")
- public static CompletableFuture> combine(Collection> futures) {
+ public static CompletableFuture> combine(Collection> futures) {
return combine(futures.toArray(new CompletableFuture[0]));
}
- public static CompletableFuture> combine(CompletableFuture[] futures) {
- CompletableFuture> future = new CompletableFuture<>();
+ @SafeVarargs
+ 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<>();
+ List results = new ArrayList<>();
for (CompletableFuture aFuture : futures) {
results.add(aFuture.join());
}
diff --git a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java
index 121f95a4..b26a8388 100644
--- a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java
+++ b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java
@@ -255,20 +255,20 @@ public class GroupSyncModule extends AbstractModule {
// Resync user
- public CompletableFuture> resync(UUID player, GroupSyncCause cause) {
+ public CompletableFuture> resync(UUID player, GroupSyncCause cause) {
return lookupLinkedAccount(player).thenCompose(userId -> {
if (userId == null) {
- return CompletableFuture.completedFuture(Collections.emptySet());
+ return CompletableFuture.completedFuture(Collections.emptyList());
}
return CompletableFutureUtil.combine(resync(player, userId, cause));
});
}
- public CompletableFuture> resync(long userId, GroupSyncCause cause) {
+ public CompletableFuture> resync(long userId, GroupSyncCause cause) {
return lookupLinkedAccount(userId).thenCompose(player -> {
if (player == null) {
- return CompletableFuture.completedFuture(Collections.emptySet());
+ return CompletableFuture.completedFuture(Collections.emptyList());
}
return CompletableFutureUtil.combine(resync(player, userId, cause));
diff --git a/common/src/main/java/com/discordsrv/common/linking/LinkStore.java b/common/src/main/java/com/discordsrv/common/linking/LinkStore.java
index 448f909c..217d38dd 100644
--- a/common/src/main/java/com/discordsrv/common/linking/LinkStore.java
+++ b/common/src/main/java/com/discordsrv/common/linking/LinkStore.java
@@ -26,6 +26,7 @@ import java.util.concurrent.CompletableFuture;
public interface LinkStore extends LinkProvider {
CompletableFuture createLink(@NotNull UUID playerUUID, long userId);
+ CompletableFuture removeLink(@NotNull UUID playerUUID, long userId);
CompletableFuture getLinkedAccountCount();
}
diff --git a/common/src/main/java/com/discordsrv/common/linking/impl/MemoryLinker.java b/common/src/main/java/com/discordsrv/common/linking/impl/MemoryLinker.java
index cb512736..c33109a5 100644
--- a/common/src/main/java/com/discordsrv/common/linking/impl/MemoryLinker.java
+++ b/common/src/main/java/com/discordsrv/common/linking/impl/MemoryLinker.java
@@ -48,6 +48,12 @@ public class MemoryLinker implements LinkProvider, LinkStore {
return CompletableFuture.completedFuture(null);
}
+ @Override
+ public CompletableFuture removeLink(@NotNull UUID playerUUID, long userId) {
+ map.remove(playerUUID);
+ return CompletableFuture.completedFuture(null);
+ }
+
@Override
public CompletableFuture getLinkedAccountCount() {
return CompletableFuture.completedFuture(map.size());
diff --git a/common/src/main/java/com/discordsrv/common/linking/impl/MinecraftAuthenticationLinker.java b/common/src/main/java/com/discordsrv/common/linking/impl/MinecraftAuthenticationLinker.java
index dcef084c..4aa7a4fa 100644
--- a/common/src/main/java/com/discordsrv/common/linking/impl/MinecraftAuthenticationLinker.java
+++ b/common/src/main/java/com/discordsrv/common/linking/impl/MinecraftAuthenticationLinker.java
@@ -19,19 +19,22 @@
package com.discordsrv.common.linking.impl;
import com.discordsrv.common.DiscordSRV;
+import com.discordsrv.common.function.CheckedSupplier;
+import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.linking.LinkProvider;
import com.discordsrv.common.logging.Logger;
import com.discordsrv.common.logging.NamedLogger;
import me.minecraftauth.lib.AuthService;
import me.minecraftauth.lib.account.AccountType;
-import me.minecraftauth.lib.account.Identity;
-import me.minecraftauth.lib.account.MinecraftAccount;
-import me.minecraftauth.lib.exception.LookupException;
+import me.minecraftauth.lib.account.platform.discord.DiscordAccount;
+import me.minecraftauth.lib.account.platform.minecraft.MinecraftAccount;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
public class MinecraftAuthenticationLinker extends CachedLinkProvider implements LinkProvider {
@@ -44,35 +47,85 @@ public class MinecraftAuthenticationLinker extends CachedLinkProvider implements
@Override
public CompletableFuture> queryUserId(@NotNull UUID playerUUID) {
- return CompletableFuture.supplyAsync(
- () -> {
- try {
- return AuthService.lookup(AccountType.MINECRAFT, playerUUID.toString())
- .map(Identity::getDiscordAccount)
- .map(discord -> Long.parseUnsignedLong(discord.getUserId()));
- } catch (LookupException e) {
- logger.error("Lookup for uuid " + playerUUID + " failed", e);
- return Optional.empty();
- }
- },
- discordSRV.scheduler().executor()
- );
+ return query(
+ () -> AuthService.lookup(AccountType.MINECRAFT, playerUUID.toString(), AccountType.DISCORD)
+ .map(account -> (DiscordAccount) account)
+ .map(discord -> Long.parseUnsignedLong(discord.getUserId())),
+ () -> discordSRV.storage().getUserId(playerUUID),
+ userId -> linked(playerUUID, userId),
+ userId -> unlinked(playerUUID, userId)
+ ).exceptionally(t -> {
+ logger.error("Lookup for uuid " + playerUUID + " failed", t);
+ return Optional.empty();
+ });
}
@Override
public CompletableFuture> queryPlayerUUID(long userId) {
- return CompletableFuture.supplyAsync(
- () -> {
- try {
- return AuthService.lookup(AccountType.DISCORD, Long.toUnsignedString(userId))
- .map(Identity::getMinecraftAccount)
- .map(MinecraftAccount::getUUID);
- } catch (LookupException e) {
- logger.error("Lookup for user id " + Long.toUnsignedString(userId) + " failed", e);
- return Optional.empty();
- }
- },
+ return query(
+ () -> AuthService.lookup(AccountType.DISCORD, Long.toUnsignedString(userId), AccountType.MINECRAFT)
+ .map(account -> (MinecraftAccount) account)
+ .map(MinecraftAccount::getUUID),
+ () -> discordSRV.storage().getPlayerUUID(userId),
+ playerUUID -> linked(playerUUID, userId),
+ playerUUID -> unlinked(playerUUID, userId)
+ ).exceptionally(t -> {
+ logger.error("Lookup for user id " + Long.toUnsignedString(userId) + " failed", t);
+ return Optional.empty();
+ });
+ }
+
+ private void linked(UUID playerUUID, long userId) {
+ logger.debug("New link: " + playerUUID + " & " + Long.toUnsignedString(userId));
+ discordSRV.storage().createLink(playerUUID, userId);
+
+ }
+
+ private void unlinked(UUID playerUUID, long userId) {
+ logger.debug("Unlink: " + playerUUID + " & " + Long.toUnsignedString(userId));
+ discordSRV.storage().removeLink(playerUUID, userId);
+
+ }
+
+ private CompletableFuture> query(
+ CheckedSupplier> authSupplier,
+ Supplier storageSupplier,
+ Consumer linked,
+ Consumer unlinked
+ ) {
+ CompletableFuture> authService = new CompletableFuture<>();
+
+ discordSRV.scheduler().run(() -> {
+ try {
+ authService.complete(authSupplier.get());
+ } catch (Throwable t) {
+ authService.completeExceptionally(t);
+ }
+ });
+ CompletableFuture> storageResult = CompletableFuture.supplyAsync(
+ () -> Optional.ofNullable(storageSupplier.get()),
discordSRV.scheduler().executor()
);
+
+ return CompletableFutureUtil.combine(authService, storageResult).thenApply(results -> {
+ Optional auth = authService.join();
+ Optional storage = storageResult.join();
+
+ if (auth.isPresent() && !storage.isPresent()) {
+ // new link
+ linked.accept(auth.get());
+ }
+ if (!auth.isPresent() && storage.isPresent()) {
+ // unlink
+ unlinked.accept(storage.get());
+ }
+ if (auth.isPresent() && storage.isPresent() && !auth.get().equals(storage.get())) {
+ // linked account changed
+ unlinked.accept(storage.get());
+ linked.accept(auth.get());
+ }
+
+ return auth;
+ });
}
}
diff --git a/common/src/main/java/com/discordsrv/common/linking/impl/StorageLinker.java b/common/src/main/java/com/discordsrv/common/linking/impl/StorageLinker.java
index 9c9d89d6..6bd29c2e 100644
--- a/common/src/main/java/com/discordsrv/common/linking/impl/StorageLinker.java
+++ b/common/src/main/java/com/discordsrv/common/linking/impl/StorageLinker.java
@@ -57,6 +57,14 @@ public class StorageLinker extends CachedLinkProvider implements LinkProvider, L
);
}
+ @Override
+ public CompletableFuture removeLink(@NotNull UUID playerUUID, long userId) {
+ return CompletableFuture.runAsync(
+ () -> discordSRV.storage().removeLink(playerUUID, userId),
+ discordSRV.scheduler().executor()
+ );
+ }
+
@Override
public CompletableFuture getLinkedAccountCount() {
return CompletableFuture.supplyAsync(
diff --git a/common/src/main/java/com/discordsrv/common/linking/requirelinking/RequiredLinkingModule.java b/common/src/main/java/com/discordsrv/common/linking/requirelinking/RequiredLinkingModule.java
new file mode 100644
index 00000000..88c2a172
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/linking/requirelinking/RequiredLinkingModule.java
@@ -0,0 +1,87 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.linking.requirelinking;
+
+import com.discordsrv.common.DiscordSRV;
+import com.discordsrv.common.linking.impl.MinecraftAuthenticationLinker;
+import com.discordsrv.common.linking.requirelinking.requirement.*;
+import com.discordsrv.common.linking.requirelinking.requirement.parser.RequirementParser;
+import com.discordsrv.common.module.type.AbstractModule;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.BiFunction;
+
+public abstract class RequiredLinkingModule extends AbstractModule {
+
+ private final List> availableRequirements = new ArrayList<>();
+
+ public RequiredLinkingModule(T discordSRV) {
+ super(discordSRV);
+ }
+
+ @Override
+ public void reloadNoResult() {
+ List> requirements = new ArrayList<>();
+
+ requirements.add(new DiscordRoleRequirement(discordSRV));
+ requirements.add(new DiscordServerRequirement(discordSRV));
+ requirements.add(new DiscordBoostingRequirement(discordSRV));
+
+ if (discordSRV.linkProvider() instanceof MinecraftAuthenticationLinker) {
+ requirements.addAll(MinecraftAuthRequirement.createRequirements(discordSRV));
+ }
+
+ synchronized (availableRequirements) {
+ availableRequirements.clear();
+ availableRequirements.addAll(requirements);
+ }
+ }
+
+ protected List compile(List requirements) {
+ List checks = new ArrayList<>();
+ for (String requirement : requirements) {
+ BiFunction> function = RequirementParser.getInstance().parse(requirement, availableRequirements);
+ checks.add(new CompiledRequirement(requirement, function));
+ }
+ return checks;
+ }
+
+ public static class CompiledRequirement {
+
+ private final String input;
+ private final BiFunction> function;
+
+ protected CompiledRequirement(String input, BiFunction> function) {
+ this.input = input;
+ this.function = function;
+ }
+
+ public String input() {
+ return input;
+ }
+
+ public BiFunction> function() {
+ return function;
+ }
+ }
+
+}
diff --git a/common/src/main/java/com/discordsrv/common/linking/requirelinking/ServerRequireLinkingModule.java b/common/src/main/java/com/discordsrv/common/linking/requirelinking/ServerRequireLinkingModule.java
new file mode 100644
index 00000000..c4c9a9d2
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/linking/requirelinking/ServerRequireLinkingModule.java
@@ -0,0 +1,114 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.linking.requirelinking;
+
+import com.discordsrv.common.DiscordSRV;
+import com.discordsrv.common.config.main.linking.RequirementsConfig;
+import com.discordsrv.common.future.util.CompletableFutureUtil;
+import com.discordsrv.common.linking.LinkProvider;
+import net.kyori.adventure.text.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public abstract class ServerRequireLinkingModule extends RequiredLinkingModule {
+
+ private final List compiledRequirements = new CopyOnWriteArrayList<>();
+
+ public ServerRequireLinkingModule(T discordSRV) {
+ super(discordSRV);
+ }
+
+ public abstract RequirementsConfig config();
+
+ @Override
+ public void reloadNoResult() {
+ super.reloadNoResult();
+
+ synchronized (compiledRequirements) {
+ compiledRequirements.clear();
+ compiledRequirements.addAll(compile(config().requirements));
+ }
+ }
+
+ public CompletableFuture getKickReason(UUID playerUUID) {
+ RequirementsConfig config = config();
+ if (config.bypassUUIDs.contains(playerUUID.toString())) {
+ // Bypasses: let them through
+ return CompletableFuture.completedFuture(null);
+ }
+
+ LinkProvider linkProvider = discordSRV.linkProvider();
+ if (linkProvider == null) {
+ // Link provider unavailable but required linking enabled: error message
+ return CompletableFuture.completedFuture(Component.text("Unable to check linking status at this time"));
+ }
+
+ return linkProvider.getUserId(playerUUID)
+ .thenCompose(opt -> {
+ if (!opt.isPresent()) {
+ // User is not linked
+ return CompletableFuture.completedFuture(Component.text("Not linked"));
+ }
+
+ List requirements;
+ synchronized (compiledRequirements) {
+ requirements = compiledRequirements;
+ }
+
+ if (requirements.isEmpty()) {
+ // No additional requirements: let them through
+ return CompletableFuture.completedFuture(null);
+ }
+
+ CompletableFuture pass = new CompletableFuture<>();
+ List> all = new ArrayList<>();
+ long userId = opt.get();
+
+ for (CompiledRequirement requirement : requirements) {
+ CompletableFuture future = requirement.function().apply(playerUUID, userId);
+ all.add(future);
+
+ future.whenComplete((val, t) -> {
+ if (val != null && val) {
+ pass.complete(null);
+ }
+ }).exceptionally(t -> {
+ logger().debug("Check \"" + requirement.input() + "\" failed for " + playerUUID + " / " + Long.toUnsignedString(userId), t);
+ return null;
+ });
+ }
+
+ // Complete when at least one passes or all of them completed
+ return CompletableFuture.anyOf(pass, CompletableFutureUtil.combine(all))
+ .thenApply(v -> {
+ if (pass.isDone()) {
+ // One of the futures passed: let them through
+ return null;
+ }
+
+ // None of the futures passed: requirements not met
+ return Component.text("You did not pass requirements");
+ });
+ });
+ }
+}
diff --git a/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/DiscordBoostingRequirement.java b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/DiscordBoostingRequirement.java
new file mode 100644
index 00000000..5e8c3d90
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/DiscordBoostingRequirement.java
@@ -0,0 +1,50 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.linking.requirelinking.requirement;
+
+import com.discordsrv.api.discord.entity.guild.DiscordGuild;
+import com.discordsrv.common.DiscordSRV;
+
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public class DiscordBoostingRequirement extends LongRequirement {
+
+ private final DiscordSRV discordSRV;
+
+ public DiscordBoostingRequirement(DiscordSRV discordSRV) {
+ this.discordSRV = discordSRV;
+ }
+
+ @Override
+ public String name() {
+ return "DiscordBoosting";
+ }
+
+ @Override
+ public CompletableFuture isMet(Long value, UUID player, long userId) {
+ DiscordGuild guild = discordSRV.discordAPI().getGuildById(value);
+ if (guild == null) {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ return guild.retrieveMemberById(userId)
+ .thenApply(member -> member != null && member.isBoosting());
+ }
+}
diff --git a/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/DiscordRoleRequirement.java b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/DiscordRoleRequirement.java
new file mode 100644
index 00000000..091984d4
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/DiscordRoleRequirement.java
@@ -0,0 +1,51 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.linking.requirelinking.requirement;
+
+import com.discordsrv.api.discord.entity.guild.DiscordRole;
+import com.discordsrv.common.DiscordSRV;
+
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public class DiscordRoleRequirement extends LongRequirement {
+
+ private final DiscordSRV discordSRV;
+
+ public DiscordRoleRequirement(DiscordSRV discordSRV) {
+ this.discordSRV = discordSRV;
+ }
+
+ @Override
+ public String name() {
+ return "DiscordRole";
+ }
+
+ @Override
+ public CompletableFuture isMet(Long value, UUID player, long userId) {
+ DiscordRole role = discordSRV.discordAPI().getRoleById(value);
+ if (role == null) {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ return role.getGuild()
+ .retrieveMemberById(userId)
+ .thenApply(member -> member.getRoles().contains(role));
+ }
+}
diff --git a/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/DiscordServerRequirement.java b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/DiscordServerRequirement.java
new file mode 100644
index 00000000..f926245b
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/DiscordServerRequirement.java
@@ -0,0 +1,51 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.linking.requirelinking.requirement;
+
+import com.discordsrv.api.discord.entity.guild.DiscordGuild;
+import com.discordsrv.common.DiscordSRV;
+
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public class DiscordServerRequirement extends LongRequirement {
+
+ private final DiscordSRV discordSRV;
+
+ public DiscordServerRequirement(DiscordSRV discordSRV) {
+ this.discordSRV = discordSRV;
+ }
+
+ @Override
+ public String name() {
+ return "DiscordServer";
+ }
+
+ @Override
+ public CompletableFuture isMet(Long value, UUID player, long userId) {
+ DiscordGuild guild = discordSRV.discordAPI().getGuildById(value);
+ if (guild == null) {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ return guild.retrieveMemberById(userId)
+ .thenApply(Objects::nonNull);
+ }
+}
diff --git a/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/LongRequirement.java b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/LongRequirement.java
new file mode 100644
index 00000000..ad13067b
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/LongRequirement.java
@@ -0,0 +1,30 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.linking.requirelinking.requirement;
+
+public abstract class LongRequirement implements Requirement {
+
+ @Override
+ public Long parse(String input) {
+ try {
+ return Long.parseUnsignedLong(input);
+ } catch (NumberFormatException ignored) {}
+ return null;
+ }
+}
diff --git a/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/MinecraftAuthRequirement.java b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/MinecraftAuthRequirement.java
new file mode 100644
index 00000000..489f837e
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/MinecraftAuthRequirement.java
@@ -0,0 +1,194 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.linking.requirelinking.requirement;
+
+import com.discordsrv.common.DiscordSRV;
+import com.discordsrv.common.function.CheckedSupplier;
+import me.minecraftauth.lib.AuthService;
+import me.minecraftauth.lib.account.platform.twitch.SubTier;
+import me.minecraftauth.lib.exception.LookupException;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+public class MinecraftAuthRequirement implements Requirement> {
+
+ private static final Reference> NULL_VALUE = new Reference<>(null);
+
+ public static List> createRequirements(DiscordSRV discordSRV) {
+ List> requirements = new ArrayList<>();
+
+ // Patreon
+ requirements.add(new MinecraftAuthRequirement<>(
+ discordSRV,
+ "PatreonSubscriber",
+ AuthService::isSubscribedPatreon,
+ AuthService::isSubscribedPatreon
+ ));
+
+ // Glimpse
+ requirements.add(new MinecraftAuthRequirement<>(
+ discordSRV,
+ "GlimpseSubscriber",
+ AuthService::isSubscribedGlimpse,
+ AuthService::isSubscribedGlimpse
+ ));
+
+ // Twitch
+ requirements.add(new MinecraftAuthRequirement<>(
+ discordSRV,
+ "TwitchFollower",
+ AuthService::isFollowingTwitch
+ ));
+ requirements.add(new MinecraftAuthRequirement<>(
+ discordSRV,
+ "TwitchSubscriber",
+ AuthService::isSubscribedTwitch,
+ AuthService::isSubscribedTwitch,
+ string -> {
+ try {
+ int value = Integer.parseInt(string);
+ return SubTier.level(value);
+ } catch (NumberFormatException ignored) {
+ return null;
+ }
+ }
+ ));
+
+ // YouTube
+ requirements.add(new MinecraftAuthRequirement<>(
+ discordSRV,
+ "YouTubeSubscriber",
+ AuthService::isSubscribedYouTube
+ ));
+ requirements.add(new MinecraftAuthRequirement<>(
+ discordSRV,
+ "YouTubeMember",
+ AuthService::isMemberYouTube,
+ AuthService::isMemberYouTube
+ ));
+
+ return requirements;
+ }
+
+ private final DiscordSRV discordSRV;
+ private final String name;
+ private final Test test;
+ private final TestSpecific testSpecific;
+ private final Function parse;
+
+ public MinecraftAuthRequirement(
+ DiscordSRV discordSRV,
+ String name,
+ Test test
+ ) {
+ this(discordSRV, name, test, null, null);
+ }
+
+ @SuppressWarnings("unchecked")
+ public MinecraftAuthRequirement(
+ DiscordSRV discordSRV,
+ String name,
+ Test test,
+ TestSpecific testSpecific
+ ) {
+ this(discordSRV, name, test, (TestSpecific) testSpecific, t -> (T) t);
+ }
+
+ public MinecraftAuthRequirement(
+ DiscordSRV discordSRV,
+ String name,
+ Test test,
+ TestSpecific testSpecific,
+ Function parse
+ ) {
+ this.discordSRV = discordSRV;
+ this.name = name;
+ this.test = test;
+ this.testSpecific = testSpecific;
+ this.parse = parse;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Reference parse(String input) {
+ if (StringUtils.isEmpty(input)) {
+ return (Reference) NULL_VALUE;
+ } else if (parse != null) {
+ return new Reference<>(parse.apply(input));
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public CompletableFuture isMet(Reference atomicReference, UUID player, long userId) {
+ String token = discordSRV.connectionConfig().minecraftAuth.token;
+ T value = atomicReference.getValue();
+ if (value == null) {
+ return supply(() -> test.test(token, player));
+ } else {
+ return supply(() -> testSpecific.test(token, player, value));
+ }
+ }
+
+ private CompletableFuture supply(CheckedSupplier provider) {
+ CompletableFuture completableFuture = new CompletableFuture<>();
+ discordSRV.scheduler().run(() -> {
+ try {
+ completableFuture.complete(provider.get());
+ } catch (Throwable t) {
+ completableFuture.completeExceptionally(t);
+ }
+ });
+ return completableFuture;
+ }
+
+ @FunctionalInterface
+ public interface Test {
+ boolean test(String serverToken, UUID player) throws LookupException;
+ }
+
+ @FunctionalInterface
+ public interface TestSpecific {
+ boolean test(String serverToken, UUID uuid, T specific) throws LookupException;
+ }
+
+ public static class Reference {
+
+ private final T value;
+
+ public Reference(T value) {
+ this.value = value;
+ }
+
+ public T getValue() {
+ return value;
+ }
+ }
+}
diff --git a/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/Requirement.java b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/Requirement.java
new file mode 100644
index 00000000..66d7116b
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/Requirement.java
@@ -0,0 +1,32 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.linking.requirelinking.requirement;
+
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public interface Requirement {
+
+ String name();
+
+ T parse(String input);
+
+ CompletableFuture isMet(T value, UUID player, long userId);
+
+}
diff --git a/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/parser/RequirementParser.java b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/parser/RequirementParser.java
new file mode 100644
index 00000000..7ffc428e
--- /dev/null
+++ b/common/src/main/java/com/discordsrv/common/linking/requirelinking/requirement/parser/RequirementParser.java
@@ -0,0 +1,199 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.linking.requirelinking.requirement.parser;
+
+import com.discordsrv.common.future.util.CompletableFutureUtil;
+import com.discordsrv.common.linking.requirelinking.requirement.Requirement;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+public class RequirementParser {
+
+ private static RequirementParser INSTANCE;
+
+ public static RequirementParser getInstance() {
+ return INSTANCE != null ? INSTANCE : (INSTANCE = new RequirementParser());
+ }
+
+ private RequirementParser() {}
+
+ @SuppressWarnings("unchecked")
+ public BiFunction> parse(String input, List> requirements) {
+ List> reqs = new ArrayList<>(requirements.size());
+ requirements.forEach(r -> reqs.add((Requirement) r));
+
+ Func func = parse(input, new AtomicInteger(0), reqs);
+ return func::test;
+ }
+
+ private Func parse(String input, AtomicInteger iterator, List> requirements) {
+ StringBuilder functionNameBuffer = new StringBuilder();
+ StringBuilder functionValueBuffer = new StringBuilder();
+ boolean isFunctionValue = false;
+
+ Func func = null;
+ Operator operator = null;
+ boolean operatorSecond = false;
+
+ Function error = text -> {
+ int i = iterator.get();
+ return new IllegalArgumentException(text + "\n" + input + "\n" + StringUtils.leftPad("^", i));
+ };
+
+ char[] chars = input.toCharArray();
+ int i;
+ for (; (i = iterator.get()) < chars.length; iterator.incrementAndGet()) {
+ char c = chars[i];
+ if (c == '(' && functionNameBuffer.length() == 0) {
+ iterator.incrementAndGet();
+ Func function = parse(input, iterator, requirements);
+ if (function == null) {
+ throw error.apply("Empty brackets");
+ }
+
+ if (func != null) {
+ if (operator == null) {
+ throw error.apply("No operator");
+ }
+
+ func = operator.function.apply(func, function);
+ operator = null;
+ } else {
+ func = function;
+ }
+ continue;
+ }
+
+ if (c == ')' && functionNameBuffer.length() == 0) {
+ return func;
+ }
+ if (c == '(' && functionNameBuffer.length() > 0) {
+ if (isFunctionValue) {
+ throw error.apply("Opening bracket inside function value");
+ }
+
+ isFunctionValue = true;
+ continue;
+ }
+ if (c == ')' && functionNameBuffer.length() > 0) {
+ String functionName = functionNameBuffer.toString();
+ String value = functionValueBuffer.toString();
+
+ for (Requirement requirement : requirements) {
+ if (requirement.name().equalsIgnoreCase(functionName)) {
+ T requirementValue = requirement.parse(value);
+ if (requirementValue == null) {
+ throw error.apply("Unacceptable function value for " + functionName);
+ }
+
+ Func function = (player, user) -> requirement.isMet(requirementValue, player, user);
+ if (func != null) {
+ if (operator == null) {
+ throw error.apply("No operator");
+ }
+
+ func = operator.function.apply(func, function);
+ operator = null;
+ } else {
+ func = function;
+ }
+
+ functionNameBuffer.setLength(0);
+ functionValueBuffer.setLength(0);
+ isFunctionValue = false;
+ break;
+ }
+ }
+ if (functionNameBuffer.length() != 0) {
+ throw error.apply("Unknown function: " + functionName);
+ }
+ continue;
+ }
+
+ if (operator != null && !operatorSecond && c == operator.character) {
+ operatorSecond = true;
+ continue;
+ } else if (operator == null && functionNameBuffer.length() == 0) {
+ boolean found = false;
+ for (Operator value : Operator.values()) {
+ if (value.character == c) {
+ if (func == null) {
+ throw error.apply("No condition");
+ }
+ operator = value;
+ operatorSecond = false;
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ continue;
+ }
+ }
+
+ if (operator != null && !operatorSecond) {
+ throw error.apply("Operators must be exactly two of the same character");
+ }
+
+ if (!Character.isSpaceChar(c)) {
+ if (isFunctionValue) {
+ functionValueBuffer.append(c);
+ } else {
+ functionNameBuffer.append(c);
+ }
+ }
+ }
+
+ if (operator != null) {
+ throw error.apply("Dangling operator");
+ }
+ return func;
+ }
+
+ @FunctionalInterface
+ private interface Func {
+ CompletableFuture test(UUID player, long user);
+ }
+
+ private enum Operator {
+
+ AND('&', (one, two) -> apply(one, two, (o, t) -> o && t)),
+ OR('|', (one, two) -> apply(one, two, (o, t) -> o || t));
+
+ private final char character;
+ private final BiFunction function;
+
+ Operator(char character, BiFunction function) {
+ this.character = character;
+ this.function = function;
+ }
+
+ private static Func apply(Func one, Func two, BiFunction function) {
+ return (player, user) -> CompletableFutureUtil.combine(one.test(player, user), two.test(player, user))
+ .thenApply(bools -> function.apply(bools.get(0), bools.get(1)));
+ }
+ }
+}
diff --git a/common/src/main/java/com/discordsrv/common/storage/Storage.java b/common/src/main/java/com/discordsrv/common/storage/Storage.java
index 988a1714..8b7adac2 100644
--- a/common/src/main/java/com/discordsrv/common/storage/Storage.java
+++ b/common/src/main/java/com/discordsrv/common/storage/Storage.java
@@ -37,6 +37,7 @@ public interface Storage {
UUID getPlayerUUID(long userId);
void createLink(@NotNull UUID player, long userId);
+ void removeLink(@NotNull UUID player, long userId);
int getLinkedAccountCount();
diff --git a/common/src/main/java/com/discordsrv/common/storage/impl/sql/SQLStorage.java b/common/src/main/java/com/discordsrv/common/storage/impl/sql/SQLStorage.java
index fb959b68..3c1eee7d 100644
--- a/common/src/main/java/com/discordsrv/common/storage/impl/sql/SQLStorage.java
+++ b/common/src/main/java/com/discordsrv/common/storage/impl/sql/SQLStorage.java
@@ -22,7 +22,6 @@ import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.exception.StorageException;
import com.discordsrv.common.function.CheckedConsumer;
import com.discordsrv.common.function.CheckedFunction;
-import com.discordsrv.common.linking.impl.StorageLinker;
import com.discordsrv.common.storage.Storage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -40,7 +39,7 @@ public abstract class SQLStorage implements Storage {
public abstract Connection getConnection();
public abstract boolean isAutoCloseConnections();
- public abstract void createTables(Connection connection, String tablePrefix, boolean linkedAccounts) throws SQLException;
+ public abstract void createTables(Connection connection, String tablePrefix) throws SQLException;
private void useConnection(CheckedConsumer connectionConsumer) throws StorageException {
useConnection(connection -> {
@@ -77,8 +76,7 @@ public abstract class SQLStorage implements Storage {
public void initialize() {
useConnection((CheckedConsumer) connection -> createTables(
connection,
- discordSRV.connectionConfig().storage.sqlTablePrefix,
- discordSRV.linkProvider() instanceof StorageLinker
+ discordSRV.connectionConfig().storage.sqlTablePrefix
));
}
@@ -128,6 +126,16 @@ public abstract class SQLStorage implements Storage {
});
}
+ @Override
+ public void removeLink(@NotNull UUID player, long userId) {
+ useConnection(connection -> {
+ try (PreparedStatement statement = connection.prepareStatement("delete " + tablePrefix() + "LINKED_ACCOUNTS where PLAYER_UUID = ?;")) {
+ statement.setString(1, player.toString());
+ exceptEffectedRows(statement.executeUpdate(), 1);
+ }
+ });
+ }
+
@Override
public int getLinkedAccountCount() {
return useConnection(connection -> {
diff --git a/common/src/main/java/com/discordsrv/common/storage/impl/sql/file/H2Storage.java b/common/src/main/java/com/discordsrv/common/storage/impl/sql/file/H2Storage.java
index 671f7eb2..7faf0407 100644
--- a/common/src/main/java/com/discordsrv/common/storage/impl/sql/file/H2Storage.java
+++ b/common/src/main/java/com/discordsrv/common/storage/impl/sql/file/H2Storage.java
@@ -99,17 +99,15 @@ public class H2Storage extends SQLStorage {
}
@Override
- public void createTables(Connection connection, String tablePrefix, boolean linkedAccounts) throws SQLException {
- if (linkedAccounts) {
- try (Statement statement = connection.createStatement()) {
- statement.execute(
- "create table if not exists " + tablePrefix + "linked_accounts "
- + "(ID int not null auto_increment, "
- + "PLAYER_UUID varchar(36), "
- + "USER_ID bigint, "
- + "constraint LINKED_ACCOUNTS_PK primary key (ID)"
- + ")");
- }
+ public void createTables(Connection connection, String tablePrefix) throws SQLException {
+ try (Statement statement = connection.createStatement()) {
+ statement.execute(
+ "create table if not exists " + tablePrefix + "linked_accounts "
+ + "(ID int not null auto_increment, "
+ + "PLAYER_UUID varchar(36), "
+ + "USER_ID bigint, "
+ + "constraint LINKED_ACCOUNTS_PK primary key (ID)"
+ + ")");
}
}
}
diff --git a/common/src/main/java/com/discordsrv/common/storage/impl/sql/hikari/MySQLStorage.java b/common/src/main/java/com/discordsrv/common/storage/impl/sql/hikari/MySQLStorage.java
index 4a29f470..d9ba9bb2 100644
--- a/common/src/main/java/com/discordsrv/common/storage/impl/sql/hikari/MySQLStorage.java
+++ b/common/src/main/java/com/discordsrv/common/storage/impl/sql/hikari/MySQLStorage.java
@@ -51,17 +51,15 @@ public class MySQLStorage extends HikariStorage {
}
@Override
- public void createTables(Connection connection, String tablePrefix, boolean linkedAccounts) throws SQLException {
- if (linkedAccounts) {
- try (Statement statement = connection.createStatement()) {
- statement.execute(
- "create table if not exists " + tablePrefix + "linked_accounts "
- + "(ID int not null auto_increment, "
- + "PLAYER_UUID varchar(36), "
- + "USER_ID bigint, "
- + "constraint LINKED_ACCOUNTS_PK primary key (ID)"
- + ")");
- }
+ public void createTables(Connection connection, String tablePrefix) throws SQLException {
+ try (Statement statement = connection.createStatement()) {
+ statement.execute(
+ "create table if not exists " + tablePrefix + "linked_accounts "
+ + "(ID int not null auto_increment, "
+ + "PLAYER_UUID varchar(36), "
+ + "USER_ID bigint, "
+ + "constraint LINKED_ACCOUNTS_PK primary key (ID)"
+ + ")");
}
}
diff --git a/common/src/test/java/com/discordsrv/common/linking/requirement/parser/RequirementParserTest.java b/common/src/test/java/com/discordsrv/common/linking/requirement/parser/RequirementParserTest.java
new file mode 100644
index 00000000..561ecaa0
--- /dev/null
+++ b/common/src/test/java/com/discordsrv/common/linking/requirement/parser/RequirementParserTest.java
@@ -0,0 +1,145 @@
+/*
+ * This file is part of DiscordSRV, licensed under the GPLv3 License
+ * Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.discordsrv.common.linking.requirement.parser;
+
+import com.discordsrv.common.linking.requirelinking.requirement.Requirement;
+import com.discordsrv.common.linking.requirelinking.requirement.parser.RequirementParser;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class RequirementParserTest {
+
+ private final RequirementParser requirementParser = RequirementParser.getInstance();
+ private final List> requirements = Arrays.asList(
+ new Requirement() {
+ @Override
+ public String name() {
+ return "F";
+ }
+
+ @Override
+ public Boolean parse(String input) {
+ return Boolean.parseBoolean(input);
+ }
+
+ @Override
+ public CompletableFuture isMet(Boolean value, UUID player, long userId) {
+ return CompletableFuture.completedFuture(value);
+ }
+ },
+ new Requirement