Linking improvements, required linking initial

This commit is contained in:
Vankka 2023-03-18 14:23:31 +02:00
parent 6ff5ddb6a7
commit 42ec392bcb
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
45 changed files with 1557 additions and 77 deletions

View File

@ -34,6 +34,8 @@ public class Color {
* Discord's blurple color (<a href="https://discord.com/branding">Discord branding</a>).
*/
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;

View File

@ -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<Guild>, 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<DiscordGuildMember> 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

View File

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

View File

@ -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<DiscordSRVBukkitBootstrap
// Modules
registerModule(MinecraftToDiscordChatModule::new);
registerModule(BukkitRequiredLinkingModule::new);
// Integrations
registerIntegration("com.discordsrv.bukkit.integration.VaultIntegration");

View File

@ -35,6 +35,8 @@ public class BukkitConfig extends MainConfig {
channels.put(ChannelConfig.DEFAULT_KEY, new ServerBaseChannelConfig());
}
public BukkitRequiredLinkingConfig requiredLinking = new BukkitRequiredLinkingConfig();
public PluginIntegrationConfig integrations = new PluginIntegrationConfig();
@Override

View File

@ -0,0 +1,41 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.bukkit.integration.chat;
import com.discordsrv.api.channel.GameChannel;

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<BukkitDiscordSRV> 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 <T extends Event> void register(Class<T> eventType, BiConsumer<T, EventPriority> 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<String> alreadyBlocked,
Consumer<String> 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);
}
}

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.channel;
import com.discordsrv.api.channel.GameChannel;

View File

@ -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<GroupSyncResult, AtomicInteger> resultCounts = new EnumMap<>(GroupSyncResult.class);
int total = 0;
for (Set<GroupSyncResult> result : results) {
for (List<GroupSyncResult> 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<CompletableFuture<Set<GroupSyncResult>>> resyncOnlinePlayers(GroupSyncModule module) {
List<CompletableFuture<Set<GroupSyncResult>>> futures = new ArrayList<>();
private List<CompletableFuture<List<GroupSyncResult>>> resyncOnlinePlayers(GroupSyncModule module) {
List<CompletableFuture<List<GroupSyncResult>>> futures = new ArrayList<>();
for (IPlayer player : discordSRV.playerProvider().allPlayers()) {
futures.add(module.resync(player.uniqueId(), GroupSyncCause.COMMAND));
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, TargetRequirementConfig> serverRequirements = new HashMap<String, TargetRequirementConfig>() {{
put("example", new TargetRequirementConfig());
}};
@ConfigSerializable
public static class TargetRequirementConfig extends RequirementsConfig {}
}

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> 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<String> requirements = new ArrayList<>();
}

View File

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

View File

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

View File

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

View File

@ -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<ReceivedDiscordMessage> messages;
public ReceivedDiscordMessageClusterImpl(Set<ReceivedDiscordMessage> messages) {
this.messages = messages;
public ReceivedDiscordMessageClusterImpl(Collection<ReceivedDiscordMessage> messages) {
this.messages = new HashSet<>(messages);
}
@Override

View File

@ -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 <T> CompletableFuture<Set<T>> combine(Collection<CompletableFuture<T>> futures) {
public static <T> CompletableFuture<List<T>> combine(Collection<CompletableFuture<T>> futures) {
return combine(futures.toArray(new CompletableFuture[0]));
}
public static <T> CompletableFuture<Set<T>> combine(CompletableFuture<T>[] futures) {
CompletableFuture<Set<T>> future = new CompletableFuture<>();
@SafeVarargs
public static <T> CompletableFuture<List<T>> combine(CompletableFuture<T>... futures) {
CompletableFuture<List<T>> future = new CompletableFuture<>();
CompletableFuture.allOf(futures).whenComplete((v, t) -> {
if (t != null) {
future.completeExceptionally(t);
return;
}
Set<T> results = new HashSet<>();
List<T> results = new ArrayList<>();
for (CompletableFuture<T> aFuture : futures) {
results.add(aFuture.join());
}

View File

@ -255,20 +255,20 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
// Resync user
public CompletableFuture<Set<GroupSyncResult>> resync(UUID player, GroupSyncCause cause) {
public CompletableFuture<List<GroupSyncResult>> 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<Set<GroupSyncResult>> resync(long userId, GroupSyncCause cause) {
public CompletableFuture<List<GroupSyncResult>> 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));

View File

@ -26,6 +26,7 @@ import java.util.concurrent.CompletableFuture;
public interface LinkStore extends LinkProvider {
CompletableFuture<Void> createLink(@NotNull UUID playerUUID, long userId);
CompletableFuture<Void> removeLink(@NotNull UUID playerUUID, long userId);
CompletableFuture<Integer> getLinkedAccountCount();
}

View File

@ -48,6 +48,12 @@ public class MemoryLinker implements LinkProvider, LinkStore {
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<Void> removeLink(@NotNull UUID playerUUID, long userId) {
map.remove(playerUUID);
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<Integer> getLinkedAccountCount() {
return CompletableFuture.completedFuture(map.size());

View File

@ -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<Optional<Long>> 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<Optional<UUID>> 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 <T> CompletableFuture<Optional<T>> query(
CheckedSupplier<Optional<T>> authSupplier,
Supplier<T> storageSupplier,
Consumer<T> linked,
Consumer<T> unlinked
) {
CompletableFuture<Optional<T>> authService = new CompletableFuture<>();
discordSRV.scheduler().run(() -> {
try {
authService.complete(authSupplier.get());
} catch (Throwable t) {
authService.completeExceptionally(t);
}
});
CompletableFuture<Optional<T>> storageResult = CompletableFuture.supplyAsync(
() -> Optional.ofNullable(storageSupplier.get()),
discordSRV.scheduler().executor()
);
return CompletableFutureUtil.combine(authService, storageResult).thenApply(results -> {
Optional<T> auth = authService.join();
Optional<T> 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;
});
}
}

View File

@ -57,6 +57,14 @@ public class StorageLinker extends CachedLinkProvider implements LinkProvider, L
);
}
@Override
public CompletableFuture<Void> removeLink(@NotNull UUID playerUUID, long userId) {
return CompletableFuture.runAsync(
() -> discordSRV.storage().removeLink(playerUUID, userId),
discordSRV.scheduler().executor()
);
}
@Override
public CompletableFuture<Integer> getLinkedAccountCount() {
return CompletableFuture.supplyAsync(

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T extends DiscordSRV> extends AbstractModule<T> {
private final List<Requirement<?>> availableRequirements = new ArrayList<>();
public RequiredLinkingModule(T discordSRV) {
super(discordSRV);
}
@Override
public void reloadNoResult() {
List<Requirement<?>> 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<CompiledRequirement> compile(List<String> requirements) {
List<CompiledRequirement> checks = new ArrayList<>();
for (String requirement : requirements) {
BiFunction<UUID, Long, CompletableFuture<Boolean>> 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<UUID, Long, CompletableFuture<Boolean>> function;
protected CompiledRequirement(String input, BiFunction<UUID, Long, CompletableFuture<Boolean>> function) {
this.input = input;
this.function = function;
}
public String input() {
return input;
}
public BiFunction<UUID, Long, CompletableFuture<Boolean>> function() {
return function;
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T extends DiscordSRV> extends RequiredLinkingModule<T> {
private final List<CompiledRequirement> 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<Component> 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<CompiledRequirement> requirements;
synchronized (compiledRequirements) {
requirements = compiledRequirements;
}
if (requirements.isEmpty()) {
// No additional requirements: let them through
return CompletableFuture.completedFuture(null);
}
CompletableFuture<Void> pass = new CompletableFuture<>();
List<CompletableFuture<Boolean>> all = new ArrayList<>();
long userId = opt.get();
for (CompiledRequirement requirement : requirements) {
CompletableFuture<Boolean> 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");
});
});
}
}

View File

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

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.linking.requirelinking.requirement;
public abstract class LongRequirement implements Requirement<Long> {
@Override
public Long parse(String input) {
try {
return Long.parseUnsignedLong(input);
} catch (NumberFormatException ignored) {}
return null;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T> implements Requirement<MinecraftAuthRequirement.Reference<T>> {
private static final Reference<?> NULL_VALUE = new Reference<>(null);
public static List<Requirement<?>> createRequirements(DiscordSRV discordSRV) {
List<Requirement<?>> 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<T> testSpecific;
private final Function<String, T> 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<String> testSpecific
) {
this(discordSRV, name, test, (TestSpecific<T>) testSpecific, t -> (T) t);
}
public MinecraftAuthRequirement(
DiscordSRV discordSRV,
String name,
Test test,
TestSpecific<T> testSpecific,
Function<String, T> 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<T> parse(String input) {
if (StringUtils.isEmpty(input)) {
return (Reference<T>) NULL_VALUE;
} else if (parse != null) {
return new Reference<>(parse.apply(input));
} else {
return null;
}
}
@Override
public CompletableFuture<Boolean> isMet(Reference<T> 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<Boolean> supply(CheckedSupplier<Boolean> provider) {
CompletableFuture<Boolean> 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<T> {
boolean test(String serverToken, UUID uuid, T specific) throws LookupException;
}
public static class Reference<T> {
private final T value;
public Reference(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.linking.requirelinking.requirement;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public interface Requirement<T> {
String name();
T parse(String input);
CompletableFuture<Boolean> isMet(T value, UUID player, long userId);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 <T> BiFunction<UUID, Long, CompletableFuture<Boolean>> parse(String input, List<Requirement<?>> requirements) {
List<Requirement<T>> reqs = new ArrayList<>(requirements.size());
requirements.forEach(r -> reqs.add((Requirement<T>) r));
Func func = parse(input, new AtomicInteger(0), reqs);
return func::test;
}
private <T> Func parse(String input, AtomicInteger iterator, List<Requirement<T>> requirements) {
StringBuilder functionNameBuffer = new StringBuilder();
StringBuilder functionValueBuffer = new StringBuilder();
boolean isFunctionValue = false;
Func func = null;
Operator operator = null;
boolean operatorSecond = false;
Function<String, RuntimeException> 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<T> 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<Boolean> 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<Func, Func, Func> function;
Operator(char character, BiFunction<Func, Func, Func> function) {
this.character = character;
this.function = function;
}
private static Func apply(Func one, Func two, BiFunction<Boolean, Boolean, Boolean> function) {
return (player, user) -> CompletableFutureUtil.combine(one.test(player, user), two.test(player, user))
.thenApply(bools -> function.apply(bools.get(0), bools.get(1)));
}
}
}

View File

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

View File

@ -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<Connection> connectionConsumer) throws StorageException {
useConnection(connection -> {
@ -77,8 +76,7 @@ public abstract class SQLStorage implements Storage {
public void initialize() {
useConnection((CheckedConsumer<Connection>) 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 -> {

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Requirement<?>> requirements = Arrays.asList(
new Requirement<Boolean>() {
@Override
public String name() {
return "F";
}
@Override
public Boolean parse(String input) {
return Boolean.parseBoolean(input);
}
@Override
public CompletableFuture<Boolean> isMet(Boolean value, UUID player, long userId) {
return CompletableFuture.completedFuture(value);
}
},
new Requirement<Object>() {
@Override
public String name() {
return "AlwaysError";
}
@Override
public Object parse(String input) {
return null;
}
@Override
public CompletableFuture<Boolean> 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()"));
}
}

View File

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