Changes in src/services/ArchiveExportService.js [ba76bf5:e9ec5e4] in Klonkt
- File:
-
- 1 edited
-
src/services/ArchiveExportService.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ArchiveExportService.js
rba76bf5 re9ec5e4 38 38 39 39 const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex'); 40 // SQLite's CURRENT_TIMESTAMP schrijft 'YYYY-MM-DD HH:MM:SS' in UTC, zonder marker. 41 // Kale Date.parse leest dat als LOKALE tijd, dus op een server op UTC+2 ging er twee 42 // uur van elke stempel af voordat hij het archief in ging. Die verschuiving wordt bij 43 // het exporteren ingebakken en valt niet weg bij het importeren: exporteer je in 44 // Amsterdam, dan is die post overal permanent twee uur te vroeg, en in zomer- en 45 // wintertijd verschillend. Het raakte de stempels die de database zelf zet (concepten, 46 // ingeplande posts, gearchiveerde antwoorden), niet die uit de editor, dus de schade 47 // was stil en gedeeltelijk. EXPORT-FORMAT.md schreef altijd al "ISO 8601, UTC" voor. 48 // Zelfde regel als isoStamp() in ActivityPubService en stampMs() in guardianship/offers. 49 const SQL_STAMP = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(\.\d+)?$/; 40 /** 41 * Naar ISO 8601 in UTC. 42 * 43 * SQLite schrijft CURRENT_TIMESTAMP als "2026-07-01 12:56:10" -- in UTC, maar 44 * ZONDER zone erbij. Date.parse leest die vorm als LOKALE tijd, en dan schuift 45 * elk tijdstempel in het archief mee met de tijdzone van de machine die de export 46 * draait. Op een server in Amsterdam is dat twee uur, en dat merk je pas als je 47 * ergens anders importeert. 48 * 49 * Gevonden doordat Bart vroeg of dit wel naar UTC normaliseert. De testmachine 50 * draait op UTC, dus geen enkele test kon het zien. 51 */ 50 52 const toISO = (d) => { 51 const s = String(d == null ? '' : d); 52 const t = Date.parse(SQL_STAMP.test(s) ? `${s.replace(' ', 'T')}Z` : s); 53 if (!d) return null; 54 const s = String(d).trim(); 55 const zonderZone = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/.test(s); 56 const t = Date.parse(zonderZone ? `${s.replace(' ', 'T')}Z` : s); 53 57 return isNaN(t) ? null : new Date(t).toISOString(); 54 58 }; … … 234 238 } 235 239 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 // `Featured` is onze kolom en draagt `auto_boost`: het vinkje dat in de UI 249 // "Uitgelicht" heet (tl.autoboost) en hun posts in jouw Cirkel laat meelopen. 250 // 251 // `Show boosts` blijft LEEG. Dat is bij Mastodon "toon de reblogs van deze 252 // persoon in mijn tijdlijn", en dat kent Klonkt niet. De verleiding is groot om 253 // er auto_boost in te schrijven omdat in beide het woord boost zit, maar het is 254 // een ander ding: dat van ons gaat over hun eigen posts in JOUW Cirkel, niet 255 // over andermans posts die zij doorgeven. `Notify` en `Languages` kent Klonkt 256 // evenmin. Die drie staan er omdat Mastodon de POSITIES telt. 257 const CSV_KOP = 'Account address,Show boosts,Notify on new posts,Languages,Featured'; 258 259 /** Een veld dat een komma, aanhalingsteken of nieuwe regel bevat moet geciteerd. */ 260 function csvVeld(v) { 261 const s = String(v == null ? '' : v); 262 return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; 263 } 264 265 /** 266 * De volglijst van een site als CSV, of null als er niets te melden valt. 267 * 268 * Alleen `accepted`: een openstaand verzoek is geen relatie, en het opnieuw 269 * versturen ervan op de nieuwe plek zou een tweede verzoek zijn bij iemand die 270 * de eerste misschien bewust liet liggen. 271 * 272 * Het adres is de handle zonder de leidende @, want zo schrijft Mastodon hem. 273 * Ontbreekt de handle, dan valt hij terug op de actor-URI: die is altijd te 274 * herleiden, ook als de webfinger-naam ooit verloren ging. 275 */ 276 export function followingCsv(slug) { 277 let rijen = []; 278 try { 279 rijen = db.prepare(`SELECT actor_uri, handle, auto_boost FROM ap_following 280 WHERE slug = ? AND status = 'accepted' 281 ORDER BY handle IS NULL, handle, actor_uri`).all(slug); 282 } catch { return null; } // oude database zonder de kolom 283 if (!rijen.length) return null; 284 const regels = rijen.map((r) => [ 285 csvVeld((r.handle || r.actor_uri || '').replace(/^@/, '')), 286 '', // Show boosts: niet van ons 287 '', // Notify on new posts: idem 288 '', // Languages: idem 289 r.auto_boost ? 'true' : 'false', // Featured: het vinkje "Uitgelicht" 290 ].join(',')); 291 return `${CSV_KOP}\n${regels.join('\n')}\n`; 292 } 293 294 /** 295 * Lees zo'n CSV terug. Puur, zodat de vorm te toetsen is zonder database. 296 * 297 * Vergeeflijk met opzet: een bestand uit Mastodon heeft vier kolommen en geen 298 * `Featured`, een handgemaakt bestand heeft misschien alleen adressen. Beide 299 * moeten werken, want anders is het geen uitwisselformaat maar een eigen 300 * bestandje dat toevallig op een CSV lijkt. 301 */ 302 export function parseFollowingCsv(text) { 303 const uit = []; 304 const regels = String(text || '').split(/\r?\n/).filter((r) => r.trim()); 305 if (!regels.length) return uit; 306 // Een kopregel herkennen we aan het eerste veld; anders is regel 1 al data. 307 const start = /^\s*"?account address"?\s*(,|$)/i.test(regels[0]) ? 1 : 0; 308 for (const regel of regels.slice(start)) { 309 const velden = splitsCsvRegel(regel); 310 const adres = (velden[0] || '').trim().replace(/^@/, ''); 311 if (!adres) continue; 312 uit.push({ 313 address: adres, 314 // Alleen kolom 5. Een bestand uit Mastodon heeft die niet en levert dus 315 // `false`, en dat is juist: hun "Show boosts" in kolom 2 gaat over iets 316 // anders en mag hier niet als uitgelicht binnenkomen. 317 featured: /^(true|1|yes)$/i.test((velden[4] || '').trim()), 318 }); 319 } 320 return uit; 321 } 322 323 /** Eén CSV-regel, met respect voor geciteerde velden en verdubbelde aanhalingstekens. */ 324 function splitsCsvRegel(regel) { 325 const velden = []; 326 let veld = ''; 327 let inCitaat = false; 328 for (let i = 0; i < regel.length; i++) { 329 const c = regel[i]; 330 if (inCitaat) { 331 if (c === '"') { 332 if (regel[i + 1] === '"') { veld += '"'; i++; } else inCitaat = false; 333 } else veld += c; 334 } else if (c === '"') inCitaat = true; 335 else if (c === ',') { velden.push(veld); veld = ''; } 336 else veld += c; 337 } 338 velden.push(veld); 339 return velden; 340 } 341 236 342 /** 237 343 * Bouw het archief als een lijst bestanden: pad -> inhoud (Buffer). … … 243 349 const origin = (opts.origin || process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 244 350 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug); 245 if (!site) throw new Error(`onbekende site: ${slug}`); 351 if (!site) { 352 // De naam van de INSTANCE (de map, de unit) en de slug van de SITE in zijn 353 // database zijn twee dingen. Ze vallen vaak samen en soms niet, en dan zat je 354 // met een foutmelding die je liet raden. Zeg dus wat er wel in staat. 355 let bestaand = []; 356 try { bestaand = db.prepare('SELECT slug FROM sites ORDER BY rowid').all().map((r) => r.slug); } catch { /* geen sites-tabel */ } 357 const wat = slug ? `onbekende site: ${slug}` : 'geen site opgegeven'; 358 throw new Error(bestaand.length 359 ? `${wat}. In deze database staat: ${bestaand.join(', ')}` 360 : `${wat}. In deze database staat geen enkele site -- wijst DATABASE_PATH naar de juiste?`); 361 } 246 362 247 363 const bestanden = new Map(); // pad -> Buffer … … 322 438 } 323 439 440 // Wie je volgt. Dit ontbrak, en daarmee was een "verhuizing" halfslachtig: de 441 // Move vertelt je VOLGERS waar je heen ging, maar niets vertelde JOU wie jij 442 // volgde. Die lijst stond alleen in de oude database, en die laat je achter. 443 const volgCsv = followingCsv(slug); 444 if (volgCsv) { bestanden.set('following.csv', Buffer.from(volgCsv, 'utf8')); tellingen.following = volgCsv.trim().split('\n').length - 1; } 445 324 446 const files = {}; 325 447 for (const pad of [...bestanden.keys()].sort()) files[pad] = sha256(bestanden.get(pad));
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)