Ignore:
Timestamp:
06/24/2026 11:16:28 AM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
eb852c5
Parents:
065452a
Message:

feat(activitypub): federate images as attachments

Cover image + inline <img> are emitted as AP attachment (Document) with absolute
URLs + mediaType; <img> stripped from content (Mastodon strips it anyway). Outbox
+ post-create delivery now include cover_image_url.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r065452a r5a93ac0  
    106106  const escTitle = String(post.title || '').replace(/[<>&]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]));
    107107  const titleHtml = post.title ? `<p><strong>${escTitle}</strong></p>` : '';
    108   const html = titleHtml + (post.content || '');
    109   return {
     108
     109  // Images travel as AP `attachment` (Mastodon strips <img> from content). Collect
     110  // the cover + any inline <img>, make absolute, then strip <img> from the content
     111  // to avoid duplicate rendering on clients that DO keep them.
     112  const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);
     113  const mediaType = (u) => {
     114    const e = ((u || '').split('?')[0].match(/\.(\w+)$/) || [])[1];
     115    return ({ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif' })[(e || '').toLowerCase()] || 'image/jpeg';
     116  };
     117  const urls = [];
     118  if (post.cover_image_url) urls.push(abs(post.cover_image_url));
     119  let body = post.content || '';
     120  for (const m of body.matchAll(/<img\b[^>]*\bsrc="([^"]+)"[^>]*>/gi)) urls.push(abs(m[1]));
     121  body = body.replace(/<img\b[^>]*>/gi, '');
     122  const seen = new Set();
     123  const attachment = urls.filter(Boolean)
     124    .filter((u) => { if (seen.has(u)) return false; seen.add(u); return true; })
     125    .map((u) => ({ type: 'Document', mediaType: mediaType(u), url: u }));
     126
     127  const note = {
    110128    id,
    111129    type: 'Note',
    112130    attributedTo: aId,
    113     content: html,
     131    content: titleHtml + body,
    114132    url: human,
    115133    published: new Date(post.published_at || post.created_at || Date.now()).toISOString(),
     
    118136    tag: Array.isArray(post.tags) ? post.tags.map((t) => ({ type: 'Hashtag', name: '#' + String(t).replace(/\s+/g, '') })) : [],
    119137  };
     138  if (attachment.length) note.attachment = attachment;
     139  return note;
    120140}
    121141
Note: See TracChangeset for help on using the changeset viewer.