openvibers/openvibe.network Commits

Go-live links open the channel: notifications (Live's direct call and the live.stream.started consumer) and the Discord go-live post linked to https://openvibe.live/<username>, which is not a page on Live (it showed the home page). stream-live.js channelUrl() builds /@<username>, and canonicalChannelUrl() rewrites an old-style link an event still carries; other Live links pass through

OP OpenVibers committed 23 Sept 2026 · 951e7b1c54 · 5 files, +29 −6


Changes

ddiff --git a/server/discord/discord-service.js b/server/discord/discord-service.js
iindex 003138a1..b4954c2d 100644
---- a/server/discord/discord-service.js
++++ b/server/discord/discord-service.js
@@@ -133,7 +133,7 @@ class DiscordService {
}
const displayName = streamer.display_name || streamer.username;
-- const streamUrl = `https://openvibe.live/${streamer.username}`;
++ const streamUrl = require('../notifications/stream-live').channelUrl(streamer.username);
const title = stream.title || 'Started streaming';
const embed = new Discord.EmbedBuilder()
ddiff --git a/server/internal/routes.js b/server/internal/routes.js
iindex dd0a0b47..10424f52 100644
---- a/server/internal/routes.js
++++ b/server/internal/routes.js
@@@ -377,7 +377,7 @@ router.post('/events/stream-live', principals.guard('network.notifications.push'
const db = getDb(req);
const notifData = streamLive.streamLiveNotification({
username: streamer.username, displayName: streamer.display_name, avatarUrl: streamer.avatar_url,
-- senderId: streamer.id || null, stream, url: `https://openvibe.live/${streamer.username}`,
++ senderId: streamer.id || null, stream, url: streamLive.channelUrl(streamer.username),
});
// The streaming follow graph lives in OpenVibe.Live, keyed by LIVE user ids; Live
ddiff --git a/server/notifications/events-consumer.js b/server/notifications/events-consumer.js
iindex 9f13b352..372877fc 100644
---- a/server/notifications/events-consumer.js
++++ b/server/notifications/events-consumer.js
@@@ -143,7 +143,7 @@ function liveStarted(event, { now, maxAgeMs }) {
return {
streamId, subject, username, displayName: clean(ch.display_name, 60) || username,
title: clean(p.title, 200) || null, protocol: clean(p.protocol, 20) || null,
-- url: publicLink(ch.url) || `https://openvibe.live/${encodeURIComponent(username)}`,
++ url: require('./stream-live').canonicalChannelUrl(publicLink(ch.url), username),
};
}
ddiff --git a/server/notifications/stream-live.js b/server/notifications/stream-live.js
iindex a4e08f71..82334e97 100644
---- a/server/notifications/stream-live.js
++++ b/server/notifications/stream-live.js
@@@ -12,6 +12,20 @@
* consumes the event, whichever arrives first announces and the other is skipped as `cooldown`.
*/
++/**
++ * A channel's page on OpenVibe.Live is /@<username>. Links built before 2026-09-23 (and events
++ * still queued from then) say https://openvibe.live/<username>, which is not a page: rewrite those.
++ */
++const LIVE_ORIGIN = 'https://openvibe.live';
++function channelUrl(username) {
++ return `${LIVE_ORIGIN}/@${encodeURIComponent(String(username || '').replace(/^@+/, ''))}`;
++}
++function canonicalChannelUrl(url, username) {
++ const m = /^https:\/\/openvibe\.live\/([A-Za-z0-9_]{3,24})\/?$/.exec(String(url || ''));
++ if (m) return channelUrl(m[1]);
++ return url || channelUrl(username);
++}
++
/** One fan-out per streamer per `stream_live_cooldown_min` (60) and at most `stream_live_daily_cap` (8) per 24h. */
function ensureAnnouncements(db) {
db.exec(`CREATE TABLE IF NOT EXISTS stream_live_announcements (
@@@ -62,7 +76,7 @@ function streamLiveNotification({ username, displayName, avatarUrl, senderId, st
sender_name: name,
sender_avatar: avatarUrl || null,
service: 'live',
-- url: url || `https://openvibe.live/${encodeURIComponent(username)}`,
++ url: canonicalChannelUrl(url, username),
rich_content: {
thumbnail: avatarUrl || null,
context: {
@@@ -76,4 +90,4 @@ function streamLiveNotification({ username, displayName, avatarUrl, senderId, st
};
}
--module.exports = { ensureAnnouncements, claimAnnouncement, allLiveSubscribers, streamLiveNotification };
++module.exports = { ensureAnnouncements, claimAnnouncement, allLiveSubscribers, streamLiveNotification, channelUrl, canonicalChannelUrl };
ddiff --git a/test/events-consumer.test.js b/test/events-consumer.test.js
iindex a7a37f17..8f3815e3 100644
---- a/test/events-consumer.test.js
++++ b/test/events-consumer.test.js
@@@ -253,7 +253,16 @@ const rows = (userId) => db.prepare('SELECT * FROM notifications WHERE user_id =
assert.strictEqual(n1.title, 'Carol 3 is live!');
assert.strictEqual(n1.message, 'Building a b robot /b', 'no markup reaches the inbox');
assert.strictEqual(n1.category, 'stream');
-- assert.strictEqual(n1.url, 'https://openvibe.live/carol');
++ assert.strictEqual(n1.url, 'https://openvibe.live/@carol', 'an old-style channel link is rewritten to the channel page');
++ {
++ const { channelUrl, canonicalChannelUrl } = require('../server/notifications/stream-live');
++ assert.strictEqual(channelUrl('carol'), 'https://openvibe.live/@carol');
++ assert.strictEqual(channelUrl('@carol'), 'https://openvibe.live/@carol');
++ assert.strictEqual(canonicalChannelUrl('https://openvibe.live/@carol', 'carol'), 'https://openvibe.live/@carol', 'a channel link is kept');
++ assert.strictEqual(canonicalChannelUrl('https://openvibe.live/carol/', 'carol'), 'https://openvibe.live/@carol');
++ assert.strictEqual(canonicalChannelUrl('https://openvibe.live/vod/5', 'carol'), 'https://openvibe.live/vod/5', 'other Live pages are kept');
++ assert.strictEqual(canonicalChannelUrl(null, 'carol'), 'https://openvibe.live/@carol', 'no link: the channel page');
++ }
assert.strictEqual(n1.sender_id, 20, 'sender is the streamer\'s Network account (dedupe key)');
assert.strictEqual(n1.sender_avatar, 'https://openvibe.media/avatar/carol');
assert.strictEqual(JSON.parse(n1.rich_content).context.event_id, s1.event_id);