tv.phantombot.PhantomBot.java Source code

Java tutorial

Introduction

Here is the source code for tv.phantombot.PhantomBot.java

Source

/*
 * Copyright (C) 2016-2018 phantombot.tv
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tv.phantombot;

import net.engio.mbassy.listener.Handler;

import com.gmt2001.datastore.DataStore;
import com.gmt2001.datastore.IniStore;
import com.gmt2001.datastore.MySQLStore;
import com.gmt2001.datastore.SqliteStore;
import com.gmt2001.datastore.H2Store;
import com.gmt2001.TwitchAPIv5;
import com.gmt2001.YouTubeAPIv3;
import com.gmt2001.datastore.DataStoreConverter;

import com.illusionaryone.GameWispAPIv1;
import com.illusionaryone.GitHubAPIv3;
import com.illusionaryone.GoogleURLShortenerAPIv1;
import com.illusionaryone.NoticeTimer;
import com.illusionaryone.SingularityAPI;
import com.illusionaryone.TwitchAlertsAPIv1;
import com.illusionaryone.TwitterAPI;
import com.illusionaryone.DataRenderServiceAPIv1;

import com.scaniatv.CustomAPI;
import com.scaniatv.TipeeeStreamAPIv1;
import com.scaniatv.StreamElementsAPIv2;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.charset.Charset;
import java.nio.file.StandardOpenOption;

import java.net.ServerSocket;

import java.security.SecureRandom;

import java.text.SimpleDateFormat;

import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import tv.phantombot.cache.DonationsCache;
import tv.phantombot.cache.EmotesCache;
import tv.phantombot.cache.FollowersCache;
import tv.phantombot.cache.TipeeeStreamCache;
import tv.phantombot.cache.StreamElementsCache;
import tv.phantombot.cache.TwitchCache;
import tv.phantombot.cache.TwitterCache;
import tv.phantombot.cache.UsernameCache;
import tv.phantombot.cache.ViewerListCache;
import tv.phantombot.console.ConsoleInputListener;
import tv.phantombot.event.EventBus;
import tv.phantombot.event.Listener;
import tv.phantombot.event.command.CommandEvent;
import tv.phantombot.event.irc.channel.IrcChannelUserModeEvent;
import tv.phantombot.event.irc.complete.IrcJoinCompleteEvent;
import tv.phantombot.event.irc.message.IrcChannelMessageEvent;
import tv.phantombot.event.irc.message.IrcPrivateMessageEvent;
import tv.phantombot.httpserver.HTTPServer;
import tv.phantombot.httpserver.HTTPSServer;
import tv.phantombot.panel.PanelSocketSecureServer;
import tv.phantombot.panel.PanelSocketServer;
import tv.phantombot.panel.NewPanelSocketServer;
import tv.phantombot.script.Script;
import tv.phantombot.script.ScriptEventManager;
import tv.phantombot.script.ScriptManager;
import tv.phantombot.script.ScriptFileWatcher;
import tv.phantombot.wschat.twitch.TwitchSession;
import tv.phantombot.wschat.twitch.pubsub.TwitchPubSub;
import tv.phantombot.wschat.twitch.host.TwitchWSHostIRC;
import tv.phantombot.ytplayer.YTWebSocketServer;
import tv.phantombot.ytplayer.YTWebSocketSecureServer;
import tv.phantombot.discord.DiscordAPI;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.apache.commons.lang3.SystemUtils;
import tv.phantombot.console.ConsoleEventHandler;

public final class PhantomBot implements Listener {
    /* Bot Information */
    private String botName;
    private String channelName;
    private String ownerName;
    private String oauth;
    private String apiOAuth;
    private String clientId;
    private static Double messageLimit;
    private static Double whisperLimit;

    /* Web Information */
    private String panelUsername;
    private String panelPassword;
    private String webOAuth;
    private String webOAuthThro;
    private String youtubeOAuth;
    private String youtubeOAuthThro;
    private String youtubeKey;
    private Boolean webEnabled;
    private Boolean musicEnabled;
    private Boolean useHttps;
    private Boolean testPanelServer;
    private int basePort;
    private String bindIP;
    private int ytSocketPort;
    private int panelSocketPort;

    /* SSL information */
    private String httpsPassword = "password";
    private String httpsFileName = "cert.jks";

    /* DataStore Information */
    private DataStore dataStore;
    private String dataStoreType;
    private String dataStoreConfig;

    /* MySQL Information */
    private String mySqlConn;
    private String mySqlHost;
    private String mySqlPort;
    private String mySqlName;
    private String mySqlUser;
    private String mySqlPass;

    /* Twitter Information */
    private String twitterUsername;
    private String twitterAccessToken;
    private String twitterSecretToken;
    private String twitterConsumerSecret;
    private String twitterConsumerToken;
    private Boolean twitterAuthenticated;

    /* TwitchAlerts Information */
    private String twitchAlertsKey = "";
    private int twitchAlertsLimit = 0;

    /* TipeeeStream Information */
    private String tipeeeStreamOAuth = "";
    private int tipeeeStreamLimit = 5;

    /* StreamElements Information */
    private String streamElementsJWT = "";
    private String streamElementsID = "";
    private int streamElementsLimit = 5;

    /* GameWisp Information */
    private String gameWispOAuth;
    private String gameWispRefresh;

    /* Notice Timer and Handling */
    private NoticeTimer noticeTimer;

    /* Discord Configuration */
    private String discordToken = "";

    /* PhantomBot Commands API Configuration */
    private String dataRenderServiceAPIToken = "";
    private String dataRenderServiceAPIURL = "";

    /* Caches */
    private FollowersCache followersCache;
    private DonationsCache twitchAlertsCache;
    private EmotesCache emotesCache;
    private TwitterCache twitterCache;
    private TwitchCache twitchCache;
    private UsernameCache usernameCache;
    private TipeeeStreamCache tipeeeStreamCache;
    private ViewerListCache viewerListCache;
    private StreamElementsCache streamElementCache;
    public static String twitchCacheReady = "false";

    /* Socket Servers */
    private YTWebSocketServer youtubeSocketServer;
    private YTWebSocketSecureServer youtubeSocketSecureServer;
    private PanelSocketServer panelSocketServer;
    private NewPanelSocketServer newPanelSocketServer;
    private PanelSocketSecureServer panelSocketSecureServer;
    private HTTPServer httpServer;
    private HTTPSServer httpsServer;
    private int socketServerTasksSize;

    /* PhantomBot Information */
    private static PhantomBot instance;
    public static Boolean reloadScripts = false;
    public static Boolean enableDebugging = false;
    public static Boolean enableDebuggingLogOnly = false;
    public static Boolean enableRhinoDebugger = false;
    public static String timeZone = "GMT";
    public static Boolean useMessageQueue = true;
    public static Boolean twitch_tcp_nodelay = true;
    public static Boolean betap = false;
    public static Boolean isInExitState = false;
    public Boolean isExiting = false;
    private Boolean interactive;
    private Boolean resetLogin = false;

    /* Other Information */
    private static Boolean newSetup = false;
    private TwitchSession session;
    private String chanName;
    private Boolean timer = false;
    private SecureRandom random;
    private Boolean joined = false;
    private TwitchWSHostIRC wsHostIRC;
    private TwitchPubSub pubSubEdge;
    private Properties pbProperties;
    private Boolean legacyServers = false;
    private Boolean backupSQLiteAuto = false;
    private int backupSQLiteHourFrequency = 0;
    private int backupSQLiteKeepDays = 0;

    /*
     * PhantomBot Instance.
     *
     * @return  PhantomBot  The current instance of PhantomBot
     */
    public static PhantomBot instance() {
        return instance;
    }

    /*
     * Current Repo Of PhantomBot.
     *
     * @return  String  The current GitHub repository version of PhantomBot.
     */
    public String repoVersion() {
        return RepoVersion.getRepoVersion();
    }

    /*
     * Current Version Of PhantomBot.
     *
     * @return  String  Display version of PhantomBot.
     */
    public String botVersion() {
        return "PhantomBot Version: " + RepoVersion.getPhantomBotVersion();
    }

    /*
     * Used by the panel on the informations tab.
     *
     * @return  String  PhantomBot information for the Panel.
     */
    public String getBotInfo() {
        return botVersion() + " (Revision: " + repoVersion() + ")";
    }

    /*
     * Current Build Revision
     *
     * @return  String  The build revision of PhantomBot.
     */
    public String botRevision() {
        return "Build Revision: " + repoVersion();
    }

    /*
     * Only used on bot boot up for now.
     *
     * @return {string} bot creator
     */
    public String getBotCreator() {
        return "Creator: mast3rplan";
    }

    /*
     * Only used on bot boot up for now.
     *
     * @return {string} bot developers
     */
    public String botDevelopers() {
        return "Developers: PhantomIndex, Kojitsari, ScaniaTV, Zackery (Zelakto) & IllusionaryOne";
    }

    /*
     * Only used on bot boot up for now.
     *
     * @return {string} bot website
     */
    public String getWebSite() {
        return "https://phantombot.tv/";
    }

    /*
     * Prints a message in the bot console.
     *
     * @param {Object} message
     */
    private void print(String message) {
        com.gmt2001.Console.out.println(message);
    }

    /*
     * Checks port availability.
     *
     * @param {int} port
     */
    public void checkPortAvailabity(int port) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = bindIP.isEmpty() ? new ServerSocket(port)
                    : new ServerSocket(port, 1, java.net.InetAddress.getByName(bindIP));
            serverSocket.setReuseAddress(true);
        } catch (IOException e) {
            com.gmt2001.Console.err.println("Port is already in use: " + port);
            com.gmt2001.Console.err.println("Ensure that another copy of PhantomBot is not running.");
            com.gmt2001.Console.err
                    .println("If another copy is not running, try to change baseport in ./config/botlogin.txt");
            com.gmt2001.Console.err.println("PhantomBot will now exit.");
            System.exit(0);
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    com.gmt2001.Console.err.println("Unable to close port for testing: " + port);
                    com.gmt2001.Console.err.println("PhantomBot will now exit.");
                    System.exit(0);
                }
            }
        }
    }

    /*
     * Constructor for PhantomBot object.
     *
     * @param  Properties  Properties object which configures the PhantomBot instance.
     */
    public PhantomBot(Properties pbProperties) {

        /* Set the exeption handler */
        Thread.setDefaultUncaughtExceptionHandler(com.gmt2001.UncaughtExceptionHandler.instance());

        /* Start loading the bot information */
        print("");
        print(botVersion());
        print(botRevision());
        print(getBotCreator());
        print(botDevelopers());
        print(getWebSite());
        print("");

        /* System interactive */
        interactive = (System.getProperty("interactive") != null);

        /* Assign properties passed in to local instance. */
        this.pbProperties = pbProperties;

        /* Set the default bot variables */
        this.botName = this.pbProperties.getProperty("user").toLowerCase();
        this.channelName = this.pbProperties.getProperty("channel").toLowerCase();
        this.ownerName = this.pbProperties.getProperty("owner").toLowerCase();
        this.apiOAuth = this.pbProperties.getProperty("apioauth", "");
        this.oauth = this.pbProperties.getProperty("oauth");

        /* Set the web variables */
        this.youtubeOAuth = this.pbProperties.getProperty("ytauth");
        this.youtubeOAuthThro = this.pbProperties.getProperty("ytauthro");
        this.youtubeKey = this.pbProperties.getProperty("youtubekey", "");
        this.basePort = Integer.parseInt(this.pbProperties.getProperty("baseport", "25000"));
        this.bindIP = this.pbProperties.getProperty("bindIP", "");
        this.ytSocketPort = Integer
                .parseInt(this.pbProperties.getProperty("ytsocketport", String.valueOf((this.basePort + 3))));
        this.panelSocketPort = Integer
                .parseInt(this.pbProperties.getProperty("panelsocketport", String.valueOf((this.basePort + 4))));
        this.webOAuth = this.pbProperties.getProperty("webauth");
        this.webOAuthThro = this.pbProperties.getProperty("webauthro");
        this.webEnabled = this.pbProperties.getProperty("webenable", "true").equalsIgnoreCase("true");
        this.musicEnabled = this.pbProperties.getProperty("musicenable", "true").equalsIgnoreCase("true");
        this.useHttps = this.pbProperties.getProperty("usehttps", "false").equalsIgnoreCase("true");
        this.socketServerTasksSize = Integer.parseInt(this.pbProperties.getProperty("wstasksize", "200"));
        this.testPanelServer = this.pbProperties.getProperty("testpanelserver", "false").equalsIgnoreCase("true");

        /* Set the datastore variables */
        this.dataStoreType = this.pbProperties.getProperty("datastore", "");
        this.dataStoreConfig = this.pbProperties.getProperty("datastoreconfig", "");

        /* Set the Twitter variables */
        this.twitterUsername = this.pbProperties.getProperty("twitterUser", "");
        this.twitterConsumerToken = this.pbProperties.getProperty("twitter_consumer_key", "");
        this.twitterConsumerSecret = this.pbProperties.getProperty("twitter_consumer_secret", "");
        this.twitterAccessToken = this.pbProperties.getProperty("twitter_access_token", "");
        this.twitterSecretToken = this.pbProperties.getProperty("twitter_secret_token", "");
        this.twitterAuthenticated = false;

        /* Set the Discord variables */
        this.discordToken = this.pbProperties.getProperty("discord_token", "");

        /* Set the GameWisp variables */
        this.gameWispOAuth = this.pbProperties.getProperty("gamewispauth", "");
        this.gameWispRefresh = this.pbProperties.getProperty("gamewisprefresh", "");

        /* Set the TwitchAlerts variables */
        this.twitchAlertsKey = this.pbProperties.getProperty("twitchalertskey", "");
        this.twitchAlertsLimit = Integer.parseInt(this.pbProperties.getProperty("twitchalertslimit", "5"));

        /* Set the TipeeeStream variables */
        this.tipeeeStreamOAuth = this.pbProperties.getProperty("tipeeestreamkey", "");
        this.tipeeeStreamLimit = Integer.parseInt(this.pbProperties.getProperty("tipeeestreamlimit", "5"));

        /* Set the StreamElements variables */
        this.streamElementsJWT = this.pbProperties.getProperty("streamelementsjwt", "");
        this.streamElementsID = this.pbProperties.getProperty("streamelementsid", "");
        this.streamElementsLimit = Integer.parseInt(this.pbProperties.getProperty("streamelementslimit", "5"));

        /* Set the PhantomBot Commands API variables */
        this.dataRenderServiceAPIToken = this.pbProperties.getProperty("datarenderservicetoken", "");
        this.dataRenderServiceAPIURL = this.pbProperties.getProperty("datarenderserviceurl",
                "https://drs.phantombot.tv");

        /* Set the MySql variables */
        this.mySqlName = this.pbProperties.getProperty("mysqlname", "");
        this.mySqlUser = this.pbProperties.getProperty("mysqluser", "");
        this.mySqlPass = this.pbProperties.getProperty("mysqlpass", "");
        this.mySqlHost = this.pbProperties.getProperty("mysqlhost", "");
        this.mySqlPort = this.pbProperties.getProperty("mysqlport", "");

        /* twitch cache */
        PhantomBot.twitchCacheReady = "false";

        /* Set the SSL info */
        this.httpsFileName = this.pbProperties.getProperty("httpsFileName", "");
        this.httpsPassword = this.pbProperties.getProperty("httpsPassword", "");

        /* Verify SSL file if useHttps is enabled. */
        if (this.useHttps) {
            if (this.httpsFileName.equals("")) {
                com.gmt2001.Console.err
                        .println("HTTPS is enabled but the Java Keystore (httpsFileName) is not defined.");
                com.gmt2001.Console.err.println("Terminating PhantomBot");
                System.exit(1);
            }

            if (!new File(httpsFileName).exists()) {
                com.gmt2001.Console.err.println(
                        "HTTPS is enabled but the Java Keystore (httpsFileName) is not present: " + httpsFileName);
                com.gmt2001.Console.err.println("Terminating PhantomBot");
                System.exit(1);
            }
        }

        /* Set the timeZone */
        PhantomBot.timeZone = this.pbProperties.getProperty("logtimezone", "GMT");

        /* Set the panel username login for the panel to use */
        this.panelUsername = this.pbProperties.getProperty("paneluser", "panel");

        /* Set the panel password login for the panel to use */
        this.panelPassword = this.pbProperties.getProperty("panelpassword", "panel");

        /* Toggle for the old servers. */
        this.legacyServers = this.pbProperties.getProperty("legacyservers", "false").equalsIgnoreCase("true");

        /* Set the tcp delay toggle. Having this set to true uses a bit more bandwidth but sends messages to Twitch faster. */
        PhantomBot.twitch_tcp_nodelay = this.pbProperties.getProperty("twitch_tcp_nodelay", "true")
                .equalsIgnoreCase("true");

        /* Setting for scania */
        PhantomBot.betap = this.pbProperties.getProperty("betap", "false").equalsIgnoreCase("true");

        /*
         * Set the message limit for session.java to use, note that Twitch rate limits at 100 messages in 30 seconds
         * for moderators.  For non-moderators, the maximum is 20 messages in 30 seconds. While it is not recommended
         * to go above anything higher than 19 in case the bot is ever de-modded, the option is available but is
         * capped at 100.0.
         */
        PhantomBot.messageLimit = Math
                .floor(Double.parseDouble(this.pbProperties.getProperty("msglimit30", "19.0")));
        if (PhantomBot.messageLimit > 99.0) {
            PhantomBot.messageLimit = 99.0;
        } else if (PhantomBot.messageLimit < 19.0) {
            PhantomBot.messageLimit = 19.0;
        }

        // *Not currently being used.*
        // If this is false the bot won't limit the bot to 1 message every 1.5 second. It will still limit to 19/30 though.
        PhantomBot.useMessageQueue = this.pbProperties.getProperty("usemessagequeue", "true").equals("true");

        /* Set the whisper limit for session.java to use. -- Currently Not Used -- */
        PhantomBot.whisperLimit = Double.parseDouble(this.pbProperties.getProperty("whisperlimit60", "60.0"));

        /* Set the client id for the twitch api to use */
        this.clientId = this.pbProperties.getProperty("clientid", "7wpchwtqz7pvivc3qbdn1kajz42tdmb");

        /* Set any SQLite backup options. */
        this.backupSQLiteAuto = this.pbProperties.getProperty("backupsqliteauto", "true").equalsIgnoreCase("true");
        this.backupSQLiteHourFrequency = Integer
                .parseInt(this.pbProperties.getProperty("backupsqlitehourfrequency", "24"));
        this.backupSQLiteKeepDays = Integer.parseInt(this.pbProperties.getProperty("backupsqlitekeepdays", "5"));

        /* Load up a new SecureRandom for the scripts to use */
        random = new SecureRandom();

        /* Load the datastore */
        if (dataStoreType.equalsIgnoreCase("inistore")) {
            dataStore = IniStore.instance();
        } else if (dataStoreType.equalsIgnoreCase("mysqlstore")) {
            dataStore = MySQLStore.instance();
            if (this.mySqlPort.isEmpty()) {
                this.mySqlConn = "jdbc:mysql://" + this.mySqlHost + "/" + this.mySqlName + "?useSSL=false";
            } else {
                this.mySqlConn = "jdbc:mysql://" + this.mySqlHost + ":" + this.mySqlPort + "/" + this.mySqlName
                        + "?useSSL=false";
            }
            /* Check to see if we can create a connection */
            if (dataStore.CreateConnection(this.mySqlConn, this.mySqlUser, this.mySqlPass) == null) {
                print("Could not create a connection with MySQL Server. PhantomBot now shutting down...");
                System.exit(0);
            }
            /* Convert to MySql */
            if (IniStore.instance().GetFileList().length > 0 && MySQLStore.instance().GetFileList().length == 0) {
                DataStoreConverter.convertDataStore(MySQLStore.instance(), IniStore.instance());
            } else if (SqliteStore.instance().GetFileList().length > 0
                    && MySQLStore.instance().GetFileList().length == 0) {
                DataStoreConverter.convertDataStore(MySQLStore.instance(), SqliteStore.instance());
            }
        } else if (dataStoreType.equalsIgnoreCase("h2store")) {
            dataStore = H2Store.instance();

            if (dataStore.CreateConnection("", "", "") == null) {
                print("Could not create a connection with H2 Database. PhantomBot now shutting down...");
                System.exit(0);
            }

            if (SqliteStore.instance().GetFileList().length > 0 && H2Store.instance().GetFileList().length == 0) {
                DataStoreConverter.convertDataStore(H2Store.instance(), SqliteStore.instance());
            }
        } else {
            dataStoreType = "sqlite3store";
            dataStore = SqliteStore.instance();

            /* Convert the inistore to sqlite if the inistore exists and the db is empty */
            if (IniStore.instance().GetFileList().length > 0 && SqliteStore.instance().GetFileList().length == 0) {
                DataStoreConverter.convertDataStore(SqliteStore.instance(), IniStore.instance());
            }

            /* Handle index operations. */
            com.gmt2001.Console.debug.println("Checking database indexes, please wait...");
            if (SqliteStore.instance().getUseIndexes()) {
                dataStore.CreateIndexes();
            } else {
                dataStore.DropIndexes();
            }
        }

        /* Set the client Id in the Twitch api. */
        TwitchAPIv5.instance().SetClientID(this.clientId);

        /* Set the oauth key in the Twitch api. */
        if (!this.apiOAuth.isEmpty()) {
            TwitchAPIv5.instance().SetOAuth(this.apiOAuth);
        }

        /* Set the TwitchAlerts OAuth key and limiter. */
        if (!twitchAlertsKey.isEmpty()) {
            TwitchAlertsAPIv1.instance().SetAccessToken(twitchAlertsKey);
            TwitchAlertsAPIv1.instance().SetDonationPullLimit(twitchAlertsLimit);
        }

        /* Set the YouTube API Key if provided. */
        if (!this.youtubeKey.isEmpty()) {
            YouTubeAPIv3.instance().SetAPIKey(this.youtubeKey);
        }

        /* Set the TipeeeStream oauth key. */
        if (!tipeeeStreamOAuth.isEmpty()) {
            TipeeeStreamAPIv1.instance().SetOauth(tipeeeStreamOAuth);
            TipeeeStreamAPIv1.instance().SetLimit(tipeeeStreamLimit);
        }

        /* Set the StreamElements JWT token. */
        if (!streamElementsJWT.isEmpty() && !streamElementsID.isEmpty()) {
            StreamElementsAPIv2.instance().SetJWT(streamElementsJWT);
            StreamElementsAPIv2.instance().SetID(streamElementsID);
            StreamElementsAPIv2.instance().SetLimit(streamElementsLimit);
        }

        /* Set the PhantomBot Commands authentication key. */
        if (!dataRenderServiceAPIToken.isEmpty()) {
            DataRenderServiceAPIv1.instance().setAPIURL(dataRenderServiceAPIURL);
            DataRenderServiceAPIv1.instance().setAPIKey(dataRenderServiceAPIToken);
        }

        /* Start things and start loading the scripts. */
        this.init();

        /* Start a session instance and then connect to WS-IRC @ Twitch. */
        this.session = TwitchSession.instance(this.channelName, this.botName, this.oauth).connect();

        /* Start a host checking instance. */
        if (apiOAuth.length() > 0 && checkModuleEnabled("./handlers/hostHandler.js")) {
            this.wsHostIRC = TwitchWSHostIRC.instance(this.channelName, this.apiOAuth, EventBus.instance());
        }

        /* Check if the OS is Linux. */
        if (SystemUtils.IS_OS_LINUX && !interactive) {
            try {
                java.lang.management.RuntimeMXBean runtime = java.lang.management.ManagementFactory
                        .getRuntimeMXBean();
                int pid = Integer.parseInt(runtime.getName().split("@")[0]);

                File file = new File("PhantomBot." + this.botName + ".pid");

                try (FileOutputStream fs = new FileOutputStream(file, false)) {
                    PrintStream ps = new PrintStream(fs);
                    ps.print(pid);
                }
                file.deleteOnExit();
            } catch (SecurityException | IllegalArgumentException | IOException ex) {
                com.gmt2001.Console.err.printStackTrace(ex);
            }
        }
    }

    /*
     * Tells you if the build is a nightly.
     *
     * @return {boolean}
     */
    public Boolean isNightly() {
        return RepoVersion.getNightlyBuild();
    }

    /*
     * Tells you if the build is a pre-release.
     *
     * @return {boolean}
     */
    public Boolean isPrerelease() {
        return RepoVersion.getPrereleaseBuild();
    }

    /*
     * Enables or disables the debug mode.
     *
     * @param {boolean} debug
     */
    public static void setDebugging(Boolean debug) {
        PhantomBot.enableDebugging = debug;
    }

    /*
     * Enables or disables log only debug mode.
     *
     * @param {boolean} debug
     */
    public static void setDebuggingLogOnly(Boolean debug) {
        PhantomBot.enableDebugging = debug;
        PhantomBot.enableDebuggingLogOnly = debug;
    }

    /*
     * Tells you the bot name.
     *
     * @return {string} bot name
     */
    public String getBotName() {
        return this.botName;
    }

    /*
     * Gives you the current data store
     *
     * @return {datastore} dataStore
     */
    public DataStore getDataStore() {
        return this.dataStore;
    }

    /*
     * Tells you if the bot is exiting
     *
     * @return {boolean} exit
     */
    public Boolean isExiting() {
        return this.isExiting;
    }

    /*
     * Give's you the channel for that channelName.
     *
     * @return {channel}
     */
    public String getChannelName() {
        return this.channelName;
    }

    /*
     * Tells you if the discord token has been set.
     *
     * @return {boolean}
     */
    public Boolean hasDiscordToken() {
        return this.discordToken.isEmpty();
    }

    /*
     * Give's you the session for that channel.
     *
     * @return {session}
     */
    public TwitchSession getSession() {
        return this.session;
    }

    /*
     * Method that returns the message limit
     *
     * @return {double} messageLimit
     */
    public static double getMessageLimit() {
        return messageLimit;
    }

    /*
     * Give's you the message limit.
     *
     * @return {long} message limit
     */
    public static long getMessageInterval() {
        return (long) ((30.0 / messageLimit) * 1000);
    }

    /*
     * Give's you the whisper limit. *Currently not used*
     *
     * @return {long} whisper limit
     */
    public static long getWhisperInterval() {
        return (long) ((60.0 / whisperLimit) * 1000);
    }

    /*
     * Helper method to see if a module is enabled.
     *
     * @param  String  Module name to check for
     * @return boolean If the module is enabled or not
     */
    public boolean checkModuleEnabled(String module) {
        try {
            return dataStore.GetString("modules", "", module).equals("true");
        } catch (NullPointerException ex) {
            return false;
        }
    }

    /*
     * Checks if a value is true in the datastore.
     *
     * @param String  Db table to check.
     * @param String  Db key to check in that table.
     */
    public boolean checkDataStore(String table, String key) {
        try {
            return (dataStore.HasKey(table, "", key) && dataStore.GetString(table, "", key).equals("true"));
        } catch (NullPointerException ex) {
            return false;
        }
    }

    /*
     * Method that returns the basic bot info.
     *
     * @return {String}
     */
    public String getBotInformation() {
        return "\r\nJava Version: " + System.getProperty("java.runtime.version") + "\r\nOS Version: "
                + System.getProperty("os.name") + " " + System.getProperty("os.version") + " ("
                + System.getProperty("os.arch") + ")\r\nPanel Version: " + RepoVersion.getPanelVersion() + "\r\n"
                + getBotInfo() + "\r\n\r\n";
    }

    /*
     * Method that gets the PhantomBot properties.
     *
     * @return 
     */
    public Properties getProperties() {
        return this.pbProperties;
    }

    /*
     * Loads everything up.
     */
    private void init() {
        /* Is the web toggle enabled? */
        if (webEnabled) {
            try {
                checkPortAvailabity(basePort);
                checkPortAvailabity(panelSocketPort);

                /* Is the music toggled on? */
                if (musicEnabled) {
                    checkPortAvailabity(ytSocketPort);
                    if (useHttps) {
                        /* Set the music player server */
                        youtubeSocketSecureServer = new YTWebSocketSecureServer(bindIP, ytSocketPort, youtubeOAuth,
                                youtubeOAuthThro, httpsFileName, httpsPassword, socketServerTasksSize);
                        /* Start this youtube socket server */
                        youtubeSocketSecureServer.start();
                        print("YouTubeSocketSecureServer accepting connections on port: " + ytSocketPort
                                + " (SSL)");
                    } else {
                        /* Set the music player server */
                        youtubeSocketServer = new YTWebSocketServer(bindIP, ytSocketPort, youtubeOAuth,
                                youtubeOAuthThro);
                        /* Start this youtube socket server */
                        youtubeSocketServer.start();
                        print("YouTubeSocketServer accepting connections on port: " + ytSocketPort);
                    }
                }

                if (useHttps) {
                    if (testPanelServer) {
                        newPanelSocketServer = new NewPanelSocketServer(panelSocketPort, webOAuth, webOAuthThro,
                                httpsFileName, httpsPassword);
                        newPanelSocketServer.start();
                        print("TEST PanelSocketSecureServer accepting connections on port: " + panelSocketPort
                                + " (SSL)");
                    } else {
                        /* Set up the panel socket server */
                        panelSocketSecureServer = new PanelSocketSecureServer(bindIP, panelSocketPort, webOAuth,
                                webOAuthThro, httpsFileName, httpsPassword, socketServerTasksSize);
                        /* Start the panel socket server */
                        panelSocketSecureServer.start();
                        print("PanelSocketSecureServer accepting connections on port: " + panelSocketPort
                                + " (SSL)");
                    }

                    /* Set up a new https server */
                    httpsServer = new HTTPSServer(bindIP, (basePort), oauth, webOAuth, panelUsername, panelPassword,
                            httpsFileName, httpsPassword);
                    print("HTTPS server accepting connection on port: " + basePort + " (SSL)");
                } else {
                    if (testPanelServer) {
                        newPanelSocketServer = new NewPanelSocketServer(panelSocketPort, webOAuth, webOAuthThro);
                        newPanelSocketServer.start();
                        print("TEST PanelSocketServer accepting connections on port: " + panelSocketPort);
                    } else {
                        /* Set up the panel socket server */
                        panelSocketServer = new PanelSocketServer(bindIP, panelSocketPort, webOAuth, webOAuthThro);
                        /* Set up the NEW panel socket server */
                        /* Start the panel socket server */
                        panelSocketServer.start();
                        print("PanelSocketServer accepting connections on port: " + panelSocketPort);
                    }

                    /* Set up a new http server */
                    httpServer = new HTTPServer(bindIP, (basePort), oauth, webOAuth, panelUsername, panelPassword);
                    print("HTTP server accepting connection on port: " + basePort);
                }
            } catch (Exception ex) {
                print("Exception occurred in one of the socket based services, PhantomBot will now exit.");
                System.exit(0);
            }
        }

        /* Enable GameWisp if the oAuth is set */
        if (!gameWispOAuth.isEmpty() && checkModuleEnabled("./handlers/gameWispHandler.js")) {
            /* Set the oAuths */
            GameWispAPIv1.instance().SetAccessToken(gameWispOAuth);
            GameWispAPIv1.instance().SetRefreshToken(gameWispRefresh);
            SingularityAPI.instance().setAccessToken(gameWispOAuth);
            SingularityAPI.instance().StartService();
            /* get a fresh token */
            doRefreshGameWispToken();
        }

        /* Connect to Discord if the data is present. */
        if (!discordToken.isEmpty()) {
            DiscordAPI.instance().connect(discordToken);
        }

        /* Set Streamlabs currency code, if possible */
        if (dataStore.HasKey("donations", "", "currencycode")) {
            TwitchAlertsAPIv1.instance().SetCurrencyCode(dataStore.GetString("donations", "", "currencycode"));
        }

        /* Check to see if all the Twitter info needed is there */
        if (!twitterUsername.isEmpty() && !twitterAccessToken.isEmpty() && !twitterConsumerToken.isEmpty()
                && !twitterConsumerSecret.isEmpty() && !twitterSecretToken.isEmpty()) {
            /* Set the Twitter tokens */
            TwitterAPI.instance().setUsername(twitterUsername);
            TwitterAPI.instance().setAccessToken(twitterAccessToken);
            TwitterAPI.instance().setSecretToken(twitterSecretToken);
            TwitterAPI.instance().setConsumerKey(twitterConsumerToken);
            TwitterAPI.instance().setConsumerSecret(twitterConsumerSecret);
            /* Check to see if the tokens worked */
            this.twitterAuthenticated = TwitterAPI.instance().authenticate();
        }

        /* print a extra line in the console. */
        print("");

        /* Create configuration for YTPlayer v2.0 for the WS port. */
        String data = "";
        String http = (useHttps ? "https://" : "http://");

        try {
            data += "// Configuration for YTPlayer\r\n";
            data += "// Automatically Generated by PhantomBot at Startup\r\n";
            data += "// Do NOT Modify! Overwritten when PhantomBot is restarted!\r\n";
            data += "var playerPort = " + ytSocketPort + ";\r\n";
            data += "var channelName = \"" + channelName + "\";\r\n";
            data += "var auth=\"" + youtubeOAuth + "\";\r\n";
            data += "var http=\"" + http + "\";\r\n";
            data += "function getPlayerPort() { return playerPort; }\r\n";
            data += "function getChannelName() { return channelName; }\r\n";
            data += "function getAuth() { return auth; }\r\n";
            data += "function getProtocol() { return http; }\r\n";

            /* Create a new file if it does not exist */
            if (!new File("./web/ytplayer/").exists())
                new File("./web/ytplayer/").mkdirs();
            if (!new File("./web/ytplayer/js").exists())
                new File("./web/ytplayer/js").mkdirs();

            /* Write the data to that file */
            Files.write(Paths.get("./web/ytplayer/js/playerConfig.js"), data.getBytes(StandardCharsets.UTF_8),
                    StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
        } catch (IOException ex) {
            com.gmt2001.Console.err.printStackTrace(ex);
        }

        /* Create configuration for YTPlayer Playlist v2.0 for the WS port. */
        data = "";
        try {
            data += "//Configuration for YTPlayer\r\n";
            data += "//Automatically Generated by PhantomBot at Startup\r\n";
            data += "//Do NOT Modify! Overwritten when PhantomBot is restarted!\r\n";
            data += "var playerPort = " + ytSocketPort + ";\r\n";
            data += "var channelName = \"" + channelName + "\";\r\n";
            data += "var auth=\"" + youtubeOAuthThro + "\";\r\n";
            data += "var http=\"" + http + "\";\r\n";
            data += "function getPlayerPort() { return playerPort; }\r\n";
            data += "function getChannelName() { return channelName; }\r\n";
            data += "function getAuth() { return auth; }\r\n";
            data += "function getProtocol() { return http; }\r\n";

            /* Create a new file if it does not exist */
            if (!new File("./web/playlist/").exists())
                new File("./web/playlist/").mkdirs();
            if (!new File("./web/playlist/js").exists())
                new File("./web/playlist/js").mkdirs();

            /* Write the data to that file */
            Files.write(Paths.get("./web/playlist/js/playerConfig.js"), data.getBytes(StandardCharsets.UTF_8),
                    StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
        } catch (IOException ex) {
            com.gmt2001.Console.err.printStackTrace(ex);
        }

        /* Create configuration for WebPanel for the WS port. */
        data = "";
        try {
            data += "// Configuration for Control Panel\r\n";
            data += "// Automatically Generated by PhantomBot at Startup\r\n";
            data += "// Do NOT Modify! Overwritten when PhantomBot is restarted!\r\n";
            data += "var panelSettings = {\r\n";
            data += "    panelPort   : " + panelSocketPort + ",\r\n";
            data += "    channelName : \"" + channelName + "\",\r\n";
            data += "    auth        : \"" + webOAuth + "\",\r\n";
            data += "    http        : \"" + http + "\"\r\n";
            data += "};\r\n\r\n";
            data += "function getPanelPort() { return panelSettings.panelPort; }\r\n";
            data += "function getChannelName() { return panelSettings.channelName; }\r\n";
            data += "function getAuth() { return panelSettings.auth; }\r\n";
            data += "function getProtocol() { return panelSettings.http; }\r\n";

            /* Create a new file if it does not exist */
            if (!new File("./web/panel/").exists())
                new File("./web/panel/").mkdirs();
            if (!new File("./web/panel/js").exists())
                new File("./web/panel/js").mkdirs();

            byte[] bytes = data.getBytes(StandardCharsets.UTF_8);

            /* Write the data to that file */
            Files.write(Paths.get("./web/panel/js/panelConfig.js"), bytes, StandardOpenOption.CREATE,
                    StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);

            // If betap write the file in that folder too.
            if (PhantomBot.betap) {
                Files.write(Paths.get("./web/beta-panel/js/utils/panelConfig.js"), bytes, StandardOpenOption.CREATE,
                        StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
            }
        } catch (IOException ex) {
            com.gmt2001.Console.err.printStackTrace(ex);
        }

        /* Create configuration for Read-Only Access to WS port. */
        data = "";
        try {
            data += "// Configuration for Control Panel\r\n";
            data += "// Automatically Generated by PhantomBot at Startup\r\n";
            data += "// Do NOT Modify! Overwritten when PhantomBot is restarted!\r\n";
            data += "var panelSettings = {\r\n";
            data += "    panelPort   : " + panelSocketPort + ",\r\n";
            data += "    channelName : \"" + channelName + "\",\r\n";
            data += "    auth        : \"" + webOAuthThro + "\",\r\n";
            data += "    http        : \"" + http + "\"\r\n";
            data += "};\r\n\r\n";
            data += "function getPanelPort() { return panelSettings.panelPort; }\r\n";
            data += "function getChannelName() { return panelSettings.channelName; }\r\n";
            data += "function getAuth() { return panelSettings.auth; }\r\n";
            data += "function getProtocol() { return panelSettings.http; }\r\n";

            /* Create a new file if it does not exist */
            if (!new File("./web/common/").exists())
                new File("./web/common/").mkdirs();
            if (!new File("./web/common/js").exists())
                new File("./web/common/js").mkdirs();

            /* Write the data to that file */
            Files.write(Paths.get("./web/common/js/wsConfig.js"), data.getBytes(StandardCharsets.UTF_8),
                    StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
        } catch (IOException ex) {
            com.gmt2001.Console.err.printStackTrace(ex);
        }

        /* check if the console is interactive */
        if (interactive) {
            ConsoleInputListener consoleIL = new ConsoleInputListener();
            /* Start the Console Input Listener */
            consoleIL.start();
        }

        /* Register PhantomBot (this) with the event bus. */
        EventBus.instance().register(this);
        /* Register the script manager with the event bus. */
        EventBus.instance().register(ScriptEventManager.instance());
        /* Register the console event handler */
        EventBus.instance().register(ConsoleEventHandler.instance());

        /* Load the datastore config */
        dataStore.LoadConfig(dataStoreConfig);

        /* Export all these to the $. api in the scripts. */
        Script.global.defineProperty("inidb", dataStore, 0);
        Script.global.defineProperty("username", UsernameCache.instance(), 0);
        Script.global.defineProperty("twitch", TwitchAPIv5.instance(), 0);
        Script.global.defineProperty("botName", botName.toLowerCase(), 0);
        Script.global.defineProperty("channelName", channelName.toLowerCase(), 0);
        Script.global.defineProperty("ownerName", ownerName.toLowerCase(), 0);
        Script.global.defineProperty("ytplayer", (useHttps ? youtubeSocketSecureServer : youtubeSocketServer), 0);
        if (testPanelServer) {
            Script.global.defineProperty("panelsocketserver", newPanelSocketServer, 0);
        } else {
            Script.global.defineProperty("panelsocketserver",
                    (useHttps ? panelSocketSecureServer : panelSocketServer), 0);
        }
        Script.global.defineProperty("random", random, 0);
        Script.global.defineProperty("youtube", YouTubeAPIv3.instance(), 0);
        Script.global.defineProperty("shortenURL", GoogleURLShortenerAPIv1.instance(), 0);
        Script.global.defineProperty("gamewisp", GameWispAPIv1.instance(), 0);
        Script.global.defineProperty("twitter", TwitterAPI.instance(), 0);
        Script.global.defineProperty("twitchCacheReady", PhantomBot.twitchCacheReady, 0);
        Script.global.defineProperty("isNightly", isNightly(), 0);
        Script.global.defineProperty("isPrerelease", isPrerelease(), 0);
        Script.global.defineProperty("version", botVersion(), 0);
        Script.global.defineProperty("changed", newSetup, 0);
        Script.global.defineProperty("discordAPI", DiscordAPI.instance(), 0);
        Script.global.defineProperty("hasDiscordToken", hasDiscordToken(), 0);
        Script.global.defineProperty("customAPI", CustomAPI.instance(), 0);
        Script.global.defineProperty("dataRenderServiceAPI", DataRenderServiceAPIv1.instance(), 0);

        /* open a new thread for when the bot is exiting */
        Thread thread = new Thread(() -> {
            onExit();
        }, "tv.phantombot.PhantomBot::onExit");

        /* Get the un time for that new thread we just created */
        Runtime.getRuntime().addShutdownHook(thread);

        /* And finally try to load init, that will then load the scripts */
        try {
            ScriptManager.loadScript(new File("./scripts/init.js"));
        } catch (IOException ex) {
            com.gmt2001.Console.err.printStackTrace(ex);
        }

        // Moved this to debug only. People are already asking questions.
        if (PhantomBot.enableDebugging) {
            /* Check for bot verification. */
            print("Bot Verification Status: " + (TwitchAPIv5.instance().getBotVerified(this.botName) ? "" : " NOT ")
                    + "Verified.");
        }

        /* Check for a update with PhantomBot */
        doCheckPhantomBotUpdate();

        /* Perform SQLite datbase backups. */
        if (this.backupSQLiteAuto) {
            doBackupSQLiteDB();
        }
    }

    /*
     * Used for exiting the bot
     *
     */
    @SuppressWarnings("SleepWhileInLoop")
    public void onExit() {
        print(this.botName + " is shutting down...");
        isExiting = true;
        PhantomBot.isInExitState = true;

        print("Stopping all events and message dispatching...");
        ScriptFileWatcher.instance().kill();
        ScriptEventManager.instance().kill();

        /* Gonna need a way to pass this to all channels */
        if (PhantomBot.instance().getSession() != null) {
            PhantomBot.instance().getSession().close();
        }

        /* Shutdown all caches */
        if (followersCache != null) {
            print("Terminating the Twitch channel follower cache...");
            FollowersCache.killall();
        }

        if (twitchAlertsCache != null) {
            print("Terminating the Streamlabs cache...");
            DonationsCache.killall();
        }

        if (tipeeeStreamCache != null) {
            print("Terminating the TipeeeStream cache...");
            TipeeeStreamCache.killall();
        }

        if (streamElementCache != null) {
            print("Terminating the StreamElementsCache cache...");
            StreamElementsCache.killall();
        }

        print("Terminating all script modules...");
        HashMap<String, Script> scripts = ScriptManager.getScripts();
        for (Entry<String, Script> script : scripts.entrySet()) {
            script.getValue().kill();
        }

        print("Saving all data...");
        dataStore.SaveAll(true);

        /* Check to see if web is enabled */
        if (webEnabled) {
            print("Shutting down all web socket servers...");
            if (!useHttps) {
                httpServer.close();
            } else {
                httpsServer.close();
            }
            youtubeSocketServer.dispose();
        }

        try {
            for (int i = 5; i > 0; i--) {
                com.gmt2001.Console.out.print("\rWaiting for everthing else to shutdown... " + i + " ");
                Thread.sleep(1000);
            }
        } catch (InterruptedException ex) {
            com.gmt2001.Console.out.print("\r\n");
            com.gmt2001.Console.err.printStackTrace(ex);
        }

        com.gmt2001.Console.out.print("\r\n");
        print("Closing the database...");
        dataStore.CloseConnection();

        print(this.botName + " is exiting.");
    }

    /*
     * Connected to Twitch.
     *
     */
    @Handler
    public void ircJoinComplete(IrcJoinCompleteEvent event) {
        /* Check if the bot already joined once. */
        if (joined) {
            return;
        }

        joined = true;

        com.gmt2001.Console.debug.println("ircJoinComplete::" + this.channelName);

        /* Start a pubsub instance here. */
        if (this.oauth.length() > 0 && checkDataStore("chatModerator", "moderationLogs")) {
            this.pubSubEdge = TwitchPubSub.instance(this.channelName,
                    TwitchAPIv5.instance().getChannelId(this.channelName),
                    TwitchAPIv5.instance().getChannelId(this.botName), this.oauth);
        }

        /* Load the caches for each channels */
        this.twitchCache = TwitchCache.instance(this.channelName);
        this.emotesCache = EmotesCache.instance(this.channelName);
        this.followersCache = FollowersCache.instance(this.channelName);
        this.viewerListCache = ViewerListCache.instance(this.channelName);

        /* Start the donations cache if the keys are not null and the module is enabled */
        if (this.twitchAlertsKey != null && !this.twitchAlertsKey.isEmpty()
                && checkModuleEnabled("./handlers/donationHandler.js")) {
            this.twitchAlertsCache = DonationsCache.instance(this.channelName);
        }

        /* Start the TipeeeStream cache if the keys are not null and the module is enabled. */
        if (this.tipeeeStreamOAuth != null && !this.tipeeeStreamOAuth.isEmpty()
                && checkModuleEnabled("./handlers/tipeeeStreamHandler.js")) {
            this.tipeeeStreamCache = TipeeeStreamCache.instance(this.channelName);
        }

        /* Start the StreamElements cache if the keys are not null and the module is enabled. */
        if (this.streamElementsJWT != null && !this.streamElementsJWT.isEmpty()
                && checkModuleEnabled("./handlers/streamElementsHandler.js")) {
            this.streamElementCache = StreamElementsCache.instance(this.channelName);
        }

        /* Start the twitter cache if the keys are not null and the module is enabled */
        if (this.twitterAuthenticated && checkModuleEnabled("./handlers/twitterHandler.js")) {
            this.twitterCache = TwitterCache.instance(this.channelName);
        }

        /* Start the notice timer and notice handler. */
        if (pbProperties.getProperty("testnotices", "false").equals("true")) {
            this.noticeTimer = NoticeTimer.instance(this.channelName, this.session);
        }

        /* Export these to the $. api for the sripts to use */
        Script.global.defineProperty("twitchcache", this.twitchCache, 0);
        Script.global.defineProperty("emotes", this.emotesCache, 0);
        Script.global.defineProperty("session", this.session, 0);
        Script.global.defineProperty("usernameCache", this.viewerListCache, 0);
    }

    /*
     * Get private messages from Twitch.
     *
     */
    @Handler
    public void ircPrivateMessage(IrcPrivateMessageEvent event) {
        String sender = event.getSender();
        String message = event.getMessage();

        /* Check to see if the sender is jtv */
        if (sender.equalsIgnoreCase("jtv")) {
            /* Splice the mod list so we can get all the mods */
            if (message.startsWith("The moderators of this room are: ")) {
                String[] moderators = message.substring(33).split(", ");

                /* Check to see if the bot is a moderator */
                for (String moderator : moderators) {
                    if (moderator.equalsIgnoreCase(this.botName)) {
                        EventBus.instance().postAsync(
                                new IrcChannelUserModeEvent(this.session, this.session.getBotName(), "O", true));
                        /* Allow the bot to sends message to this session */
                        event.getSession().setAllowSendMessages(true);
                    }
                }
            }
        }
    }

    /*
     * user modes from twitch
     *
     */
    @Handler
    public void ircUserMode(IrcChannelUserModeEvent event) {
        /* Check to see if Twitch sent a mode event for the bot name */
        if (event.getUser().equalsIgnoreCase(this.botName) && event.getMode().equalsIgnoreCase("o")) {
            if (!event.getAdd()) {
                event.getSession().getModerationStatus();
            }
            /* Allow the bot to sends message to this session */
            event.getSession().setAllowSendMessages(event.getAdd());
        }
    }

    /*
     * messages from Twitch chat
     *
     */
    @Handler
    public void ircChannelMessage(IrcChannelMessageEvent event) {
        if (this.pubSubEdge != null) {
            this.pubSubEdge.ircChannelMessageEvent(event);
        }
    }

    /* Handle commands */
    public void handleCommand(String username, String command) {
        String arguments = "";

        /* Does the command have arguments? */
        if (command.contains(" ")) {
            String commandString = command;
            command = commandString.substring(0, commandString.indexOf(" "));
            arguments = commandString.substring(commandString.indexOf(" ") + 1);
        }
        EventBus.instance().postAsync(new CommandEvent(username, command, arguments));
    }

    /* Handle commands */
    public void handleCommandSync(String username, String command) {
        String arguments = "";

        /* Does the command have arguments? */
        if (command.contains(" ")) {
            String commandString = command;
            command = commandString.substring(0, commandString.indexOf(" "));
            arguments = commandString.substring(commandString.indexOf(" ") + 1);
        }
        EventBus.instance().post(new CommandEvent(username, command, arguments));
    }

    /* Load up main */
    public static void main(String[] args) throws IOException {
        // Move user files.
        moveUserConfig();

        /* List of properties that must exist. */
        String requiredProperties[] = new String[] { "oauth", "channel", "owner", "user" };
        String requiredPropertiesErrorMessage = "";

        if (Float.valueOf(System.getProperty("java.specification.version")) < (float) 1.8
                || Float.valueOf(System.getProperty("java.specification.version")) >= (float) 1.9) {
            System.out.println("Detected Java " + System.getProperty("java.version") + ". "
                    + "PhantomBot requires Java 8. Java 9 and above will NOT work.");
            System.exit(1);
        }

        /* Properties configuration */
        Properties startProperties = new Properties();

        /* Indicates that the botlogin.txt file should be overwritten/created. */
        Boolean changed = false;

        /* Print the user dir */
        com.gmt2001.Console.out.println("The working directory is: " + System.getProperty("user.dir"));

        com.gmt2001.Console.out.println("Detected Java " + System.getProperty("java.version") + " running on "
                + System.getProperty("os.name") + " " + System.getProperty("os.version") + " ("
                + System.getProperty("os.arch") + ")");

        /* If prompted, now that the version has been reported, exit. */
        if (args.length > 0) {
            if (args[0].equals("--version") || args[0].equals("-v")) {
                com.gmt2001.Console.out.println("PhantomBot Version: " + RepoVersion.getPhantomBotVersion() + " ("
                        + RepoVersion.getRepoVersion() + ")");
                System.exit(1);
            }
        }

        /* Load up the bot info from the bot login file */
        try {
            if (new File("./config/botlogin.txt").exists()) {
                FileInputStream inputStream = new FileInputStream("./config/botlogin.txt");
                startProperties.load(inputStream);
                inputStream.close();
            } else {
                /* Fill in the Properties object with some default values. Note that some values are left
                 * unset to be caught in the upcoming logic to enforce settings.
                 */
                startProperties.setProperty("baseport", "25000");
                startProperties.setProperty("usehttps", "false");
                startProperties.setProperty("webenable", "true");
                startProperties.setProperty("msglimit30", "19.0");
                startProperties.setProperty("musicenable", "true");
                startProperties.setProperty("whisperlimit60", "60.0");
            }
        } catch (IOException ex) {
            com.gmt2001.Console.err.printStackTrace(ex);
        } catch (Exception ex) {
            com.gmt2001.Console.err.printStackTrace(ex);
        }
        /* Load up the bot info from the environment */
        for (Entry<String, String> v : System.getenv().entrySet()) {
            String Prefix = "PHANTOMBOT_";
            String Key = v.getKey().toUpperCase();
            String Value = v.getValue();
            if (Key.startsWith(Prefix) && Prefix.length() < Key.length()) {
                Key = Key.substring(Prefix.length()).toLowerCase();
                startProperties.setProperty(Key, Value);
            }
        }
        /* Check to enable debug mode */
        if (startProperties.getProperty("debugon", "false").equals("true")) {
            com.gmt2001.Console.out.println("Debug Mode Enabled");
            PhantomBot.enableDebugging = true;
        }
        /* Check to enable debug to File */
        if (startProperties.getProperty("debuglog", "false").equals("true")) {
            com.gmt2001.Console.out.println("Debug Log Only Mode Enabled");
            PhantomBot.enableDebugging = true;
            PhantomBot.enableDebuggingLogOnly = true;
        }
        /* Check to enable Script Reloading */
        if (startProperties.getProperty("reloadscripts", "false").equals("true")) {
            com.gmt2001.Console.out.println("Enabling Script Reloading");
            PhantomBot.reloadScripts = true;
        }
        /* Check to enable Rhino Debugger */
        if (startProperties.getProperty("rhinodebugger", "false").equals("true")) {
            com.gmt2001.Console.out.println("Rhino Debugger will be launched if system supports it.");
            PhantomBot.enableRhinoDebugger = true;
        }
        /* Check to see if there's a webOauth set */
        if (startProperties.getProperty("webauth") == null) {
            startProperties.setProperty("webauth", generateWebAuth());
            com.gmt2001.Console.debug.println("New webauth key has been generated for ./config/botlogin.txt");
            changed = true;
        }
        /* Check to see if there's a webOAuthRO set */
        if (startProperties.getProperty("webauthro") == null) {
            startProperties.setProperty("webauthro", generateWebAuth());
            com.gmt2001.Console.debug
                    .println("New webauth read-only key has been generated for ./config/botlogin.txt");
            changed = true;
        }
        /* Check to see if there's a panelUsername set */
        if (startProperties.getProperty("paneluser") == null) {
            com.gmt2001.Console.debug.println(
                    "No Panel Username, using default value of 'panel' for Control Panel and YouTube Player");
            startProperties.setProperty("paneluser", "panel");
            changed = true;
        }
        /* Check to see if there's a panelPassword set */
        if (startProperties.getProperty("panelpassword") == null) {
            com.gmt2001.Console.debug.println(
                    "No Panel Password, using default value of 'panel' for Control Panel and YouTube Player");
            startProperties.setProperty("panelpassword", "panel");
            changed = true;
        }
        /* Check to see if there's a youtubeOAuth set */
        if (startProperties.getProperty("ytauth") == null) {
            startProperties.setProperty("ytauth", generateWebAuth());
            com.gmt2001.Console.debug
                    .println("New YouTube websocket key has been generated for ./config/botlogin.txt");
            changed = true;
        }
        /* Check to see if there's a youtubeOAuthThro set */
        if (startProperties.getProperty("ytauthro") == null) {
            startProperties.setProperty("ytauthro", generateWebAuth());
            com.gmt2001.Console.debug
                    .println("New YouTube read-only websocket key has been generated for ./config/botlogin.txt");
            changed = true;
        }

        /* Make a new botlogin with the botName, oauth or channel is not found */
        if (startProperties.getProperty("user") == null || startProperties.getProperty("oauth") == null
                || startProperties.getProperty("channel") == null) {
            try {

                com.gmt2001.Console.out.print("\r\n");
                com.gmt2001.Console.out.print("Welcome to the PhantomBot setup process!\r\n");
                com.gmt2001.Console.out.print(
                        "If you have any issues please report them on our forum, Tweet at us, or join our Discord!\r\n");
                com.gmt2001.Console.out.print("Forum: https://community.phantombot.tv/\r\n");
                com.gmt2001.Console.out.print("Documentation: https://docs.phantombot.tv/\r\n");
                com.gmt2001.Console.out.print("Twitter: https://twitter.com/PhantomBot/\r\n");
                com.gmt2001.Console.out.print("Discord: https://discord.gg/rkPqDuK/\r\n");
                com.gmt2001.Console.out.print("Support PhantomBot on Patreon: https://phantombot.tv/support/\r\n");
                com.gmt2001.Console.out.print("\r\n");

                final String os = System.getProperty("os.name").toLowerCase();

                // Detect Windows, MacOS, Linux or any other operating system.
                if (os.startsWith("win")) {
                    com.gmt2001.Console.out
                            .print("PhantomBot has detected that your device is running Windows.\r\n");
                    com.gmt2001.Console.out.print(
                            "Here's the setup guide for Windows: https://community.phantombot.tv/t/windows-setup-guide/");
                } else if (os.startsWith("mac")) {
                    com.gmt2001.Console.out.print("PhantomBot has detected that your device is running macOS.\r\n");
                    com.gmt2001.Console.out.print(
                            "Here's the setup guide for macOS: https://community.phantombot.tv/t/macos-setup-guide/");
                } else {
                    com.gmt2001.Console.out.print("PhantomBot has detected that your device is running Linux.\r\n");
                    com.gmt2001.Console.out.print(
                            "Here's the setup guide for Ubuntu: https://community.phantombot.tv/t/ubuntu-16-04-lts-setup-guide/\r\n");
                    com.gmt2001.Console.out.print(
                            "Here's the setup guide for CentOS: https://community.phantombot.tv/t/centos-7-setup-guide/");
                }

                com.gmt2001.Console.out.print("\r\n\r\n\r\n");

                // Bot name.
                do {
                    com.gmt2001.Console.out.print("1. Please enter the bot's Twitch username: ");

                    startProperties.setProperty("user", System.console().readLine().trim().toLowerCase());
                } while (startProperties.getProperty("user", "").length() <= 0);

                // Twitch oauth.
                do {
                    com.gmt2001.Console.out.print("\r\n");
                    com.gmt2001.Console.out
                            .print("2. You will now need a OAuth token for the bot to be able to chat.\r\n");
                    com.gmt2001.Console.out.print(
                            "Please note, this OAuth token needs to be generated while you're logged in into the bot's Twitch account.\r\n");
                    com.gmt2001.Console.out.print(
                            "If you're not logged in as the bot, please go to https://twitch.tv/ and login as the bot.\r\n");
                    com.gmt2001.Console.out
                            .print("Get the bot's OAuth token here: https://twitchapps.com/tmi/\r\n");
                    com.gmt2001.Console.out.print("Please enter the bot's OAuth token: ");

                    startProperties.setProperty("oauth", System.console().readLine().trim());
                } while (startProperties.getProperty("oauth", "").length() <= 0);

                // api oauth.
                do {
                    com.gmt2001.Console.out.print("\r\n");
                    com.gmt2001.Console.out.print(
                            "3. You will now need your channel OAuth token for the bot to be able to change your title and game.\r\n");
                    com.gmt2001.Console.out.print(
                            "Please note, this OAuth token needs to be generated while you're logged in into your caster account.\r\n");
                    com.gmt2001.Console.out.print(
                            "If you're not logged in as the caster, please go to https://twitch.tv/ and login as the caster.\r\n");
                    com.gmt2001.Console.out
                            .print("Get the your OAuth token here: https://phantombot.tv/oauth/\r\n");
                    com.gmt2001.Console.out.print("Please enter your OAuth token: ");

                    startProperties.setProperty("apioauth", System.console().readLine().trim());
                } while (startProperties.getProperty("apioauth", "").length() <= 0);

                // Channel name.
                do {
                    com.gmt2001.Console.out.print("\r\n");
                    com.gmt2001.Console.out
                            .print("4. Please enter the name of the Twitch channel the bot should join: ");

                    startProperties.setProperty("channel", System.console().readLine().trim());
                } while (startProperties.getProperty("channel", "").length() <= 0);

                // Panel username.
                do {
                    com.gmt2001.Console.out.print("\r\n");
                    com.gmt2001.Console.out.print("5. Please enter a custom username for the web panel: ");

                    startProperties.setProperty("paneluser", System.console().readLine().trim());
                } while (startProperties.getProperty("paneluser", "").length() <= 0);

                // Panel password.
                do {
                    com.gmt2001.Console.out.print("\r\n");
                    com.gmt2001.Console.out.print("6. Please enter a custom password for the web panel: ");

                    startProperties.setProperty("panelpassword", System.console().readLine().trim());
                } while (startProperties.getProperty("panelpassword", "").length() <= 0);

                com.gmt2001.Console.out.print("\r\n");
                com.gmt2001.Console.out.print("PhantomBot will launch in 10 seconds.\r\n");
                com.gmt2001.Console.out.print(
                        "If you're hosting the bot locally you can access the control panel here: http://localhost:25000/panel \r\n");
                com.gmt2001.Console.out.print(
                        "If you're running the bot on a server, make sure to open the following ports: \r\n");
                com.gmt2001.Console.out.print(
                        "25000, 25003, and 25004. You have to change 'localhost' to your server ip to access the panel. \r\n");

                try {
                    Thread.sleep(10000);
                } catch (InterruptedException ex) {
                    com.gmt2001.Console.debug.println("Failed to sleep in setup: " + ex.getMessage());
                }

                changed = true;
                newSetup = true;
            } catch (NullPointerException ex) {
                com.gmt2001.Console.err.printStackTrace(ex);
                com.gmt2001.Console.out.println("[ERROR] Failed to setup PhantomBot. Now exiting...");
                System.exit(0);
            }
        }

        /* Make sure the oauth has been set correctly */
        if (startProperties.getProperty("oauth") != null) {
            if (!startProperties.getProperty("oauth").startsWith("oauth")
                    && !startProperties.getProperty("oauth").isEmpty()) {
                startProperties.setProperty("oauth", "oauth:" + startProperties.getProperty("oauth"));
                changed = true;
            }
        }

        /* Make sure the apiOAuth has been set correctly */
        if (startProperties.getProperty("apioauth") != null) {
            if (!startProperties.getProperty("apioauth").startsWith("oauth")
                    && !startProperties.getProperty("apioauth").isEmpty()) {
                startProperties.setProperty("apioauth", "oauth:" + startProperties.getProperty("apioauth"));
                changed = true;
            }
        }

        /* Make sure the channelName does not have a # */
        if (startProperties.getProperty("channel").startsWith("#")) {
            startProperties.setProperty("channel", startProperties.getProperty("channel").substring(1));
            changed = true;
        } else if (startProperties.getProperty("channel").contains(".tv")) {
            startProperties.setProperty("channel", startProperties.getProperty("channel")
                    .substring(startProperties.getProperty("channel").indexOf(".tv/") + 4).replaceAll("/", ""));
            changed = true;
        }

        /* Check for the owner after the channel check is done. */
        if (startProperties.getProperty("owner") == null) {
            if (startProperties.getProperty("channel") != null) {
                if (!startProperties.getProperty("channel").isEmpty()) {
                    startProperties.setProperty("owner", startProperties.getProperty("channel"));
                    changed = true;
                }
            }
        }

        /* Iterate the properties and delete entries for anything that does not have a
         * value.
         */
        for (String propertyKey : startProperties.stringPropertyNames()) {
            if (startProperties.getProperty(propertyKey).isEmpty()) {
                changed = true;
                startProperties.remove(propertyKey);
            }
        }

        /*
         * Check for required settings.
         */
        for (String requiredProperty : requiredProperties) {
            if (startProperties.getProperty(requiredProperty) == null) {
                requiredPropertiesErrorMessage += requiredProperty + " ";
            }
        }

        if (!requiredPropertiesErrorMessage.isEmpty()) {
            com.gmt2001.Console.err.println();
            com.gmt2001.Console.err.println("Missing Required Properties: " + requiredPropertiesErrorMessage);
            com.gmt2001.Console.err.println("Exiting PhantomBot");
            System.exit(0);
        }

        /* Check to see if anything changed */
        if (changed) {
            Properties outputProperties = new Properties() {
                @Override
                public synchronized Enumeration<Object> keys() {
                    return Collections.enumeration(new TreeSet<>(super.keySet()));
                }
            };

            try {
                try (FileOutputStream outputStream = new FileOutputStream("./config/botlogin.txt")) {
                    outputProperties.putAll(startProperties);
                    outputProperties.store(outputStream, "PhantomBot Configuration File");
                }
            } catch (IOException ex) {
                com.gmt2001.Console.err.printStackTrace(ex);
            }
        }

        /* Start PhantomBot */
        PhantomBot.instance = new PhantomBot(startProperties);
    }

    public void updateGameWispTokens(String[] newTokens) {
        Properties outputProperties = new Properties() {
            @Override
            public synchronized Enumeration<Object> keys() {
                return Collections.enumeration(new TreeSet<>(super.keySet()));
            }
        };

        gameWispOAuth = newTokens[0];
        gameWispRefresh = newTokens[1];

        pbProperties.setProperty("gamewispauth", newTokens[0]);
        pbProperties.setProperty("gamewisprefresh", newTokens[1]);

        try {
            try (FileOutputStream outputStream = new FileOutputStream("./config/botlogin.txt")) {
                outputProperties.putAll(pbProperties);
                outputProperties.store(outputStream, "PhantomBot Configuration File");
            }
            print("GameWisp Token has been refreshed.");
        } catch (IOException ex) {
            com.gmt2001.Console.err.println(
                    "!!!! CRITICAL !!!! Failed to update GameWisp Refresh Tokens into ./config/botlogin.txt! Must manually add!");
            com.gmt2001.Console.err.println(
                    "!!!! CRITICAL !!!! gamewispauth = " + newTokens[0] + " gamewisprefresh = " + newTokens[1]);
        }

        SingularityAPI.instance().setAccessToken(gameWispOAuth);
    }

    /* gen a oauth */
    private static String generateWebAuth() {
        return generateRandomString(30);
    }

    /* gen a random string */
    public static String generateRandomString(int length) {
        String randomAllowed = "01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        char[] randomChars = randomAllowed.toCharArray();
        char[] randomBuffer;

        randomBuffer = new char[length];
        SecureRandom random = new SecureRandom();
        for (int i = 0; i < randomBuffer.length; i++) {
            randomBuffer[i] = randomChars[random.nextInt(randomChars.length)];
        }
        return new String(randomBuffer);
    }

    /*
     * doRefreshGameWispToken
     *
     */
    public void doRefreshGameWispToken() {

        long curTime = System.currentTimeMillis() / 1000L;

        if (!dataStore.exists("settings", "gameWispRefreshTime")) {
            dataStore.set("settings", "gameWispRefreshTime", String.valueOf(curTime));
        }

        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        service.scheduleAtFixedRate(() -> {
            Thread.currentThread().setName("tv.phantombot.PhantomBot::doRefreshGameWispToken");

            long curTime1 = System.currentTimeMillis() / 1000L;
            String lastRunStr = dataStore.GetString("settings", "", "gameWispRefreshTime");
            long lastRun = Long.parseLong(lastRunStr);
            if ((curTime1 - lastRun) > (10 * 24 * 60 * 60)) {
                // 10 days, token expires every 35.
                dataStore.set("settings", "gameWispRefreshTime", String.valueOf(curTime1));
                updateGameWispTokens(GameWispAPIv1.instance().refreshToken());
            }
        }, 0, 1, TimeUnit.DAYS);
    }

    /*
     * doCheckPhantomBotUpdate
     */
    private void doCheckPhantomBotUpdate() {
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        service.scheduleAtFixedRate(() -> {
            Thread.currentThread().setName("tv.phantombot.PhantomBot::doCheckPhantomBotUpdate");

            String[] newVersionInfo = GitHubAPIv3.instance().CheckNewRelease();
            if (newVersionInfo != null) {
                try {
                    Thread.sleep(6000);
                    print("");
                    print("New PhantomBot Release Detected: " + newVersionInfo[0]);
                    print("Release Changelog: https://github.com/PhantomBot/PhantomBot/releases/"
                            + newVersionInfo[0]);
                    print("Download Link: " + newVersionInfo[1]);
                    print("A reminder will be provided in 24 hours!");
                    print("");
                } catch (InterruptedException ex) {
                    com.gmt2001.Console.err.printStackTrace(ex);
                }

                if (webEnabled) {
                    dataStore.set("settings", "newrelease_info", newVersionInfo[0] + "|" + newVersionInfo[1]);
                }
            } else {
                dataStore.del("settings", "newrelease_info");
            }
        }, 0, 24, TimeUnit.HOURS);
    }

    /* Set the twitch cache */
    public void setTwitchCacheReady(String twitchCacheReady) {
        PhantomBot.twitchCacheReady = twitchCacheReady;
        Script.global.defineProperty("twitchCacheReady", PhantomBot.twitchCacheReady, 0);
    }

    /**
     * Backup the database, keeping so many days.
     */
    private void doBackupSQLiteDB() {

        if (!dataStoreType.equals("sqlite3store")) {
            return;
        }

        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        service.scheduleAtFixedRate(() -> {
            Thread.currentThread().setName("tv.phantombot.PhantomBot::doBackupSQLiteDB");

            SimpleDateFormat datefmt = new SimpleDateFormat("ddMMyyyy.hhmmss");
            datefmt.setTimeZone(TimeZone.getTimeZone(timeZone));
            String timestamp = datefmt.format(new Date());

            dataStore.backupSQLite3("phantombot.auto.backup." + timestamp + ".db");

            try {
                Iterator dirIterator = FileUtils.iterateFiles(new File("./dbbackup"),
                        new WildcardFileFilter("phantombot.auto.*"), null);
                while (dirIterator.hasNext()) {
                    File backupFile = (File) dirIterator.next();
                    if (FileUtils.isFileOlder(backupFile,
                            (System.currentTimeMillis() - (long) (backupSQLiteKeepDays * 864e5)))) {
                        FileUtils.deleteQuietly(backupFile);
                    }
                }
            } catch (Exception ex) {
                com.gmt2001.Console.err.println("Failed to clean up database backup directory: " + ex.getMessage());
            }
        }, 0, backupSQLiteHourFrequency, TimeUnit.HOURS);
    }

    /*
     * Method that moves the db and botlogin into a new folder (config)
     */
    private static void moveUserConfig() {
        // Check if the config folder exists.
        if (!new File("./config/").isDirectory()) {
            new File("./config/").mkdir();
        }

        // Move the db and login file. If one of these doesn't exist it means this is a new bot.
        if (!new File("phantombot.db").exists() || !new File("botlogin.txt").exists()) {
            return;
        }

        com.gmt2001.Console.out.println("Moving the phantombot.db and botlogin.txt files into ./config");

        try {
            Files.move(Paths.get("botlogin.txt"), Paths.get("./config/botlogin.txt"));
            Files.move(Paths.get("phantombot.db"), Paths.get("./config/phantombot.db"));

            try {
                new File("phantombot.db").delete();
                new File("botlogin.txt").delete();
            } catch (Exception ex) {
                com.gmt2001.Console.err.println("Failed to delete files [phantombot.db] [botlogin.txt] ["
                        + ex.getClass().getSimpleName() + "]: " + ex.getMessage());
            }
        } catch (Exception ex) {
            com.gmt2001.Console.err.println("Failed to move files [phantombot.db] [botlogin.txt] ["
                    + ex.getClass().getSimpleName() + "]: " + ex.getMessage());
        }

        // Move audio hooks and alerts. These two files should always exists.
        if (!new File("./web/panel/js/ion-sound/sounds").exists() || !new File("./web/alerts/data").exists()) {
            return;
        }

        com.gmt2001.Console.out.println("Moving alerts and audio hooks into ./config");

        try {
            Files.move(Paths.get("./web/panel/js/ion-sound/sounds"), Paths.get("./config/audio-hooks"));
            Files.move(Paths.get("./web/alerts/data"), Paths.get("./config/gif-alerts"));

            try {
                FileUtils.deleteDirectory(new File("./web/panel/js/ion-sound/sounds"));
                FileUtils.deleteDirectory(new File("./web/alerts/data"));
            } catch (Exception ex) {
                com.gmt2001.Console.err.println("Failed to delete old audio hooks and alerts ["
                        + ex.getClass().getSimpleName() + "]: " + ex.getMessage());
            }
        } catch (Exception ex) {
            com.gmt2001.Console.err.println("Failed to move audio hooks and alerts ["
                    + ex.getClass().getSimpleName() + "]: " + ex.getMessage());
        }
    }

    /*
     * Method to export a Java list to a csv file.
     *
     * @param {String[]} headers
     * @param {List}     values
     * @param {String}   fileName
     */
    public void toCSV(String[] headers, List<String[]> values, String fileName) {
        StringBuilder builder = new StringBuilder();
        FileOutputStream stream = null;

        // Append the headers.
        builder.append(String.join(",", headers)).append("\n");

        // Append all values.
        for (String[] value : values) {
            builder.append(String.join(",", value)).append("\n");
        }

        // Write the data to a file.
        try {
            // Create a new stream.
            stream = new FileOutputStream(new File(fileName));

            // Write the content.
            stream.write(builder.toString().getBytes(Charset.forName("UTF-8")));
            stream.flush();
        } catch (IOException ex) {
            com.gmt2001.Console.err.println("Failed writing data to file [IOException]: " + ex.getMessage());
        } catch (SecurityException ex) {
            com.gmt2001.Console.err.println("Failed writing data to file [SecurityException]: " + ex.getMessage());
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException ex) {
                    com.gmt2001.Console.err.printStackTrace(ex);
                }
            }
        }
    }
}