tangled
alpha
login
or
join now
stream.place
/
streamplace
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
Nuke old branding admin stuff and cache remnants
Natalie B.
1 month ago
3cb74694
b35541d6
+20
-596
5 changed files
expand all
collapse all
unified
split
js
components
src
streamplace-store
branding.tsx
pkg
api
api_internal.go
branding-admin.html
spxrpc
place_stream_branding.go
spxrpc.go
-6
js/components/src/streamplace-store/branding.tsx
···
173
return asset?.data || "#8b5cf6";
174
}
175
176
-
// convenience hook for default stream key
177
-
export function useDefaultStreamKey(): string | undefined {
178
-
const asset = useBrandingAsset("defaultStreamKey");
179
-
return asset?.data || undefined;
180
-
}
181
-
182
// convenience hook for default streamer
183
export function useDefaultStreamer(): string | undefined {
184
const asset = useBrandingAsset("defaultStreamer");
···
173
return asset?.data || "#8b5cf6";
174
}
175
0
0
0
0
0
0
176
// convenience hook for default streamer
177
export function useDefaultStreamer(): string | undefined {
178
const asset = useBrandingAsset("defaultStreamer");
-38
pkg/api/api_internal.go
···
449
}
450
})
451
452
-
router.GET("/branding-admin", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
453
-
http.ServeFile(w, r, "pkg/api/branding-admin.html")
454
-
})
455
-
456
-
router.GET("/xrpc/place.stream.branding.getBranding", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
457
-
// call XRPC handler directly instead of proxying
458
-
broadcasterDID := r.URL.Query().Get("broadcaster")
459
-
output, err := a.XRPCServer.HandlePlaceStreamBrandingGetBrandingDirect(ctx, broadcasterDID)
460
-
if err != nil {
461
-
errors.WriteHTTPInternalServerError(w, "failed to fetch branding", err)
462
-
return
463
-
}
464
-
465
-
w.Header().Set("Content-Type", "application/json")
466
-
bs, err := json.Marshal(output)
467
-
if err != nil {
468
-
errors.WriteHTTPInternalServerError(w, "unable to marshal json", err)
469
-
return
470
-
}
471
-
if _, err := w.Write(bs); err != nil {
472
-
log.Error(ctx, "error writing response", "error", err)
473
-
}
474
-
})
475
-
476
-
router.GET("/xrpc/place.stream.branding.getBlob", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
477
-
// call XRPC handler directly instead of proxying
478
-
key := r.URL.Query().Get("key")
479
-
broadcasterDID := r.URL.Query().Get("broadcaster")
480
-
reader, err := a.XRPCServer.HandlePlaceStreamBrandingGetBlobDirect(ctx, broadcasterDID, key)
481
-
if err != nil {
482
-
errors.WriteHTTPInternalServerError(w, "failed to fetch blob", err)
483
-
return
484
-
}
485
-
if _, err := io.Copy(w, reader); err != nil {
486
-
log.Error(ctx, "error writing response", "error", err)
487
-
}
488
-
})
489
-
490
router.POST("/notification-blast", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
491
var payload notificationpkg.NotificationBlast
492
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
···
449
}
450
})
451
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
452
router.POST("/notification-blast", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
453
var payload notificationpkg.NotificationBlast
454
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
-520
pkg/api/branding-admin.html
···
1
-
<!doctype html>
2
-
<html lang="en">
3
-
<head>
4
-
<meta charset="UTF-8" />
5
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
-
<title>Branding Admin</title>
7
-
<style>
8
-
* {
9
-
box-sizing: border-box;
10
-
margin: 0;
11
-
padding: 0;
12
-
}
13
-
14
-
body {
15
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
16
-
"Helvetica Neue", Arial, sans-serif;
17
-
background: #0a0a0a;
18
-
color: #e4e4e7;
19
-
padding: 2rem;
20
-
line-height: 1.6;
21
-
}
22
-
23
-
.container {
24
-
max-width: 800px;
25
-
margin: 0 auto;
26
-
}
27
-
28
-
h1 {
29
-
font-size: 2rem;
30
-
margin-bottom: 0.5rem;
31
-
color: #fafafa;
32
-
}
33
-
34
-
.subtitle {
35
-
color: #a1a1aa;
36
-
margin-bottom: 2rem;
37
-
}
38
-
39
-
.section {
40
-
background: #18181b;
41
-
border: 1px solid #27272a;
42
-
border-radius: 8px;
43
-
padding: 1.5rem;
44
-
margin-bottom: 1.5rem;
45
-
}
46
-
47
-
.section-title {
48
-
font-size: 1.25rem;
49
-
font-weight: 600;
50
-
margin-bottom: 0.5rem;
51
-
color: #fafafa;
52
-
}
53
-
54
-
.section-subtitle {
55
-
font-size: 0.875rem;
56
-
color: #a1a1aa;
57
-
margin-bottom: 1rem;
58
-
}
59
-
60
-
.input-group {
61
-
display: flex;
62
-
gap: 0.5rem;
63
-
margin-bottom: 1rem;
64
-
}
65
-
66
-
input[type="text"],
67
-
input[type="file"] {
68
-
flex: 1;
69
-
padding: 0.625rem;
70
-
background: #27272a;
71
-
border: 1px solid #3f3f46;
72
-
border-radius: 6px;
73
-
color: #e4e4e7;
74
-
font-size: 0.875rem;
75
-
}
76
-
77
-
input[type="text"]:focus,
78
-
input[type="file"]:focus {
79
-
outline: none;
80
-
border-color: #6366f1;
81
-
}
82
-
83
-
button {
84
-
padding: 0.625rem 1.25rem;
85
-
background: #6366f1;
86
-
color: white;
87
-
border: none;
88
-
border-radius: 6px;
89
-
font-size: 0.875rem;
90
-
font-weight: 500;
91
-
cursor: pointer;
92
-
transition: background 0.2s;
93
-
}
94
-
95
-
button:hover {
96
-
background: #4f46e5;
97
-
}
98
-
99
-
button:disabled {
100
-
background: #3f3f46;
101
-
cursor: not-allowed;
102
-
}
103
-
104
-
button.secondary {
105
-
background: #27272a;
106
-
border: 1px solid #3f3f46;
107
-
}
108
-
109
-
button.secondary:hover {
110
-
background: #3f3f46;
111
-
}
112
-
113
-
button.danger {
114
-
background: #dc2626;
115
-
}
116
-
117
-
button.danger:hover {
118
-
background: #b91c1c;
119
-
}
120
-
121
-
.toast {
122
-
position: fixed;
123
-
top: 2rem;
124
-
right: 2rem;
125
-
padding: 1rem 1.5rem;
126
-
background: #18181b;
127
-
border: 1px solid #27272a;
128
-
border-radius: 8px;
129
-
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
130
-
animation: slideIn 0.3s ease-out;
131
-
z-index: 1000;
132
-
}
133
-
134
-
.toast.success {
135
-
border-color: #16a34a;
136
-
}
137
-
138
-
.toast.error {
139
-
border-color: #dc2626;
140
-
}
141
-
142
-
@keyframes slideIn {
143
-
from {
144
-
transform: translateX(100%);
145
-
opacity: 0;
146
-
}
147
-
to {
148
-
transform: translateX(0);
149
-
opacity: 1;
150
-
}
151
-
}
152
-
153
-
.info {
154
-
font-size: 0.875rem;
155
-
color: #71717a;
156
-
margin-top: 1.5rem;
157
-
}
158
-
159
-
.button-group {
160
-
display: flex;
161
-
gap: 0.5rem;
162
-
}
163
-
</style>
164
-
</head>
165
-
<body>
166
-
<div class="container">
167
-
<h1>Branding Administration</h1>
168
-
<p class="subtitle">Customize your Streamplace instance</p>
169
-
170
-
<!-- Broadcaster DID -->
171
-
<div class="section">
172
-
<div class="section-title">Broadcaster DID</div>
173
-
<div class="section-subtitle">Leave empty to use server default</div>
174
-
<div class="input-group">
175
-
<input
176
-
type="text"
177
-
id="broadcasterDIDInput"
178
-
placeholder="did:plc:..."
179
-
value=""
180
-
/>
181
-
</div>
182
-
</div>
183
-
184
-
<!-- Site Title -->
185
-
<div class="section">
186
-
<div class="section-title">Site Title</div>
187
-
<div class="section-subtitle" id="currentTitle">
188
-
Current: Loading...
189
-
</div>
190
-
<div class="input-group">
191
-
<input
192
-
type="text"
193
-
id="siteTitleInput"
194
-
placeholder="Enter new site title"
195
-
/>
196
-
<button
197
-
onclick="uploadText('siteTitle', document.getElementById('siteTitleInput').value)"
198
-
>
199
-
Update
200
-
</button>
201
-
</div>
202
-
</div>
203
-
204
-
<!-- Site Description -->
205
-
<div class="section">
206
-
<div class="section-title">Site Description</div>
207
-
<div class="section-subtitle" id="currentDescription">
208
-
Current: Loading...
209
-
</div>
210
-
<div class="input-group">
211
-
<input
212
-
type="text"
213
-
id="siteDescriptionInput"
214
-
placeholder="Enter site description"
215
-
/>
216
-
<button
217
-
onclick="uploadText('siteDescription', document.getElementById('siteDescriptionInput').value)"
218
-
>
219
-
Update
220
-
</button>
221
-
</div>
222
-
</div>
223
-
224
-
<!-- Primary Color -->
225
-
<div class="section">
226
-
<div class="section-title">Primary Color</div>
227
-
<div class="section-subtitle" id="currentPrimaryColor">
228
-
Current: Loading...
229
-
</div>
230
-
<div class="input-group">
231
-
<input type="text" id="primaryColorInput" placeholder="#6366f1" />
232
-
<button
233
-
onclick="uploadText('primaryColor', document.getElementById('primaryColorInput').value)"
234
-
>
235
-
Update
236
-
</button>
237
-
</div>
238
-
</div>
239
-
240
-
<!-- Accent Color -->
241
-
<div class="section">
242
-
<div class="section-title">Accent Color</div>
243
-
<div class="section-subtitle" id="currentAccentColor">
244
-
Current: Loading...
245
-
</div>
246
-
<div class="input-group">
247
-
<input type="text" id="accentColorInput" placeholder="#8b5cf6" />
248
-
<button
249
-
onclick="uploadText('accentColor', document.getElementById('accentColorInput').value)"
250
-
>
251
-
Update
252
-
</button>
253
-
</div>
254
-
</div>
255
-
256
-
<!-- Default Streamer -->
257
-
<div class="section">
258
-
<div class="section-title">Default Streamer</div>
259
-
<div class="section-subtitle" id="currentDefaultStreamer">
260
-
Current: None
261
-
</div>
262
-
<div class="input-group">
263
-
<input
264
-
type="text"
265
-
id="defaultStreamerInput"
266
-
placeholder="did:plc:..."
267
-
/>
268
-
<button
269
-
onclick="uploadText('defaultStreamer', document.getElementById('defaultStreamerInput').value)"
270
-
>
271
-
Update
272
-
</button>
273
-
</div>
274
-
<div class="button-group" style="margin-top: 0.5rem">
275
-
<button class="danger" onclick="deleteBlob('defaultStreamer')">
276
-
Clear Default Streamer
277
-
</button>
278
-
</div>
279
-
</div>
280
-
281
-
<!-- Main Logo -->
282
-
<div class="section">
283
-
<div class="section-title">Main Logo</div>
284
-
<div class="section-subtitle">SVG, PNG, or JPEG (max 500KB)</div>
285
-
<div id="currentLogoPreview" style="margin-bottom: 1rem"></div>
286
-
<div class="input-group">
287
-
<input
288
-
type="file"
289
-
id="logoInput"
290
-
accept="image/svg+xml,image/png,image/jpeg"
291
-
/>
292
-
<button
293
-
onclick="uploadFile('mainLogo', document.getElementById('logoInput'))"
294
-
>
295
-
Upload
296
-
</button>
297
-
</div>
298
-
<div class="button-group" style="margin-top: 0.5rem">
299
-
<button class="danger" onclick="deleteBlob('mainLogo')">
300
-
Delete Logo
301
-
</button>
302
-
</div>
303
-
</div>
304
-
305
-
<!-- Favicon -->
306
-
<div class="section">
307
-
<div class="section-title">Favicon</div>
308
-
<div class="section-subtitle">SVG, PNG, or ICO (max 100KB)</div>
309
-
<div id="currentFaviconPreview" style="margin-bottom: 1rem"></div>
310
-
<div class="input-group">
311
-
<input
312
-
type="file"
313
-
id="faviconInput"
314
-
accept="image/svg+xml,image/png,image/x-icon"
315
-
/>
316
-
<button
317
-
onclick="uploadFile('favicon', document.getElementById('faviconInput'))"
318
-
>
319
-
Upload
320
-
</button>
321
-
</div>
322
-
<div class="button-group" style="margin-top: 0.5rem">
323
-
<button class="danger" onclick="deleteBlob('favicon')">
324
-
Delete Favicon
325
-
</button>
326
-
</div>
327
-
</div>
328
-
329
-
<div class="info" id="info">Loading branding information...</div>
330
-
</div>
331
-
332
-
<script>
333
-
let currentBranding = {};
334
-
335
-
function getBroadcasterDID() {
336
-
return document.getElementById("broadcasterDIDInput").value.trim();
337
-
}
338
-
339
-
function getBroadcasterParam() {
340
-
const did = getBroadcasterDID();
341
-
return did ? `?broadcaster=${encodeURIComponent(did)}` : "";
342
-
}
343
-
344
-
async function loadBranding() {
345
-
try {
346
-
// fetch branding metadata
347
-
const response = await fetch(
348
-
`/xrpc/place.stream.branding.getBranding${getBroadcasterParam()}`,
349
-
);
350
-
if (!response.ok) {
351
-
throw new Error("Failed to load branding");
352
-
}
353
-
354
-
const data = await response.json();
355
-
currentBranding = {};
356
-
357
-
// process assets - use inline data for text, URLs for images
358
-
for (const asset of data.assets) {
359
-
if (asset.data) {
360
-
// text asset with inline data
361
-
currentBranding[asset.key] = asset.data;
362
-
} else if (asset.url) {
363
-
// image asset with URL
364
-
currentBranding[asset.key] = asset.url;
365
-
}
366
-
}
367
-
368
-
// update UI with current values
369
-
document.getElementById("currentTitle").textContent =
370
-
"Current: " + (currentBranding["siteTitle"] || "Streamplace");
371
-
document.getElementById("currentDescription").textContent =
372
-
"Current: " +
373
-
(currentBranding["siteDescription"] || "Live streaming platform");
374
-
document.getElementById("currentPrimaryColor").textContent =
375
-
"Current: " + (currentBranding["primaryColor"] || "#6366f1");
376
-
document.getElementById("currentAccentColor").textContent =
377
-
"Current: " + (currentBranding["accentColor"] || "#8b5cf6");
378
-
document.getElementById("currentDefaultStreamer").textContent =
379
-
"Current: " + (currentBranding["defaultStreamer"] || "None");
380
-
381
-
// render logo preview
382
-
const logoPreview = document.getElementById("currentLogoPreview");
383
-
if (currentBranding["mainLogo"]) {
384
-
logoPreview.innerHTML = `<img src="${currentBranding["mainLogo"]}" style="max-width: 200px; max-height: 100px; background: #27272a; padding: 1rem; border-radius: 6px;" alt="Main Logo">`;
385
-
} else {
386
-
logoPreview.innerHTML =
387
-
'<div style="color: #71717a; font-size: 0.875rem;">No custom logo</div>';
388
-
}
389
-
390
-
// render favicon preview
391
-
const faviconPreview = document.getElementById(
392
-
"currentFaviconPreview",
393
-
);
394
-
if (currentBranding["favicon"]) {
395
-
faviconPreview.innerHTML = `<img src="${currentBranding["favicon"]}" style="max-width: 64px; max-height: 64px; background: #27272a; padding: 0.5rem; border-radius: 6px;" alt="Favicon">`;
396
-
} else {
397
-
faviconPreview.innerHTML =
398
-
'<div style="color: #71717a; font-size: 0.875rem;">No custom favicon</div>';
399
-
}
400
-
401
-
document.getElementById("info").textContent =
402
-
"Branding loaded successfully";
403
-
} catch (err) {
404
-
showToast("Failed to load branding: " + err.message, "error");
405
-
document.getElementById("info").textContent =
406
-
"Error loading branding: " + err.message;
407
-
}
408
-
}
409
-
410
-
async function uploadText(key, value) {
411
-
if (!value.trim()) {
412
-
showToast("Please enter a value", "error");
413
-
return;
414
-
}
415
-
416
-
try {
417
-
const response = await fetch(
418
-
`/branding/${key}${getBroadcasterParam()}`,
419
-
{
420
-
method: "PUT",
421
-
headers: {
422
-
"Content-Type": "text/plain",
423
-
},
424
-
body: value.trim(),
425
-
},
426
-
);
427
-
428
-
if (!response.ok) {
429
-
throw new Error("Upload failed: " + response.statusText);
430
-
}
431
-
432
-
showToast(`${key} updated successfully`, "success");
433
-
434
-
// clear input
435
-
document.getElementById(key + "Input").value = "";
436
-
437
-
// reload branding
438
-
setTimeout(() => loadBranding(), 500);
439
-
} catch (err) {
440
-
showToast("Failed to upload: " + err.message, "error");
441
-
}
442
-
}
443
-
444
-
async function uploadFile(key, inputElement) {
445
-
const file = inputElement.files[0];
446
-
if (!file) {
447
-
showToast("Please select a file", "error");
448
-
return;
449
-
}
450
-
451
-
try {
452
-
const response = await fetch(
453
-
`/branding/${key}${getBroadcasterParam()}`,
454
-
{
455
-
method: "PUT",
456
-
headers: {
457
-
"Content-Type": file.type,
458
-
},
459
-
body: file,
460
-
},
461
-
);
462
-
463
-
if (!response.ok) {
464
-
throw new Error("Upload failed: " + response.statusText);
465
-
}
466
-
467
-
showToast(`${key} uploaded successfully`, "success");
468
-
469
-
// clear input
470
-
inputElement.value = "";
471
-
472
-
// reload branding
473
-
setTimeout(() => loadBranding(), 500);
474
-
} catch (err) {
475
-
showToast("Failed to upload: " + err.message, "error");
476
-
}
477
-
}
478
-
479
-
async function deleteBlob(key) {
480
-
if (!confirm(`Are you sure you want to delete ${key}?`)) {
481
-
return;
482
-
}
483
-
484
-
try {
485
-
const response = await fetch(
486
-
`/branding/${key}${getBroadcasterParam()}`,
487
-
{
488
-
method: "DELETE",
489
-
},
490
-
);
491
-
492
-
if (!response.ok) {
493
-
throw new Error("Delete failed: " + response.statusText);
494
-
}
495
-
496
-
showToast(`${key} deleted successfully`, "success");
497
-
498
-
// reload branding
499
-
setTimeout(() => loadBranding(), 500);
500
-
} catch (err) {
501
-
showToast("Failed to delete: " + err.message, "error");
502
-
}
503
-
}
504
-
505
-
function showToast(message, type = "success") {
506
-
const toast = document.createElement("div");
507
-
toast.className = `toast ${type}`;
508
-
toast.textContent = message;
509
-
document.body.appendChild(toast);
510
-
511
-
setTimeout(() => {
512
-
toast.remove();
513
-
}, 3000);
514
-
}
515
-
516
-
// load branding on page load
517
-
loadBranding();
518
-
</script>
519
-
</body>
520
-
</html>
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+6
-15
pkg/spxrpc/place_stream_branding.go
···
23
}{
24
// "mainLogo": {data: defaultLogoSVG, mime: "image/svg+xml"},
25
// "favicon": {data: defaultFaviconSVG, mime: "image/svg+xml"},
26
-
"siteTitle": {data: []byte("Streamplace"), mime: "text/plain"},
27
-
"siteDescription": {data: []byte("Live streaming platform"), mime: "text/plain"},
28
-
"primaryColor": {data: []byte("#6366f1"), mime: "text/plain"},
29
-
"accentColor": {data: []byte("#8b5cf6"), mime: "text/plain"},
30
-
"defaultStreamKey": {data: []byte(""), mime: "text/plain"},
31
-
"defaultStreamer": {data: []byte(""), mime: "text/plain"},
32
}
33
34
func (s *Server) getBroadcasterID(ctx context.Context, broadcasterDID string) string {
···
170
maxSize := 500 * 1024 // 500KB default for logos
171
if input.Key == "favicon" {
172
maxSize = 100 * 1024 // 100KB for favicons
173
-
} else if input.Key == "siteTitle" || input.Key == "siteDescription" || input.Key == "primaryColor" || input.Key == "accentColor" || input.Key == "defaultStreamKey" || input.Key == "defaultStreamer" {
174
maxSize = 1024 // 1KB for text values
175
}
176
// sidebarBackgroundImage uses default 500KB limit
···
194
log.Error(ctx, "failed to store branding blob", "err", err)
195
return nil, echo.NewHTTPError(http.StatusInternalServerError, "unable to store branding blob")
196
}
197
-
198
-
// invalidate cache
199
-
cacheKey := fmt.Sprintf("%s:%s", broadcasterID, input.Key)
200
-
s.BrandingCache.Delete(cacheKey)
201
202
return &placestreamtypes.BrandingUpdateBlob_Output{
203
Success: true,
···
231
log.Error(ctx, "failed to delete branding blob", "err", err)
232
return nil, echo.NewHTTPError(http.StatusInternalServerError, "unable to delete branding blob")
233
}
234
-
235
-
// invalidate cache
236
-
cacheKey := fmt.Sprintf("%s:%s", broadcasterID, input.Key)
237
-
s.BrandingCache.Delete(cacheKey)
238
239
return &placestreamtypes.BrandingDeleteBlob_Output{
240
Success: true,
···
23
}{
24
// "mainLogo": {data: defaultLogoSVG, mime: "image/svg+xml"},
25
// "favicon": {data: defaultFaviconSVG, mime: "image/svg+xml"},
26
+
"siteTitle": {data: []byte("Streamplace"), mime: "text/plain"},
27
+
"siteDescription": {data: []byte("Live streaming platform"), mime: "text/plain"},
28
+
"primaryColor": {data: []byte("#6366f1"), mime: "text/plain"},
29
+
"accentColor": {data: []byte("#8b5cf6"), mime: "text/plain"},
30
+
"defaultStreamer": {data: []byte(""), mime: "text/plain"},
0
31
}
32
33
func (s *Server) getBroadcasterID(ctx context.Context, broadcasterDID string) string {
···
169
maxSize := 500 * 1024 // 500KB default for logos
170
if input.Key == "favicon" {
171
maxSize = 100 * 1024 // 100KB for favicons
172
+
} else if input.Key == "siteTitle" || input.Key == "siteDescription" || input.Key == "primaryColor" || input.Key == "accentColor" || input.Key == "defaultStreamer" {
173
maxSize = 1024 // 1KB for text values
174
}
175
// sidebarBackgroundImage uses default 500KB limit
···
193
log.Error(ctx, "failed to store branding blob", "err", err)
194
return nil, echo.NewHTTPError(http.StatusInternalServerError, "unable to store branding blob")
195
}
0
0
0
0
196
197
return &placestreamtypes.BrandingUpdateBlob_Output{
198
Success: true,
···
226
log.Error(ctx, "failed to delete branding blob", "err", err)
227
return nil, echo.NewHTTPError(http.StatusInternalServerError, "unable to delete branding blob")
228
}
0
0
0
0
229
230
return &placestreamtypes.BrandingDeleteBlob_Output{
231
Success: true,
+14
-17
pkg/spxrpc/spxrpc.go
···
24
)
25
26
type Server struct {
27
-
e *echo.Echo
28
-
cli *config.CLI
29
-
model model.Model
30
-
OGImageCache *cache.Cache
31
-
BrandingCache *cache.Cache
32
-
ATSync *atproto.ATProtoSynchronizer
33
-
statefulDB *statedb.StatefulDB
34
-
bus *bus.Bus
35
}
36
37
func NewServer(ctx context.Context, cli *config.CLI, model model.Model, statefulDB *statedb.StatefulDB, op *oatproxy.OATProxy, mdlw middleware.Middleware, atsync *atproto.ATProtoSynchronizer, bus *bus.Bus) (*Server, error) {
38
e := echo.New()
39
s := &Server{
40
-
e: e,
41
-
cli: cli,
42
-
model: model,
43
-
OGImageCache: cache.New(5*time.Minute, 10*time.Minute), // 5min TTL, 10min cleanup
44
-
BrandingCache: cache.New(1*time.Hour, 15*time.Minute), // 1hr TTL, 15min cleanup
45
-
ATSync: atsync,
46
-
statefulDB: statefulDB,
47
-
bus: bus,
48
}
49
e.Use(s.ErrorHandlingMiddleware())
50
e.Use(s.ContextPreservingMiddleware())
···
65
e.GET("/xrpc/_health", func(c echo.Context) error {
66
return c.JSON(http.StatusOK, map[string]string{"version": cli.Build.Version})
67
})
68
-
e.GET("/favicon.ico", s.HandleFaviconICO)
69
e.GET("/xrpc/com.atproto.sync.subscribeRepos", s.handleComAtprotoSyncSubscribeRepos)
70
e.GET("/xrpc/place.stream.live.subscribeSegments", s.handlePlaceStreamLiveSubscribeSegments)
71
e.GET("/xrpc/*", s.HandleWildcard)
···
24
)
25
26
type Server struct {
27
+
e *echo.Echo
28
+
cli *config.CLI
29
+
model model.Model
30
+
OGImageCache *cache.Cache
31
+
ATSync *atproto.ATProtoSynchronizer
32
+
statefulDB *statedb.StatefulDB
33
+
bus *bus.Bus
0
34
}
35
36
func NewServer(ctx context.Context, cli *config.CLI, model model.Model, statefulDB *statedb.StatefulDB, op *oatproxy.OATProxy, mdlw middleware.Middleware, atsync *atproto.ATProtoSynchronizer, bus *bus.Bus) (*Server, error) {
37
e := echo.New()
38
s := &Server{
39
+
e: e,
40
+
cli: cli,
41
+
model: model,
42
+
OGImageCache: cache.New(5*time.Minute, 10*time.Minute), // 5min TTL, 10min cleanup
43
+
ATSync: atsync,
44
+
statefulDB: statefulDB,
45
+
bus: bus,
0
46
}
47
e.Use(s.ErrorHandlingMiddleware())
48
e.Use(s.ContextPreservingMiddleware())
···
63
e.GET("/xrpc/_health", func(c echo.Context) error {
64
return c.JSON(http.StatusOK, map[string]string{"version": cli.Build.Version})
65
})
0
66
e.GET("/xrpc/com.atproto.sync.subscribeRepos", s.handleComAtprotoSyncSubscribeRepos)
67
e.GET("/xrpc/place.stream.live.subscribeSegments", s.handlePlaceStreamLiveSubscribeSegments)
68
e.GET("/xrpc/*", s.HandleWildcard)