Changeset 279ca0b in Klonkt


Ignore:
Timestamp:
08/06/2026 12:42:50 PM (5 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
32a4ffb
Parents:
bedd7a6
git-author:
Robin <roboburr@…> (08/06/2026 12:42:40 PM)
git-committer:
roboburr <roboburr@…> (08/06/2026 12:42:50 PM)
Message:

De derde reactiebron ook naar ap_my_reactions (shaer-ipb)

ap_interactions.acted_like/acted_boost -- wat JIJ deed met een reactie onder je
eigen post -- was de derde bron van 'heb ik hierop gereageerd'. shaer-9e9 trok de
andere twee samen en liet deze staan omdat hij op het interactie-rij-id
gesleuteld was en de tussentabel op de object-URI.

De betekeniskeuze die de bead eerst wilde beantwoorden: dezelfde note, geliket
als comment en als post, wordt EEN feit. Er gaat immers ook maar een Like de
fediverse in; twee knoppen die los van elkaar aan konden staan voor hetzelfde
object was eerder een bug dan een feature. Mechanisch blijkt dat al veilig:
markLiked en markBoosted zijn kale UPDATEs op ap_timeline die nul rijen raken als
de URI daar niet staat, en upsertBoostedNote draait alleen met opts.note mee.

Volgorde zoals bij shaer-9e9, en om dezelfde reden -- een leespad dat naar de
tussentabel wijst terwijl de rijen nog in de kolommen staan laat reacties STIL
verdwijnen:

schrijven fedi-react roept setReaction aan; acted_* loopt nog als afgeleide

mee, hetzelfde vangnet dat ap_timeline.liked na shaer-9e9 is. Ook de
toggle leest nu uit de tussentabel: las de knop de ene bron en de
richting de andere, dan stuurt een divergentie een Undo voor iets dat
nooit verstuurd is.

migreren REACTIONS_MIGRATION_VERSION 1 -> 2, derde stap erbij. Een bump laat

alle stappen opnieuw lopen; ze zijn alle drie idempotent. Rijen
zonder object_uri vallen af, en dat verliest per constructie niets:
fedi-react eist een object_uri voordat acted_* gezet kan worden.

lezen getInteractions doet een batch-lookup langs dezelfde sleutel als de

schrijfkant, dus geen N+1 in een drukke thread.

Het backfill-script zou anders vals alarm slaan: een reactie op een COMMENT hoort
geen tijdlijnrij te hebben, dus die telt niet meer als wees. Er is een controle
bij voor acted_* zonder tegenhanger.

Gemeten op een kopie van dev: scheefActed 1 -> 0, tussentabel 19 -> 20, wees
onveranderd. Suite 472/472. Gecontroleerd dat de tests bijten: laat het leespad
weer uit de kolom lezen en vier van de zes vallen om.

De karakteriseringstest zette de versievlag hard op 1 en zou bij elke bump
omvallen zonder dat er iets stuk is; die toetst nu het gedrag (ouder = opnieuw,
gelijk of hoger = overslaan) in plaats van het getal.

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

Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • scripts/backfill-reactions.mjs

    rbedd7a6 r279ca0b  
    2525//                 niet-gereageerd -- en klikt iemand opnieuw, met een tweede
    2626//                 Like de fediverse in als gevolg.
     27//   UIT REACTIES  De derde bron (shaer-ipb): ap_interactions.acted_like/_boost,
     28//                 wat jij deed met een reactie ONDER je eigen post. Zelfde
     29//                 gevolg als hierboven, alleen op een ander oppervlak.
    2730//
    2831// Wat het NIET kan: bij AANVULLEN de oorspronkelijke reactiedatum herstellen.
     
    4548       AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r
    4649                        WHERE r.site_slug = t.slug AND r.target_uri = t.id)`).get().n,
     50  // Een reactie op een COMMENT hoort geen tijdlijnrij te hebben, dus die telt
     51  // hier niet als wees -- anders slaat de controle hieronder alarm op precies
     52  // wat stap 3 net goed heeft gezet.
    4753  wees: db.prepare(`
    4854    SELECT COUNT(*) AS n FROM ap_my_reactions r
    4955     WHERE NOT EXISTS (SELECT 1 FROM ap_timeline t
    50                         WHERE t.slug = r.site_slug AND t.id = r.target_uri)`).get().n,
     56                        WHERE t.slug = r.site_slug AND t.id = r.target_uri)
     57       AND NOT EXISTS (SELECT 1 FROM ap_interactions i WHERE i.object_uri = r.target_uri)`).get().n,
     58  scheefActed: db.prepare(`
     59    SELECT COUNT(*) AS n FROM ap_interactions i
     60     JOIN posts p ON p.id = i.post_id
     61     JOIN sites s ON s.id = p.site_id
     62     WHERE (i.acted_like = 1 OR i.acted_boost = 1) AND IFNULL(i.object_uri, '') <> ''
     63       AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r
     64                        WHERE r.site_slug = s.slug AND r.target_uri = i.object_uri)`).get().n,
    5165});
    5266
     
    6074}
    6175if (dryRun) {
    62   console.log(`\n--dry-run: zou ${uit.hersleuteld} rij(en) hersleutelen en ${uit.aangevuld} aanvullen. Niets geschreven.`);
     76  console.log(`\n--dry-run: zou ${uit.hersleuteld} rij(en) hersleutelen, ${uit.aangevuld} aanvullen`
     77    + ` en ${uit.reacties} uit reacties overnemen. Niets geschreven.`);
    6378  process.exit(0);
    6479}
    6580
    6681const na = meet();
    67 console.log('hersleuteld:', uit.hersleuteld, ' aangevuld:', uit.aangevuld);
     82console.log('hersleuteld:', uit.hersleuteld, ' aangevuld:', uit.aangevuld, ' uit reacties:', uit.reacties);
    6883console.log('achteraf   :', JSON.stringify(na));
    6984
     
    7590if (na.scheef !== 0) {
    7691  console.error(`\nFOUT: nog ${na.scheef} rij(en) met een vlag zonder tegenhanger.`);
     92  process.exit(1);
     93}
     94if (na.scheefActed !== 0) {
     95  console.error(`\nFOUT: nog ${na.scheefActed} reactie(s) onder je eigen posts met acted_* zonder tegenhanger.`);
    7796  process.exit(1);
    7897}
  • src/routes/posts.js

    rbedd7a6 r279ca0b  
    15571557  const kind = req.body.kind === 'boost' ? 'boost' : 'like';
    15581558  if (parent && parent.post_id === post.id && parent.object_uri) {
    1559     if (kind === 'boost') {
    1560       // Toggle: boost an unboosted comment, or retract it (Undo Announce) if already boosted.
    1561       const on = !parent.acted_boost;
    1562       ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', parent.object_uri, parent.actor_uri)
    1563         .catch((e) => console.warn('[AP] reaction failed:', e.message));
    1564       ActivityPubService.setInteractionBoosted(parent.id, on);
    1565     } else {
    1566       // Toggle: like an unliked comment, or un-favourite (Undo Like) if already liked.
    1567       const on = !parent.acted_like;
    1568       ActivityPubService.sendInteraction(site, on ? 'like' : 'unlike', parent.object_uri, parent.actor_uri)
    1569         .catch((e) => console.warn('[AP] reaction failed:', e.message));
    1570       ActivityPubService.setInteractionLiked(parent.id, on);
    1571     }
     1559    // Toggle: react, or retract it (Undo Announce / Undo Like) if already on.
     1560    // De stand komt uit dezelfde bron als de knop die je zag; leest de toggle uit
     1561    // de kolom en de knop uit de tussentabel, dan draait een divergentie de
     1562    // richting om en stuur je een Undo voor iets dat nooit is verstuurd.
     1563    const ik = ActivityPubService.getReaction(site.slug, parent.object_uri);
     1564    const on = kind === 'boost' ? !ik.boosted : !ik.liked;
     1565    ActivityPubService.sendInteraction(site, on ? kind : `un${kind}`, parent.object_uri, parent.actor_uri)
     1566      .catch((e) => console.warn('[AP] reaction failed:', e.message));
     1567    // De tussentabel is de waarheid (shaer-ipb), gesleuteld op object_uri -- net
     1568    // als de Like die hierboven de fediverse in gaat. acted_* blijft voorlopig
     1569    // als afgeleide meelopen, hetzelfde vangnet dat ap_timeline.liked na
     1570    // shaer-9e9 is: pas weghalen als deze migratie een release heeft ingelopen.
     1571    ActivityPubService.setReaction(site.slug, parent.object_uri, kind, on);
     1572    if (kind === 'boost') ActivityPubService.setInteractionBoosted(parent.id, on);
     1573    else ActivityPubService.setInteractionLiked(parent.id, on);
    15721574  }
    15731575  res.redirect(`${res.locals.siteUrlBase || ''}/${post.slug}#fediverse`);
  • src/services/ActivityPubService.js

    rbedd7a6 r279ca0b  
    11861186  const siteIcon = (site && site.profile_photo) || null;
    11871187
     1188  // Wat JIJ met deze reacties deed komt uit de tussentabel, niet meer uit
     1189  // acted_* (shaer-ipb). Eén batch-lookup, want een drukke thread zou anders een
     1190  // N+1 worden. De sleutel loopt door canonicalReactionUri, precies zoals aan de
     1191  // schrijfkant -- staat dezelfde note toevallig ook in je tijdlijn, dan is het
     1192  // één feit en niet twee knoppen die los van elkaar aan kunnen staan.
     1193  const mijnSleutel = new Map();
     1194  for (const r of rows) {
     1195    if (r.kind === 'reply' && r.object_uri) mijnSleutel.set(r.object_uri, canonicalReactionUri(site && site.slug, r.object_uri));
     1196  }
     1197  const mijn = getReactionsFor(site && site.slug, [...mijnSleutel.values()]);
     1198  const mijnReactie = (uri) => mijn.get(mijnSleutel.get(uri)) || { liked: false, boosted: false };
     1199
    11881200  const nodes = [];
    11891201  for (const r of rows) {
    11901202    if (r.kind !== 'reply') continue;
     1203    const ik = mijnReactie(r.object_uri);
    11911204    nodes.push({
    11921205      noteId: r.object_uri, parent: r.parent_uri || null, mine: false, id: r.id,
     
    11951208      actor_icon: r.actor_icon, content: stripLeadingMentions(r.content), created_at: r.published || r.created_at,
    11961209      emoji_json: r.emoji_json, actor_emoji_json: r.actor_emoji_json,   // FEP-9098 (thread render)
    1197       acted_boost: !!r.acted_boost, acted_like: !!r.acted_like,
     1210      acted_boost: ik.boosted, acted_like: ik.liked,
    11981211      children: [],
    11991212    });
     
    36463659// stap naar nieuwe code, en een handmatig script per instance wordt vergeten --
    36473660// terwijl het falen stil is (een reactie die niemand meer ziet geeft geen fout).
    3648 const REACTIONS_MIGRATION_VERSION = 1;
     3661// v2 haalt de derde bron erbij: ap_interactions.acted_* (shaer-ipb). Een bump
     3662// laat alle stappen opnieuw lopen, en dat mag -- ze zijn alle drie idempotent.
     3663const REACTIONS_MIGRATION_VERSION = 2;
    36493664
    36503665/**
     
    36663681 */
    36673682export function migrateReactions(opts = {}) {
    3668   const uit = { hersleuteld: 0, aangevuld: 0, overgeslagen: false };
     3683  const uit = { hersleuteld: 0, aangevuld: 0, reacties: 0, overgeslagen: false };
    36693684  try {
    36703685    if (!opts.force) {
     
    36843699       AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r
    36853700                        WHERE r.site_slug = t.slug AND r.target_uri = t.id AND r.kind = '${kind}')`;
     3701  // 3. De derde bron: wat JIJ deed met een reactie onder je eigen post. De slug
     3702  //    hangt hier niet aan de rij maar aan de post; vandaar de twee joins. Een
     3703  //    rij zonder object_uri kan nooit een reactie dragen (fedi-react eist hem),
     3704  //    dus die uitsluiting verliest per constructie niets.
     3705  const acted = (kind, kolom) => `
     3706    FROM ap_interactions i
     3707     JOIN posts p ON p.id = i.post_id
     3708     JOIN sites s ON s.id = p.site_id
     3709     WHERE i.${kolom} = 1 AND IFNULL(i.object_uri, '') <> ''
     3710       AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r
     3711                        WHERE r.site_slug = s.slug AND r.target_uri = i.object_uri AND r.kind = '${kind}')`;
    36863712
    36873713  if (opts.dryRun) {
    3688     try {
    3689       uit.hersleuteld = db.prepare(`SELECT COUNT(*) AS n ${wees}`).get().n;
    3690       uit.aangevuld = db.prepare(`SELECT COUNT(*) AS n ${scheef('like', 'liked')}`).get().n
    3691                     + db.prepare(`SELECT COUNT(*) AS n ${scheef('boost', 'boosted')}`).get().n;
    3692     } catch { /* laat de nullen staan */ }
     3714    const tel = (sql) => { try { return db.prepare(`SELECT COUNT(*) AS n ${sql}`).get().n; } catch { return 0; } };
     3715    uit.hersleuteld = tel(wees);
     3716    uit.aangevuld = tel(scheef('like', 'liked')) + tel(scheef('boost', 'boosted'));
     3717    uit.reacties = tel(acted('like', 'acted_like')) + tel(acted('boost', 'acted_boost'));
    36933718    return uit;
    36943719  }
     
    37103735          SELECT t.slug, t.id, '${kind}' ${scheef(kind, kolom)}`).run().changes;
    37113736      }
     3737
     3738      // 3. En vanuit acted_* op de reacties onder je eigen posts.
     3739      for (const [kind, kolom] of [['like', 'acted_like'], ['boost', 'acted_boost']]) {
     3740        uit.reacties += db.prepare(`
     3741          INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind)
     3742          SELECT s.slug, i.object_uri, '${kind}' ${acted(kind, kolom)}`).run().changes;
     3743      }
    37123744    })();
    3713     if (uit.hersleuteld || uit.aangevuld) {
    3714       console.log(`[AP] reaction migration v${REACTIONS_MIGRATION_VERSION}: ${uit.hersleuteld} re-keyed, ${uit.aangevuld} backfilled`);
     3745    if (uit.hersleuteld || uit.aangevuld || uit.reacties) {
     3746      console.log(`[AP] reaction migration v${REACTIONS_MIGRATION_VERSION}: ${uit.hersleuteld} re-keyed, ${uit.aangevuld} backfilled, ${uit.reacties} from comments`);
    37153747    }
    37163748    if (!opts.force) {
  • test/reactions-characterization.test.js

    rbedd7a6 r279ca0b  
    362362  assert.equal(tweede.aangevuld, 0, 'niets meer aan te vullen');
    363363  assert.ok(eerste.hersleuteld >= 0);
    364   // Zonder force draait hij niet nog eens zodra de vlag staat.
    365   db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?,?)').run('reactions_migration_version', '1');
    366   assert.equal(AP.migrateReactions().overgeslagen, true);
     364  // De vlag is VERSIEgebonden, en niet aan één vast getal: een oudere vlag laat
     365  // de migratie opnieuw lopen -- zo doet een bump als v2 zijn werk -- en een
     366  // gelijke of hogere slaat hem over. Een hardgecodeerd versienummer hier zou bij
     367  // elke bump omvallen zonder dat er iets stuk is.
     368  const vlag = (v) => db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?,?)').run('reactions_migration_version', v);
     369  vlag('1');
     370  assert.equal(AP.migrateReactions().overgeslagen, false, 'een oudere vlag laat hem opnieuw lopen');
     371  vlag('999');
     372  assert.equal(AP.migrateReactions().overgeslagen, true, 'een gelijke of hogere vlag slaat over');
    367373});
    368374
Note: See TracChangeset for help on using the changeset viewer.