Source: extensions/timers/timers.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 Timers
 * Provides timers than can be used for things like triggering actions. Provides a method to create complex trigger action sequences by allowing chaining/delaying etc.
 */
// ############################# Timers.js ##############################
// This extension creates timers for use in the system.
// ---------------------------- creation --------------------------------------
// Author: Silenus aka twitch.tv/OldDepressedGamer
// GitHub: https://github.com/SilenusTA/StreamRoller
// Date: 12-April-2022
// --------------------------- functionality ----------------------------------
// Current functionality:
// Countdown Timers etc
// ============================================================================
// ============================================================================
//                           IMPORTS/VARIABLES
// ============================================================================
// Description: Import/Variable section
// ----------------------------- notes ----------------------------------------
// none
// ============================================================================
import * as fs from "fs";
import { dirname } from 'path';
import { clearTimeout } from "timers";
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));
const localConfig = {
    DataCenterSocket: null,
    timers: [] // holds the local timers
};
const default_serverConfig = {
    __version__: 0.2,
    extensionname: "timers",
    channel: "TIMERS",
    // default values for timer modal
    TimerName: "StartCountdownTimer",
    TimerMessage: "Starting in",
    TimerTimeout: "600"
};
let serverConfig = structuredClone(default_serverConfig);

const triggersandactions =
{
    extensionname: serverConfig.extensionname,
    description: "Timers allow for actions to be triggered when a timer goes off.<BR> For Triggers just put the name of the timer in you want to trigger on.<BR> For Actions put the name and duration of the timer in you want to start.",
    version: "0.3",
    channel: serverConfig.channel,
    // these are messages we can sendout that other extensions might want to use to trigger an action
    triggers:
        [
            {
                name: "TimerStart",
                displaytitle: "Timer Started",
                description: "A timer was started",
                messagetype: "trigger_TimerStarted",
                parameters: {
                    name: "",
                    duration: "",
                    message: ""
                }
            },
            {
                name: "TimerEnd",
                displaytitle: "Timer Ended",
                description: "A timer has finished",
                messagetype: "trigger_TimerEnded",
                parameters: {
                    name: "",
                    duration: "",
                    message: "",
                }
            },
            {
                name: "TimerRunning",
                displaytitle: "Timer Running",
                description: "A timer is running",
                messagetype: "trigger_TimerRunning",
                parameters: {
                    name: "",
                    message: "",
                    duration: "",
                    timeout: "",
                }
            }
        ],
    // these are messages we can receive to perform an action
    actions:
        [
            {
                name: "TimerStart",
                displaytitle: "Timer Start",
                description: "Start/Restart a countdown timer, duration in seconds",
                messagetype: "action_TimerStart",
                parameters: {
                    name: "",
                    duration: "",
                    message: ""
                }
            },
            {
                name: "TimerStop",
                displaytitle: "Timer Stop",
                description: "Stop a running timer",
                messagetype: "action_TimerStop",
                parameters: {
                    name: ""
                }
            }
        ],
}
// ============================================================================
//                           FUNCTION: initialise
// ============================================================================
/**
 * Starts the extension using the given data.
 * @param {object:Express} app 
 * @param {string} host 
 * @param {number} port 
 * @param {number} heartbeat 
 */
function initialise (app, host, port, heartbeat)
{
    try
    {
        localConfig.DataCenterSocket = sr_api.setupConnection(onDataCenterMessage, onDataCenterConnect,
            onDataCenterDisconnect, host, port);
    } catch (err)
    {
        logger.err("[EXTENSION]" + serverConfig.extensionname + ".initialise", "localConfig.DataCenterSocket connection failed:", err);
    }
}

// ============================================================================
//                           FUNCTION: onDataCenterDisconnect
// ============================================================================
/**
 * Disconnection message sent from the server
 * @param {String} reason 
 */
function onDataCenterDisconnect (reason)
{
    logger.log("[EXTENSION]" + serverConfig.extensionname + ".onDataCenterDisconnect", reason);
}
// ============================================================================
//                           FUNCTION: onDataCenterConnect
// ============================================================================
/**
 * Connection message handler
 * @param {*} socket 
 */
function onDataCenterConnect (socket)
{
    logger.log("[EXTENSION]" + serverConfig.extensionname + ".onDataCenterConnect", "Creating our channel");
    // Request our config from the server
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestConfig", serverConfig.extensionname));

    // Create a channel for messages to be sent out on
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("CreateChannel", serverConfig.extensionname, serverConfig.channel)

    );
}
// ============================================================================
//                           FUNCTION: onDataCenterMessage
// ============================================================================
/**
 * receives message from the socket
 * @param {data} server_packet 
 */
function onDataCenterMessage (server_packet)
{
    logger.log("[EXTENSION]" + serverConfig.extensionname + ".onDataCenterMessage", "message received ", server_packet);
    if (server_packet.type === "ConfigFile")
    {
        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 === "ExtensionMessage")
    {
        let extension_packet = server_packet.data;
        if (extension_packet.type === "RequestSettingsWidgetSmallCode")
            // TBD maintain list of all extensions that has requested Modals
            SendSettingsWidgetSmall(extension_packet.from);
        else if (extension_packet.type === "SettingsWidgetSmallData")
        {
            if (extension_packet.data.extensionname === serverConfig.extensionname)
            {
                if (localConfig.timers[extension_packet.data.TimerName] === undefined)
                    localConfig.timers[extension_packet.data.TimerName] = {};
                localConfig.timers[extension_packet.data.TimerName].name = extension_packet.data.TimerName;
                localConfig.timers[extension_packet.data.TimerName].message = extension_packet.data.TimerMessage;
                localConfig.timers[extension_packet.data.TimerName].timeout = extension_packet.data.TimerTimeout;
                localConfig.timers[extension_packet.data.TimerName].duration = extension_packet.data.TimerTimeout;
                //serverConfig.checkbox = "off";
                for (const [key, value] of Object.entries(extension_packet.data))
                    serverConfig[key] = value;

                SaveConfigToServer();
                // broadcast our modal out so anyone showing it can update it
                SendSettingsWidgetSmall("");

                // check any timers needed
                StartOrRestartTimer(extension_packet.data.TimerName);
            }
        }
        else if (extension_packet.type === "action_TimerStart")
        {
            if (localConfig.timers[extension_packet.data.name] === undefined)
                localConfig.timers[extension_packet.data.name] = {};
            localConfig.timers[extension_packet.data.name].name = extension_packet.data.name;
            localConfig.timers[extension_packet.data.name].message = extension_packet.data.message;
            localConfig.timers[extension_packet.data.name].timeout = extension_packet.data.duration;
            localConfig.timers[extension_packet.data.name].duration = extension_packet.data.duration;
            // This is an extension message from the API. not currently used as timers are started from the settins modals
            StartOrRestartTimer(extension_packet.data.name);
        }
        else if (extension_packet.type === "action_TimerStop")
        {
            if (localConfig.timers[extension_packet.data.name])
                localConfig.timers[extension_packet.data.name].timeout = 0;
            clearTimeout(localConfig.timers[extension_packet.data.name].Handle);
        }
        else if (extension_packet.type === "SettingsWidgetSmallCode")
        {
            // ignore these messages as we don't have other extensions settings pages
        }
        else if (extension_packet.type === "SettingsWidgetLargeCode")
        {
            // we don't currently have a large widget so ignore these
        }
        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 (extension_packet.type === "RequestSettingsWidgetLargeCode"
            || extension_packet.type === "TriggerAndActions"
            || extension_packet.type === "RequestCredentialsModalsCode")
        {
            // we don't handle these yet
        }
        else
            logger.warn("[EXTENSION]" + serverConfig.extensionname + ".onDataCenterMessage", "received unhandled ExtensionMessage ", server_packet);

    }
    else if (server_packet.type === "UnknownChannel")
    {
        logger.info("[EXTENSION]" + 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
        setTimeout(() =>
        {
            // resent the register command to see if the extension is up and running yet
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "JoinChannel", serverConfig.extensionname, server_packet.data
                ));
        }, 5000);
    }    // we have received data from a channel we are listening to
    else if (server_packet.type === "ChannelData")
    {
        logger.log("[EXTENSION]" + serverConfig.extensionname + ".onDataCenterMessage", "received message from unhandled channel ", server_packet.dest_channel);
    }
    else if (server_packet.type === "InvalidMessage")
    {
        logger.err("[EXTENSION]" + serverConfig.extensionname + ".onDataCenterMessage",
            "InvalidMessage ", server_packet.data.error, server_packet);
    }
    else if (server_packet.type === "ChannelJoined"
        || server_packet.type === "ChannelCreated"
        || server_packet.type === "ChannelLeft"
        || server_packet.type === "LoggingLevel"
    )
    {
        // just a blank handler for items we are not using to avoid message from the catchall
    }
    // ------------------------------------------------ unknown message type received -----------------------------------------------
    else
        logger.warn("[EXTENSION]" + serverConfig.extensionname +
            ".onDataCenterMessage", "Unhandled message type", server_packet.type);
}

// ===========================================================================
//                           FUNCTION: SendSettingsWidgetSmall
// ===========================================================================
/**
 * send some modal code 
 * @param {String} tochannel 
 */
function SendSettingsWidgetSmall (tochannel)
{

    fs.readFile(__dirname + '/timerssettingswidgetsmall.html', function (err, filedata)
    {
        if (err)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.EXTENSION_NAME +
                ".SendSettingsWidgetSmall", "failed to load modal", err);
        //throw 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);
            }

            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "SettingsWidgetSmallCode",
                        serverConfig.extensionname,
                        modalstring,
                        "",
                        tochannel,
                        serverConfig.channel
                    ),
                    "",
                    tochannel
                ))
        }
    });
}
// ============================================================================
//                           FUNCTION: SaveConfigToServer
// ============================================================================
/**
 * Saves our config to the server
 */
function SaveConfigToServer ()
{
    // saves our serverConfig to the server so we can load it again next time we startup
    sr_api.sendMessage(localConfig.DataCenterSocket, sr_api.ServerPacket
        ("SaveConfig",
            serverConfig.extensionname,
            serverConfig))
}
// ============================================================================
//                           FUNCTION: StartOrRestartTimer
// ============================================================================
/**
 * Starts or restarts a time if one already exists 
 * @param {string} timerName  
 */
function StartOrRestartTimer (timerName)
{
    if (!localConfig.timers[timerName])
        return;
    if (localConfig.timers[timerName].timeout == localConfig.timers[timerName].duration)
        sendStartTimer(localConfig.timers[timerName])
    Timer(timerName)
}
// ============================================================================
//                           FUNCTION: Timer
// ============================================================================
/**
 * Poll timer to check for expiry or update the time left value and sends out a 
 * trigger when the timer is updated or expired
 * @param {object} timerName 
 */
function Timer (timerName)
{
    localConfig.timers[timerName].timeout = localConfig.timers[timerName].timeout - 1;
    sendTimerData(localConfig.timers[timerName]);
    // write the file
    clearTimeout(localConfig.timers[timerName].Handle);
    if (localConfig.timers[timerName].timeout >= 0)
    {
        let minutes = Math.floor(localConfig.timers[timerName].timeout / 60);
        let seconds = localConfig.timers[timerName].timeout - (minutes * 60);
        fs.writeFileSync(__dirname + "/timerfiles/" + timerName + ".txt", localConfig.timers[timerName].message + " " + minutes + ":" + seconds.toString().padStart(2, '0'))
        localConfig.timers[timerName].Handle = setTimeout(() =>
        {
            Timer(timerName)
        }, 1000)
    }
    else
    {   // timer has finished
        sendEndTimer(localConfig.timers[timerName])
        const index = localConfig.timers.indexOf(timerName);
        if (index > -1)
        { // only splice array when item is found
            localConfig.timers.splice(index, 1); // 2nd parameter means remove one item only
        }
        fs.writeFileSync(__dirname + "/timerfiles/" + timerName + ".txt", " ")
    }
}
// ============================================================================
//                           FUNCTION: sendTimerData
// ============================================================================
/**
 * Sends out a trigger_TimerRunning message containing the current time left
 * @param {object} timeData 
 */
function sendTimerData (timeData)
{
    let data = findtriggerByMessageType("trigger_TimerRunning")
    data.parameters = {}
    data.parameters.name = timeData.name
    data.parameters.timeout = timeData.timeout
    data.parameters.duration = timeData.duration

    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "ChannelData",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "trigger_TimerRunning",
                serverConfig.extensionname,
                data,
                serverConfig.channel
            ),
            serverConfig.channel
        ));

}
// ============================================================================
//                           FUNCTION: sendStartTimer
// ============================================================================
/**
 * sends out trigger_TimerStarted when a new timer is started
 * @param {object} timeData 
 */
function sendStartTimer (timeData)
{
    let data = findtriggerByMessageType("trigger_TimerStarted")
    data.parameters = {}
    data.parameters.name = timeData.name
    data.parameters.duration = timeData.duration
    data.parameters.message = timeData.message
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "ChannelData",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "trigger_TimerStarted",
                serverConfig.extensionname,
                data,
                serverConfig.channel
            ),
            serverConfig.channel
        ));
}
// ============================================================================
//                           FUNCTION: sendEndTimer
// ============================================================================
/**
 * Sends out a trigger_TimerEnded when a timer ends
 * @param {object} timeData 
 */
function sendEndTimer (timeData)
{
    let data = findtriggerByMessageType("trigger_TimerEnded")
    data.parameters = {}
    data.parameters.name = timeData.name
    data.parameters.duration = timeData.duration
    data.parameters.message = timeData.message

    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "ChannelData",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "trigger_TimerEnded",
                serverConfig.extensionname,
                data,
                serverConfig.channel
            ),
            serverConfig.channel
        ));
}
// ============================================================================
//                           FUNCTION: findtriggerByMessageType
// ============================================================================
/**
 * Finds a trigger by name
 * @param {string} messagetype 
 * @returns 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);
}
// ============================================================================
//                                  EXPORTS
// Note that initialise is mandatory to allow the server to start this extension
// ============================================================================
export { initialise, triggersandactions };