Source: extensions/streamlabs_api/streamlabs_api_handler.js

/**
 *      StreamRoller Copyright 2023 "SilenusTA https://www.twitch.tv/olddepressedgamer"
 * 
 *      StreamRoller is an all in one streaming solution designed to give a single
 *      'second monitor' control page and allow easy integration for configuring
 *      content (ie. tweets linked to chat, overlays triggered by messages, hue lights
 *      controlled by donations etc)
 * 
 *      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 <https://www.gnu.org/licenses/>.
 */

// ============================================================================
//                           IMPORTS/VARIABLES
// ============================================================================
// note this has to be socket.io-client version 2.0.3 to allow support for Streamlabs api.
import * as fs from "fs";
import { dirname } from 'path';
import StreamlabsIo from "socket.io-client_2.0.3";
import { fileURLToPath } from 'url';
import * as logger from "../../backend/data_center/modules/logger.js";
import sr_api from "../../backend/data_center/public/streamroller-message-api.cjs";
const __dirname = dirname(fileURLToPath(import.meta.url));

let localConfig = {
    SYSTEM_LOGGING_TAG: "[EXTENSION]",
    heartBeatTimeout: 5000,
    heartBeatHandle: null,
    status: {
        connected: false // this is our connection indicator for discord
    },
    DataCenterSocket: null,
    StreamlabsSocket: null,

    settingsSmallSchedulerHandle: null,
    settingsSmallSchedulerTimeout: 1000,

    settingsLargeSchedulerHandle: null,
    settingsLargeSchedulerTimeout: 1000,
};

const default_serverConfig = {
    __version__: 0.2,
    extensionname: "streamlabs_api",
    channel: "STREAMLABS_ALERT",
    enabled: "off",
};
let serverConfig = structuredClone(default_serverConfig)
let serverCredentials = {
    API_Token: null,
}

const triggersandactions =
{
    extensionname: serverConfig.extensionname,
    description: "Handles streamlabs alerts and data",
    version: "0.2",
    channel: serverConfig.channel,
    // these are messages we can sendout that other extensions might want to use to trigger an action
    triggers:
        [
            //Streamlabs specific items
            {
                name: "StreamlabsDonationAlert",
                displaytitle: "Streamlabs Donation Received",
                description: "A Streamlabs donation was received",
                messagetype: "trigger_StreamlabsDonationReceived",
                parameters: {
                    username: "",
                    amount: "",
                    formatted_amount: "",
                    message: ""
                }
            },
            {
                name: "StreamlabsMerchAlert",
                displaytitle: "Merch Purchase",
                description: "Someone purchased your Merch",
                messagetype: "trigger_MerchPurchaseReceived",
                parameters: {
                    username: "",
                    message: "",
                    product: "",
                    imageHref: ""
                }
            },
            {
                name: "StreamlabsLoyaltyStoreRedemptionAlert",
                displaytitle: "LoyaltyStore Redemption",
                description: "Someone Reddemed something from your LoyaltyStore",
                messagetype: "trigger_StreamlabsLoyaltyStoreRedemptionReceived",
                parameters: {
                    username: "",
                    viewcount: "",
                }
            },
            //Twitch specific alerts
            {
                name: "StreamlabTwitchFollowAlert",
                displaytitle: "Follow on Twitch",
                description: "A Viewer Followed your twitch stream",
                messagetype: "trigger_TwitchFollowReceived",
                parameters: {
                    username: ""
                }
            },
            {
                name: "StreamlabsTwitchSubscriptionAlert",
                displaytitle: "Subscription on Twitch",
                description: "Someone Subscribed to your twitch stream",
                messagetype: "trigger_TwitchSubscriptionReceived",
                parameters: {
                    username: "",
                    type: "",
                    months: "",
                    gifter: "",
                }
            },
            {
                name: "StreamlabsTwitchResubAlert",
                displaytitle: "Resub on Twitch",
                description: "Someone Resubed to your twitch stream",
                messagetype: "trigger_TwitchResubReceived",
                parameters: {
                    username: "",
                    months: "",
                    streak_months: "",
                }
            },
            {
                name: "StreamlabsTwitchHostAlert",
                displaytitle: "Host on Twitch",
                description: "Someone Hosted your stream on twitch",
                messagetype: "trigger_TwitchHostReceived",
                parameters: {
                    username: "",
                    raiders: ""
                }
            },
            {
                name: "StreamlabsTwitchBitsAlert",
                displaytitle: "Bits on Twitch",
                description: "Someone Donated bits on Twitch",
                messagetype: "trigger_TwitchBitsReceived",
                parameters: {
                    username: "",
                    amount: "",
                    message: "",
                }
            },
            {
                name: "StreamlabsTwitchRaidAlert",
                displaytitle: "Raid on Twitch",
                description: "Someone Raided your stream on Twitch",
                messagetype: "trigger_TwitchRaidReceived",
                parameters: {
                    username: "",
                    viewcount: "",
                }
            },
            {
                name: "StreamlabsTwitchCharityDonationAlert",
                displaytitle: "CharityDonation on Twitch",
                description: "Someone donated to charity on your Twitch stream",
                messagetype: "trigger_TwitchCharityDonationReceived",
                parameters: {
                    username: "",
                    amount: "",
                    formatted_amount: "",
                    message: ""
                }
            },
            {
                name: "StreamlabsCharityDonationAlert",
                displaytitle: "CharityDonation on StreamLabs",
                description: "Someone donated to charity on your StreamLabs Charity",
                messagetype: "trigger_CharityDonationReceived",
                parameters: {
                    username: "",
                    twitchDisplayName: "",
                    amount: "",
                    formatted_amount: "",
                    message: "",
                    campaignId: ""
                }
            },
            {
                name: "StreamlabsTwitchSubMysteryAlert",
                displaytitle: "SubMystery gift on Twitch",
                description: "Someone gifted some subs on your Twitch stream",
                messagetype: "trigger_TwitchSubMysteryGiftReceived",
                parameters: {
                    gifter: "",
                    amount: ""
                }
            },
            //Youtube specific alerts
            {
                name: "StreamlabsYouTubeSubscriptionAlert",
                displaytitle: "Subscription on YouTube",
                description: "Someone Subscribed on YouTube",
                messagetype: "trigger_YouTubeSubscriptionReceived",
                parameters: {
                    username: ""
                }
            },
            {
                name: "StreamlabsYouTubeMemberAlert",
                displaytitle: "Member on YouTube",
                description: "A Member joined on YouTube",
                messagetype: "trigger_YouTubeMemberReceived",
                parameters: {
                    username: "",
                    months: "",
                    subplan: "",
                    message: "",
                }
            },
            {
                name: "StreamlabsYouTubeSuperchatAlert",
                displaytitle: "Superchat on YouTube",
                description: "Someone Superchated on YouTube",
                messagetype: "trigger_YouTubeSuperchatReceived",
                parameters: {
                    username: "",
                    amount: "",
                    formatted_amount: "",
                    message: ""
                }
            },
            {
                name: "StreamlabsDataDump",
                displaytitle: "StreamLabs data dump",
                description: "Stream labs data dump, ie subs/month, top10 donators etc etc",
                messagetype: "trigger_StreamlabsDataDump",
                parameters: {
                    data: ""
                }
            },
            {
                name: "StreamlabsDataDumpUnderlying",
                displaytitle: "StreamLabs Underlying data dump",
                description: "Stream labs Underlying data dump, ie subs/month, top10 donators etc etc",
                messagetype: "trigger_StreamlabsDataDumpUnderlying",
                parameters: {
                    data: ""
                }
            }
        ],
    // these are messages we can receive to perform an action
}
// ============================================================================
//                           FUNCTION: start
// ============================================================================
/**
 * Starts the extension using the given data.
 * @param {string} host 
 * @param {number} port 
 * @param {number} heartbeat 
 */
function start (host, port, heartbeat)
{
    logger.extra(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".start", "host", host, "port", port, "heartbeat", heartbeat);
    if (typeof (heartbeat) != "undefined")
        localConfig.heartBeatTimeout = heartbeat;
    else
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".initialise", "DataCenterSocket no heatbeat passed:", heartbeat);
    // ########################## SETUP DATACENTER CONNECTION ###############################
    try
    {
        //use the helper to setup and register our callbacks
        localConfig.DataCenterSocket = sr_api.setupConnection(onDataCenterMessage, onDataCenterConnect, onDataCenterDisconnect, host, port);
    } catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + "streamlabs_api_handler.start", "localConfig.DataCenterSocket connection failed:", err);
        throw ("streamlabs_api_handler.js failed to connect to data socket");
    }
}
// ########################## STREAMLABS API CONNECTION #######################
// ============================================================================
//                           FUNCTION: connectToStreamlabs
// ============================================================================
/**
 * Connect to the streamlabs API
 */
function connectToStreamlabs ()
{
    if (!serverCredentials.API_Token)
    {
        disconnectStreamlabs();
        logger.err(localConfig.SYSTEM_LOGGING_TAG + "streamlabs_api_handler.js", "Credentials not set");
    }
    else
    {
        try
        {
            if (serverConfig.enabled)
            {
                if (localConfig.StreamlabsSocket)
                    disconnectStreamlabs();
                localConfig.StreamlabsSocket = StreamlabsIo("https://sockets.streamlabs.com:443?token=" + serverCredentials.API_Token, { transports: ["websocket"] });
                // handlers
                localConfig.StreamlabsSocket.on("connect", (data) => onStreamlabsConnect(data));
                localConfig.StreamlabsSocket.on("disconnect", (reason) => onStreamlabsDisconnect(reason));
                localConfig.StreamlabsSocket.on("event", (data) => onStreamlabsEvent(data));
            }
            else
                logger.warn(localConfig.SYSTEM_LOGGING_TAG + "streamlabs_api_handler.start", "Streamlabs disabled in config");
            if (localConfig.StreamlabsSocket == "false")
            {
                console.log("connectToStreamlabs: failed to connect")
            }
        } catch (err)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + "connectToStreamlabs", "Streamlabs connection failed:", err);
        }
    }
}
// ============================================================================
//                           FUNCTION: connectToStreamlabs
// ============================================================================
/**
 * Connect to the streamlabs API
 */
function disconnectStreamlabs ()
{
    try
    {
        if (localConfig.StreamlabsSocket)
        {
            localConfig.StreamlabsSocket.removeAllListeners()
            localConfig.StreamlabsSocket.disconnect();
            localConfig.StreamlabsSocket = null;
        }
    } catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + "disconnectStreamlabs", "Streamlabs disconnect failed:", err);
    }
}
// ============================================================================
//                           FUNCTION: onStreamlabsDisconnect
// ============================================================================
/**
 * Called when Streamlabs API socket has disconnected
 * @param {string} reason 
 */
function onStreamlabsDisconnect (reason)
{
    localConfig.status.connected = false;
    localConfig.StreamlabsSocket.destroy();
    localConfig.StreamlabsSocket = null;

    SendSettingsWidgetLarge()
    SendSettingsWidgetSmall()
    if (serverConfig.enabled == "on")
        console.log("Streamlabs disconnected, possible reasons(bad credentials, network interruption, service down)")
    serverConfig.enabled = "off";
    logger.warn(localConfig.SYSTEM_LOGGING_TAG + "streamlabs_api_handler.onStreamlabsDisconnect", reason);
}
// ============================================================================
//                           FUNCTION: onStreamlabsConnect
// ============================================================================
/**
 * Call on Streamlabs API connection
 */
function onStreamlabsConnect ()
{
    localConfig.status.connected = true;
    // start our heatbeat timer
    logger.log(localConfig.SYSTEM_LOGGING_TAG + "streamlabs_api_handler.onStreamlabsConnect", "streamlabs api socket connected");
}

// ============================================================================
//                           FUNCTION: onStreamlabsEvent
// ============================================================================
/**
 * Handles messaged from the streamlabs api
 * @param {object} data 
 */
function onStreamlabsEvent (data)
{
    // Send this data to the channel for this
    if (serverConfig.enabled === "on")
    {
        // old depreciated system. Will be removed once I update my overlay to use the new trigger system
        sr_api.sendMessage(localConfig.DataCenterSocket,
            sr_api.ServerPacket(
                "ChannelData",
                serverConfig.extensionname,
                data,
                serverConfig.channel
            ));
        // send alerts using trigger system
        parseStreamlabsMessage(data)
    }
}
// ########################## DATACENTER CONNECTION #######################
// ============================================================================
//                           FUNCTION: onDataCenterDisconnect
// ============================================================================
/**
 * Handles websocket disconnect message from StreamRoller
 * @param {string} reason 
 */
function onDataCenterDisconnect (reason)
{
    logger.log(localConfig.SYSTEM_LOGGING_TAG + "streamlabs_api_handler.onDataCenterDisconnect", reason);
}
// ============================================================================
//                           FUNCTION: onDataCenterConnect
// ============================================================================
/**
 * Called on connection to StreamRoller websocket
 */
function onDataCenterConnect ()
{
    //store our Id for futre reference
    logger.log(localConfig.SYSTEM_LOGGING_TAG + "streamlabs_api_handler.onDataCenterConnect", "Creating our channel");
    //register our channels
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("CreateChannel", serverConfig.extensionname, serverConfig.channel));
    // Request our config from the server
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestConfig", serverConfig.extensionname));
    // Request our credentials from the server
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestCredentials", serverConfig.extensionname));
    // clear the previous timeout if we have one
    clearTimeout(localConfig.heartBeatHandle);
    // start our heatbeat timer
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
}
// ============================================================================
//                           FUNCTION: onDataCenterMessage
// ============================================================================
/**
 * Called when we receive a message from StreamRoller
 * @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
                serverConfig = structuredClone(server_packet.data);

            SaveConfigToServer();
        }
    }
    else if (server_packet.type === "CredentialsFile")
    {
        if (server_packet.to === serverConfig.extensionname && server_packet.data != "")
        {
            // temp code to change discord token name to newer variable.
            // remove code in future. Added in 0.3.4
            if (server_packet.data.SL_SOCKET_TOKEN)
            {
                serverCredentials.API_Token = server_packet.data.SL_SOCKET_TOKEN;
                DeleteCredentialsOnServer()
                SaveCredentialToServer("API_Token", serverCredentials.API_Token)
            }
            else
            {
                serverCredentials.API_Token = server_packet.data.API_Token;
            }
            // end temp

            if (serverConfig.enabled == "on")
                connectToStreamlabs();
            else
                disconnectStreamlabs()
            SendSettingsWidgetLarge();
            SendSettingsWidgetSmall();
        }
    }
    else if (server_packet.type === "ExtensionMessage")
    {
        let extension_packet = server_packet.data;
        // received a reqest for our admin bootstrap modal code
        if (extension_packet.type === "RequestSettingsWidgetSmallCode")
            SendSettingsWidgetSmall(extension_packet.from);
        else if (extension_packet.type === "RequestSettingsWidgetLargeCode")
            SendSettingsWidgetLarge(extension_packet.from);
        // received data from our settings widget small. A user has requested some settings be changedd
        else if (extension_packet.type === "SettingsWidgetSmallData")
        {
            if (extension_packet.to === serverConfig.extensionname)
            {
                let enabledChanged = false;
                if (serverConfig.enabled != extension_packet.data.enabled_streamlabs_api_small_settings)
                {
                    enabledChanged = true;
                    if (serverConfig.enabled == "on")
                        serverConfig.enabled = "off"
                    else
                        serverConfig.enabled = "on"
                }

                // get our config values to the ones in message
                // mostly just sets channel and extension name
                for (const [key, value] of Object.entries(serverConfig))
                    if (key in extension_packet.data)
                        serverConfig[key] = extension_packet.data[key];

                // check if we have toggled ourselves on or off
                if (enabledChanged)
                    if (serverConfig.enabled == "on")
                        connectToStreamlabs()
                    else
                        disconnectStreamlabs()

                // save our data to the server for next time we run
                SaveConfigToServer();
                // broadcast our modal out so anyone showing it can update it
                SendSettingsWidgetSmall("");
            }
        }
        else if (extension_packet.type === "SettingsWidgetLargeData")
        {
            if (extension_packet.to === serverConfig.extensionname)
                parseSettingsWidgetLargeData(extension_packet.data)
        }
        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
                )
            )
        }
    }
    else if (server_packet.type === "UnknownChannel")
    {
        logger.info(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage",
            "Channel " + server_packet.data + " doesn't exist, scheduling rejoin");
        //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
        if (server_packet.data === serverConfig.channel)
        {
            setTimeout(() =>
            {
                sr_api.sendMessage(localConfig.DataCenterSocket,
                    sr_api.ServerPacket("CreateChannel",
                        serverConfig.extensionname,
                        server_packet.data));
            }, 5000);
        }
        else
        {
            setTimeout(() =>
            {
                sr_api.sendMessage(localConfig.DataCenterSocket,
                    sr_api.ServerPacket("JoinChannel",
                        serverConfig.extensionname,
                        server_packet.data));
            }, 5000);
        }
    }

}

// ============================================================================
//                           FUNCTION: SendSettingsWidgetSmall
// ============================================================================
/**
 * schedules the sending of the widget code to squish multiple requests down
 * @param {string} to 
 */
function SendSettingsWidgetSmall (to = "")
{

    clearTimeout(localConfig.settingsSmallSchedulerHandle)
    localConfig.settingsSmallSchedulerHandle = setTimeout(() =>
    {
        SendSettingsWidgetSmallScheduler(to)
    }, localConfig.settingsSmallSchedulerTimeout);


}
// ============================================================================
//                           FUNCTION: SendSettingsWidgetSmallScheduler
// ============================================================================

/**
 * Send our settings widget code to the given extension
 * @param {string} to 
 */
function SendSettingsWidgetSmallScheduler (to = "")
{
    let destinationChannel = ""
    if (to == "")
        destinationChannel = serverConfig.channel

    fs.readFile(__dirname + '/streamlabs_apisettingswidgetsmall.html', function (err, filedata)
    {
        if (err)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
                ".SendSettingsWidgetSmall", "failed to load modal", err);
        //throw err;
        else
        {
            let modalstring = filedata.toString();
            // first lets update our modal to the current settings
            for (const [key, value] of Object.entries(serverConfig))
            {
                // true values represent a checkbox so replace the "[key]checked" values with checked
                if (value === "on")
                    modalstring = modalstring.replace(key + "checked", "checked");
                //value is a string then we need to replace the text
                else if (typeof (value) == "string")
                    modalstring = modalstring.replace(key + "text", value);
            }
            if (serverConfig.enabled == "on")
                modalstring = modalstring.replace("enabled_streamlabs_api_small_settingschecked", "checked");

            // send the modal data to the server
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "SettingsWidgetSmallCode",
                        serverConfig.extensionname,
                        modalstring,
                        destinationChannel,
                        to
                    ),
                    destinationChannel,
                    to
                ));
        }
    });

}
// ============================================================================
//                           FUNCTION: SendSettingsWidgetLarge
// ============================================================================
/**
 * schedules the sending of the widget code to squish multiple requests down
 * @param {string} to 
 */
function SendSettingsWidgetLarge (to = "")
{
    clearTimeout(localConfig.settingsLargeSchedulerHandle)
    localConfig.settingsLargeSchedulerHandle = setTimeout(() =>
    {
        SendSettingsWidgetLargeScheduler(to)
    }, localConfig.settingsLargeSchedulerTimeout);
}
// ===========================================================================
//                           FUNCTION: SendSettingsWidgetLarge
// ===========================================================================
/**
 * @param {String} to channel to send to or "" to broadcast
 */
function SendSettingsWidgetLargeScheduler (to = "")
{
    // read our modal file
    fs.readFile(__dirname + "/streamlabs_apisettingswidgetlarge.html", function (err, filedata)
    {
        if (err)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.EXTENSION_NAME +
                ".SendSettingsWidgetLarge", "failed to load modal", err);
        else
        {
            let modalString = filedata.toString();
            // replace any of our server config variables'text' names in the file
            for (const [key, value] of Object.entries(serverConfig))
            {
                if (value === "on")
                    modalString = modalString.replaceAll(key + "checked", "checked");
                // replace text strings
                else if (typeof (value) == "string")
                    modalString = modalString.replaceAll(key + "text", value);
            }
            modalString = modalString.replace("streamlabs_api_Tokentext", serverCredentials.API_Token);
            // 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(
                        "SettingsWidgetLargeCode", // message type
                        serverConfig.extensionname, //our name
                        modalString,// data
                        "",
                        to,
                        serverConfig.channel
                    ),
                    "",
                    to // in this case we only need the "to" channel as we will send only to the requester
                ))
        }
    });
}
// ===========================================================================
//                           FUNCTION: parseSettingsWidgetLargeData
/**
 * parse the received data from a modal submit from the user
 * @param {object} extData // modal data
 */
// ===========================================================================
function parseSettingsWidgetLargeData (extData)
{
    let restartConnection = false;
    // reset to defaults
    if (extData.streamlabs_api_restore_defaults == "on")
    {
        serverConfig = structuredClone(default_serverConfig);
        //default credentials
        serverCredentials.API_Token = ""
        SaveConfigToServer()
        DeleteCredentialsOnServer()
        disconnectStreamlabs()
        restartConnection = true;
    }
    else
    {
        // update credentials if they have changed
        if (extData.streamlabs_api_Token != serverCredentials.API_Token)
        {
            restartConnection = true;
            serverCredentials.API_Token = extData.streamlabs_api_Token;
            if (serverCredentials.API_Token)
                SaveCredentialToServer("API_Token", serverCredentials.API_Token);
        }

        if (restartConnection)
        {
            // if we have changed some settings that need us to re-log into the server
            if (serverConfig.enableStreamerSongList == "on")
                connectToStreamlabs()
            else
                disconnectStreamlabs()

        }
    }
    //update anyone who is showing our code at the moment
    SendSettingsWidgetSmall("");
    SendSettingsWidgetLarge("");
    SaveConfigToServer();
}
// ============================================================================
//                           FUNCTION: SaveConfigToServer
// ============================================================================
/**
 * Saves our config to the server
 */
function SaveConfigToServer ()
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "SaveConfig",
            serverConfig.extensionname,
            serverConfig
        ));
}
// ============================================================================
//                           FUNCTION: SaveCredentialToServer
// ============================================================================
/**
 * Sends Credential to the server to be saved
 * @param {string} name 
 * @param {string} value 
 */
function SaveCredentialToServer (name, value)
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "UpdateCredentials",
            serverConfig.extensionname,
            {
                ExtensionName: serverConfig.extensionname,
                CredentialName: name,
                CredentialValue: value,
            },
        ));
}
// ============================================================================
//                           FUNCTION: DeleteCredentialsOnServer
// ============================================================================
/**
 * Delete our credential file from the server
 */
function DeleteCredentialsOnServer ()
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "DeleteCredentials",
            serverConfig.extensionname,
            {
                ExtensionName: serverConfig.extensionname,
            },
        ));
}
// ============================================================================
//                           FUNCTION: heartBeatCallback
// ============================================================================
/**
 * Sends out heartbeat messages so other extensions can see our status
 */
function heartBeatCallback ()
{
    let color = "red"
    if (serverConfig.enabled === "on")
        if (localConfig.status.connected)
            color = "green"
        else
            color = "orange"

    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ChannelData",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "HeartBeat",
                serverConfig.extensionname,
                {
                    connected: localConfig.status.connected,
                    color: color
                },
                serverConfig.channel),
            serverConfig.channel
        ),
    );
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
}
// ============================================================================
//                           FUNCTION: parseStreamlabsMessage
// ============================================================================
/**
 * Parses messages from StreamLabs events
 * @param {object} data 
 */
function parseStreamlabsMessage (data)
{
    let trigger
    // Streamlabs events
    //if (data.type === "donation" && data.for === "streamlabs")
    if (data.type === "donation")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsDonationAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].from;
            trigger.parameters.message = data.message[0].message;
            trigger.parameters.amount = data.message[0].amount;
            trigger.parameters.formatted_amount = data.message[0].formatted_amount;
            outputTrigger(trigger)
        }
    }
    else if (data.type === "loyalty_store_redemption" && data.for === "streamlabs")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsLoyaltyStoreRedemptionReceived")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].from;
            trigger.parameters.message = data.message[0].message;
            trigger.parameters.product = data.message[0].product;
            trigger.parameters.imageHref = data.message[0].imageHref;
            outputTrigger(trigger)
        }
    }
    else if (data.type === "merch" && data.for === "streamlabs")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsMerchAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].from;
            trigger.parameters.message = data.message[0].message;
            trigger.parameters.product = data.message[0].product;
            trigger.parameters.imageHref = data.message[0].imageHref;
            outputTrigger(trigger)
        }
    }
    // Twitch events
    else if (data.type === "follow" && data.for === "twitch_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabTwitchFollowAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].name;
            outputTrigger(trigger)
        }

    }
    else if (data.type === "subscription" && data.for === "twitch_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsTwitchSubscriptionAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].name
            trigger.parameters.gifter = data.message[0].gifter
            trigger.parameters.type = data.message[0].type
            trigger.parameters.months = data.message[0].months
            trigger.parameters.message = data.message[0].message
            outputTrigger(trigger)
        }
    }
    else if (data.type === "resub" && data.for === "twitch_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsTwitchResubAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].name
            trigger.parameters.months = data.message[0].months
            trigger.parameters.streak_months = data.message[0].streak_months
            trigger.parameters.message = data.message[0].message
            outputTrigger(trigger)
        }
    }
    else if (data.type === "bits" && data.for === "twitch_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsTwitchBitsAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].name
            trigger.parameters.amount = data.message[0].amount
            trigger.parameters.message = data.message[0].message
            outputTrigger(trigger)
        }
    }
    else if (data.type == "host" && data.for === "twitch_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsTwitchHostAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].name
            trigger.parameters.raiders = data.message[0].raiders
            outputTrigger(trigger)
        }
    }
    else if (data.type == "raid" && data.for === "twitch_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsTwitchRaidAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].name
            trigger.parameters.raiders = data.message[0].raiders
            outputTrigger(trigger)
        }
    }
    else if (data.type == "twitchcharitydonation" && data.for === "twitch_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsTwitchCharityDonationAlert")

        if (trigger)
        {
            trigger.parameters.username = data.message[0].from;
            trigger.parameters.message = data.message[0].message;
            trigger.parameters.amount = data.message[0].amount;
            trigger.parameters.formatted_amount = data.message[0].formattedAmount;
            outputTrigger(trigger)
        }
    }
    else if (data.type == "streamlabscharitydonation" && data.for === "streamlabscharity")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsCharityDonationAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].from;
            trigger.parameters.twitchDisplayName = data.message[0].twitchDisplayName
            trigger.parameters.message = data.message[0].message;
            trigger.parameters.amount = data.message[0].amount;
            trigger.parameters.formatted_amount = data.message[0].formattedAmount;
            trigger.parameters.campaignId = data.message[0].campaignId;
            outputTrigger(trigger)
        }
    }
    else if (data.type == "subMysteryGift" && data.for === "twitch_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsTwitchSubMysteryAlert")
        if (trigger)
        {
            trigger.parameters.gifter = data.message[0].gifter;
            trigger.parameters.amount = data.message[0].amount;
            outputTrigger(trigger)
        }
    }
    // Youtube events
    else if (data.type == "follow" && data.for === "youtube_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsYouTubeSubscriptionAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].name;
            outputTrigger(trigger)
        }
    }
    else if (data.type == "subscription" && data.for === "youtube_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsYouTubeMemberAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].name;
            trigger.parameters.months = data.message[0].months;
            trigger.parameters.message = data.message[0].message;
            trigger.parameters.subplan = data.message[0].sub_plan;
            outputTrigger(trigger)
        }
    }
    else if (data.type == "superchat" && data.for === "youtube_account")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsYouTubeSuperchatAlert")
        if (trigger)
        {
            trigger.parameters.username = data.message[0].name;
            trigger.parameters.amount = data.message[0].amount;
            trigger.parameters.formatted_amount = data.message[0].displayString;
            trigger.parameters.message = data.message[0].comment;
            outputTrigger(trigger)
        }
    }
    else if (data.type == "streamlabels")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsDataDump")
        if (trigger)
        {
            trigger.parameters.data = data
            outputTrigger(trigger)
        }
    }
    else if (data.type == "streamlabels.underlying")
    {
        trigger = triggersandactions.triggers.find(obj => obj.name === "StreamlabsDataDumpUnderlying")
        if (trigger)
        {
            trigger.parameters.data = data
            outputTrigger(trigger)
        }
    }
    else
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
            ".parseStreamlabsMessage", "no trigger setup yet. TBD. If you see this error please let me know and I'll add the trigger :", data.for, " message type:" + data.type);
        console.log(JSON.stringify(data, null, 2));
    }
}
// ============================================================================
//                           FUNCTION: outputTrigger
// ============================================================================
/**
 * Broadcast trigger on our channel
 * @param {object} data 
 */
function outputTrigger (data)
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ChannelData",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                data.messagetype,
                serverConfig.extensionname,
                data,
                serverConfig.channel),
            serverConfig.channel
        ),
    );

}
// ============================================================================
//                           EXPORTS: start
// ============================================================================
// Description: exports from this module
// ----------------------------- notes ----------------------------------------
// ============================================================================
export { start, triggersandactions };