103 lines
3.8 KiB
JavaScript
103 lines
3.8 KiB
JavaScript
"use strict";
|
|
|
|
const MAX_MESSAGE_BYTES = 64 * 1024;
|
|
const MAX_SELECTED_SOURCES = 250;
|
|
const MAX_SELECTED_EVENTS = 100;
|
|
|
|
function parseClientMessage(data) {
|
|
const body = Buffer.from(data);
|
|
if (body.length > MAX_MESSAGE_BYTES) throw coded("MESSAGE_TOO_LARGE", "Overlay control message is too large.");
|
|
let value;
|
|
try { value = JSON.parse(body.toString("utf8")); } catch { throw coded("INVALID_JSON", "Overlay control message must be valid JSON."); }
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw coded("INVALID_MESSAGE", "Overlay control message must be an object.");
|
|
const type = String(value.type || "").trim();
|
|
if (!["subscribe", "ping", "pong"].includes(type)) throw coded("INVALID_MESSAGE", `Unsupported overlay control message '${type || "empty"}'.`);
|
|
if (type !== "subscribe") return { type };
|
|
return {
|
|
type,
|
|
sources: uniqueStrings(value.sources, MAX_SELECTED_SOURCES, 300),
|
|
events: uniqueStrings(value.events, MAX_SELECTED_EVENTS, 120)
|
|
};
|
|
}
|
|
|
|
function uniqueStrings(value, limit, maxLength) {
|
|
if (!Array.isArray(value)) return [];
|
|
return [...new Set(value.map((entry) => String(entry || "").trim().slice(0, maxLength)).filter(Boolean))].slice(0, limit);
|
|
}
|
|
|
|
function cleanText(value, max = 500) {
|
|
return String(value === undefined || value === null ? "" : value).slice(0, max);
|
|
}
|
|
|
|
function sanitizeChat(message) {
|
|
return {
|
|
id: cleanText(message.id, 256),
|
|
platform: cleanText(message.platform, 24),
|
|
text: cleanText(message.text, 2000),
|
|
timestamp: Number(message.timestamp) || Date.now(),
|
|
channel: {
|
|
id: cleanText(message.channel?.id, 256),
|
|
name: cleanText(message.channel?.name, 256),
|
|
key: cleanText(message.channel?.key, 256)
|
|
},
|
|
author: {
|
|
id: cleanText(message.author?.id, 160),
|
|
name: cleanText(message.author?.name, 160),
|
|
username: cleanText(message.author?.username, 160),
|
|
avatar: safeHttps(message.author?.avatar),
|
|
color: /^#[0-9a-f]{6}$/i.test(message.author?.color || "") ? message.author.color : null,
|
|
badges: (Array.isArray(message.author?.badges) ? message.author.badges : []).slice(0, 12).map((badge) => ({
|
|
label: cleanText(badge?.label, 40),
|
|
image: safeHttps(badge?.image)
|
|
}))
|
|
},
|
|
emotes: (message.platform === "twitch" && Array.isArray(message.emotes) ? message.emotes : []).slice(0, 100).map((emote) => ({
|
|
start: Math.max(0, Number(emote?.start) || 0),
|
|
end: Math.max(0, Number(emote?.end) || 0),
|
|
label: cleanText(emote?.label, 100),
|
|
image: safeHttps(emote?.image)
|
|
})).filter((emote) => emote.image)
|
|
};
|
|
}
|
|
|
|
function sanitizeEvent(event) {
|
|
return {
|
|
id: cleanText(event.id, 256),
|
|
type: cleanText(event.type, 120),
|
|
source: cleanText(event.source, 80),
|
|
occurred_at: Number(event.occurredAt) || Date.now(),
|
|
payload: sanitizeObject(event.payload, 0)
|
|
};
|
|
}
|
|
|
|
function sanitizeObject(value, depth) {
|
|
if (depth > 3 || value === null || value === undefined) return null;
|
|
if (Array.isArray(value)) return value.slice(0, 20).map((item) => sanitizeObject(item, depth + 1));
|
|
if (typeof value === "object") {
|
|
const output = {};
|
|
for (const [key, item] of Object.entries(value).slice(0, 50)) {
|
|
if (/token|secret|authorization|credential|oauth/i.test(key)) continue;
|
|
output[cleanText(key, 80)] = sanitizeObject(item, depth + 1);
|
|
}
|
|
return output;
|
|
}
|
|
if (typeof value === "string") return cleanText(value, 1000);
|
|
return typeof value === "number" || typeof value === "boolean" ? value : null;
|
|
}
|
|
|
|
function safeHttps(value) {
|
|
try {
|
|
const parsed = new URL(String(value || ""));
|
|
return parsed.protocol === "https:" ? parsed.toString() : null;
|
|
} catch { return null; }
|
|
}
|
|
|
|
function coded(code, message) { return Object.assign(new Error(message), { code }); }
|
|
|
|
module.exports = {
|
|
MAX_MESSAGE_BYTES,
|
|
parseClientMessage,
|
|
sanitizeChat,
|
|
sanitizeEvent
|
|
};
|