/**
* 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
};
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 };