Changeset 6a99668 in Klonkt
- Timestamp:
- 07/30/2026 12:38:57 PM (6 weeks ago)
- Branches:
- main
- Children:
- 55eca8b
- Parents:
- f9b1c5c
- Files:
-
- 1 added
- 2 edited
-
src/routes/activitypub.js (modified) (1 diff)
-
src/services/ActivityPubService.js (modified) (8 diffs)
-
test/sent-notes.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
rf9b1c5c r6a99668 280 280 }, 281 281 })); 282 // Newest first over both, so the app can keep treating this as one feed. 283 const items = [...posts, ...messages].sort((a, b) => String(b.published || '').localeCompare(String(a.published || ''))); 282 // Your OWN sent notes (replies and direct messages, ap_outbox): without 283 // them a reply existed everywhere except in your own app, Messages showed 284 // half a conversation, and a retry ran into the duplicate guard (Robins 285 // melding, 30-7). Served like the other legs: same shape, one parser. 286 const mine = AP.selfAuthor(base, auth.site); 287 const sent = AP.getSentNotes(base, auth.site, 60).map((n) => ({ 288 id: `${n.id}#create`, 289 type: 'Create', 290 actor: me, 291 published: n.published, 292 // The leading mention anchor is addressing, not prose (the DM leg strips 293 // it the same way); the Mention tags built from the full content stay. 294 object: { ...n, content: AP.stripLeadingMentions(n.content), 'shaer:author': mine }, 295 })); 296 // 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 || ''))); 284 298 AP.sendAP(res, { 285 299 '@context': AP.AP_CONTEXT, -
src/services/ActivityPubService.js
rf9b1c5c r6a99668 2238 2238 } 2239 2239 2240 // The account's own outbound notes (replies and direct messages) as AS2 2241 // Notes, newest first. The C2S inbox read serves these alongside the 2242 // timeline: without them your own reply existed everywhere EXCEPT in your 2243 // own app (Robins melding, 30-7: "replyen werkt nog niet"; het antwoord 2244 // stond op de server maar de app kreeg het nooit terug, dus je probeerde 2245 // het opnieuw en liep in de duplicate-guard). 2246 export function getSentNotes(base, site, limit = 60) { 2247 return db.prepare('SELECT * FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC LIMIT ?') 2248 .all(site.slug, limit) 2249 .map((row) => buildReplyNote(base, site, row)); 2250 } 2251 2240 2252 // Resolve one of our outbound reply Notes by id (for /ap/notes/:id fallback). 2241 2253 export function getOutboxNote(base, id) { … … 2319 2331 } 2320 2332 if (object.inReplyTo) { 2321 const parent = await resolveRemoteNote(c2sIdOf(object.inReplyTo) ).catch(() => null);2333 const parent = await resolveRemoteNote(c2sIdOf(object.inReplyTo), { asSlug: site.slug }).catch(() => null); 2322 2334 if (!parent) return { status: 502, error: 'cannot_resolve_inReplyTo' }; 2323 2335 // The attachments ride along (Robins melding, 30-7: "502 … … 2359 2371 } 2360 2372 } 2361 const note = await resolveRemoteNote(targetUri ).catch(() => null);2373 const note = await resolveRemoteNote(targetUri, { asSlug: site.slug }).catch(() => null); 2362 2374 const objUri = (note && note.object_uri) || targetUri; 2363 2375 const authorUri = note && note.actor_uri; … … 2397 2409 if (innerType === 'Like' || innerType === 'Announce') { 2398 2410 const kind = innerType === 'Announce' ? 'unboost' : 'unlike'; 2399 const note = await resolveRemoteNote(innerTarget ).catch(() => null);2411 const note = await resolveRemoteNote(innerTarget, { asSlug: site.slug }).catch(() => null); 2400 2412 const objUri = (note && note.object_uri) || innerTarget; 2401 2413 await sendInteraction(site, kind, objUri, note && note.actor_uri); … … 2586 2598 // Attachments count toward "the same": two media-only replies share content. 2587 2599 const mediaJson = media.length ? JSON.stringify(media) : null; 2588 const dup = db.prepare('SELECT 1 FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? AND IFNULL(attachments, \'\') = IFNULL(?, \'\') LIMIT 1') 2600 // A duplicate is idempotent success, not an error: it answers with the 2601 // EXISTING id. Returning without one made the C2S ingest say 502 2602 // reply_failed on a double-submit (Robins schermafdruk, 30-7), so a retry 2603 // of a reply the app never showed looked like the reply itself failing. 2604 const dup = db.prepare('SELECT id FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? AND IFNULL(attachments, \'\') = IFNULL(?, \'\') LIMIT 1') 2589 2605 .get(site.slug, parent.object_uri || '', content, mediaJson); 2590 if (dup) { console.log('[AP] outreply skipped (duplicate)'); return { duplicate: true, delivered: 0 }; }2606 if (dup) { console.log('[AP] outreply skipped (duplicate)'); return { duplicate: true, id: dup.id, delivered: 0 }; } 2591 2607 const id = crypto.randomUUID(); 2592 2608 iStmts().insO.run(id, site.slug, postId, postSlug || null, parent.object_uri || null, toActorUri, toHandle, content, replyLang, mediaJson); … … 2643 2659 // Resolve a remote post URL (any fediverse/Klonkt post) into a reply target. 2644 2660 // Returns a parent-shaped object usable by deliverReply(), or null. 2645 export async function resolveRemoteNote(url ) {2661 export async function resolveRemoteNote(url, opts = {}) { 2646 2662 if (!/^https?:\/\//i.test(String(url || ''))) return null; 2647 const note = await fetchActor(url).catch(() => null); // AP GET (content-negotiates) 2663 // With `asSlug` the fetches are SIGNED as that local actor. An anonymous 2664 // GET can only read public notes; a friends-only note (Shaer's default!) 2665 // rightly refuses it, which made every reply to a friend's post fail while 2666 // a reply to your own public post worked (Robins melding, 30-7). Signed, 2667 // the other server sees WHO asks and serves what the friendship earns. 2668 const get = (u) => (opts.asSlug ? signedGetJson(opts.asSlug, u) : fetchActor(u).catch(() => null)); 2669 const note = await get(url); // AP GET (content-negotiates) 2648 2670 if (!note || !note.id) return null; 2649 2671 const att = note.attributedTo; 2650 2672 const actorUri = actorUriOf(att); 2651 2673 if (!actorUri) return null; 2652 const actor = await fetchActor(actorUri).catch(() => null);2674 const actor = await get(actorUri); 2653 2675 const ai = actorInfo(actor, actorUri); 2654 2676 // Is what we're replying to a post (or a comment) on one of OUR posts? If so, … … 2664 2686 const url = typeof cursor === 'string' ? cursor : (cursor && cursor.id); 2665 2687 if (!url) break; 2666 const pn = await fetchActor(url).catch(() => null);2688 const pn = await get(url); 2667 2689 if (!pn) break; 2668 2690 const pa = actorUriOf(pn.attributedTo); 2669 2691 if (pa && pa !== actorUri) { 2670 const paDoc = await fetchActor(pa).catch(() => null);2692 const paDoc = await get(pa); 2671 2693 const inbox = paDoc && ((paDoc.endpoints && paDoc.endpoints.sharedInbox) || paDoc.inbox); 2672 2694 if (inbox && !seenInbox.has(inbox)) { seenInbox.add(inbox); threadInboxes.push(inbox); } … … 4167 4189 buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured, 4168 4190 followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins, 4169 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,4191 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, 4170 4192 listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote, 4171 4193 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
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)