mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Gate twitch integration
This commit is contained in:
@@ -7,6 +7,7 @@ import static org.springframework.http.HttpStatus.NOT_FOUND;
|
|||||||
import dev.kruhlmann.imgfloat.model.AdminRequest;
|
import dev.kruhlmann.imgfloat.model.AdminRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.AssetView;
|
import dev.kruhlmann.imgfloat.model.AssetView;
|
||||||
import dev.kruhlmann.imgfloat.model.CanvasSettingsRequest;
|
import dev.kruhlmann.imgfloat.model.CanvasSettingsRequest;
|
||||||
|
import dev.kruhlmann.imgfloat.model.ChannelScriptSettingsRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.CodeAssetRequest;
|
import dev.kruhlmann.imgfloat.model.CodeAssetRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||||
import dev.kruhlmann.imgfloat.model.PlaybackRequest;
|
import dev.kruhlmann.imgfloat.model.PlaybackRequest;
|
||||||
@@ -209,6 +210,25 @@ public class ChannelApiController {
|
|||||||
return channelDirectoryService.updateCanvasSettings(broadcaster, request);
|
return channelDirectoryService.updateCanvasSettings(broadcaster, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/settings")
|
||||||
|
public ChannelScriptSettingsRequest getScriptSettings(@PathVariable("broadcaster") String broadcaster) {
|
||||||
|
return channelDirectoryService.getChannelScriptSettings(broadcaster);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/settings")
|
||||||
|
public ChannelScriptSettingsRequest updateScriptSettings(
|
||||||
|
@PathVariable("broadcaster") String broadcaster,
|
||||||
|
@Valid @RequestBody ChannelScriptSettingsRequest request,
|
||||||
|
OAuth2AuthenticationToken oauthToken
|
||||||
|
) {
|
||||||
|
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||||
|
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||||
|
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||||
|
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||||
|
LOG.info("Updating script settings for {} by {}", logBroadcaster, logSessionUsername);
|
||||||
|
return channelDirectoryService.updateChannelScriptSettings(broadcaster, request);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/assets", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(value = "/assets", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public ResponseEntity<AssetView> createAsset(
|
public ResponseEntity<AssetView> createAsset(
|
||||||
@PathVariable("broadcaster") String broadcaster,
|
@PathVariable("broadcaster") String broadcaster,
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ public class Channel {
|
|||||||
|
|
||||||
private double canvasHeight = 1080;
|
private double canvasHeight = 1080;
|
||||||
|
|
||||||
|
@Column(name = "allow_channel_emotes_for_assets", nullable = false)
|
||||||
|
private boolean allowChannelEmotesForAssets = true;
|
||||||
|
|
||||||
|
@Column(name = "allow_script_chat_access", nullable = false)
|
||||||
|
private boolean allowScriptChatAccess = true;
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
private Instant createdAt;
|
private Instant createdAt;
|
||||||
|
|
||||||
@@ -79,6 +85,22 @@ public class Channel {
|
|||||||
this.canvasHeight = canvasHeight;
|
this.canvasHeight = canvasHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAllowChannelEmotesForAssets() {
|
||||||
|
return allowChannelEmotesForAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowChannelEmotesForAssets(boolean allowChannelEmotesForAssets) {
|
||||||
|
this.allowChannelEmotesForAssets = allowChannelEmotesForAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowScriptChatAccess() {
|
||||||
|
return allowScriptChatAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowScriptChatAccess(boolean allowScriptChatAccess) {
|
||||||
|
this.allowScriptChatAccess = allowScriptChatAccess;
|
||||||
|
}
|
||||||
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
@PreUpdate
|
@PreUpdate
|
||||||
public void normalizeFields() {
|
public void normalizeFields() {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package dev.kruhlmann.imgfloat.model;
|
||||||
|
|
||||||
|
public class ChannelScriptSettingsRequest {
|
||||||
|
|
||||||
|
private boolean allowChannelEmotesForAssets = true;
|
||||||
|
private boolean allowScriptChatAccess = true;
|
||||||
|
|
||||||
|
public ChannelScriptSettingsRequest() {}
|
||||||
|
|
||||||
|
public ChannelScriptSettingsRequest(boolean allowChannelEmotesForAssets, boolean allowScriptChatAccess) {
|
||||||
|
this.allowChannelEmotesForAssets = allowChannelEmotesForAssets;
|
||||||
|
this.allowScriptChatAccess = allowScriptChatAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowChannelEmotesForAssets() {
|
||||||
|
return allowChannelEmotesForAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowChannelEmotesForAssets(boolean allowChannelEmotesForAssets) {
|
||||||
|
this.allowChannelEmotesForAssets = allowChannelEmotesForAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowScriptChatAccess() {
|
||||||
|
return allowScriptChatAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowScriptChatAccess(boolean allowScriptChatAccess) {
|
||||||
|
this.allowScriptChatAccess = allowScriptChatAccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import dev.kruhlmann.imgfloat.model.AudioAsset;
|
|||||||
import dev.kruhlmann.imgfloat.model.CanvasEvent;
|
import dev.kruhlmann.imgfloat.model.CanvasEvent;
|
||||||
import dev.kruhlmann.imgfloat.model.CanvasSettingsRequest;
|
import dev.kruhlmann.imgfloat.model.CanvasSettingsRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.Channel;
|
import dev.kruhlmann.imgfloat.model.Channel;
|
||||||
|
import dev.kruhlmann.imgfloat.model.ChannelScriptSettingsRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.CodeAssetRequest;
|
import dev.kruhlmann.imgfloat.model.CodeAssetRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.MarketplaceScriptHeart;
|
import dev.kruhlmann.imgfloat.model.MarketplaceScriptHeart;
|
||||||
import dev.kruhlmann.imgfloat.model.PlaybackRequest;
|
import dev.kruhlmann.imgfloat.model.PlaybackRequest;
|
||||||
@@ -193,6 +194,28 @@ public class ChannelDirectoryService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ChannelScriptSettingsRequest getChannelScriptSettings(String broadcaster) {
|
||||||
|
Channel channel = getOrCreateChannel(broadcaster);
|
||||||
|
return new ChannelScriptSettingsRequest(
|
||||||
|
channel.isAllowChannelEmotesForAssets(),
|
||||||
|
channel.isAllowScriptChatAccess()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelScriptSettingsRequest updateChannelScriptSettings(
|
||||||
|
String broadcaster,
|
||||||
|
ChannelScriptSettingsRequest request
|
||||||
|
) {
|
||||||
|
Channel channel = getOrCreateChannel(broadcaster);
|
||||||
|
channel.setAllowChannelEmotesForAssets(request.isAllowChannelEmotesForAssets());
|
||||||
|
channel.setAllowScriptChatAccess(request.isAllowScriptChatAccess());
|
||||||
|
channelRepository.save(channel);
|
||||||
|
return new ChannelScriptSettingsRequest(
|
||||||
|
channel.isAllowChannelEmotesForAssets(),
|
||||||
|
channel.isAllowScriptChatAccess()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<AssetView> createAsset(String broadcaster, MultipartFile file) throws IOException {
|
public Optional<AssetView> createAsset(String broadcaster, MultipartFile file) throws IOException {
|
||||||
long fileSize = file.getSize();
|
long fileSize = file.getSize();
|
||||||
if (fileSize > uploadLimitBytes) {
|
if (fileSize > uploadLimitBytes) {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1929,6 +1929,12 @@ button:disabled:hover {
|
|||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control-grid .checkbox-row {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.control-grid .inline-toggle {
|
.control-grid .inline-toggle {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -7,6 +7,28 @@ const scriptLayer = document.getElementById("broadcast-script-layer");
|
|||||||
setUpElectronWindowFrame();
|
setUpElectronWindowFrame();
|
||||||
|
|
||||||
const renderer = new BroadcastRenderer({ canvas, scriptLayer, broadcaster, showToast });
|
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)}`)
|
fetch(`/api/twitch/emotes?channel=${encodeURIComponent(broadcaster)}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -16,20 +38,26 @@ fetch(`/api/twitch/emotes?channel=${encodeURIComponent(broadcaster)}`)
|
|||||||
})
|
})
|
||||||
.then((catalog) => renderer.setEmoteCatalog(catalog))
|
.then((catalog) => renderer.setEmoteCatalog(catalog))
|
||||||
.catch((error) => console.warn("Unable to load Twitch emotes", error));
|
.catch((error) => console.warn("Unable to load Twitch emotes", error));
|
||||||
const disconnectChat = connectTwitchChat(
|
let disconnectChat = () => {};
|
||||||
broadcaster,
|
settingsPromise.finally(() => {
|
||||||
({ channel, displayName, message, tags, prefix, raw }) => {
|
if (!currentScriptSettings.allowScriptChatAccess) {
|
||||||
console.log(`[twitch:${broadcaster}] ${displayName}: ${message}`);
|
return;
|
||||||
renderer.receiveChatMessage({
|
}
|
||||||
channel,
|
disconnectChat = connectTwitchChat(
|
||||||
displayName,
|
broadcaster,
|
||||||
message,
|
({ channel, displayName, message, tags, prefix, raw }) => {
|
||||||
tags,
|
console.log(`[twitch:${broadcaster}] ${displayName}: ${message}`);
|
||||||
prefix,
|
renderer.receiveChatMessage({
|
||||||
raw,
|
channel,
|
||||||
});
|
displayName,
|
||||||
},
|
message,
|
||||||
);
|
tags,
|
||||||
|
prefix,
|
||||||
|
raw,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
setUpElectronWindowResizeListener(canvas);
|
setUpElectronWindowResizeListener(canvas);
|
||||||
renderer.start();
|
renderer.start();
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ export class BroadcastRenderer {
|
|||||||
this.chatMessages = [];
|
this.chatMessages = [];
|
||||||
this.emoteCatalog = [];
|
this.emoteCatalog = [];
|
||||||
this.emoteCatalogById = new Map();
|
this.emoteCatalogById = new Map();
|
||||||
|
this.globalEmotes = [];
|
||||||
|
this.channelEmotes = [];
|
||||||
this.lastChatPruneAt = 0;
|
this.lastChatPruneAt = 0;
|
||||||
|
this.allowChannelEmotesForAssets = true;
|
||||||
|
this.allowScriptChatAccess = true;
|
||||||
|
|
||||||
this.obsBrowser = !!globalThis.obsstudio;
|
this.obsBrowser = !!globalThis.obsstudio;
|
||||||
this.supportsAnimatedDecode =
|
this.supportsAnimatedDecode =
|
||||||
@@ -423,6 +427,17 @@ export class BroadcastRenderer {
|
|||||||
this.updateScriptWorkerEmoteCatalog();
|
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() {
|
updateScriptWorkerCanvas() {
|
||||||
if (!this.scriptWorker || !this.scriptWorkerReady) {
|
if (!this.scriptWorker || !this.scriptWorkerReady) {
|
||||||
return;
|
return;
|
||||||
@@ -443,7 +458,7 @@ export class BroadcastRenderer {
|
|||||||
this.scriptWorker.postMessage({
|
this.scriptWorker.postMessage({
|
||||||
type: "chatMessages",
|
type: "chatMessages",
|
||||||
payload: {
|
payload: {
|
||||||
messages: this.chatMessages,
|
messages: this.allowScriptChatAccess ? this.chatMessages : [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -461,27 +476,20 @@ export class BroadcastRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setEmoteCatalog(catalog) {
|
setEmoteCatalog(catalog) {
|
||||||
const globalEmotes = Array.isArray(catalog?.global) ? catalog.global : [];
|
this.globalEmotes = Array.isArray(catalog?.global) ? catalog.global : [];
|
||||||
const channelEmotes = Array.isArray(catalog?.channel) ? catalog.channel : [];
|
this.channelEmotes = Array.isArray(catalog?.channel) ? catalog.channel : [];
|
||||||
this.emoteCatalog = [...globalEmotes, ...channelEmotes];
|
this.refreshEmoteCatalog();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshEmoteCatalog() {
|
||||||
|
const allowedChannelEmotes = this.allowChannelEmotesForAssets ? this.channelEmotes : [];
|
||||||
|
this.emoteCatalog = [...this.globalEmotes, ...allowedChannelEmotes];
|
||||||
this.emoteCatalogById = new Map(
|
this.emoteCatalogById = new Map(
|
||||||
this.emoteCatalog.map((entry) => [String(entry?.id || ""), entry]).filter(([key]) => key),
|
this.emoteCatalog.map((entry) => [String(entry?.id || ""), entry]).filter(([key]) => key),
|
||||||
);
|
);
|
||||||
if (this.chatMessages.length) {
|
if (this.chatMessages.length) {
|
||||||
this.chatMessages = this.chatMessages.map((message) => {
|
this.chatMessages = this.chatMessages.map((message) => {
|
||||||
if (!Array.isArray(message.fragments)) {
|
const fragments = this.buildMessageFragments(message.message || "", message.tags);
|
||||||
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 };
|
|
||||||
});
|
|
||||||
return { ...message, fragments };
|
return { ...message, fragments };
|
||||||
});
|
});
|
||||||
this.updateScriptWorkerChatMessages();
|
this.updateScriptWorkerChatMessages();
|
||||||
@@ -537,13 +545,17 @@ export class BroadcastRenderer {
|
|||||||
}
|
}
|
||||||
const emoteText = message.slice(emote.start, emote.end + 1);
|
const emoteText = message.slice(emote.start, emote.end + 1);
|
||||||
const emoteInfo = this.emoteCatalogById.get(String(emote.id));
|
const emoteInfo = this.emoteCatalogById.get(String(emote.id));
|
||||||
fragments.push({
|
if (emoteInfo) {
|
||||||
type: "emote",
|
fragments.push({
|
||||||
id: emote.id,
|
type: "emote",
|
||||||
text: emoteText,
|
id: emote.id,
|
||||||
name: emoteInfo?.name || emoteText,
|
text: emoteText,
|
||||||
url: emoteInfo?.url || null,
|
name: emoteInfo?.name || emoteText,
|
||||||
});
|
url: emoteInfo?.url || null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fragments.push({ type: "text", text: emoteText });
|
||||||
|
}
|
||||||
cursor = emote.end + 1;
|
cursor = emote.end + 1;
|
||||||
});
|
});
|
||||||
if (cursor < message.length) {
|
if (cursor < message.length) {
|
||||||
@@ -553,6 +565,9 @@ export class BroadcastRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
receiveChatMessage(message) {
|
receiveChatMessage(message) {
|
||||||
|
if (!this.allowScriptChatAccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const elements = {
|
|||||||
canvasHeight: document.getElementById("canvas-height"),
|
canvasHeight: document.getElementById("canvas-height"),
|
||||||
canvasStatus: document.getElementById("canvas-status"),
|
canvasStatus: document.getElementById("canvas-status"),
|
||||||
canvasSaveButton: document.getElementById("save-canvas-btn"),
|
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)}`;
|
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) {
|
if (elements.adminInput) {
|
||||||
elements.adminInput.addEventListener("keydown", (event) => {
|
elements.adminInput.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
@@ -254,3 +306,4 @@ if (elements.adminInput) {
|
|||||||
fetchAdmins();
|
fetchAdmins();
|
||||||
fetchSuggestedAdmins();
|
fetchSuggestedAdmins();
|
||||||
fetchCanvasSettings();
|
fetchCanvasSettings();
|
||||||
|
fetchScriptSettings();
|
||||||
|
|||||||
@@ -85,6 +85,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="card-grid two-col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
|||||||
Reference in New Issue
Block a user