Changeset 4f19322 in Klonkt


Ignore:
Timestamp:
08/13/2026 05:44:20 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
fbda20e
Parents:
0824351
Message:

Een samenvatting is geen waarschuwing

Barts melding, 13-8: posts van europeanpirates.eu (WordPress + ActivityPub)
kwamen in Shaer binnen achter een content warning terwijl sensitive niet
gezet is -- en de tekst van die waarschuwing was de eerste alinea van de post
zelf, afgekapt. Precies de excerpt die WordPress voor Mastodon meestuurt.

In AS2 IS summary een samenvatting: "a natural language summarization of the
object". Dat Mastodon dat veld hergebruikt als waarschuwing is Mastodons
conventie, en die zet er sensitive bij. Zonder sensitive is het dus gewoon een
samenvatting, en die als waarschuwing tonen verbergt een post achter zijn eigen
tekst -- niemand ziet wat er staat en de waarschuwing waarschuwt nergens voor.

contentWarning() zet die regel op EEN plek; alle zes de plekken die summary als
cw wegschreven gebruiken hem nu.

TWEEDE FOUT, zichtbaar geworden door de eerste te repareren: de titel ging
verloren. timelineFields hield alleen content over, dus name verdween en het
artikel zou als kale body zijn binnengekomen. Een niet-Note-object krijgt zijn
titel nu als kop -- dezelfde greep die resolveRemoteNote al deed, dus de
tijdlijn en het antwoordpad zeggen eindelijk hetzelfde.

En selfHealTimeline bouwde zijn inhoud zelf op in plaats van via timelineFields.
Een zelfherstel dat een andere vorm oplevert dan de inname repareert naar een
derde toestand; nu dezelfde bouwer, en SELFHEAL_VERSION 22 laat het lopen. Op
dev meteen 34 van de 52 rijen hersteld.

Niet WordPress-specifiek: van de drie rijen met een valse waarschuwing kwamen er
twee van bartoverkamp.nl. 913/913 groen.

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

Files:
1 added
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r0824351 r4f19322  
    280280 * Funkwhale hoeft te weten.
    281281 */
     282/**
     283 * De waarschuwingstekst van een object, of niets.
     284 *
     285 * `summary` IS in AS2 een SAMENVATTING -- "a natural language summarization of
     286 * the object". Dat Mastodon dat veld hergebruikt als waarschuwing is Mastodons
     287 * conventie, en die zet er `sensitive` bij. Zonder `sensitive` is een summary
     288 * dus gewoon een samenvatting.
     289 *
     290 * WordPress + ActivityPub stuurt daar de EXCERPT van een artikel in, netjes
     291 * afgekapt voor Mastodon. Wij lazen dat als waarschuwing en verborgen de post
     292 * daarmee achter zijn eigen eerste alinea (Barts melding, 13-8:
     293 * europeanpirates.eu). Niemand krijgt dan te zien wat er staat, en de
     294 * waarschuwing waarschuwt nergens voor.
     295 */
     296export function contentWarning(o) {
     297  if (!o || !o.sensitive) return null;
     298  const s = typeof o.summary === 'string' ? o.summary.trim() : '';
     299  return s || null;
     300}
     301
    282302export function timelineFields(o) {
    283303  // De hoes: een `image` op het object. Bij een Note alleen als terugval (daar
     
    308328  }
    309329
    310   // Note / Article / Question -- ongewijzigd gedrag.
     330  // Een ARTIKEL heeft een titel, en die is het eerste wat je wilt zien. Zonder
     331  // dit kwam een WordPress-post binnen als kale body: de titel zit in `name` en
     332  // die gooiden we weg, terwijl de excerpt in `summary` ten onrechte als
     333  // waarschuwing dienstdeed. Nu allebei goed -- en dit is dezelfde greep die
     334  // resolveRemoteNote al doet voor niet-Note-objecten, dus de tijdlijn en het
     335  // antwoordpad zeggen eindelijk hetzelfde.
     336  if (o.type && o.type !== 'Note' && typeof o.name === 'string' && o.name.trim()) {
     337    const kop = `<p><strong>${HtmlSanitizerService.escape ? HtmlSanitizerService.escape(o.name) : o.name}</strong></p>`;
     338    const atts = mediaFromNote(o);
     339    const pagina = pickLink(o.url, (mt) => !mt || /html/i.test(mt));
     340    return {
     341      html: HtmlSanitizerService.sanitize(kop + (o.content || '')),
     342      atts,
     343      url: pagina ? pagina.href : null,
     344    };
     345  }
     346
     347  // Note / Question -- ongewijzigd gedrag.
    311348  const atts = (Array.isArray(o.attachment) ? o.attachment : [])
    312349    .map((a) => ({ url: safeUrl(a && a.url), type: (a && a.mediaType) || '' }))
     
    24632500        // the timeline).
    24642501        for (const s of subs) {
    2465           tlStmts().ins.run(o.id, s.slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, _url, o.published || null, media, o.sensitive ? 1 : 0, o.summary || null);
     2502          tlStmts().ins.run(o.id, s.slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, _url, o.published || null, media, o.sensitive ? 1 : 0, contentWarning(o));
    24662503          // FEP-633c §2.2: register the ward hint on the stored object (no action yet).
    24672504          if (Guardianship.objectHasGuardians(o)) { try { db.prepare('UPDATE ap_timeline SET has_guardians = 1 WHERE id = ? AND slug = ?').run(o.id, s.slug); } catch { /* ignore */ } }
     
    26152652        // post would keep linking to the old, now-dead URL.
    26162653        const r = db.prepare('UPDATE ap_timeline SET content = ?, media_json = ?, nsfw = ?, cw = ?, url = COALESCE(?, url) WHERE id = ? AND author_uri = ?')
    2617           .run(html, media, o.sensitive ? 1 : 0, o.summary || null, o.url || null, o.id, claimedActor);
     2654          .run(html, media, o.sensitive ? 1 : 0, contentWarning(o), o.url || null, o.id, claimedActor);
    26182655        if (r.changes) console.log('[AP] timeline update', claimedActor, '→', o.id);
    26192656        // A poll's Update carries the fresh vote counts / closed state. Refresh per-row so each
     
    26792716            // have the note (e.g. we also follow the author), keep it and DON'T relabel it.
    26802717            let inserted = false;
    2681             try { const r = tlStmts().ins.run(bn.id, s.slug, origUri || '', oai.name, oai.handle, oai.icon, oai.url, html, bn.url || null, new Date().toISOString(), media, bn.sensitive ? 1 : 0, bn.summary || null); inserted = r.changes > 0; } catch { /* ignore */ }
     2718            try { const r = tlStmts().ins.run(bn.id, s.slug, origUri || '', oai.name, oai.handle, oai.icon, oai.url, html, bn.url || null, new Date().toISOString(), media, bn.sensitive ? 1 : 0, contentWarning(bn)); inserted = r.changes > 0; } catch { /* ignore */ }
    26822719            if (inserted) { try { db.prepare('UPDATE ap_timeline SET reblog_name = ?, reblog_handle = ?, reblog_icon = ?, reblog_emoji_json = ? WHERE slug = ? AND id = ?').run(booster.name, booster.handle, booster.icon, (booster.emojis && Object.keys(booster.emojis).length) ? JSON.stringify(booster.emojis) : null, s.slug, bn.id); } catch { /* ignore */ } }
    26832720            storeAuthorEmoji(bn.id, s.slug, oai);   // custom-emoji display name for the byline
     
    38423879    published: typeof o.published === 'string' ? o.published : undefined,
    38433880    sensitive: !!o.sensitive,
    3844     summary: typeof o.summary === 'string' ? o.summary.slice(0, 500) : undefined,
     3881    summary: (contentWarning(o) || '').slice(0, 500) || undefined,
    38453882    attachment: (() => {
    38463883      const arr = Array.isArray(o.attachment) ? o.attachment : (o.attachment ? [o.attachment] : []);
     
    39583995    content: HtmlSanitizerService.sanitize(rawHtml),       // full, sanitized
    39593996    sensitive: !!note.sensitive,                            // remote CW → blur in the Cirkel
    3960     cw: note.summary || '',
     3997    cw: contentWarning(note) || '',
    39613998    images,
    39623999    // Full typed media (incl. video/mp4) for the timeline cache. `images` above is
     
    49885025// during a flux window, e.g. a fleet-wide update), and drops notes that are gone
    49895026// (404/410). Bump SELFHEAL_VERSION only on a release that warrants a re-sync.
    4990 const SELFHEAL_VERSION = 21; // v21: drop direct notes (🛟 help requests, waves) that were cached as timeline posts
     5027const SELFHEAL_VERSION = 22; // v22: summary is pas een waarschuwing MET sensitive, en een artikel houdt zijn titel
    49915028async function fetchNoteAP(url) {
    49925029  try {
     
    53755412      const poll = parsePoll(o); // a Question (poll) → carry its options/counts on backfill too
    53765413      try {
    5377         const r = tlStmts().ins.run(o.id, slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.url || null, o.published || null, mediaFromNote(o), o.sensitive ? 1 : 0, o.summary || null);
     5414        const r = tlStmts().ins.run(o.id, slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.url || null, o.published || null, mediaFromNote(o), o.sensitive ? 1 : 0, contentWarning(o));
    53785415        if (r && r.changes > 0) added++;
    53795416        // FEP-9098: keep custom-emoji tags from backfilled posts too.
     
    55345571        if (note === 404) { db.prepare('DELETE FROM ap_timeline WHERE id = ?').run(r.id); healed++; continue; }
    55355572        if (!note || typeof note !== 'object') { failed++; continue; } // origin unreachable right now
    5536         const html = HtmlSanitizerService.sanitize(note.content || '');
    5537         const media = mediaFromNote(note);
     5573        // Door DEZELFDE bouwer als de innamekant (v22). Hij bouwde de inhoud
     5574        // hier zelf op, en daardoor miste een gerepareerde rij precies wat de
     5575        // inname wel doet -- de titel van een artikel bijvoorbeeld. Een
     5576        // zelfherstel dat een andere vorm oplevert dan de inname repareert naar
     5577        // een derde toestand.
     5578        const velden = timelineFields(note);
     5579        const html = velden.html;
     5580        const media = velden.atts.length ? JSON.stringify(velden.atts) : mediaFromNote(note);
    55385581        const nsfw = note.sensitive ? 1 : 0;   // re-sync NSFW/sensitive + CW onto already-cached posts
    5539         const cw = note.summary || null;
     5582        const cw = contentWarning(note);
    55405583        const url = note.url || null;          // re-sync the human url (catches a remote slug rename)
    55415584        const emoji = extractEmojiTags(note.tag);   // FEP-9098: re-capture custom-emoji tags (v8)
     
    65906633  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote,
    65916634  listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
    6592   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineRowsByIds, getDirectMessages, readMarkers, markRead, unreadPerConversation, messageRowsByUri, replyRowsByUri, conversationHeads, conversationHistory, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
     6635  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineRowsByIds, contentWarning, getDirectMessages, readMarkers, markRead, unreadPerConversation, messageRowsByUri, replyRowsByUri, conversationHeads, conversationHistory, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
    65936636  acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision,
    65946637  gateOutgoingFollow, performApprovedFollow, recordGuardianEvent, listGuardianEvents, GUARDIAN_EVENT_KEEP,
Note: See TracChangeset for help on using the changeset viewer.