Initial for linking commands

This commit is contained in:
Vankka 2023-12-13 20:30:15 +02:00
parent dae2e7232c
commit 1dd1e4d834
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
27 changed files with 669 additions and 144 deletions

View File

@ -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<DiscordChatInputInteractionEvent> {
public abstract class CombinedCommand
implements
GameCommandExecutor,
GameCommandSuggester,
Consumer<DiscordChatInputInteractionEvent>,
DiscordCommand.AutoCompleteHandler
{
protected final DiscordSRV discordSRV;
@ -18,7 +30,7 @@ public abstract class CombinedCommand implements GameCommandExecutor, Consumer<D
@Override
public void execute(ICommandSender sender, GameCommandArguments arguments, String label) {
execute(new GameCommandExecution(discordSRV, sender, arguments));
execute(new GameCommandExecution(discordSRV, sender, arguments, label));
}
@Override
@ -28,4 +40,18 @@ public abstract class CombinedCommand implements GameCommandExecutor, Consumer<D
public abstract void execute(CommandExecution execution);
@Override
public List<String> suggestValues(ICommandSender sender, GameCommandArguments previousArguments, String currentInput) {
return suggest(new GameCommandExecution(discordSRV, sender, previousArguments, null), currentInput);
}
@Override
public void autoComplete(DiscordCommandAutoCompleteInteractionEvent event) {
List<String> suggestions = suggest(new DiscordCommandExecution(discordSRV, event), null);
suggestions.forEach(suggestion -> event.addChoice(suggestion, suggestion));
}
public List<String> suggest(CommandExecution execution, @Nullable String input) {
return Collections.emptyList();
}
}

View File

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

View File

@ -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<InteractionHook> 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<Text> texts, Collection<Text> extra) {
if (replyCallback == null) {
throw new IllegalStateException("May not be used on auto completions");
}
StringBuilder builder = new StringBuilder();
EnumMap<Text.Formatting, Boolean> 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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,11 @@ import java.util.concurrent.CompletableFuture;
public interface LinkProvider {
CompletableFuture<Optional<Long>> queryUserId(@NotNull UUID playerUUID);
default CompletableFuture<Optional<Long>> queryUserId(@NotNull UUID playerUUID) {
return queryUserId(playerUUID, false);
}
CompletableFuture<Optional<Long>> queryUserId(@NotNull UUID playerUUID, boolean canCauseLink);
default CompletableFuture<Optional<Long>> getUserId(@NotNull UUID playerUUID) {
Optional<Long> userId = getCachedUserId(playerUUID);
@ -44,7 +48,11 @@ public interface LinkProvider {
return Optional.empty();
}
CompletableFuture<Optional<UUID>> queryPlayerUUID(long userId);
default CompletableFuture<Optional<UUID>> queryPlayerUUID(long userId) {
return queryPlayerUUID(userId, false);
}
CompletableFuture<Optional<UUID>> queryPlayerUUID(long userId, boolean canCauseLink);
default CompletableFuture<Optional<UUID>> getPlayerUUID(long userId) {
Optional<UUID> playerUUID = getCachedPlayerUUID(userId);

View File

@ -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<Long, UUID> userToPlayer;
private final AsyncLoadingCache<UUID, Long> playerToUser;
private final Set<UUID> linkingAllowed = new CopyOnWriteArraySet<>();
public CachedLinkProvider(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
@ -86,7 +92,7 @@ public abstract class CachedLinkProvider implements LinkProvider {
.buildAsync(new AsyncCacheLoader<UUID, Long>() {
@Override
public @NonNull CompletableFuture<Long> 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);
}
}

View File

@ -58,8 +58,9 @@ public class MinecraftAuthenticationLinker extends CachedLinkProvider implements
}
@Override
public CompletableFuture<Optional<Long>> queryUserId(@NotNull UUID playerUUID) {
public CompletableFuture<Optional<Long>> 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<Optional<UUID>> queryPlayerUUID(long userId) {
public CompletableFuture<Optional<UUID>> 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 <T> CompletableFuture<Optional<T>> query(
boolean canCauseLink,
CheckedSupplier<Optional<T>> authSupplier,
Supplier<CompletableFuture<Optional<T>>> storageSupplier,
Consumer<T> linked,
@ -177,6 +180,9 @@ public class MinecraftAuthenticationLinker extends CachedLinkProvider implements
authService.completeExceptionally(t);
}
});
if (!canCauseLink) {
return authService;
}
CompletableFuture<Optional<T>> storageFuture = storageSupplier.get();
return CompletableFutureUtil.combine(authService, storageFuture).thenApply(results -> {

View File

@ -40,7 +40,7 @@ public class StorageLinker extends CachedLinkProvider implements LinkProvider, L
}
@Override
public CompletableFuture<Optional<Long>> queryUserId(@NotNull UUID playerUUID) {
public CompletableFuture<Optional<Long>> 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<Optional<UUID>> queryPlayerUUID(long userId) {
public CompletableFuture<Optional<UUID>> queryPlayerUUID(long userId, boolean canCauseLink) {
return CompletableFuture.supplyAsync(() -> {
UUID value = discordSRV.storage().getPlayerUUID(userId);
return Optional.ofNullable(value);

View File

@ -79,7 +79,7 @@ public abstract class ServerRequireLinkingModule<T extends DiscordSRV> extends R
return CompletableFuture.completedFuture(message);
}
return linkProvider.queryUserId(playerUUID)
return linkProvider.queryUserId(playerUUID, true)
.thenCompose(opt -> {
if (!opt.isPresent()) {
// User is not linked

View File

@ -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<Mine
MentionCachingModule mentionCaching = discordSRV.getModule(MentionCachingModule.class);
if (mentionCaching != null && mentionConfig.users && mentionConfig.uncachedUsers
&& player.hasPermission("discordsrv.mention.lookup.user")) {
&& player.hasPermission(Permission.MENTION_USER_LOOKUP)) {
List<CompletableFuture<List<MentionCachingModule.CachedMention>>> futures = new ArrayList<>();
String messageContent = discordSRV.componentFactory().plainSerializer().serialize(message);
@ -184,23 +185,23 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
.collect(Collectors.toList());
List<AllowedMention> 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);
}

View File

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

View File

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