Changeset d7526bd in Klonkt


Ignore:
Timestamp:
06/25/2026 05:20:29 AM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
e951b7c
Parents:
5a6a457
Message:

feat(fediverse): NodeInfo 2.1 + per-note replies collection

NodeInfo (/.well-known/nodeinfo + /nodeinfo/2.1) so fediverse tools recognise the
instance (software 'klonkt', user/post counts). Notes now carry a 'replies'
OrderedCollection (/ap/notes/:id/replies) listing inbound + our own reply note
URIs, so remote servers can fetch the whole thread.

Co-Authored-By: Claude <noreply@…>

Location:
src
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r5a6a457 rd7526bd  
    1212 */
    1313import express from 'express';
     14import { readFileSync } from 'fs';
    1415import db from '../config/database.js';
    1516import AP from '../services/ActivityPubService.js';
    1617
    1718const router = express.Router();
     19let _ver = '1.0.0';
     20try { _ver = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url))).version || _ver; } catch { /* keep default */ }
    1821
    1922const baseUrl = (req) => (process.env.PUBLIC_BASE_URL || `${req.protocol}://${req.get('host')}`).replace(/\/+$/, '');
     
    8588});
    8689
     90// ── Replies collection ── lets remote servers fetch a post's whole thread.
     91router.get('/ap/notes/:id/replies', (req, res) => {
     92  const base = baseUrl(req);
     93  const items = AP.getReplyUris(base, req.params.id);
     94  AP.sendAP(res, {
     95    '@context': 'https://www.w3.org/ns/activitystreams',
     96    id: `${base}/ap/notes/${req.params.id}/replies`,
     97    type: 'OrderedCollection',
     98    totalItems: items.length,
     99    orderedItems: items,
     100  });
     101});
     102
     103// ── NodeInfo ── standard instance metadata so fediverse tools recognise Klonkt.
     104router.get('/.well-known/nodeinfo', (req, res) => {
     105  res.type('application/json');
     106  res.set('Cache-Control', 'public, max-age=3600');
     107  res.send(JSON.stringify({ links: [{ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', href: `${baseUrl(req)}/nodeinfo/2.1` }] }));
     108});
     109router.get('/nodeinfo/2.1', (req, res) => {
     110  let users = 0; let posts = 0;
     111  try { users = db.prepare('SELECT COUNT(*) c FROM users').get().c; } catch { /* */ }
     112  try { posts = db.prepare("SELECT COUNT(*) c FROM posts WHERE status = 'published'").get().c; } catch { /* */ }
     113  res.type('application/json; charset=utf-8');
     114  res.set('Cache-Control', 'public, max-age=600');
     115  res.send(JSON.stringify({
     116    version: '2.1',
     117    software: { name: 'klonkt', version: _ver, repository: 'https://github.com/roboburr/klonkt' },
     118    protocols: ['activitypub'],
     119    services: { inbound: [], outbound: [] },
     120    openRegistrations: false,
     121    usage: { users: { total: users }, localPosts: posts },
     122    metadata: { nodeName: 'Klonkt' },
     123  }));
     124});
     125
    87126// ── Inbox — Follow→Accept, Undo Follow (best-effort signature verify) ──
    88127const apJson = express.json({
  • src/services/ActivityPubService.js

    r5a6a457 rd7526bd  
    153153    cc: [`${aId}/followers`],
    154154    tag: Array.isArray(post.tags) ? post.tags.map((t) => ({ type: 'Hashtag', name: '#' + String(t).replace(/\s+/g, '') })) : [],
     155    replies: `${id}/replies`,
    155156  };
    156157  if (attachment.length) note.attachment = attachment;
    157158  return note;
     159}
     160
     161// All reply note URIs on a local post (inbound fediverse replies + our own
     162// outbound replies) — backs the Note's `replies` Collection so remote servers
     163// can fetch the whole thread.
     164export function getReplyUris(base, postId) {
     165  const out = [];
     166  try {
     167    for (const r of db.prepare("SELECT object_uri FROM ap_interactions WHERE kind = 'reply' AND post_id = ? AND object_uri != '' ORDER BY created_at").all(postId)) out.push(r.object_uri);
     168    for (const r of db.prepare('SELECT id FROM ap_outbox WHERE post_id = ? ORDER BY rowid').all(postId)) out.push(`${base}/ap/notes/${r.id}`);
     169  } catch { /* non-fatal */ }
     170  return out;
    158171}
    159172
     
    906919  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
    907920  deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
     921  getReplyUris,
    908922};
Note: See TracChangeset for help on using the changeset viewer.