Gate twitch integration

This commit is contained in:
2026-01-14 00:44:58 +01:00
parent a267f9b5ec
commit c3019a1c48
10 changed files with 267 additions and 38 deletions

View File

@@ -0,0 +1,10 @@
ALTER TABLE channels ADD COLUMN allow_channel_emotes_for_assets BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE channels ADD COLUMN allow_script_chat_access BOOLEAN NOT NULL DEFAULT TRUE;
UPDATE channels
SET allow_channel_emotes_for_assets = TRUE
WHERE allow_channel_emotes_for_assets IS NULL;
UPDATE channels
SET allow_script_chat_access = TRUE
WHERE allow_script_chat_access IS NULL;

View File

@@ -1929,6 +1929,12 @@ button:disabled:hover {
color: #cbd5e1;
}
.control-grid .checkbox-row {
flex-direction: row;
align-items: center;
gap: 10px;
}
.control-grid .inline-toggle {
align-items: center;
justify-content: space-between;

View File

@@ -7,6 +7,28 @@ const scriptLayer = document.getElementById("broadcast-script-layer");
setUpElectronWindowFrame();
const renderer = new BroadcastRenderer({ canvas, scriptLayer, broadcaster, showToast });
const defaultScriptSettings = {
allowChannelEmotesForAssets: true,
allowScriptChatAccess: true,
};
let currentScriptSettings = { ...defaultScriptSettings };
const settingsPromise = fetch(`/api/channels/${encodeURIComponent(broadcaster)}/settings`)
.then((response) => {
if (!response.ok) {
throw new Error("Failed to load channel settings");
}
return response.json();
})
.then((settings) => {
currentScriptSettings = { ...defaultScriptSettings, ...settings };
renderer.setScriptSettings(currentScriptSettings);
})
.catch((error) => {
console.warn("Unable to load channel settings", error);
renderer.setScriptSettings(defaultScriptSettings);
});
fetch(`/api/twitch/emotes?channel=${encodeURIComponent(broadcaster)}`)
.then((response) => {
if (!response.ok) {
@@ -16,20 +38,26 @@ fetch(`/api/twitch/emotes?channel=${encodeURIComponent(broadcaster)}`)
})
.then((catalog) => renderer.setEmoteCatalog(catalog))
.catch((error) => console.warn("Unable to load Twitch emotes", error));
const disconnectChat = connectTwitchChat(
broadcaster,
({ channel, displayName, message, tags, prefix, raw }) => {
console.log(`[twitch:${broadcaster}] ${displayName}: ${message}`);
renderer.receiveChatMessage({
channel,
displayName,
message,
tags,
prefix,
raw,
});
},
);
let disconnectChat = () => {};
settingsPromise.finally(() => {
if (!currentScriptSettings.allowScriptChatAccess) {
return;
}
disconnectChat = connectTwitchChat(
broadcaster,
({ channel, displayName, message, tags, prefix, raw }) => {
console.log(`[twitch:${broadcaster}] ${displayName}: ${message}`);
renderer.receiveChatMessage({
channel,
displayName,
message,
tags,
prefix,
raw,
});
},
);
});
setUpElectronWindowResizeListener(canvas);
renderer.start();

View File

@@ -28,7 +28,11 @@ export class BroadcastRenderer {
this.chatMessages = [];
this.emoteCatalog = [];
this.emoteCatalogById = new Map();
this.globalEmotes = [];
this.channelEmotes = [];
this.lastChatPruneAt = 0;
this.allowChannelEmotesForAssets = true;
this.allowScriptChatAccess = true;
this.obsBrowser = !!globalThis.obsstudio;
this.supportsAnimatedDecode =
@@ -423,6 +427,17 @@ export class BroadcastRenderer {
this.updateScriptWorkerEmoteCatalog();
}
setScriptSettings(settings) {
this.allowChannelEmotesForAssets = settings?.allowChannelEmotesForAssets !== false;
this.allowScriptChatAccess = settings?.allowScriptChatAccess !== false;
if (!this.allowScriptChatAccess) {
this.chatMessages = [];
}
this.refreshEmoteCatalog();
this.updateScriptWorkerChatMessages();
this.updateScriptWorkerEmoteCatalog();
}
updateScriptWorkerCanvas() {
if (!this.scriptWorker || !this.scriptWorkerReady) {
return;
@@ -443,7 +458,7 @@ export class BroadcastRenderer {
this.scriptWorker.postMessage({
type: "chatMessages",
payload: {
messages: this.chatMessages,
messages: this.allowScriptChatAccess ? this.chatMessages : [],
},
});
}
@@ -461,27 +476,20 @@ export class BroadcastRenderer {
}
setEmoteCatalog(catalog) {
const globalEmotes = Array.isArray(catalog?.global) ? catalog.global : [];
const channelEmotes = Array.isArray(catalog?.channel) ? catalog.channel : [];
this.emoteCatalog = [...globalEmotes, ...channelEmotes];
this.globalEmotes = Array.isArray(catalog?.global) ? catalog.global : [];
this.channelEmotes = Array.isArray(catalog?.channel) ? catalog.channel : [];
this.refreshEmoteCatalog();
}
refreshEmoteCatalog() {
const allowedChannelEmotes = this.allowChannelEmotesForAssets ? this.channelEmotes : [];
this.emoteCatalog = [...this.globalEmotes, ...allowedChannelEmotes];
this.emoteCatalogById = new Map(
this.emoteCatalog.map((entry) => [String(entry?.id || ""), entry]).filter(([key]) => key),
);
if (this.chatMessages.length) {
this.chatMessages = this.chatMessages.map((message) => {
if (!Array.isArray(message.fragments)) {
return message;
}
const fragments = message.fragments.map((fragment) => {
if (fragment.type !== "emote" || fragment.url) {
return fragment;
}
const emoteInfo = this.emoteCatalogById.get(String(fragment.id));
if (!emoteInfo) {
return fragment;
}
return { ...fragment, url: emoteInfo.url, name: emoteInfo.name || fragment.name };
});
const fragments = this.buildMessageFragments(message.message || "", message.tags);
return { ...message, fragments };
});
this.updateScriptWorkerChatMessages();
@@ -537,13 +545,17 @@ export class BroadcastRenderer {
}
const emoteText = message.slice(emote.start, emote.end + 1);
const emoteInfo = this.emoteCatalogById.get(String(emote.id));
fragments.push({
type: "emote",
id: emote.id,
text: emoteText,
name: emoteInfo?.name || emoteText,
url: emoteInfo?.url || null,
});
if (emoteInfo) {
fragments.push({
type: "emote",
id: emote.id,
text: emoteText,
name: emoteInfo?.name || emoteText,
url: emoteInfo?.url || null,
});
} else {
fragments.push({ type: "text", text: emoteText });
}
cursor = emote.end + 1;
});
if (cursor < message.length) {
@@ -553,6 +565,9 @@ export class BroadcastRenderer {
}
receiveChatMessage(message) {
if (!this.allowScriptChatAccess) {
return;
}
if (!message) {
return;
}

View File

@@ -7,6 +7,10 @@ const elements = {
canvasHeight: document.getElementById("canvas-height"),
canvasStatus: document.getElementById("canvas-status"),
canvasSaveButton: document.getElementById("save-canvas-btn"),
allowChannelEmotes: document.getElementById("allow-channel-emotes"),
allowScriptChat: document.getElementById("allow-script-chat"),
scriptSettingsStatus: document.getElementById("script-settings-status"),
scriptSettingsSaveButton: document.getElementById("save-script-settings-btn"),
};
const apiBase = `/api/channels/${encodeURIComponent(broadcaster)}`;
@@ -242,6 +246,54 @@ async function saveCanvasSettings() {
}
}
function renderScriptSettings(settings) {
if (elements.allowChannelEmotes) {
elements.allowChannelEmotes.checked = settings.allowChannelEmotesForAssets !== false;
}
if (elements.allowScriptChat) {
elements.allowScriptChat.checked = settings.allowScriptChatAccess !== false;
}
}
async function fetchScriptSettings() {
try {
const data = await fetchJson("/settings", {}, "Failed to load script settings");
renderScriptSettings(data);
} catch (error) {
renderScriptSettings({ allowChannelEmotesForAssets: true, allowScriptChatAccess: true });
showToast("Using default script settings. Unable to load saved preferences.", "warning");
}
}
async function saveScriptSettings() {
const allowChannelEmotesForAssets = elements.allowChannelEmotes?.checked ?? true;
const allowScriptChatAccess = elements.allowScriptChat?.checked ?? true;
if (elements.scriptSettingsStatus) elements.scriptSettingsStatus.textContent = "Saving...";
setButtonBusy(elements.scriptSettingsSaveButton, true, "Saving...");
try {
const settings = await fetchJson(
"/settings",
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ allowChannelEmotesForAssets, allowScriptChatAccess }),
},
"Failed to save script settings",
);
renderScriptSettings(settings);
if (elements.scriptSettingsStatus) elements.scriptSettingsStatus.textContent = "Saved.";
showToast("Script settings saved successfully.", "success");
setTimeout(() => {
if (elements.scriptSettingsStatus) elements.scriptSettingsStatus.textContent = "";
}, 2000);
} catch (error) {
if (elements.scriptSettingsStatus) elements.scriptSettingsStatus.textContent = "Unable to save right now.";
showToast("Unable to save script settings. Please retry.", "error");
} finally {
setButtonBusy(elements.scriptSettingsSaveButton, false, "Saving...");
}
}
if (elements.adminInput) {
elements.adminInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
@@ -254,3 +306,4 @@ if (elements.adminInput) {
fetchAdmins();
fetchSuggestedAdmins();
fetchCanvasSettings();
fetchScriptSettings();

View File

@@ -85,6 +85,28 @@
</div>
</section>
<section class="card">
<p class="eyebrow">Script privacy</p>
<h3>Script asset access</h3>
<p class="muted">Control what data scripts can access in your channel.</p>
<div class="control-grid">
<label class="checkbox-row">
<input id="allow-channel-emotes" type="checkbox" checked />
Allow script assets to use this channel's Twitch emotes.
</label>
<label class="checkbox-row">
<input id="allow-script-chat" type="checkbox" checked />
Allow script assets to access this channel's Twitch chat log.
</label>
</div>
<div class="control-actions">
<button id="save-script-settings-btn" type="button" onclick="saveScriptSettings()">
Save script settings
</button>
<span id="script-settings-status" class="muted" role="status" aria-live="polite"></span>
</div>
</section>
<section class="card-grid two-col">
<div class="card">
<div class="card-header">