Source: extensions/streamelements/streamelements.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
// ============================================================================
import * as fs from "fs";
import { dirname } from 'path';
import socketIo from "socket.io-client_2.2.0";
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";

// debugging
//import { file_log } from './filelog.js'
function debug_file_log (message, data)
{
    //file_log(message, data)
}

const __dirname = dirname(fileURLToPath(import.meta.url));

const localConfig = {
    SYSTEM_LOGGING_TAG: "[EXTENSION]",
    DataCenterSocket: null,
    StreamElementsSocket: null,
    channelId: -1,
    heartBeatTimeout: 5000,
    SEAddress: "https://realtime.streamelements.com",
    SEPort: "3333",
    heartBeatHandle: null,
    startupCheckTimer: 500,
    readinessFlags: {
        ConfigReceived: false,
        CredentialsReceived: false,
    },
    connected: false,
    SEStartupCalled: false,
};

const default_serverConfig = {
    __version__: "0.1",
    extensionname: "streamelements",
    channel: "STREAMELEMENTS",
    streamElementsEnabled: "off",
    streamelements_restore_defaults: "off"
};
let serverConfig = structuredClone(default_serverConfig)

const default_serverCredentials =
{
    ExtensionName: 'streamelements',
    StreamerJWTToken: "",
    BotJWTToken: ""
};
let serverCredentials = structuredClone(default_serverCredentials);
const triggersandactions =
{
    extensionname: serverConfig.extensionname,
    description: "Stream Elements API for receiving alerts etc",
    version: "0.3",
    channel: serverConfig.channel,
    triggers:
        [
            {
                name: "StreamElementsFollow",
                name_UIDescription: "Stream Elements API for alerts etc",
                displaytitle: "Someone followed",
                displaytitle_UIDescription: "Someone followed",
                description: "Someone followed on the platform specified",
                description_UIDescription: "",
                messagetype: "trigger_StreamElementsFollow",
                messagetype_UIDescription: "",
                parameters: {
                    user: "",
                    user_UIDescription: "the users login name",
                    userDisplayName: "",
                    userDisplayName_UIDescription: "The display name of the user",
                    timestamp: "",
                    timestamp_UIDescription: "Time of cheer(ie 2025-05-12T00:01:13.345Z)",
                    platform: "StreamElements",
                    platform_UIDescription: "Platform the user followed on",
                    triggerActionRef: "StreamElements",
                    triggerActionRef_UIDescription: "Reference for this message",
                }
            },
            {
                name: "StreamElementsCheer",
                name_UIDescription: "Stream Elements API for alerts etc",
                displaytitle: "Someone Cheered",
                displaytitle_UIDescription: "Someone Cheered",
                description: "Someone Cheered on the platform specified",
                description_UIDescription: "",
                messagetype: "trigger_StreamElementsCheer",
                messagetype_UIDescription: "",
                parameters: {
                    user: "",
                    user_UIDescription: "the users login name",
                    userDisplayName: "",
                    userDisplayName_UIDescription: "The display name of the user",
                    avatar: "",
                    avatar_UIDescription: "Users avatar",
                    amount: 0,
                    amount_UIDescription: "Amount cheered",
                    message: 0,
                    message_UIDescription: "Cheer message",
                    timestamp: "",
                    timestamp_UIDescription: "Time of cheer(ie 2025-05-12T00:01:13.345Z)",
                    platform: "StreamElements",
                    platform_UIDescription: "Platform the user Cheer on",
                    triggerActionRef: "StreamElements",
                    triggerActionRef_UIDescription: "Reference for this message",
                }
            },
            {
                name: "StreamElementsRaid",
                name_UIDescription: "Stream Elements API for alerts etc",
                displaytitle: "Someone Raided",
                displaytitle_UIDescription: "Someone Raided",
                description: "Someone Raided on the platform specified",
                description_UIDescription: "",
                messagetype: "trigger_StreamElementsRaid",
                messagetype_UIDescription: "",
                parameters: {
                    user: "",
                    user_UIDescription: "the users login name",
                    userDisplayName: "",
                    userDisplayName_UIDescription: "The display name of the user",
                    message: "",
                    message_UIDescription: "Message if passed or empty",
                    avatar: "",
                    avatar_UIDescription: "Users avatar",
                    amount: 0,
                    amount_UIDescription: "Amount Number of raiders",
                    timestamp: "",
                    timestamp_UIDescription: "Time of cheer(ie 2025-05-12T00:01:13.345Z)",
                    platform: "StreamElements",
                    platform_UIDescription: "Platform the user Raided on",
                    triggerActionRef: "StreamElements",
                    triggerActionRef_UIDescription: "Reference for this message",
                }
            },
            {
                name: "StreamElementsSubscription",
                name_UIDescription: "Stream Elements API for alerts etc",
                displaytitle: "Someone Subscribed",
                displaytitle_UIDescription: "Someone Subscribed",
                description: "Someone Subscribed on the platform specified",
                description_UIDescription: "",
                messagetype: "trigger_StreamElementsSubscription",
                messagetype_UIDescription: "",
                parameters: {
                    user: "",
                    user_UIDescription: "the users login name",
                    userDisplayName: "",
                    userDisplayName_UIDescription: "The display name of the user",
                    message: "",
                    message_UIDescription: "Message if passed or empty",
                    avatar: "",
                    avatar_UIDescription: "Users avatar",
                    amount: 0,
                    amount_UIDescription: "Months Subscribed for",
                    gifted: false,
                    gifted_UIDescription: "Was this a gifted sub.",
                    gifter: false,
                    gifter_UIDescription: "User who gifted the sub.",
                    timestamp: "",
                    timestamp_UIDescription: "Time of Subscription(ie 2025-05-12T00:01:13.345Z)",
                    platform: "StreamElements",
                    platform_UIDescription: "Platform the user Subscribed on",
                    triggerActionRef: "StreamElements",
                    triggerActionRef_UIDescription: "Reference for this message",
                }
            },
            {
                name: "StreamElementsCommunityGiftPurchase",
                name_UIDescription: "Stream Elements API for alerts etc",
                displaytitle: "Someone gifted subs",
                displaytitle_UIDescription: "Someone gifted subs",
                description: "Someone gifted subs on the platform specified",
                description_UIDescription: "",
                messagetype: "trigger_StreamElementsCommunityGiftPurchase",
                messagetype_UIDescription: "",
                parameters: {
                    user: "",
                    user_UIDescription: "the users login name",
                    userDisplayName: "",
                    userDisplayName_UIDescription: "The display name of the user",
                    avatar: "",
                    avatar_UIDescription: "Users avatar",
                    amount: 0,
                    amount_UIDescription: "Number of gifted Subs",
                    timestamp: "",
                    timestamp_UIDescription: "Time of gift(ie 2025-05-12T00:01:13.345Z)",
                    platform: "StreamElements",
                    platform_UIDescription: "Platform the user Gifted on",
                    triggerActionRef: "StreamElements",
                    triggerActionRef_UIDescription: "Reference for this message",
                }
            },
            {
                name: "StreamElementsTip",
                name_UIDescription: "Stream Elements API for alerts etc",
                displaytitle: "Someone Tip'd",
                displaytitle_UIDescription: "Someone Tip'd",
                description: "Someone Tip'd on the platform specified",
                description_UIDescription: "",
                messagetype: "trigger_StreamElementsTip",
                messagetype_UIDescription: "",
                parameters: {
                    user: "",
                    user_UIDescription: "the users login name",
                    userDisplayName: "",
                    userDisplayName_UIDescription: "The display name of the user",
                    message: "",
                    message_UIDescription: "Message if passed or empty",
                    avatar: "",
                    avatar_UIDescription: "Users avatar",
                    amount: 0,
                    amount_UIDescription: "Tip amount",
                    timestamp: "",
                    timestamp_UIDescription: "Time of Tip(ie 2025-05-12T00:01:13.345Z)",
                    platform: "StreamElements",
                    platform_UIDescription: "Platform the user Subscribed on",
                    triggerActionRef: "StreamElements",
                    triggerActionRef_UIDescription: "Reference for this message",
                }
            },
            {
                name: "StreamElementsRedemption",
                name_UIDescription: "Stream Elements API for alerts etc",
                displaytitle: "Someone Redeemed Something",
                displaytitle_UIDescription: "Someone Redeemed",
                description: "Someone Redeemed on the platform specified",
                description_UIDescription: "",
                messagetype: "trigger_StreamElementsRedemption",
                messagetype_UIDescription: "",
                parameters: {
                    user: "",
                    user_UIDescription: "the users login name",
                    userDisplayName: "",
                    userDisplayName_UIDescription: "The display name of the user",
                    avatar: "",
                    avatar_UIDescription: "Users avatar",
                    amount: 0,
                    amount_UIDescription: "Redeemed amount",
                    providerId: 0,
                    providerId_UIDescription: "Provider ID",
                    timestamp: "",
                    timestamp_UIDescription: "Time of Redemption(ie 2025-05-12T00:01:13.345Z)",
                    platform: "StreamElements",
                    platform_UIDescription: "Platform the user Redeemed on",
                    triggerActionRef: "StreamElements",
                    triggerActionRef_UIDescription: "Reference for this message",
                }
            },
            {
                name: "StreamElementsChannelPointsRedemption",
                name_UIDescription: "Stream Elements API for alerts etc",
                displaytitle: "Someone used Channel Points",
                displaytitle_UIDescription: "Channel Points Redeemed",
                description: "Someone Redeemed Channel Points on the platform specified",
                description_UIDescription: "",
                messagetype: "trigger_StreamElementsChannelPointsRedemption",
                messagetype_UIDescription: "",
                parameters: {
                    user: "",
                    user_UIDescription: "the users login name",
                    userDisplayName: "",
                    userDisplayName_UIDescription: "The display name of the user",
                    avatar: "",
                    avatar_UIDescription: "Users avatar",
                    amount: 0,
                    amount_UIDescription: "Redeemed amount",
                    quantity: 0,
                    quantity_UIDescription: "Redeemed quantity",
                    redemption: "",
                    redemption_UIDescription: "Redemption Name",
                    timestamp: "",
                    timestamp_UIDescription: "Time of Redemption(ie 2025-05-12T00:01:13.345Z)",
                    platform: "StreamElements",
                    platform_UIDescription: "Platform the user Redeemed on",
                    triggerActionRef: "StreamElements",
                    triggerActionRef_UIDescription: "Reference for this message",
                }
            },
            {
                name: "StreamElementsMerch",
                name_UIDescription: "Stream Elements API for alerts etc",
                displaytitle: "Someone purchased something",
                displaytitle_UIDescription: "Someone purchased something",
                description: "Someone purchased something on the platform specified, items will contain the 'name price quantity' for that item. Multiple item purchase will have a trigger for each item even if brought in one purchase",
                description_UIDescription: "",
                messagetype: "trigger_StreamElementsMerch",
                messagetype_UIDescription: "",
                parameters: {
                    user: "",
                    user_UIDescription: "the users login name",
                    userDisplayName: "",
                    userDisplayName_UIDescription: "The display name of the user",
                    avatar: "",
                    avatar_UIDescription: "Users avatar",
                    item: "",
                    item_UIDescription: "Item purchased",
                    totalAmount: 0,
                    totalAmount_UIDescription: "Total Value of items purchased in sale (might be multiple items so multiple triggers for each item)",
                    providerId: 0,
                    providerId_UIDescription: "Id of the provider of the Merch",
                    activityId: 0,
                    activityId_UIDescription: "Id of the activity for this purchase",
                    timestamp: "",
                    timestamp_UIDescription: "Time of Merch(ie 2025-05-12T00:01:13.345Z)",
                    platform: "StreamElements",
                    platform_UIDescription: "Platform the message came in on",
                    triggerActionRef: "StreamElements",
                    triggerActionRef_UIDescription: "Reference for this message",
                }
            },
            {
                name: "charityDonation",
                name_UIDescription: "Stream Elements API for alerts etc",
                displaytitle: "Someone donated to your charity Campaign",
                displaytitle_UIDescription: "Charity Campaign Donation",
                description: "Charity Campaign Donation",
                description_UIDescription: "",
                messagetype: "trigger_StreamElementsCharityDonation",
                messagetype_UIDescription: "",
                parameters: {
                    user: "",
                    user_UIDescription: "the users login name",
                    userDisplayName: "",
                    userDisplayName_UIDescription: "The display name of the user",
                    message: "",
                    message_UIDescription: "Message if passed or empty",
                    avatar: "",
                    avatar_UIDescription: "Users avatar",
                    amount: 0,
                    amount_UIDescription: "Tip amount",
                    providerId: 0,
                    providerId_UIDescription: "Id of the provider of the charity campaign",
                    timestamp: "",
                    timestamp_UIDescription: "Time of Tip(ie 2025-05-12T00:01:13.345Z)",
                    platform: "StreamElements",
                    platform_UIDescription: "Platform the user Subscribed on",
                    triggerActionRef: "StreamElements",
                    triggerActionRef_UIDescription: "Reference for this message",
                }
            }
        ],
    actions:
        [],
}
// ============================================================================
//                           FUNCTION: initialise
// ============================================================================
/**
 * Starts the extension
 * @param {string} app 
 * @param {string} host 
 * @param {string} port 
 * @param {number} heartbeat 
 */
function initialise (app, host, port, heartbeat)
{
    try
    {
        localConfig.heartBeatTimeout = heartbeat;
        localConfig.DataCenterSocket = sr_api.setupConnection(onDataCenterMessage, onDataCenterConnect,
            onDataCenterDisconnect, host, port);
        startupCheck();
    } catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".initialise", "localConfig.DataCenterSocket connection failed:", err);
    }
}

// ============================================================================
//                           FUNCTION: onDataCenterDisconnect
// ============================================================================
/**
 * Disconnection message sent from the server
 * @param {String} reason 
 */
function onDataCenterDisconnect (reason)
{
    logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterDisconnect", reason);
}
// ============================================================================
//                           FUNCTION: onDataCenterConnect
// ============================================================================
/**
 * Connection message handler
 * @param {*} socket 
 */
function onDataCenterConnect (socket)
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ExtensionConnected", serverConfig.extensionname));
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestConfig", serverConfig.extensionname));
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("CreateChannel", serverConfig.extensionname, serverConfig.channel));
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestCredentials", serverConfig.extensionname));
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)

}
// ============================================================================
//                           FUNCTION: onDataCenterMessage
// ============================================================================
/**
 * receives message from the socket
 * @param {data} server_packet 
 */
function onDataCenterMessage (server_packet)
{
    if (server_packet.type === "StreamRollerReady")
        localConfig.readinessFlags.streamRollerReady = true;
    else if (server_packet.type === "ConfigFile")
    {
        if (server_packet.to == serverConfig.extensionname)
            localConfig.readinessFlags.ConfigReceived = true;
        if (server_packet.data != "" && server_packet.data.extensionname === serverConfig.extensionname)
        {
            if (server_packet.data && server_packet.data.extensionname)
            {
                let connectionChanged
                let configSubVersions = 0;
                let defaultSubVersions = default_serverConfig.__version__.split('.');
                if (!server_packet.data.__version__)
                {
                    serverConfig = structuredClone(default_serverConfig);
                    SaveConfigToServer();
                }
                configSubVersions = server_packet.data.__version__.split('.')

                if (configSubVersions[0] != defaultSubVersions[0])
                {
                    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");
                    SaveConfigToServer();
                    connectionChanged = true;
                }
                else if (configSubVersions[1] != defaultSubVersions[1])
                {
                    serverConfig = { ...default_serverConfig, ...server_packet.data };
                    serverConfig.__version__ = default_serverConfig.__version__;
                    console.log(serverConfig.extensionname + " ConfigFile Updated", "The config file has been Updated to the latest version v" + default_serverConfig.__version__);
                    SaveConfigToServer();
                    connectionChanged = true;
                }
                else
                {
                    if (serverConfig.streamElementsEnabled != server_packet.data.streamElementsEnabled)
                    {
                        if (server_packet.data.streamElementsEnabled == "on")
                        {
                            connectionChanged = true;
                            if (!localConfig.connected)
                                connectToSE()
                        }
                        else if (server_packet.data.streamElementsEnabled == "off")
                            disconnectFromSE();

                        serverConfig = structuredClone(server_packet.data);
                        SaveConfigToServer();
                    }

                    if (connectionChanged)
                        SendSettingsWidgetSmall();
                    SendSettingsWidgetLarge();
                }
            }
        }
    }
    else if (server_packet.type === "CredentialsFile")
    {
        if (server_packet.to === serverConfig.extensionname)
        {
            localConfig.readinessFlags.CredentialsReceived = true;
            // we have a saved credentials file
            if (server_packet.data != "")
            {
                serverCredentials = structuredClone(server_packet.data);
                SendSettingsWidgetSmall();
                SendSettingsWidgetLarge();
            }
            else
                serverCredentials = structuredClone(default_serverCredentials);
        }
    }
    else if (server_packet.type === "ExtensionMessage")
    {
        let extension_packet = server_packet.data;
        if (extension_packet.type === "RequestSettingsWidgetSmallCode")
            SendSettingsWidgetSmall(server_packet.from);
        else if (extension_packet.type === "RequestSettingsWidgetLargeCode")
            SendSettingsWidgetLarge(extension_packet.from);
        else if (extension_packet.type === "SettingsWidgetSmallData")
        {
            if (extension_packet.data.extensionname === serverConfig.extensionname)
            {
                if (extension_packet.data.streamelements_restore_defaults == "on")
                {
                    serverConfig = structuredClone(default_serverConfig);
                    console.log("\x1b[31m" + serverConfig.extensionname + " ConfigFile Updated.", "The config file has been Restored. Your settings may have changed" + "\x1b[0m");
                    disconnectFromSE();
                }
                else
                {
                    if (serverConfig.streamElementsEnabled == "on"
                        && !extension_packet.data.streamElementsEnabled)
                    {
                        serverConfig.streamElementsEnabled = "off";
                        disconnectFromSE();
                    }
                    else if (serverConfig.streamElementsEnabled == "off"
                        && extension_packet.data.streamElementsEnabled == "on")
                    {
                        serverConfig.streamElementsEnabled = "on"
                        connectToSE();
                    }

                    for (const [key, value] of Object.entries(extension_packet.data))
                        serverConfig[key] = value;
                }
                SaveConfigToServer();
                SendSettingsWidgetSmall(server_packet.from);
            }
        }
        else if (extension_packet.type === "SettingsWidgetLargeData")
        {
            if (extension_packet.to === serverConfig.extensionname)
                parseSettingsWidgetLarge(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 === "InvalidMessage")
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage",
            "InvalidMessage ", server_packet.data.error, server_packet);
    }
    else if (server_packet.type === "LoggingLevel")
    {
        logger.setLoggingLevel(server_packet.data)
    }
}
// ===========================================================================
//                           FUNCTION: SendSettingsWidgetLarge
// ===========================================================================
/**
 * @param {String} [to = ""] 
 * @param {String} [reference = ""] 
 */
function SendSettingsWidgetLarge (to = "", reference = "StreamElements")
{
    fs.readFile(__dirname + '/streamelementsettingswidgetlarge.html', function (err, filedata)
    {
        if (err)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
                ".SendSettingsWidgetLarge", "failed to load modal", err);
        else
        {
            let modalString = filedata.toString();
            for (const [key, value] of Object.entries(serverConfig))
            {
                // checkboxes
                if (value === "on")
                    modalString = modalString.replace(key + "checked", "checked");
                // replace text strings
                else if (typeof (value) == "string")
                    modalString = modalString.replace(key + "text", value);
            }
            modalString = modalString.replace("StreamerJWTTokentext", serverCredentials.StreamerJWTToken);
            //TBD
            modalString = modalString.replace("BotJWTTokentext", serverCredentials.BotJWTToken);

            // send the modified modal data to the server
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "SettingsWidgetLargeCode",
                        serverConfig.extensionname,
                        modalString,
                        serverConfig.channel,
                        to,
                    ),
                    serverConfig.channel,
                    to
                ))
        }
    });
}
// ============================================================================
//                           FUNCTION: parseSettingsWidgetLarge
// ============================================================================
/**
 * @param {object} data // data from user submitted form
 * @param {String} [reference = ""] 
 */
function parseSettingsWidgetLarge (data, reference = "StreamElements")
{
    try
    {
        if (data.streamelements_restore_defaults == "on")
        {
            serverConfig = structuredClone(default_serverConfig);
            DeleteCredentialsOnServer();
            console.log("\x1b[31m" + serverConfig.extensionname + " ConfigFile Updated.", "The config file has been Restored. Your settings may have changed" + "\x1b[0m");
        }
        else
        {
            let credentialsChanged = false;
            for (const [key, value] of Object.entries(data))
                if (serverConfig[key])
                    serverConfig[key] = value;
            // if we have changed the client ID lets set that
            if (serverCredentials.StreamerJWTToken != data.StreamerJWTToken)
            {
                serverCredentials.StreamerJWTToken = data.StreamerJWTToken;
                credentialsChanged = true;
            }
            if (credentialsChanged)
                SaveCredentialsToServer("parseSettingsWidgetLarge")
        }

        SaveConfigToServer();
        SendSettingsWidgetLarge("");

    } catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".parseSettingsWidgetLarge", "Error parsing data:", err, err.message);
        return null
    }
}
// ============================================================================
//                           FUNCTION: DeleteCredentialsOnServer
// ============================================================================
/**
 * @param {String} [reference = ""] 
 */
function DeleteCredentialsOnServer (reference = "StreamElements")
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "DeleteCredentials",
            serverConfig.extensionname,
            {
                ExtensionName: serverConfig.extensionname,
            },
        ));
}
// ============================================================================
//                           FUNCTION: SaveCredentialsToServer
// ============================================================================
/**
 * Sends Credential to the server to be saved
 * @param {String} [reference = ""] 
 */
function SaveCredentialsToServer (reference = "StreamElements")
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "UpdateCredentials",
            serverConfig.extensionname,
            {
                data: serverCredentials
            },
        ));
}
// ===========================================================================
//                           FUNCTION: SendSettingsWidgetSmall
// ===========================================================================
/**
 *
 * @param {String} to 
 */
function SendSettingsWidgetSmall (to = "")
{

    fs.readFile(__dirname + '/streamelementssettingswidgetsmall.html', function (err, filedata)
    {
        if (err)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
                ".SendSettingsWidgetSmall", "failed to load modal", err);
        else
        {
            let modalString = filedata.toString();
            for (const [key, value] of Object.entries(serverConfig))
            {
                if (value === "on")
                    modalString = modalString.replace(key + "checked", "checked");
                // replace text strings
                else if (typeof (value) == "string")
                    modalString = modalString.replace(key + "text", value);
            }

            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "SettingsWidgetSmallCode",
                        serverConfig.extensionname,
                        modalString,
                        serverConfig.channel,
                        to,
                    ),
                    serverConfig.channel,
                    to
                ))
        }
    });
}
// ============================================================================
//                           FUNCTION: SaveConfigToServer
// ============================================================================
/**
 * Sends our config to the server to be saved for next time we run
 */
function SaveConfigToServer ()
{
    sr_api.sendMessage(localConfig.DataCenterSocket, sr_api.ServerPacket
        ("SaveConfig",
            serverConfig.extensionname,
            serverConfig))
}
// ============================================================================
//                           FUNCTION: heartBeat
// ============================================================================
/**
 * Provides a heartbeat message to inform other extensions of our status
 */
function heartBeatCallback ()
{
    let color = "red";
    if (serverConfig.streamElementsEnabled === "on")
    {

        if (!localConfig.connected)
            color = "orange"
        else
            color = "green"
    }
    else
        color = "red"
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ChannelData",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "HeartBeat",
                serverConfig.extensionname,
                {
                    color: color
                },
                serverConfig.channel),
            serverConfig.channel
        ),
    );
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
}
// ============================================================================
//                           FUNCTION: disconnectFromSE
// ============================================================================
function disconnectFromSE ()
{
    localConfig.StreamElementsSocket.disconnect()
    localConfig.StreamElementsSocket.destroy()
    localConfig.connected = false;
}
// ============================================================================
//                           FUNCTION: connectToSE
// ============================================================================
function connectToSE ()
{
    if (serverConfig.streamElementsEnabled == "off")
    {
        localConfig.readinessFlags.SEStartup = true;
        return;
    }
    // Future dev note.
    // StreamElements provides both a websocket and socket connection.
    // documentation isn't clear on the differences in what they provide.
    // 1) astro pub/sub websocket (https://docs.streamelements.com/websockets)
    // 2) This one (https://dev.streamelements.com/docs/api-docs/ae133ffaf8c1a-personal-access-using-jwt-secert-token-to-access-the-api)

    // it seems 2 is for personal channel use and 1 is for multi channel use from what I can tell
    // for us it seems 2 is a simpler option for login (JWT token needed rather than Oauth making it easier
    // for the users to authenticate.)
    localConfig.StreamElementsSocket = socketIo(localConfig.SEAddress, {
        transports: ['websocket']
    });
    localConfig.StreamElementsSocket.on('error', (error) =>
    {
        localConfig.connected = false;
        console.log("socket error", error)
    });
    localConfig.StreamElementsSocket.on('connect', onSEConnect);
    localConfig.StreamElementsSocket.on('disconnect', onSEDisconnect);
    localConfig.StreamElementsSocket.on('authenticated', onSEAuthenticated);
    localConfig.StreamElementsSocket.on('unauthorized', onSEUnauthorized);
    localConfig.StreamElementsSocket.on('event:test', onSETestEvent);
    localConfig.StreamElementsSocket.on('event', onSEEvent)
    localConfig.StreamElementsSocket.on('event:update', onSEEventUpdate);
    localConfig.StreamElementsSocket.on('event:reset', onSEEventReset);
}
// ============================================================================
//                           FUNCTION: onSEConnect
// ============================================================================
function onSEConnect ()
{
    localConfig.StreamElementsSocket.emit('authenticate', { method: 'jwt', token: serverCredentials.StreamerJWTToken });
}
// ============================================================================
//                           FUNCTION: onSEDisconnect
// ============================================================================
function onSEDisconnect (reason)
{
    localConfig.connected = false;
    console.log('StreamElements Disconnected from websocket reason:', reason);
    // Reconnect
}
// ============================================================================
//                           FUNCTION: onSEUnauthorized
// ============================================================================
function onSEUnauthorized ()
{
    localConfig.connected = false;
    console.log('StreamElements Unauthorized');
    // Unauthorized
}
// ============================================================================
//                           FUNCTION: onSEAuthenticated
// ============================================================================
function onSEAuthenticated (data)
{
    localConfig.connected = true;
    const { channelId } = data;
    localConfig.channelId = channelId;
}
// ============================================================================
//                           FUNCTION: onSETestEvent
// ============================================================================
function onSETestEvent (data)
{
    debug_file_log("onSETestEvent:Unhandled Event", data);
    // Structure as on https://github.com/StreamElements/widgets/blob/master
}
// ============================================================================
//                           FUNCTION: onSEEvent
// ============================================================================
function onSEEvent (data)
{

    if (data.type == "follow")
    {
        let tr = findTriggerByMessageType("trigger_StreamElementsFollow")
        tr.parameters.user = data.data.username;
        tr.parameters.userDisplayName = data.data.displayName;
        tr.parameters.timestamp = data.createdAt;
        tr.parameters.platform = data.provider;
        postTrigger(tr);
    }
    else if (data.type == "cheer")
    {
        let tr = findTriggerByMessageType("trigger_StreamElementsCheer")
        tr.parameters.user = data.data.username;
        tr.parameters.userDisplayName = data.data.displayName;
        tr.parameters.avatar = data.data.avatar;
        tr.parameters.amount = data.data.amount;
        tr.parameters.message = data.data.message;
        tr.parameters.timestamp = data.createdAt;
        tr.parameters.platform = data.provider;
        postTrigger(tr);
    }
    else if (data.type == "raid")
    {
        let tr = findTriggerByMessageType("trigger_StreamElementsRaid")
        tr.parameters.user = data.data.username;
        tr.parameters.userDisplayName = data.data.displayName;
        tr.parameters.avatar = data.data.avatar;
        tr.parameters.amount = data.data.amount;
        tr.parameters.timestamp = data.createdAt;
        tr.parameters.platform = data.provider;
        if (data.data.message)
            tr.parameters.message = data.data.message;
        postTrigger(tr);
    }
    else if (data.type == "subscriber")
    {
        let tr = findTriggerByMessageType("trigger_StreamElementsSubscription")
        tr.parameters.user = data.data.username;
        tr.parameters.userDisplayName = data.data.displayName;
        tr.parameters.avatar = data.data.avatar;
        tr.parameters.amount = data.data.amount;
        tr.parameters.timestamp = data.createdAt;
        tr.parameters.platform = data.provider;
        if (data.data.message)
            tr.parameters.message = data.data.message;
        if (data.data.gifted)
        {
            tr.parameters.gifted = data.data.gifted;
            tr.parameters.gifter = data.data.sender;
        }
        postTrigger(tr);
    }
    else if (data.type == "communityGiftPurchase")
    {
        let tr = findTriggerByMessageType("trigger_StreamElementsCommunityGiftPurchase")
        tr.parameters.user = data.data.username;
        tr.parameters.userDisplayName = data.data.displayName;
        tr.parameters.avatar = data.data.avatar;
        tr.parameters.amount = data.data.amount;
        tr.parameters.timestamp = data.createdAt;
        tr.parameters.platform = data.provider;
        postTrigger(tr);
    }
    else if (data.type == "tip")
    {
        let tr = findTriggerByMessageType("trigger_StreamElementsTip")
        tr.parameters.user = data.data.username;
        tr.parameters.userDisplayName = data.data.displayName;
        tr.parameters.avatar = data.data.avatar;
        tr.parameters.amount = data.data.amount;
        tr.parameters.timestamp = data.createdAt;
        tr.parameters.platform = data.provider;
        if (data.data.message)
            tr.parameters.message = data.data.message;
        postTrigger(tr);
    }
    else if (data.type == "redemption")
    {
        let tr = findTriggerByMessageType("trigger_StreamElementsRedemption")
        tr.parameters.user = data.data.username;
        tr.parameters.userDisplayName = data.data.displayName;
        tr.parameters.avatar = data.data.avatar;
        tr.parameters.amount = data.data.amount;
        tr.parameters.timestamp = data.createdAt;
        tr.parameters.platform = data.provider;
        tr.parameters.providerId = data.data.providerId;
        postTrigger(tr);
    }
    else if (data.type == "channelPointsRedemption")
    {
        let tr = findTriggerByMessageType("trigger_StreamElementsChannelPointsRedemption")
        tr.parameters.user = data.data.username;
        tr.parameters.userDisplayName = data.data.displayName;
        tr.parameters.avatar = data.data.avatar;
        tr.parameters.amount = data.data.amount;
        tr.parameters.timestamp = data.createdAt;
        tr.parameters.platform = data.provider;
        tr.parameters.redemption = data.data.redemption;
        tr.parameters.quantity = data.data.quantity;
        postTrigger(tr);
    }
    else if (data.type == "merch")
    {
        let tr = findTriggerByMessageType("trigger_StreamElementsMerch")
        tr.parameters.user = data.data.username;
        tr.parameters.userDisplayName = data.data.displayName;
        tr.parameters.avatar = data.data.avatar;
        tr.parameters.totalAmount = data.data.amount
        tr.parameters.providerId = data.data.providerId;
        tr.parameters.activityId = data.activityId;
        tr.parameters.timestamp = data.createdAt;
        tr.parameters.platform = data.provider;
        let start = true;
        data.data.items.forEach(ele =>
        {
            for (var key in ele)
            {
                if (start)
                {
                    start = false;
                    tr.parameters.item = ""
                }
                else
                    tr.parameters.item += " "
                tr.parameters.item += `${ele[key]}`
            }
            postTrigger(tr);
            start = true;
        });
    }
    else if (data.type == "charityCampaignDonation")
    {
        let tr = findTriggerByMessageType("trigger_StreamElementsCharityDonation")
        tr.parameters.user = data.data.username;
        tr.parameters.userDisplayName = data.data.displayName;
        tr.parameters.avatar = data.data.avatar;
        tr.parameters.amount = data.data.amount;
        tr.parameters.providerId = data.data.providerId;
        tr.parameters.timestamp = data.createdAt;
        tr.parameters.platform = data.provider;
        if (data.data.message)
            tr.parameters.message = data.data.message;
        postTrigger(tr);
    }
    else
        debug_file_log("onSEEvent:Unhandled Event", data);
    // Structure as on https://github.com/StreamElements/widgets/blob/master
}
// ============================================================================
//                           FUNCTION: onSEEventUpdate
// ============================================================================
function onSEEventUpdate (data)
{
    debug_file_log("onSEEventUpdate:Unhandled Event", data);
    // Structure as on https://github.com/StreamElements/widgets/blob/master
}
// ============================================================================
//                           FUNCTION: onSEEventReset
// ============================================================================
function onSEEventReset (data)
{
    debug_file_log("onSEEventReset:Unhandled Event", data);
    // Structure as on https://github.com/StreamElements/widgets/blob/master
}
// ============================================================================
//                           FUNCTION: startupCheck
// ============================================================================
/**
 * waits for config and credentials files to set ready flag
 */

function startupCheck ()
{
    // if we have received out config and credentials start the server if needed
    if (localConfig.readinessFlags.ConfigReceived
        && localConfig.readinessFlags.CredentialsReceived
        && !localConfig.SEStartupCalled)
    {
        localConfig.SEStartupCalled = true;
        connectToSE();
    }
    // are we ready to start receiving messages from other extensions
    const allReady = Object.values(localConfig.readinessFlags).every(flag => flag);
    if (allReady)
    {
        localConfig.ready = true;
        try
        {
            postStartupActions();
        } catch (err)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".startupCheck", err);
        }
    }
    else
        setTimeout(startupCheck, localConfig.startupCheckTimer);
}
// ============================================================================
//                           FUNCTION: startupCheck
// ============================================================================
/**
 * At this point we should have any config/credentials loaded
 */
function postStartupActions ()
{
    // Let the server know we are now up and running.
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ExtensionReady", serverConfig.extensionname));

}
// ============================================================================
//                           FUNCTION: findTriggerByMessageType
// ============================================================================
/**
 * Finds a trigger from the messagetype
 * @param {string} messagetype 
 * @param {String} [reference = ""] 
 * @returns {object} trigger
 */
function findTriggerByMessageType (messagetype, reference = "Kick")
{
    for (let i = 0; i < triggersandactions.triggers.length; i++)
    {
        if (triggersandactions.triggers[i].messagetype.toLowerCase() == messagetype.toLowerCase())
            return structuredClone(triggersandactions.triggers[i]);
    }
    logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
        ".findTriggerByMessageType", "failed to find action", messagetype);
}
// ============================================================================
//                     FUNCTION: postTrigger
// ============================================================================
/**
 * Posts a trigger out on our channel
 * @param {object} data 
 */
function postTrigger (data)
{
    let message = sr_api.ServerPacket(
        "ChannelData",
        serverConfig.extensionname,
        sr_api.ExtensionPacket(
            data.messagetype,
            serverConfig.extensionname,
            data,
            serverConfig.channel
        ),
        serverConfig.channel
    )
    sr_api.sendMessage(localConfig.DataCenterSocket,
        message);
}
// ============================================================================
//                                  EXPORTS
// Note that initialise is mandatory to allow the server to start this extension
// ============================================================================
export { initialise, triggersandactions };