source: Klonkt/src/routes/circle.js@ d5aa7c8

main
Last change on this file since d5aa7c8 was b7d4458, checked in by Robin Genis <roboburr@…>, 2 months ago

feat(nsfw): custom content-warning text + blur in the Cirkel

  • Per-post content-warning text (like Mastodon): editor field; used as the on-site veil/banner label and the fediverse Note summary (falls back to 'Gevoelige inhoud').
  • Cirkel feed now blurs remote sensitive posts: ap_timeline gets nsfw+cw (captured from the incoming Note's sensitive/summary), getCirkelPosts returns them, circle.js maps them so post-card/tile show the veil.
  • Property mode set to 100644
File size: 2.8 KB
RevLine 
[0091cb7]1/**
[8ef8c3e]2 * Circle feed — the artists this site features (auto-boosts), sourced from
3 * ActivityPub. Cards link to the source post. Available whenever the site
4 * auto-boosts at least one account; otherwise next() -> postsRoutes.
5 * GET /cirkel
[0091cb7]6 */
7
8import express from 'express';
9import { renderPage } from '../middleware/render.js';
[8ef8c3e]10import { apEnabled } from '../services/SettingsService.js';
[2c22bb5]11import ActivityPubService from '../services/ActivityPubService.js';
[0091cb7]12
13const router = express.Router();
14
15function safeUrl(u) {
16 return typeof u === 'string' && /^https?:\/\//i.test(u) ? u : null;
17}
18function safeJson(s) {
19 try { return s ? JSON.parse(s) : []; } catch { return []; }
20}
[7917407]21function mediaImage(media_json) {
22 const media = safeJson(media_json).map((m) => ({ ...m, url: safeUrl(m.url) })).filter((m) => m.url);
[8ef8c3e]23 return media.find((m) => /image/i.test(m.type || '')) || media[0] || null;
24}
25function htmlToText(html) {
26 return String(html || '').replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\s+/g, ' ').trim();
[7917407]27}
[0091cb7]28
29router.get('/cirkel', (req, res, next) => {
[2c22bb5]30 const site = res.locals.site;
[5045c30]31 if (!site || !apEnabled() || (ActivityPubService.autoBoostCount(site.slug) === 0 && ActivityPubService.boostedCount(site.slug) === 0)) return next();
[8ef8c3e]32
33 const posts = ActivityPubService.getCirkelPosts(site.slug, 80).map((r) => {
34 const text = htmlToText(r.content);
[cf4cf27]35 // Show ONLY the title (the bold first line a Klonkt note carries), not the whole
36 // body. Title-less notes (e.g. plain Mastodon) fall back to a short text snippet.
37 const titleM = (r.content || '').match(/^\s*<p>\s*<strong>([\s\S]*?)<\/strong>/i);
38 const realTitle = titleM ? htmlToText(titleM[1]).trim() : '';
[8ef8c3e]39 const image = mediaImage(r.media_json);
40 const name = r.author_name || r.author_handle || 'Onbekend';
41 return {
42 id: 'ap-' + r.id,
43 slug: '',
[cf4cf27]44 title: realTitle
45 ? (realTitle.length > 90 ? realTitle.slice(0, 90) + '…' : realTitle)
46 : (text ? (text.length > 90 ? text.slice(0, 90) + '…' : text) : name),
[8ef8c3e]47 excerpt: '',
48 cover_image_url: image ? image.url : null,
49 published_at: r.published,
50 created_at: r.published,
51 type: 'post',
52 tags: '',
53 pinned: 0,
[c7c7f02]54 isBoost: !!r.boosted, // a post YOU boosted → render in the pinned style with a Boost badge
[b7d4458]55 nsfw: r.nsfw ? 1 : 0, // remote sensitive post → blur in the Cirkel (post-card/tile)
56 content_warning: r.cw || '',
[8ef8c3e]57 status: 'published',
58 source_name: name,
59 external_url: safeUrl(r.url),
60 };
61 });
[0091cb7]62
[8ef8c3e]63 const sites = ActivityPubService.getCirkelMembers(site.slug)
64 .map((s) => ({ name: s.name || 'Onbekend', url: safeUrl(s.url), avatar: safeUrl(s.icon) }));
[8ea3d0d]65
66 renderPage(req, res, 'pages/circle-feed', { pageTitle: 'Cirkel', bodyClass: 'on-cirkel', posts, sites });
[0091cb7]67});
68
69export default router;
Note: See TracBrowser for help on using the repository browser.