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

main
Last change on this file since eac3229 was eac3229, checked in by Robin <roboburr@…>, 4 weeks ago

/cirkel viel om op een media_json die geen array was

boiert.eu gaf 500 op /cirkel terwijl de voorpagina het deed. De catch in
safeJson ving KAPOTTE json af, maar niet geldige json van het verkeerde TYPE:
een remote server stuurde media_json = "[]" -- een STRING met daarin [] -- en
JSON.parse geeft dan netjes een string terug. Een string heeft geen .map:

TypeError: safeJson(...).map is not a function

at coverMedia (src/routes/circle.js:25)

Een enkele post nam zo de hele pagina mee, en beide paden (met en zonder
?append=1) omdat ze dezelfde mapping delen.

DEZE FOUT WAS AL EENS GEVONDEN EN GEREPAREERD, in views/partials/note-body.ejs,
met de reden erbij: "drie posts van een remote server droegen media_json = '[]'
[...] en daarmee lag de hele Krant plat: een vreemde note nam de pagina mee. Wat
er binnenkomt is niet van ons, dus de vorm moet hier afgedwongen worden en niet
aangenomen." Diezelfde les was in circle.js nooit toegepast -- zelfde data,
andere route.

De drie andere plekken die media_json parsen (ActivityPubService 496, 881, 4659)
dwingen de vorm wel af; die zijn nagelopen en blijven zoals ze zijn.

GEVONDEN ZONDER TOEGANG tot die machine. /cirkel?offset=999999 gaf 200, dus een
lege pagina rendert prima en de fout zat in een RIJ. Daarna van buitenaf
uitgebinaird over ?offset=: rij 19 van de eerste 72. Robin leverde daarna de
stacktrace, die precies dat bevestigde.

Vier tests, met de vorm die het omver haalde als eerste. Gecontroleerd dat ze
bijten: reparatie eruit -> 0 groen, 4 rood; terug -> 4 groen.

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

  • Property mode set to 100644
File size: 5.0 KB
Line 
1/**
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
6 */
7
8import express from 'express';
9import { renderPage } from '../middleware/render.js';
10import { apEnabled } from '../services/SettingsService.js';
11import ActivityPubService from '../services/ActivityPubService.js';
12
13const router = express.Router();
14
15function safeUrl(u) {
16 return typeof u === 'string' && /^https?:\/\//i.test(u) ? u : null;
17}
18/**
19 * media_json als ARRAY, of leeg.
20 *
21 * De catch vangt KAPOTTE json; hij ving niet geldige json van het verkeerde
22 * TYPE. Een remote server stuurde media_json = "[]" -- een string MET daarin
23 * `[]` -- en JSON.parse geeft dan netjes een string terug. Een string heeft
24 * geen .map, en daarmee lag /cirkel op boiert.eu plat: een 500 op de hele
25 * pagina door een enkele post (rij 19 van 72).
26 *
27 * Precies dezelfde fout stond al beschreven en gerepareerd in
28 * views/partials/note-body.ejs, waar drie vreemde notes de Krant meenamen.
29 * Die reparatie is hier nooit gekomen -- zelfde data, andere route. Wat er
30 * binnenkomt is niet van ons, dus de vorm hoort afgedwongen en niet aangenomen.
31 */
32function safeJson(s) {
33 try {
34 const v = s ? JSON.parse(s) : [];
35 return Array.isArray(v) ? v : [];
36 } catch { return []; }
37}
38// The cover image + (separately) a cover video from a remote note's media. NEVER use a video/audio
39// item as the cover image — that produced a broken <img> for an animated cover that federated as an
40// MP4 (the video becomes a <video> instead).
41function coverMedia(media_json) {
42 const media = safeJson(media_json).map((m) => ({ ...m, url: safeUrl(m.url) })).filter((m) => m.url);
43 const video = media.find((m) => /video/i.test(m.type || '')) || null;
44 const image = media.find((m) => /image/i.test(m.type || ''))
45 || (media[0] && !/(video|audio)/i.test(media[0].type || '') ? media[0] : null);
46 return { image, video };
47}
48function htmlToText(html) {
49 return String(html || '').replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\s+/g, ' ').trim();
50}
51// Tidy a plain-text snippet for use as a card title: drop a leading "RE: <url>" (the
52// quote/reply prefix Misskey/Akkoma and some reply federation prepend) and any other
53// leading bare URL, so the title shows the actual prose, not link noise.
54function tidySnippet(text) {
55 return String(text || '')
56 .replace(/^RE:\s*https?:\/\/\S+\s*/i, '')
57 .replace(/^https?:\/\/\S+\s*/i, '')
58 .trim();
59}
60
61const CIRKEL_PAGE = 72; // matches FEED_PAGE in posts.js: divisible by 2/3/4
62
63router.get('/cirkel', (req, res, next) => {
64 const site = res.locals.site;
65 if (!site || !apEnabled() || (ActivityPubService.autoBoostCount(site.slug) === 0 && ActivityPubService.boostedCount(site.slug) === 0)) return next();
66
67 const append = req.query.append === '1';
68 const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
69 const rows = ActivityPubService.getCirkelPosts(site.slug, CIRKEL_PAGE + 1, offset);
70 const hasMore = rows.length > CIRKEL_PAGE;
71 const posts = rows.slice(0, CIRKEL_PAGE).map((r) => {
72 const text = tidySnippet(htmlToText(r.content));
73 // Show ONLY the title (the bold first line a Klonkt note carries), not the whole
74 // body. Title-less notes (e.g. plain Mastodon) fall back to a short text snippet.
75 const titleM = (r.content || '').match(/^\s*<p>\s*<strong>([\s\S]*?)<\/strong>/i);
76 const realTitle = titleM ? htmlToText(titleM[1]).trim() : '';
77 const cover = coverMedia(r.media_json);
78 const name = r.author_name || r.author_handle || 'Onbekend';
79 return {
80 id: 'ap-' + r.id,
81 slug: '',
82 title: realTitle
83 ? (realTitle.length > 90 ? realTitle.slice(0, 90) + '…' : realTitle)
84 : (text ? (text.length > 90 ? text.slice(0, 90) + '…' : text) : name),
85 excerpt: '',
86 cover_image_url: cover.image ? cover.image.url : null,
87 cover_video_url: cover.video ? cover.video.url : null,
88 published_at: r.published,
89 created_at: r.published,
90 type: 'post',
91 tags: '',
92 pinned: 0,
93 isBoost: !!r.boosted, // a post YOU boosted → render in the pinned style with a Boost badge
94 nsfw: r.nsfw ? 1 : 0, // remote sensitive post → blur in the Cirkel (post-card/tile)
95 content_warning: r.cw || '',
96 status: 'published',
97 source_name: name,
98 external_url: safeUrl(r.url),
99 };
100 });
101
102 const moreBase = res.locals.siteUrlBase || '';
103 if (append) {
104 return renderPage(req, res, 'partials/home-append', { posts, hasMore, nextOffset: offset + CIRKEL_PAGE, moreBase, morePath: '/cirkel' });
105 }
106
107 const sites = ActivityPubService.getCirkelMembers(site.slug)
108 .map((s) => ({ name: s.name || 'Onbekend', url: safeUrl(s.url), avatar: safeUrl(s.icon) }));
109
110 renderPage(req, res, 'pages/circle-feed', {
111 pageTitle: 'Cirkel', bodyClass: 'on-cirkel', posts, sites,
112 hasMore, nextOffset: offset + CIRKEL_PAGE, moreBase,
113 });
114});
115
116export default router;
Note: See TracBrowser for help on using the repository browser.