Add link create and linked commands

This commit is contained in:
Vankka 2023-12-26 18:40:55 +02:00
parent b5b9a0dd73
commit 069f4fb218
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
39 changed files with 920 additions and 189 deletions

View File

@ -4,9 +4,9 @@ import org.bukkit.entity.Player;
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 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.common.DiscordSRV;
import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.provider.model.SkinInfo;
import net.kyori.adventure.identity.Identity;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class BukkitOfflinePlayer implements IOfflinePlayer {
@ -47,6 +49,11 @@ public class BukkitOfflinePlayer implements IOfflinePlayer {
return offlinePlayer.getName();
}
@Override
public @Nullable SkinInfo skinInfo() {
return null;
}
@Override
public @NotNull Identity identity() {
return identity;

View File

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

View File

@ -20,6 +20,7 @@ package com.discordsrv.bukkit.player;
import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.ServerPlayerProvider;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
@ -87,13 +88,23 @@ public class BukkitPlayerProvider extends ServerPlayerProvider<BukkitPlayer, Buk
}
@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));
}
@SuppressWarnings("deprecation") // Shut up, I know
@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));
}

View File

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

View File

@ -101,6 +101,7 @@ import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@ -671,7 +672,7 @@ public abstract class AbstractDiscordSRV<
scheduler().run(() -> updateChecker.check(true));
}
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)) {

View File

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

View File

@ -1,8 +1,10 @@
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;
@ -12,15 +14,22 @@ 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 {
@ -74,11 +83,13 @@ public class LinkInitCommand extends CombinedCommand {
}
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();
@ -95,7 +106,7 @@ public class LinkInitCommand extends CombinedCommand {
if (sender instanceof IPlayer) {
startLinking((IPlayer) sender, ((GameCommandExecution) execution).getLabel());
} else {
sender.sendMessage(execution.messages().minecraft.pleaseSpecifyPlayerAndUserToLink.asComponent());
sender.sendMessage(discordSRV.messagesConfig(sender).pleaseSpecifyPlayerAndUserToLink.asComponent());
}
return;
}
@ -112,84 +123,135 @@ public class LinkInitCommand extends CombinedCommand {
return;
}
UUID playerUUID = CommandUtil.lookupPlayer(discordSRV, execution, false, playerArgument, null);
if (playerUUID == null) {
execution.send(
execution.messages().minecraft.playerNotFound.asComponent(),
execution.messages().discord.playerNotFound
);
return;
}
CompletableFuture<UUID> playerUUIDFuture = CommandUtil.lookupPlayer(discordSRV, logger, execution, false, playerArgument, null);
CompletableFuture<Long> userIdFuture = CommandUtil.lookupUser(discordSRV, logger, execution, false, userArgument, null);
Long userId = CommandUtil.lookupUser(discordSRV, execution, false, userArgument, null);
if (userId == null) {
execution.send(
execution.messages().minecraft.userNotFound.asComponent(),
execution.messages().discord.userNotFound
);
return;
}
linkProvider.queryUserId(playerUUID).thenCompose(opt -> {
if (opt.isPresent()) {
playerUUIDFuture.whenComplete((playerUUID, __) -> userIdFuture.whenComplete((userId, ___) -> {
if (playerUUID == null) {
execution.send(
execution.messages().minecraft.playerAlreadyLinked3rd.asComponent(),
execution.messages().discord.playerAlreadyLinked3rd
execution.messages().minecraft.playerNotFound.asComponent(),
execution.messages().discord.playerNotFound
);
return null;
}
return linkProvider.queryPlayerUUID(userId);
}).thenCompose(opt -> {
if (opt.isPresent()) {
execution.send(
execution.messages().minecraft.userAlreadyLinked3rd.asComponent(),
execution.messages().discord.userAlreadyLinked3rd
);
return null;
}
return ((LinkStore) linkProvider).createLink(playerUUID, userId);
}).whenComplete((v, t) -> {
if (t != null) {
// TODO: it did not work
return;
}
execution.send(
execution.messages().minecraft.nowLinked3rd.asComponent(),
execution.messages().discord.nowLinked3rd
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(discordSRV.messagesConfig(player).alreadyLinked1st.asComponent());
player.sendMessage(messages.alreadyLinked1st.asComponent());
return;
}
if (linkCheckRateLimit.getIfPresent(player.uniqueId()) != null) {
player.sendMessage(discordSRV.messagesConfig(player).pleaseWaitBeforeRunningThatCommandAgain.asComponent());
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, t) -> {
if (t != null) {
player.sendMessage(discordSRV.messagesConfig(player).unableToLinkAtThisTime.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(discordSRV.messagesConfig(player).nowLinked1st.asComponent());
player.sendMessage(messages.nowLinked1st.asComponent());
return;
}
linkProvider.getLinkingInstructions(player, label).whenComplete((comp, t2) -> {
if (t2 != null) {
player.sendMessage(discordSRV.messagesConfig(player).unableToLinkAtThisTime.asComponent());
logger.error("Failed to link account", t2);
player.sendMessage(messages.unableToLinkAtThisTime.asComponent());
return;
}

View File

@ -1,17 +1,25 @@
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.Text;
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 {
@ -28,7 +36,7 @@ public class LinkedCommand extends CombinedCommand {
LinkedCommand command = getInstance(discordSRV);
GAME = GameCommand.literal("linked")
.then(
GameCommand.stringWord("target")
GameCommand.stringGreedy("target")
.requiredPermission(Permission.COMMAND_LINKED_OTHER)
.executor(command)
)
@ -60,30 +68,154 @@ public class LinkedCommand extends CombinedCommand {
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);
CommandUtil.TargetLookupResult result = CommandUtil.lookupTarget(discordSRV, execution, true, Permission.COMMAND_LINKED_OTHER);
if (!result.isValid()) {
return;
}
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()) {
execution.runAsync(() -> {
discordSRV.linkProvider().getUserId(result.getPlayerUUID()).whenComplete((userId, t) -> {
execution.send(new Text(userId.map(Long::toUnsignedString).orElse("Not linked"))); // TODO: username
});
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 {
execution.runAsync(() -> {
discordSRV.linkProvider().getPlayerUUID(result.getUserId()).whenComplete((playerUUID, t) -> {
execution.send(new Text(playerUUID.map(UUID::toString).orElse("Not linked"))); // TODO: player name
});
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

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

View File

@ -6,8 +6,10 @@ import com.discordsrv.common.command.combined.abstraction.DiscordCommandExecutio
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;
@ -15,43 +17,49 @@ import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public final class CommandUtil {
private CommandUtil() {}
@Nullable
public static UUID lookupPlayer(
public static CompletableFuture<UUID> lookupPlayer(
DiscordSRV discordSRV,
Logger logger,
CommandExecution execution,
boolean selfPermitted,
String target,
@Nullable Permission otherPermission
) {
TargetLookupResult result = lookupTarget(discordSRV, execution, target, selfPermitted, true, false, otherPermission);
if (result.isValid()) {
return result.getPlayerUUID();
}
return null;
return lookupTarget(discordSRV, logger, execution, target, selfPermitted, true, false, otherPermission)
.thenApply((result) -> {
if (result != null && result.isValid()) {
return result.getPlayerUUID();
}
return null;
});
}
@Nullable
public static Long lookupUser(
public static CompletableFuture<Long> lookupUser(
DiscordSRV discordSRV,
Logger logger,
CommandExecution execution,
boolean selfPermitted,
String target,
@Nullable Permission otherPermission
) {
TargetLookupResult result = lookupTarget(discordSRV, execution, target, selfPermitted, false, true, otherPermission);
if (result.isValid()) {
return result.getUserId();
}
return null;
return lookupTarget(discordSRV, logger, execution, target, selfPermitted, false, true, otherPermission)
.thenApply(result -> {
if (result != null && result.isValid()) {
return result.getUserId();
}
return null;
});
}
public static TargetLookupResult lookupTarget(
public static CompletableFuture<TargetLookupResult> lookupTarget(
DiscordSRV discordSRV,
Logger logger,
CommandExecution execution,
boolean selfPermitted,
@Nullable Permission otherPermission
@ -63,11 +71,12 @@ public final class CommandUtil {
if (target == null) {
target = execution.getArgument("player");
}
return lookupTarget(discordSRV, execution, target, selfPermitted, true, true, otherPermission);
return lookupTarget(discordSRV, logger, execution, target, selfPermitted, true, true, otherPermission);
}
private static TargetLookupResult lookupTarget(
private static CompletableFuture<TargetLookupResult> lookupTarget(
DiscordSRV discordSRV,
Logger logger,
CommandExecution execution,
String target,
boolean selfPermitted,
@ -82,16 +91,10 @@ public final class CommandUtil {
if (target != null) {
if (otherPermission != null && !sender.hasPermission(Permission.COMMAND_LINKED_OTHER)) {
sender.sendMessage(discordSRV.messagesConfig(sender).noPermission.asComponent());
return TargetLookupResult.INVALID;
return CompletableFuture.completedFuture(TargetLookupResult.INVALID);
}
} else if (sender instanceof IPlayer && selfPermitted && lookupPlayer) {
target = ((IPlayer) sender).uniqueId().toString();
} else {
execution.send(
messages.minecraft.pleaseSpecifyPlayer.asComponent(),
messages.discord.pleaseSpecifyPlayer
);
return TargetLookupResult.INVALID;
}
} else if (execution instanceof DiscordCommandExecution) {
if (target == null) {
@ -102,13 +105,17 @@ public final class CommandUtil {
messages.minecraft.pleaseSpecifyUser.asComponent(),
messages.discord.pleaseSpecifyUser
);
return TargetLookupResult.INVALID;
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
@ -120,10 +127,10 @@ public final class CommandUtil {
messages.minecraft.userNotFound.asComponent(),
messages.discord.userNotFound
);
return TargetLookupResult.INVALID;
return CompletableFuture.completedFuture(TargetLookupResult.INVALID);
}
return new TargetLookupResult(true, null, id);
return CompletableFuture.completedFuture(new TargetLookupResult(true, null, id));
} else if (target.startsWith("@")) {
// Discord username
String username = target.substring(1);
@ -132,7 +139,7 @@ public final class CommandUtil {
List<User> users = jda.getUsersByName(username, true);
if (users.size() == 1) {
return new TargetLookupResult(true, null, users.get(0).getIdLong());
return CompletableFuture.completedFuture(new TargetLookupResult(true, null, users.get(0).getIdLong()));
}
}
}
@ -143,34 +150,62 @@ public final class CommandUtil {
boolean shortUUID;
if ((shortUUID = target.length() == 32) || target.length() == 36) {
// Player UUID
if (shortUUID) {
target = target.substring(0, 8) + "-" + target.substring(8, 12) + "-" + target.substring(12, 16)
+ "-" + target.substring(16, 20) + "-" + target.substring(20);
}
try {
uuid = UUID.fromString(target);
if (shortUUID) {
uuid = UUIDUtil.fromShort(target);
} else {
uuid = UUID.fromString(target);
}
} catch (IllegalArgumentException ignored) {
execution.send(
messages.minecraft.playerNotFound.asComponent(),
messages.discord.playerNotFound
);
return TargetLookupResult.INVALID;
return CompletableFuture.completedFuture(TargetLookupResult.INVALID);
}
} else {
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 {
throw new IllegalStateException("lookup offline"); // TODO: lookup offline player
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 new TargetLookupResult(true, uuid, 0L);
}
return TargetLookupResult.INVALID;
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 {

View File

@ -37,15 +37,17 @@ public class MessagesConfig implements Config {
@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");
@Untranslated(Untranslated.Type.COMMENT)
@Comment("/discord link")
@Constants(ERROR_COLOR)
public MinecraftMessage unableToCheckLinkingStatus = make("%1Unable to check linking status, please try again later");
@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");
@ -61,8 +63,13 @@ public class MessagesConfig implements Config {
public MinecraftMessage checkingLinkStatus = make("%1Checking linking status...");
@Constants(SUCCESS_COLOR)
public MinecraftMessage nowLinked1st = make("%1You are now linked!");
@Constants(SUCCESS_COLOR)
public MinecraftMessage nowLinked3rd = make("%1Link created successfully");
@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,
@ -70,6 +77,38 @@ public class MessagesConfig implements Config {
})
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({
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]"
})
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();
@ -86,10 +125,14 @@ public class MessagesConfig implements Config {
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")
@ -97,7 +140,41 @@ public class MessagesConfig implements Config {
public String playerAlreadyLinked3rd = "%1That Minecraft player is already linked";
@Constants(ERROR_PREFIX)
public String userAlreadyLinked3rd = "%1That Discord user is already linked";
@Constants(SUCCESS_PREFIX)
public String nowLinked3rd = "%1Link created successfully";
@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

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

View File

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

View File

@ -18,10 +18,15 @@
package com.discordsrv.common.future.util;
import com.discordsrv.common.DiscordSRV;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeoutException;
public final class CompletableFutureUtil {
@ -58,4 +63,17 @@ public final class CompletableFutureUtil {
});
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.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@ -127,9 +128,8 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
int cycleTime = timer.cycleTime;
future = discordSRV.scheduler().runAtFixedRate(
() -> resyncPair(pair, GroupSyncCause.TIMER),
cycleTime,
cycleTime,
TimeUnit.MINUTES
Duration.ofMinutes(cycleTime),
Duration.ofMinutes(cycleTime)
);
}

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

@ -37,13 +37,13 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class DiscordSRVLogger implements Logger {
@ -111,7 +111,7 @@ public class DiscordSRVLogger implements Logger {
}
return logs;
} catch (IOException e) {
e.printStackTrace();
doLog("LOGGING", LogLevel.ERROR, "Failed to rotate log", e);
return null;
}
}
@ -180,7 +180,7 @@ public class DiscordSRVLogger implements Logger {
linesToWrite.add(entry);
synchronized (lineProcessingLock) {
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.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
@ -133,7 +134,7 @@ public class DiscordChatMessageModule extends AbstractModule<DiscordSRV> {
MessageSend send = new MessageSend(message, gameChannel, config);
sends.put(key, send);
send.setFuture(discordSRV.scheduler().runLater(() -> processSend(key), delayMillis));
send.setFuture(discordSRV.scheduler().runLater(() -> processSend(key), Duration.ofMillis(delayMillis)));
}
}

View File

@ -20,6 +20,7 @@ package com.discordsrv.common.player;
import com.discordsrv.api.placeholder.annotation.Placeholder;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.player.provider.model.SkinInfo;
import com.discordsrv.common.profile.Profile;
import net.kyori.adventure.identity.Identified;
import org.jetbrains.annotations.ApiStatus;
@ -48,4 +49,21 @@ public interface IOfflinePlayer extends Identified {
default UUID uniqueId() {
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("-", "");
}
@Placeholder("player_texture")
default @Nullable String textureId() {
return null; // TODO: implement
}
@NotNull
@Placeholder("player_display_name")
Component displayName();
@ -81,7 +76,7 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende
if (avatarConfig.autoDecideAvatarUrl) {
// 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
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);
}
public abstract CompletableFuture<IOfflinePlayer> offlinePlayer(UUID uuid);
public abstract CompletableFuture<IOfflinePlayer> offlinePlayer(String username);
@Override
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;
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.OfflinePlayer;
import com.discordsrv.common.player.event.PlayerConnectedEvent;
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.Nullable;
@ -29,12 +38,17 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
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 List<T> allPlayers = new CopyOnWriteArrayList<>();
protected final DT discordSRV;
@ -88,4 +102,47 @@ public abstract class AbstractPlayerProvider<T extends IPlayer, DT extends Disco
public @NotNull Collection<T> 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.IPlayerProvider;
import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.IPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public interface PlayerProvider<T extends IPlayer> extends IPlayerProvider {
@ -51,4 +53,11 @@ public interface PlayerProvider<T extends IPlayer> extends IPlayerProvider {
*/
@NotNull
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.NotNull;
import java.time.Duration;
import java.util.concurrent.*;
@SuppressWarnings({"UnusedReturnValue", "unused"}) // API
@ -62,36 +63,23 @@ public interface Scheduler {
*/
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.
*
* @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.
*
* @param task the task
* @param rate the rate in the given unit
* @param unit the unit for the rate
*/
@ApiStatus.NonExtendable
default ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, long rate, @NotNull TimeUnit unit) {
return runAtFixedRate(task, rate, rate, unit);
default ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, Duration rate) {
return runAtFixedRate(task, rate, rate);
}
/**
@ -100,21 +88,8 @@ public interface Scheduler {
* @param task the task
* @param initialDelay the initial delay in the provided 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
default ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, long initialDelay, long rate, @NotNull TimeUnit unit) {
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);
ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, Duration initialDelay, Duration rate);
}

View File

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

View File

@ -24,6 +24,7 @@ import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.connection.UpdateConfig;
import com.discordsrv.common.debug.data.VersionInfo;
import com.discordsrv.common.exception.MessageException;
import com.discordsrv.common.http.util.HttpUtil;
import com.discordsrv.common.logging.NamedLogger;
import com.discordsrv.common.permission.Permission;
import com.discordsrv.common.player.IPlayer;
@ -144,16 +145,6 @@ public class UpdateChecker {
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
*/
@ -167,7 +158,7 @@ public class UpdateChecker {
String responseString;
try (Response response = discordSRV.httpClient().newCall(request).execute()) {
ResponseBody responseBody = checkResponse(request, response, response.body());
ResponseBody responseBody = HttpUtil.checkIfResponseSuccessful(request, response);
responseString = responseBody.string();
}
@ -205,7 +196,7 @@ public class UpdateChecker {
.get().build();
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);
VersionCheck versionCheck = new VersionCheck();
@ -237,7 +228,7 @@ public class UpdateChecker {
.get().build();
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>>() {});
for (GithubRelease release : releases) {

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

View File

@ -20,10 +20,14 @@ package com.discordsrv.sponge.player;
import com.discordsrv.common.DiscordSRV;
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 net.kyori.adventure.identity.Identity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.api.entity.living.player.User;
import org.spongepowered.api.profile.property.ProfileProperty;
public class SpongeOfflinePlayer implements IOfflinePlayer {
@ -45,6 +49,19 @@ public class SpongeOfflinePlayer implements IOfflinePlayer {
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
public @NotNull Identity identity() {
return user.profile();

View File

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

View File

@ -19,6 +19,7 @@
package com.discordsrv.sponge.player;
import com.discordsrv.common.player.IOfflinePlayer;
import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.ServerPlayerProvider;
import com.discordsrv.sponge.SpongeDiscordSRV;
import org.spongepowered.api.entity.living.player.User;
@ -77,14 +78,24 @@ public class SpongePlayerProvider extends ServerPlayerProvider<SpongePlayer, Spo
}
@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()
.load(uuid)
.thenApply(optional -> optional.map(this::convert).orElse(null));
}
@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()
.load(username)
.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.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.command.game.sender.VelocityCommandSender;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.util.GameProfile;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
@ -49,6 +52,19 @@ public class VelocityPlayer extends VelocityCommandSender implements IPlayer {
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
public @Nullable Locale locale() {
return player.getPlayerSettings().getLocale();