source: Klonkt/test/channel-actor.test.js@ bdcb3a3

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

De Klonkt-actor is een kanaal, en Audio is een eigen soort

Robins besluit (7-8): onze site-actor past het beste op een Funkwhale-channel,
niet op een Library. En de afstand bleek klein -- beide zijn al Person.

UITGAAND. url wordt een Link-array in plaats van een kale string: de
webpagina en, ernaast, /feed.xml als application/rss+xml. Dat laatste is de
vondst: zo vindt een podcast-app een kanaal, en die feed had Klonkt al. De
text/html staat vooraan, want een lezer die maar een url verwacht pakt de
eerste -- dezelfde vorm die Funkwhale in productie met Mastodon uitwisselt.

De RSS-link gaat alleen mee voor de site waar de instance op gepind staat.
Sinds hub-modus weg is bestaat /user/<slug> niet meer als route, dus een
feed-link voor een andere site zou naar de verkeerde feed wijzen.

category is 'music', en ALLEEN als de site echt audio publiceert -- een blog
zonder muziek als muziekkanaal aankondigen is erger dan geen label. Het signaal
is een track in de kast, niet enable_audio_player, want die staat standaard aan.

INKOMEND. Create(Audio) werd tot nu toe stil weggegooid: de inbox nam alleen
Note, Article en Question aan. Nu staat Audio in TIJDLIJN_SOORTEN en weet
timelineFields per soort wat de tijdlijn nodig heeft.

NIET tot Note omgevormd, en dat is de kern. Een eerdere poging deed dat wel --
kort, elegant, en verkeerd: het maakte de soort onzichtbaar terwijl we juist
meer soorten willen kunnen tonen. Uitbreiden is nu een tak in timelineFields.

Een binnengekomen track levert hoes en dan geluid in media_json, en de Krant
rendert audio/* al als speler -- dus hij komt binnen als een echte speler
zonder dat de weergave iets van Funkwhale hoeft te weten. Een kale string-url
telt als webpagina en nooit als geluid: er een speler op zetten zou een
HTML-pagina als audiobestand aanbieden.

Ook opgeschoond: de item-link liep nog langs een rauwe o.url, die bij een Audio
een array is. Dat gaat nu overal door dezelfde link-keuze.

8 tests.

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

  • Property mode set to 100644
File size: 5.0 KB
Line 
1// De Klonkt-actor als kanaal, en Audio als eigen soort in de tijdlijn
2// (shaer-0nh, Robins besluit 7-8).
3//
4// Twee kanten. Uitgaand: de actor adverteert zijn webpagina en zijn RSS-feed
5// als Link-array, met een category zodra er muziek is. Inkomend: een
6// Create(Audio) van een kanaal wordt een tijdlijnrij met een echte speler --
7// zonder de Audio tot Note om te vormen, want die soort willen we houden.
8import { test } from 'node:test';
9import assert from 'node:assert/strict';
10
11process.env.DATABASE_PATH = ':memory:';
12process.env.PUBLIC_BASE_URL = 'https://test.example';
13
14const dbMod = await import('../src/config/database.js');
15const db = dbMod.default;
16dbMod.initializeDatabase();
17const AP = (await import('../src/services/ActivityPubService.js')).default;
18
19const BASE = 'https://test.example';
20db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
21 .run('u1', 'u1', 'u1@t', 'x', 'god');
22db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
23 .run('s1', 'band', 'De Band', 'u1');
24const site = () => db.prepare("SELECT s.*, (SELECT slug FROM sites WHERE is_primary = 1) AS primary_slug FROM sites s WHERE s.id = 's1'").get();
25
26test('de actor draagt zijn webpagina en zijn RSS-feed als Link-array', () => {
27 const a = AP.buildActor(BASE, site());
28 assert.ok(Array.isArray(a.url), 'url is een array, geen string');
29 assert.deepEqual(a.url[0], { type: 'Link', href: 'https://test.example/', mediaType: 'text/html' },
30 'de webpagina staat VOORAAN: wie er maar een verwacht, pakt de eerste');
31 assert.deepEqual(a.url[1], { type: 'Link', href: 'https://test.example/feed.xml', mediaType: 'application/rss+xml' });
32});
33
34test('zonder muziek geen category -- liever geen label dan een verkeerd label', () => {
35 assert.equal(AP.buildActor(BASE, site()).category, undefined);
36});
37
38test('met muziek is de category "music"', () => {
39 db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)')
40 .run('m1', 's1', 'a.mp3', 'audio/a.mp3', 'audio/mpeg');
41 db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id) VALUES (?,?,?,?)')
42 .run('t1', 's1', 'Een nummer', 'm1');
43 assert.equal(AP.buildActor(BASE, site()).category, 'music');
44});
45
46// ── inkomend ────────────────────────────────────────────────────────────────
47
48const AUDIO = {
49 id: 'https://audio.example/federation/music/tracks/1',
50 type: 'Audio',
51 name: '0METAL MARIO - PINK - 140 bpm',
52 duration: 'PT103S',
53 published: '2026-08-07T12:00:00Z',
54 image: { type: 'Image', url: 'https://audio.example/covers/1.jpg', mediaType: 'image/jpeg' },
55 url: [
56 { type: 'Link', href: 'https://audio.example/library/tracks/1', mediaType: 'text/html' },
57 { type: 'Link', href: 'https://audio.example/media/1.mp3', mediaType: 'audio/mpeg' },
58 ],
59};
60
61test('een Audio blijft een Audio en levert een speler plus hoes', () => {
62 const f = AP.timelineFields(AUDIO);
63 assert.equal(AUDIO.type, 'Audio', 'het bronobject is niet tot Note omgevormd');
64 assert.equal(f.url, 'https://audio.example/library/tracks/1', 'de text/html-link is de link van het item');
65 assert.deepEqual(f.atts, [
66 { url: 'https://audio.example/covers/1.jpg', type: 'image/jpeg' },
67 { url: 'https://audio.example/media/1.mp3', type: 'audio/mpeg' },
68 ], 'eerst kijken, dan luisteren -- en audio/* maakt er in de Krant een speler van');
69 assert.match(f.html, /0METAL MARIO/, 'de titel is de inhoud; een Audio heeft geen content');
70});
71
72test('een titel met HTML erin gaat door de sanitizer', () => {
73 const f = AP.timelineFields({ ...AUDIO, name: 'Track <script>alert(1)</script>' });
74 assert.ok(!/<script/i.test(f.html), 'geen script uit een vreemde titel');
75});
76
77test('een Audio zonder audio-link levert geen speler, maar breekt ook niet', () => {
78 const f = AP.timelineFields({ ...AUDIO, url: [{ type: 'Link', href: 'https://audio.example/x', mediaType: 'text/html' }] });
79 assert.ok(!f.atts.some((a) => /^audio\//.test(a.type)), 'niets om af te spelen');
80 assert.equal(f.url, 'https://audio.example/x');
81});
82
83test('een kale string-url telt als webpagina, nooit als geluid', () => {
84 // Zonder mediaType is een speler eropzetten fout: dan bieden we een
85 // HTML-pagina als audiobestand aan.
86 const f = AP.timelineFields({ ...AUDIO, url: 'https://audio.example/los' });
87 assert.equal(f.url, 'https://audio.example/los');
88 assert.ok(!f.atts.some((a) => /^audio\//.test(a.type)));
89});
90
91test('een gewone Note gedraagt zich onveranderd', () => {
92 const f = AP.timelineFields({
93 id: 'https://elders.example/n/1', type: 'Note', content: '<p>hallo</p>',
94 url: 'https://elders.example/n/1',
95 attachment: [{ type: 'Document', url: 'https://elders.example/p.jpg', mediaType: 'image/jpeg' }],
96 });
97 assert.match(f.html, /hallo/);
98 assert.deepEqual(f.atts, [{ url: 'https://elders.example/p.jpg', type: 'image/jpeg' }]);
99 assert.equal(f.url, 'https://elders.example/n/1');
100});
Note: See TracBrowser for help on using the repository browser.