diff --git a/pom.xml b/pom.xml index 8691e96..701dbc6 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ com.imaginarycode.minecraft RedisBungee - 0.2.6-SNAPSHOT + 0.3-SNAPSHOT diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java index 751f132..a2e1580 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java @@ -12,7 +12,9 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.gson.Gson; import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.imaginarycode.minecraft.redisbungee.util.UUIDTranslator; import lombok.Getter; import lombok.NonNull; import net.md_5.bungee.api.config.ServerInfo; @@ -31,10 +33,7 @@ import redis.clients.jedis.exceptions.JedisException; import java.io.*; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -51,6 +50,12 @@ public final class RedisBungee extends Plugin { private static Configuration configuration; @Getter private JedisPool pool; + @Getter + private RedisBungeeConsumer consumer; + @Getter + private UUIDTranslator uuidTranslator; + @Getter + private static Gson gson = new Gson(); private static RedisBungeeAPI api; private static PubSubListener psl = null; private List serverIds; @@ -81,7 +86,7 @@ public final class RedisBungee extends Plugin { for (Map.Entry entry : heartbeats.entrySet()) { try { long stamp = Long.valueOf(entry.getValue()); - if (stamp + 30000 < System.currentTimeMillis()) + if (System.currentTimeMillis() < stamp + 30000) servers.add(entry.getKey()); } catch (NumberFormatException ignored) { } @@ -99,9 +104,9 @@ public final class RedisBungee extends Plugin { return psl; } - final Multimap serversToPlayers() { - ImmutableMultimap.Builder multimapBuilder = ImmutableMultimap.builder(); - for (String p : getPlayers()) { + final Multimap serversToPlayers() { + ImmutableMultimap.Builder multimapBuilder = ImmutableMultimap.builder(); + for (UUID p : getPlayers()) { ServerInfo si = getServerFor(p); if (si != null) multimapBuilder = multimapBuilder.put(si.getName(), p); @@ -118,15 +123,19 @@ public final class RedisBungee extends Plugin { if (pool != null) { Jedis rsc = pool.getResource(); try { - for (String i : getServerIds()) { - if (i.equals(configuration.getString("server-id"))) continue; - if (rsc.exists("server:" + i + ":playerCount")) - try { - c += Integer.valueOf(rsc.get("server:" + i + ":playerCount")); - } catch (NumberFormatException e) { - getLogger().severe("I found a funny number for " + i + "'s player count. Resetting it to 0."); - rsc.set("server:" + i + ":playerCount", "0"); - } + List serverIds = getServerIds(); + Map counts = rsc.hgetAll("playerCounts"); + for (Map.Entry entry : counts.entrySet()) { + if (!serverIds.contains(entry.getKey())) + continue; + + if (entry.getKey().equals(configuration.getString("server-id"))) continue; + + try { + c += Integer.valueOf(entry.getValue()); + } catch (NumberFormatException e) { + rsc.hset("playerCounts", entry.getKey(), "0"); + } } } catch (JedisConnectionException e) { // Redis server has disappeared! @@ -140,23 +149,35 @@ public final class RedisBungee extends Plugin { return c; } - final Set getLocalPlayers() { - ImmutableSet.Builder setBuilder = ImmutableSet.builder(); + final Set getLocalPlayers() { + ImmutableSet.Builder setBuilder = ImmutableSet.builder(); for (ProxiedPlayer pp : getProxy().getPlayers()) - setBuilder = setBuilder.add(pp.getName()); + setBuilder = setBuilder.add(pp.getUniqueId()); return setBuilder.build(); } - final Set getPlayers() { - ImmutableSet.Builder setBuilder = ImmutableSet.builder().addAll(getLocalPlayers()); + final Set getLocalPlayersAsUuidStrings() { + ImmutableSet.Builder setBuilder = ImmutableSet.builder(); + for (ProxiedPlayer pp : getProxy().getPlayers()) + setBuilder = setBuilder.add(pp.getUniqueId().toString()); + return setBuilder.build(); + } + + final Set getPlayers() { + ImmutableSet.Builder setBuilder = ImmutableSet.builder().addAll(getLocalPlayers()); if (pool != null) { Jedis rsc = pool.getResource(); try { for (String i : getServerIds()) { if (i.equals(configuration.getString("server-id"))) continue; Set users = rsc.smembers("server:" + i + ":usersOnline"); - if (users != null && !users.isEmpty()) - setBuilder = setBuilder.addAll(users); + if (users != null && !users.isEmpty()) { + for (String user : users) { + if (UUIDTranslator.UUID_PATTERN.matcher(user).find()) { + setBuilder = setBuilder.add(UUID.fromString(user)); + } + } + } } } catch (JedisConnectionException e) { // Redis server has disappeared! @@ -170,24 +191,24 @@ public final class RedisBungee extends Plugin { return setBuilder.build(); } - final Set getPlayersOnServer(@NonNull String server) { + final Set getPlayersOnServer(@NonNull String server) { checkArgument(getProxy().getServerInfo(server) != null, "server doesn't exist"); return ImmutableSet.copyOf(serversToPlayers().get(server)); } - final ServerInfo getServerFor(@NonNull String name) { + final ServerInfo getServerFor(@NonNull UUID uuid) { ServerInfo server = null; - if (getProxy().getPlayer(name) != null) return getProxy().getPlayer(name).getServer().getInfo(); + if (getProxy().getPlayer(uuid) != null) return getProxy().getPlayer(uuid).getServer().getInfo(); if (pool != null) { Jedis tmpRsc = pool.getResource(); try { - if (tmpRsc.hexists("player:" + name, "server")) - server = getProxy().getServerInfo(tmpRsc.hget("player:" + name, "server")); + if (tmpRsc.hexists("player:" + uuid, "server")) + server = getProxy().getServerInfo(tmpRsc.hget("player:" + uuid, "server")); } catch (JedisConnectionException e) { // Redis server has disappeared! getLogger().log(Level.SEVERE, "Unable to get connection from pool - did your Redis server go away?", e); pool.returnBrokenResource(tmpRsc); - throw new RuntimeException("Unable to get server for " + name, e); + throw new RuntimeException("Unable to get server for " + uuid, e); } finally { pool.returnResource(tmpRsc); } @@ -195,21 +216,21 @@ public final class RedisBungee extends Plugin { return server; } - final long getLastOnline(@NonNull String name) { + final long getLastOnline(@NonNull UUID uuid) { long time = -1L; - if (getProxy().getPlayer(name) != null) return 0; + if (getProxy().getPlayer(uuid) != null) return 0; if (pool != null) { Jedis tmpRsc = pool.getResource(); try { - if (tmpRsc.hexists("player:" + name, "online")) + if (tmpRsc.hexists("player:" + uuid, "online")) try { - time = Long.valueOf(tmpRsc.hget("player:" + name, "online")); + time = Long.valueOf(tmpRsc.hget("player:" + uuid, "online")); } catch (NumberFormatException e) { - getLogger().info("I found a funny number for when " + name + " was last online!"); + getLogger().info("I found a funny number for when " + uuid + " was last online!"); boolean found = false; for (String proxyId : getServerIds()) { if (proxyId.equals(configuration.getString("server-id"))) continue; - if (tmpRsc.sismember("server:" + proxyId + ":usersOnline", name)) { + if (tmpRsc.sismember("server:" + proxyId + ":usersOnline", uuid.toString())) { found = true; break; } @@ -217,18 +238,18 @@ public final class RedisBungee extends Plugin { String value = "0"; if (!found) { value = String.valueOf(System.currentTimeMillis()); - getLogger().info(name + " isn't online. Setting to current time."); + getLogger().info(uuid + " isn't online. Setting to current time."); } else { - getLogger().info(name + " is online. Setting to 0. Please check your BungeeCord instances."); + getLogger().info(uuid + " is online. Setting to 0. Please check your BungeeCord instances."); getLogger().info("If they are working properly, and this error does not resolve in a few minutes, please let Tux know!"); } - tmpRsc.hset("player:" + name, "online", value); + tmpRsc.hset("player:" + uuid, "online", value); } } catch (JedisConnectionException e) { // Redis server has disappeared! getLogger().log(Level.SEVERE, "Unable to get connection from pool - did your Redis server go away?", e); pool.returnBrokenResource(tmpRsc); - throw new RuntimeException("Unable to get last time online for " + name, e); + throw new RuntimeException("Unable to get last time online for " + uuid, e); } finally { pool.returnResource(tmpRsc); } @@ -236,20 +257,20 @@ public final class RedisBungee extends Plugin { return time; } - final InetAddress getIpAddress(@NonNull String name) { - if (getProxy().getPlayer(name) != null) - return getProxy().getPlayer(name).getAddress().getAddress(); + final InetAddress getIpAddress(@NonNull UUID uuid) { + if (getProxy().getPlayer(uuid) != null) + return getProxy().getPlayer(uuid).getAddress().getAddress(); InetAddress ia = null; if (pool != null) { Jedis tmpRsc = pool.getResource(); try { - if (tmpRsc.hexists("player:" + name, "ip")) - ia = InetAddress.getByName(tmpRsc.hget("player:" + name, "ip")); + if (tmpRsc.hexists("player:" + uuid, "ip")) + ia = InetAddress.getByName(tmpRsc.hget("player:" + uuid, "ip")); } catch (JedisConnectionException e) { // Redis server has disappeared! getLogger().log(Level.SEVERE, "Unable to get connection from pool - did your Redis server go away?", e); pool.returnBrokenResource(tmpRsc); - throw new RuntimeException("Unable to fetch IP address for " + name, e); + throw new RuntimeException("Unable to fetch IP address for " + uuid, e); } catch (UnknownHostException ignored) { // Best to just return null } finally { @@ -286,36 +307,20 @@ public final class RedisBungee extends Plugin { if (pool != null) { Jedis tmpRsc = pool.getResource(); try { - tmpRsc.set("server:" + configuration.getString("server-id") + ":playerCount", "0"); // reset + tmpRsc.hset("playerCounts", configuration.getString("server-id"), "0"); // reset tmpRsc.hset("heartbeats", configuration.getString("server-id"), String.valueOf(System.currentTimeMillis())); - if (tmpRsc.scard("server:" + configuration.getString("server-id") + ":usersOnline") > 0) { - for (String member : tmpRsc.smembers("server:" + configuration.getString("server-id") + ":usersOnline")) { - // Are they simply on a different proxy? - boolean found = false; - for (String proxyId : tmpRsc.smembers("servers")) { - if (proxyId.equals(configuration.getString("server-id"))) continue; - if (tmpRsc.sismember("server:" + proxyId + ":usersOnline", member)) { - found = true; - break; - } - } - if (!found) - RedisUtil.cleanUpPlayer(member, tmpRsc); - else - tmpRsc.srem("server:" + configuration.getString("server-id") + ":usersOnline", member); - } - } } finally { pool.returnResource(tmpRsc); } - globalCount = getCurrentCount(); serverIds = getCurrentServerIds(); + globalCount = getCurrentCount(); + uuidTranslator = new UUIDTranslator(this); getProxy().getScheduler().schedule(this, new Runnable() { @Override public void run() { Jedis rsc = pool.getResource(); try { - rsc.set("server:" + configuration.getString("server-id") + ":playerCount", String.valueOf(getProxy().getOnlineCount())); + rsc.hset("playerCounts", configuration.getString("server-id"), String.valueOf(getProxy().getOnlineCount())); rsc.hset("heartbeats", configuration.getString("server-id"), String.valueOf(System.currentTimeMillis())); } catch (JedisConnectionException e) { // Redis server has disappeared! @@ -324,16 +329,19 @@ public final class RedisBungee extends Plugin { } finally { pool.returnResource(rsc); } - globalCount = getCurrentCount(); serverIds = getCurrentServerIds(); + globalCount = getCurrentCount(); } }, 0, 3, TimeUnit.SECONDS); - getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.GlistCommand()); - getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.FindCommand()); - getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.LastSeenCommand()); - getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.IpCommand()); - getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.SendToAll()); - getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.ServerId()); + consumer = new RedisBungeeConsumer(this); + new Thread(consumer, "RedisBungee Consumer Thread").start(); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.GlistCommand(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.FindCommand(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.LastSeenCommand(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.IpCommand(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.SendToAll(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.ServerId(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.ServerIds()); getProxy().getPluginManager().registerListener(this, new RedisBungeeListener(this)); api = new RedisBungeeAPI(this); psl = new PubSubListener(); @@ -343,7 +351,7 @@ public final class RedisBungee extends Plugin { public void run() { Jedis tmpRsc = pool.getResource(); try { - Set players = getLocalPlayers(); + Set players = getLocalPlayersAsUuidStrings(); for (String member : tmpRsc.smembers("server:" + configuration.getString("server-id") + ":usersOnline")) if (!players.contains(member)) { // Are they simply on a different proxy? @@ -368,7 +376,7 @@ public final class RedisBungee extends Plugin { pool.returnResource(tmpRsc); } } - }, 1, 3, TimeUnit.MINUTES); + }, 0, 3, TimeUnit.MINUTES); } getProxy().registerChannel("RedisBungee"); } @@ -378,6 +386,8 @@ public final class RedisBungee extends Plugin { if (pool != null) { // Poison the PubSub listener getProxy().getScheduler().cancel(this); + getLogger().info("Waiting for consumer to finish writing data..."); + consumer.stop(); Jedis tmpRsc = pool.getResource(); try { tmpRsc.set("server:" + configuration.getString("server-id") + ":playerCount", "0"); // reset @@ -385,7 +395,7 @@ public final class RedisBungee extends Plugin { for (String member : tmpRsc.smembers("server:" + configuration.getString("server-id") + ":usersOnline")) RedisUtil.cleanUpPlayer(member, tmpRsc); } - tmpRsc.srem("servers", configuration.getString("server-id")); + tmpRsc.hdel("heartbeats", configuration.getString("server-id")); } finally { pool.returnResource(tmpRsc); } @@ -437,13 +447,12 @@ public final class RedisBungee extends Plugin { File crashFile = new File(getDataFolder(), "restarted_from_crash.txt"); if (crashFile.exists()) crashFile.delete(); - else if (rsc.sismember("servers", configuration.getString("server-id"))) { + else if (rsc.hexists("heartbeat", configuration.getString("server-id"))) { getLogger().severe("You have launched a possible imposter BungeeCord instance. Another instance is already running."); getLogger().severe("For data consistency reasons, RedisBungee will now disable itself."); getLogger().severe("If this instance is coming up from a crash, create a file in your RedisBungee plugins directory with the name 'restarted_from_crash.txt' and RedisBungee will not perform this check."); throw new RuntimeException("Possible imposter instance!"); } - rsc.sadd("servers", configuration.getString("server-id")); getLogger().log(Level.INFO, "Successfully connected to Redis."); } catch (JedisConnectionException e) { if (rsc != null) diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeAPI.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeAPI.java index b7e9490..94df05a 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeAPI.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeAPI.java @@ -13,6 +13,7 @@ import net.md_5.bungee.api.config.ServerInfo; import java.net.InetAddress; import java.util.List; import java.util.Set; +import java.util.UUID; /** * This class exposes some internal RedisBungee functions. You obtain an instance of this object by invoking {@link RedisBungee#getApi()}. @@ -43,7 +44,7 @@ public class RedisBungeeAPI { * @param player a player name * @return the last time a player was on, if online returns a 0 */ - public final long getLastOnline(@NonNull String player) { + public final long getLastOnline(@NonNull UUID player) { return plugin.getLastOnline(player); } @@ -54,36 +55,38 @@ public class RedisBungeeAPI { * @param player a player name * @return a {@link net.md_5.bungee.api.config.ServerInfo} for the server the player is on. */ - public final ServerInfo getServerFor(@NonNull String player) { + public final ServerInfo getServerFor(@NonNull UUID player) { return plugin.getServerFor(player); } /** * Get a combined list of players on this network. - *

+ *

* Note that this function returns an immutable {@link java.util.Set}. * * @return a Set with all players found */ - public final Set getPlayersOnline() { + public final Set getPlayersOnline() { return plugin.getPlayers(); } /** * Get a full list of players on all servers. + * * @return a immutable Multimap with all players found on this server * @since 0.2.5 */ - public final Multimap getServerToPlayers() { + public final Multimap getServerToPlayers() { return plugin.serversToPlayers(); } /** * Get a list of players on the server with the given name. + * * @param server a server name * @return a Set with all players found on this server */ - public final Set getPlayersOnServer(@NonNull String server) { + public final Set getPlayersOnServer(@NonNull String server) { return plugin.getPlayersOnServer(server); } @@ -93,7 +96,7 @@ public class RedisBungeeAPI { * @param player a player name * @return if the player is online */ - public final boolean isPlayerOnline(@NonNull String player) { + public final boolean isPlayerOnline(@NonNull UUID player) { return getLastOnline(player) == 0; } @@ -104,12 +107,13 @@ public class RedisBungeeAPI { * @return an {@link java.net.InetAddress} if the player is online, null otherwise * @since 0.2.4 */ - public final InetAddress getPlayerIp(@NonNull String player) { + public final InetAddress getPlayerIp(@NonNull UUID player) { return plugin.getIpAddress(player); } /** * Sends a proxy command to all proxies. + * * @param command the command to send and execute * @see #sendProxyCommand(String, String) * @since 0.2.5 @@ -120,6 +124,7 @@ public class RedisBungeeAPI { /** * Sends a proxy command to the proxy with the given ID. "allservers" means all proxies. + * * @param proxyId a proxy ID * @param command the command to send and execute * @see #getServerId() @@ -132,9 +137,10 @@ public class RedisBungeeAPI { /** * Get the current BungeeCord server ID for this server. + * * @return the current server ID - * @since 0.2.5 * @see #getAllServers() + * @since 0.2.5 */ public final String getServerId() { return RedisBungee.getConfiguration().getString("server-id"); @@ -142,9 +148,10 @@ public class RedisBungeeAPI { /** * Get all the linked proxies in this network. + * * @return the list of all proxies - * @since 0.2.5 * @see #getServerId() + * @since 0.2.5 */ public final List getAllServers() { return plugin.getServerIds(); @@ -169,4 +176,26 @@ public class RedisBungeeAPI { public final void unregisterPubSubChannels(String... channels) { RedisBungee.getPubSubListener().removeChannel(channels); } + + /** + * Fetch a name from the specified UUID. + * + * @param uuid the UUID to fetch the name for + * @return the name for the UUID + * @since 0.3 + */ + public final String getNameFromUuid(UUID uuid) { + return plugin.getUuidTranslator().getNameFromUuid(uuid); + } + + /** + * Fetch a UUID from the specified name. + * + * @param name the UUID to fetch the name for + * @return the UUID for the name + * @since 0.3 + */ + public final UUID getUuidFromName(String name) { + return plugin.getUuidTranslator().getTranslatedUuid(name); + } } diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSender.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSender.java index a545ed4..a1c2cb6 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSender.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSender.java @@ -16,7 +16,7 @@ import java.util.Collections; /** * This class is the CommandSender that RedisBungee uses to dispatch commands to BungeeCord. - *

+ *

* It inherits all permissions of the console command sender. Sending messages and modifying permissions are no-ops. * * @author tuxed diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommands.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommands.java index 6658b08..d82514d 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommands.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommands.java @@ -7,6 +7,7 @@ package com.imaginarycode.minecraft.redisbungee; import com.google.common.base.Joiner; +import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.CommandSender; @@ -18,7 +19,9 @@ import net.md_5.bungee.api.plugin.Command; import java.net.InetAddress; import java.text.SimpleDateFormat; +import java.util.Map; import java.util.TreeSet; +import java.util.UUID; /** * This class contains subclasses that are used for the commands RedisBungee overrides or includes: /glist, /find and /lastseen. @@ -37,119 +40,158 @@ class RedisBungeeCommands { new ComponentBuilder("You must specify a command to be run.").color(ChatColor.RED).create(); public static class GlistCommand extends Command { - GlistCommand() { + private final RedisBungee plugin; + + GlistCommand(RedisBungee plugin) { super("glist", "bungeecord.command.list", "redisbungee", "rglist"); + this.plugin = plugin; } @Override - public void execute(CommandSender sender, String[] args) { - int count = RedisBungee.getApi().getPlayerCount(); - BaseComponent[] playersOnline = new ComponentBuilder("").color(ChatColor.YELLOW).append(String.valueOf(count)) - .append(" player(s) are currently online.").create(); - if (args.length > 0 && args[0].equals("showall")) { - if (RedisBungee.getConfiguration().getBoolean("canonical-glist", true)) { - Multimap serverToPlayers = RedisBungee.getApi().getServerToPlayers(); - for (String server : new TreeSet<>(serverToPlayers.keySet())) { - TextComponent serverName = new TextComponent(); - serverName.setColor(ChatColor.GREEN); - serverName.setText("[" + server + "] "); - TextComponent serverCount = new TextComponent(); - serverCount.setColor(ChatColor.YELLOW); - serverCount.setText("(" + serverToPlayers.get(server).size() + "): "); - TextComponent serverPlayers = new TextComponent(); - serverPlayers.setColor(ChatColor.WHITE); - serverPlayers.setText(Joiner.on(", ").join(serverToPlayers.get(server))); - sender.sendMessage(serverName, serverCount, serverPlayers); + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + int count = RedisBungee.getApi().getPlayerCount(); + BaseComponent[] playersOnline = new ComponentBuilder("").color(ChatColor.YELLOW).append(String.valueOf(count)) + .append(" player(s) are currently online.").create(); + if (args.length > 0 && args[0].equals("showall")) { + if (RedisBungee.getConfiguration().getBoolean("canonical-glist", true)) { + Multimap serverToPlayers = RedisBungee.getApi().getServerToPlayers(); + Multimap human = HashMultimap.create(); + for (Map.Entry entry : serverToPlayers.entries()) { + human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue())); + } + for (String server : new TreeSet<>(serverToPlayers.keySet())) { + TextComponent serverName = new TextComponent(); + serverName.setColor(ChatColor.GREEN); + serverName.setText("[" + server + "] "); + TextComponent serverCount = new TextComponent(); + serverCount.setColor(ChatColor.YELLOW); + serverCount.setText("(" + serverToPlayers.get(server).size() + "): "); + TextComponent serverPlayers = new TextComponent(); + serverPlayers.setColor(ChatColor.WHITE); + serverPlayers.setText(Joiner.on(", ").join(human.get(server))); + sender.sendMessage(serverName, serverCount, serverPlayers); + } + } else { + sender.sendMessage(new ComponentBuilder("Players: " + Joiner.on(", ").join(RedisBungee.getApi().getPlayersOnline())) + .color(ChatColor.YELLOW).create()); + } + sender.sendMessage(playersOnline); + } else { + sender.sendMessage(playersOnline); + sender.sendMessage(new ComponentBuilder("To see all players online, use /glist showall.").color(ChatColor.YELLOW).create()); } - } else { - sender.sendMessage(new ComponentBuilder("Players: " + Joiner.on(", ").join(RedisBungee.getApi().getPlayersOnline())) - .color(ChatColor.YELLOW).create()); } - sender.sendMessage(playersOnline); - } else { - sender.sendMessage(playersOnline); - sender.sendMessage(new ComponentBuilder("To see all players online, use /glist showall.").color(ChatColor.YELLOW).create()); - } + }); } } public static class FindCommand extends Command { - FindCommand() { + private final RedisBungee plugin; + + FindCommand(RedisBungee plugin) { super("find", "bungeecord.command.find", "rfind"); + this.plugin = plugin; } @Override - public void execute(CommandSender sender, String[] args) { - if (args.length > 0) { - ServerInfo si = RedisBungee.getApi().getServerFor(args[0]); - if (si != null) { - TextComponent message = new TextComponent(); - message.setColor(ChatColor.BLUE); - message.setText(args[0] + " is on " + si.getName() + "."); - sender.sendMessage(message); - } else { - sender.sendMessage(PLAYER_NOT_FOUND); + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + ServerInfo si = RedisBungee.getApi().getServerFor(plugin.getUuidTranslator().getTranslatedUuid(args[0])); + if (si != null) { + TextComponent message = new TextComponent(); + message.setColor(ChatColor.BLUE); + message.setText(args[0] + " is on " + si.getName() + "."); + sender.sendMessage(message); + } else { + sender.sendMessage(PLAYER_NOT_FOUND); + } + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } } - } else { - sender.sendMessage(NO_PLAYER_SPECIFIED); - } + }); } } public static class LastSeenCommand extends Command { - LastSeenCommand() { + private final RedisBungee plugin; + + LastSeenCommand(RedisBungee plugin) { super("lastseen", "redisbungee.command.lastseen", "rlastseen"); + this.plugin = plugin; } @Override - public void execute(CommandSender sender, String[] args) { - if (args.length > 0) { - long secs = RedisBungee.getApi().getLastOnline(args[0]); - TextComponent message = new TextComponent(); - if (secs == 0) { - message.setColor(ChatColor.GREEN); - message.setText(args[0] + " is currently online."); - sender.sendMessage(message); - } else if (secs != -1) { - message.setColor(ChatColor.BLUE); - message.setText(args[0] + " was last online on " + new SimpleDateFormat().format(secs) + "."); - sender.sendMessage(message); - } else { - message.setColor(ChatColor.RED); - message.setText(args[0] + " has never been online."); - sender.sendMessage(message); + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + long secs = RedisBungee.getApi().getLastOnline(plugin.getUuidTranslator().getTranslatedUuid(args[0])); + TextComponent message = new TextComponent(); + if (secs == 0) { + message.setColor(ChatColor.GREEN); + message.setText(args[0] + " is currently online."); + sender.sendMessage(message); + } else if (secs != -1) { + message.setColor(ChatColor.BLUE); + message.setText(args[0] + " was last online on " + new SimpleDateFormat().format(secs) + "."); + sender.sendMessage(message); + } else { + message.setColor(ChatColor.RED); + message.setText(args[0] + " has never been online."); + sender.sendMessage(message); + } + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } } - } else { - sender.sendMessage(NO_PLAYER_SPECIFIED); - } + }); } } public static class IpCommand extends Command { - IpCommand() { + private final RedisBungee plugin; + + IpCommand(RedisBungee plugin) { super("ip", "redisbungee.command.ip", "playerip", "rip", "rplayerip"); + this.plugin = plugin; } @Override - public void execute(CommandSender sender, String[] args) { - if (args.length > 0) { - InetAddress ia = RedisBungee.getApi().getPlayerIp(args[0]); - if (ia != null) { - TextComponent message = new TextComponent(); - message.setColor(ChatColor.GREEN); - message.setText(args[0] + " is connected from " + ia.toString() + "."); - } else { - sender.sendMessage(PLAYER_NOT_FOUND); + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + InetAddress ia = RedisBungee.getApi().getPlayerIp(plugin.getUuidTranslator().getTranslatedUuid(args[0])); + if (ia != null) { + TextComponent message = new TextComponent(); + message.setColor(ChatColor.GREEN); + message.setText(args[0] + " is connected from " + ia.toString() + "."); + } else { + sender.sendMessage(PLAYER_NOT_FOUND); + } + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } } - } else { - sender.sendMessage(NO_PLAYER_SPECIFIED); - } + }); } } public static class SendToAll extends Command { - SendToAll() { + private final RedisBungee plugin; + + SendToAll(RedisBungee plugin) { super("sendtoall", "redisbungee.command.sendtoall", "rsendtoall"); + this.plugin = plugin; } @Override @@ -167,8 +209,11 @@ class RedisBungeeCommands { } public static class ServerId extends Command { - ServerId() { + private final RedisBungee plugin; + + ServerId(RedisBungee plugin) { super("serverid", "redisbungee.command.serverid", "rserverid"); + this.plugin = plugin; } @Override @@ -179,4 +224,18 @@ class RedisBungeeCommands { sender.sendMessage(textComponent); } } + + public static class ServerIds extends Command { + public ServerIds() { + super("serverids", "redisbungee.command.serverids"); + } + + @Override + public void execute(CommandSender sender, String[] strings) { + TextComponent textComponent = new TextComponent(); + textComponent.setText("All server IDs: " + Joiner.on(", ").join(RedisBungee.getApi().getAllServers())); + textComponent.setColor(ChatColor.YELLOW); + sender.sendMessage(textComponent); + } + } } diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeConsumer.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeConsumer.java new file mode 100644 index 0000000..8503e58 --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeConsumer.java @@ -0,0 +1,73 @@ +/** + * Copyright © 2013 tuxed + * This work is free. You can redistribute it and/or modify it under the + * terms of the Do What The Fuck You Want To Public License, Version 2, + * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + */ +package com.imaginarycode.minecraft.redisbungee; + +import com.imaginarycode.minecraft.redisbungee.consumerevents.ConsumerEvent; +import com.imaginarycode.minecraft.redisbungee.consumerevents.PlayerChangedServerConsumerEvent; +import com.imaginarycode.minecraft.redisbungee.consumerevents.PlayerLoggedInConsumerEvent; +import com.imaginarycode.minecraft.redisbungee.consumerevents.PlayerLoggedOffConsumerEvent; +import lombok.RequiredArgsConstructor; +import redis.clients.jedis.Jedis; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +@RequiredArgsConstructor +public class RedisBungeeConsumer implements Runnable { + private final RedisBungee plugin; + private BlockingQueue consumerQueue = new LinkedBlockingQueue<>(); + private boolean stopped = false; + + @Override + public void run() { + Jedis jedis = plugin.getPool().getResource(); + try { + while (!stopped) { + ConsumerEvent event; + try { + event = consumerQueue.take(); + } catch (InterruptedException e) { + continue; + } + handle(event, jedis); + } + for (ConsumerEvent event : consumerQueue) + handle(event, jedis); + consumerQueue.clear(); + } finally { + plugin.getPool().returnResource(jedis); + } + } + + private void handle(ConsumerEvent event, Jedis jedis) { + if (event instanceof PlayerLoggedInConsumerEvent) { + PlayerLoggedInConsumerEvent event1 = (PlayerLoggedInConsumerEvent) event; + jedis.sadd("server:" + RedisBungee.getConfiguration().getString("server-id", "") + ":usersOnline", event1.getPlayer().getUniqueId().toString()); + jedis.hset("player:" + event1.getPlayer().getUniqueId().toString(), "online", "0"); + jedis.hset("player:" + event1.getPlayer().getUniqueId().toString(), "ip", event1.getPlayer().getAddress().getAddress().getHostAddress()); + jedis.hset("player:" + event1.getPlayer().getUniqueId().toString(), "name", event1.getPlayer().getName()); + jedis.hset("uuids", event1.getPlayer().getName(), event1.getPlayer().getUniqueId().toString()); + } else if (event instanceof PlayerLoggedOffConsumerEvent) { + PlayerLoggedOffConsumerEvent event1 = (PlayerLoggedOffConsumerEvent) event; + jedis.hset("player:" + event1.getPlayer().getUniqueId().toString(), "online", String.valueOf(System.currentTimeMillis())); + RedisUtil.cleanUpPlayer(event1.getPlayer().getUniqueId().toString(), jedis); + } else if (event instanceof PlayerChangedServerConsumerEvent) { + PlayerChangedServerConsumerEvent event1 = (PlayerChangedServerConsumerEvent) event; + jedis.hset("player:" + event1.getPlayer().getUniqueId().toString(), "server", event1.getNewServer().getName()); + } + } + + public void queue(ConsumerEvent event) { + if (!stopped) + consumerQueue.add(event); + } + + public void stop() { + stopped = true; + while (!consumerQueue.isEmpty()) ; + } +} diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java index be3f93a..ae53dcc 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java @@ -1,9 +1,18 @@ +/** + * Copyright © 2013 tuxed + * This work is free. You can redistribute it and/or modify it under the + * terms of the Do What The Fuck You Want To Public License, Version 2, + * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + */ package com.imaginarycode.minecraft.redisbungee; import com.google.common.base.Joiner; import com.google.common.io.ByteArrayDataInput; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; +import com.imaginarycode.minecraft.redisbungee.consumerevents.PlayerChangedServerConsumerEvent; +import com.imaginarycode.minecraft.redisbungee.consumerevents.PlayerLoggedInConsumerEvent; +import com.imaginarycode.minecraft.redisbungee.consumerevents.PlayerLoggedOffConsumerEvent; import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; import lombok.AllArgsConstructor; import net.md_5.bungee.api.ServerPing; @@ -14,7 +23,9 @@ import net.md_5.bungee.event.EventHandler; import redis.clients.jedis.Jedis; import java.util.Collections; +import java.util.HashSet; import java.util.Set; +import java.util.UUID; @AllArgsConstructor public class RedisBungeeListener implements Listener { @@ -40,60 +51,17 @@ public class RedisBungeeListener implements Listener { @EventHandler public void onPlayerConnect(final PostLoginEvent event) { - if (plugin.getPool() != null) { - plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { - @Override - public void run() { - Jedis rsc = plugin.getPool().getResource(); - try { - rsc.sadd("server:" + RedisBungee.getConfiguration().getString("server-id", "") + ":usersOnline", event.getPlayer().getName()); - rsc.hset("player:" + event.getPlayer().getName(), "online", "0"); - rsc.hset("player:" + event.getPlayer().getName(), "ip", event.getPlayer().getAddress().getAddress().getHostAddress()); - rsc.hset("player:" + event.getPlayer().getName(), "name", event.getPlayer().getName()); - } finally { - plugin.getPool().returnResource(rsc); - } - } - }); - } - // I used to have a task that eagerly waited for the user to be connected. - // Well, upon further inspection of BungeeCord's source code, this turned - // out to not be needed at all, since ServerConnectedEvent is called anyway. + plugin.getConsumer().queue(new PlayerLoggedInConsumerEvent(event.getPlayer())); } @EventHandler public void onPlayerDisconnect(final PlayerDisconnectEvent event) { - if (plugin.getPool() != null) { - plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { - @Override - public void run() { - Jedis rsc = plugin.getPool().getResource(); - try { - rsc.hset("player:" + event.getPlayer().getName(), "online", String.valueOf(System.currentTimeMillis())); - RedisUtil.cleanUpPlayer(event.getPlayer().getName(), rsc); - } finally { - plugin.getPool().returnResource(rsc); - } - } - }); - } + plugin.getConsumer().queue(new PlayerLoggedOffConsumerEvent(event.getPlayer())); } @EventHandler public void onServerChange(final ServerConnectedEvent event) { - if (plugin.getPool() != null) { - plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { - @Override - public void run() { - Jedis rsc = plugin.getPool().getResource(); - try { - rsc.hset("player:" + event.getPlayer().getName(), "server", event.getServer().getInfo().getName()); - } finally { - plugin.getPool().returnResource(rsc); - } - } - }); - } + plugin.getConsumer().queue(new PlayerChangedServerConsumerEvent(event.getPlayer(), event.getServer().getInfo())); } @EventHandler @@ -101,11 +69,11 @@ public class RedisBungeeListener implements Listener { ServerPing old = event.getResponse(); ServerPing reply = new ServerPing(); if (RedisBungee.getConfiguration().getBoolean("player-list-in-ping", false)) { - Set players = plugin.getPlayers(); + Set players = plugin.getPlayers(); ServerPing.PlayerInfo[] info = new ServerPing.PlayerInfo[players.size()]; int idx = 0; - for (String player : players) { - info[idx] = new ServerPing.PlayerInfo(player, ""); + for (UUID player : players) { + info[idx] = new ServerPing.PlayerInfo(plugin.getUuidTranslator().getNameFromUuid(player), ""); idx++; } reply.setPlayers(new ServerPing.Players(old.getPlayers().getMax(), players.size(), info)); @@ -130,18 +98,21 @@ public class RedisBungeeListener implements Listener { switch (subchannel) { case "PlayerList": out.writeUTF("Players"); - Set source = Collections.emptySet(); + Set original = Collections.emptySet(); type = in.readUTF(); if (type.equals("ALL")) { out.writeUTF("ALL"); - source = plugin.getPlayers(); + original = plugin.getPlayers(); } else { try { - source = plugin.getPlayersOnServer(type); + original = plugin.getPlayersOnServer(type); } catch (IllegalArgumentException ignored) { } } - out.writeUTF(Joiner.on(',').join(source)); + Set players = new HashSet<>(); + for (UUID uuid : original) + players.add(plugin.getUuidTranslator().getNameFromUuid(uuid)); + out.writeUTF(Joiner.on(',').join(players)); break; case "PlayerCount": out.writeUTF("PlayerCount"); @@ -163,7 +134,7 @@ public class RedisBungeeListener implements Listener { String user = in.readUTF(); out.writeUTF("LastOnline"); out.writeUTF(user); - out.writeLong(plugin.getLastOnline(user)); + out.writeLong(plugin.getLastOnline(plugin.getUuidTranslator().getTranslatedUuid(user))); break; default: break; diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java index cb13445..3b59a4f 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java @@ -1,3 +1,9 @@ +/** + * Copyright © 2013 tuxed + * This work is free. You can redistribute it and/or modify it under the + * terms of the Do What The Fuck You Want To Public License, Version 2, + * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + */ package com.imaginarycode.minecraft.redisbungee; import redis.clients.jedis.Jedis; diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/ConsumerEvent.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/ConsumerEvent.java new file mode 100644 index 0000000..6d2acc1 --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/ConsumerEvent.java @@ -0,0 +1,10 @@ +/** + * Copyright © 2013 tuxed + * This work is free. You can redistribute it and/or modify it under the + * terms of the Do What The Fuck You Want To Public License, Version 2, + * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + */ +package com.imaginarycode.minecraft.redisbungee.consumerevents; + +public interface ConsumerEvent { +} diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/PlayerChangedServerConsumerEvent.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/PlayerChangedServerConsumerEvent.java new file mode 100644 index 0000000..ca704f8 --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/PlayerChangedServerConsumerEvent.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2013 tuxed + * This work is free. You can redistribute it and/or modify it under the + * terms of the Do What The Fuck You Want To Public License, Version 2, + * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + */ +package com.imaginarycode.minecraft.redisbungee.consumerevents; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; + +@AllArgsConstructor +@Getter +public class PlayerChangedServerConsumerEvent implements ConsumerEvent { + private final ProxiedPlayer player; + private final ServerInfo newServer; +} diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/PlayerLoggedInConsumerEvent.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/PlayerLoggedInConsumerEvent.java new file mode 100644 index 0000000..871a5e5 --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/PlayerLoggedInConsumerEvent.java @@ -0,0 +1,17 @@ +/** + * Copyright © 2013 tuxed + * This work is free. You can redistribute it and/or modify it under the + * terms of the Do What The Fuck You Want To Public License, Version 2, + * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + */ +package com.imaginarycode.minecraft.redisbungee.consumerevents; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.md_5.bungee.api.connection.ProxiedPlayer; + +@AllArgsConstructor +@Getter +public class PlayerLoggedInConsumerEvent implements ConsumerEvent { + private final ProxiedPlayer player; +} diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/PlayerLoggedOffConsumerEvent.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/PlayerLoggedOffConsumerEvent.java new file mode 100644 index 0000000..67066f1 --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/consumerevents/PlayerLoggedOffConsumerEvent.java @@ -0,0 +1,17 @@ +/** + * Copyright © 2013 tuxed + * This work is free. You can redistribute it and/or modify it under the + * terms of the Do What The Fuck You Want To Public License, Version 2, + * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + */ +package com.imaginarycode.minecraft.redisbungee.consumerevents; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.md_5.bungee.api.connection.ProxiedPlayer; + +@AllArgsConstructor +@Getter +public class PlayerLoggedOffConsumerEvent implements ConsumerEvent { + private final ProxiedPlayer player; +} diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java index 0f12e4a..1d62651 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java @@ -11,7 +11,7 @@ import net.md_5.bungee.api.plugin.Event; /** * This event is posted when a PubSub message is received. - * + *

* Warning: This event is fired in a separate thread! * * @since 0.2.6 diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/util/NameFetcher.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/NameFetcher.java new file mode 100644 index 0000000..754c982 --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/NameFetcher.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2013 tuxed + * This work is free. You can redistribute it and/or modify it under the + * terms of the Do What The Fuck You Want To Public License, Version 2, + * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + */ +package com.imaginarycode.minecraft.redisbungee.util; + +import com.google.common.collect.ImmutableList; +import com.google.gson.reflect.TypeToken; +import com.imaginarycode.minecraft.redisbungee.RedisBungee; + +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Callable; + +/* Credits to evilmidget38 for this class. I modified it to use Gson. */ +public class NameFetcher implements Callable> { + private static final String PROFILE_URL = "https://sessionserver.mojang.com/session/minecraft/profile/"; + private final List uuids; + + public NameFetcher(List uuids) { + this.uuids = ImmutableList.copyOf(uuids); + } + + @Override + public Map call() throws Exception { + Map uuidStringMap = new HashMap<>(); + for (UUID uuid : uuids) { + HttpURLConnection connection = (HttpURLConnection) new URL(PROFILE_URL + uuid.toString().replace("-", "")).openConnection(); + Map response = RedisBungee.getGson().fromJson(new InputStreamReader(connection.getInputStream()), new TypeToken>() { + }.getType()); + String name = response.get("name"); + if (name == null) { + continue; + } + String cause = response.get("cause"); + String errorMessage = response.get("errorMessage"); + if (cause != null && cause.length() > 0) { + throw new IllegalStateException(errorMessage); + } + uuidStringMap.put(uuid, name); + } + return uuidStringMap; + } +} \ No newline at end of file diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/util/UUIDFetcher.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/UUIDFetcher.java new file mode 100644 index 0000000..031a2fb --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/UUIDFetcher.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2013 tuxed + * This work is free. You can redistribute it and/or modify it under the + * terms of the Do What The Fuck You Want To Public License, Version 2, + * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + */ +package com.imaginarycode.minecraft.redisbungee.util; + +import com.google.common.collect.ImmutableList; +import com.imaginarycode.minecraft.redisbungee.RedisBungee; + +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.Callable; + +/* Credits to evilmidget38 for this class. I modified it to use Gson. */ +public class UUIDFetcher implements Callable> { + private static final double PROFILES_PER_REQUEST = 100; + private static final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft"; + private final List names; + private final boolean rateLimiting; + + public UUIDFetcher(List names, boolean rateLimiting) { + this.names = ImmutableList.copyOf(names); + this.rateLimiting = rateLimiting; + } + + public UUIDFetcher(List names) { + this(names, true); + } + + public Map call() throws Exception { + Map uuidMap = new HashMap<>(); + int requests = (int) Math.ceil(names.size() / PROFILES_PER_REQUEST); + for (int i = 0; i < requests; i++) { + HttpURLConnection connection = createConnection(); + String body = RedisBungee.getGson().toJson(names.subList(i * 100, Math.min((i + 1) * 100, names.size()))); + writeBody(connection, body); + Profile[] array = RedisBungee.getGson().fromJson(new InputStreamReader(connection.getInputStream()), Profile[].class); + for (Profile profile : array) { + UUID uuid = UUIDFetcher.getUUID(profile.id); + uuidMap.put(profile.name, uuid); + } + if (rateLimiting && i != requests - 1) { + Thread.sleep(100L); + } + } + return uuidMap; + } + + private static void writeBody(HttpURLConnection connection, String body) throws Exception { + OutputStream stream = connection.getOutputStream(); + stream.write(body.getBytes()); + stream.flush(); + stream.close(); + } + + private static HttpURLConnection createConnection() throws Exception { + URL url = new URL(PROFILE_URL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setDoOutput(true); + return connection; + } + + private static UUID getUUID(String id) { + return UUID.fromString(id.substring(0, 8) + "-" + id.substring(8, 12) + "-" + id.substring(12, 16) + "-" + id.substring(16, 20) + "-" + id.substring(20, 32)); + } + + public static byte[] toBytes(UUID uuid) { + ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); + byteBuffer.putLong(uuid.getMostSignificantBits()); + byteBuffer.putLong(uuid.getLeastSignificantBits()); + return byteBuffer.array(); + } + + public static UUID fromBytes(byte[] array) { + if (array.length != 16) { + throw new IllegalArgumentException("Illegal byte array length: " + array.length); + } + ByteBuffer byteBuffer = ByteBuffer.wrap(array); + long mostSignificant = byteBuffer.getLong(); + long leastSignificant = byteBuffer.getLong(); + return new UUID(mostSignificant, leastSignificant); + } + + public static UUID getUUIDOf(String name) throws Exception { + return new UUIDFetcher(Collections.singletonList(name)).call().get(name); + } + + private class Profile { + String id; + String name; + } +} \ No newline at end of file diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/util/UUIDTranslator.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/UUIDTranslator.java new file mode 100644 index 0000000..35225d7 --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/UUIDTranslator.java @@ -0,0 +1,107 @@ +/** + * Copyright © 2013 tuxed + * This work is free. You can redistribute it and/or modify it under the + * terms of the Do What The Fuck You Want To Public License, Version 2, + * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + */ +package com.imaginarycode.minecraft.redisbungee.util; + +import com.google.common.base.Charsets; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Maps; +import com.imaginarycode.minecraft.redisbungee.RedisBungee; +import lombok.RequiredArgsConstructor; +import net.md_5.bungee.api.ProxyServer; +import redis.clients.jedis.Jedis; + +import java.util.Collections; +import java.util.UUID; +import java.util.logging.Level; +import java.util.regex.Pattern; + +@RequiredArgsConstructor +public class UUIDTranslator { + private final RedisBungee plugin; + private BiMap uuidMap = Maps.synchronizedBiMap(HashBiMap.create()); + public static final Pattern UUID_PATTERN = Pattern.compile("[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"); + + public UUID getTranslatedUuid(String player) { + if (ProxyServer.getInstance().getPlayer(player) != null) + return ProxyServer.getInstance().getPlayer(player).getUniqueId(); + + UUID uuid = uuidMap.get(player); + if (uuid != null) + return uuid; + + if (!plugin.getProxy().getConfig().isOnlineMode()) { + uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + player).getBytes(Charsets.UTF_8)); + uuidMap.put(player, uuid); + return uuid; + } + + // Okay, it wasn't locally cached. Let's try Redis. + Jedis jedis = plugin.getPool().getResource(); + try { + String stored = jedis.hget("uuids", player); + if (stored != null && UUID_PATTERN.matcher(stored).find()) { + // This is it! + uuid = UUID.fromString(stored); + uuidMap.put(stored, UUID.fromString(stored)); + return uuid; + } + + // That didn't work. Let's ask Mojang. + uuid = UUIDFetcher.getUUIDOf(player); + + if (uuid != null) { + uuidMap.put(stored, uuid); + jedis.hset("uuids", player, uuid.toString()); + } + + return uuid; + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Unable to fetch UUID for " + player, e); + return null; + } finally { + plugin.getPool().returnResource(jedis); + } + } + + public String getNameFromUuid(UUID player) { + if (ProxyServer.getInstance().getPlayer(player) != null) + return ProxyServer.getInstance().getPlayer(player).getName(); + + String name = uuidMap.inverse().get(player); + + if (name != null) + return name; + + // Okay, it wasn't locally cached. Let's try Redis. + Jedis jedis = plugin.getPool().getResource(); + try { + String stored = jedis.hget("player:" + player, "name"); + if (stored != null) { + name = stored; + uuidMap.put(name, player); + return name; + } + + // That didn't work. Let's ask Mojang. + name = new NameFetcher(Collections.singletonList(player)).call().get(player); + + if (name != null) { + jedis.hset("player:" + player, "name", name); + uuidMap.put(name, player); + return name; + } + + return null; + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Unable to fetch name for " + player, e); + return null; + } finally { + plugin.getPool().returnResource(jedis); + } + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index bf79ab6..5637954 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: RedisBungee main: com.imaginarycode.minecraft.redisbungee.RedisBungee -version: 0.2.6 +version: 0.3 author: tuxed # This is used so that we can automagically override default BungeeCord behavior. softDepends: ["cmd_find", "cmd_list"] \ No newline at end of file