Changeset 55bc7f9 in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 06/24/2026 12:09:08 PM (3 months ago)
- Branches:
- main
- Children:
- 3a2c9d3
- Parents:
- 47a0d29
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r47a0d29 r55bc7f9 191 191 export function followerCount(slug) { return fStmts().cnt.get(slug).n; } 192 192 193 // ── inbound interactions store (replies / likes / boosts) , lazy stmts ──194 let _insI, _delLA, _delReply, _listI ;193 // ── inbound interactions store (replies / likes / boosts) + our outbound replies ── 194 let _insI, _delLA, _delReply, _listI, _getI, _insO, _listO, _getO; 195 195 function iStmts() { 196 196 if (!_insI) { … … 198 198 _delLA = db.prepare('DELETE FROM ap_interactions WHERE kind = ? AND post_id = ? AND actor_uri = ?'); 199 199 _delReply = db.prepare("DELETE FROM ap_interactions WHERE kind = 'reply' AND object_uri = ?"); 200 _listI = db.prepare('SELECT kind, actor_uri, actor_name, actor_handle, actor_url, actor_icon, content, published, created_at FROM ap_interactions WHERE post_id = ? ORDER BY created_at ASC'); 201 } 202 return { ins: _insI, delLA: _delLA, delReply: _delReply, list: _listI }; 203 } 200 _listI = db.prepare('SELECT id, kind, object_uri, actor_uri, actor_name, actor_handle, actor_url, actor_icon, content, published, created_at FROM ap_interactions WHERE post_id = ? ORDER BY created_at ASC'); 201 _getI = db.prepare('SELECT * FROM ap_interactions WHERE id = ?'); 202 _insO = db.prepare('INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, created_at) VALUES (?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)'); 203 _listO = db.prepare('SELECT * FROM ap_outbox WHERE post_id = ? ORDER BY created_at ASC'); 204 _getO = db.prepare('SELECT * FROM ap_outbox WHERE id = ?'); 205 } 206 return { ins: _insI, delLA: _delLA, delReply: _delReply, list: _listI, getI: _getI, insO: _insO, listO: _listO, getO: _getO }; 207 } 208 209 export function getInteractionById(id) { return iStmts().getI.get(id); } 204 210 205 211 const localPostExists = (id) => { try { return !!db.prepare('SELECT 1 FROM posts WHERE id = ?').get(id); } catch { return false; } }; … … 226 232 } 227 233 228 // Stored, view-ready summary of a post's inbound fediverse activity .234 // Stored, view-ready summary of a post's inbound fediverse activity + our replies. 229 235 export function getInteractions(postId) { 230 const rows = iStmts().list.all(postId); 236 const s = iStmts(); 237 const rows = s.list.all(postId); 238 const outReplies = s.listO.all(postId).map((o) => ({ 239 id: o.id, content: o.content, in_reply_to: o.in_reply_to, to_handle: o.to_handle, 240 created_at: o.created_at, mine: true, 241 })); 231 242 return { 232 243 replies: rows.filter((r) => r.kind === 'reply'), 244 outReplies, 233 245 likeCount: rows.filter((r) => r.kind === 'like').length, 234 246 announceCount: rows.filter((r) => r.kind === 'announce').length, 235 total: rows.length ,247 total: rows.length + outReplies.length, 236 248 }; 237 249 } … … 399 411 } 400 412 413 // ── outbound replies (Klonkt → fediverse) ───────────────────────── 414 const escHtml = (s) => String(s || '').replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c])); 415 const toISO = (v) => { if (!v) return new Date().toISOString(); const s = String(v); const d = new Date(/[TZ]/.test(s) ? s : s.replace(' ', 'T') + 'Z'); return isNaN(d) ? new Date().toISOString() : d.toISOString(); }; 416 417 // Build one of OUR outbound reply Notes from an ap_outbox row. 418 export function buildReplyNote(base, site, row) { 419 const me = actorId(base, site.slug); 420 return { 421 id: noteId(base, row.id), 422 type: 'Note', 423 attributedTo: me, 424 inReplyTo: row.in_reply_to || undefined, 425 content: row.content, 426 url: row.post_slug ? `${base}/${encodeURIComponent(row.post_slug)}` : undefined, 427 published: toISO(row.created_at), 428 to: row.to_actor ? [row.to_actor] : [PUBLIC], 429 cc: [PUBLIC, `${me}/followers`], 430 tag: row.to_actor ? [{ type: 'Mention', href: row.to_actor, name: row.to_handle }] : [], 431 }; 432 } 433 434 // Resolve one of our outbound reply Notes by id (for /ap/notes/:id fallback). 435 export function getOutboxNote(base, id) { 436 const row = iStmts().getO.get(id); 437 if (!row) return null; 438 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(row.site_slug); 439 if (!site) return null; 440 return buildReplyNote(base, site, row); 441 } 442 443 // Send a reply FROM this site to a remote actor (in reply to their inbound reply). 444 // `parent` = an ap_interactions row (actor_uri, actor_url, actor_handle, object_uri). 445 export async function deliverReply(site, { postId, postSlug, parent, text }) { 446 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 447 if (!base || !site || !site.slug || !parent || !String(text || '').trim()) return null; 448 const me = actorId(base, site.slug); 449 const handle = parent.actor_handle || deriveHandle(parent.actor_uri); 450 const body = escHtml(String(text).trim()).replace(/\r?\n/g, '<br>'); 451 const mention = parent.actor_uri 452 ? `<a href="${escHtml(parent.actor_url || parent.actor_uri)}" class="u-url mention">${escHtml(handle)}</a> ` : ''; 453 const content = `<p>${mention}${body}</p>`; 454 const id = crypto.randomUUID(); 455 iStmts().insO.run(id, site.slug, postId, postSlug || null, parent.object_uri || null, parent.actor_uri || null, handle, content); 456 const row = iStmts().getO.get(id); 457 const note = buildReplyNote(base, site, row); 458 const create = { 459 '@context': 'https://www.w3.org/ns/activitystreams', 460 id: note.id + '#create', type: 'Create', actor: me, 461 published: note.published, to: note.to, cc: note.cc, object: note, 462 }; 463 const keys = getOrCreateKeys(site.slug); 464 const keyId = `${me}#main-key`; 465 const inboxes = new Set(); 466 if (parent.actor_uri) { 467 const a = await fetchActor(parent.actor_uri).catch(() => null); 468 if (a) inboxes.add((a.endpoints && a.endpoints.sharedInbox) || a.inbox); 469 } 470 for (const f of fStmts().list.all(site.slug)) inboxes.add(f.shared_inbox || f.inbox); 471 let delivered = 0; 472 for (const inbox of [...inboxes].filter(Boolean)) { 473 try { const st = await deliver(inbox, create, keyId, keys.private_pem); if (st >= 200 && st < 300) delivered++; } catch { /* best-effort */ } 474 } 475 console.log('[AP] outreply', site.slug, '→', parent.actor_uri, 'delivered', delivered); 476 return { id, content, delivered }; 477 } 478 401 479 export default { 402 480 getOrCreateKeys, apWants, sendAP, actorId, noteId, 403 481 buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, 404 482 followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, 405 getInteractions, 483 getInteractions, getInteractionById, buildReplyNote, getOutboxNote, deliverReply, 406 484 };
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)