Source: extensions/autopilot/server/server.js

/**
 * Copyright (C) 2023 "SilenusTA https://www.twitch.tv/olddepressedgamer"
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
import * as logger from "../../../backend/data_center/modules/logger.js";
import sr_api from "../../../backend/data_center/public/streamroller-message-api.cjs";
import { dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
import fs from "fs";
import { default_serverData } from "./default_data.js"

const localConfig = {
    host: "http://localhost",
    port: "3000",
    heartBeatHandle: null,
    heartBeatTimeout: "5000",
    DataCenterSocket: null,
}
// defaults for the serverConfig (our saved persistant data)
const default_serverConfig = {
    __version__: "0.1.1",
    extensionname: "autopilot",
    channel: "AUTOPILOT_BE",
    autopilotenabled: "on",
    autopilotresetdefaults: "off"
}
let serverConfig = structuredClone(default_serverConfig);
let serverData = structuredClone(default_serverData);
const triggersandactions =
{
    extensionname: serverConfig.extensionname,
    description: "Autopilot handles triggers/actions that allow the user to perform interesting interactions betewen extensions",
    // these are messages we can send out that other extensions might want to use to trigger an action
    version: "0.1.1",
    channel: serverConfig.channel,
    triggers:
        [
            {
                name: "Macro Triggered",
                displaytitle: "Macro Triggered",
                description: "A Macro was triggered",
                messagetype: "trigger_MacroTriggered",
                parameters: {}
            },
            {
                name: "AllTriggers",
                displaytitle: "AllTriggers",
                description: "Catches all triggers (for debugging)",
                messagetype: "trigger_AllTriggers",
                parameters: {}
            }
        ],
    // these are messages we can receive to perform an action
    actions:
        [
            {
                name: "Activate Macro",
                displaytitle: "Activate Macro",
                description: "Activate a macro function",
                messagetype: "action_ActivateMacro",
                parameters: { name: "" }
            },
            {
                name: "Set Group Pause State",
                displaytitle: "Set Group Pause State",
                description: "Pause/Unpause groups",
                messagetype: "action_SetGroupPauseState",
                parameters: {
                    group: "",
                    state: "unpaused"
                }
            },
            {
                name: "LogToConsole",
                displaytitle: "LogToConsole",
                description: "Log triggers to console",
                messagetype: "action_LogToConsole",
                parameters: {}
            },
        ],
}
// ============================================================================
//                           FUNCTION: startClient
// ============================================================================
/**
 * Starts the extension using the given data.
 * @param {String} host 
 * @param {Number} port 
 * @param {Number} heartbeat 
 */
async function startServer (host, port, heartbeat)
{
    // setup the express app that will handle client side page requests
    //app.use("/images/", express.static(__dirname + '/public/images'));
    localConfig.host = host;
    localConfig.port = port;
    try
    {
        ConnectToDataCenter(localConfig.host, localConfig.port);
    }
    catch (err)
    {
        logger.err(serverConfig.extensionname + "autopilot.startServer", "initialise failed:", err);
    }
}
// ============================================================================
//                           FUNCTION: ConnectToDataCenter
// ============================================================================
/**
 * Connect to the StreamRoller websocket
 * @param {string} host 
 * @param {number} port 
 */
function ConnectToDataCenter (host, port)
{
    try
    {
        localConfig.DataCenterSocket = sr_api.setupConnection(onDataCenterMessage, onDataCenterConnect, onDataCenterDisconnect,
            host, port);
    } catch (err)
    {
        logger.err(serverConfig.extensionname + "datahandler.initialise", "DataCenterSocket connection failed:", err);
    }
}
/**
 * Called when the StreamRoller websocket disconnects
 * @param {string} reason 
 */
function onDataCenterDisconnect (reason)
{
}
// ============================================================================
//                           FUNCTION: onDataCenterConnect
// ============================================================================
/**
 * Called when the StreaRoller websocket connection starts
 * @param {object} socket 
 */
function onDataCenterConnect (socket)
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "RequestConfig",
            serverConfig.extensionname
        ));

    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "RequestData",
            serverConfig.extensionname
        ));
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("CreateChannel", serverConfig.extensionname, serverConfig.channel)
    );
    RequestExtList();
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
}
// ============================================================================
//                           FUNCTION: onDataCenterMessage
// ============================================================================
/**
 * Handles all streamroller inbound messages
 * @param {object} server_packet 
 */
function onDataCenterMessage (server_packet)
{

    if (server_packet.type === "ConfigFile")
    {
        // check it is our config
        if (server_packet.data != "" && server_packet.to === serverConfig.extensionname)
        {
            if (server_packet.data.__version__ != default_serverConfig.__version__)
            {
                serverConfig = structuredClone(default_serverConfig);
                console.log("\x1b[31m" + serverConfig.extensionname
                    + " ConfigFile Updated", "The config file has been Updated to the latest version v"
                    + default_serverConfig.__version__ + ". Your settings may have changed " + "\x1b[0m");
            }
            else
            {
                // update our config
                if (server_packet.data != "")
                    serverConfig = structuredClone(server_packet.data)
            }
            // update server log, mainly here if we have added new default options when a user
            // updates their version of StreamRoller
            SaveConfigToServer();
        }
    }
    // -------------------------------------------------------------------------------------------------
    //                   RECEIVED DATA File
    // -------------------------------------------------------------------------------------------------
    else if (server_packet.type === "DataFile")
    {
        if (server_packet.to === serverConfig.extensionname)
        {
            // add version fixes to the users saved trigger data
            server_packet.data = UpDateOlderTriggers(server_packet.data);

            if (server_packet.data == "")
            {
                // server data is empty, possibly the first run of the code so just default it
                serverConfig = structuredClone(default_serverConfig);
                SaveConfigToServer();
            }
            else if (server_packet.data.__version__ != default_serverData.__version__)
            {
                serverData = structuredClone(default_serverData);
                console.log("\x1b[31m" + serverConfig.extensionname + " Datafile Updated", "The Data file has been Updated to the latest version v" + default_serverData.__version__ + ". Your settings may have changed" + "\x1b[0m");

                SaveDataToServer()
                SendUserPairings("");
                SendMacros()
            }
            else
            {
                if (server_packet.data != "")
                {
                    serverData = structuredClone(server_packet.data);
                    SendUserPairings("");
                    SendMacros()
                }
            }
        }
    }
    // -------------------------------------------------------------------------------------------------
    //                   RECEIVED EXTENSION LIST
    // -------------------------------------------------------------------------------------------------
    else if (server_packet.type === "ExtensionList")
    {
        if (server_packet.to === serverConfig.extensionname)
        {
            localConfig.extensions = server_packet.data
            RequestChList();
        }
    }
    // -------------------------------------------------------------------------------------------------
    //                  RECEIVED CHANNEL LIST
    // -------------------------------------------------------------------------------------------------
    else if (server_packet.type === "ChannelList")
    {
        if (server_packet.to === serverConfig.extensionname)
        {
            localConfig.channels = server_packet.data
            localConfig.channels.forEach(element =>
            {
                if (element != serverConfig.channel)
                    sr_api.sendMessage(localConfig.DataCenterSocket,
                        sr_api.ServerPacket(
                            "JoinChannel",
                            serverConfig.extensionname,
                            element
                        ));
            });
        }
    }
    // -------------------------------------------------------------------------------------------------
    //                      ### EXTENSION MESSAGE ###
    // -------------------------------------------------------------------------------------------------
    else if (server_packet.type === "ExtensionMessage")
    {
        let extension_packet = server_packet.data;
        // -------------------------------------------------------------------------------------------------
        //                   REQUEST FOR SETTINGS DIALOG
        // -------------------------------------------------------------------------------------------------
        if (extension_packet.type === "RequestSettingsWidgetSmallCode")
        {
            SendSettingsWidgetSmall(extension_packet.from);
        }
        else if (extension_packet.type.startsWith("action_LogToConsole"))
        {
            console.log("--------- action_LogToConsole -------------")
            console.log(JSON.stringify(extension_packet.data, null, 2))
            console.log("-------------------------------------------")
        }
        else if (extension_packet.type.startsWith("action_ActivateMacro"))
        {
            if (extension_packet.to == serverConfig.extensionname)
                triggerMacroButton(extension_packet.data.name)
        }

        else if (extension_packet.type.startsWith("action_SetGroupPauseState"))
        {
            if (extension_packet.to == serverConfig.extensionname)
                actionAction_SetGroupPauseState(extension_packet.data.group, extension_packet.data.state)
        }
        // -------------------------------------------------------------------------------------------------
        //                   SETTINGS DIALOG DATA
        // -------------------------------------------------------------------------------------------------
        else if (extension_packet.type === "SettingsWidgetSmallData")
        {
            if (extension_packet.to === serverConfig.extensionname)
            {
                if (extension_packet.data.autopilotresetdefaults == "on")
                {
                    serverConfig = structuredClone(default_serverConfig);
                    serverData = structuredClone(default_serverData);
                    console.log("\x1b[31m" + serverConfig.extensionname + " Defaults restored", "The config files have been reset. Your settings may have changed" + "\x1b[0m");
                    SaveConfigToServer();
                    SaveDataToServer();
                }
                else
                {
                    handleSettingsWidgetSmallData(extension_packet.data);
                    SendUserPairings("");
                    SendMacros();
                    SaveConfigToServer();
                }
            }
        }
        // -------------------------------------------------------------------------------------------------
        //                   REQUEST FOR USER TRIGGERS
        // -------------------------------------------------------------------------------------------------
        else if (extension_packet.type === "SendTriggerAndActions")
        {
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket("ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "TriggerAndActions",
                        serverConfig.extensionname,
                        triggersandactions,
                        "",
                        server_packet.from
                    ),
                    "",
                    server_packet.from
                )
            )
        }
        // -------------------------------------------------------------------------------------------------
        //                   REQUEST FOR USER TRIGGERS
        // -------------------------------------------------------------------------------------------------
        else if (extension_packet.type === "RequestUserTriggers")
        {
            SendUserPairings(extension_packet.from)
        }
        // -------------------------------------------------------------------------------------------------
        //                   REQUEST FOR USER TRIGGERS
        // -------------------------------------------------------------------------------------------------
        else if (extension_packet.type === "RequestMacroImages")
        {
            SendMacroImages(extension_packet.from)
        }
        // -------------------------------------------------------------------------------------------------
        //                   UPDATED USER PAIRINGS RECEIVED
        // -------------------------------------------------------------------------------------------------
        else if (extension_packet.type === "UpdateUserPairings")
        {
            if (server_packet.to === serverConfig.extensionname)
            {
                ProcessUserPairings(extension_packet.data)
                SaveDataToServer()
                SendUserPairings("");
                SendMacros()
            }
        }
        // -------------------------------------------------------------------------------------------------
        //                   REQUEST MACROS
        // -------------------------------------------------------------------------------------------------
        else if (extension_packet.type === "RequestMacros")
        {
            if (server_packet.to === serverConfig.extensionname)
                SendMacros()
        }
        // -------------------------------------------------------------------------------------------------
        //                   REQUEST SERVER DATA FILE
        // -------------------------------------------------------------------------------------------------
        else if (extension_packet.type === "RequestServerDataFile")
        {
            //This is used on the front end so users can save off a full data file of the server prior to updating

            if (server_packet.to === serverConfig.extensionname && server_packet.from === "autopilot_frontend")
            {

                sr_api.sendMessage(
                    localConfig.DataCenterSocket,
                    sr_api.ServerPacket(
                        "ExtensionMessage",
                        serverConfig.extensionname,
                        sr_api.ExtensionPacket(
                            "AutopilotServerData",
                            serverConfig.extensionname,
                            serverData,
                            "",
                            "autopilot_frontend"
                        ),
                        "",
                        "autopilot_frontend"
                    ));
            }
        }
        // -------------------------------------------------------------------------------------------------
        //                   REQUEST SERVER DATA FILE
        // -------------------------------------------------------------------------------------------------
        else if (extension_packet.type === "userRequestSaveDataFile")
        {
            //This is used on the front end so users can save off a full data file of the server prior to updating

            if (server_packet.to === serverConfig.extensionname && server_packet.from === "autopilot_frontend")
            {
                parseUserRequestSaveDataFile(extension_packet.data)
            }
        }
        // -------------------------------------------------------------------------------------------------
        //                   RECEIVED Unhandled extension message
        // -------------------------------------------------------------------------------------------------
        else
        {
            //console.log("ExtensionMessage not handled ", extension_packet)
            //    logger.log(serverConfig.extensionname + ".onDataCenterMessage", "ExtensionMessage not handled ", extension_packet.type, " from ", extension_packet.from);
        }
    }

    // -------------------------------------------------------------------------------------------------
    //                   RECEIVED CHANNEL DATA
    // -------------------------------------------------------------------------------------------------
    else if (server_packet.type === "ChannelData")
    {
        let extension_packet = server_packet.data;
        // -------------------------------------------------------------------------------------------------
        //                           CheckForTrigger
        //                   These are triggers in other extensions
        // -------------------------------------------------------------------------------------------------
        if (extension_packet.type.startsWith("trigger_"))
        {
            CheckTriggers(extension_packet)
        }
    }
    // -------------------------------------------------------------------------------------------------
    //                           UNKNOWN CHANNEL MESSAGE RECEIVED
    // -------------------------------------------------------------------------------------------------
    else if (server_packet.type === "UnknownChannel")
    {
        // channel might not exist yet, extension might still be starting up so lets rescehuled the join attempt
        // need to add some sort of flood control here so we are only attempting to join one at a time
        console.log("UnknownChannel", server_packet)

        if (server_packet.data != "" && server_packet.channel != undefined)
        {
            setTimeout(() =>
            {
                sr_api.sendMessage(localConfig.DataCenterSocket,
                    sr_api.ServerPacket(
                        "JoinChannel",
                        serverConfig.extensionname,
                        server_packet.data
                    ));
            }, 10000);

        }
    }
}
// ============================================================================
//                           FUNCTION: SaveConfigToServer
// ============================================================================
/**
 * Saves our config on the server
 */
function SaveConfigToServer ()
{
    // saves our serverConfig to the server so we can load it again next time we startup
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "SaveConfig",
            serverConfig.extensionname,
            serverConfig,
        ));
}
// ===========================================================================
//                           FUNCTION: SendSettingsWidgetSmall
// ===========================================================================
/**
 * Sends our small settins widget to the given channel 
 * 
 * @param {string} tochannel 
 */
function SendSettingsWidgetSmall (tochannel)
{
    fs.readFile(__dirname + "/autopilotsettingswidgetsmall.html", function (err, filedata)
    {
        if (err)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.EXTENSION_NAME +
                ".SendSettingsWidgetSmall", "failed to load modal", err);
            //throw err;
        }
        else
        {
            //get the file as a string
            let modalstring = filedata.toString();

            // mormal replaces
            for (const [key, value] of Object.entries(serverConfig))
            {
                // checkboxes
                if (value === "on")
                    modalstring = modalstring.replace(key + "checked", "checked");
                else if (typeof (value) === "string" || typeof (value) === "number")
                    modalstring = modalstring.replaceAll(key + "text", value);
            }
            // send the modified modal data to the server
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "ExtensionMessage", // this type of message is just forwarded on to the extension
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "SettingsWidgetSmallCode", // message type
                        serverConfig.extensionname, //our name
                        modalstring,// data
                        "",
                        tochannel,
                        serverConfig.channel
                    ),
                    "",
                    tochannel // in this case we only need the "to" channel as we will send only to the requester
                ))
        }
    });
}
// ===========================================================================
//                           FUNCTION: handleSettingsWidgetSmallData
// ===========================================================================
/**
 * Handles data sent when a user submits our small setting dialog box
 * @param {object} modalcode 
 */
function handleSettingsWidgetSmallData (modalcode)
{
    serverConfig.autopilotenabled = "off";
    serverConfig.autopilotresetdefaults = "off";
    for (const [key, value] of Object.entries(modalcode))
        serverConfig[key] = value;
}
// ============================================================================
//                           FUNCTION: CheckTriggers
// ============================================================================
/**
 * Handles received triggers checking if we have any matching trigger/action pairs
 * @param {object} data 
 */
function CheckTriggers (data)
{
    if (Object.keys(serverData.userPairings).length != 0 && serverData.userPairings.pairings != undefined)
    {
        for (const [key, value] of Object.entries(serverData.userPairings.pairings))
        {
            if (value.trigger.messagetype == data.data.messagetype || value.trigger.messagetype == "trigger_AllTriggers")
            {
                //console.log("value.trigger.messagetype", value.trigger.messagetype, value.action)
                ProcessReceivedTrigger(value, data)
            }
        }
    }
}
// ============================================================================
//                           FUNCTION: ProcessReceivedTrigger
// ============================================================================
/**
 * Processes a triger action pairing that has been triggered.
 * @param {object} pairing 
 * @param {object} receivedTrigger 
 */
function ProcessReceivedTrigger (pairing, receivedTrigger)
{
    //check if the event fields match the trigger fields we have set for this entry

    // if parameters are entered we need to check that they all match
    // before we trigger the action (ie if one fails to match then we must ignore this trigger)
    // IE ALL CHECKS BELOW SHOULD BE FOR FAILURES TO MATCH
    let match = true
    // we have the correct extension, channel and message type
    // lets check the variables to see if those are a match

    // this just covers { MATCHER_sender: '1', sender: '' } part of teh parameter.
    pairing.trigger.data.forEach((param) =>
    {
        for (var i in param)
        {
            let receivedParam = ""
            if (typeof receivedTrigger.data.parameters[i] !== "undefined" && receivedTrigger.data.parameters[i] !== null)
                receivedParam = receivedTrigger.data.parameters[i].toString().toLowerCase()
            let checkParam = ""
            if (typeof param[i] !== "undefined" && param[i] !== null)
                checkParam = param[i].toString().toLowerCase()
            try
            {
                // don't check the MATCHER variables as these are used to determine how to perform the match (Start of line etc)
                if (i.indexOf("MATCHER_") != 0 && i != "cooldown" && i != "lastrun")
                {
                    // get the relevant matcher for this value
                    let searchtype = param["MATCHER_" + i]

                    if (receivedParam)// && typeof checkParam === "string")
                    {
                        switch (searchtype)
                        {
                            case "2"://match anywhere
                                if (checkParam != "" && receivedParam.indexOf(checkParam) == -1)
                                    match = false;
                                break;
                            case "3"://match start of line only
                                if (checkParam != "" && receivedParam.indexOf(checkParam) != 0)
                                    match = false;
                                break;
                            case "4"://doesn't match
                                if (checkParam != "" && receivedParam.indexOf(checkParam) == 0)
                                    match = false;
                                break;
                            case "5"://match complete word only
                                if (checkParam != "" && receivedParam.indexOf(checkParam) == -1)
                                {
                                    match = false;
                                }
                                else
                                {
                                    let wordArray = receivedParam.split(" ")
                                    match = false;
                                    if (wordArray.includes(checkParam))
                                        match = true;
                                }
                                break;
                            default:
                                // check for exact match
                                if (checkParam != "" && receivedParam != checkParam)
                                    match = false;
                        }
                    }
                    //check non string types for not matching
                    else if (checkParam != "" && receivedParam != checkParam)
                        match = false;
                }
            }
            catch (err)
            {
                console.log("ProcessReceivedTrigger ERROR", pairing.trigger.data)
                console.log("ProcessReceivedTrigger error", err)
                match = false;
            }
            if (!match)
                break;
        }

    })
    if (match)
    {
        // if we have a cooldown see if we have matched it
        if (pairing.trigger.cooldown > 0)
        {
            const d = new Date();
            let now = d.getTime()
            // are we still in the the cooldown period
            if (pairing.trigger.lastrun + (pairing.trigger.cooldown * 1000) > now)
                return
            else
                pairing.trigger.lastrun = now
        }
        TriggerAction(pairing.action, receivedTrigger)
    }

}
// ============================================================================
//                           FUNCTION: TriggerAction
// ============================================================================
/**
 * Causes an action to be triggered.
 * @param {object} action action to be triggered
 * @param {object} triggerParams received trigger parameters
 */
function TriggerAction (action, triggerParams)
{
    if (action.paused)
        return
    if (serverConfig.autopilotenabled != "on")
    {
        console.log("autopilot turned off, ignoring triggers")
        return;
    }
    // regular expression to test if input is a mathmatical equasion
    // note this seems to get confused if a string has -1 in it.
    // BUG::: need a better regex
    const re = /(?:(?:^|[-+_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-]?\d+)?\s*))+$/;
    // tests to get round the bug above
    const bannedRegex = ["process", "system", "for", "while", "loop"];

    let params = {}
    // store the trigger params in the action in case the extension has use for them
    params.triggerparams = triggerParams.data
    // loop through each action field
    for (var i in action.data)
    {
        //loop through each action field name
        for (const property in action.data[i])
        {
            // store the undmodifed field data
            let tempAction = action.data[i][property]
            // *************************
            // check for user variables
            // we need a better way to do this. messy code
            // *************************
            // check if we have %%var%% in the field
            let nextVarIndex = action.data[i][property].indexOf("%%")
            // loop through all %%vars%%
            while (nextVarIndex > -1)
            {
                let endVarIndex = tempAction.indexOf("%%", nextVarIndex + 2)
                // we have a user variable
                if (endVarIndex > -1)
                {
                    // get the full variable
                    let sourceVar = tempAction.substr(nextVarIndex + 2, endVarIndex - (nextVarIndex + 2))
                    // at this point we will have either (example uses the message field and word number 2)
                    // message, 
                    // message:2 
                    // message:2*
                    // check if we have a word number option 
                    if (sourceVar.indexOf(":") > -1)
                    {
                        // get the position of the :
                        let stringIndex = sourceVar.indexOf(":")
                        // get the number the user entered after the : (minus one as non programmers don't count from 0 :P)
                        // (note currently only works with 1 digit so 0-9 words can be selected)
                        let wordNumber = (sourceVar.substr(stringIndex + 1, 1)) - 1
                        // check if we have the *
                        if ((sourceVar.substr(stringIndex + 2) == "*"))
                        {
                            // get the trigger field (ie the named variable data)
                            let sourceData = triggerParams.data.parameters[tempAction.substr(nextVarIndex + 2, stringIndex)]
                            sourceData.replaceAll("%%", "")
                            const sourceArray = sourceData.split(" ");
                            // remove the first number of words the user specified
                            for (var x = 0; x < wordNumber; x++)
                                sourceArray.splice(0, 1)
                            tempAction = tempAction.replace("%%" + sourceVar + "%%", sourceArray.join(" ").trim())
                        }
                        else
                        {
                            // get the trigger field (ie the named variable data)
                            let sourceData = triggerParams.data.parameters[tempAction.substr(nextVarIndex + 2, stringIndex)]
                            sourceData.replaceAll("%%", "")
                            // split the data into an array so we can index the work the user wants
                            const sourceArray = sourceData.split(" ");
                            tempAction = tempAction.replace("%%" + sourceVar + "%%", sourceArray[wordNumber])
                        }
                    }
                    else
                    {
                        let tmpData = triggerParams.data.parameters[sourceVar]
                        if (typeof triggerParams.data.parameters[sourceVar] == "string")
                            tmpData = tmpData.replaceAll("%%", "")
                        tempAction = tempAction.replace("%%" + sourceVar + "%%", tmpData)
                    }
                }
                if (typeof tempAction == "string")
                    nextVarIndex = tempAction.indexOf("%%", nextVarIndex + 2)
                else
                    nextVarIndex = -1;

            }
            // is this a mathmatical expression
            if (re.test(tempAction) && !bannedRegex.includes(tempAction))
            {
                try
                {
                    tempAction = eval(tempAction).toString()
                }
                catch (err)
                {
                    // this is for when the regex fails and we try to eval a string
                    tempAction = tempAction.toString()
                }
            }
            params[property] = tempAction
        }
    }
    // all actions are handled through the SR socket interface
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ExtensionMessage",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                action.messagetype,
                serverConfig.extensionname,
                params,
                "",
                action.extension),
            "",
            action.extension
        ),
    );
}
// ============================================================================
//                           FUNCTION: ProcessUserPairings
// ============================================================================
/**
 * Updates our userPairings array with the date received from the frontend webpage
 * when a user changes/adds a new item
 * @param {object} userPairings 
 */
function ProcessUserPairings (userPairings)
{
    if (userPairings != null
        && typeof (userPairings) != "undefined"
        && userPairings != ""
        && Object.keys(userPairings).length != 0)
    {
        serverData.userPairings = structuredClone(userPairings);
    }
    else
        logger.err(serverConfig.extensionname + ".ProcessUserPairings", "empty userPairings received");
}
// ============================================================================
//                           FUNCTION: RequestExtList
// ============================================================================
/**
 * Requests a list of extensions connected from the server
 */
function RequestExtList ()
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "RequestExtensionsList",
            serverConfig.extensionname,
        ));
}
/**
 * Requests a channel list from the server
 */
// ============================================================================
//                           FUNCTION: RequestChList
// ============================================================================
function RequestChList ()
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "RequestChannelsList",
            serverConfig.extensionname,
        ));
}
// ============================================================================
//                           FUNCTION: SendUserPairings
// ============================================================================
/**
 * Sends our user pairing lists to the given extension or broadcasts if we have 
 * just made a change and want to let everyone know
 * @param {string} to 
 */
function SendUserPairings (to)
{
    // temp hack to fix quizbot renaming issue of trigger_QuizbotIncorrectAnwser
    // remove this hack when we have released the code a few times. Current version at time of hack is
    // v0.3.2
    serverData.userPairings.pairings.forEach(element =>
    {
        if (element.trigger.messagetype == "trigger_QuizbotIncorrectAnwser")
        {
            element.trigger.messagetype = "trigger_QuizbotIncorrectAnswer";
            SaveDataToServer();
        }
    });
    // end hack
    if (to != "")
        sr_api.sendMessage(localConfig.DataCenterSocket,
            sr_api.ServerPacket("ExtensionMessage",
                serverConfig.extensionname,
                sr_api.ExtensionPacket(
                    "UserPairings",
                    serverConfig.extensionname,
                    serverData.userPairings,
                    serverConfig.channel,
                    to),
                serverConfig.channel,
                to
            ),
        );
    else
        sr_api.sendMessage(localConfig.DataCenterSocket,
            sr_api.ServerPacket("ChannelData",
                serverConfig.extensionname,
                sr_api.ExtensionPacket(
                    "UserPairings",
                    serverConfig.extensionname,
                    serverData.userPairings,
                    serverConfig.channel,
                    to),
                serverConfig.channel,
                to
            ),
        );
}
// ============================================================================
//                           FUNCTION: SaveDataToServer
// ============================================================================
/**
 * Save our data JSON to the server
 */
function SaveDataToServer ()
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "SaveData",
            serverConfig.extensionname,
            serverData));
}
// ============================================================================
//                           FUNCTION: SendMacros
// ============================================================================
/**
 * Sends out the current list of marcos
 */
function SendMacros ()
{
    if (serverData.userPairings.macrotriggers != undefined)
    {
        sr_api.sendMessage(localConfig.DataCenterSocket,
            sr_api.ServerPacket("ChannelData",
                serverConfig.extensionname,
                sr_api.ExtensionPacket(
                    "UserMacros",
                    serverConfig.extensionname,
                    serverData.userPairings.macrotriggers.triggers,
                    serverConfig.channel),
                serverConfig.channel
            ),
        );
    }
}
// ============================================================================
//                           FUNCTION: SendMacroImages
// ============================================================================
/**
 * Sends out the current list of macro images the user can chose from
 * @param {string} to
 */
function SendMacroImages (to)
{
    let imagelist = fs.readdirSync(__dirname + "/../public/images/deckicons")
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ExtensionMessage",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "MacroImages",
                serverConfig.extensionname,
                imagelist,
                "",
                to),
            "",
            to
        ),
    );
}
// ============================================================================
//                           FUNCTION: actionAction_SetGroupPauseState
// ============================================================================
/**
 * Handles the paused state actions to pause/unpause a trigger action pair
 * @param {string} group group to toggle
 * @param {string} state state to move to
 */
function actionAction_SetGroupPauseState (group, state)
{
    if (Object.keys(serverData.userPairings).length != 0 && serverData.userPairings.pairings != undefined)
    {
        for (const [key, value] of Object.entries(serverData.userPairings.pairings))
        {
            //console.log("value.trigger.messagetype", value.trigger.messagetype)
            if (value.group == group)
            {
                if (state == "paused")
                    value.action.paused = true;
                else if (state == "unpaused")
                    value.action.paused = false;
                else
                    logger.err(serverConfig.extensionname + ".actionAction_SetGroupPauseState", "group pause should be 'paused' or 'unpaused'. State was set to", state);
            }
        }
    }
    SaveDataToServer();
    SendUserPairings("");
    SendMacros()
}
// ============================================================================
//                           FUNCTION: triggerMacroButton
// ============================================================================
/**
 * Triggers the given actions mapped to a macro button trigger
 * @param {string} name 
 */
function triggerMacroButton (name)
{
    for (var i in serverData.userPairings.pairings)
    {
        if (serverData.userPairings.pairings[i].trigger.name == name)
        {
            let params = {}
            if (serverData.userPairings.pairings[i].action.paused)
                continue;
            for (var j in serverData.userPairings.pairings[i].action.data)
            {
                for (const property in serverData.userPairings.pairings[i].action.data[j])
                    params[property] = serverData.userPairings.pairings[i].action.data[j][property]
            }
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket("ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        serverData.userPairings.pairings[i].action.messagetype,
                        serverConfig.extensionname,
                        params,
                        "",
                        serverData.userPairings.pairings[i].action.extension),
                    "",
                    serverData.userPairings.pairings[i].action.extension
                ),
            );
        }
    }

}
// ============================================================================
//                           FUNCTION: parseUserRequestSaveDataFile
// ============================================================================
/**
 * Handles a 'userRequestSaveDataFile' message triggered when a user uploads a new
 * JSON data file containing the trigger/action pairings
 * @param {object} data 
 */
function parseUserRequestSaveDataFile (data)
{
    //do something with the file. ie check version etc.
    let response = "No Response from Server, please try again.";
    try
    {
        if (data.__version__ === default_serverData.__version__)
        {
            // overwrite our data and save it to the server.
            serverData = structuredClone(data);
            SaveDataToServer()//we have the same version of the file so we should save it over our current one.
            response = "Data saved."
        }
        else

            response = "received file version doesn't match current version: " + data.__version__ + " == " + default_serverData.__version__
    }
    catch (err)
    {
        logger.err(serverConfig.extensionname + ".parseUserRequestSaveDataFile", "Error saving data to server.Error:", err, err.message);
        response = "Error saving data to server.Error:", err, err.message;
    }
    sr_api.sendMessage(
        localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "ExtensionMessage",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "AutopilotUserSaveServerDataResponse",
                serverConfig.extensionname,
                { response: response },
                "",
                "autopilot_frontend"
            ),
            "",
            "autopilot_frontend"
        ));
}
// ============================================================================
//                           FUNCTION: heartBeat
// ============================================================================
/**
 * Sends out our heartbeat message so others can monitor the extensions status
 */
function heartBeatCallback ()
{
    let status = false;
    if (serverConfig.autopilotenabled == "on")
        status = true;
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ChannelData",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "HeartBeat",
                serverConfig.extensionname,
                { connected: status },
                serverConfig.channel),
            serverConfig.channel
        ),
    );
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
}
// ============================================================================
//                           FUNCTION: UpDateOlderTriggers
// ============================================================================
/**
 * This function parses older triggers and updates them with new fields to avoid the user having to redo them all manually. Wrote in longwinded format so it can be easier to understand for non/new coders
 * @param {object} data 
 * @returns {object}modified data packet
 */
function UpDateOlderTriggers (data)
{
    if (!data || data == "")
        return ""
    data = updatesFor_v0_3_04(data)

    return data;
}
// ============================================================================
//                           FUNCTION: UpDateOlderTriggers
// ============================================================================
/**
 * added after release v0.3.04 04-05-25
 * @param {object} data 
 * @returns {object}
 */
function updatesFor_v0_3_04 (data)
{
    // add Platform To action_SendChatMessage
    // update the action_SendChatMessage actions to add the new "platform" field if needed
    //loop over each pairing
    for (let i = 0; i < data.userPairings.pairings.length; i++)
    {
        // check if it has what we are looking for
        if (data.userPairings.pairings[i].action.messagetype == "action_SendChatMessage"
            || data.userPairings.pairings[i].action.messagetype == "action_ProcessText"
        )
        {
            let platformIndex = -1;
            // loop over each variable to see if the one we are after exists
            for (let j = 0; j < data.userPairings.pairings[i].action.data.length; j++)
            {
                if ("platform" in data.userPairings.pairings[i].action.data[j])
                    platformIndex = j;
            }
            // if it isn't there then lets add it
            if (platformIndex == -1)
            {
                if (data.userPairings.pairings[i].action.extension == "twitchchat")
                    data.userPairings.pairings[i].action.data.push({
                        platform: "twitch"
                    });
                else if (data.userPairings.pairings[i].action.extension == "twitch" || data.userPairings.pairings[i].action.extension == "kick")
                    data.userPairings.pairings[i].action.data.push({ platform: data.userPairings.pairings[i].action.extension })
                else
                    data.userPairings.pairings[i].action.data.push({ platform: "twitch" })
            }
        }
    }
    return data;
}
export { startServer, triggersandactions };