Changeset fc40410 in Klonkt for src/services
- Timestamp:
- 07/28/2026 07:43:47 AM (6 weeks ago)
- Branches:
- main
- Children:
- b258a79
- Parents:
- 6bc2e31b
- Location:
- src/services
- Files:
-
- 3 edited
-
ActivityPubService.js (modified) (4 diffs)
-
guardianship/index.js (modified) (1 diff)
-
guardianship/notes.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r6bc2e31b rfc40410 22 22 import HtmlSanitizerService from './HtmlSanitizerService.js'; 23 23 import AudioEmbedService from './AudioEmbedService.js'; 24 import EmbedResolver from './EmbedResolver.js'; 24 25 import Push from './PushService.js'; 25 26 import { getTenancy } from './SettingsService.js'; … … 1575 1576 if (!qj) return; 1576 1577 for (const sl of slugs) { try { db.prepare('UPDATE ap_timeline SET quote_json = ? WHERE id = ? AND slug = ?').run(qj, o.id, sl); } catch { /* ignore */ } } 1578 }).catch(() => { /* best-effort */ }); 1579 } else { 1580 // No fediverse quote: try an EXTERNAL embed (oEmbed / known provider), 1581 // thumbnail-only. Also out of band, and stored for everyone; the gate 1582 // that decides who may SEE it is applied at serve time (§5.3-style 1583 // gated feature, see the inbox read). 1584 const slugs = subs.map((s) => s.slug); 1585 resolveExternalEmbed(o.content).then((ej) => { 1586 if (!ej) return; 1587 for (const sl of slugs) { try { db.prepare('UPDATE ap_timeline SET embed_json = ? WHERE id = ? AND slug = ?').run(ej, o.id, sl); } catch { /* ignore */ } } 1577 1588 }).catch(() => { /* best-effort */ }); 1578 1589 } … … 2837 2848 } 2838 2849 2850 // The first external (non-fediverse) link in a note, resolved to the same card 2851 // shape as a quote: THUMBNAIL ONLY, never the provider's iframe. An arbitrary 2852 // third-party frame inside a kid-safe app is a hole you cannot close again, so 2853 // the embed carries an image and a title and nothing executable. 2854 // Returns the JSON to store, or null when there is nothing worth showing. 2855 export async function resolveExternalEmbed(html) { 2856 const first = firstExternalUrl(html); 2857 if (!first) return null; 2858 const io = EmbedResolver.liveIO({ 2859 safeFetch, 2860 detectProvider: (u) => AudioEmbedService.detectProvider(u), 2861 fetchActor, 2862 actorInfo, 2863 }); 2864 const card = await EmbedResolver.resolveEmbed(first, io).catch(() => null); 2865 // 'ap' is handled by the quote path; a bare 'link' is not worth a card. 2866 if (!card || card.kind === 'ap' || card.kind === 'link') return null; 2867 const thumb = (card.media || []).find((m) => m && m.url); 2868 if (!thumb && !card.title) return null; 2869 return JSON.stringify({ 2870 url: card.url, 2871 kind: card.kind, // 'provider' | 'oembed' 2872 provider: card.provider || null, 2873 title: card.title || null, 2874 author: card.author || null, 2875 media: thumb ? [thumb] : [], // thumbnail only, no html/iframe 2876 }); 2877 } 2878 2879 /** The first http(s) link in sanitized note HTML that is not a mention/hashtag. */ 2880 export function firstExternalUrl(html) { 2881 if (!html || typeof html !== 'string') return null; 2882 for (const m of html.matchAll(/<a\b[^>]*href=["']([^"']+)["'][^>]*>/gi)) { 2883 const tag = m[0]; 2884 if (/\b(mention|hashtag|u-url)\b/i.test(tag) && /mention|hashtag/i.test(tag)) continue; 2885 const href = m[1]; 2886 if (/^https?:\/\//i.test(href)) return href; 2887 } 2888 return null; 2889 } 2890 2891 /** The stored external-embed card, for the C2S read. */ 2892 export function timelineEmbed(embedJson) { 2893 try { const e = embedJson ? JSON.parse(embedJson) : null; return (e && typeof e === 'object' && e.url) ? e : undefined; } 2894 catch { return undefined; } 2895 } 2896 2839 2897 // FEP-044f embedded quote card: resolve the quoted post to a compact, sanitised 2840 2898 // snapshot { url, author{name,handle,icon}, content, published, media } so the … … 3576 3634 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote, 3577 3635 listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote, 3578 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, sendInteraction, voteOnPoll, voteOnRemotePoll,3636 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, sendInteraction, voteOnPoll, voteOnRemotePoll, 3579 3637 acceptGatedFollow, rejectGatedFollow, isWardGuardian, sendFollowDecision, 3580 3638 parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs, -
src/services/guardianship/index.js
r6bc2e31b rfc40410 16 16 */ 17 17 export { SHAER_CONTEXT, GUARDIAN_RELATIONSHIP, GUARDIAN_RELATIONSHIP_COMPACT, isGuardianRelationship } from './context.js'; 18 export { helpRequestProps, isHelpRequest, waveProps, isWave, hasGuardiansProps, objectHasGuardians } from './notes.js';18 export { helpRequestProps, isHelpRequest, waveProps, isWave, hasGuardiansProps, objectHasGuardians, externalEmbedsAllowed } from './notes.js'; 19 19 export { wireDelivery, c2sVisibility, deliverDirectNote } from './delivery.js'; 20 20 export { wireHandshake, handleOutbox as handleGuardianshipOutbox, handleInbox as handleGuardianshipInbox, parseRelationship } from './handshake.js'; -
src/services/guardianship/notes.js
r6bc2e31b rfc40410 15 15 try { return (slug && listGuardians(slug).length) ? { 'shaer:hasGuardians': true } : {}; } 16 16 catch { return {}; } 17 } 18 19 /** 20 * May EXTERNAL (non-fediverse) embeds be shown to this account? 21 * 22 * A gated feature in the FEP-633c sense: a ward's world outside the fediverse 23 * is the guardians' call. `setting` is `sites.external_embeds`: 24 * null/undefined → auto: off for a ward, on for anyone else 25 * 0 → off, 1 → on (the guardians decided) 26 * 27 * Pure, so the rule is testable on its own. The gate is applied SERVER-side: 28 * a blocked embed is never serialised into the feed, because an embed that the 29 * client merely hides has still been delivered. 30 */ 31 export function externalEmbedsAllowed(setting, isWard) { 32 if (setting === 0 || setting === 1) return setting === 1; 33 return !isWard; 17 34 } 18 35 … … 48 65 } 49 66 50 export default { helpRequestProps, isHelpRequest, waveProps, isWave, hasGuardiansProps, objectHasGuardians };67 export default { helpRequestProps, isHelpRequest, waveProps, isWave, hasGuardiansProps, objectHasGuardians, externalEmbedsAllowed };
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)