diff --git a/common/src/main/java/com/discordsrv/common/command/combined/abstraction/CombinedCommand.java b/common/src/main/java/com/discordsrv/common/command/combined/abstraction/CombinedCommand.java index 7eaa3e06..c0779646 100644 --- a/common/src/main/java/com/discordsrv/common/command/combined/abstraction/CombinedCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/combined/abstraction/CombinedCommand.java @@ -1,14 +1,26 @@ package com.discordsrv.common.command.combined.abstraction; +import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand; import com.discordsrv.api.discord.events.interaction.command.DiscordChatInputInteractionEvent; +import com.discordsrv.api.discord.events.interaction.command.DiscordCommandAutoCompleteInteractionEvent; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.command.game.abstraction.GameCommandArguments; import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; +import com.discordsrv.common.command.game.abstraction.GameCommandSuggester; import com.discordsrv.common.command.game.sender.ICommandSender; +import org.jetbrains.annotations.Nullable; +import java.util.Collections; +import java.util.List; import java.util.function.Consumer; -public abstract class CombinedCommand implements GameCommandExecutor, Consumer { +public abstract class CombinedCommand + implements + GameCommandExecutor, + GameCommandSuggester, + Consumer, + DiscordCommand.AutoCompleteHandler +{ protected final DiscordSRV discordSRV; @@ -18,7 +30,7 @@ public abstract class CombinedCommand implements GameCommandExecutor, Consumer suggestValues(ICommandSender sender, GameCommandArguments previousArguments, String currentInput) { + return suggest(new GameCommandExecution(discordSRV, sender, previousArguments, null), currentInput); + } + + @Override + public void autoComplete(DiscordCommandAutoCompleteInteractionEvent event) { + List suggestions = suggest(new DiscordCommandExecution(discordSRV, event), null); + suggestions.forEach(suggestion -> event.addChoice(suggestion, suggestion)); + } + + public List suggest(CommandExecution execution, @Nullable String input) { + return Collections.emptyList(); + } } diff --git a/common/src/main/java/com/discordsrv/common/command/combined/abstraction/CommandExecution.java b/common/src/main/java/com/discordsrv/common/command/combined/abstraction/CommandExecution.java index c79434f5..1d119b32 100644 --- a/common/src/main/java/com/discordsrv/common/command/combined/abstraction/CommandExecution.java +++ b/common/src/main/java/com/discordsrv/common/command/combined/abstraction/CommandExecution.java @@ -3,9 +3,12 @@ package com.discordsrv.common.command.combined.abstraction; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Locale; public interface CommandExecution { + Locale locale(); + void setEphemeral(boolean ephemeral); String getArgument(String label); diff --git a/common/src/main/java/com/discordsrv/common/command/combined/abstraction/DiscordCommandExecution.java b/common/src/main/java/com/discordsrv/common/command/combined/abstraction/DiscordCommandExecution.java index fa18f455..f5ca0f49 100644 --- a/common/src/main/java/com/discordsrv/common/command/combined/abstraction/DiscordCommandExecution.java +++ b/common/src/main/java/com/discordsrv/common/command/combined/abstraction/DiscordCommandExecution.java @@ -1,26 +1,50 @@ package com.discordsrv.common.command.combined.abstraction; import com.discordsrv.api.discord.events.interaction.command.DiscordChatInputInteractionEvent; +import com.discordsrv.api.discord.events.interaction.command.DiscordCommandAutoCompleteInteractionEvent; import com.discordsrv.common.DiscordSRV; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.interaction.GenericInteractionCreateEvent; import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; +import net.dv8tion.jda.api.interactions.commands.CommandInteractionPayload; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import org.apache.commons.lang3.StringUtils; import java.util.Collection; import java.util.EnumMap; +import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; public class DiscordCommandExecution implements CommandExecution { private final DiscordSRV discordSRV; - private final DiscordChatInputInteractionEvent event; + + private final GenericInteractionCreateEvent createEvent; + private final CommandInteractionPayload interactionPayload; + private final IReplyCallback replyCallback; + private final AtomicBoolean isEphemeral = new AtomicBoolean(true); private final AtomicReference hook = new AtomicReference<>(); public DiscordCommandExecution(DiscordSRV discordSRV, DiscordChatInputInteractionEvent event) { this.discordSRV = discordSRV; - this.event = event; + this.createEvent = event.asJDA(); + this.interactionPayload = event.asJDA(); + this.replyCallback = event.asJDA(); + } + + public DiscordCommandExecution(DiscordSRV discordSRV, DiscordCommandAutoCompleteInteractionEvent event) { + this.discordSRV = discordSRV; + this.createEvent = event.asJDA(); + this.interactionPayload = event.asJDA(); + this.replyCallback = null; + } + + @Override + public Locale locale() { + return createEvent.getUserLocale().toLocale(); } @Override @@ -30,12 +54,16 @@ public class DiscordCommandExecution implements CommandExecution { @Override public String getArgument(String label) { - OptionMapping mapping = event.asJDA().getOption(label); + OptionMapping mapping = interactionPayload.getOption(label); return mapping != null ? mapping.getAsString() : null; } @Override public void send(Collection texts, Collection extra) { + if (replyCallback == null) { + throw new IllegalStateException("May not be used on auto completions"); + } + StringBuilder builder = new StringBuilder(); EnumMap formats = new EnumMap<>(Text.Formatting.class); @@ -57,7 +85,7 @@ public class DiscordCommandExecution implements CommandExecution { if (interactionHook != null) { interactionHook.sendMessage(builder.toString()).setEphemeral(ephemeral).queue(); } else { - event.asJDA().reply(builder.toString()).setEphemeral(ephemeral).queue(); + replyCallback.reply(builder.toString()).setEphemeral(ephemeral).queue(); } } @@ -83,9 +111,13 @@ public class DiscordCommandExecution implements CommandExecution { @Override public void runAsync(Runnable runnable) { - event.asJDA().deferReply(isEphemeral.get()).queue(ih -> { + replyCallback.deferReply(isEphemeral.get()).queue(ih -> { hook.set(ih); discordSRV.scheduler().run(runnable); }); } + + public User getUser() { + return createEvent.getUser(); + } } diff --git a/common/src/main/java/com/discordsrv/common/command/combined/abstraction/GameCommandExecution.java b/common/src/main/java/com/discordsrv/common/command/combined/abstraction/GameCommandExecution.java index a4594a80..faa05fcc 100644 --- a/common/src/main/java/com/discordsrv/common/command/combined/abstraction/GameCommandExecution.java +++ b/common/src/main/java/com/discordsrv/common/command/combined/abstraction/GameCommandExecution.java @@ -3,6 +3,7 @@ package com.discordsrv.common.command.combined.abstraction; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.command.game.abstraction.GameCommandArguments; import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.common.player.IPlayer; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextReplacementConfig; @@ -10,6 +11,7 @@ import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import java.util.Collection; +import java.util.Locale; import java.util.regex.Pattern; public class GameCommandExecution implements CommandExecution { @@ -25,11 +27,18 @@ public class GameCommandExecution implements CommandExecution { private final DiscordSRV discordSRV; private final ICommandSender sender; private final GameCommandArguments arguments; + private final String label; - public GameCommandExecution(DiscordSRV discordSRV, ICommandSender sender, GameCommandArguments arguments) { + public GameCommandExecution(DiscordSRV discordSRV, ICommandSender sender, GameCommandArguments arguments, String label) { this.discordSRV = discordSRV; this.sender = sender; this.arguments = arguments; + this.label = label; + } + + @Override + public Locale locale() { + return sender instanceof IPlayer ? ((IPlayer) sender).locale() : null; } @Override @@ -67,4 +76,12 @@ public class GameCommandExecution implements CommandExecution { public void runAsync(Runnable runnable) { discordSRV.scheduler().run(runnable); } + + public ICommandSender getSender() { + return sender; + } + + public String getLabel() { + return label; + } } diff --git a/common/src/main/java/com/discordsrv/common/command/combined/commands/DebugCommand.java b/common/src/main/java/com/discordsrv/common/command/combined/commands/DebugCommand.java index 5a2921ee..23cd08c0 100644 --- a/common/src/main/java/com/discordsrv/common/command/combined/commands/DebugCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/combined/commands/DebugCommand.java @@ -31,6 +31,7 @@ import com.discordsrv.common.paste.Paste; import com.discordsrv.common.paste.PasteService; import com.discordsrv.common.paste.service.AESEncryptedPasteService; import com.discordsrv.common.paste.service.BytebinPasteService; +import com.discordsrv.common.permission.util.Permission; import net.kyori.adventure.text.format.NamedTextColor; import java.nio.charset.StandardCharsets; @@ -53,7 +54,7 @@ public class DebugCommand extends CombinedCommand { if (GAME == null) { DebugCommand command = getInstance(discordSRV); GAME = GameCommand.literal("debug") - .requiredPermission("discordsrv.admin.debug") + .requiredPermission(Permission.COMMAND_DEBUG) .executor(command) .then( GameCommand.stringWord("format") diff --git a/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkInitCommand.java b/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkInitCommand.java new file mode 100644 index 00000000..aed1ecd5 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkInitCommand.java @@ -0,0 +1,176 @@ +package com.discordsrv.common.command.combined.commands; + +import com.discordsrv.api.discord.entity.interaction.command.CommandOption; +import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand; +import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.command.combined.abstraction.CombinedCommand; +import com.discordsrv.common.command.combined.abstraction.CommandExecution; +import com.discordsrv.common.command.combined.abstraction.GameCommandExecution; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.common.command.util.CommandUtil; +import com.discordsrv.common.component.util.ComponentUtil; +import com.discordsrv.common.linking.LinkProvider; +import com.discordsrv.common.linking.LinkStore; +import com.discordsrv.common.permission.util.Permission; +import com.discordsrv.common.player.IPlayer; +import com.github.benmanes.caffeine.cache.Cache; +import org.apache.commons.lang3.StringUtils; + +import java.util.UUID; + +public class LinkInitCommand extends CombinedCommand { + + private static DebugCommand INSTANCE; + private static GameCommand GAME; + private static DiscordCommand DISCORD; + + private static DebugCommand getInstance(DiscordSRV discordSRV) { + return INSTANCE != null ? INSTANCE : (INSTANCE = new DebugCommand(discordSRV)); + } + + public static GameCommand getGame(DiscordSRV discordSRV) { + if (GAME == null) { + LinkInitCommand command = new LinkInitCommand(discordSRV); + GAME = GameCommand.literal("link") + .then( + GameCommand.stringWord("player") + .then( + GameCommand.stringWord("user") + .requiredPermission(Permission.COMMAND_LINK_OTHER) + .executor(command) + ) + ) + .requiredPermission(Permission.COMMAND_LINK) + .executor(command); + } + + return GAME; + } + + public static DiscordCommand getDiscord(DiscordSRV discordSRV) { + if (DISCORD == null) { + DebugCommand command = getInstance(discordSRV); + DISCORD = DiscordCommand.chatInput(ComponentIdentifier.of("DiscordSRV", "link"), "link", "Link players") + .addOption( + CommandOption.builder(CommandOption.Type.STRING, "player", "The player to link") + .setRequired(true) + .build() + ) + .addOption( + CommandOption.builder(CommandOption.Type.USER, "user", "The user to link") + .setRequired(true) + .build() + ) + .setAutoCompleteHandler(command) + .setEventHandler(command) + .build(); + } + + return DISCORD; + } + + private final DiscordSRV discordSRV; + private final Cache linkCheckRateLimit; + + public LinkInitCommand(DiscordSRV discordSRV) { + super(discordSRV); + this.discordSRV = discordSRV; + this.linkCheckRateLimit = discordSRV.caffeineBuilder() + .expireAfterWrite(LinkStore.LINKING_CODE_RATE_LIMIT) + .build(); + } + + @Override + public void execute(CommandExecution execution) { + String playerArgument = execution.getArgument("player"); + String userArgument = execution.getArgument("user"); + if (execution instanceof GameCommandExecution) { + ICommandSender sender = ((GameCommandExecution) execution).getSender(); + + if (StringUtils.isEmpty(playerArgument)) { + if (sender instanceof IPlayer) { + startLinking((IPlayer) sender, ((GameCommandExecution) execution).getLabel()); + } else { + // TODO: please specify player+user + } + return; + } + + if (!sender.hasPermission(Permission.COMMAND_LINK_OTHER)) { + sender.sendMessage(discordSRV.messagesConfig(sender).noPermission.asComponent()); + return; + } + } + + LinkProvider linkProvider = discordSRV.linkProvider(); + if (!(linkProvider instanceof LinkStore)) { + // TODO: not allowed + return; + } + + UUID playerUUID = CommandUtil.lookupPlayer(discordSRV, execution, false, playerArgument, null); + if (playerUUID == null) { + // TODO: player not found + return; + } + + Long userId = CommandUtil.lookupUser(discordSRV, execution, false, userArgument, null); + if (userId == null) { + // TODO: user not found + return; + } + + linkProvider.queryUserId(playerUUID).thenCompose(opt -> { + if (opt.isPresent()) { + // TODO: already linked + return null; + } + + return ((LinkStore) linkProvider).createLink(playerUUID, userId); + }).whenComplete((v, t) -> { + if (t != null) { + // TODO: it did not work + return; + } + + // TODO: it did work + }); + } + + private void startLinking(IPlayer player, String label) { + LinkProvider linkProvider = discordSRV.linkProvider(); + if (linkProvider.getCachedUserId(player.uniqueId()).isPresent()) { + player.sendMessage(discordSRV.messagesConfig(player).alreadyLinked.asComponent()); + return; + } + + if (linkCheckRateLimit.getIfPresent(player.uniqueId()) != null) { + player.sendMessage(discordSRV.messagesConfig(player).pleaseWaitBeforeRunningThatCommandAgain.asComponent()); + return; + } + linkCheckRateLimit.put(player.uniqueId(), true); + + player.sendMessage(discordSRV.messagesConfig(player).checkingLinkStatus.asComponent()); + linkProvider.queryUserId(player.uniqueId(), true).whenComplete((userId, t) -> { + if (t != null) { + player.sendMessage(discordSRV.messagesConfig(player).unableToLinkAtThisTime.asComponent()); + return; + } + if (userId.isPresent()) { + player.sendMessage(discordSRV.messagesConfig(player).youAreNowLinked.asComponent()); + return; + } + + linkProvider.getLinkingInstructions(player, label).whenComplete((comp, t2) -> { + if (t2 != null) { + player.sendMessage(discordSRV.messagesConfig(player).unableToLinkAtThisTime.asComponent()); + return; + } + + player.sendMessage(ComponentUtil.fromAPI(comp)); + }); + }); + } +} diff --git a/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkedCommand.java b/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkedCommand.java new file mode 100644 index 00000000..bf361f06 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkedCommand.java @@ -0,0 +1,86 @@ +package com.discordsrv.common.command.combined.commands; + +import com.discordsrv.api.discord.entity.interaction.command.CommandOption; +import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand; +import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.command.combined.abstraction.CombinedCommand; +import com.discordsrv.common.command.combined.abstraction.CommandExecution; +import com.discordsrv.common.command.combined.abstraction.Text; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.util.CommandUtil; +import com.discordsrv.common.permission.util.Permission; + +import java.util.UUID; + +public class LinkedCommand extends CombinedCommand { + + private static LinkedCommand INSTANCE; + private static GameCommand GAME; + private static DiscordCommand DISCORD; + + private static LinkedCommand getInstance(DiscordSRV discordSRV) { + return INSTANCE != null ? INSTANCE : (INSTANCE = new LinkedCommand(discordSRV)); + } + + public static GameCommand getGame(DiscordSRV discordSRV) { + if (GAME == null) { + LinkedCommand command = getInstance(discordSRV); + GAME = GameCommand.literal("linked") + .then( + GameCommand.stringWord("target") + .requiredPermission(Permission.COMMAND_LINKED_OTHER) + .executor(command) + ) + .requiredPermission(Permission.COMMAND_LINKED) + .executor(command); + } + + return GAME; + } + + public static DiscordCommand getDiscord(DiscordSRV discordSRV) { + if (DISCORD == null) { + LinkedCommand command = getInstance(discordSRV); + DISCORD = DiscordCommand.chatInput(ComponentIdentifier.of("DiscordSRV", "linked"), "linked", "Get the linking status of a given user") + .addOption(CommandOption.builder( + CommandOption.Type.USER, + "user", + "The Discord user to check the linking status of" + ).setRequired(false).build()) + .addOption(CommandOption.builder( + CommandOption.Type.STRING, + "player", + "The Minecraft player username or UUID to check the linking status of" + ).setRequired(false).build()) + .setEventHandler(command) + .build(); + } + + return DISCORD; + } + + public LinkedCommand(DiscordSRV discordSRV) { + super(discordSRV); + } + + @Override + public void execute(CommandExecution execution) { + execution.setEphemeral(true); + + CommandUtil.TargetLookupResult result = CommandUtil.lookupTarget(discordSRV, execution, true, Permission.COMMAND_LINKED_OTHER); + if (!result.isValid()) { + return; + } + + if (result.isPlayer()) { + execution.runAsync(() -> discordSRV.linkProvider().getUserId(result.getPlayerUUID()).whenComplete((userId, t) -> { + execution.send(new Text(userId.map(Long::toUnsignedString).orElse("Not linked"))); // TODO: username + })); + } else { + execution.runAsync(() -> discordSRV.linkProvider().getPlayerUUID(result.getUserId()).whenComplete((playerUUID, t) -> { + execution.send(new Text(playerUUID.map(UUID::toString).orElse("Not linked"))); // TODO: player name + })); + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/command/combined/commands/ResyncCommand.java b/common/src/main/java/com/discordsrv/common/command/combined/commands/ResyncCommand.java index 358dbdc8..cb0cb368 100644 --- a/common/src/main/java/com/discordsrv/common/command/combined/commands/ResyncCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/combined/commands/ResyncCommand.java @@ -12,6 +12,7 @@ import com.discordsrv.common.future.util.CompletableFutureUtil; import com.discordsrv.common.groupsync.GroupSyncModule; import com.discordsrv.common.groupsync.enums.GroupSyncCause; import com.discordsrv.common.groupsync.enums.GroupSyncResult; +import com.discordsrv.common.permission.util.Permission; import com.discordsrv.common.player.IPlayer; import net.kyori.adventure.text.format.NamedTextColor; @@ -34,7 +35,7 @@ public class ResyncCommand extends CombinedCommand { if (GAME == null) { ResyncCommand command = getInstance(discordSRV); GAME = GameCommand.literal("resync") - .requiredPermission("discordsrv.admin.resync") + .requiredPermission(Permission.COMMAND_RESYNC) .executor(command); } diff --git a/common/src/main/java/com/discordsrv/common/command/combined/commands/VersionCommand.java b/common/src/main/java/com/discordsrv/common/command/combined/commands/VersionCommand.java index 84025114..2ed6ba0b 100644 --- a/common/src/main/java/com/discordsrv/common/command/combined/commands/VersionCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/combined/commands/VersionCommand.java @@ -27,6 +27,7 @@ import com.discordsrv.common.command.combined.abstraction.CommandExecution; import com.discordsrv.common.command.combined.abstraction.Text; import com.discordsrv.common.command.game.abstraction.GameCommand; import com.discordsrv.common.debug.data.VersionInfo; +import com.discordsrv.common.permission.util.Permission; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; import org.apache.commons.lang3.StringUtils; @@ -48,7 +49,7 @@ public class VersionCommand extends CombinedCommand { if (GAME == null) { VersionCommand command = getInstance(discordSRV); GAME = GameCommand.literal("version") - .requiredPermission("discordsrv.admin.version") + .requiredPermission(Permission.COMMAND_VERSION) .executor(command); } diff --git a/common/src/main/java/com/discordsrv/common/command/discord/commands/DiscordSRVDiscordCommand.java b/common/src/main/java/com/discordsrv/common/command/discord/commands/DiscordSRVDiscordCommand.java index bb4d7e2f..1105bf95 100644 --- a/common/src/main/java/com/discordsrv/common/command/discord/commands/DiscordSRVDiscordCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/discord/commands/DiscordSRVDiscordCommand.java @@ -3,9 +3,7 @@ package com.discordsrv.common.command.discord.commands; import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand; import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; import com.discordsrv.common.DiscordSRV; -import com.discordsrv.common.command.combined.commands.DebugCommand; -import com.discordsrv.common.command.combined.commands.ResyncCommand; -import com.discordsrv.common.command.combined.commands.VersionCommand; +import com.discordsrv.common.command.combined.commands.*; import com.discordsrv.common.command.discord.commands.subcommand.ExecuteCommand; import com.discordsrv.common.config.main.DiscordCommandConfig; @@ -22,7 +20,9 @@ public class DiscordSRVDiscordCommand { DiscordCommand.ChatInputBuilder builder = DiscordCommand.chatInput(IDENTIFIER, "discordsrv", "DiscordSRV related commands") .addSubCommand(DebugCommand.getDiscord(discordSRV)) .addSubCommand(VersionCommand.getDiscord(discordSRV)) - .addSubCommand(ResyncCommand.getDiscord(discordSRV)); + .addSubCommand(ResyncCommand.getDiscord(discordSRV)) + .addSubCommand(LinkInitCommand.getDiscord(discordSRV)) + .addSubCommand(LinkedCommand.getDiscord(discordSRV)); if (config.execute.enabled) { builder = builder.addSubCommand(ExecuteCommand.get(discordSRV)); diff --git a/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java b/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java index caff648e..79745800 100644 --- a/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java +++ b/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java @@ -20,9 +20,9 @@ package com.discordsrv.common.command.game; import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.command.combined.commands.LinkInitCommand; import com.discordsrv.common.command.game.abstraction.GameCommand; import com.discordsrv.common.command.game.commands.DiscordSRVGameCommand; -import com.discordsrv.common.command.game.commands.subcommand.LinkCommand; import com.discordsrv.common.command.game.handler.ICommandHandler; import com.discordsrv.common.config.main.GameCommandConfig; import com.discordsrv.common.module.type.AbstractModule; @@ -43,7 +43,7 @@ public class GameCommandModule extends AbstractModule { super(discordSRV); this.primaryCommand = DiscordSRVGameCommand.get(discordSRV, "discordsrv"); this.discordAlias = DiscordSRVGameCommand.get(discordSRV, "discord"); - this.linkCommand = LinkCommand.get(discordSRV); + this.linkCommand = LinkInitCommand.getGame(discordSRV); registerCommand(primaryCommand); } diff --git a/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommand.java b/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommand.java index cba363f3..154d095a 100644 --- a/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommand.java @@ -20,6 +20,7 @@ package com.discordsrv.common.command.game.abstraction; import com.discordsrv.common.command.game.sender.ICommandSender; import com.discordsrv.common.function.CheckedFunction; +import com.discordsrv.common.permission.util.Permission; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; @@ -176,6 +177,10 @@ public class GameCommand { return redirection; } + public GameCommand requiredPermission(Permission permission) { + return requiredPermission(permission.permission()); + } + public GameCommand requiredPermission(String permission) { if (redirection != null) { throw new IllegalStateException("Cannot required permissions on a node with a redirection"); diff --git a/common/src/main/java/com/discordsrv/common/command/game/commands/DiscordSRVGameCommand.java b/common/src/main/java/com/discordsrv/common/command/game/commands/DiscordSRVGameCommand.java index 25e80c28..a4ed019a 100644 --- a/common/src/main/java/com/discordsrv/common/command/game/commands/DiscordSRVGameCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/game/commands/DiscordSRVGameCommand.java @@ -20,17 +20,15 @@ package com.discordsrv.common.command.game.commands; import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.common.DiscordSRV; -import com.discordsrv.common.command.combined.commands.DebugCommand; -import com.discordsrv.common.command.combined.commands.ResyncCommand; -import com.discordsrv.common.command.combined.commands.VersionCommand; +import com.discordsrv.common.command.combined.commands.*; import com.discordsrv.common.command.game.abstraction.GameCommand; import com.discordsrv.common.command.game.abstraction.GameCommandArguments; import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; import com.discordsrv.common.command.game.commands.subcommand.BroadcastCommand; -import com.discordsrv.common.command.game.commands.subcommand.LinkCommand; import com.discordsrv.common.command.game.commands.subcommand.reload.ReloadCommand; import com.discordsrv.common.command.game.sender.ICommandSender; import com.discordsrv.common.component.util.ComponentUtil; +import com.discordsrv.common.permission.util.Permission; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -46,13 +44,14 @@ public class DiscordSRVGameCommand implements GameCommandExecutor { } return INSTANCES.computeIfAbsent(alias, key -> GameCommand.literal(alias) - .requiredPermission("discordsrv.player.command") + .requiredPermission(Permission.COMMAND_ROOT) .executor(COMMAND) .then(BroadcastCommand.discord(discordSRV)) .then(BroadcastCommand.minecraft(discordSRV)) .then(BroadcastCommand.json(discordSRV)) .then(DebugCommand.getGame(discordSRV)) - .then(LinkCommand.get(discordSRV)) + .then(LinkInitCommand.getGame(discordSRV)) + .then(LinkedCommand.getGame(discordSRV)) .then(ReloadCommand.get(discordSRV)) .then(ResyncCommand.getGame(discordSRV)) .then(VersionCommand.getGame(discordSRV)) diff --git a/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/BroadcastCommand.java b/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/BroadcastCommand.java index 5e3f1289..99d93367 100644 --- a/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/BroadcastCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/BroadcastCommand.java @@ -31,6 +31,7 @@ import com.discordsrv.common.command.game.sender.ICommandSender; import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.config.main.channels.base.IChannelConfig; +import com.discordsrv.common.permission.util.Permission; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -72,7 +73,7 @@ public abstract class BroadcastCommand implements GameCommandExecutor, GameComma BroadcastCommand command = executor.get(); consumer.accept( GameCommand.literal(label) - .requiredPermission("discordsrv.admin.broadcast") + .requiredPermission(Permission.COMMAND_BROADCAST) .then( GameCommand.string("channel") .suggester(command) diff --git a/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/LinkCommand.java b/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/LinkCommand.java deleted file mode 100644 index 805720b9..00000000 --- a/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/LinkCommand.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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.command.game.commands.subcommand; - -import com.discordsrv.common.DiscordSRV; -import com.discordsrv.common.command.game.abstraction.GameCommand; -import com.discordsrv.common.command.game.abstraction.GameCommandArguments; -import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; -import com.discordsrv.common.command.game.sender.ICommandSender; -import com.discordsrv.common.component.util.ComponentUtil; -import com.discordsrv.common.linking.LinkProvider; -import com.discordsrv.common.linking.LinkStore; -import com.discordsrv.common.player.IPlayer; -import com.github.benmanes.caffeine.cache.Cache; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; - -import java.util.UUID; - -public class LinkCommand implements GameCommandExecutor { - - private static GameCommand INSTANCE; - - public static GameCommand get(DiscordSRV discordSRV) { - if (INSTANCE == null) { - INSTANCE = GameCommand.literal("link") - .requiredPermission("discordsrv.player.link") - .executor(new LinkCommand(discordSRV)); - } - - return INSTANCE; - } - - private final DiscordSRV discordSRV; - private final Cache linkCheckRateLimit; - - public LinkCommand(DiscordSRV discordSRV) { - this.discordSRV = discordSRV; - this.linkCheckRateLimit = discordSRV.caffeineBuilder() - .expireAfterWrite(LinkStore.LINKING_CODE_RATE_LIMIT) - .build(); - } - - @Override - public void execute(ICommandSender sender, GameCommandArguments arguments, String label) { - if (!(sender instanceof IPlayer)) { - sender.sendMessage(Component.text("Player only command").color(NamedTextColor.RED)); - return; - } - - IPlayer player = (IPlayer) sender; - LinkProvider linkProvider = discordSRV.linkProvider(); - if (linkProvider.getCachedUserId(player.uniqueId()).isPresent()) { - player.sendMessage(discordSRV.messagesConfig(player).alreadyLinked.asComponent()); - return; - } - - if (linkCheckRateLimit.getIfPresent(player.uniqueId()) != null) { - player.sendMessage(discordSRV.messagesConfig(player).pleaseWaitBeforeRunningThatCommandAgain.asComponent()); - return; - } - linkCheckRateLimit.put(player.uniqueId(), true); - - sender.sendMessage(discordSRV.messagesConfig(player).checkingLinkStatus.asComponent()); - linkProvider.queryUserId(player.uniqueId()).whenComplete((userId, t) -> { - if (t != null) { - sender.sendMessage(discordSRV.messagesConfig(player).unableToLinkAtThisTime.asComponent()); - return; - } - if (userId.isPresent()) { - sender.sendMessage(discordSRV.messagesConfig(player).youAreNowLinked.asComponent()); - return; - } - - linkProvider.getLinkingInstructions(player, label).whenComplete((comp, t2) -> { - if (t2 != null) { - sender.sendMessage(discordSRV.messagesConfig(player).unableToLinkAtThisTime.asComponent()); - return; - } - - sender.sendMessage(ComponentUtil.fromAPI(comp)); - }); - }); - } -} diff --git a/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/reload/ReloadCommand.java b/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/reload/ReloadCommand.java index 8435747b..59d20bd2 100644 --- a/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/reload/ReloadCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/reload/ReloadCommand.java @@ -25,6 +25,7 @@ import com.discordsrv.common.command.game.abstraction.GameCommandArguments; import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; import com.discordsrv.common.command.game.abstraction.GameCommandSuggester; import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.common.permission.util.Permission; import com.discordsrv.common.player.IPlayer; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; @@ -44,7 +45,7 @@ public class ReloadCommand implements GameCommandExecutor, GameCommandSuggester if (INSTANCE == null) { ReloadCommand cmd = new ReloadCommand(discordSRV); INSTANCE = GameCommand.literal("reload") - .requiredPermission("discordsrv.admin.reload") + .requiredPermission(Permission.COMMAND_RELOAD) .executor(cmd) .then( GameCommand.stringGreedy("flags") diff --git a/common/src/main/java/com/discordsrv/common/command/game/sender/ICommandSender.java b/common/src/main/java/com/discordsrv/common/command/game/sender/ICommandSender.java index 07cfb4ce..81334711 100644 --- a/common/src/main/java/com/discordsrv/common/command/game/sender/ICommandSender.java +++ b/common/src/main/java/com/discordsrv/common/command/game/sender/ICommandSender.java @@ -19,10 +19,15 @@ package com.discordsrv.common.command.game.sender; import com.discordsrv.common.command.game.executor.CommandExecutor; +import com.discordsrv.common.permission.util.Permission; import net.kyori.adventure.audience.ForwardingAudience; public interface ICommandSender extends ForwardingAudience.Single, CommandExecutor { + default boolean hasPermission(Permission permission) { + return hasPermission(permission.permission()); + } + boolean hasPermission(String permission); } diff --git a/common/src/main/java/com/discordsrv/common/command/util/CommandUtil.java b/common/src/main/java/com/discordsrv/common/command/util/CommandUtil.java new file mode 100644 index 00000000..140632f7 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/util/CommandUtil.java @@ -0,0 +1,194 @@ +package com.discordsrv.common.command.util; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.command.combined.abstraction.CommandExecution; +import com.discordsrv.common.command.combined.abstraction.DiscordCommandExecution; +import com.discordsrv.common.command.combined.abstraction.GameCommandExecution; +import com.discordsrv.common.command.combined.abstraction.Text; +import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.common.permission.util.Permission; +import com.discordsrv.common.player.IPlayer; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.utils.MiscUtil; +import net.kyori.adventure.text.format.NamedTextColor; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; + +public final class CommandUtil { + + private CommandUtil() {} + + @Nullable + public static UUID lookupPlayer( + DiscordSRV discordSRV, + CommandExecution execution, + boolean selfPermitted, + String target, + @Nullable Permission otherPermission + ) { + TargetLookupResult result = lookupTarget(discordSRV, execution, target, selfPermitted, true, false, otherPermission); + if (result.isValid()) { + return result.getPlayerUUID(); + } + return null; + } + + @Nullable + public static Long lookupUser( + DiscordSRV discordSRV, + CommandExecution execution, + boolean selfPermitted, + String target, + @Nullable Permission otherPermission + ) { + TargetLookupResult result = lookupTarget(discordSRV, execution, target, selfPermitted, false, true, otherPermission); + if (result.isValid()) { + return result.getUserId(); + } + return null; + } + + public static TargetLookupResult lookupTarget( + DiscordSRV discordSRV, + CommandExecution execution, + boolean selfPermitted, + @Nullable Permission otherPermission + ) { + String target = execution.getArgument("target"); + if (target == null) { + target = execution.getArgument("user"); + } + if (target == null) { + target = execution.getArgument("player"); + } + return lookupTarget(discordSRV, execution, target, selfPermitted, true, true, otherPermission); + } + + private static TargetLookupResult lookupTarget( + DiscordSRV discordSRV, + CommandExecution execution, + String target, + boolean selfPermitted, + boolean lookupPlayer, + boolean lookupUser, + @Nullable Permission otherPermission + ) { + if (execution instanceof GameCommandExecution) { + ICommandSender sender = ((GameCommandExecution) execution).getSender(); + if (target != null) { + if (otherPermission != null && !sender.hasPermission(Permission.COMMAND_LINKED_OTHER)) { + sender.sendMessage(discordSRV.messagesConfig(sender).noPermission.asComponent()); + return TargetLookupResult.INVALID; + } + } else if (sender instanceof IPlayer && selfPermitted && lookupPlayer) { + target = ((IPlayer) sender).uniqueId().toString(); + } else { + execution.send(new Text(discordSRV.messagesConfig(execution.locale()).both.placeSpecifyTarget).withGameColor(NamedTextColor.RED)); + return TargetLookupResult.INVALID; + } + } else if (execution instanceof DiscordCommandExecution) { + if (target == null) { + if (selfPermitted && lookupUser) { + target = Long.toUnsignedString(((DiscordCommandExecution) execution).getUser().getIdLong()); + } else { + execution.send(new Text(discordSRV.messagesConfig(execution.locale()).both.placeSpecifyTarget).withGameColor(NamedTextColor.RED)); + return TargetLookupResult.INVALID; + } + } + } else { + throw new IllegalStateException("Unexpected CommandExecution"); + } + + if (lookupUser) { + if (target.matches("\\d{17,22}")) { + // Discord user id + long id; + try { + id = MiscUtil.parseLong(target); + } catch (IllegalArgumentException ignored) { + execution.send(new Text(discordSRV.messagesConfig(execution.locale()).both.invalidTarget) + .withGameColor(NamedTextColor.RED)); + return TargetLookupResult.INVALID; + } + + return new TargetLookupResult(true, null, id); + } else if (target.startsWith("@")) { + // Discord username + String username = target.substring(1); + JDA jda = discordSRV.jda(); + if (jda != null) { + List users = jda.getUsersByName(username, true); + + if (users.size() == 1) { + return new TargetLookupResult(true, null, users.get(0).getIdLong()); + } + } + } + } + + if (lookupPlayer) { + UUID uuid; + boolean shortUUID; + if ((shortUUID = target.length() == 32) || target.length() == 36) { + // Player UUID + if (shortUUID) { + target = target.substring(0, 8) + "-" + target.substring(8, 12) + "-" + target.substring(12, 16) + + "-" + target.substring(16, 20) + "-" + target.substring(20); + } + + try { + uuid = UUID.fromString(target); + } catch (IllegalArgumentException ignored) { + execution.send(new Text(discordSRV.messagesConfig(execution.locale()).both.invalidTarget).withGameColor(NamedTextColor.RED)); + return TargetLookupResult.INVALID; + } + } else { + // Player name + IPlayer playerByName = discordSRV.playerProvider().player(target); + if (playerByName != null) { + uuid = playerByName.uniqueId(); + } else { + throw new IllegalStateException("lookup offline"); // TODO: lookup offline player + } + } + + return new TargetLookupResult(true, uuid, 0L); + } + + return TargetLookupResult.INVALID; + } + + public static class TargetLookupResult { + + public static TargetLookupResult INVALID = new TargetLookupResult(false, null, 0L); + + private final boolean valid; + private final UUID playerUUID; + private final long userId; + + public TargetLookupResult(boolean valid, UUID playerUUID, long userId) { + this.valid = valid; + this.playerUUID = playerUUID; + this.userId = userId; + } + + public boolean isValid() { + return valid; + } + + public boolean isPlayer() { + return playerUUID != null; + } + + public UUID getPlayerUUID() { + return playerUUID; + } + + public long getUserId() { + return userId; + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/config/messages/MessagesConfig.java b/common/src/main/java/com/discordsrv/common/config/messages/MessagesConfig.java index bf9512c6..c16cc3f4 100644 --- a/common/src/main/java/com/discordsrv/common/config/messages/MessagesConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/messages/MessagesConfig.java @@ -2,8 +2,10 @@ package com.discordsrv.common.config.messages; import com.discordsrv.common.config.Config; import com.discordsrv.common.config.configurate.annotation.Constants; +import com.discordsrv.common.config.configurate.annotation.Untranslated; import com.discordsrv.common.config.helper.MinecraftMessage; import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; @ConfigSerializable public class MessagesConfig implements Config { @@ -23,6 +25,12 @@ public class MessagesConfig implements Config { return new MinecraftMessage(rawFormat); } + @Comment("Generic") + @Constants("&c") + public MinecraftMessage noPermission = make("%1Sorry, but you do not have permission to use that command"); + + @Untranslated(Untranslated.Type.COMMENT) + @Comment("/discord link") @Constants("&c") public MinecraftMessage unableToCheckLinkingStatus = make("%1Unable to check linking status, please try again later"); @Constants("&c") @@ -33,16 +41,15 @@ public class MessagesConfig implements Config { public MinecraftMessage unableToLinkAtThisTime = make("%1Unable to check linking status, please try again later"); @Constants("&b") public MinecraftMessage checkingLinkStatus = make("%1Checking linking status..."); - @Constants("&b") public MinecraftMessage youAreNowLinked = make("%1You are now linked!"); - @Constants({ "&b", "&7[click:open_url:%minecraftauth_link%][hover:show_text:Click to open]%minecraftauth_link_simple%[click]&b", "&7MinecraftAuth" }) public MinecraftMessage minecraftAuthLinking = make("%1Please visit %2 to link your account through %3"); + } public Discord discord = new Discord(); @@ -52,5 +59,12 @@ public class MessagesConfig implements Config { } + public Both both = new Both(); + public static class Both { + + @Comment("Generic") + public String invalidTarget = "Invalid target"; + public String placeSpecifyTarget = "Please specify the target"; + } } diff --git a/common/src/main/java/com/discordsrv/common/linking/LinkProvider.java b/common/src/main/java/com/discordsrv/common/linking/LinkProvider.java index 5428b0dc..f8fbb423 100644 --- a/common/src/main/java/com/discordsrv/common/linking/LinkProvider.java +++ b/common/src/main/java/com/discordsrv/common/linking/LinkProvider.java @@ -30,7 +30,11 @@ import java.util.concurrent.CompletableFuture; public interface LinkProvider { - CompletableFuture> queryUserId(@NotNull UUID playerUUID); + default CompletableFuture> queryUserId(@NotNull UUID playerUUID) { + return queryUserId(playerUUID, false); + } + + CompletableFuture> queryUserId(@NotNull UUID playerUUID, boolean canCauseLink); default CompletableFuture> getUserId(@NotNull UUID playerUUID) { Optional userId = getCachedUserId(playerUUID); @@ -44,7 +48,11 @@ public interface LinkProvider { return Optional.empty(); } - CompletableFuture> queryPlayerUUID(long userId); + default CompletableFuture> queryPlayerUUID(long userId) { + return queryPlayerUUID(userId, false); + } + + CompletableFuture> queryPlayerUUID(long userId, boolean canCauseLink); default CompletableFuture> getPlayerUUID(long userId) { Optional playerUUID = getCachedPlayerUUID(userId); diff --git a/common/src/main/java/com/discordsrv/common/linking/impl/CachedLinkProvider.java b/common/src/main/java/com/discordsrv/common/linking/impl/CachedLinkProvider.java index d031c634..ebd2091a 100644 --- a/common/src/main/java/com/discordsrv/common/linking/impl/CachedLinkProvider.java +++ b/common/src/main/java/com/discordsrv/common/linking/impl/CachedLinkProvider.java @@ -22,14 +22,19 @@ import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.linking.LinkProvider; import com.discordsrv.common.player.event.PlayerConnectedEvent; -import com.github.benmanes.caffeine.cache.*; +import com.github.benmanes.caffeine.cache.AsyncCacheLoader; +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Expiry; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.NotNull; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -41,6 +46,7 @@ public abstract class CachedLinkProvider implements LinkProvider { protected final DiscordSRV discordSRV; private final Cache userToPlayer; private final AsyncLoadingCache playerToUser; + private final Set linkingAllowed = new CopyOnWriteArraySet<>(); public CachedLinkProvider(DiscordSRV discordSRV) { this.discordSRV = discordSRV; @@ -86,7 +92,7 @@ public abstract class CachedLinkProvider implements LinkProvider { .buildAsync(new AsyncCacheLoader() { @Override public @NonNull CompletableFuture asyncLoad(@NonNull UUID key, @NonNull Executor executor) { - return queryUserId(key).thenApply(opt -> opt.orElse(UNLINKED_USER)); + return queryUserId(key, linkingAllowed.remove(key)).thenApply(opt -> opt.orElse(UNLINKED_USER)); } @Override @@ -150,6 +156,9 @@ public abstract class CachedLinkProvider implements LinkProvider { @Subscribe public void onPlayerConnected(PlayerConnectedEvent event) { // Cache logged in players - playerToUser.get(event.player().uniqueId()); + UUID uuid = event.player().uniqueId(); + linkingAllowed.add(uuid); + playerToUser.get(uuid); + linkingAllowed.remove(uuid); } } 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 8cd24408..c42beb44 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 @@ -58,8 +58,9 @@ public class MinecraftAuthenticationLinker extends CachedLinkProvider implements } @Override - public CompletableFuture> queryUserId(@NotNull UUID playerUUID) { + public CompletableFuture> queryUserId(@NotNull UUID playerUUID, boolean canCauseLink) { return query( + canCauseLink, () -> AuthService.lookup(AccountType.MINECRAFT, playerUUID.toString(), AccountType.DISCORD) .map(account -> (DiscordAccount) account) .map(discord -> Long.parseUnsignedLong(discord.getUserId())), @@ -73,8 +74,9 @@ public class MinecraftAuthenticationLinker extends CachedLinkProvider implements } @Override - public CompletableFuture> queryPlayerUUID(long userId) { + public CompletableFuture> queryPlayerUUID(long userId, boolean canCauseLink) { return query( + canCauseLink, () -> AuthService.lookup(AccountType.DISCORD, Long.toUnsignedString(userId), AccountType.MINECRAFT) .map(account -> (MinecraftAccount) account) .map(MinecraftAccount::getUUID), @@ -163,6 +165,7 @@ public class MinecraftAuthenticationLinker extends CachedLinkProvider implements } private CompletableFuture> query( + boolean canCauseLink, CheckedSupplier> authSupplier, Supplier>> storageSupplier, Consumer linked, @@ -177,6 +180,9 @@ public class MinecraftAuthenticationLinker extends CachedLinkProvider implements authService.completeExceptionally(t); } }); + if (!canCauseLink) { + return authService; + } CompletableFuture> storageFuture = storageSupplier.get(); return CompletableFutureUtil.combine(authService, storageFuture).thenApply(results -> { 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 0151dc79..95739cb6 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 @@ -40,7 +40,7 @@ public class StorageLinker extends CachedLinkProvider implements LinkProvider, L } @Override - public CompletableFuture> queryUserId(@NotNull UUID playerUUID) { + public CompletableFuture> queryUserId(@NotNull UUID playerUUID, boolean canCauseLink) { return CompletableFuture.supplyAsync(() -> { Long value = discordSRV.storage().getUserId(playerUUID); return Optional.ofNullable(value); @@ -48,7 +48,7 @@ public class StorageLinker extends CachedLinkProvider implements LinkProvider, L } @Override - public CompletableFuture> queryPlayerUUID(long userId) { + public CompletableFuture> queryPlayerUUID(long userId, boolean canCauseLink) { return CompletableFuture.supplyAsync(() -> { UUID value = discordSRV.storage().getPlayerUUID(userId); return Optional.ofNullable(value); 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 index 616b26cd..3154bea3 100644 --- a/common/src/main/java/com/discordsrv/common/linking/requirelinking/ServerRequireLinkingModule.java +++ b/common/src/main/java/com/discordsrv/common/linking/requirelinking/ServerRequireLinkingModule.java @@ -79,7 +79,7 @@ public abstract class ServerRequireLinkingModule extends R return CompletableFuture.completedFuture(message); } - return linkProvider.queryUserId(playerUUID) + return linkProvider.queryUserId(playerUUID, true) .thenCompose(opt -> { if (!opt.isPresent()) { // User is not linked diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/game/minecrafttodiscord/MinecraftToDiscordChatModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/game/minecrafttodiscord/MinecraftToDiscordChatModule.java index 584079de..69f02af9 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/game/minecrafttodiscord/MinecraftToDiscordChatModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/game/minecrafttodiscord/MinecraftToDiscordChatModule.java @@ -38,6 +38,7 @@ import com.discordsrv.common.config.main.channels.MinecraftToDiscordChatConfig; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.future.util.CompletableFutureUtil; import com.discordsrv.common.messageforwarding.game.AbstractGameMessageModule; +import com.discordsrv.common.permission.util.Permission; import com.discordsrv.common.player.IPlayer; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Role; @@ -126,7 +127,7 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule>> futures = new ArrayList<>(); String messageContent = discordSRV.componentFactory().plainSerializer().serialize(message); @@ -184,23 +185,23 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule allowedMentions = new ArrayList<>(); - if (mentionConfig.users && player.hasPermission("discordsrv.mention.user")) { + if (mentionConfig.users && player.hasPermission(Permission.MENTION_USER)) { allowedMentions.add(AllowedMention.ALL_USERS); } if (mentionConfig.roles) { - if (player.hasPermission("discordsrv.mention.roles.mentionable")) { + if (player.hasPermission(Permission.MENTION_ROLE_MENTIONABLE)) { for (Role role : guild.getRoles()) { if (role.isMentionable()) { allowedMentions.add(AllowedMention.role(role.getIdLong())); } } } - if (player.hasPermission("discordsrv.mention.roles.all")) { + if (player.hasPermission(Permission.MENTION_ROLE_ALL)) { allowedMentions.add(AllowedMention.ALL_ROLES); } } - boolean everyone = mentionConfig.everyone && player.hasPermission("discordsrv.mention.everyone"); + boolean everyone = mentionConfig.everyone && player.hasPermission(Permission.MENTION_EVERYONE); if (everyone) { allowedMentions.add(AllowedMention.EVERYONE); } diff --git a/common/src/main/java/com/discordsrv/common/permission/util/Permission.java b/common/src/main/java/com/discordsrv/common/permission/util/Permission.java new file mode 100644 index 00000000..b1b31cfb --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/permission/util/Permission.java @@ -0,0 +1,39 @@ +package com.discordsrv.common.permission.util; + +public enum Permission { + + // Commands + // Admin + COMMAND_DEBUG("command.admin.debug"), + COMMAND_RELOAD("command.admin.reload"), + COMMAND_BROADCAST("command.admin.broadcast"), + COMMAND_RESYNC("command.admin.resync"), + COMMAND_VERSION("command.admin.version"), + // Player + COMMAND_ROOT("command.player.root"), + COMMAND_LINK("command.player.link.base"), + COMMAND_LINK_OTHER("command.player.link.other"), + COMMAND_LINKED("command.player.linked.base"), + COMMAND_LINKED_OTHER("command.player.linked.other"), + + // Mentions + MENTION_USER("mention.user.base"), + MENTION_USER_LOOKUP("mention.user.lookup"), + MENTION_ROLE_MENTIONABLE("mention.role.mentionable"), + MENTION_ROLE_ALL("mention.role.all"), + MENTION_EVERYONE("mention.everyone"), + + // Misc + UPDATE_NOTIFICATION("updatenotification"), + ; + + private final String permission; + + Permission(String permission) { + this.permission = permission; + } + + public String permission() { + return "discordsrv." + permission; + } +} diff --git a/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java b/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java index bffea82d..408e6de0 100644 --- a/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java +++ b/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java @@ -25,6 +25,7 @@ import com.discordsrv.common.config.connection.UpdateConfig; import com.discordsrv.common.debug.data.VersionInfo; import com.discordsrv.common.exception.MessageException; import com.discordsrv.common.logging.NamedLogger; +import com.discordsrv.common.permission.util.Permission; import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.event.PlayerConnectedEvent; import com.discordsrv.common.update.github.GitHubCompareResponse; @@ -303,7 +304,7 @@ public class UpdateChecker { } IPlayer player = event.player(); - if (!player.hasPermission("discordsrv.updatenotification")) { + if (!player.hasPermission(Permission.UPDATE_NOTIFICATION)) { return; }