/**
* Copyright (C) 2025 "SilenusTA https://www.twitch.tv/olddepressedgamer"
*
* 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/>.
*/
import WebSocket from "ws";
import * as logger from "../../../backend/data_center/modules/logger.js";
const localConfig =
{
onMessageCallback: null,
createDummyChatMessageCallback: null,
userName: "",
streamerName: "",
chatroomId: "",
channelId: "",
channelName: "",
connected: false,
websocket: null,
connectToChatScheduleHandle: null,
connectToChatTimeout: 2000,
listeners: [
{ event: 'message', handler: handleMessage },
{ event: 'open', handler: handleOpen },
{ event: 'error', handler: handleError },
{ event: 'close', handler: handleClose },
],
reconnectAttempts: 0,
maxReconnectAttempts: 20,
maxFailedConnections: 5,
reconnectionDelay: 10000,
// debug
// connection counters
counters: {},
DEBUG: false,
SYSTEM_LOGGING_TAG: "[KICK]"
}
// ============================================================================
// FUNCTION: connectChat
// ============================================================================
/**
* initialise the chat system
* @param {function} onMessageCallback
*/
function init (onMessageCallback, createDummyChatMessage)
{
localConfig.onMessageCallback = onMessageCallback;
localConfig.createDummyChatMessageCallback = createDummyChatMessage
}
// ============================================================================
// FUNCTION: connectChat
// ============================================================================
/**
*
* @param {string} chatroomId // channel to subscribe to
* @param {string} userName // user logged in name
* @param {string} streamerName // name for the channel Name we are connecting to (normally the same unless using the app to read/view another channel)
*/
function connectChat (chatroomId, channelId, userName, streamerName)
{
try
{
// save these values off. only the channel Name is used for connections
// the rest are used for messages back to the chat to inform the user of connections/disconnections etc
localConfig.chatroomId = chatroomId;
localConfig.channelId = channelId;
localConfig.userName = userName;
localConfig.streamerName = streamerName;
localConfig.channelName = streamerName;
// disconnect if already connected
if (localConfig.websocket && localConfig.websocket.readyState !== WebSocket.CLOSED)
{
localConfig.listeners.forEach(({ event, handler }) =>
{
localConfig.websocket.off(event, handler);
});
localConfig.websocket.close();
}
// Pusher WebSocket URL
const pusherWsUrl = 'wss://ws-us2.pusher.com/app/32cbd69e4b950bf97679?protocol=7&client=js&version=8.4.0-rc2&flash=false';
const commonHeaders = {
'Origin': 'https://kick.com',
'Cache-Control': 'no-cache',
'Accept-Language': 'en-GB,en;q=0.9,af-ZA;q=0.8,af;q=0.7,en-US;q=0.6',
'Pragma': 'no-cache',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
};
// create new socket
localConfig.websocket = new WebSocket(pusherWsUrl, {
headers: { ...commonHeaders }
});
// add event handlers
localConfig.listeners.forEach(({ event, handler }) =>
{
localConfig.websocket.on(event, handler);
});
}
catch (err)
{
logger.err(localConfig.SYSTEM_LOGGING_TAG + "connectChat", "Error Received:", err);
Reconnect();
}
}
// ============================================================================
// FUNCTION: handleOpen
// ============================================================================
/**
*
*/
function handleOpen ()
{
//console.log(`Kick:Connection opened.`);
}
// ============================================================================
// FUNCTION: handleMessage
// ============================================================================
/**
*
* @param {object} data
*/
function handleMessage (data)
{
try
{
const message = JSON.parse(data);
localConfig.connected = true;
// When connection is established, subscribe to a channel
if (message.event === 'pusher:connection_established')
{
// subscribe to a channel events
subscribeToChannel(localConfig.chatroomId)
// delay here as on startup the other extension might not be ready to receive yet
setTimeout(() =>
{
let dummyMessage =
{
"id": "1",
"chatroom_id": 1,
"channel": localConfig.streamerName,
"content": `${localConfig.userName} connected to ${localConfig.channelName} chatroom on Kick`,
"type": "system",
"created_at": "2025-04-27T07:01:49+00:00",
"sender": {
"id": 1,
"username": "System",
"identity": { "color": "Red", "badges": [{ "type": "bot", "text": "bot" }] }
}
};
localConfig.createDummyChatMessageCallback(dummyMessage)
}, 5000);
}
else if (message.event === 'pusher_internal:subscription_succeeded')
{
//
}
else if (message.event === 'App\\Events\\ChatMessageEvent')
onChatMessage(message)
else
console.log("Kick: not handling these messages yet", message)
}
catch (err)
{
logger.err(localConfig.SYSTEM_LOGGING_TAG + "handleMessage", "Error Received:", err);
}
}
// ============================================================================
// FUNCTION: handleError
// ============================================================================
/**
*
* @param {object} error
*/
function handleError (error)
{
logger.err(localConfig.SYSTEM_LOGGING_TAG + "Kick:chat:handleError", "Websocket Error Received:", error);
Reconnect();
}
// ============================================================================
// FUNCTION: handleClose
// ============================================================================
/**
*
* @param {string} code
* @param {string} reason
*/
function handleClose (code, reason)
{
localConfig.connected = false;
let dummyMessage =
{
"id": "1",
"chatroom_id": 1,
"channel": localConfig.streamerName,
"content": `Connected closed for ${localConfig.userName} connected to ${localConfig.chatroomId} chatroom on Kick for streamer ${localConfig.streamerName}`,
"type": "system",
"created_at": "2025-04-27T07:01:49+00:00",
"sender": {
"id": 1,
"username": "System",
"identity": { "color": "Red", "badges": [{ "type": "bot", "text": "bot" }] }
}
};
localConfig.createDummyChatMessageCallback(dummyMessage)
}
// ============================================================================
// FUNCTION: subscribeToChannel
// ============================================================================
function subscribeToChannel ()
{
try
{
// subscribe to chatrooms
let subscribeMessage = {
event: 'pusher:subscribe',
data: { channel: `chatrooms.${localConfig.chatroomId}.v2` }
};
localConfig.websocket.send(JSON.stringify(subscribeMessage));
// subscribe to for rewards
subscribeMessage = {
event: 'pusher:subscribe',
data: { channel: `chatrooms_${localConfig.chatroomId}` }
};
localConfig.websocket.send(JSON.stringify(subscribeMessage));
// subscribe to for rewards
subscribeMessage = {
event: 'pusher:subscribe',
data: { channel: `chatrooms_${localConfig.chatroomId}.v1` }
};
localConfig.websocket.send(JSON.stringify(subscribeMessage));
// subscribe for alerts
subscribeMessage = {
event: 'pusher:subscribe',
data: { channel: `channel.${localConfig.channelId}` }
};
localConfig.websocket.send(JSON.stringify(subscribeMessage));
// subscribe for alerts
subscribeMessage = {
event: 'pusher:subscribe',
data: { channel: `channel.${localConfig.channelId}.v1` }
};
localConfig.websocket.send(JSON.stringify(subscribeMessage));
// subscribe for alerts
subscribeMessage = {
event: 'pusher:subscribe',
data: { channel: `channel.${localConfig.channelId}.v2` }
};
localConfig.websocket.send(JSON.stringify(subscribeMessage));
}
catch (err)
{
logger.err(localConfig.SYSTEM_LOGGING_TAG + "subscribeToChannel", "failed, retrying.", err);
setTimeout(subscribeToChannel(localConfig.channelName), 1000)
}
}
// ============================================================================
// FUNCTION: disconnectChat
// ============================================================================
/**
* Disconnect socket
*/
function disconnectChat ()
{
localConfig.connected = true;
if (localConfig.websocket && localConfig.websocket.readyState !== WebSocket.CLOSED)
{
localConfig.listeners.forEach(({ event, handler }) =>
{
localConfig.websocket.off(event, handler);
});
localConfig.websocket.close();
}
localConfig.websocket = null;
}
// ============================================================================
// FUNCTION: connected
// ============================================================================
function connected ()
{
return localConfig.connected
}
// ============================================================================
// FUNCTION: onChatMessage
// ============================================================================
/**
*
* @param {object} data
*/
function onChatMessage (message)
{
if (typeof (message.data) == "string")
localConfig.onMessageCallback(JSON.parse(message.data));
else
localConfig.onMessageCallback(message.data);
}
// ============================================================================
// FUNCTION: Reconnect
// ============================================================================
function Reconnect ()
{
if (localConfig.reconnectAttempts >= localConfig.maxFailedConnections)
{
logger.err(localConfig.SYSTEM_LOGGING_TAG + "Reconnect", `Max reconnection attempts reached for ${localConfig.channelName}`);
return; // Max retries reached, stop reconnecting
}
if (localConfig.websocket && (localConfig.websocket.readyState === WebSocket.CLOSING || localConfig.websocket.readyState === WebSocket.CLOSED))
{
localConfig.reconnectAttempts++;
logger.err(localConfig.SYSTEM_LOGGING_TAG + "Reconnect", `Attempt #${localConfig.reconnectAttempts} for ${localConfig.channelName}`);
setTimeout(() =>
{
connectChat(localConfig.chatroomId, localConfig.channelId, localConfig.userName, localConfig.streamerName)
}, localConfig.reconnectionDelay);
}
}
export { init, connectChat, disconnectChat, connected }