diff options
| -rw-r--r-- | src/features/messages.js | 19 | ||||
| -rw-r--r-- | tools/moderation-test.js | 12 |
2 files changed, 22 insertions, 9 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index dc3c4ee..16bc052 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -5544,13 +5544,14 @@ export function messagesFeature(ctx) { const toObj = (m) => Object.fromEntries([...m.entries()].slice(-1500).map(([k, v]) => [k, [...v].slice(0, 50)])); try { wallet.saveFeatureState(REPORTS, { ev: toObj(r.ev), pk: toObj(r.pk), mine: [...r.mine].slice(-500), at: r.at }); } catch {} } + // A report typed 'other' says nothing anyone can act on and is ignored. function noteReport(ev) { const r = reportsNow(); const mine = myPubkeys().includes(ev.pubkey); const hex = (v) => /^[0-9a-f]{64}$/.test(v || ''); let es = 0; for (const x of ev.tags || []) { - if (x[0] !== 'e' || !hex(x[1])) continue; + if (x[0] !== 'e' || !hex(x[1]) || x[2] === 'other') continue; es++; (r.ev.get(x[1]) || r.ev.set(x[1], new Set()).get(x[1])).add(ev.pubkey); if (mine) r.mine.add(x[1]); @@ -5559,7 +5560,7 @@ export function messagesFeature(ctx) { // a PERSON is reported when the p tag carries a reason, or when the // report names no post at all; a post report's p tag merely credits // the author and is not held against them - if (x[0] !== 'p' || !hex(x[1]) || x[1] === ev.pubkey || (es && !x[2])) continue; + if (x[0] !== 'p' || !hex(x[1]) || x[1] === ev.pubkey || (es && !x[2]) || x[2] === 'other') continue; (r.pk.get(x[1]) || r.pk.set(x[1], new Set()).get(x[1])).add(ev.pubkey); if (mine) r.mine.add(x[1]); } @@ -5586,14 +5587,20 @@ export function messagesFeature(ctx) { } catch { reportsAt = 0; } } // Who among your follows flagged this post or its author. + // A flag on THIS post from one follow folds it. A flag on the PERSON + // folds everything they write, so it takes two follows to agree — one + // mis-tapped "impersonation" on a well-known key used to fold their whole + // timeline for everyone who followed the tapper. + const PERSON_REPORTS_MIN = 2; function reportsOn(ev) { if (!ev || !ev.id || !modOn('reports') || isMe(ev.pubkey)) return null; const r = reportsNow(); const f = followsNow().set; - const by = new Set(); - for (const pk of r.ev.get(ev.id) || []) if (f.has(pk) && pk !== ev.pubkey) by.add(pk); - for (const pk of r.pk.get(ev.pubkey) || []) if (f.has(pk) && pk !== ev.pubkey) by.add(pk); - return by.size ? [...by] : null; + const onPost = new Set(), onPerson = new Set(); + for (const pk of r.ev.get(ev.id) || []) if (f.has(pk) && pk !== ev.pubkey) onPost.add(pk); + for (const pk of r.pk.get(ev.pubkey) || []) if (f.has(pk) && pk !== ev.pubkey) onPerson.add(pk); + if (!onPost.size && onPerson.size < PERSON_REPORTS_MIN) return null; + return [...new Set([...onPost, ...onPerson])]; } // A flagged post, folded: who flagged it, and a tap to see it anyway. function foldedRow(ev, by) { diff --git a/tools/moderation-test.js b/tools/moderation-test.js index 78fd460..0e93633 100644 --- a/tools/moderation-test.js +++ b/tools/moderation-test.js @@ -14,7 +14,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); let ok = true; const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; const key = () => { const sk = generateSecretKey(); return { sk, pk: getPublicKey(sk) }; }; -const AUTHOR = key(), MUTED = key(), REPORTED = key(), OTHER = key(), SPAMMER = key(), TAGGER = key(); +const AUTHOR = key(), MUTED = key(), REPORTED = key(), OTHER = key(), SPAMMER = key(), TAGGER = key(), FLAGGED = key(); const now = Math.floor(Date.now() / 1000); const post = (who, content, ago, tags = []) => finalizeEvent({ kind: 1, created_at: now - ago, tags, content }, who.sk); const ROOT = post(AUTHOR, 'a post to open, about #bitcoin and other things', 300); @@ -26,6 +26,7 @@ const seeded = [ post(AUTHOR, 'my friend loves lasagna apparently', 600), post(AUTHOR, 'tagged post', 700, [['t', 'nsfw']]), post(REPORTED, 'reported post here, flagged by my friend', 800), + post(FLAGGED, 'a fine post by someone one friend mis-flagged', 900), ]; const reply = (who, text, ago) => post(who, text, ago, [['e', ROOT.id, '', 'root'], ['p', AUTHOR.pk]]); const REPLIES = [reply(OTHER, 'reply from ok person', 200), reply(MUTED, 'reply from muted person', 150)]; @@ -55,7 +56,7 @@ await page.evaluateOnNewDocument((fx) => { window.__reqs.push(f); const kinds = f.kinds || []; if (kinds.includes(10000) && window.__mutes && (f.authors || []).includes(window.__mutes.pubkey)) answer(ws, m[1], [window.__mutes]); - else if (kinds.includes(1984) && (f.authors || []).includes(fx.author)) answer(ws, m[1], [fx.report]); + else if (kinds.includes(1984) && (f.authors || []).includes(fx.author)) answer(ws, m[1], fx.reports); else if (kinds.includes(1) && f['#e'] && f['#e'].includes(fx.root)) answer(ws, m[1], fx.replies); else if (kinds.includes(1)) { subs.push([ws, m[1], f]); answer(ws, m[1], []); return; } // no real posts: the feeds hold only what the test injects } @@ -82,7 +83,11 @@ await page.evaluateOnNewDocument((fx) => { } return n; }; -}, { author: AUTHOR.pk, root: ROOT.id, replies: REPLIES, report: finalizeEvent({ kind: 1984, created_at: now - 100, tags: [['e', seeded[5].id, 'spam'], ['p', REPORTED.pk]], content: '' }, AUTHOR.sk) }); +}, { author: AUTHOR.pk, root: ROOT.id, replies: REPLIES, reports: [ + finalizeEvent({ kind: 1984, created_at: now - 100, tags: [['e', seeded[5].id, 'spam'], ['p', REPORTED.pk]], content: '' }, AUTHOR.sk), + // one friend flagging a PERSON is not enough to fold everything they write + finalizeEvent({ kind: 1984, created_at: now - 90, tags: [['p', FLAGGED.pk, 'impersonation']], content: '' }, AUTHOR.sk), +] }); const errs = []; page.on('pageerror', (e) => errs.push(String(e))); const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t); @@ -135,6 +140,7 @@ try { check('the flagged post is folded, naming who', await waitText('reported by', 12000) && !(await has('reported post here')), (await text()).match(/Reported by[^\n]*/)?.[0]); await page.evaluate(() => { const b = [...document.querySelectorAll('.note-folded button')].find((x) => /show/i.test(x.textContent)); b.click(); }); await sleep(400); check('Show unfolds it', await has('reported post here')); + check('a person flagged by only one friend is not folded', await has('a fine post by someone one friend mis-flagged') && (await text()).split('Reported by').length === 2); console.log('\n[thread replies go through the same door; Report… hides and publishes]'); await page.evaluate(() => { const r = [...document.querySelectorAll('[data-zap-post]')].find((n) => /a post to open/.test(n.textContent)); r.click(); }); await sleep(1500); |
