Changeset 4838760 in Klonkt


Ignore:
Timestamp:
08/08/2026 05:23:46 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
d1075a1
Parents:
abcf51a
Message:

De kaart op je eigen post, en de composer-preview (shaer-k3f)

Het bead-uitgangspunt "serverkant is klaar" bleek half waar: AP-quotes
kregen bij publiceren een quote_uri, maar eigen posts hadden geen kolom
voor een kaart-snapshot en de outbox serveerde geen shaer:quote of
shaer:embed -- de app kon een eigen post dus nooit als kaart tonen, ook
na publicatie niet.

Drie stukken, allemaal langs de pijplijn die er al was:

OPSLAAN deliverCreate resolvet de eerste externe link net als de

inbox dat voor binnenkomende posts doet: een fediverse-
object wordt een quote-snapshot (resolveQuoteByUri, de kern
van resolveQuote maar vanaf een kale URI), anders probeert
de link een externe kaart (resolveExternalEmbed). In
posts.quote_json/embed_json, VOOR de vroege return: ook een
post zonder volgers hoort zijn kaart te krijgen, de app
leest hem uit de outbox en niet uit een bezorging.

SERVEREN de friend-poot van de outbox draagt shaer:quote en -- alleen

voor de bearer en langs zijn eigen embeds-poort --
shaer:embed. Een remote vriend krijgt de embed niet: diens
server resolvet en gate zelf bij ontvangst.

PREVIEW GET /ap/users/:slug/card?url= (bearer-only): een URL langs

exact dezelfde pijplijn, zodat de preview in de composer
nooit iets belooft dat de post niet waarmaakt. Zelfde
gate-regels als de tijdlijn.

Drie tests met gestubde fetch: extern -> embed-snapshot, fediverse ->
quote-snapshot (en niet allebei), en de preview die voor dezelfde links
dezelfde kaarten geeft. Alle 723 groen.

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

Files:
1 added
3 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    rabcf51a r4838760  
    718718  // time so buildNote (sync, also used by the outbox) needs no network.
    719719  ensureColumn('posts', 'quote_uri', 'TEXT');     // the quoted object's id
     720  // De kaart op je EIGEN post (shaer-k3f): dezelfde snapshots die ap_timeline
     721  // voor binnenkomende posts draagt, maar dan voor wat je zelf publiceert --
     722  // zonder deze twee kan de app een eigen post nooit als kaart tonen.
     723  ensureColumn('posts', 'quote_json', 'TEXT');    // FEP-044f resolved quote snapshot
     724  ensureColumn('posts', 'embed_json', 'TEXT');    // externe linkkaart (oEmbed/OG), thumbnail-only
    720725  ensureColumn('posts', 'quote_actor', 'TEXT');   // its author, so we can address them
    721726  ensureColumn('ap_timeline', 'embed_json', 'TEXT');       // resolved EXTERNAL embed (oEmbed/provider), thumbnail-only; gated per site (sites.external_embeds)
  • src/routes/activitypub.js

    rabcf51a r4838760  
    191191    // entries carry, so your own cards get a header too (avatar + name).
    192192    const me = AP.selfAuthor(baseUrl(req), site);
     193    // De kaart op je eigen post (shaer-k3f): dezelfde shaer:quote/shaer:embed
     194    // die de tijdlijn voor andermans posts draagt, uit de snapshots die
     195    // deliverCreate bij het publiceren opsloeg. Op note-id gekoppeld, want
     196    // buildOutbox sorteert en mengt tracks erdoorheen. De embed alleen voor de
     197    // BEARER en langs zijn eigen poort: een remote vriend krijgt hem niet
     198    // (diens server resolvet en gate zelf bij ontvangst), en een ward zonder
     199    // open embeds-poort krijgt hem hier net zo min als in de tijdlijn.
     200    const byNote = new Map(posts.map((p) => [AP.noteId(baseUrl(req), p.id), p]));
     201    const bearerEmbeds = bearer ? (() => {
     202      const isWard = (() => { try { return Guardianship.listGuardians(bearer.site.slug).length > 0; } catch { return false; } })();
     203      return Guardianship.externalEmbedsAllowed(bearer.site.external_embeds, isWard)
     204        ? { playback: Guardianship.externalPlaybackAllowed(bearer.site.external_playback, isWard) } : null;
     205    })() : null;
    193206    for (const it of ob.orderedItems) {
    194       if (it && it.object && typeof it.object === 'object') it.object['shaer:author'] = me;
     207      if (it && it.object && typeof it.object === 'object') {
     208        it.object['shaer:author'] = me;
     209        const row = byNote.get(it.object.id);
     210        if (row) {
     211          it.object['shaer:quote'] = AP.timelineQuote(row.quote_json);
     212          if (bearerEmbeds) it.object['shaer:embed'] = AP.timelineEmbed(row.embed_json, { playback: bearerEmbeds.playback });
     213        }
     214      }
    195215    }
    196216  }
     
    813833
    814834// ── Replies collection ── lets remote servers fetch a post's whole thread.
     835// ── De composer-preview (shaer-k3f): een URL wordt alvast een kaart ──
     836//
     837// Bearer-only, net als de thread: dit is de eigen app die tijdens het typen
     838// vraagt wat een link gaat worden. Dezelfde pijplijn als publiceren, dus de
     839// preview kan niet iets beloven dat de post niet waarmaakt. De embed gaat
     840// langs de eigen poort van de lezer -- een ward zonder open embeds-poort
     841// krijgt in de composer geen kaart die zijn feed hem ook niet zou tonen.
     842router.get('/ap/users/:slug/card', async (req, res) => {
     843  const auth = OAuth.verifyBearer(req.headers.authorization);
     844  if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end();
     845  const uit = await AP.previewCard(String(req.query.url || ''));
     846  const isWard = (() => { try { return Guardianship.listGuardians(auth.site.slug).length > 0; } catch { return false; } })();
     847  const embedsAllowed = Guardianship.externalEmbedsAllowed(auth.site.external_embeds, isWard);
     848  const playback = embedsAllowed && Guardianship.externalPlaybackAllowed(auth.site.external_playback, isWard);
     849  AP.sendAP(res, {
     850    '@context': AP.AP_CONTEXT,
     851    'shaer:quote': AP.timelineQuote(uit.quoteJson),
     852    'shaer:embed': embedsAllowed ? AP.timelineEmbed(uit.embedJson, { playback }) : undefined,
     853  }, 'private, no-store');
     854});
     855
    815856// ── De thread onder een post (shaer-tqz): ophalen, niet bewaren ────
    816857//
  • src/services/ActivityPubService.js

    rabcf51a r4838760  
    29552955      post2 = { ...post2, quote_uri: q.uri, quote_actor: q.actor || null };
    29562956    }
     2957  }
     2958  // De kaart op de eigen post (shaer-k3f), langs dezelfde pijplijn als een
     2959  // binnenkomende: een fediverse-quote wordt een quote-snapshot, anders
     2960  // probeert de link een externe kaart. VOOR de vroege return hieronder, want
     2961  // ook een post zonder volgers hoort zijn kaart te krijgen -- de app leest
     2962  // hem uit de outbox, niet uit een bezorging. Best-effort en eenmalig: wat
     2963  // hier niet lukt blijft een kale link, precies wat het was.
     2964  if (!post2.quote_json && !post2.embed_json) {
     2965    try {
     2966      if (post2.quote_uri) {
     2967        const qj = await resolveQuoteByUri(post2.quote_uri);
     2968        if (qj) { db.prepare('UPDATE posts SET quote_json = ? WHERE id = ?').run(qj, post.id); post2 = { ...post2, quote_json: qj }; }
     2969      } else {
     2970        const ej = await resolveExternalEmbed(post2.content || '');
     2971        if (ej) { db.prepare('UPDATE posts SET embed_json = ? WHERE id = ?').run(ej, post.id); post2 = { ...post2, embed_json: ej }; }
     2972      }
     2973    } catch { /* een kaart is nooit een blokkade voor de post zelf */ }
    29572974  }
    29582975  if (post2.quote_actor) {
     
    49965013}
    49975014
     5015/**
     5016 * De composer-preview (shaer-k3f): één URL langs exact dezelfde pijplijn als
     5017 * publiceren, zodat wat de preview toont ook is wat de post krijgt. Twee
     5018 * uitkomsten, hoogstens een gevuld: een AP-object wordt een quote-snapshot,
     5019 * een externe link probeert een kaart. Beide als JSON-string, dezelfde vorm
     5020 * als de kolommen -- de route serveert ze door timelineQuote/timelineEmbed en
     5021 * de gate, net als de tijdlijn.
     5022 */
     5023export async function previewCard(url) {
     5024  if (!/^https?:\/\//i.test(String(url || ''))) return {};
     5025  const html = `<a href="${String(url).replace(/"/g, '&quot;')}">x</a>`;
     5026  const q = await resolveOwnQuote(html);
     5027  if (q && q.uri) {
     5028    const quoteJson = await resolveQuoteByUri(q.uri).catch(() => null);
     5029    if (quoteJson) return { quoteJson };
     5030  } else {
     5031    const embedJson = await resolveExternalEmbed(html).catch(() => null);
     5032    if (embedJson) return { embedJson };
     5033  }
     5034  return {};
     5035}
     5036
    49985037/** The first http(s) link in sanitized note HTML that is not a mention/hashtag. */
    49995038export function firstExternalUrl(html) {
     
    50515090  const url = quoteHrefOf(note);
    50525091  if (!url) return null;
     5092  return resolveQuoteByUri(url);
     5093}
     5094
     5095/** Hetzelfde snapshot, maar vanaf een kale URI: eigen posts en de
     5096 *  composer-preview (shaer-k3f) kennen alleen de link, niet de tag-vorm. */
     5097async function resolveQuoteByUri(url) {
    50535098  const q = await apGetJson(url);
    50545099  if (!q || typeof q !== 'object') return null;
     
    62706315  acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision,
    62716316  gateOutgoingFollow, performApprovedFollow,
    6272   parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
     6317  parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs, previewCard,
    62736318  autoBoostCount, boostedCount, setReaction, getReaction, getReactionsFor, canonicalReactionUri, migrateReactions, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
    62746319  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
Note: See TracChangeset for help on using the changeset viewer.