Changeset 55eca8b in Klonkt


Ignore:
Timestamp:
07/30/2026 12:49:18 PM (6 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
1a2f206
Parents:
6a99668
Message:

Inkomende replies op je eigen posts bereiken nu ook de app

Robins melding (30-7): de reply komt niet binnen bij de ander. Hij kwam
wel binnen: als interactie (het commentaar-mechaniek van de web-kant),
waar de inbound-handler bewust stopt voordat de mention-opslag draait.
Maar de C2S-leesroute van de app serveerde alleen timeline, mentions en
(sinds vandaag) eigen verzonden notes; een reply op je eigen post viel
er dus overal buiten en de app van de ontvanger bleef leeg.

De read krijgt een vierde leg: getReplyMessages serveert de
reply-interacties op je eigen posts als Create(Note), zelfde vorm als
de andere legs (media, quote en embed rijden de opgeslagen JSON mee,
Mention-tag zodat de app de afzender als gesprek groepeert, inReplyTo
naar de eigen post).

Changed files:
src/services/ActivityPubService.js

  • getReplyMessages(slug): reply-interacties via posts->sites join

src/routes/activitypub.js

  • C2S inbox-read: replies-leg naast timeline, messages en sent

test/sent-notes.test.js

  • inkomende reply wordt geserveerd voor de posteigenaar (met media), en alleen voor die eigenaar

remarks: dit moet op BEIDE kanten draaien om de cirkel rond te maken:
jouw server toont jou de replies van Nessie, koffieengaar moet updaten
om Nessie de jouwe te tonen.

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

Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r6a99668 r55eca8b  
    280280    },
    281281  }));
     282  // Inbound REPLIES on your own posts: stored as interactions (the web's
     283  // comment machinery), never as mentions, so this read missed them and a
     284  // friend's reply arrived everywhere except in your app (Robins melding,
     285  // 30-7). Same shape as the other legs; media/quotes ride the stored JSON.
     286  const replies = AP.getReplyMessages(auth.site.slug, 60).map((m) => ({
     287    id: `${m.object_uri}#create`,
     288    type: 'Create',
     289    actor: m.actor_uri,
     290    published: AP.isoStamp(m.published || m.created_at),
     291    object: {
     292      id: m.object_uri,
     293      type: 'Note',
     294      attributedTo: m.actor_uri,
     295      content: AP.stripLeadingMentions(m.content),
     296      inReplyTo: m.parent_uri || `${base}/ap/notes/${m.post_id}`,
     297      published: AP.isoStamp(m.published || m.created_at),
     298      to: [me],
     299      tag: [{ type: 'Mention', href: me, name: myHandle }, ...(AP.timelineEmojis(m.emoji_json) || [])],
     300      attachment: AP.timelineAttachments(m.media_json),
     301      'shaer:quote': AP.timelineQuote(m.quote_json),
     302      'shaer:author': (m.actor_name || m.actor_handle || m.actor_icon) ? {
     303        name: m.actor_name || undefined, handle: m.actor_handle || undefined,
     304        icon: m.actor_icon || undefined, url: m.actor_url || undefined,
     305        emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),
     306      } : undefined,
     307      'shaer:embed': embedsAllowed ? AP.timelineEmbed(m.embed_json, { playback: playbackAllowed }) : undefined,
     308    },
     309  }));
    282310  // Your OWN sent notes (replies and direct messages, ap_outbox): without
    283311  // them a reply existed everywhere except in your own app, Messages showed
     
    295323  }));
    296324  // Newest first over all legs, so the app can keep treating this as one feed.
    297   const items = [...posts, ...messages, ...sent].sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
     325  const items = [...posts, ...messages, ...replies, ...sent].sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
    298326  AP.sendAP(res, {
    299327    '@context': AP.AP_CONTEXT,
  • src/services/ActivityPubService.js

    r6a99668 r55eca8b  
    29002900 * skipped here and stay a post.
    29012901 */
     2902// Inbound replies on YOUR posts, for the app's message stream. They live in
     2903// ap_interactions (the web's comment machinery) and deliberately NOT in
     2904// ap_mentions (the mention store returns early for replies-to-us), so the
     2905// C2S read missed them entirely: a reply arrived at the other side
     2906// everywhere EXCEPT in the other's app (Robins melding, 30-7: "komt niet
     2907// binnen bij de ander").
     2908export function getReplyMessages(slug, limit) {
     2909  try {
     2910    return db.prepare(`
     2911      SELECT i.object_uri, i.actor_uri, i.actor_name, i.actor_handle, i.actor_icon, i.actor_url,
     2912             i.content, i.published, i.created_at, i.parent_uri, i.post_id,
     2913             i.emoji_json, i.actor_emoji_json, i.media_json, i.quote_json, i.embed_json
     2914      FROM ap_interactions i
     2915      JOIN posts p ON p.id = i.post_id
     2916      JOIN sites s ON s.id = p.site_id
     2917      WHERE s.slug = ? AND i.kind = 'reply'
     2918      ORDER BY COALESCE(i.published, i.created_at) DESC LIMIT ?`).all(slug, limit || 60);
     2919  } catch { return []; }
     2920}
     2921
    29022922export function getDirectMessages(slug, limit) {
    29032923  try {
     
    42004220  linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
    42014221  noteVisibility, belongsInTimeline, playerUrlFor, isRejectedObject, rejectInteraction, interactionReportTarget,
    4202   getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility, actorDisplay, buildActorRef, prefersEnriched, selfAuthor,
     4222  getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility, actorDisplay, buildActorRef, prefersEnriched, selfAuthor, getReplyMessages,
    42034223};
  • test/sent-notes.test.js

    r6a99668 r55eca8b  
    4444});
    4545
     46test("an inbound reply on your post reaches the app's message stream", () => {
     47  // The other half of "komt niet binnen bij de ander": inbound replies live
     48  // in ap_interactions (web comments), never in ap_mentions, so the C2S read
     49  // never served them. getReplyMessages is the leg that does.
     50  db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at, created_at, updated_at)
     51              VALUES ('p9','s1','u1','n-p9','', '<p>x</p>','published',datetime('now'),datetime('now'),datetime('now'))`).run();
     52  db.prepare(`INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, actor_name, actor_handle, content, published, parent_uri, visibility, media_json, created_at)
     53              VALUES ('reply','p9','https://unresolvable.invalid/notes/77','https://unresolvable.invalid/u/ness','Ness','@ness@unresolvable.invalid','<p>hoi terug</p>','2026-07-30T12:00:00Z','https://klonkt.test/ap/notes/p9','followers','[{"url":"https://unresolvable.invalid/m/f.jpg","type":"image/jpeg"}]',CURRENT_TIMESTAMP)`).run();
     54  const rows = AP.getReplyMessages('me', 60);
     55  const r = rows.find((x) => x.object_uri === 'https://unresolvable.invalid/notes/77');
     56  assert.ok(r, 'the reply is served for the post owner');
     57  assert.equal(r.parent_uri, 'https://klonkt.test/ap/notes/p9', 'threads under the post');
     58  assert.equal(r.actor_handle, '@ness@unresolvable.invalid');
     59  assert.ok(r.media_json.includes('f.jpg'), 'its media rides along');
     60  assert.equal(AP.getReplyMessages('bestaat-niet', 60).length, 0, 'and only for the post owner');
     61});
     62
    4663test('a duplicate reply is idempotent success with the SAME id, not a 502', async () => {
    4764  const first = await AP.deliverReply(site, { postId: '', postSlug: null, parent, text: 'nogmaals', visibility: 'friends' });
Note: See TracChangeset for help on using the changeset viewer.