Changeset 2c22bb5 in Klonkt for src/routes


Ignore:
Timestamp:
06/26/2026 08:53:42 AM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
46f3dd6
Parents:
49b1b39
Message:

feat(cirkel): rebuild the Cirkel feed on ActivityPub (featured/auto-boosted artists)

Phase 2 of the Cirkels-on-AP rework. /cirkel now shows posts from the accounts a
site auto-boosts ("feature an artist"), sourced from ap_timeline ⋈ ap_following
(auto_boost=1); cards link to the source post. The Solo|Cirkel switcher shows
whenever the site has a cirkel (new hasCirkel local) instead of only on
circle-tenancy. Legacy circle-tenancy remote_posts are merged in until the old
pull-protocol is removed (Phase 4). New: autoBoostCount / getCirkelPosts /
getCirkelMembers.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/circle.js

    r49b1b39 r2c22bb5  
    1010import { renderPage } from '../middleware/render.js';
    1111import db from '../config/database.js';
    12 import { getTenancy } from '../services/SettingsService.js';
     12import { getTenancy, apEnabled } from '../services/SettingsService.js';
     13import ActivityPubService from '../services/ActivityPubService.js';
    1314
    1415const router = express.Router();
     16
     17function htmlToText(html) {
     18  return String(html || '').replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\s+/g, ' ').trim();
     19}
    1520
    1621function safeUrl(u) {
     
    2631
    2732// ── Overview ─────────────────────────────────────────────────
     33// New model: the Cirkel = posts from the accounts this site auto-boosts
     34// ("feature an artist"), sourced from ActivityPub. Cards link to the source
     35// post (external_url). Legacy circle-tenancy remote_posts are merged in until
     36// the old pull-protocol is removed (Phase 4).
    2837router.get('/cirkel', (req, res, next) => {
    29   if (getTenancy() !== 'circle') return next();
     38  const site = res.locals.site;
     39  if (!site) return next();
     40  const slug = site.slug;
     41  const isCircle = getTenancy() === 'circle';
     42  const abCount = apEnabled() ? ActivityPubService.autoBoostCount(slug) : 0;
     43  if (!abCount && !isCircle) return next(); // no cirkel on this site
    3044
    31   const rows = db.prepare(`
    32     SELECT p.id, p.published, p.title, p.summary, p.media_json, p.tags,
    33            a.name AS actor_name
    34     FROM remote_posts p
    35     JOIN remote_actors a ON a.id = p.actor_id
    36     ORDER BY COALESCE(p.published, p.fetched_at) DESC
    37     LIMIT 100
    38   `).all();
     45  let posts = [];
    3946
    40   const posts = rows.map((r) => {
    41     const image = mediaImage(r.media_json);
    42     return {
    43       id: r.id,
    44       // Local reading page -> the card stays on the own site (post-card links
    45       // locally + htmx, NO external_url).
    46       slug: 'cirkel/' + encodeURIComponent(r.id),
    47       title: r.title || '(zonder titel)',
    48       excerpt: r.summary || '',
    49       cover_image_url: image ? image.url : null,
    50       published_at: r.published,
    51       created_at: r.published,
    52       type: 'post',
    53       tags: r.tags || '',
    54       pinned: 0,
    55       status: 'published',
    56       source_name: r.actor_name || 'Onbekend',
    57     };
    58   });
     47  // Featured (auto-boosted) fediverse posts → link to the original.
     48  if (abCount) {
     49    posts = ActivityPubService.getCirkelPosts(slug, 80).map((r) => {
     50      const text = htmlToText(r.content);
     51      const image = mediaImage(r.media_json);
     52      const name = r.author_name || r.author_handle || 'Onbekend';
     53      return {
     54        id: 'ap-' + r.id,
     55        slug: '',
     56        title: text ? (text.length > 90 ? text.slice(0, 90) + '…' : text) : name,
     57        excerpt: '',
     58        cover_image_url: image ? image.url : null,
     59        published_at: r.published,
     60        created_at: r.published,
     61        type: 'post',
     62        tags: '',
     63        pinned: 0,
     64        status: 'published',
     65        source_name: name,
     66        external_url: safeUrl(r.url),
     67      };
     68    });
     69  }
    5970
    60   // Sites in the circle — active links only (outdated/error ones are excluded, along
    61   // with their posts). Used for the graphic header with avatars.
    62   const sites = db.prepare(`
    63     SELECT a.name, a.url, a.avatar
    64     FROM remote_actors a
    65     JOIN circle_links l ON l.remote_actor_id = a.id
    66     WHERE l.status = 'active'
    67     ORDER BY a.name
    68   `).all()
    69     .map((s) => ({
    70       name: s.name || 'Onbekend',
    71       url: safeUrl(s.url),
    72       avatar: safeUrl(s.avatar),
    73     }));
     71  // Legacy circle-tenancy remote_posts (local reading page, no external_url).
     72  if (isCircle) {
     73    const rows = db.prepare(`
     74      SELECT p.id, p.published, p.title, p.summary, p.media_json, p.tags, a.name AS actor_name
     75      FROM remote_posts p JOIN remote_actors a ON a.id = p.actor_id
     76      ORDER BY COALESCE(p.published, p.fetched_at) DESC LIMIT 100
     77    `).all().map((r) => {
     78      const image = mediaImage(r.media_json);
     79      return {
     80        id: r.id, slug: 'cirkel/' + encodeURIComponent(r.id),
     81        title: r.title || '(zonder titel)', excerpt: r.summary || '',
     82        cover_image_url: image ? image.url : null, published_at: r.published, created_at: r.published,
     83        type: 'post', tags: r.tags || '', pinned: 0, status: 'published', source_name: r.actor_name || 'Onbekend',
     84      };
     85    });
     86    posts = posts.concat(rows);
     87  }
     88
     89  posts.sort((a, b) => String(b.published_at || '').localeCompare(String(a.published_at || '')));
     90
     91  // Members header (avatars): featured artists + legacy circle links.
     92  let sites = abCount
     93    ? ActivityPubService.getCirkelMembers(slug).map((s) => ({ name: s.name || 'Onbekend', url: safeUrl(s.url), avatar: safeUrl(s.icon) }))
     94    : [];
     95  if (isCircle) {
     96    const old = db.prepare(`
     97      SELECT a.name, a.url, a.avatar FROM remote_actors a
     98      JOIN circle_links l ON l.remote_actor_id = a.id WHERE l.status = 'active' ORDER BY a.name
     99    `).all().map((s) => ({ name: s.name || 'Onbekend', url: safeUrl(s.url), avatar: safeUrl(s.avatar) }));
     100    sites = sites.concat(old);
     101  }
    74102
    75103  renderPage(req, res, 'pages/circle-feed', { pageTitle: 'Cirkel', bodyClass: 'on-cirkel', posts, sites });
Note: See TracChangeset for help on using the changeset viewer.