openvibers/openvibe.fun Commits
AI text may describe people; only sex acts and porn stay gated (#21)
Changes
@@@ -233,6 +233,6 @@ AGPL-3.0-only. See [LICENSE](LICENSE).<!-- versions:start -->- openvibe-contracts: v0.236.0--- openvibe-sdk: v0.74.0++- openvibe-sdk: v0.81.0- openvibe-shared: v4.93.2<!-- versions:end -->@@@ -9,11 +9,11 @@"port": "5140","domain": "openvibe.fun","contracts": "openvibe-contracts v0.236.0",-- "sdk": "openvibe-sdk v0.74.0",++ "sdk": "openvibe-sdk v0.81.0","shared": "openvibe-shared v4.93.2","packages": ["openvibe-contracts v0.236.0",-- "openvibe-sdk v0.74.0",++ "openvibe-sdk v0.81.0","openvibe-shared v4.93.2"],"summary": "OpenVibe.Fun is the Fun room. A streamer opens an IRL quest board on their public Live channel, viewers suggest safe challenges and chat votes with !quest vote, the streamer accepts one and marks it live/completed, and the chat or a moderator verifies it; a local badge is paid and OpenCoins are credited only when the wallet is configured. The local guide reads OpenStreetMap (Nominatim/Overpass with a contact User-Agent, one request a second per host, a seven-day cache and a bounded around: query) and Ticketmaster only when a key is configured. Group plans have options, one vote each, a tie refused, and a winner the viewer can add to their own Today with their own token. No route needs a service token; Fun never posts to Chat or Live.",@@@ -16,7 +16,7 @@"helmet": "^8.1.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.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.93.2","pg": "^8.23.0"},@@@ -1166,9 +1166,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"@@@ -23,7 +23,7 @@"helmet": "^8.1.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.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.93.2","pg": "^8.23.0"},@@@ -3,7 +3,7 @@/*** The safe starter quests. These are a small catalogue in code, not an AI generation: the only "generation" in v1* is choosing one of these for a streamer (POST /quests/:slug/idea), so no model call and no new AI capability is-- * involved. Every one passes server/quests/safety.js, and they are written without describing anyone's looks.++ * involved. Every one passes server/quests/safety.js.*/const IDEAS = Object.freeze([{ title: 'Find the oldest bar on this street', brief: 'Ask a local or look at the date over the door; tell chat what you found.', category: 'near_me', accessible: false },@@@ -33,7 +33,7 @@ const RULES = Object.freeze([{code: 'adult',why: 'That challenge is not suitable for a public stream.',-- re: /\b(naked|nude|strip(?:ping|tease)?|porn(?:ography)?|sex(?:ual)?|escort)\b/i,++ re: /\b(porn(?:ography)?|sex(?:\s+acts?)?|escort)\b/i,},]);@@@ -11,7 +11,6 @@* author or token is ever part of a document.*/const { ids } = require('openvibe-contracts');--const { cleanSummary } = require('./text/no-looks');const OWNER = 'fun';const EVENT_UPSERTED = 'fun.index_document.upserted';@@@ -26,7 +25,7 @@ function questDocument(q, { baseUrl = 'https://openvibe.fun' } = {}) {if (!q || q.visibility !== 'public' || !QUEST_STATUSES.has(q.status)) return { owner: OWNER, type: 'quest', id: q.id, revision, deleted: true };// The curated idea deck is the one place Fun writes public text for a quest; a look-naming summary would// become the title. A person's own suggestion is never rewritten here.-- const summary = q.source === 'idea' ? cleanSummary(q.title, q.brief) : q.brief;++ const summary = q.brief;const body = [q.title,q.brief,@@@ -6,12 +6,11 @@** Every write that makes a public quest or plan writes its Search document (or tombstone) in the same transaction* (server/search.js). A suggestion is born unlisted and only the streamer (or staff) can accept it; a plan's owner-- * (or staff) decides its winner. No function here stores a token, a person's looks or a private message.++ * (or staff) decides its winner. No function here stores a token or a private message.*/const crypto = require('crypto');const { ids } = require('openvibe-contracts');const safety = require('./quests/safety');--const { cleanSummary } = require('./text/no-looks');const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;const USERNAME_RE = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$/;ddeleted file mode 100644@@@ -1,22 +0,0 @@--'use strict';----/**-- * No looks in public AI/curated text (the network rule, openvibe-sdk/no-looks, shared with Live, Community and-- * MediaHub): a summary we write never describes a person's body, clothing, hair, face or guessed gender. A summary-- * that names looks at all is not repaired word by word — it is replaced by the item's title. A person's own-- * suggestion or plan text is not passed through this (that would rewrite what they wrote); the safety filter and-- * reports handle those.-- */--const { noLooks, hasLooks } = require('openvibe-sdk/no-looks');----/** The summary to store for text Fun generated or curated. */--function cleanSummary(title, summary) {-- const t = String(title || '').trim() || 'Something to do';-- const s = String(summary == null ? '' : summary).trim();-- if (!s) return t;-- if (hasLooks(s)) return t;-- const cleaned = String(noLooks(s) || '').trim();-- return cleaned || t;--}----module.exports = { cleanSummary, noLooks, hasLooks };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,24 +0,0 @@--'use strict';--/**-- * No looks in public AI/curated text: a summary that names looks becomes the item's title; an ordinary summary is-- * kept. A person's own suggestion is not rewritten by this module — the safety filter and reports handle it.-- */--const assert = require('assert');--const { cleanSummary, hasLooks } = require('../server/text/no-looks');--const { check, done } = require('./helpers/boot');----(async () => {-- await check('a summary that names looks becomes the title', async () => {-- assert.strictEqual(cleanSummary('Find the best bench', 'A shirtless man in a kitchen laughs.'), 'Find the best bench');-- assert.ok(hasLooks('A woman with long red hair smiles.'));-- });---- await check('an ordinary summary survives and the empty case becomes the title', async () => {-- const s = cleanSummary('Find the best bench', 'A quiet bench above the river, ten minutes from the station.');-- assert.strictEqual(s, 'A quiet bench above the river, ten minutes from the station.');-- assert.strictEqual(cleanSummary('Find the best bench', ''), 'Find the best bench');-- assert.strictEqual(cleanSummary(null, null), 'Something to do');-- });---- done();--})();
@@ -233,6 +233,6 @@ AGPL-3.0-only. See [LICENSE](LICENSE).
@@ -233,6 +233,6 @@ AGPL-3.0-only. See [LICENSE](LICENSE).
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.236.0
- openvibe-contracts: v0.236.0
-- openvibe-sdk: v0.74.0
+- openvibe-sdk: v0.81.0
- openvibe-shared: v4.93.2
- openvibe-shared: v4.93.2
<!-- versions:end -->
<!-- versions:end -->
@@ -9,11 +9,11 @@
@@ -9,11 +9,11 @@
"port": "5140",
"port": "5140",
"domain": "openvibe.fun",
"domain": "openvibe.fun",
"contracts": "openvibe-contracts v0.236.0",
"contracts": "openvibe-contracts v0.236.0",
- "sdk": "openvibe-sdk v0.74.0",
+ "sdk": "openvibe-sdk v0.81.0",
"shared": "openvibe-shared v4.93.2",
"shared": "openvibe-shared v4.93.2",
"packages": [
"packages": [
"openvibe-contracts v0.236.0",
"openvibe-contracts v0.236.0",
- "openvibe-sdk v0.74.0",
+ "openvibe-sdk v0.81.0",
"openvibe-shared v4.93.2"
"openvibe-shared v4.93.2"
],
],
"summary": "OpenVibe.Fun is the Fun room. A streamer opens an IRL quest board on their public Live channel, viewers suggest safe challenges and chat votes with !quest vote, the streamer accepts one and marks it live/completed, and the chat or a moderator verifies it; a local badge is paid and OpenCoins are credited only when the wallet is configured. The local guide reads OpenStreetMap (Nominatim/Overpass with a contact User-Agent, one request a second per host, a seven-day cache and a bounded around: query) and Ticketmaster only when a key is configured. Group plans have options, one vote each, a tie refused, and a winner the viewer can add to their own Today with their own token. No route needs a service token; Fun never posts to Chat or Live.",
"summary": "OpenVibe.Fun is the Fun room. A streamer opens an IRL quest board on their public Live channel, viewers suggest safe challenges and chat votes with !quest vote, the streamer accepts one and marks it live/completed, and the chat or a moderator verifies it; a local badge is paid and OpenCoins are credited only when the wallet is configured. The local guide reads OpenStreetMap (Nominatim/Overpass with a contact User-Agent, one request a second per host, a seven-day cache and a bounded around: query) and Ticketmaster only when a key is configured. Group plans have options, one vote each, a tie refused, and a winner the viewer can add to their own Today with their own token. No route needs a service token; Fun never posts to Chat or Live.",
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.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.93.2",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.93.2",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -1166,9 +1166,9 @@
@@ -1166,9 +1166,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"
@@ -23,7 +23,7 @@
@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.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.93.2",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.93.2",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
/**
/**
* The safe starter quests. These are a small catalogue in code, not an AI generation: the only "generation" in v1
* The safe starter quests. These are a small catalogue in code, not an AI generation: the only "generation" in v1
* is choosing one of these for a streamer (POST /quests/:slug/idea), so no model call and no new AI capability is
* is choosing one of these for a streamer (POST /quests/:slug/idea), so no model call and no new AI capability is
- * involved. Every one passes server/quests/safety.js, and they are written without describing anyone's looks.
+ * involved. Every one passes server/quests/safety.js.
*/
*/
const IDEAS = Object.freeze([
const IDEAS = Object.freeze([
{ title: 'Find the oldest bar on this street', brief: 'Ask a local or look at the date over the door; tell chat what you found.', category: 'near_me', accessible: false },
{ title: 'Find the oldest bar on this street', brief: 'Ask a local or look at the date over the door; tell chat what you found.', category: 'near_me', accessible: false },
@@ -33,7 +33,7 @@ const RULES = Object.freeze([
@@ -33,7 +33,7 @@ const RULES = Object.freeze([
{
{
code: 'adult',
code: 'adult',
why: 'That challenge is not suitable for a public stream.',
why: 'That challenge is not suitable for a public stream.',
- re: /\b(naked|nude|strip(?:ping|tease)?|porn(?:ography)?|sex(?:ual)?|escort)\b/i,
+ re: /\b(porn(?:ography)?|sex(?:\s+acts?)?|escort)\b/i,
},
},
]);
]);
@@ -11,7 +11,6 @@
@@ -11,7 +11,6 @@
* author or token is ever part of a document.
* author or token is ever part of a document.
*/
*/
const { ids } = require('openvibe-contracts');
const { ids } = require('openvibe-contracts');
-const { cleanSummary } = require('./text/no-looks');
const OWNER = 'fun';
const OWNER = 'fun';
const EVENT_UPSERTED = 'fun.index_document.upserted';
const EVENT_UPSERTED = 'fun.index_document.upserted';
@@ -26,7 +25,7 @@ function questDocument(q, { baseUrl = 'https://openvibe.fun' } = {}) {
@@ -26,7 +25,7 @@ function questDocument(q, { baseUrl = 'https://openvibe.fun' } = {}) {
if (!q || q.visibility !== 'public' || !QUEST_STATUSES.has(q.status)) return { owner: OWNER, type: 'quest', id: q.id, revision, deleted: true };
if (!q || q.visibility !== 'public' || !QUEST_STATUSES.has(q.status)) return { owner: OWNER, type: 'quest', id: q.id, revision, deleted: true };
// The curated idea deck is the one place Fun writes public text for a quest; a look-naming summary would
// The curated idea deck is the one place Fun writes public text for a quest; a look-naming summary would
// become the title. A person's own suggestion is never rewritten here.
// become the title. A person's own suggestion is never rewritten here.
- const summary = q.source === 'idea' ? cleanSummary(q.title, q.brief) : q.brief;
+ const summary = q.brief;
const body = [
const body = [
q.title,
q.title,
q.brief,
q.brief,
@@ -6,12 +6,11 @@
@@ -6,12 +6,11 @@
*
*
* Every write that makes a public quest or plan writes its Search document (or tombstone) in the same transaction
* Every write that makes a public quest or plan writes its Search document (or tombstone) in the same transaction
* (server/search.js). A suggestion is born unlisted and only the streamer (or staff) can accept it; a plan's owner
* (server/search.js). A suggestion is born unlisted and only the streamer (or staff) can accept it; a plan's owner
- * (or staff) decides its winner. No function here stores a token, a person's looks or a private message.
+ * (or staff) decides its winner. No function here stores a token or a private message.
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
const { ids } = require('openvibe-contracts');
const { ids } = require('openvibe-contracts');
const safety = require('./quests/safety');
const safety = require('./quests/safety');
-const { cleanSummary } = require('./text/no-looks');
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const USERNAME_RE = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$/;
const USERNAME_RE = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$/;
deleted file mode 100644
deleted file mode 100644
@@ -1,22 +0,0 @@
@@ -1,22 +0,0 @@
-'use strict';
-
-/**
- * No looks in public AI/curated text (the network rule, openvibe-sdk/no-looks, shared with Live, Community and
- * MediaHub): a summary we write never describes a person's body, clothing, hair, face or guessed gender. A summary
- * that names looks at all is not repaired word by word — it is replaced by the item's title. A person's own
- * suggestion or plan text is not passed through this (that would rewrite what they wrote); the safety filter and
- * reports handle those.
- */
-const { noLooks, hasLooks } = require('openvibe-sdk/no-looks');
-
-/** The summary to store for text Fun generated or curated. */
-function cleanSummary(title, summary) {
- const t = String(title || '').trim() || 'Something to do';
- const s = String(summary == null ? '' : summary).trim();
- if (!s) return t;
- if (hasLooks(s)) return t;
- const cleaned = String(noLooks(s) || '').trim();
- return cleaned || t;
-}
-
-module.exports = { cleanSummary, noLooks, hasLooks };
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,24 +0,0 @@
@@ -1,24 +0,0 @@
-'use strict';
-/**
- * No looks in public AI/curated text: a summary that names looks becomes the item's title; an ordinary summary is
- * kept. A person's own suggestion is not rewritten by this module — the safety filter and reports handle it.
- */
-const assert = require('assert');
-const { cleanSummary, hasLooks } = require('../server/text/no-looks');
-const { check, done } = require('./helpers/boot');
-
-(async () => {
- await check('a summary that names looks becomes the title', async () => {
- assert.strictEqual(cleanSummary('Find the best bench', 'A shirtless man in a kitchen laughs.'), 'Find the best bench');
- assert.ok(hasLooks('A woman with long red hair smiles.'));
- });
-
- await check('an ordinary summary survives and the empty case becomes the title', async () => {
- const s = cleanSummary('Find the best bench', 'A quiet bench above the river, ten minutes from the station.');
- assert.strictEqual(s, 'A quiet bench above the river, ten minutes from the station.');
- assert.strictEqual(cleanSummary('Find the best bench', ''), 'Find the best bench');
- assert.strictEqual(cleanSummary(null, null), 'Something to do');
- });
-
- done();
-})();