Changeset 67c1f24 in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 07/17/2026 12:38:54 AM (8 weeks ago)
- Branches:
- main
- Children:
- 92a2c46
- Parents:
- 3778ddb
- git-author:
- Robin <roboburr@…> (07/17/2026 12:38:34 AM)
- git-committer:
- Robin <roboburr@…> (07/17/2026 12:38:54 AM)
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r3778ddb r67c1f24 596 596 // ── inbound interactions store (replies / likes / boosts) + our outbound replies ── 597 597 let _insI, _delLA, _delReply, _listI, _getI, _insO, _listO, _getO; 598 // ── moderation tombstones (ap_rejected_objects) ─────────────────── 599 // A reply the owner removed stays removed: its object URI is tombstoned and 600 // checked at ingest AND by the thread-crawler (else thread-filling would 601 // re-fetch it). Owner moderation acts on the LOCAL copy, so it also works for 602 // private notes that authorize_interaction can't fetch (401/404). 603 let _insRj, _hasRj; 604 function rjStmts() { 605 if (!_insRj) { 606 _insRj = db.prepare('INSERT OR IGNORE INTO ap_rejected_objects (object_uri, post_id, reason) VALUES (?,?,?)'); 607 _hasRj = db.prepare('SELECT 1 FROM ap_rejected_objects WHERE object_uri = ?'); 608 } 609 return { ins: _insRj, has: _hasRj }; 610 } 611 export function isRejectedObject(uri) { 612 if (!uri) return false; 613 try { return !!rjStmts().has.get(String(uri)); } catch { return false; } 614 } 615 // Owner removes an incoming reply: tombstone + delete. Tenancy-scoped: the 616 // interaction's post must belong to the caller's site. 617 export function rejectInteraction(site, interactionId, reason) { 618 if (!site || !site.slug) return { error: 'forbidden' }; 619 const row = iStmts().getI.get(interactionId); 620 if (!row) return { error: 'not_found' }; 621 const owns = db.prepare('SELECT 1 FROM posts WHERE id = ? AND site_id = (SELECT id FROM sites WHERE slug = ?)') 622 .get(row.post_id, site.slug); 623 if (!owns) return { error: 'forbidden' }; 624 if (row.object_uri) { try { rjStmts().ins.run(row.object_uri, row.post_id, reason || 'removed by site owner'); } catch { /* non-fatal */ } } 625 db.prepare('DELETE FROM ap_interactions WHERE id = ?').run(interactionId); 626 console.log('[AP] interaction removed by owner', site.slug, row.object_uri || row.actor_uri); 627 return { ok: true, object_uri: row.object_uri || null, actor_uri: row.actor_uri || null }; 628 } 629 // Stored URIs of an interaction (tenancy-scoped) → feed sendReport for flagging 630 // from the local copy (works for private notes; no remote fetch needed to target). 631 export function interactionReportTarget(site, interactionId) { 632 if (!site || !site.slug) return null; 633 const row = iStmts().getI.get(interactionId); 634 if (!row) return null; 635 const owns = db.prepare('SELECT 1 FROM posts WHERE id = ? AND site_id = (SELECT id FROM sites WHERE slug = ?)') 636 .get(row.post_id, site.slug); 637 if (!owns) return null; 638 return { objectUri: row.object_uri || null, actorUri: row.actor_uri || null }; 639 } 640 598 641 // AP addressing → visibility: 'public' | 'unlisted' | 'followers' | 'direct'. 599 642 // Mastodon-conventie: Public in `to` = public, Public in `cc` = unlisted, een … … 1165 1208 const ai = actorInfo(await resolveActor(actorUri), actorUri); 1166 1209 const html = HtmlSanitizerService.sanitize(o.content || ''); 1210 if (isRejectedObject(o.id)) { console.log('[AP] reply skipped (tombstoned)', o.id); return 202; } 1167 1211 iStmts().ins.run('reply', tgt.post_id, o.id || '', actorUri, ai.name, ai.handle, ai.url, ai.icon, html, o.published || null, tgt.parent_uri, noteVisibility(o)); 1168 1212 console.log('[AP] reply', actorUri, '→', tgt.post_id); … … 2131 2175 const seeds = [...known].filter((u) => /^https?:\/\//i.test(u)); 2132 2176 if (!seeds.length) return; // nothing remote to expand 2177 // Owner-removed replies (tombstones) join the dedup set AFTER seeding, so the 2178 // crawler never re-adds them via thread-filling (they're gone from the seeds 2179 // already because rejectInteraction deleted their ap_interactions row). 2180 try { for (const r of db.prepare('SELECT object_uri FROM ap_rejected_objects WHERE post_id = ?').all(postId)) known.add(r.object_uri); } 2181 catch { /* table always exists after boot migration */ } 2133 2182 2134 2183 let fetches = 0; … … 2151 2200 const child = await budget.get(cu); 2152 2201 if (!child || !child.id || (child.type !== 'Note' && child.type !== 'Article')) continue; 2202 if (isRejectedObject(child.id)) continue; // note id can differ from the collection URI (redirects) 2153 2203 const actorUri = actorUriOf(child.attributedTo); 2154 2204 if (!actorUri || isBlockedAny(actorUri)) continue; // skip blocked authors … … 2544 2594 getReplyUris, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio, 2545 2595 linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections, 2546 noteVisibility, 2596 noteVisibility, isRejectedObject, rejectInteraction, interactionReportTarget, 2547 2597 };
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)