/** * This file is part of Oracle, licensed under the MIT License (MIT). * * Copyright (c) 2015 Helion3 http://helion3.com/ * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.helion3.oracle; import java.io.File; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; import com.helion3.oracle.commands.LockdownCommand; import com.helion3.oracle.utils.PlaytimeUtil; import ninja.leaping.configurate.commented.CommentedConfigurationNode; import ninja.leaping.configurate.loader.ConfigurationLoader; import org.slf4j.Logger; import org.spongepowered.api.Game; import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.game.state.GameStartedServerEvent; import org.spongepowered.api.event.game.state.GameStoppingServerEvent; import org.spongepowered.api.plugin.Plugin; import org.spongepowered.api.scheduler.Task; import org.spongepowered.api.service.ban.BanService; import org.spongepowered.api.config.DefaultConfig; import org.spongepowered.api.text.Text; import com.google.inject.Inject; import com.helion3.oracle.bans.OracleBanService; import com.helion3.oracle.commands.LookupCommand; import com.helion3.oracle.commands.OracleCommands; import com.helion3.oracle.commands.PlayedCommand; import com.helion3.oracle.commands.SeenCommand; import com.helion3.oracle.listeners.PlayerJoinListener; import com.helion3.oracle.listeners.PlayerQuitListener; import com.helion3.oracle.players.PlayerIdentification; import com.helion3.oracle.players.PluginPlayer; import com.helion3.oracle.tasks.PlaytimeMonitor; import com.helion3.oracle.utils.AnnouncementUtil; import com.helion3.oracle.utils.JoinUtil; import com.helion3.oracle.utils.ServerUtil; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; @Plugin(id = "oracle", name = "oracle", version = "2.2") final public class Oracle { private static List<UUID> allowedInLockdown = new ArrayList<>(); private static Configuration config; private static boolean inLockdown; private static Game game; private static Logger logger; private static Oracle plugin; private static HikariDataSource pool; private int lastAnnouncement = 0; public static HashMap<UUID,PluginPlayer> oraclePlayers = new HashMap<UUID,PluginPlayer>(); public static HashMap<Player,Integer> playtimeHours = new HashMap<Player,Integer>(); public static int oracleServer = 0; private Task disconnectsTask; @Inject @DefaultConfig(sharedRoot = false) private File defaultConfig; @Inject @DefaultConfig(sharedRoot = false) private ConfigurationLoader<CommentedConfigurationNode> configManager; @Inject public void setGame(Game injectGame) { game = injectGame; } /** * Performs bootstrapping of Prism resources/objects. * * @param event Server started */ @Listener public void onServerStart(GameStartedServerEvent event) { plugin = this; // Load configuration file config = new Configuration(defaultConfig, configManager); // init db pool = initDbPool(); Connection testConn = dbc(); if( pool == null || testConn == null ){ String dbDisabled = "Oracle will disable itself because it couldn't connect to a database.\n"; dbDisabled += "If you're using MySQL, check your config. Be sure MySQL is running.\n"; getLogger().error(dbDisabled); // @todo disable plugin return; } if(testConn != null){ try { testConn.close(); } catch (SQLException e) { logDbError( e ); } } // Setup databases setupDatabase(); // Providers if (getConfig().getNode("bans", "enabled").getBoolean()) { game.getServiceManager().setProvider(this, BanService.class, new OracleBanService()); } // Cache server id ServerUtil.lookupServer(getConfig().getNode("server-name").getString()); // Cache online players on reload PlayerIdentification.cacheOnlinePlayerPrimaryKeys(); // Create join records for all currently online players for (Player pl : getGame().getServer().getOnlinePlayers()){ JoinUtil.registerPlayerJoin( pl, getGame().getServer().getOnlinePlayers().size() ); } // Register tasks catchUncaughtDisconnects(); runAnnouncements(); runPlaytimeMonitor(); // Register event listeners if (Oracle.getConfig().getNode("joins", "enabled").getBoolean()) { game.getEventManager().registerListeners(this, new PlayerJoinListener(this)); game.getEventManager().registerListeners(this, new PlayerQuitListener(this)); } // Register commands game.getCommandManager().register(this, OracleCommands.getCommand(game), "oracle"); game.getCommandManager().register(this, LockdownCommand.getCommand(), "lockdown"); game.getCommandManager().register(this, LookupCommand.getCommand(this), "lookup"); game.getCommandManager().register(this, PlayedCommand.getCommand(this), "played"); game.getCommandManager().register(this, SeenCommand.getCommand(this), "seen"); logger.info("Oracle is listening. Don't worry about the vase."); } /** * Check if a UUID is allowed during a lockdown. * * @param uuid * @return boolean */ public static boolean allowedInLockdown(UUID uuid) { return allowedInLockdown.contains(uuid); } /** * Disable lockdown mode. */ public static void disableLockdown() { inLockdown = false; allowedInLockdown.clear(); } /** * Enable lockdown mode. Rejects connections of * all players with less than a few hours of playtime. */ public static void enableLockdown() { // Run async so the query doesn't lag the main thread getGame().getScheduler().createTaskBuilder().async().execute(() -> { try { allowedInLockdown = PlaytimeUtil.getTrustedPlayers(); inLockdown = true; } catch (Exception e) { inLockdown = true; e.printStackTrace(); } }).submit(plugin); } /** * Check if server is locked down. * * @return boolean If locked down */ public static boolean inLockdown() { return inLockdown; } /** * Returns the plugin configuration * @return Configuration */ public static Configuration getConfig() { return config; } /** * Returns the current game * @return Game */ public static Game getGame() { return game; } /** * Injects the Logger instance for this plugin * @param log Logger */ @Inject private void setLogger(Logger log) { logger = log; } /** * Returns the Logger instance for this plugin. * @return Logger instance */ public static Logger getLogger() { return logger; } /** * * @return */ public HikariDataSource initDbPool() { String dns = "jdbc:mysql://" + getConfig().getNode("db", "host").getString() + ":" + getConfig().getNode("db", "port").getString() + "/" + getConfig().getNode("db", "name").getString(); HikariConfig config = new HikariConfig(); config.setJdbcUrl(dns); config.setUsername(getConfig().getNode("db", "user").getString()); config.setPassword(getConfig().getNode("db", "pass").getString()); pool = new HikariDataSource(config); return pool; } /** * * @return * @throws SQLException */ public static Connection dbc() { Connection con = null; try { con = pool.getConnection(); } catch (SQLException e) { System.out.print("Database connection failed. " + e.getMessage()); if (!e.getMessage().contains("Pool empty")) { e.printStackTrace(); } } return con; } /** * */ protected void setupDatabase() { Connection conn = null; Statement st = null; try { conn = dbc(); if (conn == null) return; String query = "CREATE TABLE IF NOT EXISTS `oracle_announcements` (" + "`announcement_id` int(11) NOT NULL AUTO_INCREMENT," + "`announcement` varchar(255) NOT NULL," + "`type` varchar(16) NOT NULL," + "`is_active` tinyint(1) NOT NULL," + "PRIMARY KEY (`announcement_id`)" + ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"; st = conn.createStatement(); st.executeUpdate(query); query = "CREATE TABLE IF NOT EXISTS `oracle_bans` (" + "`ban_id` int(11) NOT NULL AUTO_INCREMENT," + "`player_id` int(11) unsigned DEFAULT NULL," + "`ip_id` int(10) unsigned DEFAULT NULL," + "`staff_player_id` int(11) unsigned NOT NULL," + "`reason` varchar(255) NOT NULL," + "`epoch` int(11) unsigned NOT NULL," + "`unbanned` tinyint(1) NOT NULL DEFAULT '0'," + "PRIMARY KEY (`ban_id`)," + "KEY `ip_id` (`ip_id`)," + "KEY `player_id` (`player_id`)" + ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"; st.executeUpdate(query); query = "CREATE TABLE IF NOT EXISTS `oracle_ips` (" + "`ip_id` int(10) unsigned NOT NULL AUTO_INCREMENT," + "`ip` int(10) unsigned NOT NULL," + "PRIMARY KEY (`ip_id`)," + "KEY `ip` (`ip`)" + ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"; st.executeUpdate(query); query = "CREATE TABLE IF NOT EXISTS `oracle_joins` (" + "`join_id` int(11) unsigned NOT NULL AUTO_INCREMENT," + "`server_id` int(10) unsigned NOT NULL," + "`player_count` smallint(4) unsigned NOT NULL," + "`player_id` int(10) unsigned NOT NULL," + "`player_join` int(11) NOT NULL," + "`player_quit` int(11) unsigned DEFAULT NULL," + "`playtime` int(11) unsigned DEFAULT NULL," + "`ip_id` int(10) unsigned NOT NULL," + "PRIMARY KEY (`join_id`)," + "KEY `player_id` (`player_id`)," + "KEY `ip_id` (`ip_id`)," + "KEY `server_id` (`server_id`)," + "KEY `playtime` (`playtime`)," + "KEY `player_quit` (`player_quit`)" + ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"; st.executeUpdate(query); query = "CREATE TABLE IF NOT EXISTS `oracle_players` (" + "`player_id` int(10) unsigned NOT NULL AUTO_INCREMENT," + "`player` varchar(16) NOT NULL," + "`player_uuid` binary(16) NOT NULL," + "PRIMARY KEY (`player_id`)," + "KEY `player` (`player`)" + ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"; st.executeUpdate(query); query = "CREATE TABLE IF NOT EXISTS `oracle_servers` (" + "`server_id` int(10) unsigned NOT NULL AUTO_INCREMENT," + "`server` varchar(16) NOT NULL," + "PRIMARY KEY (`server_id`)" + ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"; st.executeUpdate(query); query = "CREATE TABLE IF NOT EXISTS `oracle_unbans` (" + "`unban_id` int(11) NOT NULL AUTO_INCREMENT," + "`player_id` int(11) unsigned DEFAULT NULL," + "`ip_id` int(10) unsigned DEFAULT NULL," + "`staff_player_id` int(11) unsigned NOT NULL," + "`epoch` int(11) unsigned NOT NULL," + "PRIMARY KEY (`unban_id`)" + ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"; st.executeUpdate(query); query = "CREATE TABLE IF NOT EXISTS `oracle_warnings` (" + "`warning_id` int(11) NOT NULL AUTO_INCREMENT," + "`player_id` int(11) unsigned NOT NULL," + "`reason` text NOT NULL," + "`staff_player_id` int(11) unsigned NOT NULL," + "`epoch` int(11) unsigned NOT NULL," + "`deleted` tinyint(1) NOT NULL DEFAULT '0'," + "PRIMARY KEY (`warning_id`)" + ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"; st.executeUpdate(query); } catch (SQLException e) { logDbError(e); } finally { try { if (st != null) { st.close(); } if (conn != null) { conn.close(); } } catch(SQLException e) { } } } /** * If a user disconnects in an unknown way that is never caught by onPlayerQuit, * this will force close all records except for players currently online. */ public void catchUncaughtDisconnects(){ if (getConfig().getNode("joins", "enabled").getBoolean() ){ disconnectsTask = getGame().getScheduler().createTaskBuilder() .async() .delay(1L, TimeUnit.MINUTES) .interval(1L, TimeUnit.MINUTES) .execute(() -> { String onUsers = ""; for(Player pl: getGame().getServer().getOnlinePlayers()) { PluginPlayer pluginPlayer = PlayerIdentification.getOraclePlayer(pl); if( pluginPlayer == null ) continue; onUsers += pluginPlayer.getId()+","; } if(!onUsers.isEmpty()){ onUsers = onUsers.substring(0, onUsers.length()-1); } JoinUtil.forceDateForOfflinePlayers( onUsers ); }).submit(this); } } /** * If a user disconnects in an unknown way that is never caught by onPlayerQuit, * this will force close all records except for players currently online. */ public void runAnnouncements(){ getGame().getScheduler().createTaskBuilder() .async() .delay(10L, TimeUnit.MINUTES) .interval(10L, TimeUnit.MINUTES) .execute(() -> { List<Text> announces = AnnouncementUtil.getActiveAnnouncements(); if(!announces.isEmpty()){ if(lastAnnouncement >= announces.size()){ lastAnnouncement = 0; } game.getServer().getBroadcastChannel().send(announces.get(lastAnnouncement)); logger.info(announces.get(lastAnnouncement).toPlain()); lastAnnouncement++; } }).submit(this); } /** * If a user disconnects in an unknown way that is never caught by onPlayerQuit, * this will force close all records except for players currently online. */ public void runPlaytimeMonitor(){ if (getConfig().getNode("joins", "enabled").getBoolean()) { getGame().getScheduler().createTaskBuilder() .async() .delay(12000L, TimeUnit.MILLISECONDS) .interval(12000L, TimeUnit.MILLISECONDS) .execute(new PlaytimeMonitor()).submit(this); } } public void logDbError( SQLException e ){ logger.error("Database connection error: " + e.getMessage()); e.printStackTrace(); } @Listener public void onServerStop(GameStoppingServerEvent event) { // Force offline date for everyone if (getConfig().getNode("joins", "enabled").getBoolean()) { JoinUtil.forceDateForAllPlayers(); } disconnectsTask.cancel(); // Close pool connections when plugin disables if (pool != null) { pool.close(); } } }