diff --git a/widgets/shared/lib/streamerbot-client.js b/widgets/shared/lib/streamerbot-client.js index 1f1ad44..e4574c7 100644 --- a/widgets/shared/lib/streamerbot-client.js +++ b/widgets/shared/lib/streamerbot-client.js @@ -1 +1 @@ -"use strict";var Streamerbot=(()=>{var W=Object.create;var p=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var D=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var O=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports),q=(n,e)=>{for(var t in e)p(n,t,{get:e[t],enumerable:!0})},E=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of U(e))!H.call(n,o)&&o!==t&&p(n,o,{get:()=>e[o],enumerable:!(r=L(e,o))||r.enumerable});return n};var M=(n,e,t)=>(t=n!=null?W(D(n)):{},E(e||!n||!n.__esModule?p(t,"default",{value:n,enumerable:!0}):t,n)),B=n=>E(p({},"__esModule",{value:!0}),n);var G=O((Q,P)=>{"use strict";P.exports=function(){throw new Error("ws does not work in the browser. Browser clients must use the native WebSocket object")}});var _={};q(_,{Client:()=>l});var m=class{constructor(e={}){this.logLevels={verbose:0,debug:1,info:2,warn:3,error:4,none:5};this.level=e.level||"info",this.customLogger=e.customLogger}setLevel(e){this.level=e}setCustomLogger(e){this.customLogger=e}verbose(...e){this.log("verbose",...e)}debug(...e){this.log("debug",...e)}info(...e){this.log("info",...e)}warn(...e){this.log("warn",...e)}error(...e){this.log("error",...e)}log(e,...t){if(!(this.logLevels[e]y.getRandomValues(n);function C(n="req"){return`sb:client:${n}:${Date.now()}-${R(new Uint32Array(12))[0]}`}function k(n){let e;return n.code==1e3?e="Connection closed.":n.code==1001?e='Endpoint is "going away".':n.code==1002?e="Connection closed due to a protocol error.":n.code==1003||n.code==1007||n.code==1008||n.code==1010?e="Bad request.":n.code==1004?e="Reserved":n.code==1005?e="Missing status code.":n.code==1006?e="The connection was closed abnormally.":n.code==1009?e="Message size limit exceeded.":n.code==1011?e="Server terminated connection because due to unexpected condition.":n.code==1015?e="TLS handshake failure":e="Unknown error",e}async function h(n,e){let{timeout:t,message:r="Operation timed out.",controller:o}=e,s;return await Promise.race([new Promise((i,a)=>{s=setTimeout(()=>(o.abort(),console.debug("[withTimeout] timeout reached",e),a(new Error(r))),t),e.signal?.addEventListener("abort",()=>{clearTimeout(s),o?.abort(),a(new Error("Operation aborted."))},{once:!0})}),n]).finally(()=>{clearTimeout(s),o.abort()})}async function S(n){let e=new TextEncoder().encode(n),t=await T.digest("SHA-256",e),o=Array.from(new Uint8Array(t)).map(s=>s.toString(16).padStart(2,"0")).join("");return I(o)}function I(n){let e=new Uint8Array(n.match(/.{1,2}/g).map(r=>parseInt(r,16)));return btoa(String.fromCharCode.apply(null,Array.from(e)))}function v({timeout:n=1e4,addEventListener:e,removeEventListener:t}){let r=C("res"),o=new AbortController,s=o.signal,i=new Promise((a,c)=>{let d=b=>{let f=b?.data;f?.eventName===r&&(o.abort(),a(f?.args))};e("Custom.Event",d);let u=setTimeout(()=>{o.abort(),c(new Error("Timed out waiting for Custom Event"))},n);s.addEventListener("abort",()=>{clearTimeout(u),t(b=>b.events?.includes("Custom.Event")&&b.callback===d)},{once:!0})});return{responseId:r,promise:i,controller:o}}var A={scheme:"ws",host:"127.0.0.1",port:8080,endpoint:"/",immediate:!0,autoReconnect:!0,retries:-1,subscribe:{},logger:w,logLevel:"info"},l=class{constructor(e=A){this._authEnabled=!1;this._authenticated=!1;this.listeners=[];this.subscriptions={};this._explicitlyClosed=!1;this._retried=0;this._connectController=new AbortController;this._reconnectTimeout=void 0;this.options={...A,...e},this.logger=this.options.logger||null,this.logger&&this.options.logLevel&&this.logger.setLevel(this.options.logLevel),this.options.immediate===!0&&this.connect().catch(t=>this.logger?.warn("Failed to connect:",t))}get authenticated(){return!!this.socket&&this.socket.readyState===this.socket.OPEN&&this._authenticated}get ready(){return!this.socket||this.socket.readyState!==this.socket.OPEN||this._authEnabled&&!this._authenticated?!1:!!this.info&&!!this.version}async connect(e=1e4){if(this.socket?.readyState!==this.socket?.CLOSED)try{await this.disconnect()}catch{}this._explicitlyClosed=!1,this._connectController.abort(),this._connectController=new AbortController;let t=new AbortController;return this._connectController.signal.addEventListener("abort",()=>{t.abort()},{once:!0}),await h(new Promise(async(r,o)=>{try{this.options.password&&(this._authEnabled=!0);let s=`${this.options.scheme}://${this.options.host}:${this.options.port}${this.options.endpoint}`;this.logger?.debug("Connecting to Streamer.bot WebSocket server at",s,this._authEnabled?"with authentication":""),this.socket=globalThis?.process?.versions?.node?new(await Promise.resolve().then(()=>M(G(),1))).WebSocket(s):new WebSocket(s),this.socket.onmessage=this.onMessage.bind(this),this.socket.onopen=this.onOpen.bind(this),this.socket.onclose=this.onClose.bind(this),this.socket.onerror=this.onError.bind(this),this.socket.addEventListener("open",()=>{if(!this.socket)return o(new Error("WebSocket not initialized"));r()},{signal:t.signal}),this.socket.addEventListener("close",()=>o(new Error("WebSocket closed")),{once:!0})}catch(s){try{await this.disconnect(),this?.options?.onError?.(s)}catch(i){this.logger?.warn("Error invoking onError handler",i)}o(s)}}),{timeout:e,message:"WebSocket connection timeout exceeded",controller:t})}async disconnect(e=1e3,t=1e3){if(this._explicitlyClosed=!0,this._connectController.abort(),this._reconnectTimeout&&clearTimeout(this._reconnectTimeout),!this.socket||this.socket.readyState===this.socket.CLOSED)return;let r=new AbortController,o=r.signal;return await h(new Promise((s,i)=>{if(this.socket?.addEventListener("close",()=>{this.logger?.debug("Disconnected from Streamer.bot WebSocket server"),s()},{signal:o}),this.socket?.readyState!==this.socket?.CLOSING)try{this.socket?.close(e)}catch(a){i(a)}}),{timeout:t,message:"Timeout exceeded while closing connection",controller:r})}async handshake(){if(!this.socket)throw new Error("WebSocket not initialized");let e=new AbortController,{signal:t}=e;this._connectController.signal.addEventListener("abort",()=>{e.abort()},{once:!0,signal:t});let r=await h(new Promise((o,s)=>{this.socket?.addEventListener("message",async i=>{if(!("data"in i)||!i.data||typeof i.data!="string"){this.logger?.debug("Unknown message received",i);return}try{let a=JSON.parse(i.data);a&&"info"in a&&o(a)}catch(a){this.logger?.warn("Invalid JSON payload received",i.data),s(a)}},{signal:t})}),{timeout:5e3,message:"Handshake timeout exceeded",controller:e});if(!r||!("info"in r))throw new Error("Handshake failed (invalid payload)");if("request"in r&&r?.request==="Hello"&&r.authentication)return await this.authenticate(r);if(r.info&&!r.authentication){this.logger?.debug("Connected to Streamer.bot WebSocket server",r.info),this.info=r.info,this.version=r.info.version;return}throw new Error("Handshake failed (unknown)")}async authenticate(e){if(!this._authEnabled||!this.options.password){if(this.logger?.debug("No password provided for authentication. Checking if auth is enforced for all requests..."),(await this.getInfo()).status==="ok"){this._authenticated=!1,this.version=e.info.version,this.info=e.info;return}throw await this.disconnect(),new Error("Authentication required")}if(!e.authentication)throw this.logger?.debug("Missing authentication payload"),await this.disconnect(),new Error("Invalid authentication payload");this.logger?.debug("Authenticating with Streamer.bot WebSocket server...");let{salt:t,challenge:r}=e?.authentication,o=await S(`${this.options.password}${t}`),s=await S(`${o}${r}`);if((await this.request({request:"Authenticate",authentication:s})).status==="ok")this._authenticated=!0,this.version=e.info.version,this.info=e.info;else throw await this.disconnect(),new Error("Authentication failed")}async onOpen(){this._retried=0,this._reconnectTimeout&&clearTimeout(this._reconnectTimeout);try{this._authEnabled||this.getInfo().catch(()=>this.logger?.debug("Failed to fetch Streamer.bot instance info")),await this.handshake(),this.version&&this.info&&(this.logger?.debug(`Connected to Streamer.bot: v${this.version} (${this.info.name})`),await this.updateSupportedEvents(),this?.options?.onConnect?.(this.info))}catch(e){return this.logger?.warn("Failed handshake with Streamer.bot",e),this.options?.onError?.(e instanceof Error?e:new Error("Failed handshake with Streamer.bot")),await this.disconnect()}try{if(this.options.subscribe==="*"||typeof this.options.subscribe=="object"&&!Array.isArray(this.options.subscribe)&&Object.keys(this.options.subscribe??{}).length)this.logger?.debug("Subscribing to initial events from options:",this.options.subscribe),await this.subscribe(this.options.subscribe);else if(typeof this.options.subscribe=="string"||Array.isArray(this.options.subscribe)){this.logger?.debug("Subscribing to initial events from options:",this.options.subscribe);let e=await this.getSubscriptionsFromEventStrings(this.options.subscribe);this.logger?.debug("Parsed subscriptions from options:",e),e&&await this.subscribe(e)}if(this.listeners.length){let e=await this.getSubscriptionsFromListeners();await this.subscribe(e)}this.logger?.verbose("Subscribed to requested events",this.subscriptions,this.listeners)}catch(e){this.logger?.warn("Error subscribing to requested events",e)}}onClose(e){this._connectController.abort();try{(e.type==="error"||!e.wasClean)&&this.options.onError&&this?.options?.onError(new Error(k(e))),this?.options?.onDisconnect?.()}catch(t){this.logger?.warn("Error invoking user-provided onDisconnect handler",t)}if(this._explicitlyClosed||!this.options.autoReconnect)return this.logger?.debug("Cleaning up..."),this.cleanup();this._retried+=1,typeof this.options.retries=="number"&&(this.options.retries<0||this._retried{if(!(this.socket&&this.socket.readyState!==this.socket.CLOSED)){this.logger?.debug(`Reconnecting... (attempt ${this._retried})`);try{await this.connect(1e4)}catch(t){this._retried&&this.logger?.warn(`Failed to reconnect (attempt ${this._retried-1})`,t)}}},Math.min(3e4,this._retried*1e3))):(this.logger?.debug("Auto-reconnect limit reached. Cleaning up..."),this.cleanup())}async onMessage(e){if(!e.data||typeof e.data!="string"){this.logger?.debug("Unknown message received",e);return}let t;try{t=JSON.parse(e.data)}catch(r){this.logger?.warn("Invalid JSON payload received",e.data,r);return}this.logger?.verbose("RECV",t);try{this.options.onData&&this?.options?.onData(t)}catch(r){this.logger?.warn("Error occurred within user-provided onData callback",r)}if(t?.event?.source&&t?.event?.type){for(let r of this.listeners)if(r.events?.length&&r.events.find(o=>o==="*"||o===`${t?.event?.source}.${t?.event?.type}`||o.split(".",2)?.[1]==="*"&&o.split(".",2)?.[0]===t?.event?.source))try{r.callback(t)}catch(o){this.logger?.warn(`Error occurred within user-provided event callback (${r.events})`,o)}}}onError(e){this.logger?.debug("WebSocket onError",e),this.socket&&this.socket.readyState!==this.socket.OPEN&&this._connectController.abort();try{this?.options?.onError?.(new Error("WebSocket Error"))}catch(t){this.logger?.warn("Error occurred within user-provided onError callback",t)}}cleanup(){this.socket&&(this.socket.onopen=null,this.socket.onclose=null,this.socket.onerror=null,this.socket.onmessage=null,this.socket=void 0),this.listeners=[],this._retried=0,this._connectController.abort(),this._reconnectTimeout&&clearTimeout(this._reconnectTimeout)}send(e){this.socket?.send(JSON.stringify(e))}async request(e,t="",r=1e4){if(!this.socket||this.socket.readyState!==this.socket.OPEN)throw new Error("WebSocket is not connected");t||(t=C());let o=new AbortController,s=o.signal;this._connectController.signal.addEventListener("abort",()=>{o.abort()},{once:!0,signal:s});let i=await h(new Promise((a,c)=>{this.socket?.addEventListener("message",d=>{if(!("data"in d)||!d.data||typeof d.data!="string"){this.logger?.debug("Unknown message received",d.data);return}try{let u=JSON.parse(d?.data);if(u?.id===t)return this.logger?.verbose(`RECV :: ${e.request}`,u),a(u)}catch(u){this.logger?.warn("Invalid JSON payload received",d.data),c(u)}},{signal:s}),this.logger?.verbose(`SEND :: ${e.request}`,{...e,id:t}),this.send({...e,id:t})}),{timeout:r,message:"Request timed out",controller:o,signal:s});if(i?.status==="ok"){try{this.options.onData&&this?.options?.onData(i)}catch(a){this.logger?.warn("Error invoking onData handler",a)}return{event:{source:"Request",type:e.request??"Unknown"},...i}}throw new Error("Request failed")}async on(e,t){try{if(!e)return;let r={events:[e],callback:t};if(this.listeners.push(r),this.ready){let o=await this.getSubscriptionsFromListeners([r]);await this.subscribe(o)}this.logger?.debug(`Added event listener for "${e}"`)}catch(r){this.logger?.warn(`Failed adding event listener for "${e}"`,r)}}async updateSupportedEvents(){if(this.ready)try{let e=await this.getEvents();if(e.status!=="ok"||!e.events)throw new Error(e.status);this.supportedEvents=e.events,this.logger?.debug(`Successfully fetched supported event types for Streamer.bot v${this.version}`)}catch(e){this.logger?.warn("Failed to fetch supported events from Streamer.bot, falling back to stored events type.",e),this.supportedEvents=g}}async getSupportedEvents(){return this.supportedEvents||(this.logger?.warn("Supported event types not yet initialized, fetching from Streamer.bot instance..."),await this.updateSupportedEvents()),this.supportedEvents??g}getEventsFromListeners(e){return(e??this.listeners).reduce((t,r)=>(r.events.forEach(o=>{t[o]||(t[o]=[]),t[o].push(r.callback)}),t),{})}async getSubscriptionsFromListeners(e){let t=this.getEventsFromListeners(e);return this.getSubscriptionsFromEventStrings(Object.keys(t))}async getSubscriptionsFromEventStrings(e){let t={};typeof e=="string"&&(e=[e]);for(let r of e){let o=await this.parseEventString(r);if(o)for(let s of o){let{source:i,eventTypes:a}=s,c=new Set([...t[i]??[],...a]);t[i]=[...c]}}return t}async parseEventString(e){let t=await this.getSupportedEvents();if(!e||typeof e!="string"){this.logger?.warn(`Invalid event subscription requested "${e}"`);return}if(e==="*")return Object.keys(t).map(r=>{let o=r,s=t[o]??[];return{source:o,eventTypes:s}});{let[r,o]=e.split(".",2);if(!r||!o||!(r in t)){this.logger?.warn(`Invalid event subscription requested "${e}"`);return}let s=r,i=o;if(i)return[{source:s,eventTypes:i==="*"?t[s]:[i]}];this.logger?.warn(`Invalid event type requested "${e}"`);return}}async subscribe(e){let t=await this.getSupportedEvents();e==="*"&&(e=t);for(let r in e){if(!r||r==="err")continue;if(!(r in t)){this.logger?.warn(`Attempted to subscribe to empty or unknown event source: "${r}"`,Object.keys(e));continue}let o=r,s=e[o]??[];if(s&&s.length){let i=new Set([...this.subscriptions[o]??[],...s]);this.subscriptions[o]=[...i]}}return Object.keys(this.subscriptions).length===0?(this.logger?.warn("No valid events to subscribe to. Please provide valid event sources and types."),{id:"invalid",status:"error",error:"No valid events to subscribe to"}):await this.request({request:"Subscribe",events:this.subscriptions})}async unsubscribe(e){let t=await this.getSupportedEvents();e==="*"&&(e=t);for(let r in e){if(r===void 0||!Object.keys(t).includes(r))continue;let o=r,s=e[o];if(s&&s.length)for(let i of s)i&&this.subscriptions[o]?.filter&&(this.subscriptions[o]=this.subscriptions[o])?.filter(a=>i!==a)}return await this.request({request:"UnSubscribe",events:e})}async getEvents(){return await this.request({request:"GetEvents"})}async getActions(){return await this.request({request:"GetActions"})}async doAction(e,t,r){if(r?.customEventResponse)return this.doActionWithCustomEventResponse(e,t);let o,s;return typeof e=="string"?o=e:(o=e.id,s=e.name),await this.request({request:"DoAction",action:{id:o,name:s},args:t})}async doActionWithCustomEventResponse(e,t={},r=1e4){if(!this.socket||this.socket.readyState!==this.socket.OPEN)throw new Error("WebSocket is not connected");let{responseId:o,promise:s,controller:i}=v({timeout:r,addEventListener:(c,d)=>this.on(c,d),removeEventListener:c=>{this.listeners=this.listeners.filter(d=>!c(d))}}),a={...t,sbClientResponse:o};try{let c=await this.doAction(e,a),d=await s;return{...c,customEventResponseArgs:d}}catch(c){throw i.signal.aborted||i.abort(),c}}async getBroadcaster(){return await this.request({request:"GetBroadcaster"})}async getMonitoredYouTubeBroadcasts(){return await this.request({request:"GetMonitoredYouTubeBroadcasts"})}async getCredits(){return await this.request({request:"GetCredits"})}async testCredits(){return await this.request({request:"TestCredits"})}async clearCredits(){return await this.request({request:"ClearCredits"})}async getInfo(){return await this.request({request:"GetInfo"})}async getActiveViewers(){return await this.request({request:"GetActiveViewers"})}async executeCodeTrigger(e,t,r){return r?.customEventResponse?this.executeCodeTriggerWithCustomEventResponse(e,t):await this.request({request:"ExecuteCodeTrigger",triggerName:e,args:t})}async executeCodeTriggerWithCustomEventResponse(e,t={},r=1e4){if(!this.socket||this.socket.readyState!==this.socket.OPEN)throw new Error("WebSocket is not connected");let{responseId:o,promise:s,controller:i}=v({timeout:r,addEventListener:(c,d)=>this.on(c,d),removeEventListener:c=>{this.listeners=this.listeners.filter(d=>!c(d))}}),a={...t,sbClientResponse:o};try{let c=await this.executeCodeTrigger(e,a),d=await s;return{...c,customEventResponseArgs:d}}catch(c){throw i.signal.aborted||i.abort(),c}}async getCodeTriggers(){return await this.request({request:"GetCodeTriggers"})}async getCommands(){return await this.request({request:"GetCommands"})}async getEmotes(e){switch(e){case"twitch":return await this.request({request:"TwitchGetEmotes"});case"youtube":return await this.request({request:"YouTubeGetEmotes"});default:throw new Error("Invalid platform")}}async getGlobals(e=!0){return await this.request({request:"GetGlobals",persisted:e})}async getGlobal(e,t=!0){let r=await this.request({request:"GetGlobal",variable:e,persisted:t});return r.status==="ok"?r.variables[e]?{id:r.id,status:r.status,variable:r.variables[e]}:{status:"error",error:"Variable not found"}:r}async getUserGlobals(e,t=null,r=!0){let s={twitch:"TwitchGetUserGlobals",youtube:"YouTubeGetUserGlobals",trovo:"TrovoGetUserGlobals",kick:"KickGetUserGlobals"}[e];if(!s)throw new Error("Invalid platform");return await this.request({request:s,variable:t,persisted:r})}async getUserGlobal(e,t,r=null,o=!0){let i={twitch:"TwitchGetUserGlobal",youtube:"YouTubeGetUserGlobal",trovo:"TrovoGetUserGlobal",kick:"KickGetUserGlobal"}[e];if(!i)throw new Error("Invalid platform");let a=await this.request({request:i,userId:t,variable:r||null,persisted:o});if(a.status==="ok"&&t&&r){let c=a.variables.find(d=>d.name===r);return c?{id:a.id,status:a.status,variable:c}:{status:"error",error:"Variable not found"}}return a}async sendMessage(e,t,{bot:r=!1,internal:o=!0,...s}={}){if(!this._authenticated)return{status:"error",error:"Authentication required"};let i={platform:e,message:t,bot:r,internal:o};return["twitch","kick"].includes(e)&&s.replyId&&Object.assign(i,{replyId:s.replyId}),["youtube"].includes(e)&&s.broadcastId&&Object.assign(i,{broadcastId:s.broadcastId}),await this.request({...i,request:"SendMessage"})}async getUserPronouns(e,t){return await this.request({request:"GetUserPronouns",platform:e,userLogin:t})}};Object.assign(globalThis,{StreamerbotClient:l});return B(_);})(); +var Streamerbot=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){"@babel/helpers - typeof";return t=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},t(e)}function n(e,n){if(t(e)!=`object`||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var i=r.call(e,n||`default`);if(t(i)!=`object`)return i;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(n===`string`?String:Number)(e)}function r(e){var r=n(e,`string`);return t(r)==`symbol`?r:r+``}function i(e,t,n){return(t=r(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}let a=new class{constructor(e={}){i(this,`level`,void 0),i(this,`customLogger`,void 0),i(this,`logLevels`,{verbose:0,debug:1,info:2,warn:3,error:4,none:5}),this.level=e.level||`info`,this.customLogger=e.customLogger}setLevel(e){this.level=e}setCustomLogger(e){this.customLogger=e}verbose(...e){this.log(`verbose`,...e)}debug(...e){this.log(`debug`,...e)}info(...e){this.log(`info`,...e)}warn(...e){this.log(`warn`,...e)}error(...e){this.log(`error`,...e)}log(e,...t){if(!(this.logLevels[e]s.getRandomValues(e);function u(e=`req`){return`sb:client:${e}:${Date.now()}-${l(new Uint32Array(12))[0]}`}function d(e){let t;return t=e.code==1e3?`Connection closed.`:e.code==1001?`Endpoint is "going away".`:e.code==1002?`Connection closed due to a protocol error.`:e.code==1003||e.code==1007||e.code==1008||e.code==1010?`Bad request.`:e.code==1004?`Reserved`:e.code==1005?`Missing status code.`:e.code==1006?`The connection was closed abnormally.`:e.code==1009?`Message size limit exceeded.`:e.code==1011?`Server terminated connection because due to unexpected condition.`:e.code==1015?`TLS handshake failure`:`Unknown error`,t}async function f(e,t){let{timeout:n,message:r=`Operation timed out.`,controller:i}=t,a;return await Promise.race([new Promise((e,o)=>{a=setTimeout(()=>(i.abort(),console.debug(`[withTimeout] timeout reached`,t),o(Error(r))),n),t.signal?.addEventListener(`abort`,()=>{clearTimeout(a),i?.abort(),o(Error(`Operation aborted.`))},{once:!0,signal:i.signal})}),e]).finally(()=>{clearTimeout(a),i.abort()})}async function p(e){let t=new TextEncoder().encode(e),n=await c.digest(`SHA-256`,t);return m(Array.from(new Uint8Array(n)).map(e=>e.toString(16).padStart(2,`0`)).join(``))}function m(e){let t=new Uint8Array(e.match(/.{1,2}/g).map(e=>parseInt(e,16)));return btoa(String.fromCharCode.apply(null,Array.from(t)))}function h({timeout:e=1e4,addEventListener:t,removeEventListener:n}){let r=u(`res`),i=new AbortController,a=i.signal;return{responseId:r,promise:new Promise((o,s)=>{let c=e=>{let t=e?.data;t?.eventName===r&&(i.abort(),o(t?.args))};t(`Custom.Event`,c);let l=setTimeout(()=>{i.abort(),s(Error(`Timed out waiting for Custom Event`))},e);a.addEventListener(`abort`,()=>{clearTimeout(l),n(e=>e.events?.includes(`Custom.Event`)&&e.callback===c)},{once:!0})}),controller:i}}let g={scheme:`ws`,host:`127.0.0.1`,port:8080,endpoint:`/`,immediate:!0,autoReconnect:!0,retries:-1,subscribe:{},logger:a,logLevel:`info`};var _=class{constructor(e=g){i(this,`options`,void 0),i(this,`logger`,void 0),i(this,`socket`,void 0),i(this,`info`,void 0),i(this,`version`,void 0),i(this,`supportedEvents`,void 0),i(this,`_authEnabled`,!1),i(this,`_authenticated`,!1),i(this,`listeners`,[]),i(this,`subscriptions`,{}),i(this,`_explicitlyClosed`,!1),i(this,`_retried`,0),i(this,`_connectController`,new AbortController),i(this,`_reconnectTimeout`,void 0),this.options={...g,...e},this.logger=this.options.logger||null,this.logger&&this.options.logLevel&&this.logger.setLevel(this.options.logLevel),!0===this.options.immediate&&this.connect().catch(e=>this.logger?.warn(`Failed to connect:`,e))}get authenticated(){return!!this.socket&&this.socket.readyState===this.socket.OPEN&&this._authenticated}get ready(){return!this.socket||this.socket.readyState!==this.socket.OPEN||this._authEnabled&&!this._authenticated?!1:!!this.info&&!!this.version}async connect(e=1e4){if(this.socket?.readyState!==this.socket?.CLOSED)try{await this.disconnect()}catch(e){this.logger?.warn(`Unexpected error while calling disconnect():`,e)}this._explicitlyClosed=!1,this._connectController.abort(),this._connectController=new AbortController;let t=new AbortController,{signal:n}=t;return this._connectController.signal.addEventListener(`abort`,()=>t.abort(),{once:!0,signal:n}),await f(new Promise((e,t)=>{try{this.options.password&&(this._authEnabled=!0);let r=`${this.options.scheme}://${this.options.host}:${this.options.port}${this.options.endpoint}`;this.logger?.debug(`Connecting to Streamer.bot WebSocket server at`,r,this._authEnabled?`with authentication`:``),this.socket=new WebSocket(r),this.socket.onmessage=this.onMessage.bind(this),this.socket.onopen=this.onOpen.bind(this),this.socket.onclose=this.onClose.bind(this),this.socket.onerror=this.onError.bind(this),this.socket.addEventListener(`open`,()=>{if(!this.socket)return t(Error(`WebSocket not initialized`));e()},{once:!0,signal:n}),this.socket.addEventListener(`close`,()=>t(Error(`WebSocket closed`)),{once:!0,signal:n})}catch(e){this.disconnect().catch(e=>this.logger?.warn(`Unexpected error while calling disconnect():`,e)),this?.options?.onError?.(e),t(e)}}),{timeout:e,message:`WebSocket connection timeout exceeded`,controller:t,signal:this._connectController.signal})}async disconnect(e=1e3,t=1e3){if(this._explicitlyClosed=!0,this._connectController.abort(),this._reconnectTimeout&&clearTimeout(this._reconnectTimeout),!this.socket||this.socket.readyState===this.socket.CLOSED)return;let n=new AbortController,r=n.signal;return this._connectController.signal.addEventListener(`abort`,()=>n.abort(),{once:!0,signal:r}),await f(new Promise((t,n)=>{if(this.socket&&(this.socket.addEventListener(`close`,()=>{this.logger?.debug(`Disconnected from Streamer.bot WebSocket server`),t()},{signal:r}),this.socket.readyState!==this.socket.CLOSING))try{this.socket?.close(e)}catch(e){n(e)}}),{timeout:t,message:`Timeout exceeded while closing connection`,controller:n,signal:this._connectController.signal})}async handshake(){if(!this.socket)throw Error(`WebSocket not initialized`);let e=new AbortController,{signal:t}=e;this._connectController.signal.addEventListener(`abort`,()=>e.abort(),{once:!0,signal:t});let n=await f(new Promise((e,n)=>{this.socket&&this.socket.addEventListener(`message`,async t=>{if(!(`data`in t)||!t.data||typeof t.data!=`string`){this.logger?.debug(`Unknown message received :: ${typeof t}`);return}try{let n=JSON.parse(t.data);n&&`info`in n&&e(n)}catch(e){this.logger?.warn(`Invalid JSON payload received`,t.data),n(e)}},{signal:t})}),{timeout:5e3,message:`Handshake timeout exceeded`,controller:e,signal:this._connectController.signal});if(!n||!(`info`in n))throw Error(`Handshake failed (invalid payload)`);if(`request`in n&&n?.request===`Hello`&&n.authentication)return await this.authenticate(n);if(n.info&&!n.authentication){this.logger?.debug(`Connected to Streamer.bot WebSocket server`,n.info),this.info=n.info,this.version=n.info.version;return}throw Error(`Handshake failed (unknown)`)}async authenticate(e){if(!this._authEnabled||!this.options.password){if(this.logger?.debug(`No password provided for authentication. Checking if auth is enforced for all requests...`),(await this.getInfo()).status===`ok`){this._authenticated=!1,this.version=e.info.version,this.info=e.info;return}throw await this.disconnect(),Error(`Authentication required`)}if(!e.authentication)throw this.logger?.debug(`Missing authentication payload`),await this.disconnect(),Error(`Invalid authentication payload`);this.logger?.debug(`Authenticating with Streamer.bot WebSocket server...`);let{salt:t,challenge:n}=e.authentication,r=await p(`${await p(`${this.options.password}${t}`)}${n}`);if((await this.request({request:`Authenticate`,authentication:r})).status===`ok`)this._authenticated=!0,this.version=e.info.version,this.info=e.info;else throw await this.disconnect(),Error(`Authentication failed`)}async onOpen(){this._retried=0,this._reconnectTimeout&&clearTimeout(this._reconnectTimeout);try{this._authEnabled||this.getInfo().catch(()=>this.logger?.debug(`Failed to fetch Streamer.bot instance info`)),await this.handshake(),this.version&&this.info&&(this.logger?.debug(`Connected to Streamer.bot: v${this.version} (${this.info.name})`),await this.updateSupportedEvents(),this?.options?.onConnect?.(this.info))}catch(e){return this.logger?.warn(`Failed handshake with Streamer.bot`,e),this.options?.onError?.(e instanceof Error?e:Error(`Failed handshake with Streamer.bot`)),await this.disconnect()}try{if(this.options.subscribe===`*`||typeof this.options.subscribe==`object`&&!Array.isArray(this.options.subscribe)&&Object.keys(this.options.subscribe??{}).length)this.logger?.debug(`Subscribing to initial events from options:`,this.options.subscribe),await this.subscribe(this.options.subscribe);else if(typeof this.options.subscribe==`string`||Array.isArray(this.options.subscribe)){this.logger?.debug(`Subscribing to initial events from options:`,this.options.subscribe);let e=await this.getSubscriptionsFromEventStrings(this.options.subscribe);this.logger?.debug(`Parsed subscriptions from options:`,e),e&&await this.subscribe(e)}if(this.listeners.length){let e=await this.getSubscriptionsFromListeners();await this.subscribe(e)}this.logger?.verbose(`Subscribed to requested events`,this.subscriptions,this.listeners)}catch(e){this.logger?.warn(`Error subscribing to requested events`,e)}}onClose(e){this._connectController.abort();try{(e.type===`error`||!e.wasClean)&&this.options.onError&&this?.options?.onError(Error(d(e))),this?.options?.onDisconnect?.()}catch(e){this.logger?.warn(`Error invoking user-provided onDisconnect handler`,e)}if(this._explicitlyClosed||!this.options.autoReconnect)return this.logger?.debug(`Cleaning up...`),this.cleanup();this._retried+=1,typeof this.options.retries==`number`&&(this.options.retries<0||this._retried{if(!(this.socket&&this.socket.readyState!==this.socket.CLOSED)){this.logger?.debug(`Reconnecting... (attempt ${this._retried})`);try{await this.connect(1e4)}catch(e){this._retried&&this.logger?.warn(`Failed to reconnect (attempt ${this._retried-1})`,e)}}},Math.min(3e4,this._retried*1e3))):(this.logger?.debug(`Auto-reconnect limit reached. Cleaning up...`),this.cleanup())}async onMessage(e){if(!e.data||typeof e.data!=`string`){this.logger?.debug(`Unknown message received`,e,e.data);return}let t;try{t=JSON.parse(e.data)}catch(t){this.logger?.warn(`Invalid JSON payload received`,e.data,t);return}this.logger?.verbose(`RECV`,t);try{this.options.onData&&this?.options?.onData(t)}catch(e){this.logger?.warn(`Error occurred within user-provided onData callback`,e)}if(t?.event?.source&&t?.event?.type){for(let e of this.listeners)if(e.events?.length&&e.events.find(e=>e===`*`||e===`${t?.event?.source}.${t?.event?.type}`||e.split(`.`,2)?.[1]===`*`&&e.split(`.`,2)?.[0]===t?.event?.source))try{e.callback(t)}catch(t){this.logger?.warn(`Error occurred within user-provided event callback (${e.events})`,t)}}}onError(e){this.logger?.debug(`WebSocket onError`,e),this.socket&&this.socket.readyState!==this.socket.OPEN&&this._connectController.abort();try{this?.options?.onError?.(Error(`WebSocket Error`))}catch(e){this.logger?.warn(`Error occurred within user-provided onError callback`,e)}}cleanup(){this.socket&&(this.socket.onopen=null,this.socket.onclose=null,this.socket.onerror=null,this.socket.onmessage=null,this.socket=void 0),this.listeners=[],this._retried=0,this._connectController.abort(),this._reconnectTimeout&&clearTimeout(this._reconnectTimeout)}send(e){this.socket?.send(JSON.stringify(e))}async request(e,t=``,n=1e4){if(!this.socket||this.socket.readyState!==this.socket.OPEN)throw Error(`WebSocket is not connected`);t||(t=u());let r=new AbortController,i=r.signal;this._connectController.signal.addEventListener(`abort`,()=>r.abort(),{once:!0,signal:i});let a=await f(new Promise((n,r)=>{this.socket&&(this.socket.addEventListener(`message`,i=>{if(!(`data`in i)||!i.data||typeof i.data!=`string`){this.logger?.debug(`Unknown message received`,i.data);return}try{let r=JSON.parse(i?.data);if(r?.id===t)return this.logger?.verbose(`RECV :: ${e.request}`,r),n(r)}catch(e){this.logger?.warn(`Invalid JSON payload received`,i.data),r(e)}},{signal:i}),this.logger?.verbose(`SEND :: ${e.request}`,{...e,id:t}),this.send({...e,id:t}))}),{timeout:n,message:`Request timed out`,controller:r,signal:this._connectController.signal});if(a?.status===`ok`){try{this.options.onData&&this?.options?.onData(a)}catch(e){this.logger?.warn(`Error invoking onData handler`,e)}return{event:{source:`Request`,type:e.request??`Unknown`},...a}}throw Error(`Request failed`)}async on(e,t){try{if(!e)return;let n={events:[e],callback:t};if(this.listeners.push(n),this.ready){let e=await this.getSubscriptionsFromListeners([n]);await this.subscribe(e)}this.logger?.debug(`LISTEN :: ${e}`)}catch(t){this.logger?.warn(`LISTEN :: ${e} :: Unexpected error while adding listener`,t)}}async updateSupportedEvents(){if(this.ready)try{let e=await this.getEvents();if(e.status!==`ok`||!e.events)throw Error(e.status);this.supportedEvents=e.events,this.logger?.debug(`Successfully fetched supported event types for Streamer.bot v${this.version}`)}catch(e){this.logger?.warn(`Failed to fetch supported events from Streamer.bot, falling back to stored events type.`,e),this.supportedEvents=o}}async getSupportedEvents(){return this.supportedEvents||(this.logger?.warn(`Supported event types not yet initialized, fetching from Streamer.bot instance...`),await this.updateSupportedEvents()),this.supportedEvents??o}getEventsFromListeners(e){return(e??this.listeners).reduce((e,t)=>(t.events.forEach(n=>{e[n]||(e[n]=[]),e[n].push(t.callback)}),e),{})}async getSubscriptionsFromListeners(e){let t=this.getEventsFromListeners(e);return this.getSubscriptionsFromEventStrings(Object.keys(t))}async getSubscriptionsFromEventStrings(e){let t={};typeof e==`string`&&(e=[e]);for(let n of e){let e=await this.parseEventString(n);if(e)for(let n of e){let{source:e,eventTypes:r}=n;t[e]=[...new Set([...t[e]??[],...r])]}}return t}async parseEventString(e){let t=await this.getSupportedEvents();if(!e||typeof e!=`string`){this.logger?.warn(`Invalid event subscription requested "${e}"`);return}if(e===`*`)return Object.keys(t).map(e=>{let n=e;return{source:n,eventTypes:t[n]??[]}});{let[n,r]=e.split(`.`,2);if(!n||!r||!(n in t)){this.logger?.warn(`Invalid event subscription requested "${e}"`);return}let i=n,a=r;if(a)return[{source:i,eventTypes:a===`*`?t[i]:[a]}];this.logger?.warn(`Invalid event type requested "${e}"`);return}}async subscribe(e){let t=await this.getSupportedEvents();e===`*`&&(e=t);for(let n in e){if(!n||n===`err`)continue;if(!(n in t)){this.logger?.warn(`Attempted to subscribe to empty or unknown event source: "${n}"`,Object.keys(e));continue}let r=n,i=e[r]??[];if(i&&i.length){let e=new Set([...this.subscriptions[r]??[],...i]);this.subscriptions[r]=[...e]}}return Object.keys(this.subscriptions).length===0?(this.logger?.warn(`No valid events to subscribe to. Please provide valid event sources and types.`),{id:`invalid`,status:`error`,error:`No valid events to subscribe to`}):await this.request({request:`Subscribe`,events:this.subscriptions})}async unsubscribe(e){let t=await this.getSupportedEvents();e===`*`&&(e=t);for(let n in e){if(n===void 0||!Object.keys(t).includes(n))continue;let r=n,i=e[r];if(i&&i.length)for(let e of i)e&&this.subscriptions[r]?.filter&&(this.subscriptions[r]=this.subscriptions[r])?.filter(t=>e!==t)}return await this.request({request:`UnSubscribe`,events:e})}async getEvents(){return await this.request({request:`GetEvents`})}async getActions(){return await this.request({request:`GetActions`})}async doAction(e,t,n){if(n?.customEventResponse)return this.doActionWithCustomEventResponse(e,t);let r,i;return typeof e==`string`?r=e:(r=e.id,i=e.name),await this.request({request:`DoAction`,action:{id:r,name:i},args:t})}async doActionWithCustomEventResponse(e,t={},n=1e4){if(!this.socket||this.socket.readyState!==this.socket.OPEN)throw Error(`WebSocket is not connected`);let{responseId:r,promise:i,controller:a}=h({timeout:n,addEventListener:(e,t)=>this.on(e,t),removeEventListener:e=>{this.listeners=this.listeners.filter(t=>!e(t))}}),o={...t,sbClientResponse:r};try{let t=await this.doAction(e,o),n=await i;return{...t,customEventResponseArgs:n}}catch(e){throw a.signal.aborted||a.abort(),e}}async getBroadcaster(){return await this.request({request:`GetBroadcaster`})}async getMonitoredYouTubeBroadcasts(){return await this.request({request:`GetMonitoredYouTubeBroadcasts`})}async getCredits(){return await this.request({request:`GetCredits`})}async testCredits(){return await this.request({request:`TestCredits`})}async clearCredits(){return await this.request({request:`ClearCredits`})}async getInfo(){return await this.request({request:`GetInfo`})}async getActiveViewers(){return await this.request({request:`GetActiveViewers`})}async executeCodeTrigger(e,t,n){return n?.customEventResponse?this.executeCodeTriggerWithCustomEventResponse(e,t):await this.request({request:`ExecuteCodeTrigger`,triggerName:e,args:t})}async executeCodeTriggerWithCustomEventResponse(e,t={},n=1e4){if(!this.socket||this.socket.readyState!==this.socket.OPEN)throw Error(`WebSocket is not connected`);let{responseId:r,promise:i,controller:a}=h({timeout:n,addEventListener:(e,t)=>this.on(e,t),removeEventListener:e=>{this.listeners=this.listeners.filter(t=>!e(t))}}),o={...t,sbClientResponse:r};try{let t=await this.executeCodeTrigger(e,o),n=await i;return{...t,customEventResponseArgs:n}}catch(e){throw a.signal.aborted||a.abort(),e}}async getCodeTriggers(){return await this.request({request:`GetCodeTriggers`})}async getCommands(){return await this.request({request:`GetCommands`})}async getEmotes(e){switch(e){case`twitch`:return await this.request({request:`TwitchGetEmotes`});case`youtube`:return await this.request({request:`YouTubeGetEmotes`});default:throw Error(`Invalid platform`)}}async getGlobals(e=!0){return await this.request({request:`GetGlobals`,persisted:e})}async getGlobal(e,t=!0){let n=await this.request({request:`GetGlobal`,variable:e,persisted:t});return n.status===`ok`?n.variables[e]?{id:n.id,status:n.status,variable:n.variables[e]}:{status:`error`,error:`Variable not found`}:n}async getUserGlobals(e,t=null,n=!0){let r={twitch:`TwitchGetUserGlobals`,youtube:`YouTubeGetUserGlobals`,trovo:`TrovoGetUserGlobals`,kick:`KickGetUserGlobals`}[e];if(!r)throw Error(`Invalid platform`);return await this.request({request:r,variable:t,persisted:n})}async getUserGlobal(e,t,n=null,r=!0){let i={twitch:`TwitchGetUserGlobal`,youtube:`YouTubeGetUserGlobal`,trovo:`TrovoGetUserGlobal`,kick:`KickGetUserGlobal`}[e];if(!i)throw Error(`Invalid platform`);let a=await this.request({request:i,userId:t,variable:n||null,persisted:r});if(a.status===`ok`&&t&&n){let e=a.variables.find(e=>e.name===n);return e?{id:a.id,status:a.status,variable:e}:{status:`error`,error:`Variable not found`}}return a}async sendMessage(e,t,{bot:n=!1,internal:r=!0,...i}={}){if(!this._authenticated)return{status:`error`,error:`Authentication required`};let a={platform:e,message:t,bot:n,internal:r};return[`twitch`,`kick`].includes(e)&&i.replyId&&Object.assign(a,{replyId:i.replyId}),[`youtube`].includes(e)&&i.broadcastId&&Object.assign(a,{broadcastId:i.broadcastId}),await this.request({...a,request:`SendMessage`})}async getUserPronouns(e,t){return await this.request({request:`GetUserPronouns`,platform:e,userLogin:t})}};return Object.assign(globalThis,{StreamerbotClient:_}),e.Client=_,e})({}); \ No newline at end of file diff --git a/widgets/shared/scripts/streamerbot.js b/widgets/shared/scripts/streamerbot.js index 82f1e0e..4eb6007 100644 --- a/widgets/shared/scripts/streamerbot.js +++ b/widgets/shared/scripts/streamerbot.js @@ -1,13 +1,20 @@ // STREAMER.BOT SETTINGS -const client = new StreamerbotClient({ - host: configs.streamerBotSettings.host, - port: configs.streamerBotSettings.port, - endpoint: configs.streamerBotSettings.endpoint, - onConnect: onConnect, - onDisconnect: onDisconnect, - onError: onError, -}); +let client; + +try { + client = new StreamerbotClient({ + host: configs.streamerBotSettings.host, + port: configs.streamerBotSettings.port, + endpoint: configs.streamerBotSettings.endpoint, + password: configs.streamerBotSettings.password ?? "", + onConnect, + onDisconnect, + onError, + }); +} catch (err) { + onError(err); +} client.on("General.Custom", (data) => onCustom(data)); if (configs.userColorSettings.autoUserColor) { @@ -56,9 +63,11 @@ function onDisconnect() { } function onError(err) { - showConnectionError( - "Connection Failed: " + (err?.message || "Unknown error"), - ); + console.error("Streamer.bot connection error:", err); + + showConnectionError( + "Connection Failed: " + (err?.message || "Unknown error"), + ); } function showConnectionError(message) {