Changeset bdb78e4 in Klonkt for src/services/EmbedResolver.js


Ignore:
Timestamp:
07/28/2026 08:59:00 AM (6 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
2a76184
Parents:
b1512e0
Message:

Publiek oEmbed-register erbij: YouTube verstopt zijn tags voor een server

Waarom het op mijn machine werkte en op de VPS niet: YouTube serveert een
datacenter-IP een uitgeklede pagina. Titel leeg, nul meta property-tags, geen
oEmbed-link. Hun oEmbed-API antwoordt daar wel gewoon (status 200, echte titel).
Discovery via de pagina faalt dus precies waar het het meest uitmaakt.

Het publieke providerregister van oembed.com dicht dat gat, en dat is iets anders
dan de whitelist die we net weggehaald hebben: die lijst onderhouden wij niet, we
lezen er een die gepubliceerd wordt. Valt oembed.com weg, dan vallen we terug op
paginadiscovery en werkt alles gewoon minder slim.

Het register komt VOOR de paginafetch: het is een match in het geheugen plus een
klein API-verzoek, en scheelt dus ook de download van een pagina van een MB.
Eenmaal opgehaald blijft de lijst een dag staan; een stale lijst is beter dan
geen lijst.

Changed files:
src/services/EmbedResolver.js

  • matchProviderEndpoint (wildcard-schemes, host-fallback, {format} invullen)
  • oembedRequestUrl; registry-stap voor de pagina; liveIO cachet de lijst 24u

src/services/ActivityPubService.js

  • SELFHEAL_VERSION 19 -> 20

test/embed-resolver.test.js

  • 4 tests: schemes en host-fallback, url-opbouw, register wint van de pagina (en bespaart de download), en een onbereikbaar register valt netjes terug

remarks: 219 tests groen.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/EmbedResolver.js

    rb1512e0 rbdb78e4  
    5959  if (!image && !title) return null;
    6060  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 */
     75export 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. */
     101export function oembedRequestUrl(endpoint, url) {
     102  const sep = endpoint.includes('?') ? '&' : '?';
     103  return `${endpoint}${sep}format=json&url=${encodeURIComponent(url)}`;
    61104}
    62105
     
    136179  }
    137180
    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.
    140196  //
    141197  //    There is deliberately NO list of known providers here. A hardcoded list
     
    247303 * @param {object} deps - { safeFetch, detectProvider, actorInfo, fetchActor }
    248304 */
     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.
     308const REGISTRY_URL = 'https://oembed.com/providers.json';
     309const REGISTRY_TTL = 24 * 60 * 60 * 1000;
     310let _registry = null;
     311let _registryAt = 0;
     312
    249313export function liveIO({ safeFetch, detectProvider, fetchActor, actorInfo }) {
    250314  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    },
    251322    provider: detectProvider ? (u) => { try { return detectProvider(u); } catch { return null; } } : null,
    252323    getAP: async (u) => {
     
    273344}
    274345
    275 export default { resolveEmbed, findOEmbedEndpoint, findOpenGraph, looksLikeAPObject, fromOEmbed, fromAPObject, liveIO };
     346export default { resolveEmbed, findOEmbedEndpoint, findOpenGraph, matchProviderEndpoint, oembedRequestUrl, looksLikeAPObject, fromOEmbed, fromAPObject, liveIO };
Note: See TracChangeset for help on using the changeset viewer.