Source: extensions/youtube/youtube.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/>.
 */
/**
 * @extension YouTube
 * YouTube functionality without the google API limits (limited free Tokens) or need for complicated OAUTH for the users requiring a google cloud account with a valid application setup to get the ClientId and ClientSecret needed.
 */
import { Innertube, UniversalCache, YTNodes } from 'youtubei.js';
import { Log } from 'youtubei.js';
import * as logger from "../../backend/data_center/modules/logger.js";
import sr_api from "../../backend/data_center/public/streamroller-message-api.cjs";
import * as fs from "fs";
import { dirname } from 'path';
import { fileURLToPath } from 'url';

const DEBUG_LOGGING = false
// set logging level for youtube.js : NONE, ERROR, WARNING, INFO, DEBUG
Log.setLevel(Log.Level.NONE);

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

const localConfig = {
    SYSTEM_LOGGING_TAG: "[EXTENSION]",
    DataCenterSocket: null,
    heartBeatTimeout: 5000,
    heartBeatHandle: null,
    liveChatMessagePageToken: null,
    youtubeAPI: null,
    liveChatAPI: null,
    youtubevideoid: "",
    ytInfo: null,
    info: { views: -1, likes: -1, date: -1 },
    connectedToLiveStream: false,
    credentialsSet: false,
    signedIn: false,
    youtubeBrowserCookieStatus: "Cookie not set",
    startupTimerBufferHandle: null, //have a startup buffer to stop multiple stars (workaround for settings credentials)
    connectedAsUsername: null
};

const default_serverConfig = {
    __version__: "0.2",
    extensionname: "youtube",
    channel: "YOUTUBE",
    youtubeenabled: "off",
    youtubedebugenabled: "off",
    youtube_restore_defaults: "off",
    enableYouTubeBrowserCookie: "off",
    youtubechatpollrate: 5000,
    youtubechannelname: "OldDepressedGamer",
    host: "http://localhost",
    port: "3000"
};
let serverConfig = structuredClone(default_serverConfig)
let serverCredentials = {};
const triggersandactions =
{
    extensionname: serverConfig.extensionname,
    description: "YouTube extension",
    version: "0.1",
    channel: serverConfig.channel,
    triggers:
        [
            {
                name: "Youtube message received",
                displaytitle: "YouTube Chat Message",
                description: "A chat message was received. textMessage field has name and message combined",
                messagetype: "trigger_ChatMessageReceived",
                parameters: {

                    triggerId: "YouTubeChatMessage", //Identifier that users can use to identify this particular trigger message if triggered by an action
                    // streamroller settings
                    type: "trigger_ChatMessageReceived",
                    platform: "Youtube",
                    textMessage: "[username]: [message]",
                    safemessage: "",
                    color: "#FF0000",

                    // youtube message data
                    id: "",
                    message: "",
                    ytmessagetype: "",
                    timestamp: -1,

                    //youtube author data
                    sender: "",
                    senderid: "",

                    senderprofileimageurl: "",
                    senderbadges: "",
                    senderisverified: false,
                    senderischatmoderator: false,
                }
            },
        ],
    actions:
        [
            {
                name: "YouTube post livechat message",
                displaytitle: "Post a Message to youtube live chat",
                description: "Post to youtube live chat if we are connected.",
                messagetype: "action_youtubePostLiveChatMessage",
                parameters: {
                    actionId: "YouTubeChatMessage", //Identifier that users can use to identify any trigger fired by this action
                    message: ""
                }
            }

        ],
}

// ============================================================================
//                           FUNCTION: initialise
// ============================================================================
/**
 * Starts the extension using the given data.
 * @param {Express} app 
 * @param {String} host 
 * @param {Number} port 
 * @param {Number} heartbeat 
 */
function initialise (app, host, port, heartbeat)
{
    serverConfig.host = host
    serverConfig.port = port
    localConfig.heartBeatTimeout = heartbeat;
    try
    {
        localConfig.DataCenterSocket = sr_api.setupConnection(onDataCenterMessage, onDataCenterConnect,
            onDataCenterDisconnect, host, port);
    } catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".initialise", "localConfig.DataCenterSocket connection failed:", err);
    }
}
// ============================================================================
//                           FUNCTION: onDataCenterDisconnect
// ============================================================================
/**
 * Called when connection to StreamRoller server disconnects
 * @param {string} reason 
 */
function onDataCenterDisconnect (reason)
{
    logger.warn(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterDisconnect", reason);
}
// ============================================================================
//                           FUNCTION: onDataCenterConnect
// ============================================================================
/**
 * Called when connection to StreamRoller server starts
 * @param {object} socket 
 */
function onDataCenterConnect (socket)
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestConfig", serverConfig.extensionname));
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestCredentials", serverConfig.extensionname));

    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("CreateChannel", serverConfig.extensionname, serverConfig.channel)
    );
    clearTimeout(localConfig.heartBeatHandle);
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
}
// ============================================================================
//                           FUNCTION: onDataCenterMessage
// ============================================================================
/**
 * Called when a message is received from StreamRoller
 * @param {object} server_packet 
 */
function onDataCenterMessage (server_packet)
{
    if (server_packet.type === "ConfigFile")
    {
        if (server_packet.data != "" && server_packet.to === serverConfig.extensionname)
        {
            let previous_enabled = serverConfig.youtubeenabled;
            if (server_packet.data.__version__ != default_serverConfig.__version__)
            {
                serverConfig = structuredClone(default_serverConfig);
                SaveConfigToServer();
                console.log("\x1b[31m" + serverConfig.extensionname + " ConfigFile Updated", "The config file has been Restored. Your settings may have changed" + "\x1b[0m");
            }
            else
                serverConfig = structuredClone(server_packet.data);
            // check if we were previously enabled in setting (on startup this will be off)
            if (previous_enabled != serverConfig.youtubeenabled)
                if (serverConfig.youtubeenabled == "on")
                {
                    //only start if we have already got our credentials
                    if (localConfig.credentialsSet)
                    {
                        stopYoutubeMonitor();
                        if (localConfig.startupTimerBufferHandle)
                            clearTimeout(localConfig.startupTimerBufferHandle)
                        localConfig.startupTimerBufferHandle = setTimeout(
                            startYoutubeMonitor()
                            , 2000
                        )

                    }
                }
                else
                    stopYoutubeMonitor()
        }
    }
    else if (server_packet.type === "CredentialsFile")
    {
        if (server_packet.data != "" && server_packet.to === serverConfig.extensionname)    
        {
            serverCredentials = server_packet.data
            if (serverCredentials.youtubeCookie && serverCredentials.youtubeCookie != "")
                localConfig.youtubeBrowserCookieStatus = "Cookie Loaded"
            localConfig.credentialsSet = true
            SendSettingsWidgetSmall()
            if (localConfig.startupTimerBufferHandle)
                clearTimeout(localConfig.startupTimerBufferHandle)
            localConfig.startupTimerBufferHandle = setTimeout(() =>
            {
                stopYoutubeMonitor();
                startYoutubeMonitor();
            }, 2000);
        }
    }
    else if (server_packet.type === "ExtensionMessage")
    {
        let extension_packet = server_packet.data;
        if (extension_packet.type === "RequestSettingsWidgetSmallCode")
            SendSettingsWidgetSmall(extension_packet.from);
        else if (extension_packet.type === "SettingsWidgetSmallData")
        {
            if (extension_packet.data.extensionname === serverConfig.extensionname)
            {
                let StatusChanged = false
                // check for restore defaults
                if (extension_packet.data.youtube_restore_defaults == "on")
                {
                    serverConfig = structuredClone(default_serverConfig);
                    serverCredentials = {};
                    deleteCredentials()
                    console.log("\x1b[31m" + serverConfig.extensionname + " Config/Credentials Files Updated.", "The config files have been Restored to defaults and credentials deleted. Your settings may have changed" + "\x1b[0m");
                }
                else
                {
                    // have we just changed something that needs a restart
                    if (extension_packet.data.youtubeenabled != serverConfig.youtubeenabled)
                        StatusChanged = true
                    if (extension_packet.data.youtubechatpollrate != serverConfig.youtubechatpollrate)
                        StatusChanged = true
                    if (extension_packet.data.youtubechannelname != serverConfig.youtubechannelname)
                        StatusChanged = true
                    if (extension_packet.data.enableYouTubeBrowserCookie == "on")
                        StatusChanged = true;
                    if (extension_packet.data.youtubeCookie != "")
                    {
                        serverCredentials.youtubeCookie = extension_packet.data.youtubeCookie
                        saveCredentialsToServer()
                    }
                    // default any checkboxes we may have in our settings
                    serverConfig.youtubeenabled = "off";
                    serverConfig.youtubedebugenabled = "off"
                    serverConfig.enableYouTubeBrowserCookie = "off"

                    // update our config
                    for (const [key, value] of Object.entries(extension_packet.data))
                    {
                        serverConfig[key] = value;
                    }
                }
                // check if we need to start/stop or restart the service
                if (StatusChanged)
                {
                    if (serverConfig.youtubeenabled == "on")
                    {
                        if (localConfig.startupTimerBufferHandle)
                            clearTimeout(localConfig.startupTimerBufferHandle)
                        localConfig.startupTimerBufferHandle = setTimeout(() =>
                        {
                            stopYoutubeMonitor();
                            startYoutubeMonitor();
                        }
                            , 2000
                        )
                    }
                    else
                        stopYoutubeMonitor();
                }
                SaveConfigToServer();
                SendSettingsWidgetSmall(extension_packet.from);
            }
        }
        else if (extension_packet.type === "action_youtubePostLiveChatMessage")
            postLiveChatMessages(extension_packet.data)
        else
            logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "received unhandled ExtensionMessage ", server_packet);

    }
    else if (server_packet.type === "UnknownChannel")
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "Channel " + server_packet.data + " doesn't exist, scheduling rejoin");
        setTimeout(() =>
        {
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "JoinChannel", serverConfig.extensionname, server_packet.data
                ));
        }, 5000);
        /*
    }*/

    }
    else if (server_packet.type === "ChannelData")
    {
        let extension_packet = server_packet.data;
        if (extension_packet.type === "HeartBeat")
        {
            //Just ignore messages we know we don't want to handle
        }
        else
        {
            logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "received message from unhandled channel ", server_packet.dest_channel);
        }
    }
    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)
    }
    else if (server_packet.type === "ChannelJoined"
        || server_packet.type === "ChannelCreated"
        || server_packet.type === "ChannelLeft"
    )
    {
        // just a blank handler for items we are not using to avoid message from the catchall
    }
    else
        logger.warn(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
            ".onDataCenterMessage", "Unhandled message type", server_packet.type);
}

// ============================================================================
//                           FUNCTION: deleteCredentials
// ============================================================================
/**
 * delete the credentials file on the server
 */
function deleteCredentials ()
{
    sr_api.sendMessage(localConfig.DataCenterSocket, sr_api.ServerPacket
        ("DeleteCredentials",
            serverConfig.extensionname,
            serverConfig))
}
// ============================================================================
//                           FUNCTION: SaveConfigToServer
// ============================================================================
/**
 * Saves the configuration to the server
 */
function SaveConfigToServer ()
{
    sr_api.sendMessage(localConfig.DataCenterSocket, sr_api.ServerPacket
        ("SaveConfig",
            serverConfig.extensionname,
            serverConfig))

}
// ============================================================================
//                           FUNCTION: saveCredentialsToServer
// ============================================================================
/**
 * 
 */
function saveCredentialsToServer ()
{
    for (var c in serverCredentials)
    {
        sr_api.sendMessage(localConfig.DataCenterSocket,
            sr_api.ServerPacket(
                "UpdateCredentials",
                serverConfig.extensionname,
                {
                    ExtensionName: serverConfig.extensionname,
                    CredentialName: c,
                    CredentialValue: serverCredentials[c]
                },
            ));
    }
}
// ============================================================================
//                     FUNCTION: startYoutubeMonitor
// ============================================================================
/**
 * Start the Monitor
 */
function startYoutubeMonitor ()
{
    if (serverConfig.youtubeenabled == "on")
    {
        // if we don't have an api handle create one
        if (!localConfig.youtubeAPI && localConfig.credentialsSet)
            connectToAPI();

        // if we haven't received the api yet then reschedule the start until we have one back
        if (!localConfig.youtubeAPI || !localConfig.credentialsSet)
        {
            if (localConfig.startupTimerBufferHandle)
                clearTimeout(localConfig.startupTimerBufferHandle)
            localConfig.startupTimerBufferHandle = setTimeout(() =>
            {
                startYoutubeMonitor();
            }, 5000);
        }
        else
        {
            if (localConfig.liveChatAPI)
                localConfig.liveChatAPI.start();
            else
            {
                console.log("Couldn't start monitoring, is there a live video available?")
                MonitorForLiveStream()
            }
        }
        SendSettingsWidgetSmall();
    }
}
// ============================================================================
//                     FUNCTION: MonitorForLiveStream
// ============================================================================
/**
 * Monitor for a stream going live on our channel
 */
function MonitorForLiveStream ()
{
    const monitorTimeout = 10000;
    let filters = { features: ["live"] }
    localConfig.youtubeAPI.search(serverConfig.youtubechannelname, filters)
        .then((search) =>
        {
            if (search.videos.length < 1)
            {
                console.log("no live videos found yet, monitoring")
                if (localConfig.youtubeenabled == "on")
                {
                    setTimeout(() =>
                    {
                        MonitorForLiveStream()
                    }, monitorTimeout);
                }
            }
            else
                connectToAPI();
            // check for found live and call the start function (to save duplicating code)
        })
        .catch((err) =>
        {
            console.log("Error searching for live stream", err, err.message)
            console.error(err)
            if (localConfig.youtubeenabled == "on") 
            {
                setTimeout(() =>
                {
                    MonitorForLiveStream()
                }, monitorTimeout);
            }
        })
}
// ============================================================================
//                     FUNCTION: stopYoutubeMonitor
// ============================================================================
/**
 * Stop monitoring YouTube chat
 */
function stopYoutubeMonitor ()
{
    try
    {
        localConfig.connectedToLiveStream = false;

        if (localConfig.liveChatAPI)
            localConfig.liveChatAPI.stop();
        localConfig.youtubeAPI = null;
        SendSettingsWidgetSmall();
    }
    catch (error)
    {
        console.log("stopYoutubeMonitor Error", error.status, error.message)
        console.log(error)
    }
}

// ============================================================================
//                     FUNCTION: startYoutubeMonitor
// ============================================================================
/**
 * Connects and starts monitoring YouTube chat
 */
function connectToAPI ()
{
    let cookie = ""
    if (serverCredentials && serverCredentials.youtubeCookie && serverCredentials.youtubeCookie != "")
        cookie = serverCredentials.youtubeCookie
    Innertube.create({ cache: new UniversalCache(false), cookie: cookie })
        .then(async (handle) =>
        {
            if (handle)
            {
                localConfig.youtubeAPI = handle;
                // search for videos on channel. first will be the live video if it exists

                let filters = { features: ["live"] }// note live doesn't include funderaiser live videos, need to add all types of live options here.
                //filters = {};
                const search = await localConfig.youtubeAPI.search(serverConfig.youtubechannelname, filters);
                file_log(JSON.stringify(search.videos, null, 2))
                if (search.videos.length < 1)
                {
                    console.log("No live video found for channel name", serverConfig.youtubechannelname)
                }
                if (search.videos[0] && search.videos[0] != [])
                {
                    localConfig.youtubevideoid = search.videos[0].id;
                    localConfig.youtubeAPI.getInfo(localConfig.youtubevideoid)
                        .then((info) =>
                        {
                            file_log("let " + serverConfig.youtubechannelname + "=" + JSON.stringify(info, null, 2))
                            if (info.basic_info.is_live)
                            {
                                localConfig.ytInfo = info;
                                localConfig.liveChatAPI = localConfig.ytInfo.getLiveChat()
                                localConfig.liveChatAPI.on('start', (initial_data) =>
                                {
                                    // filter on live chat rather than TOP_CHAT
                                    localConfig.liveChatAPI.applyFilter("LIVE_CHAT");
                                    chatStart(initial_data)
                                });

                                localConfig.liveChatAPI.on('error', (error) =>
                                {
                                    chatError(error)

                                });

                                localConfig.liveChatAPI.on('end', () => { chatEnd() });
                                localConfig.liveChatAPI.on('chat-update', (action) => chatMessage(action));
                                localConfig.liveChatAPI.on('metadata-update', (metadata) => metaUpdate(metadata));
                            }
                            else
                                logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
                                    ".youtubeAPI.getInfo", "Stream is not live");
                        })
                        .catch((err) =>
                        {
                            console.log("Error:", JSON.stringify(err))
                            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
                                ".startYoutubeMonitor.getLiveChat", "failed to load getLiveChat", err);
                        })
                }
                else
                    console.log("No live video found")

            }
            else
                console.log("YoutubeAPI returned null handle");

        })
        .catch((err) =>
        {
            console.error(err)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
                ".startYoutubeMonitor", "failed to load YoutubeAPI", err);
        })
}

// ============================================================================
//                     FUNCTION: chatStart
// ============================================================================
/**
 * Called when YouTube connection starts
 * @param {object} initial_data 
 */
function chatStart (initial_data)
{
    localConfig.connectedToLiveStream = true;
    if (localConfig.youtubeAPI && localConfig.youtubeAPI.session)
        localConfig.signedIn = localConfig.youtubeAPI.session.logged_in;
    localConfig.connectedAsUsername = initial_data.viewer_name
    SendSettingsWidgetSmall()
}
/**
 * Called on YouTube error
 * @param {object} error 
 */
// ============================================================================
//                     FUNCTION: chatError
// ============================================================================
/**
 * Called when a chatError occurs
 * @param {object} error 
 */
function chatError (error)
{
    localConfig.connectedToLiveStream = false;
    console.log('Live chat error:');
    if (localConfig.youtubeAPI && localConfig.youtubeAPI.session)
        localConfig.signedIn = localConfig.youtubeAPI.session.logged_in;
    stopYoutubeMonitor()
    if (error && error.info)
        console.log("error.code", error.info)
}
// ============================================================================
//                     FUNCTION: metaUpdate
// ============================================================================
/**
 * Called when the metadata is updated for the stream
 * @param {object} metadata 
 */
function metaUpdate (metadata)
{
    localConfig.connectedToLiveStream = true;
    if (localConfig.youtubeAPI && localConfig.youtubeAPI.session)
        localConfig.signedIn = localConfig.youtubeAPI.session.logged_in;
    localConfig.info =
    {
        views: metadata.views?.view_count.toString(),
        likes: metadata.likes?.default_text,
        date: metadata.date?.date_text
    }
}
// ============================================================================
//                     FUNCTION: chatEnd
// ============================================================================
/**
 * Called on chat end/stream end
 */
function chatEnd ()
{
    localConfig.connectedToLiveStream = false;
    if (localConfig.youtubeAPI && localConfig.youtubeAPI.session)
        localConfig.signedIn = localConfig.youtubeAPI.session.logged_in;
    localConfig.liveChatAPI = null;
}
// ============================================================================
//                     FUNCTION: chatMessage
// ============================================================================
/**
 * Called when a chat message is received
 * @param {object} action 
 */
function chatMessage (action)
{
    localConfig.connectedToLiveStream = true;
    if (localConfig.youtubeAPI && localConfig.youtubeAPI.session)
        localConfig.signedIn = localConfig.youtubeAPI.session.logged_in;
    if (action.is(YTNodes.AddChatItemAction))
    {
        const item = action.as(YTNodes.AddChatItemAction).item;

        if (!item)
        {
            console.log('Chat Action did not have an item.', action);
            return;
        }
        const hours = new Date(item.hasKey('timestamp') ? item.timestamp : Date.now()).toLocaleTimeString('en-US', {
            hour: '2-digit',
            minute: '2-digit'
        });
        switch (item.type)
        {
            case 'LiveChatTextMessage':
                sendChatMessageTrigger(item, hours);
                break;
            case 'LiveChatPaidMessage':
                sendLiveChatPaidMessage(item.as(YTNodes.LiveChatPaidMessage), hours)
                break;
            case 'LiveChatPaidSticker':
                sendLiveChatPaidSticker(item.as(YTNodes.LiveChatPaidSticker), hours)
                break;
            default:
                console.debug(action);
                break;
        }
    }

    if (action.is(YTNodes.AddBannerToLiveChatCommand))
    {
        console.info('Message pinned:', action.banner?.contents, ". Needs implementation");
    }

    if (action.is(YTNodes.RemoveBannerForLiveChatCommand))
    {
        console.info(`Message with action id ${action.target_action_id} was unpinned.`, ". Needs implementation");
    }

    if (action.is(YTNodes.RemoveChatItemAction))
    {
        console.warn(`Message with action id ${action.target_item_id} just got deleted!`, ". Needs implementation");
    }
}
// ===========================================================================
//                           FUNCTION: SendSettingsWidgetSmall
// ===========================================================================
/**
 * Processes and send out our small settings widget
 * @param {string?} toExtension optional extension name to send to other wise broadcast the message
 */
function SendSettingsWidgetSmall (toExtension = "")
{
    // read our modal file
    fs.readFile(__dirname + '/youtubesettingswidgetsmall.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");
                else if (typeof (value) == "string" || typeof (value) == "number")
                    modalString = modalString.replace(key + "text", value);
            }
            modalString = modalString.replace("youtubeBrowserCookieStatus", "Status: " + localConfig.youtubeBrowserCookieStatus);
            if (localConfig.connectedAsUsername || localConfig.connectedAsUsername != "")
                modalString = modalString.replace("YouTubeConnectedAs", `Connected as '${localConfig.connectedAsUsername || 'Guest'}'`);
            else
                modalString = modalString.replace("YouTubeConnectedAs", "Connected as 'Guest'}");

            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "SettingsWidgetSmallCode",
                        serverConfig.extensionname,
                        modalString,
                        "",
                        toExtension,
                        serverConfig.channel,
                    ),
                    "",
                    toExtension
                ))
        }
    });
}
// ===========================================================================
//                           FUNCTION: sendLiveChatPaidMessage
// ===========================================================================
/**
 * Processes a Paid chat messages message
 * @param {object} ytmessage 
 * @param {string} time 
 */
function sendLiveChatPaidMessage (ytmessage, time)
{
    console.log("TBD live chat paid message received. Needs implementation");
    console.info(
        `${ytmessage.author?.is_moderator ? '[MOD]' : ''}`,
        `${time} - ${ytmessage.author.name.toString()}:\n` +
        `${ytmessage.message.toString()}\n`,
        `${ytmessage.purchase_amount}\n`
    );

}
// ===========================================================================
//                           FUNCTION: sendLiveChatPaidSticker
// ===========================================================================
/**
 * Processes a Paid chat sticker message
 * @param {object} ytmessage 
 * @param {string} time 
 */
function sendLiveChatPaidSticker (ytmessage, time)
{
    console.log("TBD live chat paid sticker received. Needs implementation");
    console.info(
        `${ytmessage.author?.is_moderator ? '[MOD]' : ''}`,
        `${time} - ${ytmessage.author.name.toString()}:\n` +
        `${ytmessage.purchase_amount}\n`
    );

}
// ===========================================================================
//                           FUNCTION: sendChatMessageTrigger
// ===========================================================================
/**
 * Processes a chat message and sends out the trigger "trigger_ChatMessageReceived"
 * @param {object} ytmessage 
 * @param {string} time 
 */
function sendChatMessageTrigger (ytmessage, time)
{
    try
    {
        //file_log(JSON.stringify(ytmessage, null, 2))
        let triggerToSend = findTriggerByMessageType("trigger_ChatMessageReceived")
        let safemessage = sanitiseHTML(ytmessage.message);
        safemessage = ytmessage.message.text.replace(/[^\x00-\x7F]/g, "");
        triggerToSend.parameters = {

            textMessage: ytmessage.author.name + ": " + ytmessage.message.text,
            safemessage: safemessage,
            color: "#FF0000",// possibly use color from menu for mods etc

            // youtube message data
            id: ytmessage.id,
            ytmessagetype: ytmessage.type,
            message: ytmessage.message.text,
            //messageruns: ytmessage.message.runs,
            timestamp: ytmessage.timestamp,

            //youtube author data
            sender: ytmessage.author.name,
            senderid: ytmessage.author.id,

            senderprofileimageurl: ytmessage.author.thumbnails[0].url,
            senderbadges: ytmessage.author.badges,
            senderisverified: ytmessage.author.is_verified,
            senderischatmoderator: ytmessage.author.is_moderator,
        }
        postTrigger(triggerToSend);

    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
            ".SendSettingsWidgetSmall", "failed to load modal", err);
    }
}
// ============================================================================
//                           FUNCTION: postLiveChatMessages
// ============================================================================
/**
 * TBD: Send a message to youtube chat TBD
 * @param {object} data 
 */
function postLiveChatMessages (data)
{
    if (localConfig.liveChatAPI)
    {
        localConfig.liveChatAPI.sendMessage(data.message)
            .then((commentResponse) =>
            {
                // no need to do anything if it posts ok.
                //console.log("commentResponse", JSON.stringify(commentResponse, null, 2))
            })
            .catch((err) =>
            {
                console.log("comment error", JSON.stringify(err, null, 2))
            })
    }
}
// ============================================================================
//                           FUNCTION: findTriggerByMessageType
// ============================================================================
/**
 * Finds a trigger from the messagetype
 * @param {string} messagetype 
 * @returns {object} trigger
 */
function findTriggerByMessageType (messagetype)
{
    for (let i = 0; i < triggersandactions.triggers.length; i++)
    {
        if (triggersandactions.triggers[i].messagetype.toLowerCase() == messagetype.toLowerCase())
            return 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);
}
// #######################################################################
// ######################### sanitiseHTML ##########################
// #######################################################################
/**
 * Removes html chars from a string to avoid chat message html injection
 * @param {string} string 
 * @returns {string} the parsed string
 */
function sanitiseHTML (string)
{
    // sanitiser
    var entityMap = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;',
        '/': '&#x2F;',
        '`': '&#x60;',
        '=': '&#x3D;'
    };

    return String(string).replace(/[&<>"'`=\/]/g, function (s)
    {
        return entityMap[s];
    });
}
// ============================================================================
//                           FUNCTION: heartBeat
// ============================================================================
/**
 * Provides a heartbeat message to inform other extensions of our status
 */
function heartBeatCallback ()
{
    let connected = false;
    let color = "red";

    if (serverConfig.youtubeenabled === "on")
    {
        connected = true;

        if (localConfig.connectedToLiveStream && localConfig.signedIn)
            color = "green"
        else
            color = "orange"
    }
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ChannelData",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "HeartBeat",
                serverConfig.extensionname,
                {
                    connected: connected,
                    color: color
                },
                serverConfig.channel),
            serverConfig.channel
        ),
    );
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
}
// ============================================================================
//                           FUNCTION: file_log
//             For debug purposes. logs performance data
// ============================================================================
function file_log (data, override_debug)
{
    if (!DEBUG_LOGGING && !override_debug)
        return;
    try
    {
        //console.log("logging data");
        //var filename = __dirname + "\\debug_logging.txt";
        var filename = __dirname + "/debug_logging.json";
        var buffer = "";
        // first line write the headings (csv)
        //if (!fs.existsSync(filename))
        console.log("writing file:", filename)

        fs.writeFileSync(filename, "\\############################################\n"
            + data + "\n\\---------------------------------------------\n", {
            encoding: "utf8",
            flag: "a+",
        });
    }
    catch (error)
    {
        console.log("debug file logging error", error.message)
    }
}
export { initialise, triggersandactions };