diff --git a/core/src/main/java/tc/oc/pgm/PGMPlugin.java b/core/src/main/java/tc/oc/pgm/PGMPlugin.java index b095cb7bfc..89a50e2a77 100644 --- a/core/src/main/java/tc/oc/pgm/PGMPlugin.java +++ b/core/src/main/java/tc/oc/pgm/PGMPlugin.java @@ -81,8 +81,8 @@ import tc.oc.pgm.util.tablist.TablistResizer; import tc.oc.pgm.util.text.TextException; import tc.oc.pgm.util.text.TextTranslations; -import tc.oc.pgm.util.usernames.ApiUsernameResolver; import tc.oc.pgm.util.usernames.BukkitUsernameResolver; +import tc.oc.pgm.util.usernames.MojangUsernameResolver; import tc.oc.pgm.util.usernames.UsernameResolvers; import tc.oc.pgm.util.xml.InvalidXMLException; @@ -171,7 +171,7 @@ public void onEnable() { UsernameResolvers.setResolvers( new BukkitUsernameResolver(), new SqlUsernameResolver((SQLDatastore) datastore), - new ApiUsernameResolver()); + new MojangUsernameResolver()); datastore = new CacheDatastore(datastore); diff --git a/util/src/main/java/tc/oc/pgm/util/usernames/ApiUsernameResolver.java b/util/src/main/java/tc/oc/pgm/util/usernames/ApiUsernameResolver.java deleted file mode 100644 index 52fd67a558..0000000000 --- a/util/src/main/java/tc/oc/pgm/util/usernames/ApiUsernameResolver.java +++ /dev/null @@ -1,115 +0,0 @@ -package tc.oc.pgm.util.usernames; - -import static tc.oc.pgm.util.Assert.assertNotNull; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.NoRouteToHostException; -import java.net.URL; -import java.net.UnknownHostException; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.logging.Level; -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; -import tc.oc.pgm.util.bukkit.BukkitUtils; - -/** - * Utility to resolve Minecraft usernames from an external API. - * - * @link https://github.com/Electroid/mojang-api - */ -public final class ApiUsernameResolver extends AbstractBatchingUsernameResolver { - private static final Gson GSON = new Gson(); - private static final int MAX_SEQUENTIAL_FAILURES = 5; - private static String userAgent = "PGM"; - - static { - try { - final Plugin plugin = BukkitUtils.getPlugin(); - if (plugin != null) { - userAgent = plugin.getDescription().getFullName(); - } - } catch (Throwable t) { - // No-op, just to be safe in-case agent cannot be found - } - } - - @Override - protected void process(UUID uuid, CompletableFuture future) { - String name = null; - try { - name = resolveSync(uuid); - } catch (Throwable t) { - Bukkit.getLogger().log(Level.WARNING, "Could not resolve username for " + uuid, t); - } finally { - future.complete(UsernameResponse.of(name, ApiUsernameResolver.class)); - } - } - - @Override - protected void process(List uuids) { - final Map errors = new LinkedHashMap<>(); - - Instant now = Instant.now(); - - int fails = 0; - boolean stopped = false; - for (UUID id : uuids) { - // Even if there's an issue, we need to complete the futures. - if (stopped) { - complete(id, UsernameResponse.empty()); - continue; - } - - String name = null; - try { - name = resolveSync(id); - fails = 0; - } catch (Throwable t) { - errors.put(id, t); - if (++fails > MAX_SEQUENTIAL_FAILURES - || t instanceof UnknownHostException - || t instanceof NoRouteToHostException) { - stopped = true; - } - } finally { - complete(id, UsernameResponse.of(name, now, ApiUsernameResolver.class)); - } - } - - if (!errors.isEmpty()) { - Bukkit.getLogger() - .log( - Level.WARNING, - LOG_PREFIX + "Could not resolve " + errors.size() + " usernames", - errors.values().iterator().next()); - } - } - - private static String resolveSync(UUID id) throws IOException { - final HttpURLConnection url = - (HttpURLConnection) - new URL("https://api.ashcon.app/mojang/v2/user/" + assertNotNull(id)).openConnection(); - url.setRequestMethod("GET"); - url.setRequestProperty("User-Agent", userAgent); - url.setRequestProperty("Accept", "application/json"); - url.setInstanceFollowRedirects(true); - url.setConnectTimeout(10000); - url.setReadTimeout(10000); - - try (final BufferedReader br = - new BufferedReader(new InputStreamReader(url.getInputStream(), StandardCharsets.UTF_8))) { - return GSON.fromJson(br, JsonObject.class).get("username").getAsString(); - } - } -} diff --git a/util/src/main/java/tc/oc/pgm/util/usernames/MojangUsernameResolver.java b/util/src/main/java/tc/oc/pgm/util/usernames/MojangUsernameResolver.java new file mode 100644 index 0000000000..cc50bb10f9 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/usernames/MojangUsernameResolver.java @@ -0,0 +1,116 @@ +package tc.oc.pgm.util.usernames; + +import static tc.oc.pgm.util.Assert.assertNotNull; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import tc.oc.pgm.util.bukkit.BukkitUtils; + +public class MojangUsernameResolver implements UsernameResolver { + private static final Gson GSON = new Gson(); + private static final long WAIT_TIME = 6; // Mojang api allows 10 requests a minute + private static String userAgent = "PGM"; + private static Instant nextExecution = Instant.now(); + private static ExecutorService executor = Executors.newSingleThreadExecutor(); + private static long currentWaitTime = 6; + protected final Map> futures = + new ConcurrentHashMap<>(); + + static { + try { + final Plugin plugin = BukkitUtils.getPlugin(); + if (plugin != null) { + userAgent = plugin.getDescription().getFullName() + Bukkit.getName(); + } + } catch (Throwable t) { + // No-op, just to be safe in-case agent cannot be found + } + } + + private CompletableFuture resolveInternal(UUID uuid) { + CompletableFuture completableFuture = new CompletableFuture<>(); + completableFuture.whenComplete((o, t) -> futures.remove(uuid)); + executor.submit(() -> { + Instant now = Instant.now(); + + if (now.isBefore(nextExecution)) { + try { + Thread.sleep(Duration.between(now, nextExecution)); + } catch (InterruptedException e) { + // Ignore + } + } + + String name = resolveSync(uuid); + if (name != null) { + currentWaitTime = WAIT_TIME; + } + nextExecution = Instant.now().plus(Duration.ofSeconds(currentWaitTime)); + + Bukkit.getLogger().log(Level.FINE, "Resolved " + uuid + " as " + name); + + completableFuture.complete(UsernameResponse.of(name, MojangUsernameResolver.class)); + }); + return completableFuture; + } + + @Override + public CompletableFuture resolve(UUID uuid) { + return futures.computeIfAbsent(uuid, this::resolveInternal); + } + + private static String resolveSync(UUID uuid) { + try { + final HttpURLConnection url = (HttpURLConnection) new URL( + "https://api.minecraftservices.com/minecraft/profile/lookup/" + assertNotNull(uuid)) + .openConnection(); + url.setRequestMethod("GET"); + url.setRequestProperty("User-Agent", userAgent); + url.setRequestProperty("Accept", "application/json"); + url.setInstanceFollowRedirects(true); + url.setConnectTimeout(10000); + url.setReadTimeout(10000); + + try (final BufferedReader br = + new BufferedReader(new InputStreamReader(url.getInputStream(), StandardCharsets.UTF_8))) { + return GSON.fromJson(br, JsonObject.class).get("name").getAsString(); + } + } catch (Throwable t) { + currentWaitTime *= 2; + Bukkit.getLogger() + .log( + Level.WARNING, + "Could not resolve username for " + uuid + " retrying in " + currentWaitTime + + " seconds!", + t); + } + return null; + } + + @Override + public void startBatch() { + // TODO: Refactor + } + + @Override + public CompletableFuture endBatch() { + // TODO: Refactor + return new CompletableFuture<>(); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/usernames/UsernameResolvers.java b/util/src/main/java/tc/oc/pgm/util/usernames/UsernameResolvers.java index 92a1d304cd..29760eb521 100644 --- a/util/src/main/java/tc/oc/pgm/util/usernames/UsernameResolvers.java +++ b/util/src/main/java/tc/oc/pgm/util/usernames/UsernameResolvers.java @@ -8,7 +8,7 @@ public interface UsernameResolvers { AtomicReference INSTANCE = - new AtomicReference<>(of(new BukkitUsernameResolver(), new ApiUsernameResolver())); + new AtomicReference<>(of(new BukkitUsernameResolver(), new MojangUsernameResolver())); static UsernameResolver get() { return INSTANCE.get();