Changeset bdb78e4 in Klonkt
- Timestamp:
- 07/28/2026 08:59:00 AM (6 weeks ago)
- Branches:
- main
- Children:
- 2a76184
- Parents:
- b1512e0
- Files:
-
- 3 edited
-
src/services/ActivityPubService.js (modified) (1 diff)
-
src/services/EmbedResolver.js (modified) (4 diffs)
-
test/embed-resolver.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
rb1512e0 rbdb78e4 2849 2849 // during a flux window, e.g. a fleet-wide update), and drops notes that are gone 2850 2850 // (404/410). Bump SELFHEAL_VERSION only on a release that warrants a re-sync. 2851 const SELFHEAL_VERSION = 19; // v19: head-read no longer stops on an og:image mention inside inline script2851 const SELFHEAL_VERSION = 20; // v20: oEmbed provider registry, so platforms that hide their tags from a server still resolve 2852 2852 async function fetchNoteAP(url) { 2853 2853 try { -
src/services/EmbedResolver.js
rb1512e0 rbdb78e4 59 59 if (!image && !title) return null; 60 60 return { image: image && /^https?:\/\//i.test(image) ? image : null, title: title || null, site: meta['og:site_name'] || null }; 61 } 62 63 /** 64 * Match a URL against the public oEmbed provider registry (oembed.com). 65 * 66 * Discovery through the page is the pure way, but it only works when the page 67 * hands you its <link rel=oembed>, and big platforms do not always do that: from 68 * a datacentre IP, YouTube serves a stripped page with no oEmbed link and no 69 * OpenGraph at all, while its oEmbed API answers perfectly. The registry closes 70 * that gap without us keeping a list of hosts: it is published and maintained by 71 * oembed.com, we only read it. 72 * 73 * Pure, so the pattern matching is testable without a network. 74 */ 75 export function matchProviderEndpoint(url, providers) { 76 if (!Array.isArray(providers)) return null; 77 let host = ''; 78 try { host = new URL(url).host.replace(/^www\./, ''); } catch { return null; } 79 const toRe = (scheme) => new RegExp('^' + String(scheme) 80 .replace(/[.+?^${}()|[\]\\]/g, '\\$&') 81 .replace(/\*/g, '.*') + '$', 'i'); 82 for (const p of providers) { 83 for (const ep of (p.endpoints || [])) { 84 const target = typeof ep.url === 'string' ? ep.url.replace('{format}', 'json') : null; 85 if (!target) continue; 86 for (const scheme of (ep.schemes || [])) { 87 if (toRe(scheme).test(url)) return target; 88 } 89 // No schemes listed: fall back to the provider's own host. 90 if (!ep.schemes || !ep.schemes.length) { 91 let phost = ''; 92 try { phost = new URL(p.provider_url).host.replace(/^www\./, ''); } catch { /* skip */ } 93 if (phost && (host === phost || host.endsWith('.' + phost))) return target; 94 } 95 } 96 } 97 return null; 98 } 99 100 /** Add the url + json format to an oEmbed endpoint. */ 101 export function oembedRequestUrl(endpoint, url) { 102 const sep = endpoint.includes('?') ? '&' : '?'; 103 return `${endpoint}${sep}format=json&url=${encodeURIComponent(url)}`; 61 104 } 62 105 … … 136 179 } 137 180 138 // 2. oEmbed, then OpenGraph. One page fetch serves both: oEmbed is the richer 139 // protocol, OpenGraph is the one most of the web actually ships. 181 // 2. The oEmbed registry: a cheap in-memory match, then one small API call. 182 // Tried before the page because it is far cheaper AND because the big 183 // platforms are exactly the ones that hide their tags from a server. 184 if (io.registry && io.getJSON) { 185 const providers = await io.registry().catch(() => null); 186 const endpoint = matchProviderEndpoint(url, providers); 187 if (endpoint) { 188 const o = await io.getJSON(oembedRequestUrl(endpoint, url)).catch(() => null); 189 const card = fromOEmbed(url, o); 190 if (card) return card; 191 } 192 } 193 194 // 3. oEmbed via the page, then OpenGraph. One page fetch serves both: oEmbed 195 // is the richer protocol, OpenGraph is the one most of the web ships. 140 196 // 141 197 // There is deliberately NO list of known providers here. A hardcoded list … … 247 303 * @param {object} deps - { safeFetch, detectProvider, actorInfo, fetchActor } 248 304 */ 305 // The provider registry, fetched once and kept for a day. It is a public list 306 // maintained by oembed.com, not by us; if it is unreachable we simply fall back 307 // to page discovery, so nothing breaks, it just gets less clever. 308 const REGISTRY_URL = 'https://oembed.com/providers.json'; 309 const REGISTRY_TTL = 24 * 60 * 60 * 1000; 310 let _registry = null; 311 let _registryAt = 0; 312 249 313 export function liveIO({ safeFetch, detectProvider, fetchActor, actorInfo }) { 250 314 return { 315 registry: async () => { 316 if (_registry && Date.now() - _registryAt < REGISTRY_TTL) return _registry; 317 const body = await safeJsonText(safeFetch, REGISTRY_URL, 'application/json'); 318 if (!body) return _registry; // keep a stale list over none 319 try { _registry = JSON.parse(body); _registryAt = Date.now(); } catch { /* keep the old one */ } 320 return _registry; 321 }, 251 322 provider: detectProvider ? (u) => { try { return detectProvider(u); } catch { return null; } } : null, 252 323 getAP: async (u) => { … … 273 344 } 274 345 275 export default { resolveEmbed, findOEmbedEndpoint, findOpenGraph, looksLikeAPObject, fromOEmbed, fromAPObject, liveIO };346 export default { resolveEmbed, findOEmbedEndpoint, findOpenGraph, matchProviderEndpoint, oembedRequestUrl, looksLikeAPObject, fromOEmbed, fromAPObject, liveIO }; -
test/embed-resolver.test.js
rb1512e0 rbdb78e4 198 198 assert.equal(findOpenGraph(got).image, 'https://x/real.png', 'reads past the decoy to the real tag'); 199 199 }); 200 201 // The public oEmbed registry (oembed.com). Not a list we maintain: we read one 202 // that is published. It exists because discovery through the page fails exactly 203 // where it matters most, from a server YouTube hands a stripped page to. 204 const PROVIDERS = [ 205 { provider_name: 'YouTube', provider_url: 'https://www.youtube.com/', 206 endpoints: [{ schemes: ['https://*.youtube.com/watch*', 'https://youtu.be/*'], url: 'https://www.youtube.com/oembed' }] }, 207 { provider_name: 'Bare', provider_url: 'https://bare.example/', endpoints: [{ url: 'https://bare.example/oembed.{format}' }] }, 208 ]; 209 210 test('matchProviderEndpoint matches wildcard schemes and falls back to the host', async () => { 211 const { matchProviderEndpoint } = await import('../src/services/EmbedResolver.js'); 212 assert.equal(matchProviderEndpoint('https://youtu.be/abc?is=x', PROVIDERS), 'https://www.youtube.com/oembed'); 213 assert.equal(matchProviderEndpoint('https://www.youtube.com/watch?v=abc', PROVIDERS), 'https://www.youtube.com/oembed'); 214 assert.equal(matchProviderEndpoint('https://bare.example/thing/1', PROVIDERS), 'https://bare.example/oembed.json', 215 'no schemes listed, so the provider host decides, and {format} is filled in'); 216 assert.equal(matchProviderEndpoint('https://elders.example/x', PROVIDERS), null); 217 assert.equal(matchProviderEndpoint('not a url', PROVIDERS), null); 218 }); 219 220 test('oembedRequestUrl appends url + format, keeping an existing query', async () => { 221 const { oembedRequestUrl } = await import('../src/services/EmbedResolver.js'); 222 assert.ok(oembedRequestUrl('https://x/oembed', 'https://a/b?c=1').includes('format=json&url=https%3A%2F%2Fa%2Fb%3Fc%3D1')); 223 assert.ok(oembedRequestUrl('https://x/oembed?k=1', 'https://a/b').startsWith('https://x/oembed?k=1&')); 224 }); 225 226 test('the registry is tried before the page, and the page is not fetched when it hits', async () => { 227 let pageFetched = false; 228 const r = await resolveEmbed('https://youtu.be/abc', io({ 229 registry: async () => PROVIDERS, 230 getJSON: async () => OEMBED_JSON, 231 getPage: async () => { pageFetched = true; return OG_PAGE; }, 232 })); 233 assert.equal(r.kind, 'oembed'); 234 assert.equal(r.title, 'A talk'); 235 assert.ok(!pageFetched, 'a registry hit saves the whole page download'); 236 }); 237 238 test('an unreachable registry just falls through to the page', async () => { 239 const r = await resolveEmbed('https://lg.example/a', io({ 240 registry: async () => { throw new Error('offline'); }, 241 getPage: async () => OG_PAGE, 242 })); 243 assert.equal(r.kind, 'opengraph'); 244 });
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)