package tc.oc.commons.bungee.listeners;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.collect.MapMaker;
import com.google.common.util.concurrent.Futures;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.AsyncEvent;
import net.md_5.bungee.api.event.LoginEvent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.event.ServerConnectEvent;
import net.md_5.bungee.api.plugin.Cancellable;
import net.md_5.bungee.api.plugin.Event;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import tc.oc.api.bungee.users.BungeeUserStore;
import tc.oc.api.docs.PlayerId;
import tc.oc.api.docs.Server;
import tc.oc.api.docs.User;
import tc.oc.api.docs.virtual.ServerDoc;
import tc.oc.api.minecraft.MinecraftService;
import tc.oc.api.users.LoginRequest;
import tc.oc.api.users.LoginResponse;
import tc.oc.api.users.LogoutRequest;
import tc.oc.api.users.UserService;
import tc.oc.api.util.Permissions;
import tc.oc.commons.bungee.sessions.MojangSessionServiceMonitor;
import tc.oc.commons.bungee.sessions.SessionState;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.util.SystemFutureCallback;
import tc.oc.minecraft.protocol.MinecraftVersion;
@Singleton
public class LoginListener implements Listener, PluginFacet {
private static final String INTERNAL_ERROR = "Internal error\n\nPlease try again later";
private static final String NOT_ALLOWED = "You are not allowed on this server";
private final Logger logger;
private final Plugin plugin;
private final ProxyServer proxy;
private final MinecraftService minecraftService;
private final UserService userService;
private final BungeeUserStore userStore;
private final MojangSessionServiceMonitor mojangSessionServiceMonitor;
private final ConcurrentMap<PendingConnection, User> pendingConnections = new MapMaker().weakKeys().makeMap();
private final ConcurrentMap<PendingConnection, ServerInfo> initialServers = new MapMaker().weakKeys().makeMap();
@Inject LoginListener(Loggers loggers, Plugin plugin, ProxyServer proxy, MinecraftService minecraftService, UserService userService, BungeeUserStore userStore, MojangSessionServiceMonitor mojangSessionServiceMonitor) {
this.proxy = proxy;
this.mojangSessionServiceMonitor = mojangSessionServiceMonitor;
this.logger = loggers.get(getClass());
this.plugin = plugin;
this.minecraftService = minecraftService;
this.userService = userService;
this.userStore = userStore;
}
private void log(PendingConnection connection, String message) {
logger.info("[" + connection.getAddress().getAddress().getHostAddress() +
" " + connection.getName() +
" " + connection.getUniqueId() +
"] " + message);
}
@EventHandler
public void preLogin(final PreLoginEvent event) {
if(mojangSessionServiceMonitor.getState() == SessionState.OFFLINE) {
event.getConnection().setOnlineMode(false);
log(event.getConnection(), "Starting offline login");
doLogin(event, event.getConnection());
}
}
@EventHandler
public void login(final LoginEvent event) {
if(!pendingConnections.containsKey(event.getConnection())) {
log(event.getConnection(), "Starting online login");
doLogin(event, event.getConnection());
}
}
@EventHandler
public void postLogin(final PostLoginEvent event) {
final User user = pendingConnections.remove(event.getPlayer().getPendingConnection());
if(user != null) {
log(event.getPlayer().getPendingConnection(), "Completing login");
userStore.addUser(event.getPlayer(), user);
applyPermissions(event.getPlayer(), user);
updateServerStatus(proxy.getOnlineCount());
} else {
logger.severe("No pending connection for " + event.getPlayer().getName() + ":" + event.getPlayer().getUniqueId());
event.getPlayer().disconnect(INTERNAL_ERROR);
}
}
private void doLogin(final AsyncEvent event, final PendingConnection connection) {
event.registerIntent(this.plugin);
final Server localServer = minecraftService.getLocalServer();
// null if called from preLogin
final UUID uuid = connection.getUniqueId();
String username = null;
if(uuid != null && localServer.fake_usernames() != null) {
username = minecraftService.getLocalServer().fake_usernames().get(uuid);
}
if(username == null) {
username = connection.getName();
}
final LoginRequest loginRequest = new LoginRequest(username,
uuid,
connection.getAddress().getAddress(),
minecraftService.getLocalServer(),
connection.getVirtualHost(),
false,
MinecraftVersion.describeProtocol(connection.getVersion()));
Futures.addCallback(userService.login(loginRequest), new SystemFutureCallback<LoginResponse>() {
@Override
public void onSuccessThrows(LoginResponse response) {
if(response.kick() != null) {
// NOTE: if login is cancelled, other fields in the response may be null
this.finish(true, response.message());
return;
}
final Map<String, Boolean> permissions = mergePermissions(response.user());
if(!Boolean.TRUE.equals(permissions.get(Permissions.LOGIN))) {
this.finish(true, NOT_ALLOWED);
return;
}
// If we are doing an offline login, UUID will be null at this point,
// and we need to set it to the correct one from the user document,
// so that Bungee doesn't set it to a random one later.
if(uuid == null) {
connection.setUniqueId(response.user().uuid());
}
pendingConnections.put(connection, response.user());
if (response.route_to_server() != null) {
ServerInfo target = proxy.getServerInfo(response.route_to_server());
if (target == null) {
this.finish(true, "Routing to server failed\n\nPlease try again later");
return;
}
initialServers.put(connection, target);
}
this.finish(false, response.message());
}
@Override
public void onFailure(Throwable throwable) {
super.onFailure(throwable);
this.finish(true, INTERNAL_ERROR);
}
private void finish(boolean cancelled, String message) {
if(cancelled) {
log(connection, "Denying login: " + message);
cancelEvent(event, message);
} else {
log(connection, "Allowing login");
}
event.completeIntent(LoginListener.this.plugin);
}
});
}
private Map<String, Boolean> mergePermissions(User user) {
return Permissions.mergePermissions(minecraftService.getLocalServer().realms(), user.mc_permissions_by_realm());
}
private void applyPermissions(ProxiedPlayer player, User user) {
for(Map.Entry<String, Boolean> entry : mergePermissions(user).entrySet()) {
player.setPermission(entry.getKey(), entry.getValue());
}
}
private void cancelEvent(Event event, String reason) {
if(event instanceof Cancellable) {
((Cancellable) event).setCancelled(true);
}
if((event instanceof LoginEvent)) {
((LoginEvent) event).setCancelReason(reason);
} else if(event instanceof PreLoginEvent) {
((PreLoginEvent) event).setCancelReason(reason);
}
}
@EventHandler(priority = EventPriority.LOW)
public void connect(ServerConnectEvent event) {
ServerInfo target = initialServers.remove(event.getPlayer().getPendingConnection());
if(target != null) {
log(event.getPlayer().getPendingConnection(), "Routing to initial server " + target.getName());
event.setTarget(target);
}
}
@EventHandler
public void disconnect(final PlayerDisconnectEvent event) {
pendingConnections.remove(event.getPlayer().getPendingConnection());
PlayerId playerId = userStore.removeUser(event.getPlayer());
if(playerId != null) {
log(event.getPlayer().getPendingConnection(), "Logging out");
LogoutRequest logoutRequest = new LogoutRequest(playerId, this.minecraftService.getLocalServer());
this.userService.logout(logoutRequest);
updateServerStatus(proxy.getOnlineCount() - 1); // Adjust for disconnecting player included in count
}
}
private void updateServerStatus(int count) {
minecraftService.updateLocalServer(new ServerDoc.StatusUpdate() {
@Override public int max_players() {
return proxy.getConfig().getPlayerLimit();
}
@Override public int num_observing() {
return count;
}
});
}
}