BungeeChat.java

package dev.aura.bungeechat;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.typesafe.config.Config;
import dev.aura.bungeechat.account.AccountFileStorage;
import dev.aura.bungeechat.account.AccountSQLStorage;
import dev.aura.bungeechat.account.BungeecordAccountManager;
import dev.aura.bungeechat.api.BungeeChatApi;
import dev.aura.bungeechat.api.account.AccountManager;
import dev.aura.bungeechat.api.enums.ChannelType;
import dev.aura.bungeechat.api.hook.HookManager;
import dev.aura.bungeechat.api.module.ModuleManager;
import dev.aura.bungeechat.api.placeholder.BungeeChatContext;
import dev.aura.bungeechat.api.placeholder.InvalidContextError;
import dev.aura.bungeechat.api.placeholder.PlaceHolderManager;
import dev.aura.bungeechat.api.utils.BungeeChatInstanceHolder;
import dev.aura.bungeechat.command.BungeeChatCommand;
import dev.aura.bungeechat.config.Configuration;
import dev.aura.bungeechat.hook.DefaultHook;
import dev.aura.bungeechat.hook.StoredDataHook;
import dev.aura.bungeechat.hook.metrics.MetricManager;
import dev.aura.bungeechat.listener.BungeeChatEventsListener;
import dev.aura.bungeechat.listener.ChannelTypeCorrectorListener;
import dev.aura.bungeechat.listener.CommandTabCompleteListener;
import dev.aura.bungeechat.message.MessagesService;
import dev.aura.bungeechat.message.PlaceHolderUtil;
import dev.aura.bungeechat.message.PlaceHolders;
import dev.aura.bungeechat.module.BungeecordModuleManager;
import dev.aura.bungeechat.util.LoggerHelper;
import dev.aura.bungeechat.util.MapUtils;
import dev.aura.bungeechat.util.ServerNameUtil;
import dev.aura.lib.version.Version;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.net.ssl.HttpsURLConnection;
import lombok.AccessLevel;
import lombok.Cleanup;
import lombok.Getter;
import lombok.Setter;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.plugin.PluginDescription;

public class BungeeChat extends Plugin implements BungeeChatApi {
  private static final String storedDataHookName = "storedData";
  private static final String defaultHookName = "default";
  private static final String errorVersion = "error";

  @Getter
  @Setter(AccessLevel.PRIVATE)
  @VisibleForTesting
  static BungeeChat instance;

  private String latestVersion = null;
  private File configDir;
  private File langDir;
  private BungeeChatCommand bungeeChatCommand;
  private BungeecordAccountManager bungeecordAccountManager;
  private ChannelTypeCorrectorListener channelTypeCorrectorListener;
  private BungeeChatEventsListener bungeeChatEventsListener;
  private CommandTabCompleteListener commandTabCompleteListener;

  public BungeeChat() {
    super();
  }

  /** For unit tests only! */
  protected BungeeChat(ProxyServer proxy, PluginDescription description) {
    super(proxy, description);
  }

  @Override
  public void onLoad() {
    setInstance(this);
    BungeeChatInstanceHolder.setInstance(instance);
  }

  @Override
  public void onEnable() {
    onEnable(true);
  }

  public void onEnable(boolean printLoadScreen) {
    Configuration.load();
    PlaceHolderUtil.loadConfigSections();

    PlaceHolders.registerPlaceHolders();

    final Config accountDatabase = Configuration.get().getConfig("AccountDatabase");
    final Config databaseCredentials = accountDatabase.getConfig("credentials");
    final Config connectionProperties = accountDatabase.getConfig("properties");
    final ImmutableMap<String, String> connectionPropertiesMap =
        connectionProperties.entrySet().stream()
            .collect(
                MapUtils.immutableMapCollector(
                    Map.Entry::getKey, entry -> entry.getValue().unwrapped().toString()));

    if (accountDatabase.getBoolean("enabled")) {
      try {
        AccountManager.setAccountStorage(
            new AccountSQLStorage(
                databaseCredentials.getString("ip"),
                databaseCredentials.getInt("port"),
                databaseCredentials.getString("database"),
                databaseCredentials.getString("user"),
                databaseCredentials.getString("password"),
                databaseCredentials.getString("tablePrefix"),
                connectionPropertiesMap));
      } catch (SQLException e) {
        LoggerHelper.error("Could not connect to specified database. Using file storage", e);

        AccountManager.setAccountStorage(new AccountFileStorage());
      }
    } else {
      AccountManager.setAccountStorage(new AccountFileStorage());
    }

    bungeeChatCommand = new BungeeChatCommand();
    bungeecordAccountManager = new BungeecordAccountManager();
    channelTypeCorrectorListener = new ChannelTypeCorrectorListener();
    bungeeChatEventsListener = new BungeeChatEventsListener();
    commandTabCompleteListener = new CommandTabCompleteListener();

    ProxyServer.getInstance().getPluginManager().registerCommand(this, bungeeChatCommand);
    ProxyServer.getInstance().getPluginManager().registerListener(this, bungeecordAccountManager);
    ProxyServer.getInstance()
        .getPluginManager()
        .registerListener(this, channelTypeCorrectorListener);
    ProxyServer.getInstance().getPluginManager().registerListener(this, bungeeChatEventsListener);
    ProxyServer.getInstance().getPluginManager().registerListener(this, commandTabCompleteListener);

    Config prefixDefaults = Configuration.get().getConfig("PrefixSuffixSettings");

    BungeecordModuleManager.registerPluginModules();
    ModuleManager.enableModules();
    HookManager.addHook(storedDataHookName, new StoredDataHook());
    HookManager.addHook(
        defaultHookName,
        new DefaultHook(
            prefixDefaults.getString("defaultPrefix"), prefixDefaults.getString("defaultSuffix")));
    ServerNameUtil.loadAliases();

    // Refresh Cache and cache version
    getLatestVersion(true);

    if (printLoadScreen) {
      MetricManager.sendMetrics(this);

      loadScreen();
    }

    // Finally initialize BungeeChat command map
    commandTabCompleteListener.updateBungeeChatCommands();
  }

  @Override
  public void onDisable() {
    HookManager.removeHook(defaultHookName);
    HookManager.removeHook(storedDataHookName);
    ModuleManager.disableModules();

    ProxyServer.getInstance().getPluginManager().unregisterListener(bungeecordAccountManager);
    ProxyServer.getInstance().getPluginManager().unregisterCommand(bungeeChatCommand);
    ProxyServer.getInstance().getPluginManager().unregisterListener(channelTypeCorrectorListener);
    ProxyServer.getInstance().getPluginManager().unregisterListener(bungeeChatEventsListener);

    ProxyServer.getInstance().getScheduler().cancel(this);

    // Just to be sure
    ProxyServer.getInstance().getPluginManager().unregisterListeners(this);
    ProxyServer.getInstance().getPluginManager().unregisterCommands(this);

    PlaceHolderManager.clear();
    PlaceHolderUtil.clearConfigSections();
    ModuleManager.clearActiveModules();
  }

  @Override
  public File getConfigFolder() {
    if (configDir == null) {
      configDir = new File(getProxy().getPluginsFolder(), "BungeeChat");

      if (!configDir.exists() && !configDir.mkdirs())
        throw new RuntimeException(new IOException("Could not create " + configDir));
    }

    return configDir;
  }

  public File getLangFolder() {
    if (langDir == null) {
      langDir = new File(getConfigFolder(), "lang");

      if (!langDir.exists() && !langDir.mkdirs())
        throw new RuntimeException(new IOException("Could not create " + langDir));
    }

    return langDir;
  }

  @Override
  public void sendPrivateMessage(BungeeChatContext context) throws InvalidContextError {
    MessagesService.sendPrivateMessage(context);
  }

  @Override
  public void sendChannelMessage(BungeeChatContext context, ChannelType channel)
      throws InvalidContextError {
    MessagesService.sendChannelMessage(context, channel);
  }

  private void loadScreen() {
    StartupBannerSize size =
        StartupBannerSize.optionalValueOf(
                Configuration.get().getString("Miscellaneous.startupBannerSize"))
            .orElse(StartupBannerSize.NORMAL);

    if (size == StartupBannerSize.NONE) return;

    if (size != StartupBannerSize.SHORT) {
      LoggerHelper.info(
          ChatColor.GOLD
              + "---------------- "
              + ChatColor.AQUA
              + "Bungee Chat"
              + ChatColor.GOLD
              + " ----------------");
      LoggerHelper.info(getPeopleMessage("Authors", BungeeChatApi.AUTHORS));
    }

    LoggerHelper.info(ChatColor.YELLOW + "Version: " + ChatColor.GREEN + VERSION_STR);

    if (size == StartupBannerSize.LONG) {
      LoggerHelper.info(ChatColor.YELLOW + "Modules:");

      ModuleManager.getAvailableModulesStream()
          .map(
              module -> {
                if (module.isEnabled()) return "\t" + ChatColor.GREEN + "On  - " + module.getName();
                else return "\t" + ChatColor.RED + "Off - " + module.getName();
              })
          .forEachOrdered(LoggerHelper::info);
    } else {
      LoggerHelper.info(
          ChatColor.YELLOW
              + "Modules: "
              + ChatColor.GREEN
              + BungeecordModuleManager.getActiveModuleString());
    }

    if (size != StartupBannerSize.SHORT) {
      LoggerHelper.info(getPeopleMessage("Contributors", BungeeChatApi.CONTRIBUTORS));
      LoggerHelper.info(getPeopleMessage("Translators", BungeeChatApi.TRANSLATORS));
      LoggerHelper.info(getPeopleMessage("Donators", BungeeChatApi.DONATORS));
    }

    if (!isLatestVersion()) {
      LoggerHelper.info(
          ChatColor.YELLOW
              + "There is an update available. You can download version "
              + ChatColor.GREEN
              + getLatestVersion()
              + ChatColor.YELLOW
              + " on the plugin page at "
              + URL
              + " !");
    }

    if (size != StartupBannerSize.SHORT) {
      LoggerHelper.info(ChatColor.GOLD + "---------------------------------------------");
    }
  }

  private String getPeopleMessage(String name, String... people) {
    return Arrays.stream(people)
        .collect(
            Collectors.joining(
                BungeecordModuleManager.MODULE_CONCATENATOR,
                ChatColor.YELLOW + name + ": " + ChatColor.GREEN,
                ""));
  }

  private String queryLatestVersion() {
    if (!Configuration.get().getBoolean("Miscellaneous.checkForUpdates")) return VERSION_STR;

    try {
      @Cleanup("disconnect")
      HttpsURLConnection con =
          (HttpsURLConnection)
              new URL("https://api.spigotmc.org/legacy/update.php?resource=" + PLUGIN_ID)
                  .openConnection();
      con.setDoOutput(true);
      con.setRequestMethod("GET");

      con.connect();

      int responseCode = con.getResponseCode();

      try (BufferedReader reader =
          new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) {
        if (responseCode != 200) {
          LoggerHelper.warning(
              "Invalid response! HTTP code: "
                  + responseCode
                  + " Content:\n"
                  + reader.lines().collect(Collectors.joining("\n")));

          return errorVersion;
        }

        return Optional.ofNullable(reader.readLine()).orElse(errorVersion);
      }
    } catch (Exception ex) {
      LoggerHelper.warning("Could not fetch the latest version!", ex);

      return errorVersion;
    }
  }

  public String getLatestVersion() {
    return getLatestVersion(false);
  }

  public String getLatestVersion(boolean refreshCache) {
    if (refreshCache || (latestVersion == null)) {
      latestVersion = queryLatestVersion();
    }

    return latestVersion;
  }

  public boolean isLatestVersion() {
    return VERSION.compareTo(new Version(getLatestVersion())) >= 0;
  }

  private enum StartupBannerSize {
    NONE,
    SHORT,
    NORMAL,
    LONG;

    public static Optional<StartupBannerSize> optionalValueOf(String value) {
      for (StartupBannerSize element : values()) {
        if (element.name().equalsIgnoreCase(value)) return Optional.of(element);
      }

      return Optional.empty();
    }
  }
}