Changeset b4af6f3 in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 08/24/2026 07:22:58 AM (2 weeks ago)
- Branches:
- main
- Children:
- 27cb1e3
- Parents:
- fbacddc
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
rfbacddc rb4af6f3 496 496 } 497 497 498 // fedi_open tracks → real AS2 Audio attachments (the actual file URL, served ungated) so 499 // EVERY client incl. the Mastodon apps plays them inline natively. Gated tracks (default) 500 // stay link/card-only — the file is never exposed for them. Resolve from post.content so a 501 // later body mutation can't affect it. 502 // 503 // Staat apart en niet meer midden in buildNote, omdat een BETAALDE post hem ook 504 // nodig heeft: daar staat de muur om de TEKST en niet om de muziek. 505 function openAudioAttachments(base, site, post) { 506 const openAudio = []; 507 if (!/\[\[(track|album|playlist):/i.test(post.content || '')) return openAudio; 508 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`); 509 const seenA = new Set(); 510 const addRow = (r) => { 511 const fn = r.filename || (r.storage_path || '').split('/').pop(); 512 if (!fn || seenA.has(fn)) return; seenA.add(fn); 513 const a = { type: 'Audio', mediaType: r.mime_type || 'audio/mpeg', url: `${base}/audio/stream/${encodeURIComponent(fn)}`, name: r.title || 'Audio' }; 514 // Cover art on the Audio attachment (AS2 `icon`): track cover, else the post cover. 515 // Mastodon renders it as the artwork thumbnail on its native audio player. 516 const art = abs(r.cover_url || post.cover_image_url || null); 517 if (art) a.icon = { type: 'Image', mediaType: guessMediaType(art), url: art }; 518 openAudio.push(a); 519 }; 520 const SEL = 'SELECT t.title, t.cover_url, m.filename, m.storage_path, m.mime_type FROM audio_tracks t JOIN media m ON m.id = t.media_id WHERE t.fedi_open = 1 AND '; 521 try { 522 for (const mm of (post.content || '').matchAll(/\[\[track:([A-Za-z0-9_-]+)\]\]/g)) { const r = db.prepare(SEL + 't.id = ?').get(mm[1]); if (r) addRow(r); } 523 for (const mm of (post.content || '').matchAll(/\[\[album:([^\]]+)\]\]/g)) for (const r of db.prepare(SEL + 't.site_id = ? AND t.album = ? ORDER BY t.rowid').all(site.id, mm[1].trim())) addRow(r); 524 for (const mm of (post.content || '').matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) for (const r of db.prepare('SELECT t.title, t.cover_url, m.filename, m.storage_path, m.mime_type FROM playlist_tracks pt JOIN audio_tracks t ON t.id = pt.track_id JOIN media m ON m.id = t.media_id WHERE t.fedi_open = 1 AND pt.playlist_id = ? ORDER BY pt.position').all(mm[1])) addRow(r); 525 } catch { /* non-fatal */ } 526 return openAudio; 527 } 528 529 // HET BANDJE OP DE DRAAD. Zonder dit stuk bestaat `Mixtape` alleen in onze 530 // eigen code: de playlist-collectie blijft namelijk een OrderedCollection 531 // (dat moet, anders verliest een lezer die `type` als tekst uitpakt het hele 532 // object), en dan zegt niets naar buiten toe ooit dat dit een cassette is. 533 // Gemeten op 21-8: in de Note van een mixtape-post kwam het woord Mixtape 534 // niet voor, en de hub gooide zo'n bandje daarom stil weg. 535 // 536 // Als bijlage en niet als het object zelf: de post blijft een Note, zodat 537 // Mastodon en alles wat `Mixtape` niet kent gewoon een bericht met audio 538 // ziet. Wie het type wel kent, vindt het bandje als geheel. 539 // 540 // Het bandje draagt alleen wat al open staat: playlistOpenTracks filtert op 541 // fedi_open. Daarom is het veilig om hem ook aan een betaalde teaser te hangen. 542 function mixtapeAttachment(base, site, post) { 543 try { 544 const soort = postMusicType(post.content || '', site.id); 545 if (!soort || soort.type !== 'mixtape' || !soort.collectie || !soort.collectie.id) return null; 546 const pl = db.prepare('SELECT * FROM playlists WHERE id = ? AND site_id = ?') 547 .get(soort.collectie.id, site.id); 548 if (!pl) return null; 549 return buildMixtapeObject(base, site, { ...pl, _post: post }, playlistOpenTracks(pl.id)) || null; 550 } catch { return null; /* een bandje minder is geen kapotte post */ } 551 } 552 498 553 // A single post as an AS2 Note (the object), and as a Create activity (for outbox/delivery). 499 554 export function buildNote(base, site, post, opts = {}) { … … 565 620 566 621 // Paid post (klonkt-demo-aki): federate a PUBLIC teaser + link, never the full 567 // content, so nothing leaks past the paywall. No media attachments either. 622 // content, so nothing leaks past the paywall. Images stay home too. 623 // 624 // MAAR DE OPENGEZETTE AUDIO REIST WEL MEE (Robin, 24-8, naar aanleiding van 625 // boiert.eu/introducing-this-machine). De muur staat om de TEKST. `fedi_open` 626 // is een aparte, eenrichtings, per nummer bewust gezette vlag van de eigenaar, 627 // en die nummers federeren toch al los als eigen Audio-objecten met hun 628 // `context` naar deze post. Hield deze tak het bandje tegen, dan hield hij 629 // niets geheim -- alleen de VOLGORDE en het feit dat het een cassette is. In 630 // de hub viel het bandje daardoor uiteen in vier losse nummers onder een kale 631 // teaserkaart. Een cassette die terugwijst naar "lees verder (supporters)" 632 // dient de betaalde post beter dan vier weesnummers. 568 633 if (post.paid) { 569 634 const esc = (x) => String(x || '').replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c])); … … 571 636 const rawTeaser = String(post.excerpt || '').trim() 572 637 || _firstP.replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\s+/g, ' ').trim().slice(0, 280); 638 const openBijlagen = openAudioAttachments(base, site, post); 639 const band = mixtapeAttachment(base, site, post); 640 // Het bandje alleen als er ook echt iets open in zit: een cassette waarvan 641 // elk nummer gesloten is, is een lege doos met een titel erop. 642 if (band && openBijlagen.length) openBijlagen.push(band); 573 643 return { 574 644 '@context': AP_CONTEXT, … … 579 649 url: human, 580 650 published: toISO(post.published_at || post.created_at || Date.now()), 651 ...(openBijlagen.length ? { attachment: openBijlagen } : {}), 581 652 to: [PUBLIC], 582 653 cc: [`${aId}/followers`], … … 705 776 } 706 777 } catch { /* non-fatal */ } 707 // fedi_open tracks → real AS2 Audio attachments (the actual file URL, served ungated) so 708 // EVERY client incl. the Mastodon apps plays them inline natively. Gated tracks (default) 709 // stay link/card-only — the file is never exposed for them. Resolve from post.content so a 710 // later body mutation can't affect it. 711 const openAudio = []; 712 if (hadAudio) { 713 const seenA = new Set(); 714 const addRow = (r) => { 715 const fn = r.filename || (r.storage_path || '').split('/').pop(); 716 if (!fn || seenA.has(fn)) return; seenA.add(fn); 717 const a = { type: 'Audio', mediaType: r.mime_type || 'audio/mpeg', url: `${base}/audio/stream/${encodeURIComponent(fn)}`, name: r.title || 'Audio' }; 718 // Cover art on the Audio attachment (AS2 `icon`): track cover, else the post cover. 719 // Mastodon renders it as the artwork thumbnail on its native audio player. 720 const art = abs(r.cover_url || post.cover_image_url || null); 721 if (art) a.icon = { type: 'Image', mediaType: guessMediaType(art), url: art }; 722 openAudio.push(a); 723 }; 724 const SEL = 'SELECT t.title, t.cover_url, m.filename, m.storage_path, m.mime_type FROM audio_tracks t JOIN media m ON m.id = t.media_id WHERE t.fedi_open = 1 AND '; 725 try { 726 for (const mm of (post.content || '').matchAll(/\[\[track:([A-Za-z0-9_-]+)\]\]/g)) { const r = db.prepare(SEL + 't.id = ?').get(mm[1]); if (r) addRow(r); } 727 for (const mm of (post.content || '').matchAll(/\[\[album:([^\]]+)\]\]/g)) for (const r of db.prepare(SEL + 't.site_id = ? AND t.album = ? ORDER BY t.rowid').all(site.id, mm[1].trim())) addRow(r); 728 for (const mm of (post.content || '').matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) for (const r of db.prepare('SELECT t.title, t.cover_url, m.filename, m.storage_path, m.mime_type FROM playlist_tracks pt JOIN audio_tracks t ON t.id = pt.track_id JOIN media m ON m.id = t.media_id WHERE t.fedi_open = 1 AND pt.playlist_id = ? ORDER BY pt.position').all(mm[1])) addRow(r); 729 } catch { /* non-fatal */ } 730 } 778 const openAudio = openAudioAttachments(base, site, post); 731 779 // ONVERTAALD voor een verhuizing (FEP-1580). De doelinstantie IS een Klonkt: 732 780 // die rendert [[track:]], [[album:]] en [[playlist:]] zelf en maakt er een … … 795 843 for (const a of openAudio) attachment.push(a); // fedi_open tracks → native Audio players 796 844 797 // HET BANDJE OP DE DRAAD. Zonder dit stuk bestaat `Mixtape` alleen in onze 798 // eigen code: de playlist-collectie blijft namelijk een OrderedCollection 799 // (dat moet, anders verliest een lezer die `type` als tekst uitpakt het hele 800 // object), en dan zegt niets naar buiten toe ooit dat dit een cassette is. 801 // Gemeten op 21-8: in de Note van een mixtape-post kwam het woord Mixtape 802 // niet voor, en de hub gooide zo'n bandje daarom stil weg. 803 // 804 // Als bijlage en niet als het object zelf: de post blijft een Note, zodat 805 // Mastodon en alles wat `Mixtape` niet kent gewoon een bericht met audio 806 // ziet. Wie het type wel kent, vindt het bandje als geheel. 807 try { 808 const soort = postMusicType(post.content || '', site.id); 809 if (soort && soort.type === 'mixtape' && soort.collectie && soort.collectie.id) { 810 const pl = db.prepare('SELECT * FROM playlists WHERE id = ? AND site_id = ?') 811 .get(soort.collectie.id, site.id); 812 if (pl) { 813 const tape = buildMixtapeObject(base, site, { ...pl, _post: post }, playlistOpenTracks(pl.id)); 814 if (tape) attachment.push(tape); 815 } 816 } 817 } catch { /* een bandje minder is geen kapotte post */ } 845 // Het bandje als bijlage — zie mixtapeAttachment() voor het waarom. 846 const tape = mixtapeAttachment(base, site, post); 847 if (tape) attachment.push(tape); 818 848 819 849 // Inline @user@host mentions: the Mention tag objects + the mentioned actor URIs. Only
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)