openvibers/openvibe.live Commits
Live: go-lives reach followers through Network's consumer only (GOLIVE_NOTIFY and the direct call deleted, C-85) (#93)
Changes
@@@ -63,7 +63,6 @@ OV_OAUTH_CLIENT_SECRET=# Go-live notifications (compatibility register C-85): `events` leaves them to Network's live.stream.started# consumer (no direct POST /internal/events/stream-live, no fallback push); only while EVENTS_URL publishes.# Unset = both paths, as before.--#GOLIVE_NOTIFY=events# OpenRestream (roadmap Wave 7): the ingest/restream authority. Unset = off, and every stream slot# is ingested by Live exactly as before. When set, an admin can switch one slot at a time@@@ -1087,14 +1087,13 @@ https://www.101soundboards.com/sounds/12345 150p 2</code></pre><table class="doc-table"><thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead><tbody>--<tr><td>POST</td><td>/internal/events/stream-live</td><td>Notify: stream went live { streamer, stream }</td></tr><tr><td>POST</td><td>/internal/push-single</td><td>Send push to one user { userId, title, body, url }</td></tr><tr><td>POST</td><td>/internal/push-bulk</td><td>Send push to multiple users { userIds[], title, body, url }</td></tr></tbody></table>--<h3>Fallback Webhook (OpenVibe.Live direct)</h3>--<p>If openvibe.network is unreachable, OpenVibe.Live falls back to its built-in Discord webhook sender. Configure the webhook URL in OpenVibe.Live site settings (<code>discord_webhook_url</code>).</p>++<h3>Go-live alerts</h3>++<p>When a stream starts, OpenVibe.Live publishes <code>live.stream.started</code> through OpenVibe.Events, and openvibe.network tells the streamer's followers: inbox, push, email and the Discord alert. Live makes no direct call.</p><h3>Multistream / Restream — <code>/api/restream</code></h3><p>Relay a live stream out to YouTube / Twitch / Kick / custom RTMP. Manage destinations per slot (max 10); stream keys are masked on read. All <code>requireAuth</code>.</p>ddeleted file mode 100644@@@ -1,77 +0,0 @@--'use strict';----const db = require('../db/database');----// Cooldown tracking: streamId → lastNotifyTimestamp--const _cooldowns = new Map();--const DEFAULT_COOLDOWN_MS = 15 * 60 * 1000; // 15 minutes----/**-- * Send a Discord webhook embed when a stream goes live.-- * @param {{ id: number, username: string, display_name?: string, avatar_url?: string }} streamer-- * @param {{ id: number, title?: string }} stream-- */--async function notifyDiscordGoLive(streamer, stream) {-- try {-- const webhookUrl = await db.getSetting('discord_webhook_url');-- if (!webhookUrl) return;---- // Validate URL format-- if (!/^https:\/\/discord\.com\/api\/webhooks\/\d+\/[\w-]+$/.test(webhookUrl) &&-- !/^https:\/\/discordapp\.com\/api\/webhooks\/\d+\/[\w-]+$/.test(webhookUrl)) {-- return;-- }---- // Cooldown check per stream-- const cooldownMs = (await db.getSetting('discord_webhook_cooldown') || 15) * 60 * 1000;-- const lastNotify = _cooldowns.get(stream.id);-- if (lastNotify && (Date.now() - lastNotify) < cooldownMs) return;---- const displayName = streamer.display_name || streamer.username;-- const streamUrl = `https://openvibe.live/@${encodeURIComponent(streamer.username)}`;-- const title = stream.title || 'Started streaming';---- const embed = {-- title: `🔴 ${displayName} is live!`,-- description: title,-- url: streamUrl,-- color: 0xef4444, // red-- timestamp: new Date().toISOString(),-- footer: { text: 'OpenVibe.Live.com' },-- };---- if (streamer.avatar_url) {-- embed.thumbnail = { url: streamer.avatar_url };-- }---- // Custom message template support-- const customContent = await db.getSetting('discord_webhook_message') || null;---- const body = {-- embeds: [embed],-- };-- if (customContent) {-- body.content = customContent-- .replace(/{username}/g, streamer.username)-- .replace(/{display_name}/g, displayName)-- .replace(/{title}/g, title)-- .replace(/{url}/g, streamUrl);-- }---- const res = await fetch(webhookUrl, {-- method: 'POST',-- headers: { 'Content-Type': 'application/json' },-- body: JSON.stringify(body),-- });---- if (res.ok || res.status === 204) {-- _cooldowns.set(stream.id, Date.now());-- } else {-- console.warn(`[discord-webhook] Discord returned ${res.status} for stream ${stream.id}`);-- }-- } catch (err) {-- console.warn('[discord-webhook] Failed to send:', err.message);-- }--}----module.exports = { notifyDiscordGoLive };@@@ -102,7 +102,6 @@ async function apply(event) {if (configId) await db.applyConfigToStream(configId, streamId);} catch (e) { console.warn('[OpenRestream] control config for mirrored stream failed:', e.message); }const stream = await db.getStreamById(streamId) || { id: streamId };-- try { await require('../streaming/golive-notify').notifyFollowersGoLive(user, stream); } catch (e) { console.warn('[OpenRestream] go-live notify failed:', e.message); }try { require('../streaming/live-events').announceGoLive(stream, user); } catch { /* */ }console.log(`[OpenRestream] session ${sessionId} mirrored into stream ${streamId} (${user.username}, slot ${slot.id})`);},ddeleted file mode 100644@@@ -1,117 +0,0 @@--'use strict';--/**-- * "X is live" fan-out — ONE place, called from every go-live path (Go-Live page, OBS/RTMP-- * ingest, WHIP/browser). Sends followers' NETWORK ids to openvibe.network's unified-- * stream-live event (Discord + inbox + push + email), with a fallback to the plain bulk-- * push if the network is unreachable.-- *-- * Deduped per streamer+slot for 60 minutes: reconnects, deploy restarts and the recorder-- * re-creating a session must not spam followers.-- *-- * GOLIVE_NOTIFY=events (compatibility register C-85): leave go-lives to Network's live.stream.started-- * consumer. Live then makes neither the direct call nor the fallback push; the event (queued with-- * the streams row by server/events/stream-events.js, retried until Events has it) is the only path,-- * and Network sends the inbox, push, email and Discord alerts. It applies only while Live publishes-- * events; otherwise, and by default, the direct call stays. While both run, this call nearly always-- * claims Network's per-streamer window first, so the consumer can never show a real `notified`:-- * this switch is how C-85 gets its evidence. Rollback: unset it.-- */--const config = require('../config');--const db = require('../db/database');--const { pushBulkNotification, toNetworkIds, toNetworkId, OV_NETWORK_INTERNAL_URL } = require('../utils/notify');--const { notifyDiscordGoLive } = require('../integrations/discord-webhook');----const DEDUPE_MS = 60 * 60 * 1000;--const _recent = new Map(); // `${userId}:${slot}` → ts----function _channelUrl(streamer) { return `${(config.baseUrl || 'https://openvibe.live').replace(/\/$/, '')}/@${encodeURIComponent(streamer.username)}`; }----/** GOLIVE_NOTIFY=events and Live's events outbox is on: Network's consumer is the only go-live path. */--function leftToEvents() {-- if (String(process.env.GOLIVE_NOTIFY || '').trim().toLowerCase() !== 'events') return false;-- try { return !!require('../events/stream-events').status().enabled; } catch { return false; }--}----async function notifyFollowersGoLive(streamer, stream, { force = false } = {}) {-- if (!streamer || !streamer.id) return;-- if (leftToEvents()) {-- console.log(`[GoLive] ${streamer.username}: left to Network's live.stream.started consumer (GOLIVE_NOTIFY=events)`);-- return;-- }-- const slot = (stream && (stream.managed_stream_id || stream.slot_slug)) || 'default';-- // Keyed by STREAMER (not slot): a person flapping between slots is still one person-- // going live. This in-memory guard is only a fast path — openvibe.network enforces the-- // persisted per-streamer cooldown + daily cap and reports `skipped` back.-- const key = String(streamer.id);-- const now = Date.now();-- if (!force && _recent.get(key) && now - _recent.get(key) < DEDUPE_MS) {-- console.log(`[GoLive] ${streamer.username}: followers already notified for slot ${slot} in the last hour — skipped`);-- return;-- }-- _recent.set(key, now);-- for (const [k, t] of _recent) if (now - t > DEDUPE_MS * 2) _recent.delete(k);---- let followerLiveIds = [];-- try { followerLiveIds = await db.getFollowerIds(streamer.id) || []; } catch { /* */ }-- const { ids: followerNetworkIds, unlinked } = await toNetworkIds(followerLiveIds);-- console.log(`[GoLive] ${streamer.username} (slot ${slot}): ${followerLiveIds.length} follower(s), ${followerNetworkIds.length} reachable on openvibe.network${unlinked ? `, ${unlinked} never linked` : ''}`);---- const payload = {-- streamer: {-- id: streamer.id,-- network_id: await toNetworkId(streamer.id),-- username: streamer.username,-- display_name: streamer.display_name || null,-- avatar_url: streamer.avatar_url || null,-- },-- stream: {-- id: stream?.id || null,-- title: stream?.title || null,-- protocol: stream?.protocol || null,-- managed_stream_id: stream?.managed_stream_id || null,-- url: _channelUrl(streamer),-- },-- follower_network_ids: followerNetworkIds,-- };---- // Live's service token (network.notifications.push).-- const principal = require('../net/network-principal');-- if (!principal.configured()) { await _fallback(streamer, stream, followerNetworkIds); return; }-- principal.headersFor('/internal/events/stream-live').then((auth) => fetch(`${OV_NETWORK_INTERNAL_URL}/internal/events/stream-live`, {-- method: 'POST',-- headers: { 'Content-Type': 'application/json', ...auth },-- body: JSON.stringify(payload),-- })).then(async (r) => {-- if (r.status === 401) principal.tokenRejected('stream-live');-- if (r.ok) {-- let d = null; try { d = await r.json(); } catch { /* */ }-- if (d && d.skipped) { console.log(`[GoLive] ${streamer.username}: network skipped announcement (${d.reason}${d.next_allowed_at ? `, next ${d.next_allowed_at}` : ''})`); return; }-- console.log(`[GoLive] Unified event sent for ${streamer.username}: notifications ${d?.notifications?.sent ?? '?'}/${d?.notifications?.total ?? '?'}, discord ${d?.discord?.sent ? 'sent' : 'no'}`);-- } else {-- console.warn(`[GoLive] Unified event failed (${r.status}), using fallback`);-- await _fallback(streamer, stream, followerNetworkIds);-- }-- }).catch(async err => {-- console.warn('[GoLive] Unified event error, using fallback:', err.message);-- await _fallback(streamer, stream, followerNetworkIds);-- });--}----/** Fallback: direct Discord webhook + bulk push (if openvibe.network is down/unauthorized). */--async function _fallback(streamer, stream, followerNetworkIds) {-- try { await notifyDiscordGoLive(streamer, stream); } catch { /* */ }-- if (!followerNetworkIds.length) return;-- pushBulkNotification(followerNetworkIds, {-- type: 'STREAM_LIVE',-- title: `${streamer.display_name || streamer.username} is live!`,-- message: stream?.title || 'Started streaming',-- icon: '🔴',-- sender_id: await toNetworkId(streamer.id),-- sender_name: streamer.display_name || streamer.username,-- sender_avatar: streamer.avatar_url || null,-- url: _channelUrl(streamer),-- rich_content: { thumbnail: streamer.avatar_url || null, context: { stream_id: stream?.id || null, username: streamer.username, title: stream?.title || 'Started streaming' } },-- }, { alreadyNetworkIds: true });--}----module.exports = { notifyFollowersGoLive, leftToEvents };@@@ -113,17 +113,13 @@ async function ownControlConfigId(req, raw) {return id;}--const { notifyDiscordGoLive } = require('../integrations/discord-webhook');/**-- * Push "X went live" notification via openvibe.network unified event endpoint.-- * This lets openvibe.network handle Discord bot alerts + push notifications centrally.-- * Falls back to direct webhook + bulk push if openvibe.network is unreachable.++ * Followers hear about a go-live from openvibe.network: Live publishes live.stream.started (server/events/++ * stream-events.js, with the streams row), and Network's consumer sends the inbox, push, email and Discord alerts++ * (compatibility register C-85). Live makes no direct call.*/--// Go-live fan-out lives in ./golive-notify (shared with the RTMP and WHIP ingest paths,--// which never notified anyone before), with follower-id translation + 60-min dedupe.--const { notifyFollowersGoLive } = require('./golive-notify');const cache = require('openvibe-shared/cache-policy');function hasOwn(obj, key) {@@@ -1999,8 +1995,6 @@ router.post('/', requireAuth, async (req, res) => {});try { require('../integrations/ai-chatbot-service').startForStream(stream); } catch (aiErr) { console.warn('[AI-Bots] start failed:', aiErr.message); }-- // Notify followers that this streamer went live (fire-and-forget)-- await notifyFollowersGoLive(req.user, stream);// Cross-site on-screen "went live" notification (rate-limited per slot/hour).try { require('./live-events').announceGoLive(stream, req.user); } catch { /* */ }@@@ -9,7 +9,6 @@ const EventEmitter = require('events');const config = require('../config');const db = require('../db/database');const recorder = require('./recorder');--const { notifyDiscordGoLive } = require('../integrations/discord-webhook');const RTMP_HEARTBEAT_INTERVAL_MS = 30000; // Refresh live stream timestamp while RTMP session is active@@@ -213,10 +212,7 @@ class RTMPServer extends EventEmitter {// Discord webhook notification (fire-and-forget)const stream = db.getStreamById ? await db.getStreamById(streamId) : { id: streamId, title: `${resolvedUser.display_name}'s Stream` };-- // Unified go-live event (inbox + push + email to followers, Discord via network;-- // falls back to the webhook). Deduped per slot/hour inside.-- try { await require('./golive-notify').notifyFollowersGoLive(resolvedUser, stream || { id: streamId }); }-- catch (e) { console.warn('[RTMP] go-live notify failed:', e.message); await notifyDiscordGoLive(resolvedUser, stream || { id: streamId }); }++ // Followers hear it from Network (live.stream.started via Events); the page banner comes from live-events.try { require('./live-events').announceGoLive(stream || { id: streamId }, resolvedUser); } catch { /* */ }// Start server-side VOD recording via FFmpeg@@@ -14,7 +14,6 @@ const webrtcSFU = require('./webrtc-sfu');const { verifyToken, resolveNetworkUser } = require('../auth/auth');const db = require('../db/database');const recorder = require('./recorder');--const { notifyDiscordGoLive } = require('../integrations/discord-webhook');let chatRelayService;try { chatRelayService = require('../integrations/chat-relay-service'); } catch {}@@@ -611,8 +610,6 @@ async function autoCreateWhipSession(managedStream, user) {console.log(`[WHIP] Auto-created live session ${streamId} for WHIP slot ${managedStream.id} (user: ${user.username})`);// Fire-and-forget side effects-- try { await require('./golive-notify').notifyFollowersGoLive(user, stream || { id: streamId }); }-- catch (e) { console.warn('[WHIP] go-live notify failed:', e.message); await notifyDiscordGoLive(user, stream || { id: streamId }); }try { require('./live-events').announceGoLive(stream || { id: streamId }, user); } catch { /* */ }if (chatRelayService) {chatRelayService.startForStream(stream).catch(() => {});ddeleted file mode 100644@@@ -1,66 +0,0 @@--'use strict';--// GOLIVE_NOTIFY=events (compatibility register C-85): with Live's events outbox on, a go-live makes no call to--// Network (no POST /internal/events/stream-live, no fallback push) and leaves the followers to Network's--// live.stream.started consumer; the event itself is still queued with the streams row. Unset, or with the--// outbox off, the direct call stays.----const assert = require('assert');----process.env.OV_OAUTH_CLIENT_SECRET = 'live-secret';--process.env.OV_NETWORK_INTERNAL_URL = 'http://127.0.0.1:9';--const { tokenBody } = require('./helpers/network-token-stub');----const calls = [];--global.fetch = async (url, init = {}) => {-- if (String(url).endsWith('/oauth/token')) return new Response(JSON.stringify(tokenBody()), { status: 200, headers: { 'content-type': 'application/json' } });-- calls.push({ url: String(url), method: init.method || 'GET' });-- return new Response(JSON.stringify({ ok: true, notifications: { sent: 0, total: 0 } }), { status: 200, headers: { 'content-type': 'application/json' } });--};--const logs = [];--const log = console.log;--console.log = (...a) => { logs.push(a.join(' ')); };----(async () => {-- try {-- const db = require('../server/db/database');-- await db.initDb();-- await db.getDb().prepare("INSERT INTO users (id, username, display_name, password_hash) OVERRIDING SYSTEM VALUE VALUES (601, 'caster', 'Caster', 'x')").run();-- const streamEvents = require('../server/events/stream-events');-- const { notifyFollowersGoLive, leftToEvents } = require('../server/streaming/golive-notify');-- const settle = () => new Promise((r) => setTimeout(r, 50));-- const toNetwork = () => calls.filter((c) => c.url.startsWith('http://127.0.0.1:9/')).length;---- // Default: the direct call, as before.-- delete process.env.GOLIVE_NOTIFY;-- assert.strictEqual(leftToEvents(), false);-- await notifyFollowersGoLive({ id: 601, username: 'caster' }, { id: 1, title: 'one' });-- await settle();-- assert.ok(toNetwork() >= 1, 'unset: Live calls Network directly');---- // GOLIVE_NOTIFY=events while the outbox is off: still the direct call (nothing else would tell anyone).-- process.env.GOLIVE_NOTIFY = 'events';-- assert.strictEqual(streamEvents.status().enabled, false);-- assert.strictEqual(leftToEvents(), false);---- // With the outbox on: no call at all, and the log says who announces.-- streamEvents.init({ eventsUrl: 'http://127.0.0.1:9', clientSecret: 's3cret', intervalMs: 60000 });-- assert.strictEqual(leftToEvents(), true);-- const before = calls.length;-- await notifyFollowersGoLive({ id: 601, username: 'caster' }, { id: 2, title: 'two' }, { force: true });-- await settle();-- assert.deepStrictEqual(calls.slice(before).filter((c) => /\/internal\//.test(c.url)), [], 'no direct call, no fallback push');-- assert.ok(logs.some((l) => l.includes("caster: left to Network's live.stream.started consumer (GOLIVE_NOTIFY=events)")));-- // The event path is intact: a new stream row still queues live.stream.started.-- await db.createStream({ user_id: 601, title: 'three', protocol: 'rtmp' });-- assert.strictEqual((await db.getDb().prepare("SELECT COUNT(*) AS c FROM event_outbox WHERE envelope::text ILIKE '%live.stream.started%'").get()).c, 1);---- // Any other value keeps both paths.-- process.env.GOLIVE_NOTIFY = 'both';-- assert.strictEqual(leftToEvents(), false);-- streamEvents._reset();-- } finally {-- console.log = log;-- }-- console.log('golive notify mode: all checks passed');-- process.exit(0);--})().catch((err) => { console.log = log; console.error(err); process.exit(1); });@@@ -351,7 +351,6 @@ async function check(name, fn) {'server/chat/chat-client.js': 'OpenVibe.Chat typed ingress (configured OV_CHAT_INTERNAL_URL, fixed /internal/chat/* paths)','server/chat/chat-delivery.js': 'OpenVibe.Tools anon resolve (configured)','server/streaming/calls-authority.js': 'OpenVibe.Chat (configured)',-- 'server/streaming/golive-notify.js': 'OpenVibe.Network (configured)','server/utils/notify.js': 'OpenVibe.Network (configured)','server/auth/identity-sync.js': 'OpenVibe.Network (configured)','server/auth/account-data.js': 'OpenVibe.Network (configured): export parts and deletion confirmations',@@@ -373,7 +372,6 @@ async function check(name, fn) {'server/news/sources/rss-source.js': 'feeds the site owner configures','server/integrations/platform-oauth.js': 'Twitch / YouTube / Kick OAuth (fixed hosts)','server/integrations/powerchat-oauth.js': 'PowerChat (owner setting)',-- 'server/integrations/discord-webhook.js': 'Discord webhook (owner-only setting)','server/integrations/robotstreamer-service.js': 'RobotStreamer API and the endpoints it returns','server/integrations/rs-passthrough-relay.js': 'RobotStreamer SFU (from its API)','server/integrations/chat-relay-service.js': 'YouTube API (fixed host); user channel pages via egress.fetchText',@@@ -101,20 +101,6 @@ const SEED = `(async () => {le.subscribe({ on() {} }, { writeHead() {}, write(s) { writes.push(String(s)); } });await le.announceGoLive(raw, streamer);out.sse = writes.join('');-- const posted = [];-- const realFetch = global.fetch;-- // Network mints Live's service token, then takes the go-live call; only calls to Network's internal API are kept-- // (anything else this process sends meanwhile, a Media read from a background job, is not the go-live call).-- global.fetch = async (url, opts = {}) => {-- if (String(url).endsWith('/oauth/token')) return { ok: true, status: 200, json: async () => ({ access_token: 'golive-service-token', expires_in: 300, token_type: 'Bearer' }) };-- if (String(url).startsWith('http://127.0.0.1:9/internal/')) posted.push(String(opts.body || ''));-- return { ok: true, status: 200, json: async () => ({}) };-- };-- try {-- await require('./server/streaming/golive-notify').notifyFollowersGoLive(streamer, raw, { force: true });-- for (let i = 0; i < 50 && !posted.length; i++) await new Promise((r) => setTimeout(r, 100));-- } finally { global.fetch = realFetch; }-- out.goLive = posted;out.rawHadKey = raw.managed_stream_key === K.slotMain && streamer.stream_key === K.account;return { ids: { owner, fan, peer, mod, admin, channel: ch.id, slot, slot2, live, ended, cam }, out };})()`;@@@ -145,10 +131,6 @@ function hits(text, needles) {assert.match(out.sse, /stream-live/);assert.deepStrictEqual(hits(out.sse, K), []);});-- await check('the go-live call to Network carries no credential', () => {-- assert.ok(out.goLive.length >= 1, 'Network was called');-- assert.deepStrictEqual(hits(out.goLive.join('\n'), K), []);-- });// ── The booted server ──const keys = crawl.networkKeys(tmp);@@@ -56,7 +56,6 @@ require.cache[require.resolve('../server/streaming/broadcast-server')] = { id: 'const recorder = require('../server/streaming/recorder');recorder.startRecording = () => {};recorder.stopRecording = () => {};--require('../server/streaming/golive-notify').notifyFollowersGoLive = () => {};require('../server/streaming/live-events').announceGoLive = () => {};const rtmp = require('../server/streaming/rtmp-server');rtmp.start();
@@ -63,7 +63,6 @@ OV_OAUTH_CLIENT_SECRET=
@@ -63,7 +63,6 @@ OV_OAUTH_CLIENT_SECRET=
# Go-live notifications (compatibility register C-85): `events` leaves them to Network's live.stream.started
# Go-live notifications (compatibility register C-85): `events` leaves them to Network's live.stream.started
# consumer (no direct POST /internal/events/stream-live, no fallback push); only while EVENTS_URL publishes.
# consumer (no direct POST /internal/events/stream-live, no fallback push); only while EVENTS_URL publishes.
# Unset = both paths, as before.
# Unset = both paths, as before.
-#GOLIVE_NOTIFY=events
# OpenRestream (roadmap Wave 7): the ingest/restream authority. Unset = off, and every stream slot
# OpenRestream (roadmap Wave 7): the ingest/restream authority. Unset = off, and every stream slot
# is ingested by Live exactly as before. When set, an admin can switch one slot at a time
# is ingested by Live exactly as before. When set, an admin can switch one slot at a time
@@ -1087,14 +1087,13 @@ https://www.101soundboards.com/sounds/12345 150p 2</code></pre>
@@ -1087,14 +1087,13 @@ https://www.101soundboards.com/sounds/12345 150p 2</code></pre>
<table class="doc-table">
<table class="doc-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tbody>
-<tr><td>POST</td><td>/internal/events/stream-live</td><td>Notify: stream went live { streamer, stream }</td></tr>
<tr><td>POST</td><td>/internal/push-single</td><td>Send push to one user { userId, title, body, url }</td></tr>
<tr><td>POST</td><td>/internal/push-single</td><td>Send push to one user { userId, title, body, url }</td></tr>
<tr><td>POST</td><td>/internal/push-bulk</td><td>Send push to multiple users { userIds[], title, body, url }</td></tr>
<tr><td>POST</td><td>/internal/push-bulk</td><td>Send push to multiple users { userIds[], title, body, url }</td></tr>
</tbody>
</tbody>
</table>
</table>
-<h3>Fallback Webhook (OpenVibe.Live direct)</h3>
-<p>If openvibe.network is unreachable, OpenVibe.Live falls back to its built-in Discord webhook sender. Configure the webhook URL in OpenVibe.Live site settings (<code>discord_webhook_url</code>).</p>
+<h3>Go-live alerts</h3>
+<p>When a stream starts, OpenVibe.Live publishes <code>live.stream.started</code> through OpenVibe.Events, and openvibe.network tells the streamer's followers: inbox, push, email and the Discord alert. Live makes no direct call.</p>
<h3>Multistream / Restream — <code>/api/restream</code></h3>
<h3>Multistream / Restream — <code>/api/restream</code></h3>
<p>Relay a live stream out to YouTube / Twitch / Kick / custom RTMP. Manage destinations per slot (max 10); stream keys are masked on read. All <code>requireAuth</code>.</p>
<p>Relay a live stream out to YouTube / Twitch / Kick / custom RTMP. Manage destinations per slot (max 10); stream keys are masked on read. All <code>requireAuth</code>.</p>
deleted file mode 100644
deleted file mode 100644
@@ -1,77 +0,0 @@
@@ -1,77 +0,0 @@
-'use strict';
-
-const db = require('../db/database');
-
-// Cooldown tracking: streamId → lastNotifyTimestamp
-const _cooldowns = new Map();
-const DEFAULT_COOLDOWN_MS = 15 * 60 * 1000; // 15 minutes
-
-/**
- * Send a Discord webhook embed when a stream goes live.
- * @param {{ id: number, username: string, display_name?: string, avatar_url?: string }} streamer
- * @param {{ id: number, title?: string }} stream
- */
-async function notifyDiscordGoLive(streamer, stream) {
- try {
- const webhookUrl = await db.getSetting('discord_webhook_url');
- if (!webhookUrl) return;
-
- // Validate URL format
- if (!/^https:\/\/discord\.com\/api\/webhooks\/\d+\/[\w-]+$/.test(webhookUrl) &&
- !/^https:\/\/discordapp\.com\/api\/webhooks\/\d+\/[\w-]+$/.test(webhookUrl)) {
- return;
- }
-
- // Cooldown check per stream
- const cooldownMs = (await db.getSetting('discord_webhook_cooldown') || 15) * 60 * 1000;
- const lastNotify = _cooldowns.get(stream.id);
- if (lastNotify && (Date.now() - lastNotify) < cooldownMs) return;
-
- const displayName = streamer.display_name || streamer.username;
- const streamUrl = `https://openvibe.live/@${encodeURIComponent(streamer.username)}`;
- const title = stream.title || 'Started streaming';
-
- const embed = {
- title: `🔴 ${displayName} is live!`,
- description: title,
- url: streamUrl,
- color: 0xef4444, // red
- timestamp: new Date().toISOString(),
- footer: { text: 'OpenVibe.Live.com' },
- };
-
- if (streamer.avatar_url) {
- embed.thumbnail = { url: streamer.avatar_url };
- }
-
- // Custom message template support
- const customContent = await db.getSetting('discord_webhook_message') || null;
-
- const body = {
- embeds: [embed],
- };
- if (customContent) {
- body.content = customContent
- .replace(/{username}/g, streamer.username)
- .replace(/{display_name}/g, displayName)
- .replace(/{title}/g, title)
- .replace(/{url}/g, streamUrl);
- }
-
- const res = await fetch(webhookUrl, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body),
- });
-
- if (res.ok || res.status === 204) {
- _cooldowns.set(stream.id, Date.now());
- } else {
- console.warn(`[discord-webhook] Discord returned ${res.status} for stream ${stream.id}`);
- }
- } catch (err) {
- console.warn('[discord-webhook] Failed to send:', err.message);
- }
-}
-
-module.exports = { notifyDiscordGoLive };
@@ -102,7 +102,6 @@ async function apply(event) {
@@ -102,7 +102,6 @@ async function apply(event) {
if (configId) await db.applyConfigToStream(configId, streamId);
if (configId) await db.applyConfigToStream(configId, streamId);
} catch (e) { console.warn('[OpenRestream] control config for mirrored stream failed:', e.message); }
} catch (e) { console.warn('[OpenRestream] control config for mirrored stream failed:', e.message); }
const stream = await db.getStreamById(streamId) || { id: streamId };
const stream = await db.getStreamById(streamId) || { id: streamId };
- try { await require('../streaming/golive-notify').notifyFollowersGoLive(user, stream); } catch (e) { console.warn('[OpenRestream] go-live notify failed:', e.message); }
try { require('../streaming/live-events').announceGoLive(stream, user); } catch { /* */ }
try { require('../streaming/live-events').announceGoLive(stream, user); } catch { /* */ }
console.log(`[OpenRestream] session ${sessionId} mirrored into stream ${streamId} (${user.username}, slot ${slot.id})`);
console.log(`[OpenRestream] session ${sessionId} mirrored into stream ${streamId} (${user.username}, slot ${slot.id})`);
},
},
deleted file mode 100644
deleted file mode 100644
@@ -1,117 +0,0 @@
@@ -1,117 +0,0 @@
-'use strict';
-/**
- * "X is live" fan-out — ONE place, called from every go-live path (Go-Live page, OBS/RTMP
- * ingest, WHIP/browser). Sends followers' NETWORK ids to openvibe.network's unified
- * stream-live event (Discord + inbox + push + email), with a fallback to the plain bulk
- * push if the network is unreachable.
- *
- * Deduped per streamer+slot for 60 minutes: reconnects, deploy restarts and the recorder
- * re-creating a session must not spam followers.
- *
- * GOLIVE_NOTIFY=events (compatibility register C-85): leave go-lives to Network's live.stream.started
- * consumer. Live then makes neither the direct call nor the fallback push; the event (queued with
- * the streams row by server/events/stream-events.js, retried until Events has it) is the only path,
- * and Network sends the inbox, push, email and Discord alerts. It applies only while Live publishes
- * events; otherwise, and by default, the direct call stays. While both run, this call nearly always
- * claims Network's per-streamer window first, so the consumer can never show a real `notified`:
- * this switch is how C-85 gets its evidence. Rollback: unset it.
- */
-const config = require('../config');
-const db = require('../db/database');
-const { pushBulkNotification, toNetworkIds, toNetworkId, OV_NETWORK_INTERNAL_URL } = require('../utils/notify');
-const { notifyDiscordGoLive } = require('../integrations/discord-webhook');
-
-const DEDUPE_MS = 60 * 60 * 1000;
-const _recent = new Map(); // `${userId}:${slot}` → ts
-
-function _channelUrl(streamer) { return `${(config.baseUrl || 'https://openvibe.live').replace(/\/$/, '')}/@${encodeURIComponent(streamer.username)}`; }
-
-/** GOLIVE_NOTIFY=events and Live's events outbox is on: Network's consumer is the only go-live path. */
-function leftToEvents() {
- if (String(process.env.GOLIVE_NOTIFY || '').trim().toLowerCase() !== 'events') return false;
- try { return !!require('../events/stream-events').status().enabled; } catch { return false; }
-}
-
-async function notifyFollowersGoLive(streamer, stream, { force = false } = {}) {
- if (!streamer || !streamer.id) return;
- if (leftToEvents()) {
- console.log(`[GoLive] ${streamer.username}: left to Network's live.stream.started consumer (GOLIVE_NOTIFY=events)`);
- return;
- }
- const slot = (stream && (stream.managed_stream_id || stream.slot_slug)) || 'default';
- // Keyed by STREAMER (not slot): a person flapping between slots is still one person
- // going live. This in-memory guard is only a fast path — openvibe.network enforces the
- // persisted per-streamer cooldown + daily cap and reports `skipped` back.
- const key = String(streamer.id);
- const now = Date.now();
- if (!force && _recent.get(key) && now - _recent.get(key) < DEDUPE_MS) {
- console.log(`[GoLive] ${streamer.username}: followers already notified for slot ${slot} in the last hour — skipped`);
- return;
- }
- _recent.set(key, now);
- for (const [k, t] of _recent) if (now - t > DEDUPE_MS * 2) _recent.delete(k);
-
- let followerLiveIds = [];
- try { followerLiveIds = await db.getFollowerIds(streamer.id) || []; } catch { /* */ }
- const { ids: followerNetworkIds, unlinked } = await toNetworkIds(followerLiveIds);
- console.log(`[GoLive] ${streamer.username} (slot ${slot}): ${followerLiveIds.length} follower(s), ${followerNetworkIds.length} reachable on openvibe.network${unlinked ? `, ${unlinked} never linked` : ''}`);
-
- const payload = {
- streamer: {
- id: streamer.id,
- network_id: await toNetworkId(streamer.id),
- username: streamer.username,
- display_name: streamer.display_name || null,
- avatar_url: streamer.avatar_url || null,
- },
- stream: {
- id: stream?.id || null,
- title: stream?.title || null,
- protocol: stream?.protocol || null,
- managed_stream_id: stream?.managed_stream_id || null,
- url: _channelUrl(streamer),
- },
- follower_network_ids: followerNetworkIds,
- };
-
- // Live's service token (network.notifications.push).
- const principal = require('../net/network-principal');
- if (!principal.configured()) { await _fallback(streamer, stream, followerNetworkIds); return; }
- principal.headersFor('/internal/events/stream-live').then((auth) => fetch(`${OV_NETWORK_INTERNAL_URL}/internal/events/stream-live`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', ...auth },
- body: JSON.stringify(payload),
- })).then(async (r) => {
- if (r.status === 401) principal.tokenRejected('stream-live');
- if (r.ok) {
- let d = null; try { d = await r.json(); } catch { /* */ }
- if (d && d.skipped) { console.log(`[GoLive] ${streamer.username}: network skipped announcement (${d.reason}${d.next_allowed_at ? `, next ${d.next_allowed_at}` : ''})`); return; }
- console.log(`[GoLive] Unified event sent for ${streamer.username}: notifications ${d?.notifications?.sent ?? '?'}/${d?.notifications?.total ?? '?'}, discord ${d?.discord?.sent ? 'sent' : 'no'}`);
- } else {
- console.warn(`[GoLive] Unified event failed (${r.status}), using fallback`);
- await _fallback(streamer, stream, followerNetworkIds);
- }
- }).catch(async err => {
- console.warn('[GoLive] Unified event error, using fallback:', err.message);
- await _fallback(streamer, stream, followerNetworkIds);
- });
-}
-
-/** Fallback: direct Discord webhook + bulk push (if openvibe.network is down/unauthorized). */
-async function _fallback(streamer, stream, followerNetworkIds) {
- try { await notifyDiscordGoLive(streamer, stream); } catch { /* */ }
- if (!followerNetworkIds.length) return;
- pushBulkNotification(followerNetworkIds, {
- type: 'STREAM_LIVE',
- title: `${streamer.display_name || streamer.username} is live!`,
- message: stream?.title || 'Started streaming',
- icon: '🔴',
- sender_id: await toNetworkId(streamer.id),
- sender_name: streamer.display_name || streamer.username,
- sender_avatar: streamer.avatar_url || null,
- url: _channelUrl(streamer),
- rich_content: { thumbnail: streamer.avatar_url || null, context: { stream_id: stream?.id || null, username: streamer.username, title: stream?.title || 'Started streaming' } },
- }, { alreadyNetworkIds: true });
-}
-
-module.exports = { notifyFollowersGoLive, leftToEvents };
@@ -113,17 +113,13 @@ async function ownControlConfigId(req, raw) {
@@ -113,17 +113,13 @@ async function ownControlConfigId(req, raw) {
return id;
return id;
}
}
-const { notifyDiscordGoLive } = require('../integrations/discord-webhook');
/**
/**
- * Push "X went live" notification via openvibe.network unified event endpoint.
- * This lets openvibe.network handle Discord bot alerts + push notifications centrally.
- * Falls back to direct webhook + bulk push if openvibe.network is unreachable.
+ * Followers hear about a go-live from openvibe.network: Live publishes live.stream.started (server/events/
+ * stream-events.js, with the streams row), and Network's consumer sends the inbox, push, email and Discord alerts
+ * (compatibility register C-85). Live makes no direct call.
*/
*/
-// Go-live fan-out lives in ./golive-notify (shared with the RTMP and WHIP ingest paths,
-// which never notified anyone before), with follower-id translation + 60-min dedupe.
-const { notifyFollowersGoLive } = require('./golive-notify');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
function hasOwn(obj, key) {
function hasOwn(obj, key) {
@@ -1999,8 +1995,6 @@ router.post('/', requireAuth, async (req, res) => {
@@ -1999,8 +1995,6 @@ router.post('/', requireAuth, async (req, res) => {
});
});
try { require('../integrations/ai-chatbot-service').startForStream(stream); } catch (aiErr) { console.warn('[AI-Bots] start failed:', aiErr.message); }
try { require('../integrations/ai-chatbot-service').startForStream(stream); } catch (aiErr) { console.warn('[AI-Bots] start failed:', aiErr.message); }
- // Notify followers that this streamer went live (fire-and-forget)
- await notifyFollowersGoLive(req.user, stream);
// Cross-site on-screen "went live" notification (rate-limited per slot/hour).
// Cross-site on-screen "went live" notification (rate-limited per slot/hour).
try { require('./live-events').announceGoLive(stream, req.user); } catch { /* */ }
try { require('./live-events').announceGoLive(stream, req.user); } catch { /* */ }
@@ -9,7 +9,6 @@ const EventEmitter = require('events');
@@ -9,7 +9,6 @@ const EventEmitter = require('events');
const config = require('../config');
const config = require('../config');
const db = require('../db/database');
const db = require('../db/database');
const recorder = require('./recorder');
const recorder = require('./recorder');
-const { notifyDiscordGoLive } = require('../integrations/discord-webhook');
const RTMP_HEARTBEAT_INTERVAL_MS = 30000; // Refresh live stream timestamp while RTMP session is active
const RTMP_HEARTBEAT_INTERVAL_MS = 30000; // Refresh live stream timestamp while RTMP session is active
@@ -213,10 +212,7 @@ class RTMPServer extends EventEmitter {
@@ -213,10 +212,7 @@ class RTMPServer extends EventEmitter {
// Discord webhook notification (fire-and-forget)
// Discord webhook notification (fire-and-forget)
const stream = db.getStreamById ? await db.getStreamById(streamId) : { id: streamId, title: `${resolvedUser.display_name}'s Stream` };
const stream = db.getStreamById ? await db.getStreamById(streamId) : { id: streamId, title: `${resolvedUser.display_name}'s Stream` };
- // Unified go-live event (inbox + push + email to followers, Discord via network;
- // falls back to the webhook). Deduped per slot/hour inside.
- try { await require('./golive-notify').notifyFollowersGoLive(resolvedUser, stream || { id: streamId }); }
- catch (e) { console.warn('[RTMP] go-live notify failed:', e.message); await notifyDiscordGoLive(resolvedUser, stream || { id: streamId }); }
+ // Followers hear it from Network (live.stream.started via Events); the page banner comes from live-events.
try { require('./live-events').announceGoLive(stream || { id: streamId }, resolvedUser); } catch { /* */ }
try { require('./live-events').announceGoLive(stream || { id: streamId }, resolvedUser); } catch { /* */ }
// Start server-side VOD recording via FFmpeg
// Start server-side VOD recording via FFmpeg
@@ -14,7 +14,6 @@ const webrtcSFU = require('./webrtc-sfu');
@@ -14,7 +14,6 @@ const webrtcSFU = require('./webrtc-sfu');
const { verifyToken, resolveNetworkUser } = require('../auth/auth');
const { verifyToken, resolveNetworkUser } = require('../auth/auth');
const db = require('../db/database');
const db = require('../db/database');
const recorder = require('./recorder');
const recorder = require('./recorder');
-const { notifyDiscordGoLive } = require('../integrations/discord-webhook');
let chatRelayService;
let chatRelayService;
try { chatRelayService = require('../integrations/chat-relay-service'); } catch {}
try { chatRelayService = require('../integrations/chat-relay-service'); } catch {}
@@ -611,8 +610,6 @@ async function autoCreateWhipSession(managedStream, user) {
@@ -611,8 +610,6 @@ async function autoCreateWhipSession(managedStream, user) {
console.log(`[WHIP] Auto-created live session ${streamId} for WHIP slot ${managedStream.id} (user: ${user.username})`);
console.log(`[WHIP] Auto-created live session ${streamId} for WHIP slot ${managedStream.id} (user: ${user.username})`);
// Fire-and-forget side effects
// Fire-and-forget side effects
- try { await require('./golive-notify').notifyFollowersGoLive(user, stream || { id: streamId }); }
- catch (e) { console.warn('[WHIP] go-live notify failed:', e.message); await notifyDiscordGoLive(user, stream || { id: streamId }); }
try { require('./live-events').announceGoLive(stream || { id: streamId }, user); } catch { /* */ }
try { require('./live-events').announceGoLive(stream || { id: streamId }, user); } catch { /* */ }
if (chatRelayService) {
if (chatRelayService) {
chatRelayService.startForStream(stream).catch(() => {});
chatRelayService.startForStream(stream).catch(() => {});
deleted file mode 100644
deleted file mode 100644
@@ -1,66 +0,0 @@
@@ -1,66 +0,0 @@
-'use strict';
-// GOLIVE_NOTIFY=events (compatibility register C-85): with Live's events outbox on, a go-live makes no call to
-// Network (no POST /internal/events/stream-live, no fallback push) and leaves the followers to Network's
-// live.stream.started consumer; the event itself is still queued with the streams row. Unset, or with the
-// outbox off, the direct call stays.
-
-const assert = require('assert');
-
-process.env.OV_OAUTH_CLIENT_SECRET = 'live-secret';
-process.env.OV_NETWORK_INTERNAL_URL = 'http://127.0.0.1:9';
-const { tokenBody } = require('./helpers/network-token-stub');
-
-const calls = [];
-global.fetch = async (url, init = {}) => {
- if (String(url).endsWith('/oauth/token')) return new Response(JSON.stringify(tokenBody()), { status: 200, headers: { 'content-type': 'application/json' } });
- calls.push({ url: String(url), method: init.method || 'GET' });
- return new Response(JSON.stringify({ ok: true, notifications: { sent: 0, total: 0 } }), { status: 200, headers: { 'content-type': 'application/json' } });
-};
-const logs = [];
-const log = console.log;
-console.log = (...a) => { logs.push(a.join(' ')); };
-
-(async () => {
- try {
- const db = require('../server/db/database');
- await db.initDb();
- await db.getDb().prepare("INSERT INTO users (id, username, display_name, password_hash) OVERRIDING SYSTEM VALUE VALUES (601, 'caster', 'Caster', 'x')").run();
- const streamEvents = require('../server/events/stream-events');
- const { notifyFollowersGoLive, leftToEvents } = require('../server/streaming/golive-notify');
- const settle = () => new Promise((r) => setTimeout(r, 50));
- const toNetwork = () => calls.filter((c) => c.url.startsWith('http://127.0.0.1:9/')).length;
-
- // Default: the direct call, as before.
- delete process.env.GOLIVE_NOTIFY;
- assert.strictEqual(leftToEvents(), false);
- await notifyFollowersGoLive({ id: 601, username: 'caster' }, { id: 1, title: 'one' });
- await settle();
- assert.ok(toNetwork() >= 1, 'unset: Live calls Network directly');
-
- // GOLIVE_NOTIFY=events while the outbox is off: still the direct call (nothing else would tell anyone).
- process.env.GOLIVE_NOTIFY = 'events';
- assert.strictEqual(streamEvents.status().enabled, false);
- assert.strictEqual(leftToEvents(), false);
-
- // With the outbox on: no call at all, and the log says who announces.
- streamEvents.init({ eventsUrl: 'http://127.0.0.1:9', clientSecret: 's3cret', intervalMs: 60000 });
- assert.strictEqual(leftToEvents(), true);
- const before = calls.length;
- await notifyFollowersGoLive({ id: 601, username: 'caster' }, { id: 2, title: 'two' }, { force: true });
- await settle();
- assert.deepStrictEqual(calls.slice(before).filter((c) => /\/internal\//.test(c.url)), [], 'no direct call, no fallback push');
- assert.ok(logs.some((l) => l.includes("caster: left to Network's live.stream.started consumer (GOLIVE_NOTIFY=events)")));
- // The event path is intact: a new stream row still queues live.stream.started.
- await db.createStream({ user_id: 601, title: 'three', protocol: 'rtmp' });
- assert.strictEqual((await db.getDb().prepare("SELECT COUNT(*) AS c FROM event_outbox WHERE envelope::text ILIKE '%live.stream.started%'").get()).c, 1);
-
- // Any other value keeps both paths.
- process.env.GOLIVE_NOTIFY = 'both';
- assert.strictEqual(leftToEvents(), false);
- streamEvents._reset();
- } finally {
- console.log = log;
- }
- console.log('golive notify mode: all checks passed');
- process.exit(0);
-})().catch((err) => { console.log = log; console.error(err); process.exit(1); });
@@ -351,7 +351,6 @@ async function check(name, fn) {
@@ -351,7 +351,6 @@ async function check(name, fn) {
'server/chat/chat-client.js': 'OpenVibe.Chat typed ingress (configured OV_CHAT_INTERNAL_URL, fixed /internal/chat/* paths)',
'server/chat/chat-client.js': 'OpenVibe.Chat typed ingress (configured OV_CHAT_INTERNAL_URL, fixed /internal/chat/* paths)',
'server/chat/chat-delivery.js': 'OpenVibe.Tools anon resolve (configured)',
'server/chat/chat-delivery.js': 'OpenVibe.Tools anon resolve (configured)',
'server/streaming/calls-authority.js': 'OpenVibe.Chat (configured)',
'server/streaming/calls-authority.js': 'OpenVibe.Chat (configured)',
- 'server/streaming/golive-notify.js': 'OpenVibe.Network (configured)',
'server/utils/notify.js': 'OpenVibe.Network (configured)',
'server/utils/notify.js': 'OpenVibe.Network (configured)',
'server/auth/identity-sync.js': 'OpenVibe.Network (configured)',
'server/auth/identity-sync.js': 'OpenVibe.Network (configured)',
'server/auth/account-data.js': 'OpenVibe.Network (configured): export parts and deletion confirmations',
'server/auth/account-data.js': 'OpenVibe.Network (configured): export parts and deletion confirmations',
@@ -373,7 +372,6 @@ async function check(name, fn) {
@@ -373,7 +372,6 @@ async function check(name, fn) {
'server/news/sources/rss-source.js': 'feeds the site owner configures',
'server/news/sources/rss-source.js': 'feeds the site owner configures',
'server/integrations/platform-oauth.js': 'Twitch / YouTube / Kick OAuth (fixed hosts)',
'server/integrations/platform-oauth.js': 'Twitch / YouTube / Kick OAuth (fixed hosts)',
'server/integrations/powerchat-oauth.js': 'PowerChat (owner setting)',
'server/integrations/powerchat-oauth.js': 'PowerChat (owner setting)',
- 'server/integrations/discord-webhook.js': 'Discord webhook (owner-only setting)',
'server/integrations/robotstreamer-service.js': 'RobotStreamer API and the endpoints it returns',
'server/integrations/robotstreamer-service.js': 'RobotStreamer API and the endpoints it returns',
'server/integrations/rs-passthrough-relay.js': 'RobotStreamer SFU (from its API)',
'server/integrations/rs-passthrough-relay.js': 'RobotStreamer SFU (from its API)',
'server/integrations/chat-relay-service.js': 'YouTube API (fixed host); user channel pages via egress.fetchText',
'server/integrations/chat-relay-service.js': 'YouTube API (fixed host); user channel pages via egress.fetchText',
@@ -101,20 +101,6 @@ const SEED = `(async () => {
@@ -101,20 +101,6 @@ const SEED = `(async () => {
le.subscribe({ on() {} }, { writeHead() {}, write(s) { writes.push(String(s)); } });
le.subscribe({ on() {} }, { writeHead() {}, write(s) { writes.push(String(s)); } });
await le.announceGoLive(raw, streamer);
await le.announceGoLive(raw, streamer);
out.sse = writes.join('');
out.sse = writes.join('');
- const posted = [];
- const realFetch = global.fetch;
- // Network mints Live's service token, then takes the go-live call; only calls to Network's internal API are kept
- // (anything else this process sends meanwhile, a Media read from a background job, is not the go-live call).
- global.fetch = async (url, opts = {}) => {
- if (String(url).endsWith('/oauth/token')) return { ok: true, status: 200, json: async () => ({ access_token: 'golive-service-token', expires_in: 300, token_type: 'Bearer' }) };
- if (String(url).startsWith('http://127.0.0.1:9/internal/')) posted.push(String(opts.body || ''));
- return { ok: true, status: 200, json: async () => ({}) };
- };
- try {
- await require('./server/streaming/golive-notify').notifyFollowersGoLive(streamer, raw, { force: true });
- for (let i = 0; i < 50 && !posted.length; i++) await new Promise((r) => setTimeout(r, 100));
- } finally { global.fetch = realFetch; }
- out.goLive = posted;
out.rawHadKey = raw.managed_stream_key === K.slotMain && streamer.stream_key === K.account;
out.rawHadKey = raw.managed_stream_key === K.slotMain && streamer.stream_key === K.account;
return { ids: { owner, fan, peer, mod, admin, channel: ch.id, slot, slot2, live, ended, cam }, out };
return { ids: { owner, fan, peer, mod, admin, channel: ch.id, slot, slot2, live, ended, cam }, out };
})()`;
})()`;
@@ -145,10 +131,6 @@ function hits(text, needles) {
@@ -145,10 +131,6 @@ function hits(text, needles) {
assert.match(out.sse, /stream-live/);
assert.match(out.sse, /stream-live/);
assert.deepStrictEqual(hits(out.sse, K), []);
assert.deepStrictEqual(hits(out.sse, K), []);
});
});
- await check('the go-live call to Network carries no credential', () => {
- assert.ok(out.goLive.length >= 1, 'Network was called');
- assert.deepStrictEqual(hits(out.goLive.join('\n'), K), []);
- });
// ── The booted server ──
// ── The booted server ──
const keys = crawl.networkKeys(tmp);
const keys = crawl.networkKeys(tmp);
@@ -56,7 +56,6 @@ require.cache[require.resolve('../server/streaming/broadcast-server')] = { id: '
@@ -56,7 +56,6 @@ require.cache[require.resolve('../server/streaming/broadcast-server')] = { id: '
const recorder = require('../server/streaming/recorder');
const recorder = require('../server/streaming/recorder');
recorder.startRecording = () => {};
recorder.startRecording = () => {};
recorder.stopRecording = () => {};
recorder.stopRecording = () => {};
-require('../server/streaming/golive-notify').notifyFollowersGoLive = () => {};
require('../server/streaming/live-events').announceGoLive = () => {};
require('../server/streaming/live-events').announceGoLive = () => {};
const rtmp = require('../server/streaming/rtmp-server');
const rtmp = require('../server/streaming/rtmp-server');
rtmp.start();
rtmp.start();