source: Klonkt/test/sent-notes.test.js@ 1d5ffc0

main
Last change on this file since 1d5ffc0 was 55eca8b, checked in by Robin <roboburr@…>, 6 weeks ago

Inkomende replies op je eigen posts bereiken nu ook de app

Robins melding (30-7): de reply komt niet binnen bij de ander. Hij kwam
wel binnen: als interactie (het commentaar-mechaniek van de web-kant),
waar de inbound-handler bewust stopt voordat de mention-opslag draait.
Maar de C2S-leesroute van de app serveerde alleen timeline, mentions en
(sinds vandaag) eigen verzonden notes; een reply op je eigen post viel
er dus overal buiten en de app van de ontvanger bleef leeg.

De read krijgt een vierde leg: getReplyMessages serveert de
reply-interacties op je eigen posts als Create(Note), zelfde vorm als
de andere legs (media, quote en embed rijden de opgeslagen JSON mee,
Mention-tag zodat de app de afzender als gesprek groepeert, inReplyTo
naar de eigen post).

Changed files:
src/services/ActivityPubService.js

  • getReplyMessages(slug): reply-interacties via posts->sites join

src/routes/activitypub.js

  • C2S inbox-read: replies-leg naast timeline, messages en sent

test/sent-notes.test.js

  • inkomende reply wordt geserveerd voor de posteigenaar (met media), en alleen voor die eigenaar

remarks: dit moet op BEIDE kanten draaien om de cirkel rond te maken:
jouw server toont jou de replies van Nessie, koffieengaar moet updaten
om Nessie de jouwe te tonen.

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

  • Property mode set to 100644
File size: 4.4 KB
Line 
1// Your own sent replies must come BACK over C2S (Robins melding, 30-7):
2// a reply that stores fine but never shows in the app reads as "replyen
3// werkt niet", gets retried, and the retry hit the duplicate guard which
4// answered without an id — which the ingest then called 502 reply_failed.
5// Two guarantees here: getSentNotes serves the reply as an AS2 Note the
6// app can thread, and a duplicate is idempotent success with the same id.
7import { test } from 'node:test';
8import assert from 'node:assert/strict';
9
10process.env.DATABASE_PATH = ':memory:';
11process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
12
13const dbMod = await import('../src/config/database.js');
14const db = dbMod.default;
15dbMod.initializeDatabase();
16const AP = await import('../src/services/ActivityPubService.js');
17
18db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
19 .run('u1', 'robin', 'r@test', 'x', 'god');
20db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
21const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get('me');
22const parent = {
23 id: 1, post_id: '', actor_uri: 'https://unresolvable.invalid/u/ness',
24 actor_url: 'https://unresolvable.invalid/@ness', actor_handle: '@ness@unresolvable.invalid',
25 object_uri: 'https://unresolvable.invalid/notes/9',
26};
27
28test('a sent reply comes back as a threadable AS2 Note', async () => {
29 const r = await AP.deliverReply(site, {
30 postId: '', postSlug: null, parent, text: 'test', visibility: 'friends',
31 attachments: [{ url: '/media/reply-media/foto.jpg', mediaType: 'image/jpeg', name: 'kiek' }],
32 });
33 assert.ok(r && r.id, 'the reply stored');
34
35 const notes = AP.getSentNotes('https://klonkt.test', site);
36 const note = notes.find((n) => n.id.endsWith(r.id));
37 assert.ok(note, 'getSentNotes serves it');
38 assert.equal(note.inReplyTo, parent.object_uri, 'threads under the parent');
39 assert.ok(note.to.includes(parent.actor_uri), 'addressed to the parent author: the app finds the counterpart');
40 assert.ok((note.tag || []).some((t) => t.type === 'Mention' && t.href === parent.actor_uri), 'and the Mention tag agrees');
41 const att = (note.attachment || []).find((a) => a.url.endsWith('foto.jpg'));
42 assert.ok(att && att.type === 'Image', 'the media rides along, absolute');
43 assert.match(String(note.published), /T.*Z$/, 'published is ISO, so the merged feed sorts');
44});
45
46test("an inbound reply on your post reaches the app's message stream", () => {
47 // The other half of "komt niet binnen bij de ander": inbound replies live
48 // in ap_interactions (web comments), never in ap_mentions, so the C2S read
49 // never served them. getReplyMessages is the leg that does.
50 db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at, created_at, updated_at)
51 VALUES ('p9','s1','u1','n-p9','', '<p>x</p>','published',datetime('now'),datetime('now'),datetime('now'))`).run();
52 db.prepare(`INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, actor_name, actor_handle, content, published, parent_uri, visibility, media_json, created_at)
53 VALUES ('reply','p9','https://unresolvable.invalid/notes/77','https://unresolvable.invalid/u/ness','Ness','@ness@unresolvable.invalid','<p>hoi terug</p>','2026-07-30T12:00:00Z','https://klonkt.test/ap/notes/p9','followers','[{"url":"https://unresolvable.invalid/m/f.jpg","type":"image/jpeg"}]',CURRENT_TIMESTAMP)`).run();
54 const rows = AP.getReplyMessages('me', 60);
55 const r = rows.find((x) => x.object_uri === 'https://unresolvable.invalid/notes/77');
56 assert.ok(r, 'the reply is served for the post owner');
57 assert.equal(r.parent_uri, 'https://klonkt.test/ap/notes/p9', 'threads under the post');
58 assert.equal(r.actor_handle, '@ness@unresolvable.invalid');
59 assert.ok(r.media_json.includes('f.jpg'), 'its media rides along');
60 assert.equal(AP.getReplyMessages('bestaat-niet', 60).length, 0, 'and only for the post owner');
61});
62
63test('a duplicate reply is idempotent success with the SAME id, not a 502', async () => {
64 const first = await AP.deliverReply(site, { postId: '', postSlug: null, parent, text: 'nogmaals', visibility: 'friends' });
65 assert.ok(first && first.id);
66 const again = await AP.deliverReply(site, { postId: '', postSlug: null, parent, text: 'nogmaals', visibility: 'friends' });
67 assert.ok(again && again.duplicate, 'recognised as a duplicate');
68 assert.equal(again.id, first.id, 'and it answers with the existing id, so the ingest 201s');
69});
Note: See TracBrowser for help on using the repository browser.