Changeset 08ab8ad in Klonkt
- Timestamp:
- 07/29/2026 11:26:00 AM (6 weeks ago)
- Branches:
- main
- Children:
- 6100ce9
- Parents:
- 6d5ce0c
- Files:
-
- 1 added
- 2 edited
-
src/routes/activitypub.js (modified) (2 diffs)
-
src/services/ActivityPubService.js (modified) (2 diffs)
-
test/c2s-messages.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
r6d5ce0c r08ab8ad 161 161 const playbackAllowed = embedsAllowed 162 162 && Guardianship.externalPlaybackAllowed(auth.site.external_playback, isWard); 163 const items = AP.getTimeline(auth.site.slug, 60).map((t) => ({163 const posts = AP.getTimeline(auth.site.slug, 60).map((t) => ({ 164 164 id: `${t.id}#create`, 165 165 type: 'Create', … … 217 217 }, 218 218 })); 219 // The direct notes addressed to this account: a plain DM, a guardian's wave 220 // (§5), a ward's 🛟 help request (§5.2.1). Those are messages, not posts, so 221 // they are not in the timeline; without them the app's Berichten shows only 222 // what you said yourself. Same shape as a post, so one parser handles both. 223 const me = AP.actorId(base, auth.site.slug); 224 const myHandle = (() => { try { return `@${auth.site.slug}@${new URL(base).host}`; } catch { return `@${auth.site.slug}`; } })(); 225 const messages = AP.getDirectMessages(auth.site.slug, 60).map((m) => ({ 226 id: `${m.object_uri}#create`, 227 type: 'Create', 228 actor: m.actor_uri, 229 published: AP.isoStamp(m.published || m.created_at), 230 object: { 231 id: m.object_uri, 232 type: 'Note', 233 attributedTo: m.actor_uri, 234 content: AP.stripLeadingMentions(m.content), 235 url: m.note_url || undefined, 236 published: AP.isoStamp(m.published || m.created_at), 237 // Addressed to us and to nobody we know of: the other recipients of a 238 // note to several people are not ours to see, so we serve what we know. 239 to: [me], 240 // The Mention is how the client recognises itself as the addressee and 241 // groups the note into a conversation. No FEP-e232 link tags here: a 242 // mention row keeps the resolved quote, not the raw tags. 243 tag: [{ type: 'Mention', href: me, name: myHandle }, ...(AP.timelineEmojis(m.emoji_json) || [])], 244 attachment: AP.timelineAttachments(m.media_json), 245 // FEP-633c: what kind of message this is. The wave is a gentle nudge from 246 // a guardian; the help request is the buoy. Both render differently. 247 'shaer:wave': m.wave ? true : undefined, 248 'shaer:helpRequest': m.help_request ? true : undefined, 249 'shaer:quote': AP.timelineQuote(m.quote_json), 250 'shaer:author': (m.actor_name || m.actor_handle || m.actor_icon) ? { 251 name: m.actor_name || undefined, handle: m.actor_handle || undefined, 252 icon: m.actor_icon || undefined, url: m.actor_url || undefined, 253 emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(), 254 } : undefined, 255 'shaer:embed': embedsAllowed ? AP.timelineEmbed(m.embed_json, { playback: playbackAllowed }) : undefined, 256 }, 257 })); 258 // Newest first over both, so the app can keep treating this as one feed. 259 const items = [...posts, ...messages].sort((a, b) => String(b.published || '').localeCompare(String(a.published || ''))); 219 260 AP.sendAP(res, { 220 261 '@context': AP.AP_CONTEXT, -
src/services/ActivityPubService.js
r6d5ce0c r08ab8ad 2734 2734 export function getTimeline(slug, limit, offset) { return tlStmts().list.all(slug, limit || 50, offset || 0); } 2735 2735 2736 /** 2737 * The direct notes addressed to this account: a plain DM, a guardian's wave 2738 * (§5), a ward's 🛟 help request (§5.2.1). They live in ap_mentions and NOT in 2739 * the timeline, because a note addressed to named people is a message and not a 2740 * post (belongsInTimeline). 2741 * 2742 * A client that only reads the timeline therefore sees none of them, which is 2743 * exactly what happened to Shaer: Berichten showed your own replies (those come 2744 * from your outbox) and nothing that was said to you. The C2S inbox read serves 2745 * both, so the app has one door for everything that arrives. 2746 * 2747 * A public mention from someone you follow is stored in both tables; those are 2748 * skipped here and stay a post. 2749 */ 2750 export function getDirectMessages(slug, limit) { 2751 try { 2752 return db.prepare(` 2753 SELECT m.object_uri, m.note_url, m.actor_uri, m.actor_name, m.actor_handle, m.actor_icon, m.actor_url, 2754 m.content, m.published, m.created_at, m.wave, m.help_request, 2755 m.emoji_json, m.actor_emoji_json, m.media_json, m.quote_json, m.embed_json 2756 FROM ap_mentions m 2757 WHERE m.slug = ? 2758 AND NOT EXISTS (SELECT 1 FROM ap_timeline t WHERE t.slug = m.slug AND t.id = m.object_uri) 2759 ORDER BY COALESCE(m.published, m.created_at) DESC LIMIT ?`).all(slug, limit || 60); 2760 } catch { return []; } 2761 } 2762 2763 /** 2764 * A stored stamp as an ISO instant. SQLite's CURRENT_TIMESTAMP writes 2765 * 'YYYY-MM-DD HH:MM:SS' in UTC, which Date.parse reads as LOCAL time; on a 2766 * server two hours ahead that dated every message two hours early and put the 2767 * conversation in the wrong order. A `published` from the wire is already ISO 2768 * and passes through untouched. 2769 */ 2770 export function isoStamp(v) { 2771 if (!v) return undefined; 2772 const s = String(v); 2773 if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}$/.test(s)) return `${s.replace(' ', 'T')}Z`; 2774 const t = Date.parse(s); 2775 return Number.isFinite(t) ? new Date(t).toISOString() : undefined; 2776 } 2777 2736 2778 // Inbox C2S read: a timeline row's media_json ([{url, type}], written on the 2737 2779 // inbound Create) → AS2 `attachment` array, so a client (Shaer) can render a … … 3927 3969 3928 3970 export default { 3929 AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, 3971 AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions, 3930 3972 buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured, 3931 3973 followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins, 3932 3974 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote, 3933 3975 listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote, 3934 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,3976 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll, 3935 3977 acceptGatedFollow, rejectGatedFollow, isWardGuardian, sendFollowDecision, 3936 3978 parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
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)