Ignore:
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    rb4af6f3 r8bb3b09  
    496496}
    497497
    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 
    553498// A single post as an AS2 Note (the object), and as a Create activity (for outbox/delivery).
    554499export function buildNote(base, site, post, opts = {}) {
     
    620565
    621566  // Paid post (klonkt-demo-aki): federate a PUBLIC teaser + link, never the full
    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.
     567  // content, so nothing leaks past the paywall. No media attachments either.
    633568  if (post.paid) {
    634569    const esc = (x) => String(x || '').replace(/[<>&]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]));
     
    636571    const rawTeaser = String(post.excerpt || '').trim()
    637572      || _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);
    643573    return {
    644574      '@context': AP_CONTEXT,
     
    649579      url: human,
    650580      published: toISO(post.published_at || post.created_at || Date.now()),
    651       ...(openBijlagen.length ? { attachment: openBijlagen } : {}),
    652581      to: [PUBLIC],
    653582      cc: [`${aId}/followers`],
     
    776705    }
    777706  } catch { /* non-fatal */ }
    778   const openAudio = openAudioAttachments(base, site, post);
     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  }
    779731  // ONVERTAALD voor een verhuizing (FEP-1580). De doelinstantie IS een Klonkt:
    780732  // die rendert [[track:]], [[album:]] en [[playlist:]] zelf en maakt er een
     
    843795  for (const a of openAudio) attachment.push(a); // fedi_open tracks → native Audio players
    844796
    845   // Het bandje als bijlage — zie mixtapeAttachment() voor het waarom.
    846   const tape = mixtapeAttachment(base, site, post);
    847   if (tape) attachment.push(tape);
     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 */ }
    848818
    849819  // Inline @user@host mentions: the Mention tag objects + the mentioned actor URIs. Only
Note: See TracChangeset for help on using the changeset viewer.