Skip to content

Commit

Permalink
Offline player fetching improvements (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
EpicPlayerA10 authored Mar 24, 2024
1 parent 1cb35ab commit 26802cb
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -176,28 +176,26 @@ public Task startMaintenanceRunnable(final Runnable runnable) {
}

@Override
public void getOfflinePlayer(final String name, final Consumer<@Nullable SenderInfo> consumer) {
public CompletableFuture<@Nullable SenderInfo> getOfflinePlayer(final String name) {
final ProxiedPlayer player = getProxy().getPlayer(name);
if (player != null) {
consumer.accept(new BungeeSenderInfo(player));
return;
return CompletableFuture.completedFuture(new BungeeSenderInfo(player));
}

getProxy().getScheduler().runAsync(plugin, () -> {
return CompletableFuture.supplyAsync(() -> {
try {
final ProfileLookup profile = doUUIDLookup(name);
consumer.accept(new ProxyOfflineSenderInfo(profile.getUuid(), profile.getName()));
} catch (final IOException e) {
e.printStackTrace();
consumer.accept(null);
return new ProxyOfflineSenderInfo(profile.getUuid(), profile.getName());
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}

@Override
public void getOfflinePlayer(final UUID uuid, final Consumer<@Nullable SenderInfo> consumer) {
public CompletableFuture<@Nullable SenderInfo> getOfflinePlayer(final UUID uuid) {
final ProxiedPlayer player = getProxy().getPlayer(uuid);
consumer.accept(player != null ? new BungeeSenderInfo(player) : null);
return CompletableFuture.completedFuture(player != null ? new BungeeSenderInfo(player) : null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,19 @@
import eu.kennytv.maintenance.core.proxy.runnable.SingleMaintenanceScheduleRunnable;
import eu.kennytv.maintenance.core.proxy.util.ProfileLookup;
import eu.kennytv.maintenance.core.runnable.MaintenanceRunnableBase;
import eu.kennytv.maintenance.core.util.RateLimitedException;
import eu.kennytv.maintenance.core.util.SenderInfo;
import eu.kennytv.maintenance.core.util.ServerType;
import eu.kennytv.maintenance.core.util.Task;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -187,9 +190,66 @@ protected void kickPlayers() {
kickPlayersFromProxy();
}

@Blocking
@Nullable
protected ProfileLookup doUUIDLookup(final String name) throws IOException {
ProfileLookup profileLookup;
try {
profileLookup = doUUIDLookupMojangAPI(name);
} catch (RateLimitedException e) {
// Use fallback API if rate limit is reached
profileLookup = doUUIDLookupAshconAPI(name);
}

if (settingsProxy.isFallbackToOfflineUUID() && profileLookup == null) {
// Use offline uuid
return new ProfileLookup(UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(StandardCharsets.UTF_8)), name);
}

return profileLookup;
}

/**
* Official Mojang API
*/
@Nullable
private ProfileLookup doUUIDLookupMojangAPI(final String name) throws IOException {
final URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + name);
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");

int status = connection.getResponseCode();
if (status == 429) {
throw new RateLimitedException();
}
if (status == 404) {
// Return null if profile not found
return null;
}

try (final InputStream in = connection.getInputStream()) {
final String output = CharStreams.toString(new InputStreamReader(in));
final JsonObject json = GSON.fromJson(output, JsonObject.class);

final UUID uuid = fromStringUUIDWithoutDashes(json.getAsJsonPrimitive("id").getAsString());
final String username = json.getAsJsonPrimitive("name").getAsString();
return new ProfileLookup(uuid, username);
}
}

/**
* Fallback API (Ashcon API)
*/
@Nullable
private ProfileLookup doUUIDLookupAshconAPI(final String name) throws IOException {
final URL url = new URL("https://api.ashcon.app/mojang/v2/user/" + name);
final URLConnection connection = url.openConnection();
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();

if (connection.getResponseCode() == 403) {
// Return null if profile not found
return null;
}

try (final InputStream in = connection.getInputStream()) {
final String output = CharStreams.toString(new InputStreamReader(in));
final JsonObject json = GSON.fromJson(output, JsonObject.class);
Expand All @@ -200,6 +260,14 @@ protected ProfileLookup doUUIDLookup(final String name) throws IOException {
}
}

private UUID fromStringUUIDWithoutDashes(String undashedUUID) {
return UUID.fromString(
undashedUUID.substring(0, 8) + "-" + undashedUUID.substring(8, 12) + "-" +
undashedUUID.substring(12, 16) + "-" + undashedUUID.substring(16, 20) + "-" +
undashedUUID.substring(20, 32)
);
}

public SettingsProxy getSettingsProxy() {
return settingsProxy;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public final class SettingsProxy extends Settings {
private Set<String> maintenanceServers;
private List<String> fallbackServers;
private String waitingServer;
private boolean fallbackToOfflineUUID;

private Map<String, List<String>> commandsOnMaintenanceEnable;
private Map<String, List<String>> commandsOnMaintenanceDisable;
Expand Down Expand Up @@ -103,6 +104,7 @@ protected void loadExtraSettings() {
if (waitingServer.isEmpty() || waitingServer.equalsIgnoreCase("none")) {
waitingServer = null;
}
fallbackToOfflineUUID = config.getBoolean("fallback-to-offline-uuid", false);

commandsOnMaintenanceEnable = new HashMap<>();
final ConfigSection enableCommandsSection = config.getSection("commands-on-single-maintenance-enable");
Expand Down Expand Up @@ -279,6 +281,10 @@ public String getWaitingServer() {
return waitingServer;
}

public boolean isFallbackToOfflineUUID() {
return fallbackToOfflineUUID;
}

public List<String> getCommandsOnMaintenanceEnable(final Server server) {
final List<String> enableCommands = commandsOnMaintenanceEnable.getOrDefault("all", new ArrayList<>());
final List<String> serverEnableCommands = commandsOnMaintenanceEnable.get(server.getName().toLowerCase(Locale.ROOT));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand Down Expand Up @@ -447,9 +447,9 @@ protected String getPluginFolder() {
*
* @param name name of the player
*/
public abstract void getOfflinePlayer(String name, Consumer<@Nullable SenderInfo> consumer);
public abstract CompletableFuture<@Nullable SenderInfo> getOfflinePlayer(String name);

public abstract void getOfflinePlayer(UUID uuid, Consumer<@Nullable SenderInfo> consumer);
public abstract CompletableFuture<@Nullable SenderInfo> getOfflinePlayer(UUID uuid);

public abstract File getDataFolder();

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/eu/kennytv/maintenance/core/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@

public class Settings implements eu.kennytv.maintenance.api.Settings {
public static final String NEW_LINE_REPLACEMENT = "<br>";
private static final int CONFIG_VERSION = 7;
private static final int LANGUAGE_VERSION = 1;
private static final int CONFIG_VERSION = 8;
private static final int LANGUAGE_VERSION = 2;
private static final Random RANDOM = new Random();
protected final MaintenancePlugin plugin;
private final Map<UUID, String> whitelistedPlayers = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;

public final class WhitelistAddCommand extends CommandInfo {

Expand Down Expand Up @@ -64,7 +65,13 @@ public List<String> getTabCompletion(final SenderInfo sender, final String[] arg
}

private void addPlayerToWhitelist(final SenderInfo sender, final String name) {
plugin.getOfflinePlayer(name, selected -> {
plugin.getOfflinePlayer(name).whenComplete((selected, ex) -> {
if (ex != null) {
plugin.getLogger().log(Level.SEVERE, "Error while fetching offline player", ex);
sender.send(getMessage("offlinePlayerFetchError"));
return;
}

if (selected == null) {
sender.send(getMessage("playerNotOnline"));
return;
Expand All @@ -75,7 +82,13 @@ private void addPlayerToWhitelist(final SenderInfo sender, final String name) {
}

private void addPlayerToWhitelist(final SenderInfo sender, final UUID uuid) {
plugin.getOfflinePlayer(uuid, selected -> {
plugin.getOfflinePlayer(uuid).whenComplete((selected, ex) -> {
if (ex != null) {
plugin.getLogger().log(Level.SEVERE, "Error while fetching offline player", ex);
sender.send(getMessage("offlinePlayerFetchError"));
return;
}

if (selected == null) {
sender.send(getMessage("playerNotFoundUuid"));
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,17 @@ public List<String> getTabCompletion(final SenderInfo sender, final String[] arg
if (args.length != 2) return Collections.emptyList();

final List<String> list = new ArrayList<>();
String matchNameLowerCase = args[1].toLowerCase();
for (final String name : getSettings().getWhitelistedPlayers().values()) {
if (name.toLowerCase().startsWith(args[1])) {
if (name.toLowerCase().startsWith(matchNameLowerCase)) {
list.add(name);
}
}
return list;
}

private void removePlayerFromWhitelist(final SenderInfo sender, final String name) {
plugin.getOfflinePlayer(name, selected -> {
plugin.getOfflinePlayer(name).whenComplete((selected, ex) -> {
if (selected == null) {
if (getSettings().removeWhitelistedPlayer(name)) {
sender.send(getMessage("whitelistRemoved", "%PLAYER%", name));
Expand All @@ -74,7 +75,7 @@ private void removePlayerFromWhitelist(final SenderInfo sender, final String nam
}

private void removePlayerFromWhitelist(final SenderInfo sender, final UUID uuid) {
plugin.getOfflinePlayer(uuid, selected -> {
plugin.getOfflinePlayer(uuid).whenComplete((selected, ex) -> {
if (selected == null) {
removePlayerFromWhitelist(sender, uuid, uuid.toString());
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package eu.kennytv.maintenance.core.util;

import java.io.IOException;

/**
* Exception thrown when an API rate limit is reached.
*/
public class RateLimitedException extends IOException {
}
5 changes: 4 additions & 1 deletion core/src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ send-join-notification: false
# ... I don't know why you would want that, but you can disable it. :p
kick-online-players: true

# When fetched player does not exist then fallback to offline uuid. Only works on proxies like Velocity or BungeeCord
fallback-to-offline-uuid: false

# Changes the language of command feedback/messages.
# If you find missing translations or want to contribute a new language file, you are very welcome to message me on Spigot or my Discord server! :)
# Currently available are: en (English), de (German), fr (French), pt (Portuguese), es (Spanish), ru (Russian), zh (Chinese), it (Italian),
Expand Down Expand Up @@ -123,4 +126,4 @@ timer-broadcast-for-seconds: [1200, 900, 600, 300, 120, 60, 30, 20, 10, 5, 4, 3,
update-checks: true

# Used for autoupdating the config, do not change this value.
config-version: 7
config-version: 8
3 changes: 2 additions & 1 deletion core/src/main/resources/language-en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ whitelistEmpty: "<prefix><red>The maintenance whitelist is empty! Use <yellow>/m
playerNotFound: "<prefix><red>No player with this name has played on this server before."
playerNotFoundUuid: "<prefix><red>No player with that uuid could be found."
playerNotOnline: "<prefix><red>There is no player online with that name."
offlinePlayerFetchError: "<prefix><red>There was an error while fetching offline player. Please try again later."
invalidUuid: "<prefix><red>Invalid uuid format!"
#Messages for the Bungee/Velocity part, you can ignore them if you use the plugin on Spigot/Sponge
sentToWaitingServer: "<prefix><red>You have been sent to a waiting server!"
Expand Down Expand Up @@ -100,4 +101,4 @@ helpSingleScheduleTimer: "<gold>/maintenance scheduletimer [server] <timer minut
helpSingleToggle: "<gold>/maintenance <on/off> [server] <gray>(Enables/disables maintenance mode)"
helpStatus: "<gold>/maintenance status <gray>(Lists all proxied servers, that are currently under maintenance)"
#Used for autoupdating the language file, do not change this value.
language-version: 1
language-version: 2
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.imageio.ImageIO;
Expand Down Expand Up @@ -140,15 +140,15 @@ public Task startMaintenanceRunnable(final Runnable runnable) {
}

@Override
public void getOfflinePlayer(final String name, final Consumer<@Nullable SenderInfo> consumer) {
public CompletableFuture<@Nullable SenderInfo> getOfflinePlayer(final String name) {
final OfflinePlayer player = getServer().getOfflinePlayer(name);
consumer.accept(player.getName() != null ? new BukkitOfflinePlayerInfo(player) : null);
return CompletableFuture.completedFuture(player.getName() != null ? new BukkitOfflinePlayerInfo(player) : null);
}

@Override
public void getOfflinePlayer(final UUID uuid, final Consumer<@Nullable SenderInfo> consumer) {
public CompletableFuture<@Nullable SenderInfo> getOfflinePlayer(final UUID uuid) {
final OfflinePlayer player = getServer().getOfflinePlayer(uuid);
consumer.accept(player.getName() != null ? new BukkitOfflinePlayerInfo(player) : null);
return CompletableFuture.completedFuture(player.getName() != null ? new BukkitOfflinePlayerInfo(player) : null);
}

@Override
Expand Down
Loading

0 comments on commit 26802cb

Please sign in to comment.