openvibers/openvibe.live Commits

No redirect from /<username>: a channel is only ever /@<username>, so usernames and page routes never compete for the same paths (a user named like a route, or a route added later, can never change where a link goes). A bare username path is a 404 again. The 677 stored Network notifications that linked to /<username> were rewritten to /@<username> at the source instead (backup taken first)

OP OpenVibers committed 23 Sept 2026 · 2789db40bb · 2 files, +5 −32


Changes

ddiff --git a/server/web/page-status.js b/server/web/page-status.js
iindex a9a24ef6..470c970f 100644
---- a/server/web/page-status.js
++++ b/server/web/page-status.js
@@@ -137,25 +137,6 @@ function channelStatus(segment) {
} catch { return OK; }
}
--/**
-- * Channel links used to be written without the @ (openvibe.live/<username>, in go-live
-- * notifications, Discord posts and follower notices): the canonical `/@<username>[/<slot>]` for
-- * a path that names an existing user that way, else null. Only asked for paths no route knows.
-- */
--const LEGACY_CHANNEL_RE = /^[A-Za-z0-9_]{3,24}$/;
--function legacyChannelPath(req) {
-- const segs = String(req.path || '/').split('/').filter(Boolean);
-- if (!segs.length || segs.length > 2 || !LEGACY_CHANNEL_RE.test(segs[0])) return null;
-- if (segs[1] !== undefined && !SLUG_RE.test(segs[1])) return null;
-- let row = null;
-- try { row = db.getChannelByUsername(segs[0]) || db.getUserByUsername(segs[0]); } catch { return null; }
-- const name = row && row.username;
-- if (!name || !LEGACY_CHANNEL_RE.test(name)) return null;
-- const q = String(req.originalUrl || req.url || '').indexOf('?');
-- const query = q >= 0 ? String(req.originalUrl || req.url).slice(q) : '';
-- return `/@${name}${segs[1] ? `/${segs[1]}` : ''}${query}`;
--}
--
function streamStatus(id) {
if (!ID_RE.test(id)) return NOT_FOUND;
try { return db.getStreamById(Number(id)) ? OK : NOT_FOUND; } catch { return OK; }
@@@ -201,8 +182,6 @@ function spaFallback(sendShell) {
}
const status = await statusFor(req);
if (res.headersSent) return;
-- const channel = status === NOT_FOUND ? legacyChannelPath(req) : null;
-- if (channel) return res.redirect(301, channel);
try {
res.status(status);
if (!sendShell(res, req.path)) res.status(503).type('text/plain').send('Site shell unavailable');
@@@ -213,4 +192,4 @@ function spaFallback(sendShell) {
/** Forget cached lookups (tests; or after a change that must show at once). */
function clearCache() { _cache.clear(); }
--module.exports = { statusFor, spaFallback, legacyChannelPath, clearCache, EXACT, PREFIX, LOOKUP_DEADLINE_MS };
++module.exports = { statusFor, spaFallback, clearCache, EXACT, PREFIX, LOOKUP_DEADLINE_MS };
ddiff --git a/test/page-status.test.js b/test/page-status.test.js
iindex be7e5e48..d1903b79 100644
---- a/test/page-status.test.js
++++ b/test/page-status.test.js
@@@ -145,21 +145,15 @@ server.listen(0, '127.0.0.1', async () => {
await check('unknown paths answer 404', () => expectAll([
'/nope', '/search', '/login', '/wp-login.php', '/favicon-missing.ico', '/Vods', '/vods/extra', '/updates/1',
-- '/@nobody', '/@ab', '/@alice/main/extra', '/@bad.name', '/nobody', '/nobody/main', '/alice/main/extra', '/alice/bad!slot',
++ '/@nobody', '/@ab', '/@alice/main/extra', '/@bad.name', '/nobody', '/nobody/main',
'/vod', '/vod/999', '/vod/abc', '/vod/100/extra', '/clip/999', '/clip/-1',
'/p/missing', '/p/bad!slug', '/p/abc123/extra',
'/recap/999999', '/recap/x', '/stream/999999', '/stream',
], 404));
-- await check('a channel link without the @ redirects to the channel page (old notifications)', async () => {
-- for (const [from, to] of [['/alice', '/@alice'], ['/Alice/', '/@alice'], ['/alice/main', '/@alice/main'], ['/bob?stream=5', '/@bob?stream=5']]) {
-- for (const html of [false, true]) {
-- const r = await get(from, { html });
-- assert.strictEqual(r.status, 301, `${from}${html ? ' (html)' : ''} → ${r.status}`);
-- assert.strictEqual(r.location, to, `${from} → ${r.location}`);
-- }
-- }
-- assert.strictEqual((await get('/vods')).status, 200, 'a route name is never taken for a user');
++ await check('a channel is only ever /@<username>: a bare username is not a page, even for a user that exists', async () => {
++ await expectAll(['/alice', '/Alice/', '/alice/main', '/bob'], 404);
++ for (const p of ['/alice', '/alice/main']) assert.strictEqual((await get(p)).location, null, `${p} does not redirect`);
});
await check('a private VOD or clip is a 404 to strangers, with or without a sign-in', async () => {