Changeset bdcb3a3 in Klonkt
- Timestamp:
- 08/08/2026 04:35:20 AM (5 weeks ago)
- Branches:
- main
- Children:
- 39a9d21
- Parents:
- 553bd70
- git-author:
- Robin <roboburr@…> (08/08/2026 04:34:22 AM)
- git-committer:
- Robin <roboburr@…> (08/08/2026 04:35:20 AM)
- Files:
-
- 1 added
- 1 edited
-
src/services/ActivityPubService.js (modified) (7 diffs)
-
test/channel-actor.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r553bd70 rbdcb3a3 56 56 // Question stays valid JSON-LD (a strict processor would otherwise drop votersCount). 57 57 votersCount: 'toot:votersCount', 58 // Kanaal-vocabulaire (shaer-0nh). Funkwhale declareert `category` niet 59 // inline maar via zijn eigen remote context https://funkwhale.audio/ns, en 60 // die host is vanaf hier onbereikbaar -- de IRI hieronder is dus AFGELEID 61 // en niet geverifieerd. Wat vandaag telt voor interop is de JSON-sleutel, 62 // want daar matchen lezers op; de declaratie zorgt alleen dat een strikte 63 // JSON-LD-processor hem niet laat vallen. Nakijken zodra die host weer 64 // antwoordt. 65 category: { '@id': 'https://funkwhale.audio/ns#category' }, 58 66 // FEP-633c (Guardians): the shaer namespace, owned by the guardianship 59 67 // module (src/services/guardianship/). … … 173 181 export function noteId(base, postId) { return `${base}/ap/notes/${encodeURIComponent(postId)}`; } 174 182 183 /** Eén Link uit een AS2 `url` kiezen op mediaType. Een `url` mag een string, 184 * een Link of een array van beide zijn; dit is de enige plek die dat weet. */ 185 function pickLink(url, test) { 186 const links = Array.isArray(url) ? url : (url ? [url] : []); 187 for (const l of links) { 188 const href = safeUrl(typeof l === 'string' ? l : (l && l.href)); 189 const mt = (l && typeof l === 'object' && l.mediaType) || ''; 190 if (href && test(mt)) return { href, mediaType: mt }; 191 } 192 return null; 193 } 194 195 /** 196 * De `url` van de actor als kanaal (shaer-0nh): de webpagina en, als die er is, 197 * de RSS-feed ernaast. 198 * 199 * De RSS-link gaat er ALLEEN in voor de site waar de instance op gepind staat. 200 * Sinds hub-modus verdween serveert routes/feed.js `/feed.xml` van de primaire 201 * site en bestaat `/user/<slug>` niet meer als route; een feed-link voor een 202 * andere site zou naar de verkeerde feed wijzen. Liever een link minder dan een 203 * link die iemand anders' muziek belooft. 204 */ 205 export function channelUrls(base, site) { 206 const isPrimair = site.slug === site.primary_slug; 207 const pagina = `${base}/${isPrimair ? '' : 'user/' + encodeURIComponent(site.slug)}`; 208 const uit = [{ type: 'Link', href: pagina, mediaType: 'text/html' }]; 209 if (isPrimair) uit.push({ type: 'Link', href: `${base}/feed.xml`, mediaType: 'application/rss+xml' }); 210 return uit; 211 } 212 213 /** 214 * `category` is kanaal-vocabulaire, en de waarde is 'music' (Robins keuze, 7-8). 215 * Alleen gezet als de site ECHT audio publiceert: een blog zonder muziek als 216 * muziekkanaal aankondigen is erger dan geen label. Het signaal is een track in 217 * de kast, niet enable_audio_player -- die staat standaard aan en zegt niets. 218 */ 219 function channelCategory(site) { 220 try { 221 return db.prepare('SELECT 1 FROM audio_tracks WHERE site_id = ? LIMIT 1').get(site.id) ? 'music' : null; 222 } catch { return null; } 223 } 224 225 /** 226 * Welke objectsoorten deze inbox in de tijdlijn opneemt. 227 * 228 * `Audio` staat erbij sinds de kanaalbeslissing (shaer-0nh): een Funkwhale- 229 * kanaal stuurt Create(Audio), geen Note. Uitbreiden gebeurt HIER en in 230 * timelineFields -- en uitdrukkelijk NIET door vreemde soorten tot Note om te 231 * vormen. Een Audio is geen Note, en die soort willen we kunnen blijven zien. 232 */ 233 const TIJDLIJN_SOORTEN = new Set(['Note', 'Article', 'Question', 'Audio']); 234 235 /** 236 * Wat de tijdlijn van een binnengekomen object nodig heeft, PER SOORT: de 237 * inhoud-HTML, de bijlagen voor media_json, en de link van het item. 238 * 239 * Eén plek, zodat een nieuwe soort erbij een tak is en geen speurtocht. De 240 * Krant rendert media_json al naar soort -- audio/* wordt een speler -- dus een 241 * track komt vanzelf als echte speler binnen zonder dat de weergave iets van 242 * Funkwhale hoeft te weten. 243 */ 244 export function timelineFields(o) { 245 // De hoes: een `image` op het object. Bij een Note alleen als terugval (daar 246 // is het de kaart-afbeelding van een player-post), bij een Audio altijd, 247 // want daar IS het de albumhoes. 248 const hoes = () => { 249 if (!o.image) return null; 250 const im = Array.isArray(o.image) ? o.image[0] : o.image; 251 const iu = safeUrl(typeof im === 'string' ? im : (im && im.url)); 252 return iu ? { url: iu, type: (im && im.mediaType) || 'image/jpeg' } : null; 253 }; 254 255 if (o.type === 'Audio') { 256 const geluid = pickLink(o.url, (mt) => /^audio\//i.test(mt)); 257 // De webpagina van de track. Zonder mediaType is dat de veilige aanname: 258 // er een speler op zetten zou een HTML-pagina als geluid aanbieden. 259 const pagina = pickLink(o.url, (mt) => /^text\/html/i.test(mt)) || pickLink(o.url, (mt) => !mt); 260 const atts = []; 261 const h = hoes(); if (h) atts.push(h); // eerst kijken, dan luisteren 262 if (geluid) atts.push({ url: geluid.href, type: geluid.mediaType || 'audio/mpeg' }); 263 // Een Audio heeft geen `content`; de titel is wat er te lezen valt. Door de 264 // sanitizer, want hij komt van een vreemde server. 265 return { 266 html: o.name ? HtmlSanitizerService.sanitize(`<p>${o.name}</p>`) : '', 267 atts, 268 url: pagina ? pagina.href : null, 269 }; 270 } 271 272 // Note / Article / Question -- ongewijzigd gedrag. 273 const atts = (Array.isArray(o.attachment) ? o.attachment : []) 274 .map((a) => ({ url: safeUrl(a && a.url), type: (a && a.mediaType) || '' })) 275 .filter((m) => m.url); 276 if (!atts.some((m) => !m.type || /image/i.test(m.type))) { 277 const h = hoes(); if (h) atts.push(h); 278 } 279 const pagina = pickLink(o.url, () => true); 280 return { html: HtmlSanitizerService.sanitize(o.content || ''), atts, url: pagina ? pagina.href : null }; 281 } 282 175 283 export function buildActor(base, site) { 176 284 const id = actorId(base, site.slug); … … 187 295 name: site.title || site.slug, 188 296 summary: site.tagline || site.description || '', 189 url: `${base}/${site.slug === site.primary_slug ? '' : 'user/' + encodeURIComponent(site.slug)}`, 297 // Een Link-ARRAY in plaats van een kale string (shaer-0nh): zo adverteert 298 // een kanaal zichzelf, en zo vindt een podcast-app de feed. De text/html 299 // staat VOORAAN, want een lezer die maar één url verwacht pakt de eerste -- 300 // dezelfde vorm die Funkwhale in productie met Mastodon uitwisselt. 301 url: channelUrls(base, site), 302 ...(channelCategory(site) ? { category: channelCategory(site) } : {}), 190 303 manuallyApprovesFollowers: isWard, 191 304 discoverable: true, … … 2194 2307 2195 2308 // Inbound reply: a Create whose object replies to one of our notes (post OR comment). 2196 if (type === 'Create' && act.object && (act.object.type === 'Note' || act.object.type === 'Article' || act.object.type === 'Question')) {2309 if (type === 'Create' && act.object && TIJDLIJN_SOORTEN.has(act.object.type)) { 2197 2310 const o = act.object; 2198 2311 // A poll ballot: a Note carrying a `name` (the chosen option) inReplyTo one of OUR poll … … 2247 2360 if (subs.length) { 2248 2361 const ai = actorInfo(await resolveActor(actorUri), actorUri); 2249 const html = HtmlSanitizerService.sanitize(o.content || ''); 2250 const _atts = (Array.isArray(o.attachment) ? o.attachment : []).map((a) => ({ url: safeUrl(a && a.url), type: (a && a.mediaType) || '' })).filter((m) => m.url); 2251 // Fallback cover: a Note's `image` (set when the attachment was suppressed 2252 // for a player-card post, e.g. hosted-audio posts). 2253 if (!_atts.some((m) => !m.type || /image/i.test(m.type)) && o.image) { 2254 const _im = Array.isArray(o.image) ? o.image[0] : o.image; 2255 const _iu = safeUrl(typeof _im === 'string' ? _im : (_im && _im.url)); 2256 if (_iu) _atts.push({ url: _iu, type: (_im && _im.mediaType) || 'image/jpeg' }); 2257 } 2362 const { html, atts: _atts, url: _url } = timelineFields(o); 2258 2363 const media = JSON.stringify(_atts); 2259 2364 const poll = parsePoll(o); // a Question (fediverse poll) → cache its options/counts … … 2263 2368 // the timeline). 2264 2369 for (const s of subs) { 2265 tlStmts().ins.run(o.id, s.slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.url || null, o.published || null, media, o.sensitive ? 1 : 0, o.summary || null);2370 tlStmts().ins.run(o.id, s.slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, _url, o.published || null, media, o.sensitive ? 1 : 0, o.summary || null); 2266 2371 // FEP-633c §2.2: register the ward hint on the stored object (no action yet). 2267 2372 if (Guardianship.objectHasGuardians(o)) { try { db.prepare('UPDATE ap_timeline SET has_guardians = 1 WHERE id = ? AND slug = ?').run(o.id, s.slug); } catch { /* ignore */ } } … … 5790 5895 AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions, 5791 5896 buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured, 5897 channelUrls, timelineFields, 5792 5898 buildPlaylistCollection, playlistOpenTracks, listPlaylistsAP, playlistLinkTags, 5793 5899 followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
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)