Changeset 911d5cd in Klonkt
- Timestamp:
- 08/24/2026 04:20:30 PM (2 weeks ago)
- Branches:
- main
- Children:
- 4d64a4b
- Parents:
- b05eb97
- Location:
- src/services
- Files:
-
- 1 added
- 1 edited
-
ActivityPubService.js (modified) (4 diffs)
-
ap-reactions.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
rb05eb97 r911d5cd 70 70 extractQuoteUrl, extractLinkJson, quoteHrefOf, timelineQuote, 71 71 }; 72 // Stap 6 (shaer-drc): het reactiecluster woont in ap-reactions.js. Dat 73 // importeert tlStmts zelf statisch uit ap-timeline; alleen movedLock gaat er 74 // onderaan via wireReactions in. 75 import { 76 wireReactions, 77 setMyReaction, getMyReactions, 78 markBoosted, unmarkBoosted, markLiked, unmarkLiked, 79 migrateReactions, canonicalReactionUri, getReaction, getReactionsFor, 80 setReaction, getTimelineReaction, upsertBoostedNote, boostedCount, 81 } from './ap-reactions.js'; 82 export { 83 setMyReaction, getMyReactions, 84 markBoosted, unmarkBoosted, markLiked, unmarkLiked, 85 migrateReactions, canonicalReactionUri, getReaction, getReactionsFor, 86 setReaction, getTimelineReaction, upsertBoostedNote, boostedCount, 87 }; 72 88 // Doorgeven wat hier altijd vandaan kwam, zodat elke bestaande aanroep blijft werken. 73 89 export { AP_CONTEXT, actorId, noteId, guessMediaType }; … … 1461 1477 export function setInteractionLiked(id, on) { 1462 1478 db.prepare('UPDATE ap_interactions SET acted_like = ? WHERE id = ?').run(on ? 1 : 0, id); 1463 }1464 // Your like/boost state on a REMOTE post (interact page toggles).1465 export function setMyReaction(slug, uri, kind, on) {1466 if (on) db.prepare('INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind) VALUES (?,?,?)').run(slug, uri, kind);1467 else db.prepare('DELETE FROM ap_my_reactions WHERE site_slug = ? AND target_uri = ? AND kind = ?').run(slug, uri, kind);1468 }1469 export function getMyReactions(slug, uri) {1470 const rows = (slug && uri) ? db.prepare('SELECT kind FROM ap_my_reactions WHERE site_slug = ? AND target_uri = ?').all(slug, uri) : [];1471 return { liked: rows.some((r) => r.kind === 'like'), boosted: rows.some((r) => r.kind === 'boost') };1472 1479 } 1473 1480 … … 3828 3835 try { if (!_cirkelMembers) _cirkelMembers = db.prepare('SELECT name, url, icon FROM ap_following WHERE slug = ? AND auto_boost = 1 ORDER BY name'); return _cirkelMembers.all(slug); } catch { return []; } 3829 3836 } 3830 // AFGELEIDE, GEEN BRON (shaer-9e9). De waarheid over "heb ik hierop gereageerd"3831 // staat in ap_my_reactions; deze vlaggen worden daaruit bijgehouden door3832 // setReaction en door niets anders. Roep ze niet los aan -- dan schrijf je de3833 // helft, en dat is precies hoe shaer:liked maandenlang false bleef (04aca12).3834 //3835 // ap_timeline.boosted verdient zijn bestaan wel: hij staat in de WHERE van de3836 // Cirkel-feed (getCirkelPosts) en in boostedCount, dus hij is een index en geen3837 // kopie. ap_timeline.liked wordt nergens als verzameling bevraagd en kan weg3838 // zodra fase 2 lang genoeg goed staat; hij is nu nog het vangnet waarmee3839 // terugdraaien een code-revert blijft in plaats van dataherstel.3840 let _markBoost, _unmarkBoost, _boostedCount;3841 export function markBoosted(slug, noteId) {3842 try { if (!_markBoost) _markBoost = db.prepare('UPDATE ap_timeline SET boosted = 1 WHERE slug = ? AND id = ?'); _markBoost.run(slug, noteId); } catch { /* ignore */ }3843 }3844 export function unmarkBoosted(slug, noteId) {3845 try { if (!_unmarkBoost) _unmarkBoost = db.prepare('UPDATE ap_timeline SET boosted = 0 WHERE slug = ? AND id = ?'); _unmarkBoost.run(slug, noteId); } catch { /* ignore */ }3846 }3847 let _markLike, _unmarkLike;3848 export function markLiked(slug, noteId) {3849 try { if (!_markLike) _markLike = db.prepare('UPDATE ap_timeline SET liked = 1 WHERE slug = ? AND id = ?'); _markLike.run(slug, noteId); } catch { /* ignore */ }3850 }3851 export function unmarkLiked(slug, noteId) {3852 try { if (!_unmarkLike) _unmarkLike = db.prepare('UPDATE ap_timeline SET liked = 0 WHERE slug = ? AND id = ?'); _unmarkLike.run(slug, noteId); } catch { /* ignore */ }3853 }3854 /**3855 * Zet een reactie van JOU op een object. Dit hoort het enige schrijfpad te zijn3856 * (shaer-9e9): de tussentabel ap_my_reactions is de waarheid, de vlaggen op3857 * ap_timeline zijn de afgeleide. Zolang markLiked en broers los aanroepbaar3858 * blijven kan een aanroeper ze vergeten, en dat is niet hypothetisch -- precies3859 * dat leverde de shaer:liked-bug op (04aca12).3860 *3861 * `opts.note` is de opgeloste remote note bij een boost. Die is niet optioneel3862 * uit netheid: een boost moet de post je tijdlijn IN trekken als je de auteur3863 * niet volgt, anders heeft de vlag geen rij om op te landen en verschijnt de3864 * boost nergens -- ook niet in de Cirkel.3865 *3866 * `opts.flagUri` bestaat omdat de twee bronnen vandaag verschillend gesleuteld3867 * worden: de tussentabel op de URI die de client stuurde, de vlag op de3868 * opgeloste object-URI. Meestal zijn die gelijk, maar niet gegarandeerd. Deze3869 * naad houdt fase 1 gedragsbehoudend; het samentrekken van die twee sleutels is3870 * werk voor fase 2, mét datamigratie.3871 */3872 // Reactie-migratie (shaer-9e9). Draait bij boot, EEN keer per bump, net als3873 // selfHealTimeline. Bewust automatisch: klonkt-update tilt een hele vloot in een3874 // stap naar nieuwe code, en een handmatig script per instance wordt vergeten --3875 // terwijl het falen stil is (een reactie die niemand meer ziet geeft geen fout).3876 // v2 haalt de derde bron erbij: ap_interactions.acted_* (shaer-ipb). Een bump3877 // laat alle stappen opnieuw lopen, en dat mag -- ze zijn alle drie idempotent.3878 const REACTIONS_MIGRATION_VERSION = 2;3879 3880 /**3881 * Brengt alle reacties naar de tussentabel, onder de canonieke object-URI.3882 *3883 * Twee stappen, en ze zijn allebei nodig:3884 *3885 * 1. HERSLEUTELEN. De oude interact-route bewaarde de URI waarmee je binnenkwam3886 * en de bookmarklet geeft window.location.href door, dus de permalink. Sinds3887 * canonicalReactionUri wordt er op de object-URI gezocht, waardoor die rijen3888 * wees zouden zijn. De created_at reist mee: bij hersleutelen weten we3889 * wanneer je reageerde, bij aanvullen niet.3890 * 2. AANVULLEN vanuit de afgeleide kolommen. Alles wat op oude code via de3891 * Krant is gegeven staat alleen daar; zonder deze stap toont het als3892 * niet-gereageerd en klikt een gebruiker opnieuw -- met een tweede Like de3893 * fediverse in als gevolg.3894 *3895 * Idempotent. Geeft terug wat er gebeurd is, zodat het script het kan tonen.3896 */3897 export function migrateReactions(opts = {}) {3898 const uit = { hersleuteld: 0, aangevuld: 0, reacties: 0, overgeslagen: false };3899 try {3900 if (!opts.force) {3901 const r = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('reactions_migration_version');3902 const cur = r ? (parseInt(r.value, 10) || 0) : 0;3903 if (cur >= REACTIONS_MIGRATION_VERSION) { uit.overgeslagen = true; return uit; }3904 }3905 } catch { return uit; } // geen app_settings → deze database is te oud om aan te raken3906 3907 // Een rij die NIET op een tijdlijn-id staat maar wel op een tijdlijn-url.3908 const wees = `3909 FROM ap_my_reactions r JOIN ap_timeline t ON t.slug = r.site_slug AND t.url = r.target_uri3910 WHERE NOT EXISTS (SELECT 1 FROM ap_timeline t2 WHERE t2.slug = r.site_slug AND t2.id = r.target_uri)`;3911 const scheef = (kind, kolom) => `3912 FROM ap_timeline t3913 WHERE t.${kolom} = 13914 AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r3915 WHERE r.site_slug = t.slug AND r.target_uri = t.id AND r.kind = '${kind}')`;3916 // 3. De derde bron: wat JIJ deed met een reactie onder je eigen post. De slug3917 // hangt hier niet aan de rij maar aan de post; vandaar de twee joins. Een3918 // rij zonder object_uri kan nooit een reactie dragen (fedi-react eist hem),3919 // dus die uitsluiting verliest per constructie niets.3920 const acted = (kind, kolom) => `3921 FROM ap_interactions i3922 JOIN posts p ON p.id = i.post_id3923 JOIN sites s ON s.id = p.site_id3924 WHERE i.${kolom} = 1 AND IFNULL(i.object_uri, '') <> ''3925 AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r3926 WHERE r.site_slug = s.slug AND r.target_uri = i.object_uri AND r.kind = '${kind}')`;3927 3928 if (opts.dryRun) {3929 const tel = (sql) => { try { return db.prepare(`SELECT COUNT(*) AS n ${sql}`).get().n; } catch { return 0; } };3930 uit.hersleuteld = tel(wees);3931 uit.aangevuld = tel(scheef('like', 'liked')) + tel(scheef('boost', 'boosted'));3932 uit.reacties = tel(acted('like', 'acted_like')) + tel(acted('boost', 'acted_boost'));3933 return uit;3934 }3935 3936 try {3937 db.transaction(() => {3938 // 1. Hersleutelen: eerst de canonieke variant erbij, dan de permalink weg.3939 // In die volgorde, zodat een onderbreking hooguit een dubbele rij3940 // oplevert en nooit een verdwenen reactie.3941 uit.hersleuteld = db.prepare(`3942 INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind, created_at)3943 SELECT r.site_slug, t.id, r.kind, r.created_at ${wees}`).run().changes;3944 db.prepare(`DELETE FROM ap_my_reactions WHERE rowid IN (SELECT r.rowid ${wees})`).run();3945 3946 // 2. Aanvullen vanuit de kolommen.3947 for (const [kind, kolom] of [['like', 'liked'], ['boost', 'boosted']]) {3948 uit.aangevuld += db.prepare(`3949 INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind)3950 SELECT t.slug, t.id, '${kind}' ${scheef(kind, kolom)}`).run().changes;3951 }3952 3953 // 3. En vanuit acted_* op de reacties onder je eigen posts.3954 for (const [kind, kolom] of [['like', 'acted_like'], ['boost', 'acted_boost']]) {3955 uit.reacties += db.prepare(`3956 INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind)3957 SELECT s.slug, i.object_uri, '${kind}' ${acted(kind, kolom)}`).run().changes;3958 }3959 })();3960 if (uit.hersleuteld || uit.aangevuld || uit.reacties) {3961 console.log(`[AP] reaction migration v${REACTIONS_MIGRATION_VERSION}: ${uit.hersleuteld} re-keyed, ${uit.aangevuld} backfilled, ${uit.reacties} from comments`);3962 }3963 if (!opts.force) {3964 db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)')3965 .run('reactions_migration_version', String(REACTIONS_MIGRATION_VERSION));3966 }3967 } catch (e) {3968 // Niet fataal: de kolommen staan er nog, dus de oude waarheid is niet weg.3969 // Een volgende boot probeert het opnieuw, want de versie is niet gezet.3970 console.warn('[AP] reaction migration failed:', e.message);3971 }3972 return uit;3973 }3974 3975 /**3976 * Van wat de client stuurde naar de canonieke sleutel voor een reactie.3977 *3978 * Een post heeft twee URI's: zijn AP-object-id (.../ap/notes/<uuid>) en zijn3979 * leesbare permalink (.../effortlesseffect). De Krant en het C2S-pad spreken de3980 * eerste, de interact-pagina de tweede. Werden reacties onder allebei opgeslagen,3981 * dan bestond dezelfde like twee keer -- en erger: een like uit de Krant was op3982 * de interact-pagina onzichtbaar, want daar werd op de permalink gezocht.3983 *3984 * Dit was de naad die fase 1 bewust open liet ("samentrekken is werk voor fase3985 * 2"). Robin liep er meteen tegenaan: een geboost en geliket bericht toonde geen3986 * highlight. Vandaar hier, en niet later.3987 *3988 * De object-URI wint, want dat is waar ap_timeline op sleutelt en waar de3989 * backfill op is gebaseerd. Kennen we de post niet, dan blijft de invoer staan:3990 * een reactie op iets buiten je tijdlijn moet gewoon werken.3991 */3992 export function canonicalReactionUri(slug, uri) {3993 if (!slug || !uri) return uri;3994 try {3995 if (db.prepare('SELECT 1 FROM ap_timeline WHERE slug = ? AND id = ?').get(slug, uri)) return uri;3996 const row = db.prepare('SELECT id FROM ap_timeline WHERE slug = ? AND url = ? LIMIT 1').get(slug, uri);3997 return (row && row.id) || uri;3998 } catch { return uri; }3999 }4000 4001 /**4002 * Wat heb IK met dit object gedaan? Leest de tussentabel, de bron van waarheid4003 * sinds shaer-9e9 fase 2. Vervangt getMyReactions en getTimelineReaction, die4004 * dezelfde vraag beantwoordden uit twee verschillende bronnen.4005 */4006 export function getReaction(slug, uri) {4007 try {4008 const key = canonicalReactionUri(slug, uri);4009 const rows = (slug && key)4010 ? db.prepare('SELECT kind FROM ap_my_reactions WHERE site_slug = ? AND target_uri = ?').all(slug, key)4011 : [];4012 return { liked: rows.some((r) => r.kind === 'like'), boosted: rows.some((r) => r.kind === 'boost') };4013 } catch { return { liked: false, boosted: false }; }4014 }4015 4016 /**4017 * Dezelfde vraag voor een hele pagina in EEN query. De C2S-tijdlijn zet4018 * shaer:liked op elke post; per rij vragen zou dat een N+1 maken, en dan had je4019 * een consistentiebug geruild voor een traagheidsbug.4020 */4021 export function getReactionsFor(slug, uris) {4022 const out = new Map();4023 const list = [...new Set((uris || []).filter(Boolean))].slice(0, 500);4024 if (!slug || !list.length) return out;4025 try {4026 const rows = db.prepare(4027 `SELECT target_uri, kind FROM ap_my_reactions4028 WHERE site_slug = ? AND target_uri IN (${list.map(() => '?').join(',')})`,4029 ).all(slug, ...list);4030 for (const r of rows) {4031 const cur = out.get(r.target_uri) || { liked: false, boosted: false };4032 if (r.kind === 'like') cur.liked = true;4033 if (r.kind === 'boost') cur.boosted = true;4034 out.set(r.target_uri, cur);4035 }4036 } catch { /* leeg = niets gereageerd, en dat is een veilige uitkomst */ }4037 return out;4038 }4039 4040 export function setReaction(slug, uri, kind, on, opts = {}) {4041 if (!slug || !uri || (kind !== 'like' && kind !== 'boost')) return;4042 // Ook hier, en niet alleen bij sendInteraction. Deze functie schrijft ALLEEN de4043 // lokale vlag; het versturen gebeurt elders. Zonder deze poort zou je op een4044 // verhuisd account een like zien staan die nooit de deur uit is gegaan, en dat4045 // is de halve toestand die erger is dan een duidelijke weigering.4046 try {4047 const s = db.prepare('SELECT moved_to FROM sites WHERE slug = ?').get(slug);4048 if (movedLock(s).locked) { console.warn('[AP] reactie geweigerd, account verhuisd:', slug, kind); return; }4049 } catch { /* geen sites-tabel = geen verhuizing */ }4050 // EEN sleutel voor beide bronnen. opts.flagUri is de opgeloste object-URI van4051 // de aanroeper (het C2S-pad kent die uit resolveRemoteNote en dat is4052 // betrouwbaarder dan onze cache); anders leiden we hem af. Vroeger kreeg de4053 // tussentabel de URI die de client stuurde en de vlag de opgeloste -- dat4054 // maakte dezelfde like onvindbaar vanaf de andere pagina.4055 const flagUri = opts.flagUri || canonicalReactionUri(slug, uri);4056 setMyReaction(slug, flagUri, kind, !!on);4057 if (kind === 'boost') {4058 if (!on) unmarkBoosted(slug, flagUri);4059 else if (opts.note) upsertBoostedNote(slug, opts.note);4060 else markBoosted(slug, flagUri);4061 } else if (on) markLiked(slug, flagUri);4062 else unmarkLiked(slug, flagUri);4063 }4064 4065 export function getTimelineReaction(slug, noteId) {4066 try { const r = db.prepare('SELECT liked, boosted FROM ap_timeline WHERE slug = ? AND id = ?').get(slug, noteId); return { liked: !!(r && r.liked), boosted: !!(r && r.boosted) }; } catch { return { liked: false, boosted: false }; }4067 }4068 // Boost a REMOTE post that may not be in your timeline (you don't follow the author):4069 // store it in ap_timeline (INSERT OR IGNORE → no dup for followed posts) so it shows in4070 // the Cirkel with a Boost badge, then flag it boosted.4071 export function upsertBoostedNote(slug, note) {4072 if (!slug || !note || !note.object_uri) return;4073 const id = note.object_uri;4074 // Prefer the full typed media (incl. video/mp4 — a Loops boost is video-only and4075 // rendered a bare text tile); fall back to the image-only list for older callers.4076 const media = (note.media && note.media !== '[]')4077 ? note.media4078 : JSON.stringify((note.images || []).map((u) => ({ url: u, type: 'image/jpeg' })));4079 try {4080 const r = tlStmts().ins.run(id, slug, note.actor_uri || '', note.actor_name || '', note.actor_handle || '',4081 note.actor_icon || '', note.actor_url || '', note.content || '', note.url || null,4082 new Date().toISOString(), media, note.sensitive ? 1 : 0, note.cw || null);4083 if (!r.changes) {4084 // Row already cached (INSERT OR IGNORE) → refresh it with the freshly4085 // resolved note. Without this a row cached without its cover (or with4086 // stale content) stayed stale forever — even boosting again didn't heal it.4087 // Keep the CACHED media when the resolve yielded none: an empty re-resolve4088 // used to clobber a good media_json (the followed copy had the video, the4089 // boost wiped it to []).4090 db.prepare(`UPDATE ap_timeline SET content = ?, media_json = CASE WHEN ? = '[]' THEN media_json ELSE ? END,4091 nsfw = ?, cw = ?, url = COALESCE(?, url) WHERE slug = ? AND id = ?`)4092 .run(note.content || '', media, media, note.sensitive ? 1 : 0, note.cw || null, note.url || null, slug, id);4093 }4094 } catch { /* ignore */ }4095 markBoosted(slug, id);4096 }4097 export function boostedCount(slug) {4098 // Geboost EN in je tijdlijn, zoals voorheen: de tussentabel kan ook een boost4099 // bevatten van iets dat er (nog) niet in staat.4100 try {4101 if (!_boostedCount) _boostedCount = db.prepare(`SELECT COUNT(*) AS n FROM ap_my_reactions r4102 JOIN ap_timeline t ON t.slug = r.site_slug AND t.id = r.target_uri4103 WHERE r.site_slug = ? AND r.kind = 'boost'`);4104 return _boostedCount.get(slug).n;4105 } catch { return 0; }4106 }4107 3837 4108 3838 // Resolve a Klonkt/AP actor URL from a site root: a Klonkt site's root 302s to … … 5772 5502 deliverCreate, 5773 5503 }); 5774 // En de tijdlijn-leeskant zijn ene werktuig (stap 5): liked/boosted komen uit 5775 // het reactiecluster, dat hier woont. 5504 // En de tijdlijn-leeskant zijn ene werktuig (stap 5): liked/boosted komen 5505 // sinds stap 6 uit ap-reactions, maar de koppeling blijft HIER lopen -- twee 5506 // zustermodules die elkaar importeren zou een kring zijn. 5776 5507 wireTimeline({ getReactionsFor }); 5508 // Het reactiecluster zijn ene werktuig (stap 6): de verhuisgrendel (FEP-7628). 5509 wireReactions({ movedLock }); 5777 5510 5778 5511 export default {
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)