Changeset f2796d3 in Klonkt
- Timestamp:
- 06/29/2026 10:19:54 PM (2 months ago)
- Branches:
- main
- Children:
- 3f9f71d
- Parents:
- fb6819d
- Location:
- src
- Files:
-
- 2 edited
-
routes/activitypub.js (modified) (3 diffs)
-
services/ActivityPubService.js (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
rfb6819d rf2796d3 46 46 res.type('application/jrd+json; charset=utf-8'); 47 47 res.set('Cache-Control', 'public, max-age=300'); 48 const actorUri = AP.actorId(baseUrl(req), site.slug); 49 const profileUrl = baseUrl(req) + (site.slug === primarySlug() ? '/' : `/user/${encodeURIComponent(site.slug)}`); 48 50 res.send(JSON.stringify({ 49 51 subject: `acct:${site.slug}@${hostOf(req)}`, 50 links: [{ rel: 'self', type: 'application/activity+json', href: AP.actorId(baseUrl(req), site.slug) }], 52 aliases: [actorUri, profileUrl], 53 links: [ 54 { rel: 'self', type: 'application/activity+json', href: actorUri }, 55 { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: profileUrl }, 56 ], 51 57 })); 52 58 }); … … 83 89 const n = db.prepare('SELECT COUNT(*) n FROM ap_followers WHERE slug = ?').get(site.slug).n; 84 90 AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, n)); 91 }); 92 93 // ── Following (count only) ──────────────────────────────────────── 94 router.get('/ap/users/:slug/following', (req, res) => { 95 const site = publicSite(req.params.slug); 96 if (!site) return res.status(404).end(); 97 let n = 0; 98 try { n = db.prepare("SELECT COUNT(*) n FROM ap_following WHERE slug = ? AND status = 'accepted'").get(site.slug).n; } catch { /* table may not exist */ } 99 AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, n)); 85 100 }); 86 101 … … 151 166 router.get('/nodeinfo/2.1', (req, res) => { 152 167 let users = 0; let posts = 0; 153 try { users = db.prepare('SELECT COUNT(*) c FROM users').get().c; } catch { /* */ } 168 // "users" = public AP actors (sites), not the admin/member account rows. 169 try { users = db.prepare('SELECT COUNT(*) c FROM sites WHERE (is_public IS NULL OR is_public = 1)').get().c; } catch { /* */ } 154 170 try { posts = db.prepare("SELECT COUNT(*) c FROM posts WHERE status = 'published'").get().c; } catch { /* */ } 155 171 res.type('application/json; charset=utf-8'); -
src/services/ActivityPubService.js
rfb6819d rf2796d3 137 137 outbox: `${id}/outbox`, 138 138 followers: `${id}/followers`, 139 following: `${id}/following`, 139 140 featured: `${id}/featured`, 140 141 endpoints: { sharedInbox: `${base}/ap/inbox` }, … … 149 150 actor.icon = { type: 'Image', url: u }; 150 151 } 152 // Account creation date — shown by Mastodon + read by indexers (additive, standard AS2). 153 if (site.created_at) { try { actor.published = new Date(site.created_at).toISOString(); } catch { /* skip bad date */ } } 154 // Profile links → PropertyValue rows: Mastodon/PeerTube/WordPress-ActivityPub render these as 155 // profile metadata (rel=me enables link-back verification). Additive; ignored by simpler receivers. 156 try { 157 const links = JSON.parse(site.profile_links || '[]'); 158 if (Array.isArray(links) && links.length) { 159 const esc = (s) => String(s).replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c])); 160 const rows = links 161 .filter((l) => l && l.url && /^https?:/i.test(l.url)) 162 .map((l) => ({ 163 type: 'PropertyValue', 164 name: esc(l.platform || 'Link'), 165 value: `<a href="${esc(l.url).replace(/"/g, '"')}" rel="me nofollow noopener" target="_blank">${esc(String(l.url).replace(/^https?:\/\//, ''))}</a>`, 166 })); 167 if (rows.length) actor.attachment = rows; 168 } 169 } catch { /* skip malformed profile_links */ } 151 170 return actor; 152 171 } … … 340 359 } 341 360 361 // The accounts this site follows — count only, mirroring buildFollowers. The spec lists 362 // `following` as a standard actor property; Hubzilla/Friendica + crawlers expect it. 363 export function buildFollowing(base, site, count) { 364 const id = `${actorId(base, site.slug)}/following`; 365 return { 366 '@context': 'https://www.w3.org/ns/activitystreams', 367 id, 368 type: 'OrderedCollection', 369 totalItems: count || 0, 370 orderedItems: [], // count only 371 }; 372 } 373 342 374 // Pinned posts → the actor's `featured` collection. Mastodon reads this and shows 343 375 // these as the "Featured" tab (pinned to the profile). Posts come ordered by pin … … 1483 1515 const keys = getOrCreateKeys(site.slug); 1484 1516 const row = fwStmts().one.get(site.slug, actorUri); 1485 if (row && row.inbox) { 1486 const undo = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${me}#unfollow-${Date.now()}-${rid()}`, type: 'Undo', actor: me, object: { id: row.follow_id || `${me}#follow`, type: 'Follow', actor: me, object: actorUri } }; 1487 try { await deliver(row.inbox, undo, `${me}#main-key`, keys.private_pem); } catch { /* best-effort */ } 1517 // Undo(Follow) MUST reference the original Follow's real id so the remote can correlate it 1518 // and drop the follow. The old `${me}#follow` fallback never matched anything → the unfollow 1519 // silently failed on the remote. With no stored follow id (legacy row), skip the network Undo 1520 // rather than send an unmatchable one. Deliver durably via the retry queue. 1521 if (row && row.inbox && row.follow_id) { 1522 const undo = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${me}/undo/${Date.now()}-${rid()}`, type: 'Undo', actor: me, object: { id: row.follow_id, type: 'Follow', actor: me, object: actorUri } }; 1523 deliverWithRetry(site.slug, row.inbox, undo, `${me}#main-key`, keys.private_pem); 1524 } else if (row && row.inbox) { 1525 console.warn('[AP] unfollow', site.slug, '→', actorUri, '— no stored follow id; removed locally only (legacy follow, remote may keep it)'); 1488 1526 } 1489 1527 fwStmts().del.run(site.slug, actorUri); … … 1621 1659 export default { 1622 1660 getOrCreateKeys, apWants, sendAP, actorId, noteId, 1623 buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildF eatured,1661 buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured, 1624 1662 followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins, 1625 1663 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
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)