Changeset 75bda38 in Klonkt


Ignore:
Timestamp:
06/25/2026 06:54:59 AM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
f1e0c1f
Parents:
abb34d2
Message:

feat(fediverse): expose pinned posts via the actor's featured collection

Adds actor.featured + GET /ap/users/:slug/featured (OrderedCollection of pinned,
published, non-fan_only posts as Notes, ordered by pin rank). Mastodon reads this
and shows them under the profile's Featured tab.

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

Location:
src
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    rabb34d2 r75bda38  
    66 *   GET /ap/users/:slug/outbox     OrderedCollection of Create(Note)
    77 *   GET /ap/users/:slug/followers  count-only OrderedCollection
     8 *   GET /ap/users/:slug/featured   pinned posts (Mastodon "Featured" tab)
    89 *   GET /ap/notes/:id              a single Note
    910 *   POST /ap/users/:slug/inbox, /ap/inbox  → 202 (Follow/Accept + signature verify: next step)
     
    7273});
    7374
     75// ── Featured (pinned posts → Mastodon "Featured" tab) ─────────────
     76router.get('/ap/users/:slug/featured', (req, res) => {
     77  const site = publicSite(req.params.slug);
     78  if (!site) return res.status(404).end();
     79  const posts = db.prepare(
     80    `SELECT id, slug, title, content, cover_image_url, published_at, created_at
     81     FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)
     82       AND pinned IS NOT NULL AND pinned > 0
     83     ORDER BY pinned ASC, COALESCE(published_at, created_at) DESC LIMIT 20`
     84  ).all(site.id);
     85  AP.sendAP(res, AP.buildFeatured(baseUrl(req), site, posts));
     86});
     87
    7488// ── Note ──────────────────────────────────────────────────────────
    7589router.get('/ap/notes/:id', (req, res) => {
  • src/services/ActivityPubService.js

    rabb34d2 r75bda38  
    8383    outbox: `${id}/outbox`,
    8484    followers: `${id}/followers`,
     85    featured: `${id}/featured`,
    8586    endpoints: { sharedInbox: `${base}/ap/inbox` },
    8687    publicKey: {
     
    240241    totalItems: count || 0,
    241242    orderedItems: [], // hidden for privacy; count only
     243  };
     244}
     245
     246// Pinned posts → the actor's `featured` collection. Mastodon reads this and shows
     247// these as the "Featured" tab (pinned to the profile). Posts come ordered by pin
     248// rank; embedded as full Notes so a remote server doesn't need extra fetches.
     249export function buildFeatured(base, site, posts) {
     250  const id = `${actorId(base, site.slug)}/featured`;
     251  const items = (posts || []).map((p) => buildNote(base, site, p));
     252  return {
     253    '@context': 'https://www.w3.org/ns/activitystreams',
     254    id,
     255    type: 'OrderedCollection',
     256    totalItems: items.length,
     257    orderedItems: items,
    242258  };
    243259}
     
    968984export default {
    969985  getOrCreateKeys, apWants, sendAP, actorId, noteId,
    970   buildActor, buildNote, buildCreate, buildOutbox, buildFollowers,
     986  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFeatured,
    971987  followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate,
    972988  getInteractions, getInteractionById, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
Note: See TracChangeset for help on using the changeset viewer.