Lumi/src/services/twitch-chat-assets.js
2026-07-19 12:15:43 +02:00

72 lines
2.5 KiB
JavaScript

const { getSetting } = require("./settings");
const badgeCatalogs = new Map();
const CACHE_MS = 30 * 60 * 1000;
function fallbackBadges(badges = {}) {
return Object.entries(badges).map(([label]) => ({ label, image: null }));
}
async function badgeRequest(path) {
const clientId = String(getSetting("twitch_client_id", "") || "").trim();
const token = String(getSetting("twitch_bot_oauth", "") || "").trim().replace(/^oauth:/i, "");
if (!clientId || !token) throw new Error("Twitch badge credentials are unavailable.");
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2500);
try {
const response = await fetch(`https://api.twitch.tv/helix/chat/badges${path}`, {
headers: { "Client-Id": clientId, Authorization: `Bearer ${token}` },
signal: controller.signal
});
if (!response.ok) throw new Error(`Twitch badge request returned HTTP ${response.status}.`);
return response.json();
} finally {
clearTimeout(timeout);
}
}
function indexCatalog(payload) {
const catalog = new Map();
for (const set of payload?.data || []) {
for (const version of set.versions || []) {
catalog.set(`${set.set_id}/${version.id}`, {
label: version.title || set.set_id,
image: version.image_url_2x || version.image_url_1x || version.image_url_4x || null
});
}
}
return catalog;
}
async function loadCatalog(roomId) {
const key = String(roomId || "global");
const cached = badgeCatalogs.get(key);
if (cached && cached.expiresAt > Date.now()) return cached.promise;
const promise = Promise.allSettled([
badgeRequest("/global"),
roomId ? badgeRequest(`?broadcaster_id=${encodeURIComponent(roomId)}`) : Promise.resolve({ data: [] })
]).then((results) => {
const catalog = new Map();
for (const result of results) {
if (result.status !== "fulfilled") continue;
for (const [id, badge] of indexCatalog(result.value)) catalog.set(id, badge);
}
return catalog;
});
badgeCatalogs.set(key, { expiresAt: Date.now() + CACHE_MS, promise });
return promise;
}
async function resolveTwitchBadges(roomId, badges = {}) {
const entries = Object.entries(badges || {});
if (!entries.length) return [];
try {
const catalog = await loadCatalog(roomId);
return entries.map(([setId, version]) => catalog.get(`${setId}/${version}`) || { label: setId, image: null });
} catch {
return fallbackBadges(badges);
}
}
module.exports = { resolveTwitchBadges };