Changeset 2c22bb5 in Klonkt
- Timestamp:
- 06/26/2026 08:53:42 AM (2 months ago)
- Branches:
- main
- Children:
- 46f3dd6
- Parents:
- 49b1b39
- Location:
- src
- Files:
-
- 4 edited
-
middleware/render.js (modified) (1 diff)
-
routes/circle.js (modified) (2 diffs)
-
services/ActivityPubService.js (modified) (2 diffs)
-
views/partials/view-switcher.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/middleware/render.js
r49b1b39 r2c22bb5 123 123 canManageFedi, 124 124 apEnabled: apEnabled(), 125 // Cirkel = the artists you feature (auto-boost). Shown when AP is on and you 126 // auto-boost ≥1 account, or (legacy) on a circle-tenancy site. 127 hasCirkel: !!(_site && ((apEnabled() && ActivityPubService.autoBoostCount(_site.slug) > 0) || (res.locals.tenancy || 'solo') === 'circle')), 125 128 isViewer: _isViewer, 126 129 canMutate: !_isViewer, -
src/routes/circle.js
r49b1b39 r2c22bb5 10 10 import { renderPage } from '../middleware/render.js'; 11 11 import db from '../config/database.js'; 12 import { getTenancy } from '../services/SettingsService.js'; 12 import { getTenancy, apEnabled } from '../services/SettingsService.js'; 13 import ActivityPubService from '../services/ActivityPubService.js'; 13 14 14 15 const router = express.Router(); 16 17 function htmlToText(html) { 18 return String(html || '').replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\s+/g, ' ').trim(); 19 } 15 20 16 21 function safeUrl(u) { … … 26 31 27 32 // ── 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). 28 37 router.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 30 44 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 = []; 39 46 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 } 59 70 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 } 74 102 75 103 renderPage(req, res, 'pages/circle-feed', { pageTitle: 'Cirkel', bodyClass: 'on-cirkel', posts, sites }); -
src/services/ActivityPubService.js
r49b1b39 r2c22bb5 1032 1032 export function getTimeline(slug, limit) { return tlStmts().list.all(slug, limit || 50); } 1033 1033 1034 // ── Cirkel = posts from the accounts you auto-boost ("feature an artist") ── 1035 let _abCount, _cirkelPosts, _cirkelMembers; 1036 export function autoBoostCount(slug) { 1037 try { if (!_abCount) _abCount = db.prepare('SELECT COUNT(*) AS n FROM ap_following WHERE slug = ? AND auto_boost = 1'); return _abCount.get(slug).n; } catch { return 0; } 1038 } 1039 export function getCirkelPosts(slug, limit) { 1040 try { 1041 if (!_cirkelPosts) _cirkelPosts = db.prepare(` 1042 SELECT t.id, t.author_uri, t.author_name, t.author_handle, t.author_icon, t.author_url, 1043 t.content, t.url, t.published, t.media_json 1044 FROM ap_timeline t 1045 JOIN ap_following f ON f.slug = t.slug AND f.actor_uri = t.author_uri AND f.auto_boost = 1 1046 WHERE t.slug = ? 1047 ORDER BY COALESCE(t.published, t.created_at) DESC, t.rowid DESC 1048 LIMIT ?`); 1049 return _cirkelPosts.all(slug, limit || 60); 1050 } catch { return []; } 1051 } 1052 export function getCirkelMembers(slug) { 1053 try { if (!_cirkelMembers) _cirkelMembers = db.prepare('SELECT name, url, icon FROM ap_following WHERE slug = ? AND auto_boost = 1 ORDER BY name'); return _cirkelMembers.all(slug); } catch { return []; } 1054 } 1055 1034 1056 // Follow a fediverse account by @handle (WebFinger → actor → signed Follow). 1035 1057 export async function followActor(site, handle, autoBoost = false) { … … 1190 1212 listOutbox, deliverOutboxDelete, 1191 1213 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, getTimeline, sendInteraction, 1214 autoBoostCount, getCirkelPosts, getCirkelMembers, 1192 1215 getNotifications, listBlocks, isBlockedAny, blockTarget, unblock, 1193 1216 deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker, -
src/views/partials/view-switcher.ejs
r49b1b39 r2c22bb5 23 23 </div> 24 24 <% } %> 25 <% if (typeof tenancy !== 'undefined' && tenancy === 'circle') {25 <% if (typeof hasCirkel !== 'undefined' && hasCirkel) { 26 26 var _onCirkel = (typeof bodyClass === 'string' && bodyClass.indexOf('on-cirkel') >= 0); 27 27 var _sb = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
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)