Changeset 096f099 in Klonkt
- Timestamp:
- 08/13/2026 06:01:50 PM (4 weeks ago)
- Branches:
- main
- Children:
- e9ec5e4
- Parents:
- 0824351
- Files:
-
- 1 added
- 3 edited
-
src/config/database.js (modified) (1 diff)
-
src/services/ArchiveExportService.js (modified) (2 diffs)
-
src/services/ArchiveImportService.js (modified) (2 diffs)
-
test/following-csv.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
r0824351 r096f099 739 739 // "Feature" a followed account: its posts show in the local Cirkel. 740 740 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'); 741 749 // A timeline post you boosted (đ) â also shown in the Cirkel (mixed by date). 742 750 ensureColumn('ap_timeline', 'boosted', 'INTEGER DEFAULT 0'); -
src/services/ArchiveExportService.js
r0824351 r096f099 238 238 } 239 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 // 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. 253 const 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. */ 256 function 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 */ 272 export 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 */ 298 export 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. */ 318 function 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 240 336 /** 241 337 * Bouw het archief als een lijst bestanden: pad -> inhoud (Buffer). … … 336 432 } 337 433 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 338 440 const files = {}; 339 441 for (const pad of [...bestanden.keys()].sort()) files[pad] = sha256(bestanden.get(pad)); -
src/services/ArchiveImportService.js
r0824351 r096f099 25 25 import db from '../config/database.js'; 26 26 import { MEDIA_ROOT } from '../config/paths.js'; 27 import { FORMAT_VERSION } from './ArchiveExportService.js';27 import { FORMAT_VERSION, parseFollowingCsv } from './ArchiveExportService.js'; 28 28 29 29 const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex'); … … 116 116 function bestemming(a) { 117 117 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 */ 134 export 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; 118 160 } 119 161
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)