package fr.Alphart.BAT;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.TimeZone;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import fr.Alphart.BAT.I18n.I18n;
import fr.Alphart.BAT.Modules.ModulesManager;
import fr.Alphart.BAT.Modules.Core.Core;
import fr.Alphart.BAT.Utils.CallbackUtils.Callback;
import fr.Alphart.BAT.Utils.RedisUtils;
import fr.Alphart.BAT.database.DataSourceHandler;
/**
* Main class BungeeAdminTools
*
* @author Alphart
*/
public class BAT extends Plugin {
// This way we can check at runtime if the required BC build (or a higher one) is installed
private final int requiredBCBuild = 878;
private static BAT instance;
private static DataSourceHandler dsHandler;
private Configuration config;
private static String prefix;
private ModulesManager modules;
private RedisUtils redis;
@Override
public void onEnable() {
instance = this;
config = new Configuration();
getLogger().setLevel(Level.INFO);
if (!ProxyServer.getInstance().getName().equals("BungeeCord")) {
getLogger().warning("BungeeCord version check disabled because a fork has been detected."
+ " Make sur your fork is based on a BungeeCord build > #" + requiredBCBuild);
}
else if(getBCBuild() < requiredBCBuild && !new File(getDataFolder(), "skipversiontest").exists()){
getLogger().severe("Your BungeeCord build (#" + getBCBuild() + ") is not supported. Please use at least BungeeCord #" + requiredBCBuild);
getLogger().severe("If you want to skip that test, create a file named 'skipversiontest' in BAT directory.");
getLogger().severe("BAT is going to shutdown ...");
return;
}
if(config.isDebugMode()){
try{
final File debugFile = new File(getDataFolder(), "debug.log");
if(debugFile.exists()){
debugFile.delete();
}
// Write header into debug log
Files.asCharSink(debugFile, Charsets.UTF_8).writeLines(Arrays.asList("BAT log debug file"
+ " - If you have an error with BAT, you should post this file on BAT topic on spigotmc",
"Bungee build : " + ProxyServer.getInstance().getVersion(),
"BAT version : " + getDescription().getVersion(),
"Operating System : " + System.getProperty("os.name"),
"Timezone : " + TimeZone.getDefault().getID(),
"------------------------------------------------------------"));
final FileHandler handler = new FileHandler(debugFile.getAbsolutePath(), true);
handler.setFormatter(new Formatter() {
private final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
private final String pattern = "time [level] message\n";
@Override
public String format(LogRecord record) {
return pattern.replace("level", record.getLevel().getName())
.replace("message", record.getMessage())
.replace("[BungeeAdminTools]", "")
.replace("time", sdf.format(Calendar.getInstance().getTime()));
}
});
getLogger().addHandler(handler);
getLogger().setLevel(Level.CONFIG);
getLogger().info("The debug mode is now enabled ! Log are available in debug.log file located in BAT folder");
getLogger().config("Debug mode enabled ...");
getLogger().setUseParentHandlers(false);
}catch(final Exception e){
getLogger().log(Level.SEVERE, "An exception occured during the initialization of debug logging file", e);
}
}
prefix = config.getPrefix();
loadDB(new Callback<Boolean>(){
@Override
public void done(final Boolean dbState, Throwable throwable) {
if (dbState) {
getLogger().config("Connection to the database established");
// Try enabling redis support.
redis = new RedisUtils(config.isRedisSupport());
modules = new ModulesManager();
modules.loadModules();
} else {
getLogger().severe("BAT is gonna shutdown because it can't connect to the database.");
return;
}
// Init the I18n module
I18n.getString("global");
}
});
}
public int getBCBuild(){
final Pattern p = Pattern.compile(".*?:(.*?:){3}(\\d*)");
final Matcher m = p.matcher(ProxyServer.getInstance().getVersion());
int BCBuild;
try{
if (m.find()) {
BCBuild = Integer.parseInt(m.group(2));
}else{
throw new NumberFormatException();
}
}catch(final NumberFormatException e){
// We can't determine BC build, just display a message, and set the build so it doesn't trigger the security
getLogger().info("BC build can't be detected. If you encounter any problems, please report that message. Otherwise don't take into account");
BCBuild = requiredBCBuild;
}
return BCBuild;
}
@Override
public void onDisable() {
if(redis != null){
getRedis().destroy();
}
modules.unloadModules();
instance = null;
}
public void loadDB(final Callback<Boolean> dbState) {
if (config.isMysql_enabled()) {
getLogger().config("Starting connection to the mysql database ...");
final String username = config.getMysql_user();
final String password = config.getMysql_password();
final String database = config.getMysql_database();
final String port = config.getMysql_port();
final String host = config.getMysql_host();
// BoneCP can accept no database and we want to avoid that
Preconditions.checkArgument(!"".equals(database), "You must set the database.");
ProxyServer.getInstance().getScheduler().runAsync(this, new Runnable() {
@Override
public void run() {
try{
dsHandler = new DataSourceHandler(host, port, database, username, password);
final Connection c = dsHandler.getConnection();
if (c != null) {
c.close();
dbState.done(true, null);
return;
}
}catch(final SQLException handledByDatasourceHandler){}
getLogger().severe("The connection pool (database connection)"
+ " wasn't able to be launched !");
dbState.done(false, null);
}
});
}
// If MySQL is disabled, we are gonna use SQLite
// Before initialize the connection, we must download the sqlite driver
// (if it isn't already in the lib folder) and load it
else {
getLogger().config("Starting connection to the sqlite database ...");
getLogger().warning("It is strongly DISRECOMMENDED to use SQLite with BAT,"
+ " as the SQLite implementation is less stable and much slower than the MySQL implementation.");
if(loadSQLiteDriver()){
dsHandler = new DataSourceHandler();
dbState.done(true, null);
}else{
dbState.done(false, null);
}
}
}
public boolean loadSQLiteDriver(){
final File driverPath = new File(getDataFolder() + File.separator + "lib" + File.separator
+ "sqlite_driver.jar");
new File(getDataFolder() + File.separator + "lib").mkdir();
// Download the driver if it doesn't exist
if (!new File(getDataFolder() + File.separator + "lib" + File.separator + "sqlite_driver.jar").exists()) {
getLogger().info("The SQLLite driver was not found. It is being downloaded, please wait ...");
final String driverUrl = "https://www.dropbox.com/s/ls7qoddx9m6t4vh/sqlite_driver.jar?dl=1";
FileOutputStream fos = null;
try {
final ReadableByteChannel rbc = Channels.newChannel(new URL(driverUrl).openStream());
fos = new FileOutputStream(driverPath);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
} catch (final IOException e) {
getLogger()
.severe("An error occured during the downloading of the SQLite driver. Please report this error : ");
e.printStackTrace();
return false;
} finally {
DataSourceHandler.close(fos);
}
getLogger().info("The driver has been successfully downloaded.");
}
// Load the driver
try {
URLClassLoader systemClassLoader;
URL u;
Class<URLClassLoader> sysclass;
u = driverPath.toURI().toURL();
systemClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
sysclass = URLClassLoader.class;
final Method method = sysclass.getDeclaredMethod("addURL", new Class[] { URL.class });
method.setAccessible(true);
method.invoke(systemClassLoader, new Object[] { u });
Class.forName("org.sqlite.JDBC");
return true;
} catch (final Throwable t) {
getLogger().severe("The sqlite driver cannot be loaded. Please report this error : ");
t.printStackTrace();
return false;
}
}
public static BAT getInstance() {
return BAT.instance;
}
public static BaseComponent[] __(final String message) {
return TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', prefix + message));
}
/**
* Send a broadcast message to everyone with the given perm <br>
* Also broadcast through Redis if it's installed that's why this method <strong>should not be called
* from a Redis call</strong> otherwise it will broadcast it again and again
* @param message
* @param perm
*/
public static void broadcast(final String message, final String perm) {
noRedisBroadcast(message, perm);
if(BAT.getInstance().getRedis().isRedisEnabled()){
BAT.getInstance().getRedis().sendBroadcast(perm, message);
}
}
public static void noRedisBroadcast(final String message, final String perm) {
final BaseComponent[] bsMsg = __(message);
for (final ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
if (p.hasPermission(perm) || p.hasPermission("bat.admin")) {
p.sendMessage(bsMsg);
}
// If he has a grantall permission, he will have the broadcast on all the servers
else{
for(final String playerPerm : Core.getCommandSenderPermission(p)){
if(playerPerm.startsWith("bat.grantall.")){
p.sendMessage(bsMsg);
break;
}
}
}
}
getInstance().getLogger().info(ChatColor.translateAlternateColorCodes('&', message));
}
public ModulesManager getModules() {
return modules;
}
public Configuration getConfiguration() {
return config;
}
public static Connection getConnection() {
return dsHandler.getConnection();
}
public DataSourceHandler getDsHandler() {
return dsHandler;
}
public RedisUtils getRedis() {
return redis;
}
/**
* Kick a player from the proxy for a specified reason
*
* @param player
* @param reason
*/
public static void kick(final ProxiedPlayer player, final String reason) {
if (reason == null || reason.equals("")) {
player.disconnect(TextComponent.fromLegacyText("You have been disconnected of the server."));
} else {
player.disconnect(TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', reason)));
}
}
}