Changeset feced2c in Klonkt for test


Ignore:
Timestamp:
07/19/2026 05:25:26 PM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
5190152
Parents:
33e1dbd
git-author:
Robin <roboburr@…> (07/19/2026 05:24:58 PM)
git-committer:
Robin <roboburr@…> (07/19/2026 05:25:26 PM)
Message:

Feature: media in replies — rich replies phase 2 (klonkt-demo-c7f)

Drop, paste or pick images/audio/video in the reply editor; they upload, show
as removable chips, travel as AS2 attachments on the federated Note, and render
in the thread.

  • POST /posts/upload-reply-media (requireSiteManager): image/audio/video by extension AND mimetype, stored as-is under /media/reply-media/ (no transcode; a reply attachment is not a track), 32MB cap, returns {url, mediaType, name}.
  • Editor: paperclip button + hidden file input (the mobile path), paste-files and drag/drop handlers, busy/error chips, image thumbnails, max 4, hidden attachments JSON field. Media-only submit allowed (text no longer required when something is attached).
  • deliverReply({attachments}): re-validates server-side — own /media/ paths only (the upload route is the sole producer, remote URLs rejected), image|audio|video mimetypes, capped at 4; stored as JSON on ap_outbox (additive column). Dedup guard now includes attachments so two media-only replies to the same parent are distinct from each other but double-submits still dedup.
  • buildNote reply branch: attachment array with Image/Audio/Video types and absolute URLs. getInteractions passes media through; fedi-node renders it (img/audio/video) for visitors too, loading the stylesheet when the owner-only editor is not on the page.

3 new tests (foreign-URL and type rejection, typed absolute Note attachments,
media-only allowed, only-invalid rejected); 91 green. Browser-verified end to
end: real upload via the endpoint, paste-event -> chip with thumbnail ->
submit -> ap_outbox row with content+language+attachments -> media rendered in
the thread -> /ap/notes/<id> serves the typed absolute attachment.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • test/rich-reply.test.js

    r33e1dbd rfeced2c  
    8080  assert.equal(row.language, null);
    8181});
     82
     83test('attachments: own /media/ urls stored, note carries typed absolute attachments', async () => {
     84  const r = await AP.deliverReply(site, {
     85    postId: 'p1', postSlug: 'hallo', parent, text: 'met media', html: '',
     86    attachments: [
     87      { url: '/media/reply-media/a.webp', mediaType: 'image/webp', name: 'foto' },
     88      { url: '/media/reply-media/b.mp3', mediaType: 'audio/mpeg', name: 'liedje' },
     89      { url: 'https://evil.example/x.png', mediaType: 'image/png', name: 'remote' },  // rejected: not ours
     90      { url: '/media/reply-media/c.pdf', mediaType: 'application/pdf', name: 'doc' }, // rejected: type
     91    ],
     92  });
     93  const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get(r.id);
     94  const stored = JSON.parse(row.attachments);
     95  assert.equal(stored.length, 2);
     96  assert.deepEqual(stored.map((a) => a.url), ['/media/reply-media/a.webp', '/media/reply-media/b.mp3']);
     97
     98  const note = AP.buildReplyNote('https://klonkt.test', site, row);
     99  assert.equal(note.attachment.length, 2);
     100  assert.deepEqual(note.attachment.map((a) => a.type), ['Image', 'Audio']);
     101  assert.equal(note.attachment[0].url, 'https://klonkt.test/media/reply-media/a.webp');
     102});
     103
     104test('a media-only reply (no text) is delivered', async () => {
     105  const r = await AP.deliverReply(site, {
     106    postId: 'p1', postSlug: 'hallo', parent, text: '', html: '',
     107    attachments: [{ url: '/media/reply-media/solo.webp', mediaType: 'image/webp', name: '' }],
     108  });
     109  assert.ok(r && r.id);
     110  const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get(r.id);
     111  assert.match(row.content, /^<p><a /); // just the mention paragraph
     112  assert.equal(JSON.parse(row.attachments).length, 1);
     113});
     114
     115test('only foreign/invalid attachments and no text -> rejected', async () => {
     116  const r = await AP.deliverReply(site, {
     117    postId: 'p1', postSlug: 'hallo', parent, text: '', html: '',
     118    attachments: [{ url: 'https://evil.example/x.png', mediaType: 'image/png' }],
     119  });
     120  assert.equal(r, null);
     121});
Note: See TracChangeset for help on using the changeset viewer.