Changeset bea59a1 in Klonkt
- Timestamp:
- 06/30/2026 05:57:53 AM (2 months ago)
- Branches:
- main
- Children:
- 4d63c36
- Parents:
- 69b509d
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r69b509d rbea59a1 819 819 return 202; 820 820 } 821 // A remote post we cached was edited upstream → refresh our cached copy. This is the 822 // push-based edit-sync that keeps the Cirkel/timeline fresh without polling (selfHeal 823 // does it on a version bump; this does it live). Scope to the SIGNING actor so B can't 824 // edit A's note (the signature gate guarantees claimedActor == the verified signer). 825 if (type === 'Update' && act.object && (act.object.type === 'Note' || act.object.type === 'Article')) { 826 const o = act.object; 827 if (o.id && claimedActor) { 828 const html = HtmlSanitizerService.sanitize(o.content || ''); 829 const media = mediaFromNote(o); 830 try { 831 const r = db.prepare('UPDATE ap_timeline SET content = ?, media_json = ?, nsfw = ?, cw = ? WHERE id = ? AND author_uri = ?') 832 .run(html, media, o.sensitive ? 1 : 0, o.summary || null, o.id, claimedActor); 833 if (r.changes) console.log('[AP] timeline update', claimedActor, '→', o.id); 834 } catch { /* ignore */ } 835 // If this note is a cached fediverse reply on one of our posts, refresh its text too. 836 try { db.prepare('UPDATE ap_interactions SET content = ? WHERE object_uri = ? AND actor_uri = ?').run(html, o.id, claimedActor); } catch { /* ignore */ } 837 } 838 return 202; 839 } 821 840 if (type === 'Like' || type === 'Announce') { 822 841 const tgt = act.object; … … 1371 1390 export function setAutoBoost(slug, actorUri, on) { 1372 1391 try { fwStmts().setAB.run(on ? 1 : 0, slug, actorUri); } catch { /* ignore */ } 1392 // Featuring an account → AP-native catch-up so the Cirkel isn't empty until they next 1393 // post (push doesn't backfill history-before-follow). Fire-and-forget pull, sends nothing. 1394 if (on) backfillFromOutbox(slug, actorUri).catch(() => {}); 1373 1395 return { ok: true }; 1374 1396 } … … 1483 1505 } 1484 1506 return JSON.stringify(atts); 1507 } 1508 // A generic SSRF-safe AP GET (collections / pages). 1509 async function apGetJson(url) { 1510 try { 1511 const r = await safeFetch(url, { headers: { Accept: 'application/activity+json' } }); 1512 if (!r.ok) return null; 1513 const len = Number(r.headers.get('content-length') || 0); 1514 if (len > 3_000_000) return null; 1515 return await r.json(); 1516 } catch { return null; } 1517 } 1518 // AP-native catch-up: pull an actor's standard `outbox` collection and merge their recent 1519 // top-level posts into the timeline for `slug`. Push (Create delivery) cannot backfill 1520 // history-from-before-you-followed or a delivery that was missed while you were down; 1521 // reading the outbox is the spec-conform way to catch up. PULL ONLY — sends nothing. 1522 export async function backfillFromOutbox(slug, actorUri, limit = 20) { 1523 try { 1524 if (!slug || !actorUri) return 0; 1525 const actor = await fetchActor(actorUri); 1526 if (!actor || !actor.outbox) return 0; 1527 let page = await apGetJson(typeof actor.outbox === 'string' ? actor.outbox : actor.outbox.id); 1528 let items = (page && (page.orderedItems || page.items)) || []; 1529 if (!items.length && page && page.first) { 1530 page = await apGetJson(typeof page.first === 'string' ? page.first : page.first.id); 1531 items = (page && (page.orderedItems || page.items)) || []; 1532 } 1533 if (!Array.isArray(items) || !items.length) return 0; 1534 const ai = actorInfo(actor, actorUri); 1535 let added = 0; 1536 for (const it of items.slice(0, limit)) { 1537 // Each item is usually a Create wrapping a Note, or sometimes the Note itself. 1538 const o = (it && typeof it.object === 'object' && it.object) ? it.object : it; 1539 if (!o || !o.id) continue; 1540 if (o.type && o.type !== 'Note' && o.type !== 'Article') continue; // skip boosts/other 1541 if (o.inReplyTo) continue; // top-level only 1542 const auth = actorUriOf(o.attributedTo); 1543 if (auth && auth !== actorUri) continue; // their OWN posts only 1544 const html = HtmlSanitizerService.sanitize(o.content || ''); 1545 try { 1546 const r = tlStmts().ins.run(o.id, slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.url || null, o.published || null, mediaFromNote(o), o.sensitive ? 1 : 0, o.summary || null); 1547 if (r && r.changes > 0) added++; 1548 } catch { /* ignore */ } 1549 } 1550 if (added) console.log('[AP] outbox backfill', actorUri, '→', slug, '+' + added); 1551 return added; 1552 } catch { return 0; } 1485 1553 } 1486 1554 let _selfHealing = false; … … 1539 1607 catch (e) { console.warn('[AP] follow deliver failed:', e.message); } 1540 1608 console.log('[AP] follow', site.slug, '→', actor.id); 1609 // Follow + feature in one step → backfill their recent posts into the Cirkel right away. 1610 if (autoBoost) backfillFromOutbox(site.slug, actor.id).catch(() => {}); 1541 1611 return { ok: true, name: ai.name, handle: ai.handle, actor: actor.id }; 1542 1612 } … … 1708 1778 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote, 1709 1779 listOutbox, deliverOutboxDelete, deliverOutboxUpdate, 1710 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, getTimeline, sendInteraction,1780 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, sendInteraction, 1711 1781 autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline, 1712 1782 getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
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)