openvibers/openvibe.community Commits
Signed-out pastes that advertise a link are kept unlisted (a secret slug, never listed or indexed); the answer says why (#120)
Changes
@@@ -19,6 +19,7 @@ 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');const { stripImageMetadata } = require('../media/strip-metadata');@@@ -406,7 +407,11 @@ function createPasteService({ db, network = null, media = null, config = {}, limif (!content || typeof content !== 'string' || content.trim().length === 0) fail(400, 'Content is required');if (content.length > L.maxSizeKb * 1024) fail(400, `Paste too large (max ${L.maxSizeKb} KB)`);const metadata = serviceMetadata(v, body);-- const visibility = visibilityOf(body.visibility, !!v.subject);++ let visibility = visibilityOf(body.visibility, !!v.subject);++ // Link spam from someone signed out is kept but unlisted (./spam.js); people with an account and AI pastes are++ // never checked.++ const spammy = !v.subject && v.origin !== 'ai' && visibility === 'public' ? spam.linkSpam({ title: body.title, content }) : null;++ if (spammy && spammy.unlist) visibility = 'unlisted';const row = await store.insertPaste(db, {slug: await slugFor(v, body, visibility, truthy(body.burn_after_read)),owner_subject: v.subject || null,@@@ -423,7 +428,7 @@ function createPasteService({ db, network = null, media = null, config = {}, limis_nsfw: truthy(body.is_nsfw) ? 1 : 0,...aiFields(v, body),});-- return await created(row, { paste: await shapeOne(row, v) });++ return await created(row, { paste: await shapeOne(row, v), ...(spammy && spammy.unlist ? { unlisted_reason: spammy.reason, notice: spam.NOTICE } : {}) });},/** POST /api/pastes/screenshot (and multipart POST /api/pastes) */nnew file mode 100644@@@ -0,0 +1,30 @@++'use strict';++/**++ * Link spam from signed-out posters. A paste without an account that would be public and looks like an advert for a++ * link is kept, but unlisted: it gets a secret slug, so anyone with the link still opens it, and it is never listed,++ * searchable or indexed. A signed-in person's paste and an AI paste are never checked.++ *++ * 2026-10-22: the one spam paste so far was 367 characters with one link, recruiting for an "18+ creator opportunity".++ * So a paste is unlisted when, title and text together, it has++ * - three or more links, or++ * - a link and advertising words (PROMO), or++ * - a link and under SHORT characters of text besides it (a paste that is mostly its link).++ * Code, logs and notes with a link or two in a longer text pass. Nothing is refused and nothing is hidden from its++ * poster: the answer says why, and signing in publishes it.++ */++const LINK_RE = /\bhttps?:\/\/[^\s<>"')\]]+|\bwww\.[a-z0-9-]+\.[a-z]{2,}[^\s<>"')\]]*/gi;++const PROMO = /\b(?:18\+|onlyfans|fansly|casino|betting|sportsbook|viagra|cialis|escort|webcam model|cam (?:model|girl)s?|creator opportunit(?:y|ies)|work from home|make money|earn \$?\d|passive income|crypto signals?|forex|payday loan|buy followers|followers? for sale|seo services?|backlinks?|telegram @|whatsapp \+?\d)/i;++const SHORT = 600;++const NOTICE = 'Signed-out pastes that advertise a link are unlisted: anyone with this link can open it, but it is not listed or searchable. Sign in to publish a paste.';++++/** → { unlist, reason, links } for a would-be public paste from someone signed out. */++function linkSpam({ title = '', content = '' } = {}) {++ const text = `${title || ''}\n${content || ''}`;++ const links = text.match(LINK_RE) || [];++ if (!links.length) return { unlist: false, reason: null, links: 0 };++ const rest = links.reduce((s, l) => s.replace(l, ' '), text).replace(/\s+/g, ' ').trim();++ const reason = links.length >= 3 ? 'links' : PROMO.test(text) ? 'promotion' : rest.length < SHORT ? 'mostly_link' : null;++ return { unlist: Boolean(reason), reason, links: links.length };++}++++module.exports = { linkSpam, NOTICE, SHORT };nnew file mode 100644@@@ -0,0 +1,73 @@++'use strict';++/**++ * Link spam from signed-out posters (server/pastes/spam.js): a would-be public paste with no account that advertises a++ * link is kept but unlisted (a secret slug, never listed or indexed), and the answer says why; code or notes with a++ * link in longer text, a signed-in person's paste and an AI paste are never touched.++ */++const assert = require('assert');++const { boot, check, done } = require('./helpers/app');++const { linkSpam } = require('../server/pastes/spam');++++const LONG_NOTES = `${'The build failed at the link step; the log below shows the missing symbol and the flags used. '.repeat(8)}\nSee https://github.com/example/repo/issues/12 for the same error.`;++++(async () => {++ await check('the rule: three links, a link with advertising words, or a paste that is mostly its link', () => {++ // 2026-10-22's spam paste, in shape: a short recruitment post with one link.++ assert.deepStrictEqual(linkSpam({ title: 'Studio creator opportunity | Leeds | 18+', content: 'Paid weekly, apply now: https://example.test/apply' }), { unlist: true, reason: 'promotion', links: 1 });++ assert.strictEqual(linkSpam({ content: 'a https://a.test b https://b.test c https://c.test' }).reason, 'links');++ assert.strictEqual(linkSpam({ content: 'look www.example.test' }).reason, 'mostly_link');++ assert.deepStrictEqual(linkSpam({ content: LONG_NOTES }), { unlist: false, reason: null, links: 1 }, 'notes with one link in a longer text');++ assert.deepStrictEqual(linkSpam({ content: 'console.log(1)' }), { unlist: false, reason: null, links: 0 });++ });++++ const t = await boot({ authority: 'community', pasteLimits: { cooldownSeconds: 0 } });++ const net = t.network;++ const ana = net.addUser({ network_user_id: 21, username: 'ana', display_name: 'Ana' });++ const anaJwt = net.sign({ id: 21, subject_id: ana.subject_id, username: 'ana', display_name: 'Ana', role: 'user' });++ const post = (json, { cookie, token, ip = '198.51.100.7' } = {}) => {++ const headers = { 'content-type': 'application/json', 'x-forwarded-for': ip };++ if (token) headers.authorization = `Bearer ${token}`;++ return t.get('/api/pastes', { method: 'POST', headers, body: JSON.stringify(json), cookies: cookie ? [`ov_token=${cookie}`] : [] });++ };++ const SPAM = { title: 'Studio creator opportunity | 18+', content: 'Paid weekly, apply now: https://example.test/apply', visibility: 'public' };++++ await check('signed out, an advert for a link is kept unlisted with a secret slug, and the answer says why', async () => {++ const r = await post(SPAM);++ assert.strictEqual(r.status, 201, r.text);++ const out = r.json();++ assert.strictEqual(out.paste.visibility, 'unlisted');++ assert.strictEqual(out.unlisted_reason, 'promotion');++ assert.match(out.notice, /Sign in to publish/);++ assert.ok(out.slug.length >= 16, 'a secret slug, not a guessable adjective-noun one');++ const list = await t.get('/api/pastes?limit=50');++ assert.ok(!list.text.includes('creator opportunity'), 'not in the public list');++ const page = await t.get(`/p/${out.slug}`);++ assert.strictEqual(page.status, 200, 'still opens by its link');++ });++++ await check('signed out, notes with a link in longer text stay public; signed in, the same advert stays public', async () => {++ const notes = await post({ title: 'Build log', content: LONG_NOTES });++ assert.strictEqual(notes.json().paste.visibility, 'public');++ assert.strictEqual(notes.json().notice, undefined);++ const mine = await post(SPAM, { cookie: anaJwt });++ assert.strictEqual(mine.json().paste.visibility, 'public', 'a person with an account is answerable for their paste');++ });++++ await check('a signed-out paste a service forwards is checked; an AI paste never is', async () => {++ const r = await post({ ...SPAM, content: 'Moment: https://openvibe.live/@x and https://openvibe.video/v/1 and https://openvibe.video/c/2' }, {++ token: net.signService({ cap: ['community.paste.create'] }), ip: '127.0.0.1',++ });++ assert.strictEqual(r.status, 201, r.text);++ // A service without X-OV-Origin: ai is a signed-out person's paste forwarded (Live's /api/pastes): checked.++ assert.strictEqual(r.json().paste.visibility, 'unlisted');++ const ai = await t.get('/api/pastes', {++ method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${net.signService({ cap: ['community.paste.create'] })}`, 'x-ov-origin': 'ai' },++ body: JSON.stringify({ title: 'From the stream', content: 'Clip: https://openvibe.video/c/2 https://openvibe.video/c/3 https://openvibe.video/c/4' }),++ });++ assert.strictEqual(ai.status, 201, ai.text);++ assert.strictEqual(ai.json().paste.visibility, 'public');++ });++++ await t.close();++ done();++})();
@@ -19,6 +19,7 @@ const crypto = require('crypto');
@@ -19,6 +19,7 @@ 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 looks = require('./no-looks');
+const spam = require('./spam');
const commentStore = require('../comments/store');
const commentStore = require('../comments/store');
const { discussionModerator } = require('../identity/capabilities');
const { discussionModerator } = require('../identity/capabilities');
const { stripImageMetadata } = require('../media/strip-metadata');
const { stripImageMetadata } = require('../media/strip-metadata');
@@ -406,7 +407,11 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -406,7 +407,11 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
if (!content || typeof content !== 'string' || content.trim().length === 0) fail(400, 'Content is required');
if (!content || typeof content !== 'string' || content.trim().length === 0) fail(400, 'Content is required');
if (content.length > L.maxSizeKb * 1024) fail(400, `Paste too large (max ${L.maxSizeKb} KB)`);
if (content.length > L.maxSizeKb * 1024) fail(400, `Paste too large (max ${L.maxSizeKb} KB)`);
const metadata = serviceMetadata(v, body);
const metadata = serviceMetadata(v, body);
- const visibility = visibilityOf(body.visibility, !!v.subject);
+ let visibility = visibilityOf(body.visibility, !!v.subject);
+ // Link spam from someone signed out is kept but unlisted (./spam.js); people with an account and AI pastes are
+ // never checked.
+ const spammy = !v.subject && v.origin !== 'ai' && visibility === 'public' ? spam.linkSpam({ title: body.title, content }) : null;
+ if (spammy && spammy.unlist) visibility = 'unlisted';
const row = await store.insertPaste(db, {
const row = await store.insertPaste(db, {
slug: await slugFor(v, body, visibility, truthy(body.burn_after_read)),
slug: await slugFor(v, body, visibility, truthy(body.burn_after_read)),
owner_subject: v.subject || null,
owner_subject: v.subject || null,
@@ -423,7 +428,7 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -423,7 +428,7 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
is_nsfw: truthy(body.is_nsfw) ? 1 : 0,
is_nsfw: truthy(body.is_nsfw) ? 1 : 0,
...aiFields(v, body),
...aiFields(v, body),
});
});
- return await created(row, { paste: await shapeOne(row, v) });
+ return await created(row, { paste: await shapeOne(row, v), ...(spammy && spammy.unlist ? { unlisted_reason: spammy.reason, notice: spam.NOTICE } : {}) });
},
},
/** POST /api/pastes/screenshot (and multipart POST /api/pastes) */
/** POST /api/pastes/screenshot (and multipart POST /api/pastes) */
new file mode 100644
new file mode 100644
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+'use strict';
+/**
+ * Link spam from signed-out posters. A paste without an account that would be public and looks like an advert for a
+ * link is kept, but unlisted: it gets a secret slug, so anyone with the link still opens it, and it is never listed,
+ * searchable or indexed. A signed-in person's paste and an AI paste are never checked.
+ *
+ * 2026-10-22: the one spam paste so far was 367 characters with one link, recruiting for an "18+ creator opportunity".
+ * So a paste is unlisted when, title and text together, it has
+ * - three or more links, or
+ * - a link and advertising words (PROMO), or
+ * - a link and under SHORT characters of text besides it (a paste that is mostly its link).
+ * Code, logs and notes with a link or two in a longer text pass. Nothing is refused and nothing is hidden from its
+ * poster: the answer says why, and signing in publishes it.
+ */
+const LINK_RE = /\bhttps?:\/\/[^\s<>"')\]]+|\bwww\.[a-z0-9-]+\.[a-z]{2,}[^\s<>"')\]]*/gi;
+const PROMO = /\b(?:18\+|onlyfans|fansly|casino|betting|sportsbook|viagra|cialis|escort|webcam model|cam (?:model|girl)s?|creator opportunit(?:y|ies)|work from home|make money|earn \$?\d|passive income|crypto signals?|forex|payday loan|buy followers|followers? for sale|seo services?|backlinks?|telegram @|whatsapp \+?\d)/i;
+const SHORT = 600;
+const NOTICE = 'Signed-out pastes that advertise a link are unlisted: anyone with this link can open it, but it is not listed or searchable. Sign in to publish a paste.';
+
+/** → { unlist, reason, links } for a would-be public paste from someone signed out. */
+function linkSpam({ title = '', content = '' } = {}) {
+ const text = `${title || ''}\n${content || ''}`;
+ const links = text.match(LINK_RE) || [];
+ if (!links.length) return { unlist: false, reason: null, links: 0 };
+ const rest = links.reduce((s, l) => s.replace(l, ' '), text).replace(/\s+/g, ' ').trim();
+ const reason = links.length >= 3 ? 'links' : PROMO.test(text) ? 'promotion' : rest.length < SHORT ? 'mostly_link' : null;
+ return { unlist: Boolean(reason), reason, links: links.length };
+}
+
+module.exports = { linkSpam, NOTICE, SHORT };
new file mode 100644
new file mode 100644
@@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
+'use strict';
+/**
+ * Link spam from signed-out posters (server/pastes/spam.js): a would-be public paste with no account that advertises a
+ * link is kept but unlisted (a secret slug, never listed or indexed), and the answer says why; code or notes with a
+ * link in longer text, a signed-in person's paste and an AI paste are never touched.
+ */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/app');
+const { linkSpam } = require('../server/pastes/spam');
+
+const LONG_NOTES = `${'The build failed at the link step; the log below shows the missing symbol and the flags used. '.repeat(8)}\nSee https://github.com/example/repo/issues/12 for the same error.`;
+
+(async () => {
+ await check('the rule: three links, a link with advertising words, or a paste that is mostly its link', () => {
+ // 2026-10-22's spam paste, in shape: a short recruitment post with one link.
+ assert.deepStrictEqual(linkSpam({ title: 'Studio creator opportunity | Leeds | 18+', content: 'Paid weekly, apply now: https://example.test/apply' }), { unlist: true, reason: 'promotion', links: 1 });
+ assert.strictEqual(linkSpam({ content: 'a https://a.test b https://b.test c https://c.test' }).reason, 'links');
+ assert.strictEqual(linkSpam({ content: 'look www.example.test' }).reason, 'mostly_link');
+ assert.deepStrictEqual(linkSpam({ content: LONG_NOTES }), { unlist: false, reason: null, links: 1 }, 'notes with one link in a longer text');
+ assert.deepStrictEqual(linkSpam({ content: 'console.log(1)' }), { unlist: false, reason: null, links: 0 });
+ });
+
+ const t = await boot({ authority: 'community', pasteLimits: { cooldownSeconds: 0 } });
+ const net = t.network;
+ const ana = net.addUser({ network_user_id: 21, username: 'ana', display_name: 'Ana' });
+ const anaJwt = net.sign({ id: 21, subject_id: ana.subject_id, username: 'ana', display_name: 'Ana', role: 'user' });
+ const post = (json, { cookie, token, ip = '198.51.100.7' } = {}) => {
+ const headers = { 'content-type': 'application/json', 'x-forwarded-for': ip };
+ if (token) headers.authorization = `Bearer ${token}`;
+ return t.get('/api/pastes', { method: 'POST', headers, body: JSON.stringify(json), cookies: cookie ? [`ov_token=${cookie}`] : [] });
+ };
+ const SPAM = { title: 'Studio creator opportunity | 18+', content: 'Paid weekly, apply now: https://example.test/apply', visibility: 'public' };
+
+ await check('signed out, an advert for a link is kept unlisted with a secret slug, and the answer says why', async () => {
+ const r = await post(SPAM);
+ assert.strictEqual(r.status, 201, r.text);
+ const out = r.json();
+ assert.strictEqual(out.paste.visibility, 'unlisted');
+ assert.strictEqual(out.unlisted_reason, 'promotion');
+ assert.match(out.notice, /Sign in to publish/);
+ assert.ok(out.slug.length >= 16, 'a secret slug, not a guessable adjective-noun one');
+ const list = await t.get('/api/pastes?limit=50');
+ assert.ok(!list.text.includes('creator opportunity'), 'not in the public list');
+ const page = await t.get(`/p/${out.slug}`);
+ assert.strictEqual(page.status, 200, 'still opens by its link');
+ });
+
+ await check('signed out, notes with a link in longer text stay public; signed in, the same advert stays public', async () => {
+ const notes = await post({ title: 'Build log', content: LONG_NOTES });
+ assert.strictEqual(notes.json().paste.visibility, 'public');
+ assert.strictEqual(notes.json().notice, undefined);
+ const mine = await post(SPAM, { cookie: anaJwt });
+ assert.strictEqual(mine.json().paste.visibility, 'public', 'a person with an account is answerable for their paste');
+ });
+
+ await check('a signed-out paste a service forwards is checked; an AI paste never is', async () => {
+ const r = await post({ ...SPAM, content: 'Moment: https://openvibe.live/@x and https://openvibe.video/v/1 and https://openvibe.video/c/2' }, {
+ token: net.signService({ cap: ['community.paste.create'] }), ip: '127.0.0.1',
+ });
+ assert.strictEqual(r.status, 201, r.text);
+ // A service without X-OV-Origin: ai is a signed-out person's paste forwarded (Live's /api/pastes): checked.
+ assert.strictEqual(r.json().paste.visibility, 'unlisted');
+ const ai = await t.get('/api/pastes', {
+ method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${net.signService({ cap: ['community.paste.create'] })}`, 'x-ov-origin': 'ai' },
+ body: JSON.stringify({ title: 'From the stream', content: 'Clip: https://openvibe.video/c/2 https://openvibe.video/c/3 https://openvibe.video/c/4' }),
+ });
+ assert.strictEqual(ai.status, 201, ai.text);
+ assert.strictEqual(ai.json().paste.visibility, 'public');
+ });
+
+ await t.close();
+ done();
+})();