source: Klonkt/test/c2s-messages.test.js@ 75f2897

main
Last change on this file since 75f2897 was 75f2897, checked in by Robin <roboburr@…>, 4 weeks ago

Het dialect eruit waar de standaard het al draagt

shaer-nmw, tweede helft. shaer:author, shaer:booster, shaer:quote en
shaer:embed gaan van alle C2S-legs af: outbox-vriendenleg, tijdlijn,
berichten, antwoorden, eigen verzonden notes, de composer-kaart en de thread.
De standaardvormen uit de vorige commit dragen ze nu alleen.

De thread-leg had de standaardvorm nog niet en kreeg hem hier: de byline van
een antwoord zit in de ingesloten attributedTo, en de emoji-poort knipt in de
tag van die actor in plaats van in een eigen emoji-kaart ernaast.

Twee dingen die stil hadden kunnen breken:

  • filterThreadToCircle vergeleek n.attributedTo als string. Nu dat een object is zou hij ALLES wegfilteren: een ward met een lege thread en nergens een foutmelding. Loopt nu door actorUriOf.
  • de composer-kaart las op beide clients alleen de oude velden; die had ik in ronde een gemist. De preview zou leeg zijn gebleven.

Wat blijft: shaer:liked en shaer:boosted (per-lezer-interactiestatus, AS2 heeft
er niets voor) en de FEP-633c-termen, die bewust van ons zijn.

Nieuwe wachten: de C2S-leg door de echte route op afwezig dialect -- op de
SLEUTEL, want 'shaer:quote' zit ook in shaer:quoteCards en dat is een poort die
blijft -- en de thread-route, die tot nu toe geen route-dekking had.

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

  • Property mode set to 100644
File size: 5.6 KB
Line 
1// What the app reads is what the app can show.
2//
3// Shaer builds Berichten, gesprekken and the help-escalation list from one
4// source: the C2S inbox read. That read served only the timeline, and a direct
5// note (a DM, a guardian's wave, a ward's 🛟) is not in the timeline, because a
6// note addressed to named people is a message and not a post.
7//
8// The result was an app that showed your own replies and nothing that was said
9// to you. These tests hold the two tables apart in the database and together in
10// the read.
11import { test } from 'node:test';
12import assert from 'node:assert/strict';
13
14process.env.DATABASE_PATH = ':memory:';
15process.env.PUBLIC_BASE_URL = 'https://test.example';
16
17const dbMod = await import('../src/config/database.js');
18const db = dbMod.default;
19dbMod.initializeDatabase();
20const AP = (await import('../src/services/ActivityPubService.js')).default;
21
22db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
23 .run('u1', 'u1', 'u1@t', 'x', 'god');
24db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
25
26const mention = (uri, { wave = 0, help = 0, content = '<p>hoi</p>', published = null } = {}) =>
27 db.prepare(`INSERT INTO ap_mentions (slug, object_uri, actor_uri, actor_name, actor_handle, content, published, wave, help_request)
28 VALUES ('kid', ?, 'https://oma.test/u/oma', 'Oma', '@oma@oma.test', ?, ?, ?, ?)`)
29 .run(uri, content, published, wave, help);
30
31mention('https://oma.test/n/1', { wave: 1, content: '<p><a href="x">@kid@test.example</a> 👋</p>' });
32mention('https://oma.test/n/2', { help: 1 });
33mention('https://oma.test/n/3');
34// A public mention from someone you follow is stored in BOTH tables. It is a
35// post, and it must not turn up twice.
36mention('https://vriend.test/n/9');
37db.prepare(`INSERT INTO ap_timeline (id, slug, author_uri, content) VALUES (?, 'kid', 'https://vriend.test/u/v', '<p>publiek</p>')`)
38 .run('https://vriend.test/n/9');
39
40test('a direct note is not a timeline row', () => {
41 // The premise. If this ever flips, the app gets its messages back by
42 // accident and the Krant fills up with DMs again (d9ad6c5).
43 assert.equal(AP.getTimeline('kid', 50).length, 1, 'only the public post is a post');
44});
45
46test('the direct notes come out of the message read', () => {
47 const msgs = AP.getDirectMessages('kid', 60);
48 const ids = msgs.map((m) => m.object_uri).sort();
49 assert.deepEqual(ids, ['https://oma.test/n/1', 'https://oma.test/n/2', 'https://oma.test/n/3'],
50 'the three messages, and NOT the note that is already a post');
51 assert.equal(msgs.find((m) => m.object_uri.endsWith('/1')).wave, 1, 'the wave is marked as one');
52 assert.equal(msgs.find((m) => m.object_uri.endsWith('/2')).help_request, 1, 'and the buoy as a buoy');
53});
54
55test('a stored stamp becomes an instant, not a two-hour lie', () => {
56 // SQLite writes CURRENT_TIMESTAMP as 'YYYY-MM-DD HH:MM:SS' in UTC, which
57 // Date.parse reads as local time. On this server that is off by an hour or
58 // two, which is enough to scramble the order of a conversation.
59 assert.equal(AP.isoStamp('2026-07-29 08:15:00'), '2026-07-29T08:15:00Z');
60 assert.equal(AP.isoStamp('2026-07-29T08:15:00.000Z'), '2026-07-29T08:15:00.000Z');
61 assert.equal(AP.isoStamp(null), undefined);
62 assert.equal(AP.isoStamp('nonsense'), undefined);
63});
64
65test('the inbox read serves posts and messages in one collection', async (t) => {
66 // Straight through the route, because the mapping is where the app-facing
67 // shape is decided: the addressing that turns a note into a conversation and
68 // the flag that turns one into a wave.
69 const crypto = await import('crypto');
70 const express = (await import('express')).default;
71 const routes = (await import('../src/routes/activitypub.js')).default;
72
73 const bearer = 'test-token-' + 'a'.repeat(24);
74 const hash = crypto.createHash('sha256').update(bearer).digest('base64url');
75 db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)')
76 .run(hash, 'test-client', 'u1', 'kid', 'read write');
77
78 const app = express();
79 app.use(routes);
80 const server = app.listen(0);
81 t.after(() => server.close());
82 await new Promise((r) => server.once('listening', r));
83 const url = `http://127.0.0.1:${server.address().port}/ap/users/kid/inbox`;
84 const doc = await (await fetch(url, { headers: { Authorization: `Bearer ${bearer}` } })).json();
85
86 const byId = Object.fromEntries(doc.orderedItems.map((i) => [i.object.id, i.object]));
87 assert.equal(doc.orderedItems.length, 4, 'one post and three messages, the double counted once');
88
89 const wave = byId['https://oma.test/n/1'];
90 assert.ok(wave, 'the wave is in the read at all (this is the whole bug)');
91 assert.equal(wave['shaer:wave'], true, 'and it says it is a wave');
92 assert.deepEqual(wave.to, ['https://test.example/ap/users/kid'],
93 'addressed to me, which is what makes it a conversation instead of a loose note');
94 assert.ok((wave.tag || []).some((x) => x.type === 'Mention' && x.href === 'https://test.example/ap/users/kid'),
95 'with a Mention the client recognises itself in');
96 assert.equal(wave.attributedTo.name, 'Oma', 'and a byline to show');
97 assert.ok(!/@kid@test\.example/.test(wave.content), 'the leading @mention is stripped, like Berichten on the web');
98
99 assert.equal(byId['https://oma.test/n/2']['shaer:helpRequest'], true, 'the buoy stays a buoy');
100 assert.equal(byId['https://oma.test/n/3']['shaer:wave'], undefined, 'an ordinary DM claims to be neither');
101 assert.equal(byId['https://vriend.test/n/9']['shaer:wave'], undefined, 'and the public post is served once, as a post');
102});
Note: See TracBrowser for help on using the repository browser.