Changeset b258a79 in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 07/28/2026 07:53:36 AM (6 weeks ago)
- Branches:
- main
- Children:
- 0101d0a
- Parents:
- fc40410
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
rfc40410 rb258a79 503 503 // FEP-633c §2.2: object hint that the author is a ward (safely ignorable). 504 504 Object.assign(note, Guardianship.hasGuardiansProps(site.slug)); 505 // FEP-044f: this post quotes a fediverse object. Emit it the way the network 506 // actually reads it, and address the quoted author so they get told. 507 applyQuoteProps(note, post.quote_uri, post.quote_actor); 505 508 if (post.nsfw) note.summary = post.content_warning || 'Gevoelige inhoud'; 506 509 if (attachment.length) note.attachment = attachment; … … 1775 1778 // mentioned person is notified even if they don't follow us (Mastodon-standard mention). 1776 1779 const mres = await resolveMentionsInText(base, post.content || ''); 1777 const post2 = mres.inboxes.length ? { ...post, content: mres.html } : post; 1780 let post2 = mres.inboxes.length ? { ...post, content: mres.html } : post; 1781 // FEP-044f: does this post quote a fediverse object? Resolve it once, here, 1782 // and remember it on the post, so buildNote (sync, also used by the outbox) 1783 // never has to fetch. The quoted author's inbox joins the delivery set: that 1784 // IS the notification. 1785 const quoteInboxes = []; 1786 if (post2.quote_uri === undefined || post2.quote_uri === null) { 1787 const q = await resolveOwnQuote(post2.content || ''); 1788 if (q) { 1789 try { db.prepare('UPDATE posts SET quote_uri = ?, quote_actor = ? WHERE id = ?').run(q.uri, q.actor || null, post.id); } catch { /* ignore */ } 1790 post2 = { ...post2, quote_uri: q.uri, quote_actor: q.actor || null }; 1791 } 1792 } 1793 if (post2.quote_actor) { 1794 const a = await fetchActor(post2.quote_actor).catch(() => null); 1795 const inbox = a && ((a.endpoints && a.endpoints.sharedInbox) || a.inbox); 1796 if (inbox) quoteInboxes.push(inbox); 1797 } 1778 1798 const followers = fStmts().list.all(site.slug); 1779 const inboxes = [...new Set([...followers.map((f) => f.shared_inbox || f.inbox), ...mres.inboxes ].filter(Boolean))];1780 if (!inboxes.length) return; // no followers and no one mentioned1799 const inboxes = [...new Set([...followers.map((f) => f.shared_inbox || f.inbox), ...mres.inboxes, ...quoteInboxes].filter(Boolean))]; 1800 if (!inboxes.length) return; // no followers, no one mentioned, no one quoted 1781 1801 const keys = getOrCreateKeys(site.slug); 1782 1802 const keyId = `${actorId(base, site.slug)}#main-key`; … … 2848 2868 } 2849 2869 2870 // FEP-044f, emit side. The mirror of extractQuoteUrl (ingest): when one of our 2871 // own posts quotes a fediverse object, say so in the shapes the network really 2872 // reads. `quote` is the FEP property; quoteUrl / _misskey_quote are the de-facto 2873 // ones Mastodon and Misskey look at, and the FEP-e232 `Link` in `tag` is the 2874 // third form. All three point at the same object, which is what every reader 2875 // expects. The quoted author goes in `cc`, because being quoted without being 2876 // told is exactly the rudeness this FEP is trying to design away. 2877 export function applyQuoteProps(note, quoteUri, quoteActor) { 2878 if (!note || typeof quoteUri !== 'string' || !/^https?:\/\//i.test(quoteUri)) return note; 2879 note.quote = quoteUri; 2880 note.quoteUrl = quoteUri; 2881 note['_misskey_quote'] = quoteUri; 2882 note.tag = [...(note.tag || []), { 2883 type: 'Link', 2884 mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 2885 href: quoteUri, 2886 rel: ['https://misskey-hub.net/ns#_misskey_quote'], 2887 name: quoteUri, 2888 }]; 2889 if (typeof quoteActor === 'string' && /^https?:\/\//i.test(quoteActor)) { 2890 note.cc = [...new Set([...(note.cc || []), quoteActor])]; 2891 } 2892 return note; 2893 } 2894 2850 2895 // The first external (non-fediverse) link in a note, resolved to the same card 2851 2896 // shape as a quote: THUMBNAIL ONLY, never the provider's iframe. An arbitrary … … 2867 2912 const thumb = (card.media || []).find((m) => m && m.url); 2868 2913 if (!thumb && !card.title) return null; 2914 // Title, provider and author name come from a third party. Store them as 2915 // PLAIN TEXT (tags stripped, length-capped), so no renderer downstream has to 2916 // be the one that remembers to escape. A card is a card, not an essay. 2917 const plain = (v) => (v ? HtmlSanitizerService.toPlainText(String(v)).trim().slice(0, 200) : null); 2869 2918 return JSON.stringify({ 2870 2919 url: card.url, 2871 2920 kind: card.kind, // 'provider' | 'oembed' 2872 provider: card.provider || null,2873 title: card.title || null,2874 author: card.author ||null,2921 provider: plain(card.provider), 2922 title: plain(card.title), 2923 author: card.author ? { ...card.author, name: plain(card.author.name), handle: plain(card.author.handle) } : null, 2875 2924 media: thumb ? [thumb] : [], // thumbnail only, no html/iframe 2876 2925 }); 2926 } 2927 2928 /** 2929 * Does our own post link to a fediverse object? Returns { uri, actor } when the 2930 * first external link resolves to a quotable AP object, else null. Runs once at 2931 * publish time; the answer is stored on the post. 2932 */ 2933 export async function resolveOwnQuote(html) { 2934 const first = firstExternalUrl(html); 2935 if (!first) return null; 2936 const io = EmbedResolver.liveIO({ safeFetch, detectProvider: () => null, fetchActor, actorInfo }); 2937 const card = await EmbedResolver.resolveEmbed(first, io).catch(() => null); 2938 if (!card || card.kind !== 'ap' || !card.id) return null; 2939 return { uri: card.id, actor: card.attributedTo || null }; 2877 2940 } 2878 2941 … … 3634 3697 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote, 3635 3698 listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote, 3636 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, sendInteraction, voteOnPoll, voteOnRemotePoll,3699 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, sendInteraction, voteOnPoll, voteOnRemotePoll, 3637 3700 acceptGatedFollow, rejectGatedFollow, isWardGuardian, sendFollowDecision, 3638 3701 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)