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() { + @Override + public String name() { + return "AlwaysError"; + } + + @Override + public Object parse(String input) { + return null; + } + + @Override + public CompletableFuture isMet(Object value, UUID player, long userId) { + return null; + } + } + ); + + private boolean parse(String input) { + return requirementParser.parse(input, requirements).apply(null, 0L).join(); + } + + @Test + public void differentCase() { + assertFalse(parse("f(false) || F(false)")); + } + + @Test + public void orFail() { + assertFalse(parse("F(false) || F(false)")); + } + + @Test + public void orPass() { + assertTrue(parse("F(true) || F(false)")); + } + + @Test + public void andFail() { + assertFalse(parse("F(true) && F(false)")); + } + + @Test + public void andPass() { + assertTrue(parse("F(true) && F(true)")); + } + + @Test + public void complexFail() { + assertFalse(parse("F(true) && (F(false) && F(true))")); + } + + @Test + public void complexPass() { + assertTrue(parse("F(true) && (F(false) || F(true))")); + } + + private void assertExceptionMessageStartsWith(String exceptionMessage, Executable executable) { + try { + executable.execute(); + } catch (Throwable e) { + if (!e.getMessage().startsWith(exceptionMessage)) { + fail("Exception message did not start with: " + exceptionMessage + " Actually: " + e.getMessage()); + } + } + } + + @Test + public void noConditionError() { + assertExceptionMessageStartsWith("No condition", () -> parse("&&")); + } + + @Test + public void operatorLengthError() { + assertExceptionMessageStartsWith("Operators must be exactly two of the same character", () -> parse("F(true) & F(true)")); + } + + @Test + public void danglingOperatorError() { + assertExceptionMessageStartsWith("Dangling operator", () -> parse("F(true) &&")); + } + + @Test + public void emptyBracketsError() { + assertExceptionMessageStartsWith("Empty brackets", () -> parse("()")); + } + + @Test + public void unacceptableValueError() { + assertExceptionMessageStartsWith("Unacceptable function value for", () -> parse("AlwaysError()")); + } + +} diff --git a/settings.gradle b/settings.gradle index 0dbb0f22..a4423168 100644 --- a/settings.gradle +++ b/settings.gradle @@ -91,7 +91,7 @@ dependencyResolutionManagement { library('mysql', 'mysql', 'mysql-connector-java').version('8.0.28') // MinecraftAuth lib - library('minecraftauth-lib', 'me.minecraftauth', 'lib').version('1.0.1') + library('minecraftauth-lib', 'me.minecraftauth', 'lib').version('1.1.0') // Brigadier & Commodore library('brigadier', 'com.mojang', 'brigadier').version('1.0.18')