Changeset e27b8db in Klonkt for src/routes


Ignore:
Timestamp:
07/29/2026 10:04:36 AM (6 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
329873e
Parents:
88d7c8f
Message:

Afspelen in de app als tweede gated feature, en het gat in de gate

Bij het uitzoeken van de YouTube-vraag bleek de gate lek. De web-Krant bouwt de
speler uit de inhoud van de post via timelineEmbedHtml, en dat pad raakte
gateEmbeds nooit. Een ward wiens guardians niets hadden toegestaan kreeg dus de
volledige YouTube-speler op het web, terwijl de app niets liet zien: het zware
ding open, het lichte dicht. Precies omgekeerd.

Nu zijn het twee besluiten, want het zijn twee dingen. Zien dat er een filmpje
is, is niet hetzelfde als het scherm afstaan aan de motor van een derde partij,
compleet met eindscherm en volgende-video. shaer:externalEmbeds houdt de kaart,
shaer:externalPlayback de speler, allebei standaard uit voor een ward, en
afspelen vereist de kaart: je kunt niet spelen wat je niet mag zien.

En het antwoord op Robins vraag over de links: die vallen er ook onder. De gate
verborg tot nu toe alleen het plaatje terwijl de kale link eronder gewoon
aantikbaar bleef, dus de deur stond open met een doek eroverheen. Staat de gate
dicht, dan toont de kaart zich nog wel maar is hij geen deur meer.

De server bepaalt wat gespeeld mag worden, niet de client: hij levert
shaer:playerUrl mee, alleen bij een open gate en alleen in de privacy-variant
(youtube-nocookie met rel=0, of de eigen speler van de PeerTube-instance). De
app houdt zo geen lijst van hosts bij; hij speelt wat hij krijgt aangereikt.

Changed files:
src/config/database.js

  • kolom sites.external_playback

src/services/guardianship/notes.js

  • externalPlaybackAllowed naast externalEmbedsAllowed

src/services/guardianship/gated.js

  • shaer:externalPlayback in de feature-tabel

src/services/ActivityPubService.js

  • timelineEmbed voegt shaer:playerUrl toe als afspelen mag; playerUrlFor kent alleen privacy-varianten en weigert de rest

src/routes/activitypub.js

  • shaer:capabilities op de owner-only inbox-read: wat mag dit account
  • de embed draagt de speler-URL alleen bij een open playback-gate

src/routes/posts.js

  • het gat gedicht: de speler-iframe op de web-Krant valt nu onder de gate

src/routes/guardian.js

  • de voorstel-route is feature-bewust; het lokale pad stuurt nu ook door

src/assets/js/guardian.js

  • tweede knop in het paneel, alleen zichtbaar als de kaart al aan staat

src/services/i18n.js

  • de labels in nl, en, de

test/gated-settings.test.js

  • drie tests: de speler-URL rijdt alleen mee bij een open gate, een pagina die we niet framen blijft een thumbnail, en afspelen vereist de kaart

remarks: 280 tests groen. Niets geforceerd: beide gates staan standaard uit
voor een ward en twee van de drie guardians moeten nog steeds akkoord gaan.

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

Location:
src/routes
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r88d7c8f re27b8db  
    154154  const isWard = (() => { try { return Guardianship.listGuardians(auth.site.slug).length > 0; } catch { return false; } })();
    155155  const embedsAllowed = Guardianship.externalEmbedsAllowed(auth.site.external_embeds, isWard);
     156  // The heavier sibling (5.6): may a third party's PLAYER run inside the app,
     157  // and may a link hand the child over to a browser? Both are the guardians'
     158  // call, both default to off for a ward, and both need the preview gate open
     159  // first: you cannot play, or follow, what you may not see. Served here so
     160  // the app knows what it may offer instead of guessing.
     161  const playbackAllowed = embedsAllowed
     162    && Guardianship.externalPlaybackAllowed(auth.site.external_playback, isWard);
    156163  const items = AP.getTimeline(auth.site.slug, 60).map((t) => ({
    157164    id: `${t.id}#create`,
     
    206213      // An external (non-fediverse) embed, thumbnail-only and never an iframe.
    207214      // Omitted entirely when the gate is closed (see above).
    208       'shaer:embed': embedsAllowed ? AP.timelineEmbed(t.embed_json) : undefined,
     215      // Carries shaer:playerUrl only when the playback gate is open too.
     216      'shaer:embed': embedsAllowed ? AP.timelineEmbed(t.embed_json, { playback: playbackAllowed }) : undefined,
    209217    },
    210218  }));
     
    213221    id: `${base}/ap/users/${auth.site.slug}/inbox`,
    214222    type: 'OrderedCollection',
     223    // What this account may do with what is in here (FEP-633c 5.6). Owner-only
     224    // by construction, and never on the public actor document: it says
     225    // something about a child, and only the child and its guardians need it.
     226    'shaer:capabilities': {
     227      'shaer:externalEmbeds': embedsAllowed,
     228      'shaer:externalPlayback': playbackAllowed,
     229      // Leaving the app is the same decision as playing inside it: with the
     230      // gate shut a link is shown but not followed, so the door is closed too
     231      // and not just the picture over it.
     232      'shaer:externalLinks': playbackAllowed,
     233    },
    215234    totalItems: items.length,
    216235    orderedItems: items,
  • src/routes/guardian.js

    r88d7c8f re27b8db  
    5454    'away_title', 'away_sub', 'away_week', 'away_month', 'away_done',
    5555    // A gated-setting proposal from a fellow guardian (5.6).
    56     'gated_title', 'gated_line_on', 'gated_line_off', 'gated_agree', 'gated_disagree'];
     56    'gated_title', 'gated_line_on', 'gated_line_off', 'gated_agree', 'gated_disagree',
     57    'play_propose', 'play_on', 'play_off'];
    5758  const s = Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)]));
    5859  s.wave = i18nT(L, 'guardian.wave');
     
    9091      ...w,
    9192      embeds: wardEmbedSetting(w.other_uri),
     93      playback: wardPlaybackSetting(w.other_uri),
    9294      guardians: wardGuardianStatuses(w.other_uri),
    9395    })),
     
    466468 * the ward lives elsewhere and the setting is not ours to show.
    467469 */
    468 function wardEmbedSetting(uri) {
     470function wardEmbedSetting(uri) { return wardGateSetting(uri, 'external_embeds'); }
     471/** The playback gate of a ward we host (5.6): the heavier sibling. */
     472function wardPlaybackSetting(uri) { return wardGateSetting(uri, 'external_playback'); }
     473function wardGateSetting(uri, column) {
    469474  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    470475  if (!base || !String(uri || '').startsWith(`${base}/`)) return null;
    471476  const slug = String(uri).trim().replace(/\/+$/, '').split('/').pop();
    472   const row = slug ? db.prepare('SELECT external_embeds FROM sites WHERE slug = ?').get(slug) : null;
     477  const row = slug ? db.prepare(`SELECT ${column === 'external_playback' ? 'external_playback' : 'external_embeds'} AS v FROM sites WHERE slug = ?`).get(slug) : null;
    473478  if (!row) return null;
    474   return row.external_embeds === null || row.external_embeds === undefined ? false : row.external_embeds === 1;
     479  return row.v === null || row.v === undefined ? false : row.v === 1;
    475480}
    476481
     
    480485// can move, and only a committed guardian of THAT ward may move it.
    481486router.post('/wards/embeds', requireAuth, express.json({ limit: '4kb' }), (req, res) => {
     487  req.body = { ...req.body, feature: req.body?.feature === 'shaer:externalPlayback' ? 'shaer:externalPlayback' : 'shaer:externalEmbeds' };
     488  return proposeGated(req, res);
     489});
     490function proposeGated(req, res) {
    482491  const site = siteForUser(req);
    483492  if (!site) return res.status(404).json({ error: 'no_site' });
     
    496505  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    497506  const me = AP.actorId(base, site.slug);
    498   const feature = 'shaer:externalEmbeds';
     507  const feature = req.body.feature;   // normalised by the route above
    499508  const offerId = `${me}/gated/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
    500509  const offer = Guardianship.gated.buildGatedOffer(offerId, me, uri, feature, allow);
     
    504513    Guardianship.gated.rememberGatedOffer(offerId, localWard.slug, feature, allow);
    505514    const r = Guardianship.gated.recordGatedVote(localWard.slug, feature, me, allow);
     515    // Same forward as the S2S path: without it the other guardians never learn
     516    // the proposal exists and a threshold of two can never be met.
     517    if (r.state === 'open') {
     518      for (const g of Guardianship.listGuardians(localWard.slug).map((x) => x.other_uri)) {
     519        if (g === me) continue;
     520        AP.deliverToActor(site, g, { ...offer, to: [g] }).catch(() => { /* queued */ });
     521      }
     522    }
    506523    return res.json({ ok: true, allow, state: r.state, need: r.need, of: r.of });
    507524  }
  • src/routes/posts.js

    r88d7c8f re27b8db  
    10841084function gateEmbeds(site, rows) {
    10851085  if (!site || !rows.length) return rows;
    1086   let isWard = false;
    1087   try { isWard = Guardianship.listGuardians(site.slug).length > 0; } catch { /* no relations yet */ }
    1088   if (Guardianship.externalEmbedsAllowed(site.external_embeds, isWard)) return rows;
     1086  if (embedsAllowedFor(site)) return rows;
    10891087  return rows.map((r) => (r && r.embed_json ? { ...r, embed_json: null } : r));
     1088}
     1089
     1090function isWardSite(site) {
     1091  try { return !!site && Guardianship.listGuardians(site.slug).length > 0; } catch { return false; }
     1092}
     1093function embedsAllowedFor(site) {
     1094  return !site || Guardianship.externalEmbedsAllowed(site.external_embeds, isWardSite(site));
     1095}
     1096/**
     1097 * May a third-party PLAYER run inside this page? (FEP-633c 5.6, the heavier
     1098 * sibling of the preview gate.) This was the hole: the player iframe is built
     1099 * from the note's content by timelineEmbedHtml, on a path that never touched
     1100 * gateEmbeds. A ward whose guardians had allowed nothing still got the full
     1101 * YouTube player on the web, while the app showed nothing at all: the heavy
     1102 * thing open, the light thing shut. Playback also requires the preview gate,
     1103 * because you cannot play what you may not see.
     1104 */
     1105function playbackAllowedFor(site) {
     1106  if (!site) return true;
     1107  if (!embedsAllowedFor(site)) return false;
     1108  return Guardianship.externalPlaybackAllowed(site.external_playback, isWardSite(site));
    10901109}
    10911110
     
    10981117  const rows = gateEmbeds(site, site ? ActivityPubService.getTimeline(site.slug, FEED_PAGE + 1, offset) : []);
    10991118  const hasMore = rows.length > FEED_PAGE;
     1119  // Players (a third party's engine inside our page) ride the playback gate;
     1120  // a Klonkt site's own audio embed is ours and stays.
     1121  const mayPlay = playbackAllowedFor(site);
    11001122  const timeline = rows.slice(0, FEED_PAGE).map((p) => {
    1101     let embedHtml = timelineEmbedHtml(p.content);
     1123    let embedHtml = mayPlay ? timelineEmbedHtml(p.content) : null;
    11021124    let content = p.content;
    11031125    let embedUrl = null;
Note: See TracChangeset for help on using the changeset viewer.