openvibers/openvibe.community Commits
AI text may describe people; only sex acts and porn stay gated (#197)
Changes
@@@ -41,7 +41,7 @@ little progressive JavaScript for pagination, copy buttons and the upload path.- OpenVibe.Media (screenshot and attachment uploads), OpenVibe.VIP (members-only gates), OpenVibe.Chat(a space's room), OpenVibe.Events (the outbox relay and the Pulse and account subscriptions)- OpenVibe.Live only for old VOD/clip comment imports (it reads and writes no pastes here any more)--- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.74.0 (events outbox, per-actor limits, no-looks),++- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.81.0 (events outbox, per-actor limits, no-looks),`openvibe-shared` v4.94.0, pinned by release tarball## How it fits the network@@@ -851,6 +851,6 @@ release after paste comments moved onto the typed comment threads. [Submissions]<!-- versions:start -->- openvibe-contracts: v0.222.0--- openvibe-sdk: v0.74.0++- openvibe-sdk: v0.81.0- openvibe-shared: v4.94.0<!-- versions:end -->@@@ -8,7 +8,7 @@"port": 4200,"domain": "openvibe.community","contracts": "openvibe-contracts v0.222.0",-- "sdk": "openvibe-sdk v0.74.0",++ "sdk": "openvibe-sdk v0.81.0","shared": "openvibe-shared v4.94.0","capabilities": ["community.paste.create",@@@ -18,7 +18,7 @@"iovalkey": "^0.4.0","multer": "^2.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.94.0","pg": "^8.23.0"},@@@ -1208,9 +1208,9 @@}},"node_modules/openvibe-sdk": {-- "version": "0.74.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",-- "integrity": "sha512-DO9KVPnUriY/I4lX6ZEJtXaUvRDbJyqnqJxvlpl+zkNGFILpmuBXVJogNOCfK071Qci1gg7C4Iw1Xrseklcajg==",++ "version": "0.81.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",++ "integrity": "sha512-GWD+qkkKiOyer1SATAaVs8qdWR1Z4WIRvtcYQU0NijzoL9BDRmkaXzLHjwwkRezdkHmdOgtvNIU8GAFaUaS8Ow==","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -29,7 +29,7 @@"iovalkey": "^0.4.0","multer": "^2.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.94.0","pg": "^8.23.0"},@@@ -38,9 +38,6 @@ const { createPgTelemetrySink, startTelemetryRetention } = require('openvibe-sdkconsole.log(`[Community] pastes: this site is the authority (PostgreSQL), identity via ${config.networkUrl}`);});server.keepAliveTimeout = 65_000;-- // Public AI text never describes a person: rows stored before the rule are repaired once listening-- // (server/pastes/no-looks.js; a boot with nothing to repair only reads).-- require('./pastes/no-looks').repairStored(require('./db').getDb()).catch((err) => console.warn('[Pastes] AI text repair skipped:', err.message));// Request telemetry (openvibe-sdk/service, plan T1): one aggregated sample per route template, method and status// class each minute, stored in this service's telemetry_samples table; rows older than 14 days go daily.telemetry.init({ service: 'community', sink: createPgTelemetrySink({ db: require('./db').getDb(), log: console }), skipPrefixes: ['/shared'], intervalMs: 60_000 });ddeleted file mode 100644@@@ -1,82 +0,0 @@--'use strict';--/**-- * Public AI text never describes a person (the owner's rule; openvibe-sdk/no-looks): no looks, body, clothing, hair,-- * face, pose, age or guessed gender. Services send Community AI text (Live's picture descriptions of screenshots, its-- * AI moment pastes), and models ignore the rule in their prompts about one time in ten, so Community applies it to-- * every AI field it stores:-- *-- * - a paste's ai_summary and ai_tags, whoever owns the paste;-- * - the title and body of an AI paste (origin 'ai'). The body's footer ("🔴 Caught live on @x's stream — …",-- * "▶ Watch this moment on …") is links, not description, and stays as written, except "on @null's stream",-- * which reads "on the stream".-- *-- * repairStored() rewrites rows stored before (187 of 1,207 summaries named people on 2026-10-22). It runs at boot-- * and changes only rows whose text the rules change, so a later boot finds nothing. Search's hourly refresh-- * (server/search/documents.js) re-sends the repaired pastes. A person's own title and text are never touched.-- */--const { noLooks, noLooksTags } = require('openvibe-sdk/no-looks');----// Live writes the footer after a blank line, CRLF on every AI moment paste (2026-10-22: all 478 '@null' ones).--const FOOTER = /(\r?\n\r?\n(?:🔴|▶)[\s\S]*)$/u;----const summary = (s) => (typeof s === 'string' ? noLooks(s).trim() : s);----/** ai_tags as stored (a JSON array string, an array, or comma text) → the same shape without looks tags. */--function tags(t) {-- if (t == null) return t;-- if (Array.isArray(t)) return noLooksTags(t);-- const s = String(t);-- try {-- const arr = JSON.parse(s);-- if (Array.isArray(arr)) return JSON.stringify(noLooksTags(arr));-- } catch { /* comma text */ }-- return noLooksTags(s.split(',').map((x) => x.trim())).join(', ');--}----/** An AI paste's body: the description before its footer. */--function body(content) {-- if (typeof content !== 'string') return content;-- const m = FOOTER.exec(content);-- const text = m ? content.slice(0, m.index) : content;-- const clean = noLooks(text).trim();-- // Live wrote "on @null's stream" when Media's list named no owner (478 pastes, 2026-08-18 to 10-22; fixed in Live).-- const footer = m ? m[1].replace(/ on @null's stream:/, ' on the stream:') : '';-- // A description that was all looks leaves the footer alone.-- return clean ? clean + footer : footer.trim();--}----/** An AI paste's title; '' when it was all looks (store.sanitizeTitle then gives the default). */--function title(t) {-- if (typeof t !== 'string') return t;-- return noLooks(t).trim();--}----/** The AI fields of a row as Community stores them: { ai_summary, ai_tags } and, for an AI paste, title and content. */--function cleanRow(row) {-- const out = {};-- if (row.ai_summary != null) out.ai_summary = summary(row.ai_summary);-- if (row.ai_tags != null) out.ai_tags = tags(row.ai_tags);-- if (row.origin === 'ai') {-- if (row.title != null) out.title = title(row.title) || 'Untitled';-- if (row.content != null) out.content = body(row.content);-- }-- return out;--}----/** Rewrite stored rows the rules change; returns how many changed. */--async function repairStored(db, { log = console } = {}) {-- const rows = await db.prepare(`SELECT id, origin, title, content, ai_summary, ai_tags FROM pastes-- WHERE deleted_at IS NULL AND (ai_summary IS NOT NULL OR ai_tags IS NOT NULL OR origin = 'ai')`).all();-- let changed = 0;-- for (const row of rows) {-- const clean = cleanRow(row);-- const cols = Object.keys(clean).filter((c) => clean[c] !== row[c]);-- if (!cols.length) continue;-- await db.prepare(`UPDATE pastes SET ${cols.map((c) => `${c} = ?`).join(', ')} WHERE id = ?`).run(...cols.map((c) => clean[c]), row.id);-- changed++;-- }-- if (changed && log) log.log(`[Pastes] public AI text no longer describes people: ${changed} paste(s) repaired`);-- return changed;--}----module.exports = { summary, tags, body, title, cleanRow, repairStored };@@@ -18,7 +18,6 @@const crypto = require('crypto');const events = require('../events');const store = require('./store');--const looks = require('./no-looks');const spam = require('./spam');const commentStore = require('../comments/store');const { discussionModerator } = require('../identity/capabilities');@@@ -300,8 +299,8 @@ function createPasteService({ db, network = null, media = null, config = {}, limfunction aiFields(v, body) {if (!isService(v) || body.ai_summary == null) return {};const tags = body.ai_tags == null ? null : (typeof body.ai_tags === 'string' ? body.ai_tags : JSON.stringify(body.ai_tags)).slice(0, 2000);-- // Public AI text never describes a person (./no-looks.js).-- return { ai_summary: looks.summary(String(body.ai_summary).slice(0, 2000)), ai_tags: looks.tags(tags), ai_analyzed_at: new Date().toISOString().replace('T', ' ').slice(0, 19) };++ // AI text is stored as written: describing people is fine (owner, 2026-10-31).++ return { ai_summary: String(body.ai_summary).slice(0, 2000), ai_tags: tags, ai_analyzed_at: new Date().toISOString().replace('T', ' ').slice(0, 19) };}/**@@@ -432,8 +431,8 @@ function createPasteService({ db, network = null, media = null, config = {}, limorigin: v.origin === 'ai' ? 'ai' : 'user',type: 'paste',// An AI paste's title and body are AI text too; a person's are theirs, as written.-- title: store.sanitizeTitle(v.origin === 'ai' ? looks.title(body.title) : body.title),-- content: v.origin === 'ai' ? looks.body(content.trim()) : content.trim(),++ title: store.sanitizeTitle(body.title),++ content: content.trim(),language: store.detectLanguage(content, body.language),visibility,stream_ref: streamRefFor(v, body),@@@ -479,8 +478,8 @@ function createPasteService({ db, network = null, media = null, config = {}, limowner_subject: v.subject || null,origin: v.origin === 'ai' ? 'ai' : 'user',type: 'screenshot',-- title: store.sanitizeTitle(v.origin === 'ai' ? looks.title(body.title || 'Screenshot') : (body.title || 'Screenshot')),-- content: v.origin === 'ai' ? looks.body(String(body.description || body.content || '')) : String(body.description || body.content || ''),++ title: store.sanitizeTitle(body.title || 'Screenshot'),++ content: String(body.description || body.content || ''),language: 'text',visibility,screenshot_url: stored.url,@@@ -734,8 +733,8 @@ function createPasteService({ db, network = null, media = null, config = {}, limasync setAi(v, slug, body = {}) {const p = await store.getBySlug(db, String(slug));if (!p) fail(404, 'Paste not found');-- const summary = body.ai_summary == null ? null : looks.summary(String(body.ai_summary).slice(0, 2000));-- const tags = body.ai_tags == null ? null : looks.tags((typeof body.ai_tags === 'string' ? body.ai_tags : JSON.stringify(body.ai_tags)).slice(0, 2000));++ const summary = body.ai_summary == null ? null : String(body.ai_summary).slice(0, 2000);++ const tags = body.ai_tags == null ? null : (typeof body.ai_tags === 'string' ? body.ai_tags : JSON.stringify(body.ai_tags)).slice(0, 2000);const row = await store.setAi(db, p.id, summary, tags);pingIndexNow(row);return { ok: true, paste: await shapeOne(row, v) };nnew file mode 100644@@@ -0,0 +1,18 @@++'use strict';++// Owner, 2026-10-31: AI may describe people; only sex acts and porn stay gated. No server file may call the old++// looks filter again.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const root = path.join(__dirname, '..');++const offenders = [];++(function walk(dir) {++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {++ const p = path.join(dir, e.name);++ if (e.isDirectory()) walk(p);++ else if (e.name.endsWith('.js') && /\b(noLooks|noLooksTags|hasLooks)\s*\(|openvibe-sdk\/no-looks/.test(fs.readFileSync(p, 'utf8'))) offenders.push(path.relative(root, p));++ }++})(path.join(root, 'server'));++assert.deepStrictEqual(offenders, [], 'no looks filter in server/');++console.log('No looks filter: AI text describes people as written.');ddeleted file mode 100644@@@ -1,88 +0,0 @@--'use strict';--/**-- * Public AI text never describes a person (server/pastes/no-looks.js, openvibe-sdk/no-looks): a service's AI summary-- * and tags, and an AI paste's title and body, are stored without looks or a guessed gender; a person's own title and-- * text are theirs as written; rows stored before are repaired at boot, once.-- */--const assert = require('assert');--const { boot, check, done } = require('./helpers/app');--const looks = require('../server/pastes/no-looks');----(async () => {-- const t = await boot({ authority: 'community', pasteLimits: { cooldownSeconds: 0, commentCooldownSeconds: 0 } });-- const net = t.network;-- const alex = net.addUser({ network_user_id: 7, username: 'alex', display_name: 'Alex' });-- const alexJwt = net.sign({ id: 7, subject_id: alex.subject_id, username: 'alex', display_name: 'Alex', role: 'user' });-- const svc = (cap) => net.signService({ cap });-- const call = (path, { method = 'GET', token, cookie, headers = {}, json } = {}) => {-- const h = { ...headers };-- if (token) h.authorization = `Bearer ${token}`;-- let body;-- if (json !== undefined) { h['content-type'] = 'application/json'; body = JSON.stringify(json); }-- return t.get(path, { method, headers: h, body, cookies: cookie ? [`ov_token=${cookie}`] : [] });-- };---- await check('an AI paste from a service: title, body, summary and tags carry no looks; the footer stays', async () => {-- const r = await call('/api/pastes', {-- method: 'POST', token: svc(['community.paste.create']), headers: { 'x-ov-origin': 'ai' },-- json: {-- title: 'A Shirtless Man Cooks', slug: 'cooks-1',-- content: 'A shirtless man in a red hoodie cooks pasta.\n\n🔴 Caught live on @chef\'s stream — https://openvibe.live/@chef',-- ai_summary: 'A man wearing a hat cooks pasta.', ai_tags: ['man', 'pasta', 'red-hoodie'],-- },-- });-- assert.strictEqual(r.status, 201, r.text);-- const p = r.json().paste;-- assert.strictEqual(p.title, 'The streamer Cooks');-- assert.strictEqual(p.content, 'The streamer cooks pasta.\n\n🔴 Caught live on @chef\'s stream — https://openvibe.live/@chef');-- assert.strictEqual(p.ai_summary, 'A person cooks pasta.');-- assert.deepStrictEqual(JSON.parse(p.ai_tags), ['pasta']);-- });---- await check('a person\'s paste: their title and text as written; the AI summary of it follows the rule', async () => {-- const r = await call('/api/pastes', { method: 'POST', cookie: alexJwt, json: { title: 'A man and his dog', content: 'A man in a red shirt walks his dog.' } });-- assert.strictEqual(r.status, 201, r.text);-- const slug = r.json().slug;-- assert.strictEqual(r.json().paste.title, 'A man and his dog');-- assert.strictEqual(r.json().paste.content, 'A man in a red shirt walks his dog.');-- const w = await call(`/api/pastes/${slug}/ai`, { method: 'POST', token: svc(['community.paste.moderate']), json: { ai_summary: 'Close-up selfie of a man, taken outdoors.', ai_tags: '["selfie","outdoors","beard"]' } });-- assert.strictEqual(w.status, 200, w.text);-- assert.strictEqual(w.json().paste.ai_summary, 'Close-up selfie of a person, taken outdoors.');-- assert.deepStrictEqual(JSON.parse(w.json().paste.ai_tags), ['outdoors']);-- const row = await t.db.prepare('SELECT title, content FROM pastes WHERE slug = ?').get(slug);-- assert.deepStrictEqual([row.title, row.content], ['A man and his dog', 'A man in a red shirt walks his dog.']);-- });---- await check('rows stored before the rule are repaired once; a person\'s text is untouched; a second pass changes nothing', async () => {-- const ins = (origin, title, content, summary, tags) => t.db.prepare(`INSERT INTO pastes (slug, origin, type, title, content, language, visibility, ai_summary, ai_tags)-- VALUES (?, ?, 'paste', ?, ?, 'text', 'public', ?, ?)`).run(`old-${Math.random().toString(36).slice(2, 10)}`, origin, title, content, summary, tags);-- await ins('ai', 'A Close-Up Selfie Moment Where A Man Looks Straight', 'Close-up selfie of a man, taken outdoors with trees in the background.\n\n▶ Watch this moment on @x\'s stream: https://openvibe.video/v/1', 'Close-up selfie of a man, taken outdoors with trees in the background.', '["man","trees"]');-- await ins('user', 'Two men at a desk', 'Two men at a desk.', 'A man lounging on a sofa, holding a remote.', 'sofa, man');-- await ins('user', 'Notes', 'my notes', 'A list of notes about a game.', null);-- const n = await looks.repairStored(t.db, { log: null });-- assert.strictEqual(n, 2);-- const ai = await t.db.prepare("SELECT title, content, ai_summary, ai_tags FROM pastes WHERE title LIKE 'A Close-Up%'").get();-- assert.strictEqual(ai.title, 'A Close-Up Selfie Moment Where A person Looks Straight');-- assert.strictEqual(ai.content, 'Close-up selfie of a person, taken outdoors with trees in the background.\n\n▶ Watch this moment on @x\'s stream: https://openvibe.video/v/1');-- assert.strictEqual(ai.ai_summary, 'Close-up selfie of a person, taken outdoors with trees in the background.');-- assert.strictEqual(ai.ai_tags, '["trees"]');-- const user = await t.db.prepare("SELECT title, content, ai_summary, ai_tags FROM pastes WHERE title = 'Two men at a desk'").get();-- assert.deepStrictEqual([user.title, user.content], ['Two men at a desk', 'Two men at a desk.'], 'a person\'s own words stay');-- assert.strictEqual(user.ai_summary, 'A person lounging on a sofa, holding a remote.');-- assert.strictEqual(user.ai_tags, 'sofa');-- assert.strictEqual(await looks.repairStored(t.db, { log: null }), 0, 'nothing left to repair');-- });---- await check('helpers: non-strings pass through; a body or title that is all looks leaves nothing of it', async () => {-- assert.strictEqual(looks.summary(null), null);-- assert.strictEqual(looks.tags(null), null);-- assert.deepStrictEqual(looks.tags(['hat', 'game']), ['game']);-- assert.strictEqual(looks.body('A person adjusts their hair.\n\n🔴 Caught live on @x\'s stream'), '🔴 Caught live on @x\'s stream', 'a description that was all looks leaves the footer');-- assert.strictEqual(looks.body('A person adjusts their hair.'), '');-- assert.strictEqual(looks.body("A desk.\n\n▶ Watch this moment on @null's stream: https://openvibe.video/v/9"), 'A desk.\n\n▶ Watch this moment on the stream: https://openvibe.video/v/9');-- assert.strictEqual(looks.body("A man at a desk.\r\n\r\n▶ Watch this moment on @null's stream: https://openvibe.video/v/9"), 'A person at a desk.\r\n\r\n▶ Watch this moment on the stream: https://openvibe.video/v/9', 'CRLF, as Live writes it');-- assert.strictEqual(looks.cleanRow({ origin: 'ai', title: 'A person adjusts their hair' }).title, 'Untitled');-- });---- done();--})().catch((err) => { console.error(err); process.exit(1); });
@@ -41,7 +41,7 @@ little progressive JavaScript for pagination, copy buttons and the upload path.
@@ -41,7 +41,7 @@ little progressive JavaScript for pagination, copy buttons and the upload path.
- OpenVibe.Media (screenshot and attachment uploads), OpenVibe.VIP (members-only gates), OpenVibe.Chat
- OpenVibe.Media (screenshot and attachment uploads), OpenVibe.VIP (members-only gates), OpenVibe.Chat
(a space's room), OpenVibe.Events (the outbox relay and the Pulse and account subscriptions)
(a space's room), OpenVibe.Events (the outbox relay and the Pulse and account subscriptions)
- OpenVibe.Live only for old VOD/clip comment imports (it reads and writes no pastes here any more)
- OpenVibe.Live only for old VOD/clip comment imports (it reads and writes no pastes here any more)
-- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.74.0 (events outbox, per-actor limits, no-looks),
+- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.81.0 (events outbox, per-actor limits, no-looks),
`openvibe-shared` v4.94.0, pinned by release tarball
`openvibe-shared` v4.94.0, pinned by release tarball
## How it fits the network
## How it fits the network
@@ -851,6 +851,6 @@ release after paste comments moved onto the typed comment threads. [Submissions]
@@ -851,6 +851,6 @@ release after paste comments moved onto the typed comment threads. [Submissions]
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.222.0
- openvibe-contracts: v0.222.0
-- openvibe-sdk: v0.74.0
+- openvibe-sdk: v0.81.0
- openvibe-shared: v4.94.0
- openvibe-shared: v4.94.0
<!-- versions:end -->
<!-- versions:end -->
@@ -8,7 +8,7 @@
@@ -8,7 +8,7 @@
"port": 4200,
"port": 4200,
"domain": "openvibe.community",
"domain": "openvibe.community",
"contracts": "openvibe-contracts v0.222.0",
"contracts": "openvibe-contracts v0.222.0",
- "sdk": "openvibe-sdk v0.74.0",
+ "sdk": "openvibe-sdk v0.81.0",
"shared": "openvibe-shared v4.94.0",
"shared": "openvibe-shared v4.94.0",
"capabilities": [
"capabilities": [
"community.paste.create",
"community.paste.create",
@@ -18,7 +18,7 @@
@@ -18,7 +18,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"multer": "^2.4.0",
"multer": "^2.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.94.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.94.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -1208,9 +1208,9 @@
@@ -1208,9 +1208,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.74.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
- "integrity": "sha512-DO9KVPnUriY/I4lX6ZEJtXaUvRDbJyqnqJxvlpl+zkNGFILpmuBXVJogNOCfK071Qci1gg7C4Iw1Xrseklcajg==",
+ "version": "0.81.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",
+ "integrity": "sha512-GWD+qkkKiOyer1SATAaVs8qdWR1Z4WIRvtcYQU0NijzoL9BDRmkaXzLHjwwkRezdkHmdOgtvNIU8GAFaUaS8Ow==",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -29,7 +29,7 @@
@@ -29,7 +29,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"multer": "^2.4.0",
"multer": "^2.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.94.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.94.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -38,9 +38,6 @@ const { createPgTelemetrySink, startTelemetryRetention } = require('openvibe-sdk
@@ -38,9 +38,6 @@ const { createPgTelemetrySink, startTelemetryRetention } = require('openvibe-sdk
console.log(`[Community] pastes: this site is the authority (PostgreSQL), identity via ${config.networkUrl}`);
console.log(`[Community] pastes: this site is the authority (PostgreSQL), identity via ${config.networkUrl}`);
});
});
server.keepAliveTimeout = 65_000;
server.keepAliveTimeout = 65_000;
- // Public AI text never describes a person: rows stored before the rule are repaired once listening
- // (server/pastes/no-looks.js; a boot with nothing to repair only reads).
- require('./pastes/no-looks').repairStored(require('./db').getDb()).catch((err) => console.warn('[Pastes] AI text repair skipped:', err.message));
// Request telemetry (openvibe-sdk/service, plan T1): one aggregated sample per route template, method and status
// Request telemetry (openvibe-sdk/service, plan T1): one aggregated sample per route template, method and status
// class each minute, stored in this service's telemetry_samples table; rows older than 14 days go daily.
// class each minute, stored in this service's telemetry_samples table; rows older than 14 days go daily.
telemetry.init({ service: 'community', sink: createPgTelemetrySink({ db: require('./db').getDb(), log: console }), skipPrefixes: ['/shared'], intervalMs: 60_000 });
telemetry.init({ service: 'community', sink: createPgTelemetrySink({ db: require('./db').getDb(), log: console }), skipPrefixes: ['/shared'], intervalMs: 60_000 });
deleted file mode 100644
deleted file mode 100644
@@ -1,82 +0,0 @@
@@ -1,82 +0,0 @@
-'use strict';
-/**
- * Public AI text never describes a person (the owner's rule; openvibe-sdk/no-looks): no looks, body, clothing, hair,
- * face, pose, age or guessed gender. Services send Community AI text (Live's picture descriptions of screenshots, its
- * AI moment pastes), and models ignore the rule in their prompts about one time in ten, so Community applies it to
- * every AI field it stores:
- *
- * - a paste's ai_summary and ai_tags, whoever owns the paste;
- * - the title and body of an AI paste (origin 'ai'). The body's footer ("🔴 Caught live on @x's stream — …",
- * "▶ Watch this moment on …") is links, not description, and stays as written, except "on @null's stream",
- * which reads "on the stream".
- *
- * repairStored() rewrites rows stored before (187 of 1,207 summaries named people on 2026-10-22). It runs at boot
- * and changes only rows whose text the rules change, so a later boot finds nothing. Search's hourly refresh
- * (server/search/documents.js) re-sends the repaired pastes. A person's own title and text are never touched.
- */
-const { noLooks, noLooksTags } = require('openvibe-sdk/no-looks');
-
-// Live writes the footer after a blank line, CRLF on every AI moment paste (2026-10-22: all 478 '@null' ones).
-const FOOTER = /(\r?\n\r?\n(?:🔴|▶)[\s\S]*)$/u;
-
-const summary = (s) => (typeof s === 'string' ? noLooks(s).trim() : s);
-
-/** ai_tags as stored (a JSON array string, an array, or comma text) → the same shape without looks tags. */
-function tags(t) {
- if (t == null) return t;
- if (Array.isArray(t)) return noLooksTags(t);
- const s = String(t);
- try {
- const arr = JSON.parse(s);
- if (Array.isArray(arr)) return JSON.stringify(noLooksTags(arr));
- } catch { /* comma text */ }
- return noLooksTags(s.split(',').map((x) => x.trim())).join(', ');
-}
-
-/** An AI paste's body: the description before its footer. */
-function body(content) {
- if (typeof content !== 'string') return content;
- const m = FOOTER.exec(content);
- const text = m ? content.slice(0, m.index) : content;
- const clean = noLooks(text).trim();
- // Live wrote "on @null's stream" when Media's list named no owner (478 pastes, 2026-08-18 to 10-22; fixed in Live).
- const footer = m ? m[1].replace(/ on @null's stream:/, ' on the stream:') : '';
- // A description that was all looks leaves the footer alone.
- return clean ? clean + footer : footer.trim();
-}
-
-/** An AI paste's title; '' when it was all looks (store.sanitizeTitle then gives the default). */
-function title(t) {
- if (typeof t !== 'string') return t;
- return noLooks(t).trim();
-}
-
-/** The AI fields of a row as Community stores them: { ai_summary, ai_tags } and, for an AI paste, title and content. */
-function cleanRow(row) {
- const out = {};
- if (row.ai_summary != null) out.ai_summary = summary(row.ai_summary);
- if (row.ai_tags != null) out.ai_tags = tags(row.ai_tags);
- if (row.origin === 'ai') {
- if (row.title != null) out.title = title(row.title) || 'Untitled';
- if (row.content != null) out.content = body(row.content);
- }
- return out;
-}
-
-/** Rewrite stored rows the rules change; returns how many changed. */
-async function repairStored(db, { log = console } = {}) {
- const rows = await db.prepare(`SELECT id, origin, title, content, ai_summary, ai_tags FROM pastes
- WHERE deleted_at IS NULL AND (ai_summary IS NOT NULL OR ai_tags IS NOT NULL OR origin = 'ai')`).all();
- let changed = 0;
- for (const row of rows) {
- const clean = cleanRow(row);
- const cols = Object.keys(clean).filter((c) => clean[c] !== row[c]);
- if (!cols.length) continue;
- await db.prepare(`UPDATE pastes SET ${cols.map((c) => `${c} = ?`).join(', ')} WHERE id = ?`).run(...cols.map((c) => clean[c]), row.id);
- changed++;
- }
- if (changed && log) log.log(`[Pastes] public AI text no longer describes people: ${changed} paste(s) repaired`);
- return changed;
-}
-
-module.exports = { summary, tags, body, title, cleanRow, repairStored };
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
const crypto = require('crypto');
const crypto = require('crypto');
const events = require('../events');
const events = require('../events');
const store = require('./store');
const store = require('./store');
-const looks = require('./no-looks');
const spam = require('./spam');
const spam = require('./spam');
const commentStore = require('../comments/store');
const commentStore = require('../comments/store');
const { discussionModerator } = require('../identity/capabilities');
const { discussionModerator } = require('../identity/capabilities');
@@ -300,8 +299,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -300,8 +299,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
function aiFields(v, body) {
function aiFields(v, body) {
if (!isService(v) || body.ai_summary == null) return {};
if (!isService(v) || body.ai_summary == null) return {};
const tags = body.ai_tags == null ? null : (typeof body.ai_tags === 'string' ? body.ai_tags : JSON.stringify(body.ai_tags)).slice(0, 2000);
const tags = body.ai_tags == null ? null : (typeof body.ai_tags === 'string' ? body.ai_tags : JSON.stringify(body.ai_tags)).slice(0, 2000);
- // Public AI text never describes a person (./no-looks.js).
- return { ai_summary: looks.summary(String(body.ai_summary).slice(0, 2000)), ai_tags: looks.tags(tags), ai_analyzed_at: new Date().toISOString().replace('T', ' ').slice(0, 19) };
+ // AI text is stored as written: describing people is fine (owner, 2026-10-31).
+ return { ai_summary: String(body.ai_summary).slice(0, 2000), ai_tags: tags, ai_analyzed_at: new Date().toISOString().replace('T', ' ').slice(0, 19) };
}
}
/**
/**
@@ -432,8 +431,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -432,8 +431,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
origin: v.origin === 'ai' ? 'ai' : 'user',
origin: v.origin === 'ai' ? 'ai' : 'user',
type: 'paste',
type: 'paste',
// An AI paste's title and body are AI text too; a person's are theirs, as written.
// An AI paste's title and body are AI text too; a person's are theirs, as written.
- title: store.sanitizeTitle(v.origin === 'ai' ? looks.title(body.title) : body.title),
- content: v.origin === 'ai' ? looks.body(content.trim()) : content.trim(),
+ title: store.sanitizeTitle(body.title),
+ content: content.trim(),
language: store.detectLanguage(content, body.language),
language: store.detectLanguage(content, body.language),
visibility,
visibility,
stream_ref: streamRefFor(v, body),
stream_ref: streamRefFor(v, body),
@@ -479,8 +478,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -479,8 +478,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
owner_subject: v.subject || null,
owner_subject: v.subject || null,
origin: v.origin === 'ai' ? 'ai' : 'user',
origin: v.origin === 'ai' ? 'ai' : 'user',
type: 'screenshot',
type: 'screenshot',
- title: store.sanitizeTitle(v.origin === 'ai' ? looks.title(body.title || 'Screenshot') : (body.title || 'Screenshot')),
- content: v.origin === 'ai' ? looks.body(String(body.description || body.content || '')) : String(body.description || body.content || ''),
+ title: store.sanitizeTitle(body.title || 'Screenshot'),
+ content: String(body.description || body.content || ''),
language: 'text',
language: 'text',
visibility,
visibility,
screenshot_url: stored.url,
screenshot_url: stored.url,
@@ -734,8 +733,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -734,8 +733,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
async setAi(v, slug, body = {}) {
async setAi(v, slug, body = {}) {
const p = await store.getBySlug(db, String(slug));
const p = await store.getBySlug(db, String(slug));
if (!p) fail(404, 'Paste not found');
if (!p) fail(404, 'Paste not found');
- const summary = body.ai_summary == null ? null : looks.summary(String(body.ai_summary).slice(0, 2000));
- const tags = body.ai_tags == null ? null : looks.tags((typeof body.ai_tags === 'string' ? body.ai_tags : JSON.stringify(body.ai_tags)).slice(0, 2000));
+ const summary = body.ai_summary == null ? null : String(body.ai_summary).slice(0, 2000);
+ const tags = body.ai_tags == null ? null : (typeof body.ai_tags === 'string' ? body.ai_tags : JSON.stringify(body.ai_tags)).slice(0, 2000);
const row = await store.setAi(db, p.id, summary, tags);
const row = await store.setAi(db, p.id, summary, tags);
pingIndexNow(row);
pingIndexNow(row);
return { ok: true, paste: await shapeOne(row, v) };
return { ok: true, paste: await shapeOne(row, v) };
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+'use strict';
+// Owner, 2026-10-31: AI may describe people; only sex acts and porn stay gated. No server file may call the old
+// looks filter again.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const root = path.join(__dirname, '..');
+const offenders = [];
+(function walk(dir) {
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) walk(p);
+ else if (e.name.endsWith('.js') && /\b(noLooks|noLooksTags|hasLooks)\s*\(|openvibe-sdk\/no-looks/.test(fs.readFileSync(p, 'utf8'))) offenders.push(path.relative(root, p));
+ }
+})(path.join(root, 'server'));
+assert.deepStrictEqual(offenders, [], 'no looks filter in server/');
+console.log('No looks filter: AI text describes people as written.');
deleted file mode 100644
deleted file mode 100644
@@ -1,88 +0,0 @@
@@ -1,88 +0,0 @@
-'use strict';
-/**
- * Public AI text never describes a person (server/pastes/no-looks.js, openvibe-sdk/no-looks): a service's AI summary
- * and tags, and an AI paste's title and body, are stored without looks or a guessed gender; a person's own title and
- * text are theirs as written; rows stored before are repaired at boot, once.
- */
-const assert = require('assert');
-const { boot, check, done } = require('./helpers/app');
-const looks = require('../server/pastes/no-looks');
-
-(async () => {
- const t = await boot({ authority: 'community', pasteLimits: { cooldownSeconds: 0, commentCooldownSeconds: 0 } });
- const net = t.network;
- const alex = net.addUser({ network_user_id: 7, username: 'alex', display_name: 'Alex' });
- const alexJwt = net.sign({ id: 7, subject_id: alex.subject_id, username: 'alex', display_name: 'Alex', role: 'user' });
- const svc = (cap) => net.signService({ cap });
- const call = (path, { method = 'GET', token, cookie, headers = {}, json } = {}) => {
- const h = { ...headers };
- if (token) h.authorization = `Bearer ${token}`;
- let body;
- if (json !== undefined) { h['content-type'] = 'application/json'; body = JSON.stringify(json); }
- return t.get(path, { method, headers: h, body, cookies: cookie ? [`ov_token=${cookie}`] : [] });
- };
-
- await check('an AI paste from a service: title, body, summary and tags carry no looks; the footer stays', async () => {
- const r = await call('/api/pastes', {
- method: 'POST', token: svc(['community.paste.create']), headers: { 'x-ov-origin': 'ai' },
- json: {
- title: 'A Shirtless Man Cooks', slug: 'cooks-1',
- content: 'A shirtless man in a red hoodie cooks pasta.\n\n🔴 Caught live on @chef\'s stream — https://openvibe.live/@chef',
- ai_summary: 'A man wearing a hat cooks pasta.', ai_tags: ['man', 'pasta', 'red-hoodie'],
- },
- });
- assert.strictEqual(r.status, 201, r.text);
- const p = r.json().paste;
- assert.strictEqual(p.title, 'The streamer Cooks');
- assert.strictEqual(p.content, 'The streamer cooks pasta.\n\n🔴 Caught live on @chef\'s stream — https://openvibe.live/@chef');
- assert.strictEqual(p.ai_summary, 'A person cooks pasta.');
- assert.deepStrictEqual(JSON.parse(p.ai_tags), ['pasta']);
- });
-
- await check('a person\'s paste: their title and text as written; the AI summary of it follows the rule', async () => {
- const r = await call('/api/pastes', { method: 'POST', cookie: alexJwt, json: { title: 'A man and his dog', content: 'A man in a red shirt walks his dog.' } });
- assert.strictEqual(r.status, 201, r.text);
- const slug = r.json().slug;
- assert.strictEqual(r.json().paste.title, 'A man and his dog');
- assert.strictEqual(r.json().paste.content, 'A man in a red shirt walks his dog.');
- const w = await call(`/api/pastes/${slug}/ai`, { method: 'POST', token: svc(['community.paste.moderate']), json: { ai_summary: 'Close-up selfie of a man, taken outdoors.', ai_tags: '["selfie","outdoors","beard"]' } });
- assert.strictEqual(w.status, 200, w.text);
- assert.strictEqual(w.json().paste.ai_summary, 'Close-up selfie of a person, taken outdoors.');
- assert.deepStrictEqual(JSON.parse(w.json().paste.ai_tags), ['outdoors']);
- const row = await t.db.prepare('SELECT title, content FROM pastes WHERE slug = ?').get(slug);
- assert.deepStrictEqual([row.title, row.content], ['A man and his dog', 'A man in a red shirt walks his dog.']);
- });
-
- await check('rows stored before the rule are repaired once; a person\'s text is untouched; a second pass changes nothing', async () => {
- const ins = (origin, title, content, summary, tags) => t.db.prepare(`INSERT INTO pastes (slug, origin, type, title, content, language, visibility, ai_summary, ai_tags)
- VALUES (?, ?, 'paste', ?, ?, 'text', 'public', ?, ?)`).run(`old-${Math.random().toString(36).slice(2, 10)}`, origin, title, content, summary, tags);
- await ins('ai', 'A Close-Up Selfie Moment Where A Man Looks Straight', 'Close-up selfie of a man, taken outdoors with trees in the background.\n\n▶ Watch this moment on @x\'s stream: https://openvibe.video/v/1', 'Close-up selfie of a man, taken outdoors with trees in the background.', '["man","trees"]');
- await ins('user', 'Two men at a desk', 'Two men at a desk.', 'A man lounging on a sofa, holding a remote.', 'sofa, man');
- await ins('user', 'Notes', 'my notes', 'A list of notes about a game.', null);
- const n = await looks.repairStored(t.db, { log: null });
- assert.strictEqual(n, 2);
- const ai = await t.db.prepare("SELECT title, content, ai_summary, ai_tags FROM pastes WHERE title LIKE 'A Close-Up%'").get();
- assert.strictEqual(ai.title, 'A Close-Up Selfie Moment Where A person Looks Straight');
- assert.strictEqual(ai.content, 'Close-up selfie of a person, taken outdoors with trees in the background.\n\n▶ Watch this moment on @x\'s stream: https://openvibe.video/v/1');
- assert.strictEqual(ai.ai_summary, 'Close-up selfie of a person, taken outdoors with trees in the background.');
- assert.strictEqual(ai.ai_tags, '["trees"]');
- const user = await t.db.prepare("SELECT title, content, ai_summary, ai_tags FROM pastes WHERE title = 'Two men at a desk'").get();
- assert.deepStrictEqual([user.title, user.content], ['Two men at a desk', 'Two men at a desk.'], 'a person\'s own words stay');
- assert.strictEqual(user.ai_summary, 'A person lounging on a sofa, holding a remote.');
- assert.strictEqual(user.ai_tags, 'sofa');
- assert.strictEqual(await looks.repairStored(t.db, { log: null }), 0, 'nothing left to repair');
- });
-
- await check('helpers: non-strings pass through; a body or title that is all looks leaves nothing of it', async () => {
- assert.strictEqual(looks.summary(null), null);
- assert.strictEqual(looks.tags(null), null);
- assert.deepStrictEqual(looks.tags(['hat', 'game']), ['game']);
- assert.strictEqual(looks.body('A person adjusts their hair.\n\n🔴 Caught live on @x\'s stream'), '🔴 Caught live on @x\'s stream', 'a description that was all looks leaves the footer');
- assert.strictEqual(looks.body('A person adjusts their hair.'), '');
- assert.strictEqual(looks.body("A desk.\n\n▶ Watch this moment on @null's stream: https://openvibe.video/v/9"), 'A desk.\n\n▶ Watch this moment on the stream: https://openvibe.video/v/9');
- assert.strictEqual(looks.body("A man at a desk.\r\n\r\n▶ Watch this moment on @null's stream: https://openvibe.video/v/9"), 'A person at a desk.\r\n\r\n▶ Watch this moment on the stream: https://openvibe.video/v/9', 'CRLF, as Live writes it');
- assert.strictEqual(looks.cleanRow({ origin: 'ai', title: 'A person adjusts their hair' }).title, 'Untitled');
- });
-
- done();
-})().catch((err) => { console.error(err); process.exit(1); });