Merge branch 'linking-commands'

This commit is contained in:
Vankka 2023-12-26 18:41:11 +02:00
commit 6d4adebfd3
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
61 changed files with 1605 additions and 240 deletions

View File

@ -4,9 +4,9 @@ import org.bukkit.entity.Player;
import java.util.Locale; import java.util.Locale;
public final class PlayerLocaleProvider { public final class PaperPlayer {
private PlayerLocaleProvider() {} private PaperPlayer() {}
private static final boolean localeMethodExists; private static final boolean localeMethodExists;
private static final boolean getLocaleMethodExists; private static final boolean getLocaleMethodExists;

View File

@ -0,0 +1,45 @@
package com.discordsrv.bukkit.player;
import com.discordsrv.common.player.provider.model.SkinInfo;
import org.bukkit.entity.Player;
import org.bukkit.profile.PlayerTextures;
import java.net.URL;
import java.util.Locale;
public final class SpigotPlayer {
private SpigotPlayer() {}
private static final boolean playerProfileExists;
static {
Class<?> playerClass = Player.class;
boolean playerProfile = false;
try {
playerClass.getMethod("getPlayerProfile");
playerProfile = true;
} catch (ReflectiveOperationException ignored) {}
playerProfileExists = playerProfile;
}
public static SkinInfo getSkinInfo(Player player) {
if (!playerProfileExists) {
return null;
}
PlayerTextures textures = player.getPlayerProfile().getTextures();
URL skinUrl = textures.getSkin();
if (skinUrl == null) {
return null;
}
String skinUrlPlain = skinUrl.toString();
return new SkinInfo(
skinUrlPlain.substring(skinUrlPlain.lastIndexOf('/') + 1),
textures.getSkinModel().toString().toLowerCase(Locale.ROOT)
);
}
}

View File

@ -21,9 +21,11 @@ package com.discordsrv.bukkit.player;
import com.discordsrv.bukkit.BukkitDiscordSRV; import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.player.IOfflinePlayer; import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.provider.model.SkinInfo;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class BukkitOfflinePlayer implements IOfflinePlayer { public class BukkitOfflinePlayer implements IOfflinePlayer {
@ -47,6 +49,11 @@ public class BukkitOfflinePlayer implements IOfflinePlayer {
return offlinePlayer.getName(); return offlinePlayer.getName();
} }
@Override
public @Nullable SkinInfo skinInfo() {
return null;
}
@Override @Override
public @NotNull Identity identity() { public @NotNull Identity identity() {
return identity; return identity;

View File

@ -24,10 +24,12 @@ import com.discordsrv.bukkit.component.PaperComponentHandle;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.provider.model.SkinInfo;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Locale; import java.util.Locale;
@ -62,9 +64,14 @@ public class BukkitPlayer extends BukkitCommandSender implements IPlayer {
return player.getName(); return player.getName();
} }
@Override
public @Nullable SkinInfo skinInfo() {
return SpigotPlayer.getSkinInfo(player);
}
@Override @Override
public Locale locale() { public Locale locale() {
return PlayerLocaleProvider.getLocale(player); return PaperPlayer.getLocale(player);
} }
@Override @Override

View File

@ -20,6 +20,7 @@ package com.discordsrv.bukkit.player;
import com.discordsrv.bukkit.BukkitDiscordSRV; import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.common.player.IOfflinePlayer; import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.ServerPlayerProvider; import com.discordsrv.common.player.ServerPlayerProvider;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -87,13 +88,23 @@ public class BukkitPlayerProvider extends ServerPlayerProvider<BukkitPlayer, Buk
} }
@Override @Override
public CompletableFuture<IOfflinePlayer> offlinePlayer(UUID uuid) { public CompletableFuture<IOfflinePlayer> lookupOfflinePlayer(UUID uuid) {
IPlayer player = player(uuid);
if (player != null) {
return CompletableFuture.completedFuture(player);
}
return getFuture(() -> discordSRV.server().getOfflinePlayer(uuid)); return getFuture(() -> discordSRV.server().getOfflinePlayer(uuid));
} }
@SuppressWarnings("deprecation") // Shut up, I know @SuppressWarnings("deprecation") // Shut up, I know
@Override @Override
public CompletableFuture<IOfflinePlayer> offlinePlayer(String username) { public CompletableFuture<IOfflinePlayer> lookupOfflinePlayer(String username) {
IPlayer player = player(username);
if (player != null) {
return CompletableFuture.completedFuture(player);
}
return getFuture(() -> discordSRV.server().getOfflinePlayer(username)); return getFuture(() -> discordSRV.server().getOfflinePlayer(username));
} }

View File

@ -23,6 +23,7 @@ import com.discordsrv.bungee.command.game.sender.BungeeCommandSender;
import com.discordsrv.bungee.component.util.BungeeComponentUtil; import com.discordsrv.bungee.component.util.BungeeComponentUtil;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.provider.model.SkinInfo;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.ProxiedPlayer;
@ -52,6 +53,11 @@ public class BungeePlayer extends BungeeCommandSender implements IPlayer {
return commandSender.getName(); return commandSender.getName();
} }
@Override
public @Nullable SkinInfo skinInfo() {
return null;
}
@Override @Override
public @Nullable Locale locale() { public @Nullable Locale locale() {
return player.getLocale(); return player.getLocale();

View File

@ -101,6 +101,7 @@ import java.lang.reflect.Constructor;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -669,7 +670,7 @@ public abstract class AbstractDiscordSRV<
scheduler().run(() -> updateChecker.check(true)); scheduler().run(() -> updateChecker.check(true));
} }
if (initial) { if (initial) {
scheduler().runAtFixedRate(() -> updateChecker.check(false), 6L, TimeUnit.HOURS); scheduler().runAtFixedRate(() -> updateChecker.check(false), Duration.ofHours(6));
} }
if (flags.contains(ReloadFlag.LINKED_ACCOUNT_PROVIDER)) { if (flags.contains(ReloadFlag.LINKED_ACCOUNT_PROVIDER)) {

View File

@ -32,6 +32,7 @@ import net.dv8tion.jda.api.managers.channel.ChannelManager;
import net.dv8tion.jda.api.managers.channel.concrete.TextChannelManager; import net.dv8tion.jda.api.managers.channel.concrete.TextChannelManager;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.time.Duration;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -79,9 +80,8 @@ public class TimedUpdaterModule extends AbstractModule<DiscordSRV> {
futures.add(discordSRV.scheduler().runAtFixedRate( futures.add(discordSRV.scheduler().runAtFixedRate(
() -> update(updaterConfig), () -> update(updaterConfig),
firstReload ? 0 : time, firstReload ? Duration.ZERO : Duration.ofSeconds(time),
time, Duration.ofSeconds(time)
TimeUnit.SECONDS
)); ));
} }
firstReload = false; firstReload = false;

View File

@ -1,14 +1,26 @@
package com.discordsrv.common.command.combined.abstraction; 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.DiscordChatInputInteractionEvent;
import com.discordsrv.api.discord.events.interaction.command.DiscordCommandAutoCompleteInteractionEvent;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.game.abstraction.GameCommandArguments; import com.discordsrv.common.command.game.abstraction.GameCommandArguments;
import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; 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.command.game.sender.ICommandSender;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer; 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; protected final DiscordSRV discordSRV;
@ -18,7 +30,7 @@ public abstract class CombinedCommand implements GameCommandExecutor, Consumer<D
@Override @Override
public void execute(ICommandSender sender, GameCommandArguments arguments, String label) { public void execute(ICommandSender sender, GameCommandArguments arguments, String label) {
execute(new GameCommandExecution(discordSRV, sender, arguments)); execute(new GameCommandExecution(discordSRV, sender, arguments, label));
} }
@Override @Override
@ -28,4 +40,18 @@ public abstract class CombinedCommand implements GameCommandExecutor, Consumer<D
public abstract void execute(CommandExecution execution); 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

@ -1,11 +1,18 @@
package com.discordsrv.common.command.combined.abstraction; package com.discordsrv.common.command.combined.abstraction;
import com.discordsrv.common.config.messages.MessagesConfig;
import net.kyori.adventure.text.Component;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Locale;
public interface CommandExecution { public interface CommandExecution {
Locale locale();
MessagesConfig messages();
void setEphemeral(boolean ephemeral); void setEphemeral(boolean ephemeral);
String getArgument(String label); String getArgument(String label);
@ -20,5 +27,7 @@ public interface CommandExecution {
void send(Collection<Text> texts, Collection<Text> extra); void send(Collection<Text> texts, Collection<Text> extra);
void send(Component minecraft, String discord);
void runAsync(Runnable runnable); void runAsync(Runnable runnable);
} }

View File

@ -1,26 +1,57 @@
package com.discordsrv.common.command.combined.abstraction; package com.discordsrv.common.command.combined.abstraction;
import com.discordsrv.api.discord.events.interaction.command.DiscordChatInputInteractionEvent; 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.DiscordSRV;
import com.discordsrv.common.config.messages.MessagesConfig;
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.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 net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.kyori.adventure.text.Component;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.Collection; import java.util.Collection;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
public class DiscordCommandExecution implements CommandExecution { public class DiscordCommandExecution implements CommandExecution {
private final DiscordSRV discordSRV; 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 AtomicBoolean isEphemeral = new AtomicBoolean(true);
private final AtomicReference<InteractionHook> hook = new AtomicReference<>(); private final AtomicReference<InteractionHook> hook = new AtomicReference<>();
public DiscordCommandExecution(DiscordSRV discordSRV, DiscordChatInputInteractionEvent event) { public DiscordCommandExecution(DiscordSRV discordSRV, DiscordChatInputInteractionEvent event) {
this.discordSRV = discordSRV; 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
public MessagesConfig messages() {
return discordSRV.messagesConfig(locale());
} }
@Override @Override
@ -30,7 +61,7 @@ public class DiscordCommandExecution implements CommandExecution {
@Override @Override
public String getArgument(String label) { public String getArgument(String label) {
OptionMapping mapping = event.asJDA().getOption(label); OptionMapping mapping = interactionPayload.getOption(label);
return mapping != null ? mapping.getAsString() : null; return mapping != null ? mapping.getAsString() : null;
} }
@ -52,12 +83,25 @@ public class DiscordCommandExecution implements CommandExecution {
verifyStyle(builder, formats, null); verifyStyle(builder, formats, null);
} }
sendResponse(builder.toString());
}
@Override
public void send(Component minecraft, String discord) {
sendResponse(discord);
}
private void sendResponse(String content) {
if (replyCallback == null) {
throw new IllegalStateException("May not be used on auto completions");
}
InteractionHook interactionHook = hook.get(); InteractionHook interactionHook = hook.get();
boolean ephemeral = isEphemeral.get(); boolean ephemeral = isEphemeral.get();
if (interactionHook != null) { if (interactionHook != null) {
interactionHook.sendMessage(builder.toString()).setEphemeral(ephemeral).queue(); interactionHook.sendMessage(content).setEphemeral(ephemeral).queue();
} else { } else {
event.asJDA().reply(builder.toString()).setEphemeral(ephemeral).queue(); replyCallback.reply(content).setEphemeral(ephemeral).queue();
} }
} }
@ -83,9 +127,13 @@ public class DiscordCommandExecution implements CommandExecution {
@Override @Override
public void runAsync(Runnable runnable) { public void runAsync(Runnable runnable) {
event.asJDA().deferReply(isEphemeral.get()).queue(ih -> { replyCallback.deferReply(isEphemeral.get()).queue(ih -> {
hook.set(ih); hook.set(ih);
discordSRV.scheduler().run(runnable); discordSRV.scheduler().run(runnable);
}); });
} }
public User getUser() {
return createEvent.getUser();
}
} }

View File

@ -3,6 +3,8 @@ package com.discordsrv.common.command.combined.abstraction;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.game.abstraction.GameCommandArguments; import com.discordsrv.common.command.game.abstraction.GameCommandArguments;
import com.discordsrv.common.command.game.sender.ICommandSender; import com.discordsrv.common.command.game.sender.ICommandSender;
import com.discordsrv.common.config.messages.MessagesConfig;
import com.discordsrv.common.player.IPlayer;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.TextReplacementConfig;
@ -10,6 +12,7 @@ import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEvent;
import java.util.Collection; import java.util.Collection;
import java.util.Locale;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class GameCommandExecution implements CommandExecution { public class GameCommandExecution implements CommandExecution {
@ -25,11 +28,23 @@ public class GameCommandExecution implements CommandExecution {
private final DiscordSRV discordSRV; private final DiscordSRV discordSRV;
private final ICommandSender sender; private final ICommandSender sender;
private final GameCommandArguments arguments; 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.discordSRV = discordSRV;
this.sender = sender; this.sender = sender;
this.arguments = arguments; this.arguments = arguments;
this.label = label;
}
@Override
public Locale locale() {
return sender instanceof IPlayer ? ((IPlayer) sender).locale() : null;
}
@Override
public MessagesConfig messages() {
return discordSRV.messagesConfig(locale());
} }
@Override @Override
@ -51,6 +66,11 @@ public class GameCommandExecution implements CommandExecution {
sender.sendMessage(builder.build().replaceText(URL_REPLACEMENT)); sender.sendMessage(builder.build().replaceText(URL_REPLACEMENT));
} }
@Override
public void send(Component minecraft, String discord) {
sender.sendMessage(minecraft);
}
private TextComponent.Builder render(Collection<Text> texts) { private TextComponent.Builder render(Collection<Text> texts) {
TextComponent.Builder builder = Component.text(); TextComponent.Builder builder = Component.text();
for (Text text : texts) { for (Text text : texts) {
@ -67,4 +87,12 @@ public class GameCommandExecution implements CommandExecution {
public void runAsync(Runnable runnable) { public void runAsync(Runnable runnable) {
discordSRV.scheduler().run(runnable); discordSRV.scheduler().run(runnable);
} }
public ICommandSender getSender() {
return sender;
}
public String getLabel() {
return label;
}
} }

View File

@ -18,8 +18,8 @@
package com.discordsrv.common.command.combined.commands; package com.discordsrv.common.command.combined.commands;
import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.command.CommandOption; 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.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.combined.abstraction.CombinedCommand; import com.discordsrv.common.command.combined.abstraction.CombinedCommand;
@ -31,6 +31,7 @@ import com.discordsrv.common.paste.Paste;
import com.discordsrv.common.paste.PasteService; import com.discordsrv.common.paste.PasteService;
import com.discordsrv.common.paste.service.AESEncryptedPasteService; import com.discordsrv.common.paste.service.AESEncryptedPasteService;
import com.discordsrv.common.paste.service.BytebinPasteService; import com.discordsrv.common.paste.service.BytebinPasteService;
import com.discordsrv.common.permission.Permission;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -53,7 +54,7 @@ public class DebugCommand extends CombinedCommand {
if (GAME == null) { if (GAME == null) {
DebugCommand command = getInstance(discordSRV); DebugCommand command = getInstance(discordSRV);
GAME = GameCommand.literal("debug") GAME = GameCommand.literal("debug")
.requiredPermission("discordsrv.admin.debug") .requiredPermission(Permission.COMMAND_DEBUG)
.executor(command) .executor(command)
.then( .then(
GameCommand.stringWord("format") GameCommand.stringWord("format")

View File

@ -0,0 +1,262 @@
package com.discordsrv.common.command.combined.commands;
import com.discordsrv.api.discord.entity.DiscordUser;
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.api.placeholder.provider.SinglePlaceholder;
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.combined.abstraction.Text;
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.config.messages.MessagesConfig;
import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.linking.LinkProvider;
import com.discordsrv.common.linking.LinkStore;
import com.discordsrv.common.logging.Logger;
import com.discordsrv.common.logging.NamedLogger;
import com.discordsrv.common.permission.Permission;
import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.IPlayer;
import com.github.benmanes.caffeine.cache.Cache;
import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.commons.lang3.StringUtils;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
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 Logger logger;
private final Cache<UUID, Boolean> linkCheckRateLimit;
public LinkInitCommand(DiscordSRV discordSRV) {
super(discordSRV);
this.discordSRV = discordSRV;
this.logger = new NamedLogger(discordSRV, "LINK_COMMAND");
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 {
sender.sendMessage(discordSRV.messagesConfig(sender).pleaseSpecifyPlayerAndUserToLink.asComponent());
}
return;
}
if (!sender.hasPermission(Permission.COMMAND_LINK_OTHER)) {
sender.sendMessage(discordSRV.messagesConfig(sender).noPermission.asComponent());
return;
}
}
LinkProvider linkProvider = discordSRV.linkProvider();
if (!(linkProvider instanceof LinkStore)) {
execution.send(new Text("Cannot create links using this link provider").withGameColor(NamedTextColor.DARK_RED));
return;
}
CompletableFuture<UUID> playerUUIDFuture = CommandUtil.lookupPlayer(discordSRV, logger, execution, false, playerArgument, null);
CompletableFuture<Long> userIdFuture = CommandUtil.lookupUser(discordSRV, logger, execution, false, userArgument, null);
playerUUIDFuture.whenComplete((playerUUID, __) -> userIdFuture.whenComplete((userId, ___) -> {
if (playerUUID == null) {
execution.send(
execution.messages().minecraft.playerNotFound.asComponent(),
execution.messages().discord.playerNotFound
);
return;
}
if (userId == null) {
execution.send(
execution.messages().minecraft.userNotFound.asComponent(),
execution.messages().discord.userNotFound
);
return;
}
CompletableFuture<IOfflinePlayer> playerFuture = CompletableFutureUtil.timeout(
discordSRV,
discordSRV.playerProvider().lookupOfflinePlayer(playerUUID),
Duration.ofSeconds(5)
);
CompletableFuture<DiscordUser> userFuture = CompletableFutureUtil.timeout(
discordSRV,
discordSRV.discordAPI().retrieveUserById(userId),
Duration.ofSeconds(5)
);
linkProvider.queryUserId(playerUUID).whenComplete((linkedUser, t) -> {
if (t != null) {
logger.error("Failed to check linking status", t);
execution.send(
execution.messages().minecraft.unableToCheckLinkingStatus.asComponent(),
execution.messages().discord.unableToCheckLinkingStatus
);
return;
}
if (linkedUser.isPresent()) {
execution.send(
execution.messages().minecraft.playerAlreadyLinked3rd.asComponent(),
execution.messages().discord.playerAlreadyLinked3rd
);
return;
}
linkProvider.queryPlayerUUID(userId).whenComplete((linkedPlayer, t2) -> {
if (t2 != null) {
logger.error("Failed to check linking status", t2);
execution.send(
execution.messages().minecraft.unableToCheckLinkingStatus.asComponent(),
execution.messages().discord.unableToCheckLinkingStatus
);
return;
}
if (linkedPlayer.isPresent()) {
execution.send(
execution.messages().minecraft.userAlreadyLinked3rd.asComponent(),
execution.messages().discord.userAlreadyLinked3rd
);
return;
}
((LinkStore) linkProvider).createLink(playerUUID, userId).whenComplete((v, t3) -> {
if (t3 != null) {
logger.error("Failed to check linking status", t3);
execution.send(
execution.messages().minecraft.unableToLinkAtThisTime.asComponent(),
execution.messages().discord.unableToCheckLinkingStatus
);
return;
}
userFuture.whenComplete((user, ____) -> playerFuture.whenComplete((player, _____) -> execution.send(
ComponentUtil.fromAPI(
execution.messages().minecraft.nowLinked3rd.textBuilder()
.applyPlaceholderService()
.addContext(user, player)
.addPlaceholder("user_id", userId)
.addPlaceholder("player_uuid", playerUUID)
.build()
),
discordSRV.placeholderService().replacePlaceholders(
execution.messages().discord.nowLinked3rd,
user,
player,
new SinglePlaceholder("user_id", userId),
new SinglePlaceholder("player_uuid", playerUUID)
)
)));
});
});
});
}));
}
private void startLinking(IPlayer player, String label) {
MessagesConfig.Minecraft messages = discordSRV.messagesConfig(player);
LinkProvider linkProvider = discordSRV.linkProvider();
if (linkProvider.getCachedUserId(player.uniqueId()).isPresent()) {
player.sendMessage(messages.alreadyLinked1st.asComponent());
return;
}
if (linkCheckRateLimit.getIfPresent(player.uniqueId()) != null) {
player.sendMessage(messages.pleaseWaitBeforeRunningThatCommandAgain.asComponent());
return;
}
linkCheckRateLimit.put(player.uniqueId(), true);
player.sendMessage(discordSRV.messagesConfig(player).checkingLinkStatus.asComponent());
linkProvider.queryUserId(player.uniqueId(), true).whenComplete((userId, t1) -> {
if (t1 != null) {
logger.error("Failed to check linking status", t1);
player.sendMessage(messages.unableToLinkAtThisTime.asComponent());
return;
}
if (userId.isPresent()) {
player.sendMessage(messages.nowLinked1st.asComponent());
return;
}
linkProvider.getLinkingInstructions(player, label).whenComplete((comp, t2) -> {
if (t2 != null) {
logger.error("Failed to link account", t2);
player.sendMessage(messages.unableToLinkAtThisTime.asComponent());
return;
}
player.sendMessage(ComponentUtil.fromAPI(comp));
});
});
}
}

View File

@ -0,0 +1,222 @@
package com.discordsrv.common.command.combined.commands;
import com.discordsrv.api.discord.entity.DiscordUser;
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.api.placeholder.provider.SinglePlaceholder;
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.game.abstraction.GameCommand;
import com.discordsrv.common.command.util.CommandUtil;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.logging.Logger;
import com.discordsrv.common.logging.NamedLogger;
import com.discordsrv.common.permission.Permission;
import com.discordsrv.common.player.IOfflinePlayer;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
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.stringGreedy("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;
}
private final Logger logger;
public LinkedCommand(DiscordSRV discordSRV) {
super(discordSRV);
this.logger = new NamedLogger(discordSRV, "LINKED_COMMAND");
}
@Override
public void execute(CommandExecution execution) {
execution.setEphemeral(true);
execution.runAsync(() -> CommandUtil.lookupTarget(discordSRV, logger, execution, true, Permission.COMMAND_LINKED_OTHER)
.whenComplete((result, t) -> {
if (t != null) {
logger.error("Failed to execute linked command", t);
return;
}
if (result.isValid()) {
processResult(result, execution);
}
})
);
}
private void processResult(CommandUtil.TargetLookupResult result, CommandExecution execution) {
if (result.isPlayer()) {
UUID playerUUID = result.getPlayerUUID();
CompletableFuture<IOfflinePlayer> playerFuture = CompletableFutureUtil.timeout(
discordSRV,
discordSRV.playerProvider().lookupOfflinePlayer(playerUUID),
Duration.ofSeconds(5)
);
discordSRV.linkProvider().getUserId(playerUUID).whenComplete((optUserId, t) -> {
if (t != null) {
logger.error("Failed to check linking status during linked command", t);
execution.send(
execution.messages().minecraft.unableToCheckLinkingStatus.asComponent(),
execution.messages().discord.unableToCheckLinkingStatus
);
return;
}
if (!optUserId.isPresent()) {
playerFuture.whenComplete((player, ___) -> execution.send(
ComponentUtil.fromAPI(
execution.messages().minecraft.minecraftPlayerUnlinked
.textBuilder()
.applyPlaceholderService()
.addContext(player)
.addPlaceholder("player_uuid", playerUUID)
.build()
),
discordSRV.placeholderService().replacePlaceholders(
execution.messages().discord.minecraftPlayerUnlinked,
player,
new SinglePlaceholder("player_uuid", playerUUID)
)
));
return;
}
long userId = optUserId.get();
CompletableFuture<DiscordUser> userFuture = CompletableFutureUtil.timeout(
discordSRV,
discordSRV.discordAPI().retrieveUserById(userId),
Duration.ofSeconds(5)
);
playerFuture.whenComplete((player, __) -> userFuture.whenComplete((user, ___) -> execution.send(
ComponentUtil.fromAPI(
execution.messages().minecraft.minecraftPlayerLinkedTo
.textBuilder()
.applyPlaceholderService()
.addContext(player, user)
.addPlaceholder("player_uuid", playerUUID)
.addPlaceholder("user_id", userId)
.build()
),
discordSRV.placeholderService().replacePlaceholders(
execution.messages().discord.minecraftPlayerLinkedTo,
player,
user,
new SinglePlaceholder("player_uuid", playerUUID),
new SinglePlaceholder("user_id", userId)
)
)));
});
} else {
long userId = result.getUserId();
CompletableFuture<DiscordUser> userFuture = CompletableFutureUtil.timeout(
discordSRV,
discordSRV.discordAPI().retrieveUserById(userId),
Duration.ofSeconds(5)
);
discordSRV.linkProvider().getPlayerUUID(userId).whenComplete((optPlayerUUID, t) -> {
if (t != null) {
logger.error("Failed to check linking status during linked command", t);
execution.send(
execution.messages().minecraft.unableToCheckLinkingStatus.asComponent(),
execution.messages().discord.unableToCheckLinkingStatus
);
return;
}
if (!optPlayerUUID.isPresent()) {
userFuture.whenComplete((user, ___) -> execution.send(
ComponentUtil.fromAPI(
execution.messages().minecraft.discordUserUnlinked
.textBuilder()
.applyPlaceholderService()
.addContext(user)
.addPlaceholder("user_id", userId)
.build()
),
discordSRV.placeholderService().replacePlaceholders(
execution.messages().discord.discordUserUnlinked,
user,
new SinglePlaceholder("user_id", userId)
)
));
return;
}
UUID playerUUID = optPlayerUUID.get();
CompletableFuture<IOfflinePlayer> playerFuture = CompletableFutureUtil.timeout(
discordSRV,
discordSRV.playerProvider().lookupOfflinePlayer(playerUUID),
Duration.ofSeconds(5)
);
userFuture.whenComplete((user, __) -> playerFuture.whenComplete((player, ___) -> execution.send(
ComponentUtil.fromAPI(
execution.messages().minecraft.discordUserLinkedTo
.textBuilder()
.applyPlaceholderService()
.addContext(user, player)
.addPlaceholder("user_id", userId)
.addPlaceholder("player_uuid", playerUUID)
.build()
),
discordSRV.placeholderService().replacePlaceholders(
execution.messages().discord.discordUserLinkedTo,
user,
player,
new SinglePlaceholder("user_id", userId),
new SinglePlaceholder("player_uuid", playerUUID)
)
)));
});
}
}
}

View File

@ -12,6 +12,7 @@ import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.groupsync.GroupSyncModule; import com.discordsrv.common.groupsync.GroupSyncModule;
import com.discordsrv.common.groupsync.enums.GroupSyncCause; import com.discordsrv.common.groupsync.enums.GroupSyncCause;
import com.discordsrv.common.groupsync.enums.GroupSyncResult; import com.discordsrv.common.groupsync.enums.GroupSyncResult;
import com.discordsrv.common.permission.Permission;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@ -34,7 +35,7 @@ public class ResyncCommand extends CombinedCommand {
if (GAME == null) { if (GAME == null) {
ResyncCommand command = getInstance(discordSRV); ResyncCommand command = getInstance(discordSRV);
GAME = GameCommand.literal("resync") GAME = GameCommand.literal("resync")
.requiredPermission("discordsrv.admin.resync") .requiredPermission(Permission.COMMAND_RESYNC)
.executor(command); .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.combined.abstraction.Text;
import com.discordsrv.common.command.game.abstraction.GameCommand; import com.discordsrv.common.command.game.abstraction.GameCommand;
import com.discordsrv.common.debug.data.VersionInfo; import com.discordsrv.common.debug.data.VersionInfo;
import com.discordsrv.common.permission.Permission;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -48,7 +49,7 @@ public class VersionCommand extends CombinedCommand {
if (GAME == null) { if (GAME == null) {
VersionCommand command = getInstance(discordSRV); VersionCommand command = getInstance(discordSRV);
GAME = GameCommand.literal("version") GAME = GameCommand.literal("version")
.requiredPermission("discordsrv.admin.version") .requiredPermission(Permission.COMMAND_VERSION)
.executor(command); .executor(command);
} }

View File

@ -3,11 +3,10 @@ package com.discordsrv.common.command.discord.commands;
import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand; import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.combined.commands.DebugCommand; import com.discordsrv.common.command.combined.commands.*;
import com.discordsrv.common.command.combined.commands.ResyncCommand;
import com.discordsrv.common.command.combined.commands.VersionCommand;
import com.discordsrv.common.command.discord.commands.subcommand.ExecuteCommand; import com.discordsrv.common.command.discord.commands.subcommand.ExecuteCommand;
import com.discordsrv.common.config.main.DiscordCommandConfig; import com.discordsrv.common.config.main.DiscordCommandConfig;
import com.discordsrv.common.linking.LinkStore;
public class DiscordSRVDiscordCommand { public class DiscordSRVDiscordCommand {
@ -22,11 +21,15 @@ public class DiscordSRVDiscordCommand {
DiscordCommand.ChatInputBuilder builder = DiscordCommand.chatInput(IDENTIFIER, "discordsrv", "DiscordSRV related commands") DiscordCommand.ChatInputBuilder builder = DiscordCommand.chatInput(IDENTIFIER, "discordsrv", "DiscordSRV related commands")
.addSubCommand(DebugCommand.getDiscord(discordSRV)) .addSubCommand(DebugCommand.getDiscord(discordSRV))
.addSubCommand(VersionCommand.getDiscord(discordSRV)) .addSubCommand(VersionCommand.getDiscord(discordSRV))
.addSubCommand(ResyncCommand.getDiscord(discordSRV)); .addSubCommand(ResyncCommand.getDiscord(discordSRV))
.addSubCommand(LinkedCommand.getDiscord(discordSRV));
if (config.execute.enabled) { if (config.execute.enabled) {
builder = builder.addSubCommand(ExecuteCommand.get(discordSRV)); builder = builder.addSubCommand(ExecuteCommand.get(discordSRV));
} }
if (discordSRV.linkProvider() instanceof LinkStore) {
builder = builder.addSubCommand(LinkInitCommand.getDiscord(discordSRV));
}
INSTANCE = builder INSTANCE = builder
.setGuildOnly(false) .setGuildOnly(false)

View File

@ -18,6 +18,7 @@ import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -207,7 +208,7 @@ public class ExecuteCommand implements Consumer<DiscordChatInputInteractionEvent
synchronized (queued) { synchronized (queued) {
queued.offer(component); queued.offer(component);
if (future == null) { if (future == null) {
future = discordSRV.scheduler().runLater(this::send, 500); future = discordSRV.scheduler().runLater(this::send, Duration.ofMillis(500));
} }
} }
} }

View File

@ -20,9 +20,9 @@ package com.discordsrv.common.command.game;
import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.common.DiscordSRV; 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.abstraction.GameCommand;
import com.discordsrv.common.command.game.commands.DiscordSRVGameCommand; 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.command.game.handler.ICommandHandler;
import com.discordsrv.common.config.main.GameCommandConfig; import com.discordsrv.common.config.main.GameCommandConfig;
import com.discordsrv.common.module.type.AbstractModule; import com.discordsrv.common.module.type.AbstractModule;
@ -43,7 +43,7 @@ public class GameCommandModule extends AbstractModule<DiscordSRV> {
super(discordSRV); super(discordSRV);
this.primaryCommand = DiscordSRVGameCommand.get(discordSRV, "discordsrv"); this.primaryCommand = DiscordSRVGameCommand.get(discordSRV, "discordsrv");
this.discordAlias = DiscordSRVGameCommand.get(discordSRV, "discord"); this.discordAlias = DiscordSRVGameCommand.get(discordSRV, "discord");
this.linkCommand = LinkCommand.get(discordSRV); this.linkCommand = LinkInitCommand.getGame(discordSRV);
registerCommand(primaryCommand); 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.command.game.sender.ICommandSender;
import com.discordsrv.common.function.CheckedFunction; import com.discordsrv.common.function.CheckedFunction;
import com.discordsrv.common.permission.Permission;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@ -176,6 +177,10 @@ public class GameCommand {
return redirection; return redirection;
} }
public GameCommand requiredPermission(Permission permission) {
return requiredPermission(permission.permission());
}
public GameCommand requiredPermission(String permission) { public GameCommand requiredPermission(String permission) {
if (redirection != null) { if (redirection != null) {
throw new IllegalStateException("Cannot required permissions on a node with a redirection"); 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.api.component.MinecraftComponent;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.combined.commands.DebugCommand; import com.discordsrv.common.command.combined.commands.*;
import com.discordsrv.common.command.combined.commands.ResyncCommand;
import com.discordsrv.common.command.combined.commands.VersionCommand;
import com.discordsrv.common.command.game.abstraction.GameCommand; import com.discordsrv.common.command.game.abstraction.GameCommand;
import com.discordsrv.common.command.game.abstraction.GameCommandArguments; import com.discordsrv.common.command.game.abstraction.GameCommandArguments;
import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; 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.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.commands.subcommand.reload.ReloadCommand;
import com.discordsrv.common.command.game.sender.ICommandSender; import com.discordsrv.common.command.game.sender.ICommandSender;
import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.permission.Permission;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -46,13 +44,14 @@ public class DiscordSRVGameCommand implements GameCommandExecutor {
} }
return INSTANCES.computeIfAbsent(alias, key -> return INSTANCES.computeIfAbsent(alias, key ->
GameCommand.literal(alias) GameCommand.literal(alias)
.requiredPermission("discordsrv.player.command") .requiredPermission(Permission.COMMAND_ROOT)
.executor(COMMAND) .executor(COMMAND)
.then(BroadcastCommand.discord(discordSRV)) .then(BroadcastCommand.discord(discordSRV))
.then(BroadcastCommand.minecraft(discordSRV)) .then(BroadcastCommand.minecraft(discordSRV))
.then(BroadcastCommand.json(discordSRV)) .then(BroadcastCommand.json(discordSRV))
.then(DebugCommand.getGame(discordSRV)) .then(DebugCommand.getGame(discordSRV))
.then(LinkCommand.get(discordSRV)) .then(LinkInitCommand.getGame(discordSRV))
.then(LinkedCommand.getGame(discordSRV))
.then(ReloadCommand.get(discordSRV)) .then(ReloadCommand.get(discordSRV))
.then(ResyncCommand.getGame(discordSRV)) .then(ResyncCommand.getGame(discordSRV))
.then(VersionCommand.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.component.util.ComponentUtil;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.IChannelConfig; import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.permission.Permission;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
@ -69,7 +70,7 @@ public abstract class BroadcastCommand implements GameCommandExecutor, GameComma
BroadcastCommand command = executor.get(); BroadcastCommand command = executor.get();
consumer.accept( consumer.accept(
GameCommand.literal(label) GameCommand.literal(label)
.requiredPermission("discordsrv.admin.broadcast") .requiredPermission(Permission.COMMAND_BROADCAST)
.then( .then(
GameCommand.string("channel") GameCommand.string("channel")
.suggester(command) .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.GameCommandExecutor;
import com.discordsrv.common.command.game.abstraction.GameCommandSuggester; import com.discordsrv.common.command.game.abstraction.GameCommandSuggester;
import com.discordsrv.common.command.game.sender.ICommandSender; import com.discordsrv.common.command.game.sender.ICommandSender;
import com.discordsrv.common.permission.Permission;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.ClickEvent;
@ -44,7 +45,7 @@ public class ReloadCommand implements GameCommandExecutor, GameCommandSuggester
if (INSTANCE == null) { if (INSTANCE == null) {
ReloadCommand cmd = new ReloadCommand(discordSRV); ReloadCommand cmd = new ReloadCommand(discordSRV);
INSTANCE = GameCommand.literal("reload") INSTANCE = GameCommand.literal("reload")
.requiredPermission("discordsrv.admin.reload") .requiredPermission(Permission.COMMAND_RELOAD)
.executor(cmd) .executor(cmd)
.then( .then(
GameCommand.stringGreedy("flags") GameCommand.stringGreedy("flags")

View File

@ -19,10 +19,15 @@
package com.discordsrv.common.command.game.sender; package com.discordsrv.common.command.game.sender;
import com.discordsrv.common.command.game.executor.CommandExecutor; import com.discordsrv.common.command.game.executor.CommandExecutor;
import com.discordsrv.common.permission.Permission;
import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.audience.ForwardingAudience;
public interface ICommandSender extends ForwardingAudience.Single, CommandExecutor { public interface ICommandSender extends ForwardingAudience.Single, CommandExecutor {
default boolean hasPermission(Permission permission) {
return hasPermission(permission.permission());
}
boolean hasPermission(String permission); boolean hasPermission(String permission);
} }

View File

@ -0,0 +1,241 @@
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.game.sender.ICommandSender;
import com.discordsrv.common.config.messages.MessagesConfig;
import com.discordsrv.common.logging.Logger;
import com.discordsrv.common.permission.Permission;
import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.uuid.util.UUIDUtil;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.utils.MiscUtil;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public final class CommandUtil {
private CommandUtil() {}
public static CompletableFuture<UUID> lookupPlayer(
DiscordSRV discordSRV,
Logger logger,
CommandExecution execution,
boolean selfPermitted,
String target,
@Nullable Permission otherPermission
) {
return lookupTarget(discordSRV, logger, execution, target, selfPermitted, true, false, otherPermission)
.thenApply((result) -> {
if (result != null && result.isValid()) {
return result.getPlayerUUID();
}
return null;
});
}
public static CompletableFuture<Long> lookupUser(
DiscordSRV discordSRV,
Logger logger,
CommandExecution execution,
boolean selfPermitted,
String target,
@Nullable Permission otherPermission
) {
return lookupTarget(discordSRV, logger, execution, target, selfPermitted, false, true, otherPermission)
.thenApply(result -> {
if (result != null && result.isValid()) {
return result.getUserId();
}
return null;
});
}
public static CompletableFuture<TargetLookupResult> lookupTarget(
DiscordSRV discordSRV,
Logger logger,
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, logger, execution, target, selfPermitted, true, true, otherPermission);
}
private static CompletableFuture<TargetLookupResult> lookupTarget(
DiscordSRV discordSRV,
Logger logger,
CommandExecution execution,
String target,
boolean selfPermitted,
boolean lookupPlayer,
boolean lookupUser,
@Nullable Permission otherPermission
) {
MessagesConfig messages = discordSRV.messagesConfig(execution.locale());
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 CompletableFuture.completedFuture(TargetLookupResult.INVALID);
}
} else if (sender instanceof IPlayer && selfPermitted && lookupPlayer) {
target = ((IPlayer) sender).uniqueId().toString();
}
} else if (execution instanceof DiscordCommandExecution) {
if (target == null) {
if (selfPermitted && lookupUser) {
target = Long.toUnsignedString(((DiscordCommandExecution) execution).getUser().getIdLong());
} else {
execution.send(
messages.minecraft.pleaseSpecifyUser.asComponent(),
messages.discord.pleaseSpecifyUser
);
return CompletableFuture.completedFuture(TargetLookupResult.INVALID);
}
}
} else {
throw new IllegalStateException("Unexpected CommandExecution");
}
if (target == null) {
return CompletableFuture.completedFuture(requireTarget(execution, lookupUser, lookupPlayer, messages));
}
if (lookupUser) {
if (target.matches("\\d{17,22}")) {
// Discord user id
long id;
try {
id = MiscUtil.parseLong(target);
} catch (IllegalArgumentException ignored) {
execution.send(
messages.minecraft.userNotFound.asComponent(),
messages.discord.userNotFound
);
return CompletableFuture.completedFuture(TargetLookupResult.INVALID);
}
return CompletableFuture.completedFuture(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 CompletableFuture.completedFuture(new TargetLookupResult(true, null, users.get(0).getIdLong()));
}
}
}
}
if (lookupPlayer) {
UUID uuid;
boolean shortUUID;
if ((shortUUID = target.length() == 32) || target.length() == 36) {
// Player UUID
try {
if (shortUUID) {
uuid = UUIDUtil.fromShort(target);
} else {
uuid = UUID.fromString(target);
}
} catch (IllegalArgumentException ignored) {
execution.send(
messages.minecraft.playerNotFound.asComponent(),
messages.discord.playerNotFound
);
return CompletableFuture.completedFuture(TargetLookupResult.INVALID);
}
return CompletableFuture.completedFuture(new TargetLookupResult(true, uuid, 0L));
} else if (target.matches("[a-zA-Z0-9_]{1,16}")) {
// Player name
IPlayer playerByName = discordSRV.playerProvider().player(target);
if (playerByName != null) {
uuid = playerByName.uniqueId();
} else {
return discordSRV.playerProvider().lookupOfflinePlayer(target)
.thenApply(offlinePlayer -> new TargetLookupResult(true, offlinePlayer.uniqueId(), 0L))
.exceptionally(t -> {
logger.error("Failed to lookup offline player by username", t);
return TargetLookupResult.INVALID;
});
}
return CompletableFuture.completedFuture(new TargetLookupResult(true, uuid, 0L));
}
}
return CompletableFuture.completedFuture(requireTarget(execution, lookupUser, lookupPlayer, messages));
}
private static TargetLookupResult requireTarget(CommandExecution execution, boolean lookupUser, boolean lookupPlayer, MessagesConfig messages) {
if (lookupPlayer && lookupUser) {
execution.send(
messages.minecraft.pleaseSpecifyPlayerOrUser.asComponent(),
messages.discord.pleaseSpecifyPlayerOrUser
);
return TargetLookupResult.INVALID;
} else if (lookupPlayer) {
execution.send(
messages.minecraft.pleaseSpecifyPlayer.asComponent(),
messages.discord.pleaseSpecifyPlayer
);
return TargetLookupResult.INVALID;
} else if (lookupUser) {
execution.send(
messages.minecraft.pleaseSpecifyUser.asComponent(),
messages.discord.pleaseSpecifyUser
);
return TargetLookupResult.INVALID;
} else {
throw new IllegalStateException("lookupPlayer & lookupUser are false");
}
}
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.Config;
import com.discordsrv.common.config.configurate.annotation.Constants; import com.discordsrv.common.config.configurate.annotation.Constants;
import com.discordsrv.common.config.configurate.annotation.Untranslated;
import com.discordsrv.common.config.helper.MinecraftMessage; import com.discordsrv.common.config.helper.MinecraftMessage;
import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ConfigSerializable @ConfigSerializable
public class MessagesConfig implements Config { public class MessagesConfig implements Config {
@ -19,30 +21,94 @@ public class MessagesConfig implements Config {
@ConfigSerializable @ConfigSerializable
public static class Minecraft { public static class Minecraft {
private static final String ERROR_COLOR = "&c";
private static final String SUCCESS_COLOR = "&a";
private static final String NEUTRAL_COLOR = "&b";
private MinecraftMessage make(String rawFormat) { private MinecraftMessage make(String rawFormat) {
return new MinecraftMessage(rawFormat); return new MinecraftMessage(rawFormat);
} }
@Constants("&c") @Comment("Generic")
@Constants(ERROR_COLOR)
public MinecraftMessage noPermission = make("%1Sorry, but you do not have permission to use that command");
@Constants(ERROR_COLOR)
public MinecraftMessage pleaseSpecifyPlayer = make("%1Please specify the Minecraft player");
@Constants(ERROR_COLOR)
public MinecraftMessage pleaseSpecifyUser = make("%1Please specify the Discord user");
@Constants(ERROR_COLOR)
public MinecraftMessage pleaseSpecifyPlayerOrUser = make("%1Please specify the Minecraft player or Discord user");
@Constants(ERROR_COLOR)
public MinecraftMessage playerNotFound = make("%1Minecraft player not found");
@Constants(ERROR_COLOR)
public MinecraftMessage userNotFound = make("%1Discord user not found");
@Constants(ERROR_COLOR)
public MinecraftMessage unableToCheckLinkingStatus = make("%1Unable to check linking status, please try again later"); public MinecraftMessage unableToCheckLinkingStatus = make("%1Unable to check linking status, please try again later");
@Constants("&c")
public MinecraftMessage alreadyLinked = make("%1You are already linked");
@Constants("&c")
public MinecraftMessage pleaseWaitBeforeRunningThatCommandAgain = make("%1Please wait before running that command again");
@Constants("&c")
public MinecraftMessage unableToLinkAtThisTime = make("%1Unable to check linking status, please try again later");
@Constants("&b")
public MinecraftMessage checkingLinkStatus = make("%1Checking linking status...");
@Constants("&b") @Untranslated(Untranslated.Type.COMMENT)
public MinecraftMessage youAreNowLinked = make("%1You are now linked!"); @Comment("/discord link")
@Constants(ERROR_COLOR)
public MinecraftMessage alreadyLinked1st = make("%1You are already linked");
@Constants(ERROR_COLOR)
public MinecraftMessage pleaseSpecifyPlayerAndUserToLink = make("%1Please specify the Minecraft player and the Discord user to link");
@Constants(ERROR_COLOR)
public MinecraftMessage playerAlreadyLinked3rd = make("%1That player is already linked");
@Constants(ERROR_COLOR)
public MinecraftMessage userAlreadyLinked3rd = make("%1That player is already linked");
@Constants(ERROR_COLOR)
public MinecraftMessage pleaseWaitBeforeRunningThatCommandAgain = make("%1Please wait before running that command again");
@Constants(ERROR_COLOR)
public MinecraftMessage unableToLinkAtThisTime = make("%1Unable to check linking status, please try again later");
@Constants(NEUTRAL_COLOR)
public MinecraftMessage checkingLinkStatus = make("%1Checking linking status...");
@Constants(SUCCESS_COLOR)
public MinecraftMessage nowLinked1st = make("%1You are now linked!");
@Constants({
SUCCESS_COLOR,
NEUTRAL_COLOR,
SUCCESS_COLOR + "[hover:show_text:%player_uuid%][click:copy_to_clipboard:%player_uuid%]%player_name|text:'<Unknown>'%[click][hover]" + NEUTRAL_COLOR,
SUCCESS_COLOR + "[hover:show_text:%user_id%][click:copy_to_clipboard:%user_id%]@%user_name%[click][hover]" + NEUTRAL_COLOR
})
public MinecraftMessage nowLinked3rd = make("%1Link created successfully %2(%3 and %4)");
@Constants({
NEUTRAL_COLOR,
"&f[click:open_url:%minecraftauth_link%][hover:show_text:Click to open]%minecraftauth_link_simple%[click]" + NEUTRAL_COLOR,
"&fMinecraftAuth"
})
public MinecraftMessage minecraftAuthLinking = make("%1Please visit %2 to link your account through %4");
@Untranslated(Untranslated.Type.COMMENT)
@Comment("/discord linked")
@Constants({
SUCCESS_COLOR + "[hover:show_text:%user_id%][click:copy_to_clipboard:%user_id%]@%user_name%[click][hover]",
NEUTRAL_COLOR,
SUCCESS_COLOR + "[hover:show_text:%player_uuid%][click:copy_to_clipboard:%player_uuid%]%player_name|text:'<Unknown>'%[click][hover]"
})
public MinecraftMessage discordUserLinkedTo = make("%1 %2is linked to %3");
@Untranslated(Untranslated.Type.COMMENT)
@Comment("/discord linked")
@Constants({
SUCCESS_COLOR + "[hover:show_text:%user_id%][click:copy_to_clipboard:%user_id%]@%user_name%[click][hover]",
NEUTRAL_COLOR,
ERROR_COLOR
})
public MinecraftMessage discordUserUnlinked = make("%1 %2is %3unlinked");
@Constants({ @Constants({
"&b", SUCCESS_COLOR + "[hover:show_text:%player_uuid%][click:copy_to_clipboard:%player_uuid%]%player_name|text:'<Unknown>'%[click][hover]",
"&7[click:open_url:%minecraftauth_link%][hover:show_text:Click to open]%minecraftauth_link_simple%[click]&b", NEUTRAL_COLOR,
"&7MinecraftAuth" SUCCESS_COLOR + "[hover:show_text:%user_id%][click:copy_to_clipboard:%user_id%]@%user_name%[click][hover]"
}) })
public MinecraftMessage minecraftAuthLinking = make("%1Please visit %2 to link your account through %3"); public MinecraftMessage minecraftPlayerLinkedTo = make("%1 %2is linked to %3");
@Constants({
SUCCESS_COLOR + "[hover:show_text:%player_uuid%][click:copy_to_clipboard:%player_uuid%]%player_name|text:'<Unknown>'%[click][hover]",
NEUTRAL_COLOR,
ERROR_COLOR
})
public MinecraftMessage minecraftPlayerUnlinked = make("%1 %2is %3unlinked");
} }
public Discord discord = new Discord(); public Discord discord = new Discord();
@ -50,7 +116,65 @@ public class MessagesConfig implements Config {
@ConfigSerializable @ConfigSerializable
public static class Discord { public static class Discord {
private static final String SUCCESS_PREFIX = "";
private static final String INPUT_ERROR_PREFIX = "\uD83D\uDDD2 ";
private static final String ERROR_PREFIX = "";
@Comment("Generic")
@Constants(INPUT_ERROR_PREFIX)
public String pleaseSpecifyPlayer = "%1Please specify the Minecraft player";
@Constants(INPUT_ERROR_PREFIX)
public String pleaseSpecifyUser = "%1Please specify the Discord user";
@Constants(INPUT_ERROR_PREFIX)
public String pleaseSpecifyPlayerOrUser = "%1Please specify the Minecraft player or Discord user";
@Constants(ERROR_PREFIX)
public String playerNotFound = "%1Minecraft player not found";
@Constants(ERROR_PREFIX)
public String userNotFound = "%1Discord user not found";
@Constants(ERROR_PREFIX)
public String unableToCheckLinkingStatus = "%1Unable to check linking status, please try again later";
@Untranslated(Untranslated.Type.COMMENT)
@Comment("/discord link")
@Constants(ERROR_PREFIX)
public String playerAlreadyLinked3rd = "%1That Minecraft player is already linked";
@Constants(ERROR_PREFIX)
public String userAlreadyLinked3rd = "%1That Discord user is already linked";
@Constants({
SUCCESS_PREFIX,
"**%player_name%** (%player_uuid%)",
"**%user_name%** (%user_id%)"
})
public String nowLinked3rd = "%1Link created successfully\n%2 and %3";
@Untranslated(Untranslated.Type.COMMENT)
@Comment("/discord linked")
@Constants({
SUCCESS_PREFIX,
"**%user_name%** (<@%user_id%>)",
"**%player_name%** (%player_uuid%)"
})
public String discordUserLinkedTo = "%1%2 is linked to %3";
@Untranslated(Untranslated.Type.COMMENT)
@Comment("/discord linked")
@Constants({
ERROR_PREFIX,
"**%user_name%** (%user_id%)"
})
public String discordUserUnlinked = "%1%2 is __unlinked__";
@Constants({
SUCCESS_PREFIX,
"**%player_name%** (%player_uuid%)",
"**%user_name%** (<@%user_id%>)"
})
public String minecraftPlayerLinkedTo = "%1%2 is linked to %3";
@Constants({
ERROR_PREFIX,
"**%player_name%** (%player_uuid%)"
})
public String minecraftPlayerUnlinked = "%1%2 is __unlinked__";
} }
} }

View File

@ -27,6 +27,7 @@ import net.dv8tion.jda.api.entities.Message;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@ -196,7 +197,7 @@ public class SingleConsoleHandler {
if (config.appender.outputMode == ConsoleConfig.OutputMode.OFF) { if (config.appender.outputMode == ConsoleConfig.OutputMode.OFF) {
return; return;
} }
this.queueProcessingFuture = discordSRV.scheduler().runLater(this::processQueue, 2, TimeUnit.SECONDS); this.queueProcessingFuture = discordSRV.scheduler().runLater(this::processQueue, Duration.ofSeconds(2));
} }
private void processQueue() { private void processQueue() {

View File

@ -69,6 +69,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.time.Duration;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -303,9 +304,8 @@ public class JDAConnectionManager implements DiscordConnectionManager {
); );
this.failureCallbackFuture = discordSRV.scheduler().runAtFixedRate( this.failureCallbackFuture = discordSRV.scheduler().runAtFixedRate(
this::checkDefaultFailureCallback, this::checkDefaultFailureCallback,
30, Duration.ofSeconds(30),
120, Duration.ofSeconds(120)
TimeUnit.SECONDS
); );
MemberCachingConfig memberCachingConfig = discordSRV.config().memberCaching; MemberCachingConfig memberCachingConfig = discordSRV.config().memberCaching;

View File

@ -18,10 +18,15 @@
package com.discordsrv.common.future.util; package com.discordsrv.common.future.util;
import com.discordsrv.common.DiscordSRV;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeoutException;
public final class CompletableFutureUtil { public final class CompletableFutureUtil {
@ -58,4 +63,17 @@ public final class CompletableFutureUtil {
}); });
return future; return future;
} }
public static <T> CompletableFuture<T> timeout(DiscordSRV discordSRV, CompletableFuture<T> future, Duration timeout) {
ScheduledFuture<?> scheduledFuture = discordSRV.scheduler().runLater(() -> {
if (!future.isDone()) {
future.completeExceptionally(new TimeoutException());
}
}, timeout);
return future.whenComplete((__, t) -> {
if (t == null) {
scheduledFuture.cancel(false);
}
});
}
} }

View File

@ -42,6 +42,7 @@ import com.github.benmanes.caffeine.cache.Cache;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -127,9 +128,8 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
int cycleTime = timer.cycleTime; int cycleTime = timer.cycleTime;
future = discordSRV.scheduler().runAtFixedRate( future = discordSRV.scheduler().runAtFixedRate(
() -> resyncPair(pair, GroupSyncCause.TIMER), () -> resyncPair(pair, GroupSyncCause.TIMER),
cycleTime, Duration.ofMinutes(cycleTime),
cycleTime, Duration.ofMinutes(cycleTime)
TimeUnit.MINUTES
); );
} }

View File

@ -0,0 +1,45 @@
package com.discordsrv.common.http.util;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.exception.MessageException;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
public final class HttpUtil {
private HttpUtil() {}
public static ResponseBody checkIfResponseSuccessful(Request request, Response response) throws IOException {
ResponseBody responseBody = response.body();
if (responseBody == null || !response.isSuccessful()) {
String responseString = responseBody == null
? "response body is null"
: StringUtils.substring(responseBody.string(), 0, 500);
throw new MessageException("Request to " + request.url().host() + " failed: " + response.code() + ": " + responseString);
}
return responseBody;
}
public static <T> CompletableFuture<T> readJson(DiscordSRV discordSRV, Request request, Class<T> type) {
CompletableFuture<T> future = new CompletableFuture<>();
discordSRV.scheduler().run(() -> {
try (Response response = discordSRV.httpClient().newCall(request).execute()) {
ResponseBody responseBody = checkIfResponseSuccessful(request, response);
T result = discordSRV.json().readValue(responseBody.byteStream(), type);
if (result == null) {
throw new MessageException("Response json cannot be parsed");
}
future.complete(result);
} catch (Throwable t) {
future.completeExceptionally(t);
}
});
return future;
}
}

View File

@ -30,7 +30,11 @@ import java.util.concurrent.CompletableFuture;
public interface LinkProvider { 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) { default CompletableFuture<Optional<Long>> getUserId(@NotNull UUID playerUUID) {
Optional<Long> userId = getCachedUserId(playerUUID); Optional<Long> userId = getCachedUserId(playerUUID);
@ -44,7 +48,11 @@ public interface LinkProvider {
return Optional.empty(); 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) { default CompletableFuture<Optional<UUID>> getPlayerUUID(long userId) {
Optional<UUID> playerUUID = getCachedPlayerUUID(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.DiscordSRV;
import com.discordsrv.common.linking.LinkProvider; import com.discordsrv.common.linking.LinkProvider;
import com.discordsrv.common.player.event.PlayerConnectedEvent; 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.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -41,6 +46,7 @@ public abstract class CachedLinkProvider implements LinkProvider {
protected final DiscordSRV discordSRV; protected final DiscordSRV discordSRV;
private final Cache<Long, UUID> userToPlayer; private final Cache<Long, UUID> userToPlayer;
private final AsyncLoadingCache<UUID, Long> playerToUser; private final AsyncLoadingCache<UUID, Long> playerToUser;
private final Set<UUID> linkingAllowed = new CopyOnWriteArraySet<>();
public CachedLinkProvider(DiscordSRV discordSRV) { public CachedLinkProvider(DiscordSRV discordSRV) {
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
@ -86,7 +92,7 @@ public abstract class CachedLinkProvider implements LinkProvider {
.buildAsync(new AsyncCacheLoader<UUID, Long>() { .buildAsync(new AsyncCacheLoader<UUID, Long>() {
@Override @Override
public @NonNull CompletableFuture<Long> asyncLoad(@NonNull UUID key, @NonNull Executor executor) { 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 @Override
@ -150,6 +156,9 @@ public abstract class CachedLinkProvider implements LinkProvider {
@Subscribe @Subscribe
public void onPlayerConnected(PlayerConnectedEvent event) { public void onPlayerConnected(PlayerConnectedEvent event) {
// Cache logged in players // 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 @Override
public CompletableFuture<Optional<Long>> queryUserId(@NotNull UUID playerUUID) { public CompletableFuture<Optional<Long>> queryUserId(@NotNull UUID playerUUID, boolean canCauseLink) {
return query( return query(
canCauseLink,
() -> AuthService.lookup(AccountType.MINECRAFT, playerUUID.toString(), AccountType.DISCORD) () -> AuthService.lookup(AccountType.MINECRAFT, playerUUID.toString(), AccountType.DISCORD)
.map(account -> (DiscordAccount) account) .map(account -> (DiscordAccount) account)
.map(discord -> Long.parseUnsignedLong(discord.getUserId())), .map(discord -> Long.parseUnsignedLong(discord.getUserId())),
@ -73,8 +74,9 @@ public class MinecraftAuthenticationLinker extends CachedLinkProvider implements
} }
@Override @Override
public CompletableFuture<Optional<UUID>> queryPlayerUUID(long userId) { public CompletableFuture<Optional<UUID>> queryPlayerUUID(long userId, boolean canCauseLink) {
return query( return query(
canCauseLink,
() -> AuthService.lookup(AccountType.DISCORD, Long.toUnsignedString(userId), AccountType.MINECRAFT) () -> AuthService.lookup(AccountType.DISCORD, Long.toUnsignedString(userId), AccountType.MINECRAFT)
.map(account -> (MinecraftAccount) account) .map(account -> (MinecraftAccount) account)
.map(MinecraftAccount::getUUID), .map(MinecraftAccount::getUUID),
@ -163,6 +165,7 @@ public class MinecraftAuthenticationLinker extends CachedLinkProvider implements
} }
private <T> CompletableFuture<Optional<T>> query( private <T> CompletableFuture<Optional<T>> query(
boolean canCauseLink,
CheckedSupplier<Optional<T>> authSupplier, CheckedSupplier<Optional<T>> authSupplier,
Supplier<CompletableFuture<Optional<T>>> storageSupplier, Supplier<CompletableFuture<Optional<T>>> storageSupplier,
Consumer<T> linked, Consumer<T> linked,
@ -177,6 +180,9 @@ public class MinecraftAuthenticationLinker extends CachedLinkProvider implements
authService.completeExceptionally(t); authService.completeExceptionally(t);
} }
}); });
if (!canCauseLink) {
return authService;
}
CompletableFuture<Optional<T>> storageFuture = storageSupplier.get(); CompletableFuture<Optional<T>> storageFuture = storageSupplier.get();
return CompletableFutureUtil.combine(authService, storageFuture).thenApply(results -> { return CompletableFutureUtil.combine(authService, storageFuture).thenApply(results -> {

View File

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

View File

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

View File

@ -37,13 +37,13 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class DiscordSRVLogger implements Logger { public class DiscordSRVLogger implements Logger {
@ -111,7 +111,7 @@ public class DiscordSRVLogger implements Logger {
} }
return logs; return logs;
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); doLog("LOGGING", LogLevel.ERROR, "Failed to rotate log", e);
return null; return null;
} }
} }
@ -180,7 +180,7 @@ public class DiscordSRVLogger implements Logger {
linesToWrite.add(entry); linesToWrite.add(entry);
synchronized (lineProcessingLock) { synchronized (lineProcessingLock) {
if (lineProcessingFuture == null || lineProcessingFuture.isDone()) { if (lineProcessingFuture == null || lineProcessingFuture.isDone()) {
lineProcessingFuture = discordSRV.scheduler().runLater(this::processLines, TimeUnit.SECONDS.toMillis(2)); lineProcessingFuture = discordSRV.scheduler().runLater(this::processLines, Duration.ofSeconds(2));
} }
} }
} }

View File

@ -47,6 +47,7 @@ import net.kyori.adventure.text.Component;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@ -133,7 +134,7 @@ public class DiscordChatMessageModule extends AbstractModule<DiscordSRV> {
MessageSend send = new MessageSend(message, gameChannel, config); MessageSend send = new MessageSend(message, gameChannel, config);
sends.put(key, send); sends.put(key, send);
send.setFuture(discordSRV.scheduler().runLater(() -> processSend(key), delayMillis)); send.setFuture(discordSRV.scheduler().runLater(() -> processSend(key), Duration.ofMillis(delayMillis)));
} }
} }

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.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.future.util.CompletableFutureUtil; import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.messageforwarding.game.AbstractGameMessageModule; import com.discordsrv.common.messageforwarding.game.AbstractGameMessageModule;
import com.discordsrv.common.permission.Permission;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.Role;
@ -126,7 +127,7 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
MentionCachingModule mentionCaching = discordSRV.getModule(MentionCachingModule.class); MentionCachingModule mentionCaching = discordSRV.getModule(MentionCachingModule.class);
if (mentionCaching != null && mentionConfig.users && mentionConfig.uncachedUsers 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<>(); List<CompletableFuture<List<MentionCachingModule.CachedMention>>> futures = new ArrayList<>();
String messageContent = discordSRV.componentFactory().plainSerializer().serialize(message); String messageContent = discordSRV.componentFactory().plainSerializer().serialize(message);
@ -184,23 +185,23 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
.collect(Collectors.toList()); .collect(Collectors.toList());
List<AllowedMention> allowedMentions = new ArrayList<>(); 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); allowedMentions.add(AllowedMention.ALL_USERS);
} }
if (mentionConfig.roles) { if (mentionConfig.roles) {
if (player.hasPermission("discordsrv.mention.roles.mentionable")) { if (player.hasPermission(Permission.MENTION_ROLE_MENTIONABLE)) {
for (Role role : guild.getRoles()) { for (Role role : guild.getRoles()) {
if (role.isMentionable()) { if (role.isMentionable()) {
allowedMentions.add(AllowedMention.role(role.getIdLong())); 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); 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) { if (everyone) {
allowedMentions.add(AllowedMention.EVERYONE); allowedMentions.add(AllowedMention.EVERYONE);
} }

View File

@ -0,0 +1,39 @@
package com.discordsrv.common.permission;
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

@ -20,6 +20,7 @@ package com.discordsrv.common.player;
import com.discordsrv.api.placeholder.annotation.Placeholder; import com.discordsrv.api.placeholder.annotation.Placeholder;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.player.provider.model.SkinInfo;
import com.discordsrv.common.profile.Profile; import com.discordsrv.common.profile.Profile;
import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identified;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
@ -48,4 +49,21 @@ public interface IOfflinePlayer extends Identified {
default UUID uniqueId() { default UUID uniqueId() {
return identity().uuid(); return identity().uuid();
} }
@Nullable
SkinInfo skinInfo();
@Placeholder("player_skin_texture_id")
@Nullable
default String skinTextureId() {
SkinInfo info = skinInfo();
return info != null ? info.textureId() : null;
}
@Placeholder("player_skin_model")
@Nullable
default String skinModel() {
SkinInfo info = skinInfo();
return info != null ? info.model() : null;
}
} }

View File

@ -63,11 +63,6 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende
return uniqueId().toString().replace("-", ""); return uniqueId().toString().replace("-", "");
} }
@Placeholder("player_texture")
default @Nullable String textureId() {
return null; // TODO: implement
}
@NotNull @NotNull
@Placeholder("player_display_name") @Placeholder("player_display_name")
Component displayName(); Component displayName();
@ -81,7 +76,7 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende
if (avatarConfig.autoDecideAvatarUrl) { if (avatarConfig.autoDecideAvatarUrl) {
// Offline mode // Offline mode
if (uniqueId().version() == 3) avatarUrlTemplate = "https://cravatar.eu/helmavatar/%player_name%/128.png#%texture%"; if (uniqueId().version() == 3) avatarUrlTemplate = "https://cravatar.eu/helmavatar/%player_name%/128.png#%player_skin_texture_id%";
// Bedrock // Bedrock
else if (uniqueId().getLeastSignificantBits() == 0) avatarUrlTemplate = "https://api.tydiumcraft.net/skin?uuid=%player_uuid_nodashes%&type=avatar&size=128"; else if (uniqueId().getLeastSignificantBits() == 0) avatarUrlTemplate = "https://api.tydiumcraft.net/skin?uuid=%player_uuid_nodashes%&type=avatar&size=128";
} }

View File

@ -0,0 +1,44 @@
package com.discordsrv.common.player;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.player.provider.model.SkinInfo;
import net.kyori.adventure.identity.Identity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public class OfflinePlayer implements IOfflinePlayer {
private final DiscordSRV discordSRV;
private final String username;
private final Identity identity;
private final SkinInfo skinInfo;
public OfflinePlayer(DiscordSRV discordSRV, String username, UUID uuid, SkinInfo skinInfo) {
this.discordSRV = discordSRV;
this.username = username;
this.identity = Identity.identity(uuid);
this.skinInfo = skinInfo;
}
@Override
public DiscordSRV discordSRV() {
return discordSRV;
}
@Override
public @Nullable String username() {
return username;
}
@Override
public @NotNull Identity identity() {
return identity;
}
@Override
public @Nullable SkinInfo skinInfo() {
return skinInfo;
}
}

View File

@ -30,6 +30,14 @@ public abstract class ServerPlayerProvider<T extends IPlayer, DT extends Discord
super(discordSRV); super(discordSRV);
} }
public abstract CompletableFuture<IOfflinePlayer> offlinePlayer(UUID uuid); @Override
public abstract CompletableFuture<IOfflinePlayer> offlinePlayer(String username); public CompletableFuture<UUID> lookupUUIDForUsername(String username) {
return lookupOfflinePlayer(username).thenApply(IOfflinePlayer::uniqueId);
}
@Override
public abstract CompletableFuture<IOfflinePlayer> lookupOfflinePlayer(String username);
@Override
public abstract CompletableFuture<IOfflinePlayer> lookupOfflinePlayer(UUID uuid);
} }

View File

@ -19,9 +19,18 @@
package com.discordsrv.common.player.provider; package com.discordsrv.common.player.provider;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.http.util.HttpUtil;
import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.OfflinePlayer;
import com.discordsrv.common.player.event.PlayerConnectedEvent; import com.discordsrv.common.player.event.PlayerConnectedEvent;
import com.discordsrv.common.player.event.PlayerDisconnectedEvent; import com.discordsrv.common.player.event.PlayerDisconnectedEvent;
import com.discordsrv.common.player.provider.model.GameProfileResponse;
import com.discordsrv.common.player.provider.model.SkinInfo;
import com.discordsrv.common.player.provider.model.Textures;
import com.discordsrv.common.player.provider.model.UUIDResponse;
import com.discordsrv.common.uuid.util.UUIDUtil;
import okhttp3.Request;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -29,12 +38,17 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AbstractPlayerProvider<T extends IPlayer, DT extends DiscordSRV> implements PlayerProvider<T> { public abstract class AbstractPlayerProvider<T extends IPlayer, DT extends DiscordSRV> implements PlayerProvider<T> {
private static final String MOJANG_API_URL = "https://api.mojang.com";
private static final String USERNAME_TO_UUID_URL = MOJANG_API_URL + "/users/profiles/minecraft/%s";
private static final String UUID_TO_PROFILE_URL = MOJANG_API_URL + "/session/minecraft/profile/%s";
private final Map<UUID, T> players = new ConcurrentHashMap<>(); private final Map<UUID, T> players = new ConcurrentHashMap<>();
private final List<T> allPlayers = new CopyOnWriteArrayList<>(); private final List<T> allPlayers = new CopyOnWriteArrayList<>();
protected final DT discordSRV; protected final DT discordSRV;
@ -88,4 +102,47 @@ public abstract class AbstractPlayerProvider<T extends IPlayer, DT extends Disco
public @NotNull Collection<T> allPlayers() { public @NotNull Collection<T> allPlayers() {
return allPlayers; return allPlayers;
} }
@Override
public CompletableFuture<UUID> lookupUUIDForUsername(String username) {
IPlayer player = player(username);
if (player != null) {
return CompletableFuture.completedFuture(player.uniqueId());
}
Request request = new Request.Builder()
.url(String.format(USERNAME_TO_UUID_URL, username))
.get()
.build();
return HttpUtil.readJson(discordSRV, request, UUIDResponse.class)
.thenApply(response -> UUIDUtil.fromShort(response.id));
}
@Override
public CompletableFuture<IOfflinePlayer> lookupOfflinePlayer(UUID uuid) {
IPlayer player = player(uuid);
if (player != null) {
return CompletableFuture.completedFuture(player);
}
Request request = new Request.Builder()
.url(String.format(UUID_TO_PROFILE_URL, uuid))
.get()
.build();
return HttpUtil.readJson(discordSRV, request, GameProfileResponse.class).thenApply(response -> {
SkinInfo skinInfo = null;
for (GameProfileResponse.Property property : response.properties) {
if (!Textures.KEY.equals(property.name)) {
continue;
}
Textures textures = Textures.getFromBase64(discordSRV, property.value);
skinInfo = textures.getSkinInfo();
}
return new OfflinePlayer(discordSRV, response.name, uuid, skinInfo);
});
}
} }

View File

@ -20,12 +20,14 @@ package com.discordsrv.common.player.provider;
import com.discordsrv.api.player.DiscordSRVPlayer; import com.discordsrv.api.player.DiscordSRVPlayer;
import com.discordsrv.api.player.IPlayerProvider; import com.discordsrv.api.player.IPlayerProvider;
import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Collection; import java.util.Collection;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public interface PlayerProvider<T extends IPlayer> extends IPlayerProvider { public interface PlayerProvider<T extends IPlayer> extends IPlayerProvider {
@ -51,4 +53,11 @@ public interface PlayerProvider<T extends IPlayer> extends IPlayerProvider {
*/ */
@NotNull @NotNull
Collection<T> allPlayers(); Collection<T> allPlayers();
CompletableFuture<UUID> lookupUUIDForUsername(String username);
default CompletableFuture<IOfflinePlayer> lookupOfflinePlayer(String username) {
return lookupUUIDForUsername(username).thenCompose(this::lookupOfflinePlayer);
}
CompletableFuture<IOfflinePlayer> lookupOfflinePlayer(UUID uuid);
} }

View File

@ -0,0 +1,15 @@
package com.discordsrv.common.player.provider.model;
import java.util.List;
public class GameProfileResponse {
public String id;
public String name;
public List<Property> properties;
public static class Property {
public String name;
public String value;
}
}

View File

@ -0,0 +1,20 @@
package com.discordsrv.common.player.provider.model;
public class SkinInfo {
private final String textureId;
private final String model;
public SkinInfo(String textureId, String model) {
this.textureId = textureId;
this.model = model;
}
public String textureId() {
return textureId;
}
public String model() {
return model;
}
}

View File

@ -0,0 +1,46 @@
package com.discordsrv.common.player.provider.model;
import com.discordsrv.common.DiscordSRV;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Base64;
import java.util.Map;
public class Textures {
public static String KEY = "textures";
public String profileId;
public String profileName;
public boolean signatureRequired;
public Map<String, Texture> textures;
public static class Texture {
public String url;
public Map<String, Object> metadata;
}
public SkinInfo getSkinInfo() {
Textures.Texture texture = textures.get("SKIN");
if (texture == null) {
return null;
}
String url = texture.url;
Map<String, Object> metadata = texture.metadata;
String textureId = url.substring(url.lastIndexOf("/") + 1);
return new SkinInfo(textureId, metadata != null ? (String) metadata.get("model") : null);
}
public static Textures getFromBase64(DiscordSRV discordSRV, String base64) {
byte[] bytes = Base64.getDecoder().decode(base64);
Textures textures;
try {
textures = discordSRV.json().readValue(bytes, Textures.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return textures;
}
}

View File

@ -0,0 +1,7 @@
package com.discordsrv.common.player.provider.model;
public class UUIDResponse {
public String name;
public String id;
}

View File

@ -21,6 +21,7 @@ package com.discordsrv.common.scheduler;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.concurrent.*; import java.util.concurrent.*;
@SuppressWarnings({"UnusedReturnValue", "unused"}) // API @SuppressWarnings({"UnusedReturnValue", "unused"}) // API
@ -62,36 +63,23 @@ public interface Scheduler {
*/ */
Future<?> run(@NotNull Runnable task); Future<?> run(@NotNull Runnable task);
/**
* Schedules the given task to run after the provided time in the provided {@link TimeUnit}.
*
* @param task the task
* @param time the amount of time in the provided unit
* @param unit the unit for the time
*/
@ApiStatus.NonExtendable
default ScheduledFuture<?> runLater(@NotNull Runnable task, long time, @NotNull TimeUnit unit) {
return runLater(task, unit.toMillis(time));
}
/** /**
* Schedules the given task after the provided amount of milliseconds. * Schedules the given task after the provided amount of milliseconds.
* *
* @param task the task * @param task the task
* @param timeMillis the delay before executing the task * @param delay the delay before executing the task
*/ */
ScheduledFuture<?> runLater(Runnable task, long timeMillis); ScheduledFuture<?> runLater(Runnable task, Duration delay);
/** /**
* Schedules the given task at the given rate. * Schedules the given task at the given rate.
* *
* @param task the task * @param task the task
* @param rate the rate in the given unit * @param rate the rate in the given unit
* @param unit the unit for the rate
*/ */
@ApiStatus.NonExtendable @ApiStatus.NonExtendable
default ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, long rate, @NotNull TimeUnit unit) { default ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, Duration rate) {
return runAtFixedRate(task, rate, rate, unit); return runAtFixedRate(task, rate, rate);
} }
/** /**
@ -100,21 +88,8 @@ public interface Scheduler {
* @param task the task * @param task the task
* @param initialDelay the initial delay in the provided unit * @param initialDelay the initial delay in the provided unit
* @param rate the rate to run the task at in the given unit * @param rate the rate to run the task at in the given unit
* @param unit the unit for the initial delay and rate
*/ */
@ApiStatus.NonExtendable @ApiStatus.NonExtendable
default ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, long initialDelay, long rate, @NotNull TimeUnit unit) { ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, Duration initialDelay, Duration rate);
return runAtFixedRate(task, unit.toMillis(initialDelay), unit.toMillis(rate));
}
/**
* Schedules a task to run at the given rate after the initial delay.
*
* @param task the task
* @param initialDelayMillis the initial delay in milliseconds
* @param rateMillis the rate in milliseconds
*/
ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, long initialDelayMillis, long rateMillis);
} }

View File

@ -27,6 +27,7 @@ import com.discordsrv.common.scheduler.threadfactory.CountingForkJoinWorkerThrea
import com.discordsrv.common.scheduler.threadfactory.CountingThreadFactory; import com.discordsrv.common.scheduler.threadfactory.CountingThreadFactory;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.concurrent.*; import java.util.concurrent.*;
public class StandardScheduler implements Scheduler { public class StandardScheduler implements Scheduler {
@ -124,13 +125,13 @@ public class StandardScheduler implements Scheduler {
} }
@Override @Override
public ScheduledFuture<?> runLater(Runnable task, long timeMillis) { public ScheduledFuture<?> runLater(Runnable task, Duration delay) {
return scheduledExecutorService.schedule(wrap(task), timeMillis, TimeUnit.MILLISECONDS); return scheduledExecutorService.schedule(wrap(task), delay.toMillis(), TimeUnit.MILLISECONDS);
} }
@Override @Override
public ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, long initialDelayMillis, long rateMillis) { public ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, Duration initialDelay, Duration rate) {
return scheduledExecutorService.scheduleAtFixedRate(wrap(task), initialDelayMillis, rateMillis, TimeUnit.MILLISECONDS); return scheduledExecutorService.scheduleAtFixedRate(wrap(task), initialDelay.toMillis(), rate.toMillis(), TimeUnit.MILLISECONDS);
} }
public class ExceptionHandlingExecutor implements Executor { public class ExceptionHandlingExecutor implements Executor {

View File

@ -24,7 +24,9 @@ import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.connection.UpdateConfig; import com.discordsrv.common.config.connection.UpdateConfig;
import com.discordsrv.common.debug.data.VersionInfo; import com.discordsrv.common.debug.data.VersionInfo;
import com.discordsrv.common.exception.MessageException; import com.discordsrv.common.exception.MessageException;
import com.discordsrv.common.http.util.HttpUtil;
import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.logging.NamedLogger;
import com.discordsrv.common.permission.Permission;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.event.PlayerConnectedEvent; import com.discordsrv.common.player.event.PlayerConnectedEvent;
import com.discordsrv.common.update.github.GitHubCompareResponse; import com.discordsrv.common.update.github.GitHubCompareResponse;
@ -143,16 +145,6 @@ public class UpdateChecker {
return true; return true;
} }
private ResponseBody checkResponse(Request request, Response response, ResponseBody responseBody) throws IOException {
if (responseBody == null || !response.isSuccessful()) {
String responseString = responseBody == null
? "response body is null"
: StringUtils.substring(responseBody.string(), 0, 500);
throw new MessageException("Request to " + request.url().host() + " failed: " + response.code() + ": " + responseString);
}
return responseBody;
}
/** /**
* @return {@code null} for preventing shutdown * @return {@code null} for preventing shutdown
*/ */
@ -166,7 +158,7 @@ public class UpdateChecker {
String responseString; String responseString;
try (Response response = discordSRV.httpClient().newCall(request).execute()) { try (Response response = discordSRV.httpClient().newCall(request).execute()) {
ResponseBody responseBody = checkResponse(request, response, response.body()); ResponseBody responseBody = HttpUtil.checkIfResponseSuccessful(request, response);
responseString = responseBody.string(); responseString = responseBody.string();
} }
@ -204,7 +196,7 @@ public class UpdateChecker {
.get().build(); .get().build();
try (Response response = discordSRV.httpClient().newCall(request).execute()) { try (Response response = discordSRV.httpClient().newCall(request).execute()) {
ResponseBody responseBody = checkResponse(request, response, response.body()); ResponseBody responseBody = HttpUtil.checkIfResponseSuccessful(request, response);
GitHubCompareResponse compare = discordSRV.json().readValue(responseBody.byteStream(), GitHubCompareResponse.class); GitHubCompareResponse compare = discordSRV.json().readValue(responseBody.byteStream(), GitHubCompareResponse.class);
VersionCheck versionCheck = new VersionCheck(); VersionCheck versionCheck = new VersionCheck();
@ -236,7 +228,7 @@ public class UpdateChecker {
.get().build(); .get().build();
try (Response response = discordSRV.httpClient().newCall(request).execute()) { try (Response response = discordSRV.httpClient().newCall(request).execute()) {
ResponseBody responseBody = checkResponse(request, response, response.body()); ResponseBody responseBody = HttpUtil.checkIfResponseSuccessful(request, response);
List<GithubRelease> releases = discordSRV.json().readValue(responseBody.byteStream(), new TypeReference<List<GithubRelease>>() {}); List<GithubRelease> releases = discordSRV.json().readValue(responseBody.byteStream(), new TypeReference<List<GithubRelease>>() {});
for (GithubRelease release : releases) { for (GithubRelease release : releases) {
@ -303,7 +295,7 @@ public class UpdateChecker {
} }
IPlayer player = event.player(); IPlayer player = event.player();
if (!player.hasPermission("discordsrv.updatenotification")) { if (!player.hasPermission(Permission.UPDATE_NOTIFICATION)) {
return; return;
} }

View File

@ -0,0 +1,37 @@
package com.discordsrv.common.uuid.util;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public final class UUIDUtil {
private UUIDUtil() {}
public static UUID fromShortOrFull(@NotNull String uuidString) {
int length = uuidString.length();
if (length == 32) {
return fromShort(uuidString);
} else if (length == 36) {
return UUID.fromString(uuidString);
}
throw new IllegalArgumentException("Not a valid 36 or 32 character long UUID");
}
public static UUID fromShort(@NotNull String shortUUID) {
if (shortUUID.length() != 32) {
throw new IllegalArgumentException("Short uuids are 32 characters long");
}
String fullLengthUUID = shortUUID.substring(0, 8)
+ "-" + shortUUID.substring(8, 12)
+ "-" + shortUUID.substring(12, 16)
+ "-" + shortUUID.substring(16, 20)
+ "-" + shortUUID.substring(20);
return UUID.fromString(fullLengthUUID);
}
public static String toShort(@NotNull UUID uuid) {
return uuid.toString().replace("-", "");
}
}

View File

@ -14,6 +14,7 @@ import com.discordsrv.common.MockDiscordSRV;
import com.discordsrv.common.channel.GlobalChannel; import com.discordsrv.common.channel.GlobalChannel;
import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.provider.model.SkinInfo;
import com.discordsrv.common.testing.TestHelper; import com.discordsrv.common.testing.TestHelper;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
@ -73,6 +74,11 @@ public class MinecraftToDiscordChatMessageTest {
return "Vankka"; return "Vankka";
} }
@Override
public @Nullable SkinInfo skinInfo() {
return null;
}
@Override @Override
public @Nullable Locale locale() { public @Nullable Locale locale() {
return Locale.getDefault(); return Locale.getDefault();

View File

@ -20,10 +20,14 @@ package com.discordsrv.sponge.player;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.player.IOfflinePlayer; import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.provider.model.SkinInfo;
import com.discordsrv.common.player.provider.model.Textures;
import com.discordsrv.sponge.SpongeDiscordSRV; import com.discordsrv.sponge.SpongeDiscordSRV;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.api.entity.living.player.User; import org.spongepowered.api.entity.living.player.User;
import org.spongepowered.api.profile.property.ProfileProperty;
public class SpongeOfflinePlayer implements IOfflinePlayer { public class SpongeOfflinePlayer implements IOfflinePlayer {
@ -45,6 +49,19 @@ public class SpongeOfflinePlayer implements IOfflinePlayer {
return user.name(); return user.name();
} }
@Override
public @Nullable SkinInfo skinInfo() {
for (ProfileProperty property : user.profile().properties()) {
if (!Textures.KEY.equals(property.name())) {
continue;
}
Textures textures = Textures.getFromBase64(discordSRV, property.value());
return textures.getSkinInfo();
}
return null;
}
@Override @Override
public @NotNull Identity identity() { public @NotNull Identity identity() {
return user.profile(); return user.profile();

View File

@ -20,6 +20,8 @@ package com.discordsrv.sponge.player;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.provider.model.SkinInfo;
import com.discordsrv.common.player.provider.model.Textures;
import com.discordsrv.sponge.SpongeDiscordSRV; import com.discordsrv.sponge.SpongeDiscordSRV;
import com.discordsrv.sponge.command.game.sender.SpongeCommandSender; import com.discordsrv.sponge.command.game.sender.SpongeCommandSender;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
@ -51,6 +53,13 @@ public class SpongePlayer extends SpongeCommandSender implements IPlayer {
return player.name(); return player.name();
} }
@Override
public @Nullable SkinInfo skinInfo() {
String texturesRaw = player.skinProfile().get().value();
Textures textures = Textures.getFromBase64(discordSRV, texturesRaw);
return textures.getSkinInfo();
}
@Override @Override
public @Nullable Locale locale() { public @Nullable Locale locale() {
return player.locale(); return player.locale();

View File

@ -19,6 +19,7 @@
package com.discordsrv.sponge.player; package com.discordsrv.sponge.player;
import com.discordsrv.common.player.IOfflinePlayer; import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.ServerPlayerProvider; import com.discordsrv.common.player.ServerPlayerProvider;
import com.discordsrv.sponge.SpongeDiscordSRV; import com.discordsrv.sponge.SpongeDiscordSRV;
import org.spongepowered.api.entity.living.player.User; import org.spongepowered.api.entity.living.player.User;
@ -77,14 +78,24 @@ public class SpongePlayerProvider extends ServerPlayerProvider<SpongePlayer, Spo
} }
@Override @Override
public CompletableFuture<IOfflinePlayer> offlinePlayer(UUID uuid) { public CompletableFuture<IOfflinePlayer> lookupOfflinePlayer(UUID uuid) {
IPlayer player = player(uuid);
if (player != null) {
return CompletableFuture.completedFuture(player);
}
return discordSRV.game().server().userManager() return discordSRV.game().server().userManager()
.load(uuid) .load(uuid)
.thenApply(optional -> optional.map(this::convert).orElse(null)); .thenApply(optional -> optional.map(this::convert).orElse(null));
} }
@Override @Override
public CompletableFuture<IOfflinePlayer> offlinePlayer(String username) { public CompletableFuture<IOfflinePlayer> lookupOfflinePlayer(String username) {
IPlayer player = player(username);
if (player != null) {
return CompletableFuture.completedFuture(player);
}
return discordSRV.game().server().userManager() return discordSRV.game().server().userManager()
.load(username) .load(username)
.thenApply(optional -> optional.map(this::convert).orElse(null)); .thenApply(optional -> optional.map(this::convert).orElse(null));

View File

@ -20,9 +20,12 @@ package com.discordsrv.velocity.player;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.provider.model.SkinInfo;
import com.discordsrv.common.player.provider.model.Textures;
import com.discordsrv.velocity.VelocityDiscordSRV; import com.discordsrv.velocity.VelocityDiscordSRV;
import com.discordsrv.velocity.command.game.sender.VelocityCommandSender; import com.discordsrv.velocity.command.game.sender.VelocityCommandSender;
import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.util.GameProfile;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -49,6 +52,19 @@ public class VelocityPlayer extends VelocityCommandSender implements IPlayer {
return player.getUsername(); return player.getUsername();
} }
@Override
public @Nullable SkinInfo skinInfo() {
for (GameProfile.Property property : player.getGameProfile().getProperties()) {
if (!Textures.KEY.equals(property.getName())) {
continue;
}
Textures textures = Textures.getFromBase64(discordSRV, property.getValue());
return textures.getSkinInfo();
}
return null;
}
@Override @Override
public @Nullable Locale locale() { public @Nullable Locale locale() {
return player.getPlayerSettings().getLocale(); return player.getPlayerSettings().getLocale();