source: Klonkt/test/seen-notes.test.js@ 32a4ffb

main
Last change on this file since 32a4ffb was 32a4ffb, checked in by roboburr <roboburr@…>, 5 weeks ago

Onthoud de URI van antwoorden die we al ontvangen (shaer-e9g)

Antwoorden van accounts die je volgt komen gewoon binnen, ondertekend door de
schrijver zelf. belongsInTimeline houdt ze uit de Krant -- een antwoord hoort in
zijn draad, niet in de tijdlijn -- en daarna bewaarden we ze nergens. Kwam er
later een DOORGESTUURD antwoord op zo'n bericht, dan kende knownNoteUri de ouder
niet en werd het afgewezen. Terwijl we die ouder wel degelijk hadden gehad.

Op boiert ging daar 72% van het doorstuurverkeer op stuk: 18 unieke reacties
geweigerd tegen 7 geaccepteerd, in drie uur. Alle achttien waren echte antwoorden
in echte gesprekken, en de ontbrekende ouders waren stuk voor stuk geschreven
door accounts die boiert volgt.

Nieuwe tabel ap_seen_notes, alleen de URI. Geen inhoud, niets extra's op het
scherm, geen tweede administratie naast ap_timeline: dit beantwoordt uitsluitend
de vraag 'kennen wij dit bericht?'. Na 30 dagen gesnoeid, af en toe tijdens het
schrijven zodat een server die weken doorloopt ook opruimt -- doorsturen gebeurt
kort na het antwoord.

De grens zit in WIE er onthouden wordt, en dat is de hele veiligheidsvraag.
Alleen schrijvers die je zelf volgt. Zou het voor iedereen gelden, dan kan een
vreemde eerst een bericht neerleggen en daarna met een doorgestuurd antwoord
daarop de dereference naar een adres van zijn keuze sturen. Er wordt dus niets
van een onbekende aangenomen dat we niet al hadden.

Dit volgt op Barts keuze om gesprekken van anderen NIET te gaan volgen: dat
blijft zo. We stoppen alleen met weggooien wat we al kregen.

Gecontroleerd dat de tests bijten: haal de lookup uit knownNoteUri en geval 5
valt om, haal de volg-eis weg en geval 3. Suite 478/478.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 5.7 KB
Line 
1// shaer-e9g: onthoud de URI van antwoorden die we al ontvangen.
2//
3// Antwoorden van accounts die je volgt komen gewoon binnen, ondertekend door de
4// schrijver zelf. Ze horen niet in de Krant (belongsInTimeline weigert alles met
5// een inReplyTo) en werden daarna nergens bewaard. Kwam er later een
6// DOORGESTUURD antwoord op zo'n bericht, dan kende Klonkt de ouder niet en wees
7// het af -- terwijl het die ouder wel degelijk had gehad. Op boiert ging daar 72%
8// van het doorstuurverkeer op stuk.
9//
10// Wat hier bewaakt wordt is vooral de grens: onthouden gebeurt ALLEEN voor
11// schrijvers die je zelf volgt. Zou dat voor iedereen gelden, dan kan een vreemde
12// eerst een bericht neerleggen en daarna met een antwoord daarop de dereference
13// naar een adres van zijn keuze sturen.
14//
15// Run: npm test
16
17import { test } from 'node:test';
18import assert from 'node:assert/strict';
19
20process.env.DATABASE_PATH = ':memory:';
21process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
22
23const dbMod = await import('../src/config/database.js');
24const db = dbMod.default;
25dbMod.initializeDatabase();
26const AP = await import('../src/services/ActivityPubService.js');
27
28db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
29 .run('u1', 'u1', 'u1@test', 'x', 'god');
30db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
31
32// IP-literals: safeFetch slaat de DNS-lookup over, dus offline.
33const GEVOLGD = 'https://203.0.113.10/users/catsalad'; // volgen we
34const VREEMDE = 'https://203.0.113.90/users/onbekend'; // volgen we niet
35const DOORSTUURDER = 'https://203.0.113.20/users/relay';
36
37db.prepare(`INSERT INTO ap_following (slug, actor_uri, handle, status) VALUES (?,?,?,?)`)
38 .run('me', GEVOLGD, '@catsalad@203.0.113.10', 'accepted');
39
40let bron = null;
41const echteFetch = globalThis.fetch;
42globalThis.fetch = async (url) => {
43 const u = String(url);
44 if (bron && bron.id === u) {
45 return new Response(JSON.stringify(bron), { status: 200, headers: { 'content-type': 'application/activity+json' } });
46 }
47 return new Response('not found', { status: 404 });
48};
49
50const req = (body) => ({ body, headers: { signature: 'keyId="x",signature="y"' }, ip: '203.0.113.9' });
51const gezien = (uri) => !!db.prepare('SELECT 1 FROM ap_seen_notes WHERE uri = ?').get(uri);
52
53/** Een gewoon bezorgd antwoord: ondertekend door de schrijver zelf. */
54function bezorgdAntwoord(auteur, id, inReplyTo) {
55 return {
56 type: 'Create',
57 actor: auteur,
58 object: {
59 id, type: 'Note', attributedTo: auteur, inReplyTo,
60 content: '<p>een antwoord midden in een draad</p>',
61 to: ['https://www.w3.org/ns/activitystreams#Public'],
62 },
63 };
64}
65
66test('een antwoord van iemand die we volgen wordt onthouden', async () => {
67 const uri = 'https://203.0.113.10/notes/catsalad-1';
68 const status = await AP.handleInbox(req(bezorgdAntwoord(GEVOLGD, uri, 'https://203.0.113.10/notes/draad-start')), 'me', { id: GEVOLGD });
69 assert.equal(status, 202);
70 assert.ok(gezien(uri), 'de URI hoort bewaard te zijn');
71});
72
73test('maar het verschijnt NIET in de Krant', () => {
74 // Alleen de URI. Er verandert niets aan wat je te zien krijgt -- een antwoord
75 // hoort in zijn draad, niet in de tijdlijn, en dat blijft zo.
76 assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline').get().n, 0);
77});
78
79test('een antwoord van een VREEMDE wordt niet onthouden', async () => {
80 const uri = 'https://203.0.113.90/notes/vreemde-1';
81 await AP.handleInbox(req(bezorgdAntwoord(VREEMDE, uri, 'https://203.0.113.90/notes/iets')), 'me', { id: VREEMDE });
82 assert.equal(gezien(uri), false, 'anders kan een vreemde de poort voor zichzelf openzetten');
83});
84
85test('een gewone POST van iemand die we volgen wordt niet als gezien-antwoord bewaard', async () => {
86 // Die gaat naar de Krant en staat daarmee al in ap_timeline; een tweede
87 // administratie zou alleen maar uiteen kunnen lopen.
88 const uri = 'https://203.0.113.10/notes/gewone-post';
89 const act = bezorgdAntwoord(GEVOLGD, uri, null);
90 delete act.object.inReplyTo;
91 await AP.handleInbox(req(act), 'me', { id: GEVOLGD });
92 assert.equal(gezien(uri), false);
93 assert.ok(db.prepare('SELECT 1 FROM ap_timeline WHERE id = ?').get(uri), 'die hoort juist wel in de Krant');
94});
95
96test('en dan komt het doorgestuurde antwoord OP dat bericht wel binnen', async () => {
97 // De hele reden van deze bead. Zonder het onthouden was dit een afwijzing met
98 // "skipped (unknown inReplyTo)".
99 const ouder = 'https://203.0.113.10/notes/catsalad-1'; // hierboven onthouden
100 const antwoord = 'https://203.0.113.30/notes/drinkcoaster-1';
101 const schrijver = 'https://203.0.113.30/users/drinkcoaster';
102 bron = {
103 id: antwoord, type: 'Note', attributedTo: schrijver, inReplyTo: ouder,
104 content: '<p>ECHTE versie</p>', to: ['https://www.w3.org/ns/activitystreams#Public'],
105 };
106 const act = { type: 'Create', actor: schrijver, object: { ...bron, content: '<p>BEZORGDE versie</p>' } };
107 const status = await AP.handleInbox(req(act), 'me', { id: DOORSTUURDER });
108 assert.equal(status, 202, 'de doorgestuurde Create hoort geaccepteerd te worden');
109});
110
111test('een doorgestuurd antwoord op een bericht dat we NIET kenden blijft geweigerd', async () => {
112 const antwoord = 'https://203.0.113.30/notes/drinkcoaster-2';
113 const schrijver = 'https://203.0.113.30/users/drinkcoaster';
114 bron = {
115 id: antwoord, type: 'Note', attributedTo: schrijver, inReplyTo: 'https://203.0.113.90/notes/nooit-gezien',
116 content: '<p>x</p>', to: ['https://www.w3.org/ns/activitystreams#Public'],
117 };
118 const act = { type: 'Create', actor: schrijver, object: bron };
119 assert.equal(await AP.handleInbox(req(act), 'me', { id: DOORSTUURDER }), 401);
120});
121
122test.after(() => { globalThis.fetch = echteFetch; });
Note: See TracBrowser for help on using the repository browser.