Changeset 096f099 in Klonkt


Ignore:
Timestamp:
08/13/2026 06:01:50 PM (4 weeks ago)
Author:
Robin <roboburr@â€Ļ>
Branches:
main
Children:
e9ec5e4
Parents:
0824351
Message:

Wie je volgt gaat mee bij een verhuizing, als CSV

De exporter dekte posts, tracks en antwoorden, maar niet je relaties. Daarmee was
een verhuizing halfslachtig: de Move vertelt je VOLGERS waar je heen ging, maar
niets vertelde JOU wie jij volgde. Die lijst stond alleen in de database die je
achterlaat, en die negen mensen moest je uit je hoofd opnieuw opzoeken.

Kolomvorm van Mastodon, zodat de lijst beide kanten op werkt: hiervandaan naar een
Mastodon, en die van daar naar hier. Een uitwisselformaat dat alleen met zichzelf
praat is er geen.

Account address,Show boosts,Notify on new posts,Languages,Featured

De vijfde kolom is van ons; Mastodon leest de eerste vier en negeert de rest, dus
het blijft daar importeerbaar. Andersom werkt een bestand uit Mastodon hier ook,
en een kale lijst adressen zonder kopregel eveneens.

Over de naam: featured betekent in Klonkt al de collectie VASTGEZETTE POSTS
(toot:featured), en het commentaar bij auto_boost noemt dat al "feature a
followed account" (hun posts in jouw Cirkel). Drie dingen die featured heten is er
twee te veel, dus de kolom heet in de database highlighted: wie je op je eigen
profiel uitlicht. In de CSV blijft de kop Featured, want dat is het woord dat
mensen en Mastodon kennen. FEP-d471 (Endorsements) modelleert dit als een eigen
object voor webs of trust; dat is een zwaarder ding dan hier bedoeld, dus dit
blijft een lokale vlag.

importFollowing staat BEWUST buiten importArchive. Die draait in een transactie en
raakt alleen de database; opnieuw volgen stuurt activiteiten de deur uit en wacht
op het netwerk. Een trage peer zou de transactie openhouden en een rollback neemt
verzonden Follows niet terug. Het is ook een aparte stap omdat een archief inlezen
stil is en negen mensen aanschrijven niet; dat mag geen bijwerking zijn.

Changed files:
src/config/database.js

  • kolom ap_following.highlighted, met de drie betekenissen van "featured" uit elkaar gehouden

src/services/ArchiveExportService.js

  • followingCsv() en parseFollowingCsv(), met een CSV-lezer die geciteerde velden aankan
  • buildArchive schrijft following.csv en telt hem mee

src/services/ArchiveImportService.js

  • importFollowing(), met injecteerbare followFn zodat de test geen netwerk raakt

New file:
test/following-csv.test.js

  • 13 tests: de kopvorm, openstaande verzoeken die NIET meegaan, terugval op de actor-URI, de ronde export-naar-import, een Mastodon-bestand zonder onze kolom, een kale lijst, een geciteerd veld met een komma, jezelf overslaan, en dat een mislukte follow de rest niet stopt
  • plus de koppeling: dat buildArchive de CSV er echt in stopt en in het manifest zet. Die ontbrak eerst, en dat is precies het gat waar dit op zou stranden: losse functies groen, archief zonder volglijst

remarks: gebouwd op branch following-csv, afgetakt van github/main (0824351). Stond
eerst per ongeluk op ward-pwa; die branch is dev-only en mag niet naar main, en is
onaangeroerd gebleven. Suite 921 groen in UTC en Europe/Amsterdam. Nog te doen: een
UI om highlighted te zetten, en de knop die importFollowing aanroept met
followActor als followFn.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@â€Ļ>

Files:
1 added
3 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r0824351 r096f099  
    739739  // "Feature" a followed account: its posts show in the local Cirkel.
    740740  ensureColumn('ap_following', 'auto_boost', 'INTEGER DEFAULT 0');
     741  // Uitgelicht: dit account tonen op je eigen profiel, als aanbeveling aan je
     742  // bezoekers. NIET hetzelfde als de twee buren: `featured` op de actor is de
     743  // collectie VASTGEZETTE POSTS (toot:featured), en `auto_boost` hierboven laat
     744  // hun posts in JOUW Cirkel meelopen. Dit gaat over wie je aan ANDEREN laat
     745  // zien. Vandaar een eigen naam; drie dingen die "featured" heten is er twee
     746  // te veel. Lokale vlag: FEP-d471 (Endorsements) modelleert dit als een eigen
     747  // object voor webs of trust, en dat is een zwaarder ding dan hier bedoeld.
     748  ensureColumn('ap_following', 'highlighted', 'INTEGER DEFAULT 0');
    741749  // A timeline post you boosted (🔁) — also shown in the Cirkel (mixed by date).
    742750  ensureColumn('ap_timeline', 'boosted', 'INTEGER DEFAULT 0');
  • src/services/ArchiveExportService.js

    r0824351 r096f099  
    238238}
    239239
     240// ── Wie je volgt, als CSV ─────────────────────────────────────────
     241//
     242// Kolomvorm van Mastodon, zodat deze lijst ook DAAR te importeren is en die van
     243// daar hier. Dat is het hele punt van een verhuisformaat: het moet ook werken
     244// als je naar iets anders vertrekt dan waar je vandaan kwam.
     245//
     246//   Account address,Show boosts,Notify on new posts,Languages,Featured
     247//
     248// De vijfde kolom is van ons. Mastodon leest de eerste vier en negeert de rest,
     249// dus dit blijft daar gewoon importeerbaar. `Featured` is de kolom voor
     250// `highlighted`: accounts die je op je profiel uitlicht. `Show boosts` is onze
     251// `auto_boost`. `Notify` en `Languages` kent Klonkt niet en blijven leeg; ze
     252// staan er alleen omdat Mastodon de posities telt.
     253const CSV_KOP = 'Account address,Show boosts,Notify on new posts,Languages,Featured';
     254
     255/** Een veld dat een komma, aanhalingsteken of nieuwe regel bevat moet geciteerd. */
     256function csvVeld(v) {
     257  const s = String(v == null ? '' : v);
     258  return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
     259}
     260
     261/**
     262 * De volglijst van een site als CSV, of null als er niets te melden valt.
     263 *
     264 * Alleen `accepted`: een openstaand verzoek is geen relatie, en het opnieuw
     265 * versturen ervan op de nieuwe plek zou een tweede verzoek zijn bij iemand die
     266 * de eerste misschien bewust liet liggen.
     267 *
     268 * Het adres is de handle zonder de leidende @, want zo schrijft Mastodon hem.
     269 * Ontbreekt de handle, dan valt hij terug op de actor-URI: die is altijd te
     270 * herleiden, ook als de webfinger-naam ooit verloren ging.
     271 */
     272export function followingCsv(slug) {
     273  let rijen = [];
     274  try {
     275    rijen = db.prepare(`SELECT actor_uri, handle, auto_boost, highlighted FROM ap_following
     276                         WHERE slug = ? AND status = 'accepted'
     277                         ORDER BY handle IS NULL, handle, actor_uri`).all(slug);
     278  } catch { return null; }        // oude database zonder de kolom
     279  if (!rijen.length) return null;
     280  const regels = rijen.map((r) => [
     281    csvVeld((r.handle || r.actor_uri || '').replace(/^@/, '')),
     282    r.auto_boost ? 'true' : 'false',
     283    'false',
     284    '',
     285    r.highlighted ? 'true' : 'false',
     286  ].join(','));
     287  return `${CSV_KOP}\n${regels.join('\n')}\n`;
     288}
     289
     290/**
     291 * Lees zo'n CSV terug. Puur, zodat de vorm te toetsen is zonder database.
     292 *
     293 * Vergeeflijk met opzet: een bestand uit Mastodon heeft vier kolommen en geen
     294 * `Featured`, een handgemaakt bestand heeft misschien alleen adressen. Beide
     295 * moeten werken, want anders is het geen uitwisselformaat maar een eigen
     296 * bestandje dat toevallig op een CSV lijkt.
     297 */
     298export function parseFollowingCsv(text) {
     299  const uit = [];
     300  const regels = String(text || '').split(/\r?\n/).filter((r) => r.trim());
     301  if (!regels.length) return uit;
     302  // Een kopregel herkennen we aan het eerste veld; anders is regel 1 al data.
     303  const start = /^\s*"?account address"?\s*,/i.test(regels[0]) || /^\s*"?account address"?\s*$/i.test(regels[0]) ? 1 : 0;
     304  for (const regel of regels.slice(start)) {
     305    const velden = splitsCsvRegel(regel);
     306    const adres = (velden[0] || '').trim().replace(/^@/, '');
     307    if (!adres) continue;
     308    uit.push({
     309      address: adres,
     310      autoBoost: /^(true|1|yes)$/i.test((velden[1] || '').trim()),
     311      highlighted: /^(true|1|yes)$/i.test((velden[4] || '').trim()),
     312    });
     313  }
     314  return uit;
     315}
     316
     317/** EÊn CSV-regel, met respect voor geciteerde velden en verdubbelde aanhalingstekens. */
     318function splitsCsvRegel(regel) {
     319  const velden = [];
     320  let veld = '';
     321  let inCitaat = false;
     322  for (let i = 0; i < regel.length; i++) {
     323    const c = regel[i];
     324    if (inCitaat) {
     325      if (c === '"') {
     326        if (regel[i + 1] === '"') { veld += '"'; i++; } else inCitaat = false;
     327      } else veld += c;
     328    } else if (c === '"') inCitaat = true;
     329    else if (c === ',') { velden.push(veld); veld = ''; }
     330    else veld += c;
     331  }
     332  velden.push(veld);
     333  return velden;
     334}
     335
    240336/**
    241337 * Bouw het archief als een lijst bestanden: pad -> inhoud (Buffer).
     
    336432  }
    337433
     434  // Wie je volgt. Dit ontbrak, en daarmee was een "verhuizing" halfslachtig: de
     435  // Move vertelt je VOLGERS waar je heen ging, maar niets vertelde JOU wie jij
     436  // volgde. Die lijst stond alleen in de oude database, en die laat je achter.
     437  const volgCsv = followingCsv(slug);
     438  if (volgCsv) { bestanden.set('following.csv', Buffer.from(volgCsv, 'utf8')); tellingen.following = volgCsv.trim().split('\n').length - 1; }
     439
    338440  const files = {};
    339441  for (const pad of [...bestanden.keys()].sort()) files[pad] = sha256(bestanden.get(pad));
  • src/services/ArchiveImportService.js

    r0824351 r096f099  
    2525import db from '../config/database.js';
    2626import { MEDIA_ROOT } from '../config/paths.js';
    27 import { FORMAT_VERSION } from './ArchiveExportService.js';
     27import { FORMAT_VERSION, parseFollowingCsv } from './ArchiveExportService.js';
    2828
    2929const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
     
    116116function bestemming(a) {
    117117  return padVanOrigineel(a && a['shaer:originalUrl']) || `/media/archief/${path.basename(String((a && a.url) || ''))}`;
     118}
     119
     120/**
     121 * Volg opnieuw wie je volgde, uit de `following.csv` van een archief.
     122 *
     123 * BEWUST BUITEN importArchive. Die draait in ÊÊn transactie en raakt alleen de
     124 * database; opnieuw volgen stuurt Follow-activiteiten de deur uit en wacht op
     125 * het netwerk. Dat hoort niet in een transactie: een trage peer houdt hem open,
     126 * en een rollback neemt verzonden activiteiten niet terug.
     127 *
     128 * Ook een aparte, expliciete stap omdat een archief inlezen stil is maar negen
     129 * mensen aanschrijven niet. Dat mag geen bijwerking zijn van een import.
     130 *
     131 * `followFn` is injecteerbaar, zodat de test geen netwerk raakt en dit bestand
     132 * ActivityPubService niet hoeft te importeren.
     133 */
     134export async function importFollowing(site, csvText, { followFn = null } = {}) {
     135  const rijen = parseFollowingCsv(csvText);
     136  const rapport = { totaal: rijen.length, gevolgd: 0, overgeslagen: 0, mislukt: [] };
     137  if (!followFn) return { ...rapport, error: 'no_follow_fn' };
     138  for (const r of rijen) {
     139    // Jezelf volgen is geen relatie maar een lus. Kan echt gebeuren bij een
     140    // archief van een instance die je onder een nieuwe naam opnieuw opzet.
     141    if (site && site.slug && r.address.startsWith(`${site.slug}@`)) { rapport.overgeslagen += 1; continue; }
     142    try {
     143      const ok = await followFn(site, r.address, r.autoBoost);
     144      if (ok === false) { rapport.mislukt.push({ adres: r.address, reden: 'geweigerd' }); continue; }
     145      // De uitgelicht-vlag hangt aan de RELATIE, dus die zetten we pas als de
     146      // rij bestaat. Mislukt de Follow, dan valt er niets te markeren, en dat
     147      // is juist: iemand uitlichten die je niet volgt klopt niet.
     148      if (r.highlighted) {
     149        try {
     150          db.prepare('UPDATE ap_following SET highlighted = 1 WHERE slug = ? AND (handle = ? OR handle = ?)')
     151            .run(site.slug, r.address, `@${r.address}`);
     152        } catch { /* oude database zonder de kolom */ }
     153      }
     154      rapport.gevolgd += 1;
     155    } catch (e) {
     156      rapport.mislukt.push({ adres: r.address, reden: (e && e.message) || 'onbekend' });
     157    }
     158  }
     159  return rapport;
    118160}
    119161
Note: See TracChangeset for help on using the changeset viewer.