openvibers/openvibe.live Commits

The home's "Lately on OpenVibe" quotes skip a take with a slur, strong profanity or a hate topic and show the next best; the Rundown keeps every take (#345)

OP OpenVibers committed 20 days ago · 48596b5f51 · 3 files, +59 −3

Changes

ddiff --git a/server/home/front-page-text.js b/server/home/front-page-text.js
nnew file mode 100644
iindex 00000000..5e3ec603
---- /dev/null
++++ b/server/home/front-page-text.js
@@@ -0,0 +1,20 @@
++'use strict';
++/**
++ * What streamers said, as the home page's "Lately on OpenVibe" strip quotes it: the front page of the site, read by
++ * everyone who arrives. A quote with a slur (chat's built-in categories), strong profanity or a hate topic out of
++ * context ("…that's got an anti-semitic trope", "…people that I want to fucking follow anymore", the strip on
++ * 2026-10-24) is left off it; the next best take is shown instead. The Rundown keeps every take as said.
++ */
++const { CORE_SLUR_CATEGORIES } = require('../chat/moderation-utils');
++
++const ROUGH = /\b(?:f+u+c+k\w*|motherf\w*|sh[i1]t(?:s|ty|ting|ted)?|bullshit|cunts?|bitch\w*|whores?|sluts?|pussy|retard\w*|nazis?|hitler|holocaust|anti-?semit\w*|kkk|rap(?:e|ed|es|ist|ists|ing))\b/i;
++const SLUR_RES = CORE_SLUR_CATEGORIES.map((c) => ({ c, res: c.patterns.map((p) => new RegExp(p, 'i')) }));
++
++/** True when the text may lead the front page. */
++function frontPageOk(text) {
++ const t = String(text || '');
++ if (!t.trim() || ROUGH.test(t)) return false;
++ return !SLUR_RES.some(({ c, res }) => c.generateCandidates(t).some((x) => res.some((re) => re.test(x))));
++}
++
++module.exports = { frontPageOk };
ddiff --git a/server/home/routes.js b/server/home/routes.js
iindex 3a3a7662..f853ffd7 100644
---- a/server/home/routes.js
++++ b/server/home/routes.js
@@@ -503,13 +503,17 @@ router.get('/digest', optionalAuth, async (req, res) => {
new_members: Number((await one('SELECT COUNT(*) AS n FROM users WHERE created_at >= ?', [sinceSql]) || {}).n || 0),
takes: Number((await one("SELECT COUNT(*) AS n FROM rundown_segments WHERE status = 'live' AND said_at >= ?", [sinceSql]) || {}).n || 0),
};
-- // The best Rundown takes since then (server/rundown): the quote when there is one, else the headline.
++ // The best Rundown takes since then (server/rundown): the quote when there is one, else the headline. The front
++ // page leaves out a take with a slur, strong profanity or a hate topic (front-page-text.js) and shows the next.
let hot = [];
try {
++ const { frontPageOk } = require('./front-page-text');
hot = (await db.all(`SELECT s.title, s.quotes_json, s.said_at, t.title AS topic, t.slug, u.username, u.display_name FROM rundown_segments s
JOIN users u ON u.id = s.user_id LEFT JOIN rundown_topics t ON t.id = s.topic_id
-- WHERE s.status = 'live' AND s.said_at >= ? AND s.score >= 4 AND COALESCE(u.is_banned, 0) = 0 ORDER BY s.score DESC, s.said_at DESC LIMIT 3`, [sinceSql]) || [])
-- .map((r) => { let q = null; try { q = JSON.parse(r.quotes_json || '[]')[0] || null; } catch { q = null; } return { text: q ? q.text : r.title, quoted: !!q, topic: r.topic || null, slug: r.slug || null, said_at: r.said_at, username: r.username, display_name: r.display_name }; });
++ WHERE s.status = 'live' AND s.said_at >= ? AND s.score >= 4 AND COALESCE(u.is_banned, 0) = 0 ORDER BY s.score DESC, s.said_at DESC LIMIT 15`, [sinceSql]) || [])
++ .map((r) => { let q = null; try { q = JSON.parse(r.quotes_json || '[]')[0] || null; } catch { q = null; } return { text: q ? q.text : r.title, quoted: !!q, topic: r.topic || null, slug: r.slug || null, said_at: r.said_at, username: r.username, display_name: r.display_name }; })
++ .filter((h) => frontPageOk(h.text) && (!h.topic || frontPageOk(h.topic)))
++ .slice(0, 3);
} catch { hot = []; }
res.set('Cache-Control', uid ? 'private, max-age=30' : 'public, max-age=60');
res.json({ since, signed_in: !!uid, liveNow, streamed, stats, hot });
ddiff --git a/test/home-front-page-text.test.js b/test/home-front-page-text.test.js
nnew file mode 100644
iindex 00000000..b1fffd66
---- /dev/null
++++ b/test/home-front-page-text.test.js
@@@ -0,0 +1,32 @@
++'use strict';
++/**
++ * The home page's "Lately on OpenVibe" quotes leave out a take with a slur, strong profanity or a hate topic, and show
++ * the next best one (server/home/front-page-text.js); the Rundown keeps every take as said.
++ *
++ * node test/home-front-page-text.test.js
++ */
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const { frontPageOk } = require('../server/home/front-page-text');
++
++// The strip on 2026-10-24, and more of the same kind.
++for (const t of [
++ 'I look up to and frankly I don\'t even see people that I want to fucking follow anymore.',
++ 'I had Hermes say, oh, that\'s got an anti-semitic trope. I can\'t help.',
++ 'that was some bullshit', 'what a bitch move', 'he went full nazi on it', 'the Holocaust bit again',
++ 'n1gger', 'f a g g o t', 'Motherfucker!', '', null,
++]) assert.ok(!frontPageOk(t), `left off the front page: ${t}`);
++for (const t of [
++ 'That first time I walked in, I knew that I was not alone.', 'We finally beat the boss on hard mode',
++ 'Shitake mushrooms are underrated', 'My dad Dick says hi', 'Scunthorpe won again', 'Grape soda forever',
++ 'The cockpit view in this sim is amazing',
++]) assert.ok(frontPageOk(t), `fine for the front page: ${t}`);
++console.log(' ✓ slurs, strong profanity and hate topics are left off; ordinary takes (and Shitake, Scunthorpe, grape) stay');
++
++const routes = fs.readFileSync(path.join(__dirname, '..', 'server', 'home', 'routes.js'), 'utf8');
++assert.ok(routes.includes("const { frontPageOk } = require('./front-page-text');"));
++assert.ok(routes.includes('.filter((h) => frontPageOk(h.text) && (!h.topic || frontPageOk(h.topic)))') && routes.includes('.slice(0, 3);'), 'the digest filters, then keeps three');
++assert.ok(/ORDER BY s\.score DESC, s\.said_at DESC LIMIT 15/.test(routes), 'from the best fifteen, so a filtered take has a replacement');
++console.log(' ✓ the home digest takes the three best takes that pass');
++console.log('home front-page text: all passed');