package io.github.lucaseasedup.logit.session; import static io.github.lucaseasedup.logit.message.MessageHelper.sendMsg; import static io.github.lucaseasedup.logit.message.MessageHelper.t; import io.github.lucaseasedup.logit.CancelledState; import io.github.lucaseasedup.logit.LogItCoreObject; import io.github.lucaseasedup.logit.account.AccountManager.RegistrationFetchMode; import io.github.lucaseasedup.logit.config.TimeUnit; import io.github.lucaseasedup.logit.storage.DataType; import io.github.lucaseasedup.logit.storage.SqliteStorage; import io.github.lucaseasedup.logit.storage.Storage; import io.github.lucaseasedup.logit.storage.StorageEntry; import io.github.lucaseasedup.logit.storage.UnitKeys; import io.github.lucaseasedup.logit.util.CollectionUtils; import io.github.lucaseasedup.logit.util.PlayerUtils; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.entity.Player; /** * Provides a facility manage login sessions. */ public final class SessionManager extends LogItCoreObject implements Runnable { /** * Do not call directly. */ @Override public void dispose() { if (sessions != null) { sessions.clear(); sessions = null; } } /** * Internal method. Do not call directly. */ @Override public void run() { boolean timeoutEnabled = getConfig("config.yml") .getBoolean("forceLogin.timeout.enabled"); long timeoutValueLogin = getConfig("config.yml") .getTime("forceLogin.timeout.value.login", TimeUnit.TICKS); long timeoutValueRegister = getConfig("config.yml") .getTime("forceLogin.timeout.value.register", TimeUnit.TICKS); List<String> disableTimeoutForPlayers = getConfig("config.yml") .getStringList("forceLogin.timeout.disableForPlayers"); boolean automaticLogoutEnabled = getConfig("config.yml") .getBoolean("automaticLogout.enabled"); long inactivityTimeToLogOut = getConfig("config.yml") .getTime("automaticLogout.inactivityTime", TimeUnit.TICKS); for (Map.Entry<String, Session> entry : sessions.entrySet()) { String username = entry.getKey(); Session session = entry.getValue(); Player player = Bukkit.getPlayerExact(username); // Player is logged in, either online or offline. if (session.getStatus() >= 0L) { // If player is online. if (player != null) { session.setStatus(0L); if (automaticLogoutEnabled) { if (session.getInactivityTime() >= inactivityTimeToLogOut) { endSession(username); sendMsg(player, t("automaticallyLoggedOut")); if (getCore().isPlayerForcedToLogIn(player)) { getMessageDispatcher().sendForceLoginMessage(player); } session.resetInactivityTime(); } else { session.advanceInactivityTime(TASK_PERIOD); } } } else if (session.getIp() != null) { destroySession(username); } } // Player is online but otherwise logged out. else if (player != null) { boolean disableTimeoutForPlayer = CollectionUtils.containsIgnoreCase( username, disableTimeoutForPlayers ); if (!disableTimeoutForPlayer && getCore().isPlayerForcedToLogIn(player)) { boolean loginTimeoutElapsed = session.getStatus() <= -timeoutValueLogin; boolean registerTimeoutElapsed = session.getStatus() <= -timeoutValueRegister; boolean timedOut; if (!loginTimeoutElapsed && !registerTimeoutElapsed) { timedOut = false; } else { boolean playerRegistered = getAccountManager().isRegistered(username, RegistrationFetchMode.STORAGE_FALLBACK); timedOut = (playerRegistered && loginTimeoutElapsed) || (!playerRegistered && registerTimeoutElapsed); } if (timeoutEnabled && timedOut) { player.kickPlayer(t("forcedLoginTimeout")); } else { session.updateStatus(-TASK_PERIOD); } } } // Player is logged out and offline. else { destroySession(username); } } } /** * Returns a {@code Session} object associated with a specific username. * * @param username * The username. * * @return The {@code Session} object, or {@code null} if no session * is associated with this username. */ public Session getSession(String username) { return sessions.get(username.toLowerCase()); } /** * Returns a {@code Session} object associated with a specific player. * * @param player * The player. * * @return The {@code Session} object, or {@code null} if no session * is associated with this player. */ public Session getSession(Player player) { return getSession(player.getName()); } /** * Checks whether the session associated with a specific username is alive. * * @param username * The username. * * @return {@code true} if such session exists, is alive and, * if a player with this username is online, player IP matches * session IP; {@code false} otherwise. * * @throws IllegalArgumentException * If {@code username} is {@code null}. * * @see #isSessionAlive(Player) */ public boolean isSessionAlive(String username) { if (username == null) throw new IllegalArgumentException(); Session session = getSession(username); if (session == null) return false; if (PlayerUtils.isPlayerOnline(username)) { Player player = Bukkit.getPlayerExact(username); String ip = PlayerUtils.getPlayerIp(player); return session.isAlive() && (ip == null || ip.equals(session.getIp())); } else { return session.isAlive(); } } /** * Checks whether a session associated with a specific player is alive. * * @param player * The player. * * @return {@code true} if {@code player} is not {@code null}, * such session exists, is alive and player IP matches session IP; * {@code false} otherwise. */ public boolean isSessionAlive(Player player) { if (player == null) return false; Session session = getSession(player); if (session == null) return false; String ip = PlayerUtils.getPlayerIp(player); return session.isAlive() && (ip == null || ip.equals(session.getIp())); } /** * Creates a new session and associates it with a specific player. * * <p> If a session for this player already exists, no action will be taken. * * <p> This method emits the {@code SessionCreateEvent} event. * * @param player * The player. * * @return A {@code CancellableState} indicating whether this operation * has been cancelled by one of the {@code SessionCreateEvent} * handlers. * * @throws IllegalArgumentException * If {@code player} is {@code null}. */ public CancelledState createSession(Player player) { if (player == null) throw new IllegalArgumentException(); String username = player.getName().toLowerCase(); String ip = PlayerUtils.getPlayerIp(player); if (getSession(player) != null) return CancelledState.NOT_CANCELLED; SessionEvent evt = new SessionCreateEvent(username); Bukkit.getPluginManager().callEvent(evt); if (evt.isCancelled()) return CancelledState.CANCELLED; // Create session. Session session = new Session(ip); sessions.put(username, session); log(Level.FINE, t("createSession.success.log") .replace("{0}", username)); return CancelledState.NOT_CANCELLED; } /** * Destroys a session associated with a specific username. * * <p> If no session for this username exists, no action will be taken. * If the session exists and is alive, this method will try to end it * before proceeding. * * <p> This method emits the {@code SessionDestroyEvent} event. * * @param username * The username. * * @return A {@code CancellableState} indicating whether this operation * has been cancelled by one of the {@code SessionDestroyEvent} * handlers. * * @throws IllegalArgumentException * If {@code username} is {@code null}. */ public CancelledState destroySession(String username) { if (username == null) throw new IllegalArgumentException(); Session session = getSession(username); if (session == null) return CancelledState.NOT_CANCELLED; if (session.isAlive()) { endSession(username); } SessionEvent evt = new SessionDestroyEvent(username, session); Bukkit.getPluginManager().callEvent(evt); if (evt.isCancelled()) return CancelledState.CANCELLED; sessions.remove(username.toLowerCase()); log(Level.FINE, t("destroySession.success.log") .replace("{0}", username.toLowerCase())); return CancelledState.NOT_CANCELLED; } /** * Destroys a session associated with a specific player. * * <p> If no session for this player exists, no action will be taken. * If the session exists and is alive, this method will try to end it * before proceeding. * * <p> This method emits the {@code SessionDestroyEvent} event. * * @param player * The player. * * @return A {@code CancellableState} indicating whether this operation * has been cancelled by one of the {@code SessionDestroyEvent} * handlers. * * @throws IllegalArgumentException * If {@code player} is {@code null}. */ public CancelledState destroySession(Player player) { if (player == null) throw new IllegalArgumentException(); return destroySession(player.getName()); } /** * Starts a session associated with a specific player. * * <p> If the session has already been started (e.i. is alive), * no action will be taken. If no session exists for this player, * it will be created. * * <p> This method emits the {@code SessionStartEvent} event. * * @param player * The player. * * @return A {@code CancellableState} indicating whether this operation * has been cancelled by one of the {@code SessionStartEvent} * handlers. * * @throws IllegalArgumentException * If {@code player} is {@code null}. */ public CancelledState startSession(Player player) { if (player == null) throw new IllegalArgumentException(); String username = player.getName().toLowerCase(); Session session = getSession(player); if (session == null) { createSession(player); session = getSession(player); } if (session.isAlive()) return CancelledState.NOT_CANCELLED; SessionEvent evt = new SessionStartEvent(username, session); Bukkit.getPluginManager().callEvent(evt); if (evt.isCancelled()) return CancelledState.CANCELLED; // Start the session. session.setStatus(0L); log(Level.FINE, t("startSession.success.log") .replace("{0}", username)); return CancelledState.NOT_CANCELLED; } /** * Starts a session associated with a specific username. * * <p> If the session has already been started (e.i. is alive), * no action will be taken. If no session exists for this username, * it will be created. * * <p> This method emits the {@code SessionStartEvent} event. * * @param username * The username. * * @return A {@code CancellableState} indicating whether this operation * has been cancelled by one of the {@code SessionStartEvent} * handlers. * * @throws IllegalArgumentException * If {@code player} is {@code null}. * * @deprecated Use {@link #startSession(Player)} instead. */ @Deprecated public CancelledState startSession(String username) { if (username == null) throw new IllegalArgumentException(); Player player = PlayerUtils.getPlayer(username); if (player != null) { return startSession(player); } else { return CancelledState.NOT_CANCELLED; } } /** * Ends a session associated with a specific username. * * <p> If the session is not alive or does not exist, * no action will be taken. * * <p> This method emits the {@code SessionEndEvent} event. * * @param username * The username. * * @return A {@code CancellableState} indicating whether this operation * has been cancelled by one of the {@code SessionEndEvent} * handlers. * * @throws IllegalArgumentException * If {@code username} is {@code null}. */ public CancelledState endSession(String username) { if (username == null) throw new IllegalArgumentException(); Session session = getSession(username); if (session == null) return CancelledState.NOT_CANCELLED; if (!session.isAlive()) return CancelledState.NOT_CANCELLED; SessionEvent evt = new SessionEndEvent(username, session); Bukkit.getPluginManager().callEvent(evt); if (evt.isCancelled()) return CancelledState.CANCELLED; // End the session. session.setStatus(-1L); log(Level.FINE, t("endSession.success.log") .replace("{0}", username.toLowerCase())); return CancelledState.NOT_CANCELLED; } /** * Ends a session associated with a specific player. * * <p> If the session is not alive or does not exist, * no action will be taken. * * <p> This method emits the {@code SessionEndEvent} event. * * @param player * The player. * * @return A {@code CancellableState} indicating whether this operation * has been cancelled by one of the {@code SessionEndEvent} * handlers. * * @throws IllegalArgumentException * If {@code player} is {@code null}. */ public CancelledState endSession(Player player) { if (player == null) throw new IllegalArgumentException(); return endSession(player.getName()); } /** * Returns an iterator over the sessions in this {@code SessionManager}. * * <p> Element removal is not supported. * * @return The session iterator. */ public Iterator<Map.Entry<String, Session>> sessionIterator() { return newSessionIterator(); } private Iterator<Map.Entry<String, Session>> newSessionIterator() { return new Iterator<Map.Entry<String, Session>>() { @Override public void remove() { throw new UnsupportedOperationException(); } @Override public Map.Entry<String, Session> next() { return it.next(); } @Override public boolean hasNext() { return it.hasNext(); } private final Iterator<Map.Entry<String, Session>> it = sessions.entrySet().iterator(); }; } /** * Exports all sessions from this {@code SessionManager} to a file. * * <p> The file will be deleted before exporting. * * @param file * The file to which the sessions will be exported. * * @throws IOException * If an I/O error occurred. * * @throws IllegalArgumentException * If {@code file} is {@code null}. */ public void exportSessions(File file) throws IOException { if (file == null) throw new IllegalArgumentException(); file.delete(); try (Storage sessionsStorage = new SqliteStorage("jdbc:sqlite:" + file)) { UnitKeys keys = new UnitKeys(); keys.put("username", DataType.TINYTEXT); keys.put("status", DataType.INTEGER); keys.put("ip", DataType.TINYTEXT); sessionsStorage.connect(); sessionsStorage.createUnit("sessions", keys, "username"); sessionsStorage.setAutobatchEnabled(true); for (Map.Entry<String, Session> e : sessions.entrySet()) { sessionsStorage.addEntry("sessions", new StorageEntry.Builder() .put("username", e.getKey()) .put("status", String.valueOf(e.getValue().getStatus())) .put("ip", e.getValue().getIp()) .build()); } sessionsStorage.executeBatch(); sessionsStorage.clearBatch(); sessionsStorage.setAutobatchEnabled(false); } } /** * Imports all sessions from a file to this {@code SessionManager}. * * <p> Only the sessions that don't exist in this {@code SessionManager} * will be imported. * * @param file * The file from which the sessions will be imported. * * @throws FileNotFoundException * If no such file exists. * * @throws IOException * If an I/O error occurred. * * @throws IllegalArgumentException * If {@code file} is {@code null}. */ public void importSessions(File file) throws FileNotFoundException, IOException { if (file == null) throw new IllegalArgumentException(); if (!file.isFile()) throw new FileNotFoundException(); try (Storage sessionsStorage = new SqliteStorage("jdbc:sqlite:" + file)) { sessionsStorage.connect(); List<StorageEntry> entries = sessionsStorage.selectEntries("sessions", Arrays.asList("username", "status", "ip")); for (StorageEntry entry : entries) { String username = entry.get("username"); if (getSession(username) == null) { String ip = entry.get("ip"); long status = Long.parseLong(entry.get("status")); Session session = new Session(ip); session.setStatus(status); sessions.put(username, session); } } } } /** * Recommended task period of {@code SessionManager} running as a Bukkit task. */ public static final long TASK_PERIOD = TimeUnit.TICKS.convertTo(1, TimeUnit.TICKS); private Map<String, Session> sessions = new ConcurrentHashMap<>(); }