Skip to content

Commit c07c1e1

Browse files
committed
Update source integrations and VPZone config
1 parent dac2c15 commit c07c1e1

30 files changed

Lines changed: 1194 additions & 475 deletions

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ If you need more context on how Electron wiring differs from the extension boots
3636
## Message Contracts
3737

3838
- Every outbound event follows the canonical structure referenced in `docs/event-reference.html`. Required fields (`platform`, `type`, `chatname`, `chatmessage`, etc.) must stay intact.
39-
- Additional, non-standard details belong inside the top-level `meta` object. Populate `meta` with plain JSON values (no functions/classes) so downstream consumers can safely parse them.
39+
- Donation-style chat rows should use `hasDonation` and optional `donoValue`. Do not set `event: "donation"` just because a normal chat/tip row has a donation value; `event` changes routing/filter behavior and should only be used for true normalized events.
40+
- Use existing payload fields first. Only populate `meta` when there is additional structured data that downstream consumers actually need and no existing field handles it well.
41+
- Additional, non-standard details that truly need to be transmitted belong inside the top-level `meta` object. Populate `meta` with plain JSON values (no functions/classes) so downstream consumers can safely parse them.
4042
- Avoid emitting ad-hoc top-level keys—coordinate changes through the event reference doc before shipping.
4143

4244
## Custom Overlay Notes

actions/styles.css

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
/* style.css */
22

3-
/* Import Fonts from Google Fonts */
4-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap');
5-
6-
/* --- CSS Custom Properties (Variables) --- */
7-
:root {
3+
/* --- CSS Custom Properties (Variables) --- */
4+
:root {
85
/* Base colors */
96
--primary-color: #8A2BE2; /* Twitch Purple - vibrant and recognizable */
107
--primary-light: #A450F0;
@@ -69,8 +66,8 @@
6966
--state-bg: rgba(147, 51, 234, 0.2); /* State node background */
7067

7168
/* Fonts */
72-
--font-primary: 'Inter', sans-serif;
73-
--font-monospace: 'Fira Code', monospace;
69+
--font-primary: "Segoe UI", Tahoma, Arial, sans-serif;
70+
--font-monospace: "Cascadia Code", Consolas, "Courier New", monospace;
7471
}
7572

7673
/* --- Base & Global Styles --- */

background.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ async function restoreBrowserHandle(key, mode = "readwrite") {
470470

471471
var urlParams = new URLSearchParams(window.location.search);
472472
var devmode = urlParams.has("devmode") || false;
473+
var forceNinjaSDKFromUrl = urlParams.has("sdk") && !/^(0|false|off|no)$/i.test(urlParams.get("sdk") || "1");
473474
var lastUseNinjaSDK = undefined; // track effective SDK usage across settings loads
474475
// initial default (may be recalculated when settings load)
475476
useNinjaSDK = false;
@@ -1595,7 +1596,7 @@ function loadSettings(item, resave = false) {
15951596
// Recompute effective SDK usage on settings load
15961597
try {
15971598
const settingsSDK = settings?.sdk?.setting === true || settings?.sdk === true || settings?.usesdk?.setting === true;
1598-
const effective = !!settingsSDK;
1599+
const effective = !!(forceNinjaSDKFromUrl || settingsSDK);
15991600
if (lastUseNinjaSDK === undefined) {
16001601
lastUseNinjaSDK = effective;
16011602
} else if (effective !== lastUseNinjaSDK) {
@@ -2432,11 +2433,16 @@ async function bringBackgroundPageToFrontForPicker() {
24322433

24332434
const backgroundTabs = await new Promise((resolve, reject) => {
24342435
try {
2435-
chrome.tabs.query({ url: backgroundUrl }, function (tabs) {
2436+
chrome.tabs.query({}, function (tabs) {
24362437
if (chrome.runtime.lastError) {
24372438
reject(chrome.runtime.lastError);
24382439
} else {
2439-
resolve(tabs || []);
2440+
resolve(
2441+
(tabs || []).filter(tab => {
2442+
const tabUrl = tab && (tab.url || tab.pendingUrl || "");
2443+
return tabUrl === backgroundUrl || tabUrl.startsWith(backgroundUrl + "?") || tabUrl.startsWith(backgroundUrl + "#");
2444+
})
2445+
);
24402446
}
24412447
});
24422448
} catch (err) {
@@ -12307,15 +12313,15 @@ async function initTransport(roomStreamID, pass = false) {
1230712313
// Re-evaluate effective SDK flag each init, based on flexible truthy parsing
1230812314
try {
1230912315
const raw = settings && (settings.sdk !== undefined ? settings.sdk : settings.usesdk);
12310-
let flag = false;
12316+
let flag = !!forceNinjaSDKFromUrl;
1231112317
if (typeof raw === "boolean") {
12312-
flag = raw;
12318+
flag = flag || raw;
1231312319
} else if (raw && typeof raw === "object") {
1231412320
// supports { setting: true/"true"/1 }
1231512321
const v = raw.setting;
12316-
flag = v === true || v === 1 || (typeof v === "string" && /^(1|true|on|yes)$/i.test(v));
12322+
flag = flag || v === true || v === 1 || (typeof v === "string" && /^(1|true|on|yes)$/i.test(v));
1231712323
} else if (typeof raw === "string") {
12318-
flag = /^(1|true|on|yes)$/i.test(raw);
12324+
flag = flag || /^(1|true|on|yes)$/i.test(raw);
1231912325
} else if (raw === 1) {
1232012326
flag = true;
1232112327
}
@@ -12539,9 +12545,9 @@ async function ensureNinjaSDKLoaded() {
1253912545

1254012546
if (typeof window.VDONinjaSDK === "undefined") {
1254112547
if (typeof window.loadScript === "function") {
12542-
await window.loadScript("./thirdparty/vdoninja-sdk.js");
12548+
await window.loadScript("./sources/grabvideo.js").catch(() => window.loadScript("./thirdparty/vdoninja-sdk.js"));
1254312549
} else {
12544-
await dynamicLoadScript("./thirdparty/vdoninja-sdk.js");
12550+
await dynamicLoadScript("./sources/grabvideo.js").catch(() => dynamicLoadScript("./thirdparty/vdoninja-sdk.js"));
1254512551
}
1254612552
}
1254712553
if (typeof window.NinjaBridge === "undefined") {

bot.html

Lines changed: 83 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,13 +1098,51 @@
10981098
}
10991099

11001100

1101-
function stripHtmlFunction(html) {
1102-
let tmp = document.createElement("DIV");
1103-
tmp.innerHTML = html;
1104-
return tmp.textContent || tmp.innerText || "";
1105-
}
1106-
1107-
function upscaleImages(data) {
1101+
function removeHtmlTagsFromPlainTextField(value) {
1102+
return String(value || "")
1103+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "")
1104+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "")
1105+
.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, "")
1106+
.replace(/<template\b[^>]*>[\s\S]*?<\/template>/gi, "")
1107+
.replace(/<[^>]*>/g, "");
1108+
}
1109+
1110+
function stripHtmlFunction(html) {
1111+
html = String(html == null ? "" : html);
1112+
try {
1113+
if (typeof DOMParser !== "undefined") {
1114+
let doc = new DOMParser().parseFromString(html, "text/html");
1115+
if (doc && doc.body) {
1116+
doc.body.querySelectorAll("script,style,noscript,template").forEach(function (node) {
1117+
node.remove();
1118+
});
1119+
return removeHtmlTagsFromPlainTextField(doc.body.textContent || "");
1120+
}
1121+
}
1122+
} catch (e) {}
1123+
let tmp = document.createElement("DIV");
1124+
tmp.innerHTML = html;
1125+
return removeHtmlTagsFromPlainTextField(tmp.textContent || tmp.innerText || "");
1126+
}
1127+
1128+
function sanitizePlainTextAlertFields(data) {
1129+
if (!data) {
1130+
return;
1131+
}
1132+
["hasDonation", "donation", "membership", "hasMembership", "subtitle", "title"].forEach(function (field) {
1133+
if (typeof data[field] === "string") {
1134+
data[field] = stripHtmlFunction(data[field]);
1135+
}
1136+
});
1137+
}
1138+
1139+
function plainTextToHtml(value) {
1140+
let span = document.createElement("span");
1141+
span.textContent = value == null ? "" : String(value);
1142+
return span.innerHTML;
1143+
}
1144+
1145+
function upscaleImages(data) {
11081146
// for third party APIs
11091147
let chatimg = "";
11101148
if (data.type && (data.type == "twitch") && data.chatname && !data.chatimg) {
@@ -2162,33 +2200,35 @@
21622200
}
21632201
} else if (!data.chatname) {
21642202
data.chatname = "";
2165-
} else {
2166-
data.chatname = data.chatname.toString();
2167-
}
2168-
2169-
var memebershipHTML = "";
2203+
} else {
2204+
data.chatname = data.chatname.toString();
2205+
}
2206+
2207+
sanitizePlainTextAlertFields(data);
2208+
2209+
var memebershipHTML = "";
21702210

21712211
if (data.hasMembership) {
21722212
if (data.hasMembership === true) {
21732213
data.hasMembership = getTranslation("membership", "Membership");
2174-
}
2175-
// doesn't have subtitles; shouldn't at least.
2176-
data.membership = data.hasMembership.toString();
2177-
memebershipHTML = data.membership;
2178-
} else if (data.membership) {
2214+
}
2215+
// doesn't have subtitles; shouldn't at least.
2216+
data.membership = data.hasMembership.toString();
2217+
memebershipHTML = plainTextToHtml(data.membership);
2218+
} else if (data.membership) {
21792219
if (data.membership === true) {
21802220
data.membership = getTranslation("membership", "Membership");
21812221
} else {
21822222
data.membership = data.membership.toString();
21832223
}
21842224

2185-
if (data.subtitle) {
2186-
memebershipHTML = '<div class="donation membership"><div class="donation-title">' + data.membership.toUpperCase() + "</div><div class='subtitle'>" + data.subtitle + "</div></div>";
2187-
} else {
2188-
memebershipHTML = '<div class="donation membership"><div class="donation-title">' + data.membership.toUpperCase() + "</div></div>";
2189-
}
2190-
} else if (data.subtitle) {
2191-
memebershipHTML = '<div class="donation membership"><div class="subtitle">' + data.subtitle + "</div></div>";
2225+
if (data.subtitle) {
2226+
memebershipHTML = '<div class="donation membership"><div class="donation-title">' + plainTextToHtml(data.membership.toUpperCase()) + "</div><div class='subtitle'>" + plainTextToHtml(data.subtitle) + "</div></div>";
2227+
} else {
2228+
memebershipHTML = '<div class="donation membership"><div class="donation-title">' + plainTextToHtml(data.membership.toUpperCase()) + "</div></div>";
2229+
}
2230+
} else if (data.subtitle) {
2231+
memebershipHTML = '<div class="donation membership"><div class="subtitle">' + plainTextToHtml(data.subtitle) + "</div></div>";
21922232
} else {
21932233
data.membership = "";
21942234
}
@@ -2203,28 +2243,28 @@
22032243
if (data.hasDonation) {
22042244
if (data.type === "twitch") {
22052245
let donationTitle = getTranslation("cheers", "CHEERS");
2206-
if ("title" in data) {
2207-
donationTitle = data.title;
2208-
}
2209-
donationHTML = '<div class="donation"><div class="donation-title">'+ donationTitle +'</div><div class="subtitle">' + data.hasDonation + "</div></div>";
2246+
if ("title" in data) {
2247+
donationTitle = data.title;
2248+
}
2249+
donationHTML = '<div class="donation"><div class="donation-title">'+ plainTextToHtml(donationTitle) +'</div><div class="subtitle">' + plainTextToHtml(data.hasDonation) + "</div></div>";
22102250

22112251
} else if ((data.type == "youtube") || (data.type == "youtubeshorts")) {
22122252
let donationTitle = getTranslation("donation", "DONATION");
22132253
if ("title" in data) {
22142254
donationTitle = data.title;
22152255
}
2216-
if (memebershipHTML) {
2217-
donationHTML = '<div class="donation">' + data.hasDonation + "</div>";
2218-
} else {
2219-
donationHTML = '<div class="donation"><div class="donation-title">'+ donationTitle +'</div><div class="subtitle">' + data.hasDonation + "</div></div>";
2220-
}
2221-
2222-
} else if (data.title) {
2223-
donationHTML = '<div class="donation"><div class="donation-title">'+ data.title +'</div><div class="subtitle">' + data.hasDonation + "</div></div>";
2224-
2225-
} else {
2226-
donationHTML = '<div class="donation">' + data.hasDonation + "</div>";
2227-
}
2256+
if (memebershipHTML) {
2257+
donationHTML = '<div class="donation">' + plainTextToHtml(data.hasDonation) + "</div>";
2258+
} else {
2259+
donationHTML = '<div class="donation"><div class="donation-title">'+ plainTextToHtml(donationTitle) +'</div><div class="subtitle">' + plainTextToHtml(data.hasDonation) + "</div></div>";
2260+
}
2261+
2262+
} else if (data.title) {
2263+
donationHTML = '<div class="donation"><div class="donation-title">'+ plainTextToHtml(data.title) +'</div><div class="subtitle">' + plainTextToHtml(data.hasDonation) + "</div></div>";
2264+
2265+
} else {
2266+
donationHTML = '<div class="donation">' + plainTextToHtml(data.hasDonation) + "</div>";
2267+
}
22282268
} else {
22292269
data.hasDonation = "";
22302270
}
@@ -2303,11 +2343,12 @@
23032343
if (chatbadges) {
23042344
chatbadges = '<div class="hl-badges" >' + chatbadges + "</div>";
23052345
}
2306-
2346+
23072347
var renderChatMessageAsText = !!(data.textonly && !largeImage);
2348+
// Trusted relay HTML: chatname/chatmessage are sanitized upstream in background.js before rendering here.
23082349
var renderedChatMessage = renderChatMessageAsText ? "" : data.chatmessage;
23092350

2310-
if (stacking) {
2351+
if (stacking) {
23112352
try {
23122353
document.getElementById("message").id = "";
23132354
document.getElementById("nameDIV").id = "";

chathistory.js

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,42 @@ function initDatabase() {
107107
});
108108
}
109109

110+
function removeHtmlTagsFromPlainText(value) {
111+
return String(value || '')
112+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
113+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '')
114+
.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, '')
115+
.replace(/<template\b[^>]*>[\s\S]*?<\/template>/gi, '')
116+
.replace(/<[^>]*>/g, '');
117+
}
118+
119+
function stripHtmlToPlainText(value) {
120+
const text = String(value == null ? '' : value);
121+
try {
122+
if (typeof DOMParser !== 'undefined') {
123+
const doc = new DOMParser().parseFromString(text, 'text/html');
124+
if (doc && doc.body) {
125+
doc.body.querySelectorAll('script,style,noscript,template').forEach(node => node.remove());
126+
return removeHtmlTagsFromPlainText(doc.body.textContent || '');
127+
}
128+
}
129+
} catch (e) {}
130+
return removeHtmlTagsFromPlainText(text);
131+
}
132+
133+
function escapeHtml(value) {
134+
return String(value == null ? '' : value)
135+
.replace(/&/g, '&amp;')
136+
.replace(/</g, '&lt;')
137+
.replace(/>/g, '&gt;')
138+
.replace(/"/g, '&quot;')
139+
.replace(/'/g, '&#039;');
140+
}
141+
142+
function safePlainText(value) {
143+
return escapeHtml(stripHtmlToPlainText(value));
144+
}
145+
110146
function debounceFilters() {
111147
if (filterDebounceHandle) {
112148
clearTimeout(filterDebounceHandle);
@@ -366,6 +402,7 @@ function renderMessages() {
366402
return;
367403
}
368404

405+
// Stored relay HTML comes from the background.js path where chat fields are sanitized before persistence.
369406
const html = messages.map(message => `
370407
<div class="message-wrapper" id="message-${message.id}">
371408
<div class="message">
@@ -378,8 +415,8 @@ function renderMessages() {
378415
</div>
379416
<p class="message-text">${message.chatmessage || ''}</p>
380417
${message.contentimg ? `<img src="${message.contentimg}" alt="Content" class="content-image" data-error-hide="self">` : ''}
381-
${message.hasDonation ? `<p class="donation">Donation: ${message.hasDonation}</p>` : ''}
382-
${(message.membership || message.hasMembership) ? `<p class="membership">Membership: ${message.membership || message.hasMembership}</p>` : ''}
418+
${message.hasDonation ? `<p class="donation">Donation: ${safePlainText(message.hasDonation)}</p>` : ''}
419+
${(message.membership || message.hasMembership) ? `<p class="membership">Membership: ${safePlainText(message.membership || message.hasMembership)}</p>` : ''}
383420
</div>
384421
</div>
385422
</div>

0 commit comments

Comments
 (0)