diff --git a/Code.cs b/Code.cs
index ef9bbd2..c6a2d66 100644
--- a/Code.cs
+++ b/Code.cs
@@ -464,7 +464,7 @@ public class TaskOperations
}
int newIndex = taskData[key].Tasks.Count - 1;
- broadcast(new { mode = "refresh", task = taskName, completed = completed, focused = focused }, null);
+ broadcast(new { mode = "add", task = taskName, completed = completed, focused = focused }, null);
return new Response<(int, string)>(true, (newIndex, taskName), null);
}
@@ -481,7 +481,7 @@ public class TaskOperations
string oldName = userTasks[index].Name;
userTasks[index].Name = newTask;
SaveIntoTasks(userTasks);
- broadcast(new { mode = "refresh", index = index, task = newTask }, null);
+ broadcast(new { mode = "edit", index = index, task = newTask }, null);
return new Response<(string, string)>(true, (oldName, newTask), null);
}
@@ -502,7 +502,7 @@ public class TaskOperations
UnfocusAll(tasks);
tasks[n].Focused = true;
SaveIntoTasks(tasks);
- broadcast(new { mode = "refresh", index = n }, null);
+ broadcast(new { mode = "focus", index = n }, null);
return new Response<(int, string)>(true, (n, tasks[n].Name), null);
}
else if (indexByName > -1)
@@ -513,7 +513,7 @@ public class TaskOperations
UnfocusAll(tasks);
tasks[n].Focused = true;
SaveIntoTasks(tasks);
- broadcast(new { mode = "refresh", index = n }, null);
+ broadcast(new { mode = "focus", index = n }, null);
return new Response<(int, string)>(true, (n, tasks[n].Name), null);
}
else
@@ -521,7 +521,7 @@ public class TaskOperations
var response = AddTask(rawInput, false, true);
if (response.Success)
{
- broadcast(new { mode = "refresh", index = response.Data.Item1 }, null);
+ broadcast(new { mode = "focus", index = response.Data.Item1 }, null);
return new Response<(int, string)>(true, response.Data, null);
}
@@ -846,9 +846,9 @@ public class CPHInline
string completedTaskName = userTasks[focusedTaskIndex].Name;
userTasks[focusedTaskIndex].Completed = true;
userTasks[focusedTaskIndex].Focused = false;
- Broadcast(new { mode = "refresh", index = focusedTaskIndex }, null);
operations.SaveIntoTasks(userTasks);
SaveTasks();
+ Broadcast(new { mode = "done", index = focusedTaskIndex }, null);
Respond(BotResponses.NextSuccess(completedTaskName, focusResponse.Data.Item1 + 1, focusResponse.Data.Item2));
return true;
}
@@ -997,12 +997,15 @@ public class CPHInline
foreach (int i in taskIndices.OrderByDescending(n => n))
{
userTasks.RemoveAt(i);
- Broadcast(new { mode = "refresh", index = i }, null);
}
operations.SaveIntoTasks(userTasks);
operations.Cleanup(true);
SaveTasks();
+ foreach (int i in taskIndices.OrderByDescending(n => n))
+ {
+ Broadcast(new { mode = "remove", index = i }, null);
+ }
Respond(MessageBuilder.BuildRemoveMessage(tasksRemoved, tasksFailedToRemove, allTasks));
return true;
}
@@ -1024,7 +1027,7 @@ public class CPHInline
operations.RemoveUser(key);
SaveTasks();
Respond(BotResponses.AdminDeleteSuccess);
- Broadcast(new { mode = "refresh", id = key }, null);
+ Broadcast(new { mode = "admindelete", id = key }, null);
return true;
}
@@ -1081,12 +1084,15 @@ public class CPHInline
{
userTasks[i].Completed = true;
userTasks[i].Focused = false;
- Broadcast(new { mode = "refresh", index = i }, null);
}
IncrementDoneCount(taskIndices.Count);
operations.SaveIntoTasks(userTasks);
SaveTasks();
+ foreach (int i in taskIndices)
+ {
+ Broadcast(new { mode = "done", index = i }, null);
+ }
Respond(MessageBuilder.BuildCompletedMessage(tasksCompleted, tasksFailedToComplete, allTasks));
return true;
}
@@ -1096,7 +1102,7 @@ public class CPHInline
operations.Unfocus();
SaveTasks();
Respond(BotResponses.Unfocused);
- Broadcast(new { mode = "refresh" }, null);
+ Broadcast(new { mode = "unfocus" }, null);
return true;
}
@@ -1144,11 +1150,14 @@ public class CPHInline
{
userTasks[i].Completed = false;
userTasks[i].Focused = false;
- Broadcast(new { mode = "refresh", index = i }, null);
}
operations.SaveIntoTasks(userTasks);
SaveTasks();
+ foreach (int i in taskIndices)
+ {
+ Broadcast(new { mode = "undone", index = i }, null);
+ }
Respond(MessageBuilder.BuildUndoneMessage(tasksCompleted, tasksFailedToComplete));
return true;
}
@@ -1169,7 +1178,7 @@ public class CPHInline
operations.ClearUserCompletedTasks(key);
operations.Cleanup(false);
SaveTasks();
- Broadcast(new { mode = "refresh" }, null);
+ Broadcast(new { mode = "clearmydone" }, null);
Respond(BotResponses.ClearMyDone);
return true;
}
@@ -1179,7 +1188,7 @@ public class CPHInline
operations.ClearCompletedTasks();
operations.Cleanup(false);
SaveTasks();
- Broadcast(new { mode = "refresh" }, null);
+ Broadcast(new { mode = "cleardone" }, null);
Respond(BotResponses.ClearDone);
return true;
}
@@ -1189,7 +1198,7 @@ public class CPHInline
operations.FilterToStreamers(GetStreamerUsernames());
operations.Cleanup(false);
SaveTasks();
- Broadcast(new { mode = "refresh" }, null);
+ Broadcast(new { mode = "clearns" }, null);
Respond(BotResponses.ClearNotStreamer);
return true;
}
diff --git a/widgets/horizontal/README.md b/widgets/horizontal/README.md
index f49b08a..c8eb6d8 100644
--- a/widgets/horizontal/README.md
+++ b/widgets/horizontal/README.md
@@ -6,7 +6,7 @@
Websocket server running is required for browser source to work.
-> 
+> 
1. Streamer.Bot -> Servers/Clients -> Websocket Server
2. Auto Start: `ON`; Click on `Start Server`
diff --git a/widgets/horizontal/index.html b/widgets/horizontal/index.html
index 6fc7ee0..318dfcf 100644
--- a/widgets/horizontal/index.html
+++ b/widgets/horizontal/index.html
@@ -3,11 +3,11 @@
-
+
-
-
-
+
+
+
diff --git a/widgets/horizontal/scripts/emotes.js b/widgets/horizontal/scripts/emotes.js
deleted file mode 100644
index c6eafb9..0000000
--- a/widgets/horizontal/scripts/emotes.js
+++ /dev/null
@@ -1,273 +0,0 @@
-"use strict";
-
-class EmoteManager {
- #emotes = new Map();
- #channelName = "";
- #channelId = "";
- #providers = ["7tv", "bttv", "ffz"];
- #size = "1x";
- #loaded = false;
- #cacheTTL = 24 * 60 * 60 * 1000;
- #CACHE_KEY = "emote-cache-v1";
- #pattern = null;
- #patternSize = 0;
-
- constructor(config = {}) {
- this.#channelName = config.channelName;
- this.#channelId = config.channelId;
- if (config.providers) this.#providers = config.providers;
- if (config.size) this.#size = config.size;
- }
-
- get loaded() {
- return this.#loaded;
- }
-
- get emoteCount() {
- return this.#emotes.size;
- }
-
- get providers() {
- return [...this.#providers];
- }
-
- getEmote(name) {
- return this.#emotes.get(name) || null;
- }
-
- #buildPattern() {
- if (this.#emotes.size === 0) {
- this.#pattern = null;
- this.#patternSize = 0;
- return;
- }
-
- if (this.#pattern && this.#emotes.size === this.#patternSize) return;
-
- const sorted = [...this.#emotes.keys()].sort(
- (a, b) => b.length - a.length,
- );
- const escaped = sorted.map((n) =>
- n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
- );
- this.#pattern = new RegExp(
- `(? {
- const emote = this.#emotes.get(match);
- if (!emote) return match;
- console.log(emote);
- return `
`;
- });
- }
-
- async init() {
- const cached = this.#loadCache();
- if (cached) {
- this.#emotes = new Map(Object.entries(cached.emotes));
- this.#loaded = true;
- this.#patternSize = 0;
- return;
- }
-
- await this.#fetchAll();
- this.#loaded = true;
- this.#patternSize = 0;
- this.#saveCache();
- }
-
- async forceRefresh() {
- this.#emotes.clear();
- this.#patternSize = 0;
- await this.#fetchAll();
- this.#loaded = true;
- this.#patternSize = 0;
- this.#saveCache();
- }
-
- async #fetchAll() {
- const fetchers = [];
- if (this.#providers.includes("7tv")) fetchers.push(this.#fetch7TV());
- if (this.#providers.includes("bttv")) fetchers.push(this.#fetchBTTV());
- if (this.#providers.includes("ffz")) fetchers.push(this.#fetchFFZ());
- await Promise.allSettled(fetchers);
- }
-
- async #fetch7TV() {
- try {
- const globalResp = await fetch(
- "https://7tv.io/v3/emote-sets/global",
- );
- if (!globalResp.ok) return;
- const globalData = await globalResp.json();
- if (globalData.emotes) {
- for (const emote of globalData.emotes) {
- this.#add7TVEmote(emote);
- }
- }
-
- if (this.#channelId) {
- const userResp = await fetch(
- `https://7tv.io/v3/users/twitch/${this.#channelId}`,
- );
- if (!userResp.ok) return;
- const userData = await userResp.json();
- if (userData.emote_set?.emotes) {
- for (const emote of userData.emote_set.emotes) {
- this.#add7TVEmote(emote);
- }
- }
- }
- } catch (e) {
- console.warn("7TV emote fetch failed:", e.message);
- }
- }
-
- #add7TVEmote(emote) {
- const entry = emote.data || emote;
- const id = entry.id || emote.id;
- const name = entry.name || emote.name;
- if (!id || !name) return;
- const nameLower = name;
- const host = entry.host.url;
- this.#emotes.set(nameLower, {
- url: `https:${host}/${this.#size}.webp`,
- provider: "7tv",
- animated: entry.animated || false,
- });
- }
-
- async #fetchBTTV() {
- try {
- const globalResp = await fetch(
- "https://api.betterttv.net/3/cached/emotes/global",
- );
- if (!globalResp.ok) return;
- const globalData = await globalResp.json();
- if (Array.isArray(globalData)) {
- for (const emote of globalData) {
- this.#emotes.set(emote.code, {
- url: `https://cdn.betterttv.net/emote/${emote.id}/${this.#size}`,
- provider: "bttv",
- animated: emote.imageType === "gif",
- });
- }
- }
-
- if (this.#channelId) {
- const userResp = await fetch(
- `https://api.betterttv.net/3/cached/users/twitch/${this.#channelId}`,
- );
- if (!userResp.ok) return;
- const userData = await userResp.json();
- const allEmotes = [
- ...(userData.channelEmotes || []),
- ...(userData.sharedEmotes || []),
- ];
- for (const emote of allEmotes) {
- this.#emotes.set(emote.code, {
- url: `https://cdn.betterttv.net/emote/${emote.id}/${this.#size}`,
- provider: "bttv",
- animated: emote.imageType === "gif",
- });
- }
- }
- } catch (e) {
- console.warn("BTTV emote fetch failed:", e.message);
- }
- }
-
- async #fetchFFZ() {
- try {
- const globalResp = await fetch(
- "https://api.frankerfacez.com/v1/set/global",
- );
- if (!globalResp.ok) return;
- const globalData = await globalResp.json();
- if (globalData.sets) {
- for (const setId in globalData.sets) {
- const set = globalData.sets[setId];
- for (const emote of set.emoticons || []) {
- if (emote.modifier) continue;
- this.#emotes.set(emote.name, {
- url: emote.urls[this.#toFFZSize(this.#size)],
- provider: "ffz",
- animated: false,
- });
- }
- }
- }
-
- const ffzId = this.#channelName || this.#channelId;
- if (ffzId) {
- const roomUrl = this.#channelId
- ? `https://api.frankerfacez.com/v1/room/id/${this.#channelId}`
- : `https://api.frankerfacez.com/v1/room/${this.#channelName}`;
- const roomResp = await fetch(roomUrl);
- if (!roomResp.ok) return;
- const roomData = await roomResp.json();
- if (roomData.sets) {
- for (const setId in roomData.sets) {
- const set = roomData.sets[setId];
- for (const emote of set.emoticons || []) {
- if (emote.modifier) continue;
- this.#emotes.set(emote.name, {
- url: emote.urls[this.#toFFZSize(this.#size)],
- provider: "ffz",
- animated: false,
- });
- }
- }
- }
- }
- } catch (e) {
- console.warn("FFZ emote fetch failed:", e.message);
- }
- }
-
- #toFFZSize(size) {
- switch (size) {
- case "3x":
- case "4x":
- return "4";
- case "2x":
- return "2";
- default:
- return "1";
- }
- }
-
- #saveCache() {
- try {
- const cache = {
- timestamp: Date.now(),
- channelId: this.#channelId,
- emotes: Object.fromEntries(this.#emotes),
- };
- localStorage.setItem(this.#CACHE_KEY, JSON.stringify(cache));
- } catch (e) {}
- }
-
- #loadCache() {
- try {
- const raw = localStorage.getItem(this.#CACHE_KEY);
- if (!raw) return null;
- const cache = JSON.parse(raw);
- if (cache.channelId !== this.#channelId) return null;
- if (Date.now() - cache.timestamp > this.#cacheTTL) return null;
- return cache;
- } catch (e) {
- return null;
- }
- }
-}
diff --git a/widgets/horizontal/scripts/streamerbot.js b/widgets/horizontal/scripts/streamerbot.js
deleted file mode 100644
index 51a2b9c..0000000
--- a/widgets/horizontal/scripts/streamerbot.js
+++ /dev/null
@@ -1,200 +0,0 @@
-// 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,
-});
-
-function getAllLocalstorage() {
- const allLocalStorage = {};
-
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- allLocalStorage[key] = localStorage.getItem(key);
- }
-
- return JSON.stringify(allLocalStorage);
-}
-
-client.on("General.Custom", (data) => onCustom(data));
-if (configs.userColorSettings.autoUserColor) {
- client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
- client.on("Kick.ChatMessage", (data) => onChatMessage(data));
-}
-
-let taskList;
-let userColors = {};
-
-function parseHexColor(hex) {
- if (typeof hex !== "string") return null;
- const s = hex.trim();
- const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
- if (!m) return null;
- let h = m[1].toLowerCase();
- if (h.length === 3) {
- h = h
- .split("")
- .map((c) => c + c)
- .join("");
- }
- const r = parseInt(h.slice(0, 2), 16);
- const g = parseInt(h.slice(2, 4), 16);
- const b = parseInt(h.slice(4, 6), 16);
- return { r, g, b };
-}
-
-function relativeLuminance({ r, g, b }) {
- const toLinear = (v) => {
- const s = v / 255;
- return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
- };
- const R = toLinear(r);
- const G = toLinear(g);
- const B = toLinear(b);
- return 0.2126 * R + 0.7152 * G + 0.0722 * B;
-}
-
-async function onChatMessage(data) {
- let userColor = data.data.user.color; // hex colour #FF69B4
-
- if (userColor != undefined || userColor != "undefined") {
- // update localstorage
- // get id: platform-userID
- let userId = data.data.user.id;
- let key = `twitch-${userId}`;
-
- localStorage.setItem(`${key}-color`, userColor);
- userColors[`${key}-color`] = userColor;
- }
-
- return;
-}
-
-function onDisconnect() {
- showConnectionError("Connection Failed: Unable to connect to Streamer.bot");
-}
-
-function onError(err) {
- showConnectionError(
- "Connection Failed: " + (err?.message || "Unknown error"),
- );
-}
-
-function showConnectionError(message) {
- // Remove existing popup if any
- const existing = document.getElementById("connection-error");
- if (existing) existing.remove();
-
- const popup = document.createElement("div");
- popup.id = "connection-error";
- popup.textContent = message;
- Object.assign(popup.style, {
- position: "fixed",
- top: "20px",
- left: "50%",
- transform: "translateX(-50%)",
- background: "#e53935",
- color: "#fff",
- padding: "12px 24px",
- borderRadius: "8px",
- fontFamily: "'Fredoka', sans-serif",
- fontSize: "1.1rem",
- fontWeight: "700",
- zIndex: "9999",
- boxShadow: "0 4px 12px rgba(0,0,0,0.4)",
- textAlign: "center",
- });
- document.body.appendChild(popup);
-
- // Auto-dismiss after 5 seconds
- setTimeout(() => {
- popup.animate([{ opacity: 1 }, { opacity: 0 }], {
- duration: 300,
- fill: "forwards",
- }).onfinish = () => popup.remove();
- }, 5000);
-}
-
-async function refresh() {
- const response = await client.getGlobal("rython-task-bot", true);
- console.log(response);
-
- if (response.status !== "ok" || !response.variable?.value) return;
-
- const users = JSON.parse(response.variable.value);
- const sections = transformToSections(users);
-
- taskList.load(sections);
-}
-
-// LOAD TASK LIST
-async function onConnect() {
- taskList = new TaskList(".task-panel");
-
- if (configs.emoteSettings.enabled) {
- let broadcaster = await client.getBroadcaster();
-
- console.log(broadcaster);
-
- let broadcasterName =
- broadcaster.platforms.twitch?.broadcastUser ??
- broadcaster.platforms.youtube?.broadcastUser ??
- broadcaster.platforms.kick?.broadcastUser;
-
- let broadcasterId =
- broadcaster.platforms.twitch?.broadcastUserId ??
- broadcaster.platforms.youtube?.broadcastUserId ??
- broadcaster.platforms.kick?.broadcastUserId;
-
- window.emoteManager = new EmoteManager({
- channelName: broadcasterName,
- channelId: broadcasterId,
- providers: configs.emoteSettings.providers,
- size: configs.emoteSettings.size,
- });
-
- window.emoteManager.init().then(() => {
- if (taskList) {
- taskList.load(taskList.getData());
- }
- });
- }
-
- refresh();
-}
-
-function transformToSections(users) {
- return Object.entries(users).map(([userId, userData]) => ({
- id: userId,
- title: userData.Username,
- tasks: userData.Tasks.map((task) => ({
- text: task.Name,
- done: task.Completed,
- focused: task.Focused,
- })),
- }));
-}
-
-// Update task list action by action
-function onCustom(payload) {
- const data = payload.data;
- if (!data.source && data.source != "rython-task-bot") {
- return;
- }
- if (!taskList) return;
- const body = data.body;
- const id = data.id;
- const username = data.username;
-
- switch (body.mode) {
- case "refresh":
- refresh();
- break;
- default:
- break;
- }
-}
diff --git a/widgets/horizontal/scripts/tasklist-view.js b/widgets/horizontal/scripts/tasklist-view.js
deleted file mode 100644
index edff58c..0000000
--- a/widgets/horizontal/scripts/tasklist-view.js
+++ /dev/null
@@ -1,591 +0,0 @@
-// Task list view
-
-class TaskList {
- #data = [];
- #firstRender = true;
- #pendingRemovals = 0;
-
- #scroll = {
- offset: 0,
- contentW: 0,
- viewportW: 0,
- speed: 50,
- raf: null,
- lastT: 0,
- active: false,
- wantStop: false,
- };
-
- #els = {};
-
- #ANIM_DURATION = 400;
- #ANIM_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
-
- #parseHexColor(hex) {
- if (typeof hex !== "string") return null;
- const s = hex.trim();
- const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
- if (!m) return null;
- let h = m[1].toLowerCase();
- if (h.length === 3) {
- h = h
- .split("")
- .map((c) => c + c)
- .join("");
- }
- const r = parseInt(h.slice(0, 2), 16);
- const g = parseInt(h.slice(2, 4), 16);
- const b = parseInt(h.slice(4, 6), 16);
- return { r, g, b };
- }
-
- #relativeLuminance({ r, g, b }) {
- const toLinear = (v) => {
- const s = v / 255;
- return s <= 0.04045
- ? s / 12.92
- : Math.pow((s + 0.055) / 1.055, 2.4);
- };
- const R = toLinear(r);
- const G = toLinear(g);
- const B = toLinear(b);
- return 0.2126 * R + 0.7152 * G + 0.0722 * B;
- }
-
- constructor(container) {
- const el =
- typeof container === "string"
- ? document.querySelector(container)
- : container;
-
- this.#els = {
- track: el.querySelector(".scroll-track"),
- content: el.querySelector(".scroll-content"),
- viewport: el.querySelector(".scroll-viewport"),
- };
- }
-
- // ── public API ─────────────────────────────────────
-
- load(sections) {
- this.#data = structuredClone(sections);
- this.#render();
- return this;
- }
-
- addSection(section) {
- this.#data.push(structuredClone(section));
- this.#render();
- return this;
- }
-
- removeSection(id) {
- const i = this.#data.findIndex((s) => s.id === id);
- if (i === -1) return this;
- this.#data.splice(i, 1);
- this.#render();
- return this;
- }
-
- updateSection(id, updater) {
- const section = this.#data.find((s) => s.id === id);
- if (!section) return this;
- updater(section);
- this.#render();
- return this;
- }
-
- addTask(sectionId, task, sectionTitle) {
- if (!this.#data.find((s) => s.id === sectionId)) {
- this.addSection({
- id: sectionId,
- title: sectionTitle || sectionId,
- tasks: [],
- });
- }
-
- return this.updateSection(sectionId, (s) => {
- s.tasks.push(structuredClone(task));
- });
- }
-
- removeTask(sectionId, taskIndex) {
- this.updateSection(sectionId, (s) => {
- s.tasks.splice(taskIndex, 1);
- });
- this.#data = this.#data.filter((s) => s.tasks.length > 0);
- this.#render();
- return this;
- }
-
- editTask(sectionId, taskIndex, newText, userColor) {
- return this.updateSection(sectionId, (s) => {
- if (s.tasks[taskIndex]) {
- s.tasks[taskIndex].text = newText;
- }
- });
- }
-
- doneTask(sectionId, taskIndex) {
- return this.updateSection(sectionId, (s) => {
- if (s.tasks[taskIndex]) {
- s.tasks[taskIndex].done = true;
- s.tasks[taskIndex].focused = false;
- }
- });
- }
-
- undoneTask(sectionId, taskIndex) {
- return this.updateSection(sectionId, (s) => {
- if (s.tasks[taskIndex]) {
- s.tasks[taskIndex].done = false;
- }
- });
- }
-
- focusTask(sectionId, taskIndex) {
- // unfocus any previously focused task
- this.unfocusTask(sectionId);
-
- return this.updateSection(sectionId, (s) => {
- if (s.tasks[taskIndex]) {
- s.tasks[taskIndex].focused = true;
- }
- });
- }
-
- unfocusTask(sectionId) {
- return this.updateSection(sectionId, (s) => {
- for (let i = 0; i < s.tasks.length; i++) {
- s.tasks[i].focused = false;
- }
- });
- }
-
- clearmydone(sectionId) {
- this.updateSection(sectionId, (s) => {
- s.tasks = s.tasks.filter((t) => !t.done);
- });
- this.#data = this.#data.filter((s) => s.tasks.length > 0);
- this.#render();
- return this;
- }
-
- cleardone() {
- this.#data.forEach((s) => {
- s.tasks = s.tasks.filter((t) => !t.done);
- });
- this.#data = this.#data.filter((s) => s.tasks.length > 0);
- this.#render();
- return this;
- }
-
- getData() {
- return structuredClone(this.#data);
- }
-
- get sectionCount() {
- return this.#data.length;
- }
-
- get taskCount() {
- return this.#data.reduce((sum, s) => sum + s.tasks.length, 0);
- }
-
- get doneCount() {
- return this.#data.reduce(
- (sum, s) => sum + s.tasks.filter((t) => t.done).length,
- 0,
- );
- }
-
- destroy() {
- this.#forceStop();
- this.#els.content.innerHTML = "";
- this.#data = [];
- this.#firstRender = true;
- }
-
- // ── render ─────────────────────────────────────────
-
- #render() {
- if (this.#firstRender) {
- this.#data.forEach((s) =>
- this.#els.content.appendChild(this.#createSectionEl(s)),
- );
- this.#firstRender = false;
- } else {
- this.#patchContainer(this.#els.content, this.#data, true);
- }
-
- if (this.#pendingRemovals === 0) {
- this.#syncScroll();
- }
- }
-
- // ── animations ─────────────────────────────────────
-
- #animateIn(el) {
- el.style.overflow = "hidden";
-
- const w = el.scrollWidth;
- el.style.width = "0px";
- el.style.opacity = "0";
-
- requestAnimationFrame(() => {
- const anim = el.animate(
- [
- {
- width: "0px",
- opacity: 0,
- transform: "translateX(-8px)",
- },
- {
- width: w + "px",
- opacity: 1,
- transform: "translateX(0)",
- },
- ],
- {
- duration: this.#ANIM_DURATION,
- easing: this.#ANIM_EASING,
- fill: "forwards",
- },
- );
-
- anim.onfinish = () => {
- el.style.width = "";
- el.style.opacity = "";
- el.style.overflow = "";
- el.style.transform = "";
- anim.cancel();
- this.#syncScroll();
- };
- });
- }
-
- #animateRemove(el) {
- this.#pendingRemovals++;
- const w = el.scrollWidth;
-
- const anim = el.animate(
- [
- { width: w + "px", opacity: 1, transform: "translateX(0)" },
- { width: "0px", opacity: 0, transform: "translateX(-8px)" },
- ],
- {
- duration: this.#ANIM_DURATION,
- easing: this.#ANIM_EASING,
- fill: "forwards",
- },
- );
-
- anim.onfinish = () => {
- el.remove();
- this.#pendingRemovals--;
-
- if (this.#pendingRemovals === 0) {
- this.#syncScroll();
- }
- };
- }
-
- #animateStrikethrough(el) {
- el.animate(
- [
- { opacity: 0.5, transform: "scale(0.98)" },
- { opacity: 1, transform: "scale(1)" },
- ],
- { duration: 250, easing: "ease-out" },
- );
- }
-
- // ── dom helpers ────────────────────────────────────
-
- #parseTaskText(text) {
- if (window.emoteManager && window.emoteManager.loaded) {
- return window.emoteManager.parseText(text);
- }
- return text;
- }
-
- #createTaskEl(task, index) {
- const div = document.createElement("div");
- div.className =
- "task" +
- (task.done ? " done" : "") +
- (task.focused ? " focused" : "");
- div.dataset.text = task.text;
- const numberSpan = document.createElement("span");
- numberSpan.className = "task-number";
- numberSpan.textContent = `${index + 1}.`;
-
- const textSpan = document.createElement("span");
- textSpan.className = "task-text";
- textSpan.innerHTML = this.#parseTaskText(task.text);
-
- div.replaceChildren(numberSpan, textSpan);
- return div;
- }
-
- #createSectionEl(section) {
- const div = document.createElement("div");
- div.className = "section";
- div.dataset.key = section.id;
- if (section.title) {
- const t = document.createElement("div");
- t.className = "section-title";
- t.textContent = section.title;
- let color =
- section.color ?? localStorage.getItem(`${section.id}-color`);
- if (
- configs.userColorSettings.autoUserColor &&
- color != undefined &&
- color != "undefined" &&
- color != null
- ) {
- t.style.setProperty("--user-color", color);
- t.classList.add("has-user-color");
- }
-
- div.appendChild(t);
- }
- section.tasks.forEach((task, i) =>
- div.appendChild(this.#createTaskEl(task, i)),
- );
- return div;
- }
-
- // ── patching ───────────────────────────────────────
-
- #patchTasks(sectionEl, tasks, animate) {
- const existing = [...sectionEl.querySelectorAll(":scope > .task")];
-
- tasks.forEach((task, i) => {
- if (i < existing.length) {
- const el = existing[i];
- const wantClass =
- "task" +
- (task.done ? " done" : "") +
- (task.focused ? " focused" : "");
- if (el.className !== wantClass) {
- el.className = wantClass;
- if (animate) this.#animateStrikethrough(el);
- }
- if (el.dataset.text !== task.text) {
- el.dataset.text = task.text;
- const textEl =
- el.querySelector(".task-text") ||
- el.querySelector("span:last-child");
- textEl.innerHTML = this.#parseTaskText(task.text);
- }
- el.querySelector(".task-number").textContent = `${i + 1}.`;
-
- // Update color if changed
- if (task.color != undefined && task.color != "undefined") {
- el.style.setProperty("--user-color", task.color);
- el.classList.add("has-user-color");
- } else {
- el.style.removeProperty("--user-color");
- el.classList.remove("has-user-color");
- }
- } else {
- const newEl = this.#createTaskEl(task, i);
- sectionEl.appendChild(newEl);
- if (animate) this.#animateIn(newEl);
- }
- });
-
- for (let i = existing.length - 1; i >= tasks.length; i--) {
- if (animate) {
- this.#animateRemove(existing[i]);
- } else {
- existing[i].remove();
- }
- }
- }
-
- #patchContainer(container, sections, animate) {
- const oldMap = new Map();
- container.querySelectorAll(":scope > .section").forEach((el) => {
- oldMap.set(el.dataset.key, el);
- });
-
- let cursor = container.firstElementChild;
-
- sections.forEach((section) => {
- const key = section.id;
- const existing = oldMap.get(key);
-
- if (existing) {
- this.#patchTasks(existing, section.tasks, animate);
- if (existing !== cursor) {
- container.insertBefore(existing, cursor);
- } else {
- cursor = cursor.nextElementSibling;
- }
- oldMap.delete(key);
- } else {
- const newEl = this.#createSectionEl(section);
- container.insertBefore(newEl, cursor);
- if (animate) this.#animateIn(newEl);
- }
- });
-
- oldMap.forEach((el) => {
- if (animate) {
- this.#animateRemove(el);
- } else {
- el.remove();
- }
- });
- }
-
- // ── scroll ─────────────────────────────────────────
-
- #tick = (now) => {
- const s = this.#scroll;
- const dt = (now - s.lastT) / 1000;
- s.lastT = now;
-
- if (dt < 0.2) {
- s.contentW = this.#els.content.scrollWidth;
- s.offset += s.speed * dt;
-
- if (s.contentW > 0 && s.offset >= s.contentW) {
- s.offset -= s.contentW;
-
- if (s.wantStop) {
- s.wantStop = false;
- this.#forceStop();
- return;
- }
- }
-
- this.#els.track.style.transform = `translateX(${-s.offset}px)`;
- }
-
- s.raf = requestAnimationFrame(this.#tick);
- };
-
- #startScroll() {
- const s = this.#scroll;
- s.wantStop = false;
- if (s.active) return;
- s.active = true;
- s.lastT = performance.now();
- s.raf = requestAnimationFrame(this.#tick);
- }
-
- #requestStop() {
- if (!this.#scroll.active) return;
- this.#scroll.wantStop = true;
- }
-
- #forceStop() {
- const s = this.#scroll;
- s.active = false;
- s.wantStop = false;
- cancelAnimationFrame(s.raf);
- s.raf = null;
- s.offset = 0;
- this.#els.track.style.transform = "";
-
- const clone = this.#els.track.querySelector(".scroll-clone");
- if (clone) clone.remove();
- }
-
- #syncScroll() {
- const s = this.#scroll;
- s.contentW = this.#els.content.scrollWidth;
- s.viewportW = this.#els.viewport.clientWidth;
- const needs = s.contentW > s.viewportW;
-
- let clone = this.#els.track.querySelector(".scroll-clone");
-
- if (needs) {
- if (!clone) {
- clone = this.#els.content.cloneNode(true);
- clone.id = "";
- clone.classList.add("scroll-clone");
- this.#els.track.appendChild(clone);
- } else {
- this.#patchContainer(clone, this.#data, false);
- }
- if (s.offset >= s.contentW) {
- s.offset %= s.contentW;
- }
- this.#startScroll();
- } else {
- if (s.active) {
- if (clone) this.#patchContainer(clone, this.#data, false);
- this.#requestStop();
- } else {
- if (clone) clone.remove();
- }
- }
- }
-}
-
-// ── usage ──────────────────────────────────────────────
-
-// const list2 = new TaskList(".task-panel");
-
-// list2.load([
-// {
-// title: "someone",
-// tasks: [
-// { text: "dinner" },
-// { text: "watch bad bunny halftime show" },
-// { text: "tie tf down", done: true },
-// { text: "boy dinner", done: true },
-// { text: "3 hour SOOP" },
-// ],
-// },
-// {
-// title: "Cape_Codder",
-// tasks: [
-// { text: "Part 2", done: true },
-// { text: "Ch 1-2", done: true },
-// ],
-// },
-// {
-// title: "sunflawer",
-// tasks: [
-// { text: "fix walls from home office" },
-// { text: "sand walls zzzsdkjhfsk", done: true },
-// { text: "meal prep" },
-// { text: "buy wrist brace maybe" },
-// ],
-// },
-// {
-// title: "extra_user1",
-// tasks: [
-// { text: "go grocery shopping" },
-// { text: "clean the kitchen", done: true },
-// { text: "respond to emails" },
-// ],
-// },
-// ]);
-
-// demo
-
-// setTimeout(() => {
-// list2.addTask("someone2", { text: "new task just dropped" });
-// }, 1000);
-
-// setTimeout(() => {
-// list2.;
-// }, 3000);
-
-// setTimeout(() => {
-// list.toggleTask("sunflawer", 0);
-// }, 10000);
-
-// setTimeout(() => {
-// list.removeSection("Cape_Codder");
-// }, 12000);
-
-// setTimeout(() => {
-// list.removeTask("extra_user1", 1);
-// }, 14000);
diff --git a/widgets/horizontal/images/websocket.png b/widgets/shared/images/websocket.png
similarity index 100%
rename from widgets/horizontal/images/websocket.png
rename to widgets/shared/images/websocket.png
diff --git a/widgets/horizontal/lib/streamerbot-client.js b/widgets/shared/lib/streamerbot-client.js
similarity index 100%
rename from widgets/horizontal/lib/streamerbot-client.js
rename to widgets/shared/lib/streamerbot-client.js
diff --git a/widgets/vertical/scripts/emotes.js b/widgets/shared/scripts/emotes.js
similarity index 100%
rename from widgets/vertical/scripts/emotes.js
rename to widgets/shared/scripts/emotes.js
diff --git a/widgets/vertical/scripts/streamerbot.js b/widgets/shared/scripts/streamerbot.js
similarity index 67%
rename from widgets/vertical/scripts/streamerbot.js
rename to widgets/shared/scripts/streamerbot.js
index 6aa5b00..2c09e78 100644
--- a/widgets/vertical/scripts/streamerbot.js
+++ b/widgets/shared/scripts/streamerbot.js
@@ -9,17 +9,6 @@ const client = new StreamerbotClient({
onError: onError,
});
-function getAllLocalstorage() {
- const allLocalStorage = {};
-
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- allLocalStorage[key] = localStorage.getItem(key);
- }
-
- return JSON.stringify(allLocalStorage);
-}
-
client.on("General.Custom", (data) => onCustom(data));
if (configs.userColorSettings.autoUserColor) {
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
@@ -29,49 +18,35 @@ if (configs.userColorSettings.autoUserColor) {
let taskList;
let userColors = {};
-function parseHexColor(hex) {
- if (typeof hex !== "string") return null;
- const s = hex.trim();
- const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
- if (!m) return null;
- let h = m[1].toLowerCase();
- if (h.length === 3) {
- h = h
- .split("")
- .map((c) => c + c)
- .join("");
- }
- const r = parseInt(h.slice(0, 2), 16);
- const g = parseInt(h.slice(2, 4), 16);
- const b = parseInt(h.slice(4, 6), 16);
- return { r, g, b };
+function getUserColor(sectionId) {
+ return (
+ userColors[`${sectionId}-color`] ??
+ localStorage.getItem(`${sectionId}-color`)
+ );
}
-function relativeLuminance({ r, g, b }) {
- const toLinear = (v) => {
- const s = v / 255;
- return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
- };
- const R = toLinear(r);
- const G = toLinear(g);
- const B = toLinear(b);
- return 0.2126 * R + 0.7152 * G + 0.0722 * B;
+function taskColorFromSettings(sectionId) {
+ const usernameColor = getUserColor(sectionId);
+ if (
+ configs.userColorSettings.autoUserColor &&
+ usernameColor != undefined &&
+ usernameColor != null
+ ) {
+ return usernameColor;
+ }
+ return undefined;
}
async function onChatMessage(data) {
- let userColor = data.data.user.color; // hex colour #FF69B4
+ const userColor = data.data.user.color;
- if (userColor != undefined || userColor != "undefined") {
- // update localstorage
- // get id: platform-userID
- let userId = data.data.user.id;
- let key = `twitch-${userId}`;
+ if (userColor != undefined && userColor != "undefined") {
+ const userId = data.data.user.id;
+ const key = `twitch-${userId}`;
localStorage.setItem(`${key}-color`, userColor);
userColors[`${key}-color`] = userColor;
}
-
- return;
}
function onDisconnect() {
@@ -85,7 +60,6 @@ function onError(err) {
}
function showConnectionError(message) {
- // Remove existing popup if any
const existing = document.getElementById("connection-error");
if (existing) existing.remove();
@@ -110,7 +84,6 @@ function showConnectionError(message) {
});
document.body.appendChild(popup);
- // Auto-dismiss after 5 seconds
setTimeout(() => {
popup.animate([{ opacity: 1 }, { opacity: 0 }], {
duration: 300,
@@ -121,7 +94,6 @@ function showConnectionError(message) {
async function refresh() {
const response = await client.getGlobal("rython-task-bot", true);
- console.log(response);
if (response.status !== "ok" || !response.variable?.value) return;
@@ -131,19 +103,18 @@ async function refresh() {
taskList.load(sections);
}
-// LOAD TASK LIST
async function onConnect() {
taskList = new TaskList(".task-panel");
if (configs.emoteSettings.enabled) {
- let broadcaster = await client.getBroadcaster();
+ const broadcaster = await client.getBroadcaster();
- let broadcasterName =
+ const broadcasterName =
broadcaster.platforms.twitch?.broadcastUser ??
broadcaster.platforms.youtube?.broadcastUser ??
broadcaster.platforms.kick?.broadcastUser;
- let broadcasterId =
+ const broadcasterId =
broadcaster.platforms.twitch?.broadcastUserId ??
broadcaster.platforms.youtube?.broadcastUserId ??
broadcaster.platforms.kick?.broadcastUserId;
@@ -177,19 +148,58 @@ function transformToSections(users) {
}));
}
-// Update task list action by action
function onCustom(payload) {
const data = payload.data;
- if (!data.source && data.source != "rython-task-bot") {
+ if (data.source !== "rython-task-bot") {
return;
}
if (!taskList) return;
+
const body = data.body;
const id = data.id;
const username = data.username;
switch (body.mode) {
- case "refresh":
+ case "add": {
+ const taskPayload = {
+ text: body.task,
+ done: body.completed,
+ focused: body.focused,
+ };
+ const color = taskColorFromSettings(id);
+ if (color) taskPayload.color = color;
+ taskList.addTask(id, taskPayload, username);
+ break;
+ }
+ case "focus":
+ taskList.focusTask(id, body.index);
+ break;
+ case "edit":
+ taskList.editTask(id, body.index, body.task, taskColorFromSettings(id));
+ break;
+ case "remove":
+ taskList.removeTask(id, body.index);
+ break;
+ case "done":
+ taskList.doneTask(id, body.index);
+ break;
+ case "undone":
+ taskList.undoneTask(id, body.index);
+ break;
+ case "unfocus":
+ taskList.unfocusTask(id);
+ break;
+ case "admindelete":
+ taskList.removeSection(body.id);
+ break;
+ case "clearmydone":
+ taskList.clearmydone(id);
+ break;
+ case "cleardone":
+ taskList.cleardone();
+ break;
+ case "clearall":
+ case "clearns":
refresh();
break;
default:
diff --git a/widgets/vertical/scripts/tasklist-view.js b/widgets/shared/scripts/tasklist-view.js
similarity index 100%
rename from widgets/vertical/scripts/tasklist-view.js
rename to widgets/shared/scripts/tasklist-view.js
diff --git a/widgets/streamer-only/README.md b/widgets/streamer-only/README.md
index f49b08a..c8eb6d8 100644
--- a/widgets/streamer-only/README.md
+++ b/widgets/streamer-only/README.md
@@ -6,7 +6,7 @@
Websocket server running is required for browser source to work.
-> 
+> 
1. Streamer.Bot -> Servers/Clients -> Websocket Server
2. Auto Start: `ON`; Click on `Start Server`
diff --git a/widgets/streamer-only/images/websocket.png b/widgets/streamer-only/images/websocket.png
deleted file mode 100644
index 9bfc514..0000000
Binary files a/widgets/streamer-only/images/websocket.png and /dev/null differ
diff --git a/widgets/streamer-only/index.html b/widgets/streamer-only/index.html
index 6fc7ee0..033836f 100644
--- a/widgets/streamer-only/index.html
+++ b/widgets/streamer-only/index.html
@@ -3,11 +3,11 @@
-
+
-
+
-
+
diff --git a/widgets/streamer-only/lib/streamerbot-client.js b/widgets/streamer-only/lib/streamerbot-client.js
deleted file mode 100644
index 1f1ad44..0000000
--- a/widgets/streamer-only/lib/streamerbot-client.js
+++ /dev/null
@@ -1 +0,0 @@
-"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(_);})();
diff --git a/widgets/streamer-only/scripts/emotes.js b/widgets/streamer-only/scripts/emotes.js
deleted file mode 100644
index c6eafb9..0000000
--- a/widgets/streamer-only/scripts/emotes.js
+++ /dev/null
@@ -1,273 +0,0 @@
-"use strict";
-
-class EmoteManager {
- #emotes = new Map();
- #channelName = "";
- #channelId = "";
- #providers = ["7tv", "bttv", "ffz"];
- #size = "1x";
- #loaded = false;
- #cacheTTL = 24 * 60 * 60 * 1000;
- #CACHE_KEY = "emote-cache-v1";
- #pattern = null;
- #patternSize = 0;
-
- constructor(config = {}) {
- this.#channelName = config.channelName;
- this.#channelId = config.channelId;
- if (config.providers) this.#providers = config.providers;
- if (config.size) this.#size = config.size;
- }
-
- get loaded() {
- return this.#loaded;
- }
-
- get emoteCount() {
- return this.#emotes.size;
- }
-
- get providers() {
- return [...this.#providers];
- }
-
- getEmote(name) {
- return this.#emotes.get(name) || null;
- }
-
- #buildPattern() {
- if (this.#emotes.size === 0) {
- this.#pattern = null;
- this.#patternSize = 0;
- return;
- }
-
- if (this.#pattern && this.#emotes.size === this.#patternSize) return;
-
- const sorted = [...this.#emotes.keys()].sort(
- (a, b) => b.length - a.length,
- );
- const escaped = sorted.map((n) =>
- n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
- );
- this.#pattern = new RegExp(
- `(? {
- const emote = this.#emotes.get(match);
- if (!emote) return match;
- console.log(emote);
- return `
`;
- });
- }
-
- async init() {
- const cached = this.#loadCache();
- if (cached) {
- this.#emotes = new Map(Object.entries(cached.emotes));
- this.#loaded = true;
- this.#patternSize = 0;
- return;
- }
-
- await this.#fetchAll();
- this.#loaded = true;
- this.#patternSize = 0;
- this.#saveCache();
- }
-
- async forceRefresh() {
- this.#emotes.clear();
- this.#patternSize = 0;
- await this.#fetchAll();
- this.#loaded = true;
- this.#patternSize = 0;
- this.#saveCache();
- }
-
- async #fetchAll() {
- const fetchers = [];
- if (this.#providers.includes("7tv")) fetchers.push(this.#fetch7TV());
- if (this.#providers.includes("bttv")) fetchers.push(this.#fetchBTTV());
- if (this.#providers.includes("ffz")) fetchers.push(this.#fetchFFZ());
- await Promise.allSettled(fetchers);
- }
-
- async #fetch7TV() {
- try {
- const globalResp = await fetch(
- "https://7tv.io/v3/emote-sets/global",
- );
- if (!globalResp.ok) return;
- const globalData = await globalResp.json();
- if (globalData.emotes) {
- for (const emote of globalData.emotes) {
- this.#add7TVEmote(emote);
- }
- }
-
- if (this.#channelId) {
- const userResp = await fetch(
- `https://7tv.io/v3/users/twitch/${this.#channelId}`,
- );
- if (!userResp.ok) return;
- const userData = await userResp.json();
- if (userData.emote_set?.emotes) {
- for (const emote of userData.emote_set.emotes) {
- this.#add7TVEmote(emote);
- }
- }
- }
- } catch (e) {
- console.warn("7TV emote fetch failed:", e.message);
- }
- }
-
- #add7TVEmote(emote) {
- const entry = emote.data || emote;
- const id = entry.id || emote.id;
- const name = entry.name || emote.name;
- if (!id || !name) return;
- const nameLower = name;
- const host = entry.host.url;
- this.#emotes.set(nameLower, {
- url: `https:${host}/${this.#size}.webp`,
- provider: "7tv",
- animated: entry.animated || false,
- });
- }
-
- async #fetchBTTV() {
- try {
- const globalResp = await fetch(
- "https://api.betterttv.net/3/cached/emotes/global",
- );
- if (!globalResp.ok) return;
- const globalData = await globalResp.json();
- if (Array.isArray(globalData)) {
- for (const emote of globalData) {
- this.#emotes.set(emote.code, {
- url: `https://cdn.betterttv.net/emote/${emote.id}/${this.#size}`,
- provider: "bttv",
- animated: emote.imageType === "gif",
- });
- }
- }
-
- if (this.#channelId) {
- const userResp = await fetch(
- `https://api.betterttv.net/3/cached/users/twitch/${this.#channelId}`,
- );
- if (!userResp.ok) return;
- const userData = await userResp.json();
- const allEmotes = [
- ...(userData.channelEmotes || []),
- ...(userData.sharedEmotes || []),
- ];
- for (const emote of allEmotes) {
- this.#emotes.set(emote.code, {
- url: `https://cdn.betterttv.net/emote/${emote.id}/${this.#size}`,
- provider: "bttv",
- animated: emote.imageType === "gif",
- });
- }
- }
- } catch (e) {
- console.warn("BTTV emote fetch failed:", e.message);
- }
- }
-
- async #fetchFFZ() {
- try {
- const globalResp = await fetch(
- "https://api.frankerfacez.com/v1/set/global",
- );
- if (!globalResp.ok) return;
- const globalData = await globalResp.json();
- if (globalData.sets) {
- for (const setId in globalData.sets) {
- const set = globalData.sets[setId];
- for (const emote of set.emoticons || []) {
- if (emote.modifier) continue;
- this.#emotes.set(emote.name, {
- url: emote.urls[this.#toFFZSize(this.#size)],
- provider: "ffz",
- animated: false,
- });
- }
- }
- }
-
- const ffzId = this.#channelName || this.#channelId;
- if (ffzId) {
- const roomUrl = this.#channelId
- ? `https://api.frankerfacez.com/v1/room/id/${this.#channelId}`
- : `https://api.frankerfacez.com/v1/room/${this.#channelName}`;
- const roomResp = await fetch(roomUrl);
- if (!roomResp.ok) return;
- const roomData = await roomResp.json();
- if (roomData.sets) {
- for (const setId in roomData.sets) {
- const set = roomData.sets[setId];
- for (const emote of set.emoticons || []) {
- if (emote.modifier) continue;
- this.#emotes.set(emote.name, {
- url: emote.urls[this.#toFFZSize(this.#size)],
- provider: "ffz",
- animated: false,
- });
- }
- }
- }
- }
- } catch (e) {
- console.warn("FFZ emote fetch failed:", e.message);
- }
- }
-
- #toFFZSize(size) {
- switch (size) {
- case "3x":
- case "4x":
- return "4";
- case "2x":
- return "2";
- default:
- return "1";
- }
- }
-
- #saveCache() {
- try {
- const cache = {
- timestamp: Date.now(),
- channelId: this.#channelId,
- emotes: Object.fromEntries(this.#emotes),
- };
- localStorage.setItem(this.#CACHE_KEY, JSON.stringify(cache));
- } catch (e) {}
- }
-
- #loadCache() {
- try {
- const raw = localStorage.getItem(this.#CACHE_KEY);
- if (!raw) return null;
- const cache = JSON.parse(raw);
- if (cache.channelId !== this.#channelId) return null;
- if (Date.now() - cache.timestamp > this.#cacheTTL) return null;
- return cache;
- } catch (e) {
- return null;
- }
- }
-}
diff --git a/widgets/streamer-only/scripts/streamerbot.js b/widgets/streamer-only/scripts/streamerbot.js
deleted file mode 100644
index 54bef1f..0000000
--- a/widgets/streamer-only/scripts/streamerbot.js
+++ /dev/null
@@ -1,201 +0,0 @@
-// 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,
-});
-
-function getAllLocalstorage() {
- const allLocalStorage = {};
-
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- allLocalStorage[key] = localStorage.getItem(key);
- }
-
- return JSON.stringify(allLocalStorage);
-}
-
-client.on("General.Custom", (data) => onCustom(data));
-if (configs.userColorSettings.autoUserColor) {
- client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
- client.on("Kick.ChatMessage", (data) => onChatMessage(data));
-}
-
-let taskList;
-let userColors = {};
-
-function parseHexColor(hex) {
- if (typeof hex !== "string") return null;
- const s = hex.trim();
- const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
- if (!m) return null;
- let h = m[1].toLowerCase();
- if (h.length === 3) {
- h = h
- .split("")
- .map((c) => c + c)
- .join("");
- }
- const r = parseInt(h.slice(0, 2), 16);
- const g = parseInt(h.slice(2, 4), 16);
- const b = parseInt(h.slice(4, 6), 16);
- return { r, g, b };
-}
-
-function relativeLuminance({ r, g, b }) {
- const toLinear = (v) => {
- const s = v / 255;
- return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
- };
- const R = toLinear(r);
- const G = toLinear(g);
- const B = toLinear(b);
- return 0.2126 * R + 0.7152 * G + 0.0722 * B;
-}
-
-async function onChatMessage(data) {
- let userColor = data.data.user.color; // hex colour
-
- if (userColor != undefined || userColor != "undefined") {
- // update localstorage
- // get id: platform-userID
- let userId = data.data.user.id;
- let key = `twitch-${userId}`;
-
- localStorage.setItem(`${key}-color`, userColor);
- userColors[`${key}-color`] = userColor;
- }
-
- return;
-}
-
-function onDisconnect() {
- showConnectionError("Connection Failed: Unable to connect to Streamer.bot");
-}
-
-function onError(err) {
- showConnectionError(
- "Connection Failed: " + (err?.message || "Unknown error"),
- );
-}
-
-function showConnectionError(message) {
- // Remove existing popup if any
- const existing = document.getElementById("connection-error");
- if (existing) existing.remove();
-
- const popup = document.createElement("div");
- popup.id = "connection-error";
- popup.textContent = message;
- Object.assign(popup.style, {
- position: "fixed",
- top: "20px",
- left: "50%",
- transform: "translateX(-50%)",
- background: "#e53935",
- color: "#fff",
- padding: "12px 24px",
- borderRadius: "8px",
- fontFamily: "'Fredoka', sans-serif",
- fontSize: "1.1rem",
- fontWeight: "700",
- zIndex: "9999",
- boxShadow: "0 4px 12px rgba(0,0,0,0.4)",
- textAlign: "center",
- });
- document.body.appendChild(popup);
-
- // Auto-dismiss after 5 seconds
- setTimeout(() => {
- popup.animate([{ opacity: 1 }, { opacity: 0 }], {
- duration: 300,
- fill: "forwards",
- }).onfinish = () => popup.remove();
- }, 5000);
-}
-
-async function refresh() {
- const response = await client.getGlobal("rython-task-bot", true);
- console.log(response);
-
- if (response.status !== "ok" || !response.variable?.value) return;
-
- const users = JSON.parse(response.variable.value);
- console.log(users);
- const sections = transformToSections(users);
-
- taskList.load(sections);
-}
-
-// LOAD TASK LIST
-async function onConnect() {
- taskList = new TaskList(".task-panel");
-
- if (configs.emoteSettings.enabled) {
- let broadcaster = await client.getBroadcaster();
-
- console.log(broadcaster);
-
- let broadcasterName =
- broadcaster.platforms.twitch?.broadcastUser ??
- broadcaster.platforms.youtube?.broadcastUser ??
- broadcaster.platforms.kick?.broadcastUser;
-
- let broadcasterId =
- broadcaster.platforms.twitch?.broadcastUserId ??
- broadcaster.platforms.youtube?.broadcastUserId ??
- broadcaster.platforms.kick?.broadcastUserId;
-
- window.emoteManager = new EmoteManager({
- channelName: broadcasterName,
- channelId: broadcasterId,
- providers: configs.emoteSettings.providers,
- size: configs.emoteSettings.size,
- });
-
- window.emoteManager.init().then(() => {
- if (taskList) {
- taskList.load(taskList.getData());
- }
- });
- }
-
- refresh();
-}
-
-function transformToSections(users) {
- return Object.entries(users).map(([userId, userData]) => ({
- id: userId,
- title: userData.Username,
- tasks: userData.Tasks.map((task) => ({
- text: task.Name,
- done: task.Completed,
- focused: task.Focused,
- })),
- }));
-}
-
-// Update task list action by action
-function onCustom(payload) {
- const data = payload.data;
- if (!data.source && data.source != "rython-task-bot") {
- return;
- }
- if (!taskList) return;
- const body = data.body;
- const id = data.id;
- const username = data.username;
-
- switch (body.mode) {
- case "refresh":
- refresh();
- break;
- default:
- break;
- }
-}
diff --git a/widgets/vertical/README.md b/widgets/vertical/README.md
index aec396b..52ab2a8 100644
--- a/widgets/vertical/README.md
+++ b/widgets/vertical/README.md
@@ -6,7 +6,7 @@ Optional widget for [Rython Task Bot v2](https://github.com/liyunze-coding/rytho
Websocket server running is required for browser source to work.
-> 
+> 
1. Streamer.Bot -> Servers/Clients -> Websocket Server
2. Auto Start: `ON`; Click on `Start Server`
diff --git a/widgets/vertical/images/websocket.png b/widgets/vertical/images/websocket.png
deleted file mode 100644
index 9bfc514..0000000
Binary files a/widgets/vertical/images/websocket.png and /dev/null differ
diff --git a/widgets/vertical/index.html b/widgets/vertical/index.html
index 6fc7ee0..318dfcf 100644
--- a/widgets/vertical/index.html
+++ b/widgets/vertical/index.html
@@ -3,11 +3,11 @@
-
+
-
-
-
+
+
+
diff --git a/widgets/vertical/lib/streamerbot-client.js b/widgets/vertical/lib/streamerbot-client.js
deleted file mode 100644
index 1f1ad44..0000000
--- a/widgets/vertical/lib/streamerbot-client.js
+++ /dev/null
@@ -1 +0,0 @@
-"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(_);})();