- Timestamp:
- 08/24/2026 04:01:56 PM (2 weeks ago)
- Branches:
- main
- Children:
- b05eb97
- Parents:
- df7de6e
- Location:
- src/services
- Files:
-
- 1 added
- 1 edited
-
ActivityPubService.js (modified) (3 diffs)
-
ap-c2s.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
rdf7de6e r1d5ffc0 42 42 verifyRequest, signedGetHeaders, signedGetJson, 43 43 }; 44 // Stap 4 (shaer-drc): de C2S-inname woont in ap-c2s.js. Die is een coordinator 45 // en krijgt zijn werktuigen uit de dienstlaag onderaan dit bestand via 46 // wireC2S -- de regel blijft dat een module NOOIT uit dit bestand importeert. 47 import { ingestOutboxActivity, wireC2S } from './ap-c2s.js'; 48 export { ingestOutboxActivity }; 44 49 // Doorgeven wat hier altijd vandaan kwam, zodat elke bestaande aanroep blijft werken. 45 50 export { AP_CONTEXT, actorId, noteId, guessMediaType }; … … 3146 3151 } 3147 3152 3148 // ── ActivityPub Client-to-Server: ingest an activity POSTed to the outbox ──3149 // The C2S counterpart of handleInbox: a native/web client (Shaer) posts an3150 // activity here and we translate it onto the SAME delivery machinery the web UI3151 // uses (deliverReply / sendInteraction / followActor / deliverCreate). Returns3152 // { status, id?, url?, error? }. Auth + site-ownership are checked by the route.3153 const c2sIdOf = (x) => (typeof x === 'string' ? x : (x && (x.id || x.href))) || null;3154 3155 export async function ingestOutboxActivity(site, user, activity) {3156 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');3157 if (!base || !site || !activity || typeof activity !== 'object') return { status: 400, error: 'invalid_activity' };3158 3159 // AP §6: a client MAY POST a bare object; the server wraps it in a Create.3160 let type = activity.type;3161 let object = activity.object;3162 if (type === 'Note' || type === 'Article') { object = activity; type = 'Create'; }3163 if (Array.isArray(type)) type = type.find((t) => typeof t === 'string');3164 3165 // FEP-633c: the adoption handshake (Offer/Accept/Reject on a guardianship3166 // Relationship) belongs to the guardianship module; anything else falls3167 // through to the switch below.3168 if (type === 'Offer' || type === 'Accept' || type === 'Reject') {3169 const g = await Guardianship.handleGuardianshipOutbox(site, activity).catch(() => null);3170 if (g) return g;3171 }3172 // Een gate-voorstel uit de app (5.6, shaer-8ru): een Offer van een3173 // shaer:GatedSetting, de vorm die 5.6 al beschrijft.3174 //3175 // HIER, EN GEEN `case` IN DE SWITCH. Dat was hij eerst, en die claimde ELKE3176 // Offer: wat geen gate-voorstel was kreeg 400 unsupported_offer -- ook de3177 // adoptie-handshake, en straks elke Offer-vorm die we nog toevoegen. Barts3178 // honderd aanbiedingen liepen er meteen op stuk. Alleen claimen wat je3179 // herkent, en de rest laten doorlopen.3180 if (type === 'Offer') {3181 const gs = Guardianship.gated.parseGatedSetting(activity.object);3182 if (gs) {3183 const uit = proposeGate(site, gs.ward, gs.feature, gs.value);3184 return uit.status === 200 ? { ...uit, status: 201, id: uit.offerId } : uit;3185 }3186 }3187 3188 try {3189 switch (type) {3190 case 'Create': {3191 if (!object || typeof object !== 'object') return { status: 400, error: 'missing_object' };3192 // Innamepoorten (shaer-ahy.1, 8-8): wat de ward niet mag versturen3193 // wordt HIER geweigerd, niet in de app verstopt -- een knop die de3194 // client alleen verbergt is geen poort. De reddingsboei gaat ALTIJD3195 // voor: een hulpvraag aan de guardians mag door elke dichte deur heen,3196 // anders sluit een messages-poort precies het kanaal af dat het kind3197 // veilig houdt.3198 {3199 const isWard = (() => { try { return Guardianship.listGuardians(site.slug).length > 0; } catch { return false; } })();3200 const isHelp = object['shaer:helpRequest'] === true || object.helpRequest === true;3201 // Een poortverzoek van het kind zelf (shaer-8ru) gaat langs de3202 // messages-poort. Dat lijkt een gat en is het niet: het verzoek draagt3203 // ALLEEN de naam van de feature, geen vrije tekst, dus er ontstaat geen3204 // kanaal om omheen die poort te praten. Zonder deze uitzondering kan3205 // een kind met berichten dicht nergens meer om vragen -- en dan is de3206 // hele weg dood op precies het moment dat hij nodig is.3207 const isGateReq = !!Guardianship.gatereq.parseRequest(object);3208 const direct = c2sVisibility(object) === 'direct';3209 if (!isHelp && !isGateReq) {3210 if (direct && !Guardianship.wardGateAllowed(site.gate_messages, isWard)) {3211 return { status: 403, error: 'gated_messages' };3212 }3213 if (!direct && !object.inReplyTo && !Guardianship.wardGateAllowed(site.gate_compose, isWard)) {3214 return { status: 403, error: 'gated_compose' };3215 }3216 // Meedoen aan een gesprek is ook iets (Bart, 8-8). Hier stond de3217 // aanname dat een antwoord geen eigen podium is en dus onder compose3218 // door mocht. Dat is teruggedraaid: antwoorden heeft een EIGEN poort,3219 // los van compose in beide richtingen -- je kunt willen dat een kind3220 // meepraat zonder podium, en ook andersom.3221 //3222 // Geldt ook voor een DIRECT antwoord, bovenop de messages-poort: een3223 // privé-antwoord is allebei, en dan mag allebei hem tegenhouden.3224 if (object.inReplyTo && !Guardianship.wardGateAllowed(site.gate_replies, isWard)) {3225 return { status: 403, error: 'gated_replies' };3226 }3227 }3228 }3229 // Client sends `source` (plain/markdown) + `content` (HTML). deliverReply3230 // re-escapes, so it needs plain text; a top-level post keeps sanitized HTML.3231 const plain = (object.source && object.source.content) || HtmlSanitizerService.toPlainText(object.content || '');3232 // A picture (or a recording) can be the whole message: media-only3233 // notes pass here; c2sCreatePost validates the attachments themselves.3234 if (!plain.trim() && !object.content && !(Array.isArray(object.attachment) && object.attachment.length)) {3235 return { status: 400, error: 'empty_note' };3236 }3237 // Direct (private mention, shaer-tqc): NOT a post. Delivered over the3238 // outbox machinery to the addressed inboxes only; shows under Messages.3239 if (c2sVisibility(object) === 'direct') {3240 const arr = (v) => (Array.isArray(v) ? v : (v ? [v] : [])).filter((x) => typeof x === 'string');3241 const recipients = [...new Set([...arr(object.to), ...arr(object.cc)])]3242 .filter((u) => /^https?:\/\//i.test(u) && !/\/followers\/?$/.test(u) && u !== PUBLIC);3243 if (!recipients.length) return { status: 400, error: 'no_recipients' };3244 // AS2 attachments (e.g. the help-buoy capture, uploaded via3245 // uploadMedia): normalize our own absolute /media/ URLs to relative3246 // so the deliverReply-style validation applies unchanged.3247 const atts = (Array.isArray(object.attachment) ? object.attachment : [])3248 .map((a) => a && typeof a === 'object' ? {3249 url: String(a.url || '').replace(new RegExp('^' + base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), ''),3250 mediaType: String(a.mediaType || ''),3251 name: String(a.name || '').slice(0, 120),3252 } : null)3253 .filter(Boolean);3254 const help = object['shaer:helpRequest'] === true || object.helpRequest === true;3255 // FEP-633c 3.6.1: a guardian here declaring itself away to its3256 // wards. An away without a (future) end fails loudly, exactly as3257 // the daemon refuses it: stored quietly it would be a nominal3258 // guardian holding a seat.3259 let awayUntil = null;3260 if (Guardianship.availability.isAway(object)) {3261 awayUntil = Guardianship.availability.parseEndTime(object.endTime);3262 if (!awayUntil || awayUntil <= Date.now()) return { status: 400, error: 'away_needs_an_end' };3263 // No local shortcut here: the note below reaches a ward on this3264 // instance through the loopback, and its inbox handler applies the3265 // absence like it does for a ward anywhere else. One path.3266 }3267 const gateReq = Guardianship.gatereq.parseRequest(object);3268 // Een hulpvraag oppikken of afsluiten vanuit de app (5.2.1, shaer-lgo).3269 // De markering IS al een gewone directe note met een shaer:-eigenschap,3270 // dus hier hoeft niets nieuws bij: de app stuurt precies wat de PWA3271 // stuurt, en het gaat over dezelfde bezorging naar de mede-guardians.3272 //3273 // We boeken hem ook LOKAAL. Zonder dat zou de guardian die de knop3274 // indrukt zijn eigen markering pas zien als hij bij zichzelf3275 // terugkomt -- en die weg bestaat niet.3276 const mark = Guardianship.help.parseMarker(object);3277 if (mark) {3278 const base2 = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');3279 // Met de VOLLEDIGE handle. Hier stond `@${site.slug}` -- zonder host,3280 // dus een derde vorm naast de kale URI van de PWA-route en de echte3281 // handle die een binnengekomen markering draagt. Drie spellingen van3282 // dezelfde naam, en "door wie" was de hele vraag van shaer-lgo.3283 const mij = actorId(base2, site.slug);3284 Guardianship.help.record(mark.noteUri, mij, mark.kind, deriveHandle(mij));3285 }3286 const r = await deliverDirectNote(site, { recipients, text: plain, language: object.language || null, inReplyTo: typeof object.inReplyTo === 'string' ? object.inReplyTo : null, attachments: atts, helpRequest: help, awayUntil, gateRequest: gateReq && gateReq.feature, helpMark: mark });3287 if (!r || !r.id) return { status: 502, error: 'direct_failed' };3288 return { status: 201, id: r.id, url: `${base}/ap/notes/${r.id}` };3289 }3290 if (object.inReplyTo) {3291 const parent = await resolveRemoteNote(c2sIdOf(object.inReplyTo), { asSlug: site.slug }).catch(() => null);3292 if (!parent) return { status: 502, error: 'cannot_resolve_inReplyTo' };3293 // The attachments ride along (Robins melding, 30-7: "5023294 // reply_failed" op een reply met een foto): deliverReply validates3295 // them itself (own /media only, image|audio|video, max 4) and a3296 // media-only reply is a valid reply there. Dropping them here made3297 // a photo reply arrive naked, and a photo-ONLY reply fail outright.3298 const atts = (Array.isArray(object.attachment) ? object.attachment : [])3299 .map((a) => a && typeof a === 'object' ? {3300 url: String(a.url || '').replace(new RegExp('^' + base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), ''),3301 mediaType: String(a.mediaType || ''),3302 name: String(a.name || '').slice(0, 120),3303 } : null)3304 .filter(Boolean);3305 // Honour the client's visibility for the reply: 'friends' (followers-3306 // only, the Shaer detail-view Reply) drops Public; anything else stays3307 // quiet-public. 'direct' was already handled above.3308 const r = await deliverReply(site, {3309 postId: parent.localPostId || '', postSlug: null, parent, text: plain,3310 html: object.content || null, attachments: atts,3311 language: object.language || null, visibility: c2sVisibility(object),3312 });3313 if (!r || !r.id) return { status: 502, error: 'reply_failed' };3314 return { status: 201, id: r.id, url: `${base}/ap/notes/${r.id}` };3315 }3316 return await c2sCreatePost(base, site, user, object);3317 }3318 // ── Gelezen tot hier (shaer-frontend-3tx) ───────────────────3319 //3320 // AS2 kent Read: 'the actor has read the object'. Geen shaer:seen3321 // verzinnen, en geen zetbare stand: dit is een GEBEURTENIS, dus twee3322 // toestellen kunnen elkaar niet terugzetten. Blijft lokaal -- een3323 // leesbevestiging heeft in de fediverse niets te zoeken.3324 case 'Read': {3325 const targetUri = c2sIdOf(object);3326 if (!targetUri) return { status: 400, error: 'missing_object' };3327 const uit = markRead(site.slug, targetUri);3328 // Kennen we die note niet, dan is er niets gelezen om te onthouden.3329 // Geen fout: een client mag best een oud bericht aanwijzen.3330 return { status: uit ? 200 : 202 };3331 }3332 case 'Like':3333 case 'Announce': {3334 const targetUri = c2sIdOf(object);3335 if (!targetUri) return { status: 400, error: 'missing_object' };3336 // A non-public local note cannot be boosted or liked into the open3337 // (shaer-tqc hardening; the Mastodon 422 equivalent).3338 const localPid = postIdFromNoteUrl(targetUri, base);3339 if (localPid) {3340 const p = db.prepare('SELECT fan_only, ap_visibility FROM posts WHERE id = ?').get(localPid);3341 if (p && (p.fan_only || p.ap_visibility === 'direct' || p.ap_visibility === 'friends')) {3342 return { status: 403, error: 'not_public' };3343 }3344 }3345 const note = await resolveRemoteNote(targetUri, { asSlug: site.slug }).catch(() => null);3346 const objUri = (note && note.object_uri) || targetUri;3347 const authorUri = note && note.actor_uri;3348 const kind = type === 'Announce' ? 'boost' : 'like';3349 await sendInteraction(site, kind, objUri, authorUri);3350 // Eén schrijfpad (shaer-9e9): tussentabel + afgeleide vlag in één keer.3351 // De note gaat mee zodat een boost de post je tijdlijn in trekt.3352 try { setReaction(site.slug, targetUri, kind, true, { flagUri: objUri, note: type === 'Announce' ? note : null }); }3353 catch { /* non-fatal: een reactie mag nooit de bezorging blokkeren */ }3354 // Een Like uit een app moet ook in ap_timeline.liked landen, want dat3355 // is wat de C2S-tijdlijn als shaer:liked teruggeeft. Zonder dit werd3356 // de reactie wel opgeslagen (setMyReaction, de webroute leest die),3357 // maar kreeg de app altijd liked:false terug: het hartje sprong bij de3358 // eerste herlaadbeurt uit, en un-liken kon niet meer -- de app bood3359 // alleen nog "Like" aan en stuurde bij elke tik een nieuwe Like.3360 // Anders dan bij een boost geen upsert: een like hoort een post niet3361 // in je tijdlijn te trekken, dus staat de post er niet in, dan is dit3362 // terecht een no-op.3363 return { status: 202, url: objUri };3364 }3365 case 'Follow': {3366 const actorUri = c2sIdOf(object);3367 if (!actorUri) return { status: 400, error: 'missing_object' };3368 // FEP-633c §5.3 outbound (shaer-p729): a ward asks its guardians first.3369 // A held request is a THIRD outcome — not sent, not failed — and it3370 // travels to the app as one, so Shaer can show "waiting" instead of a3371 // tile that already looks followed.3372 const held = await gateOutgoingFollow(site, actorUri);3373 if (held) {3374 return {3375 status: 202, url: actorUri, id: held.id,3376 state: held.status === 'denied' ? 'refused_by_guardian' : 'awaiting_guardian',3377 };3378 }3379 // The error REACHES the app (Robins melding, 31-7): swallowing it3380 // made a failed follow look exactly like a successful one.3381 const r = await followActor(site, actorUri);3382 if (r && r.error) return { status: 502, error: 'follow_failed', detail: r.error };3383 return { status: 202, url: actorUri };3384 }3385 // Shaer "in Orbit" = a real Block (FEP-c648 client side): lands in3386 // ap_blocks, shows in the Block tab, and purges the actor's cached3387 // content. Client-side filtering becomes a cache of this state.3388 case 'Block': {3389 const targetUri = c2sIdOf(object);3390 if (!targetUri) return { status: 400, error: 'missing_object' };3391 const r = await blockTarget(site, targetUri);3392 if (r && r.error) return { status: 400, error: r.error };3393 return { status: 202, url: targetUri };3394 }3395 case 'Undo': {3396 const inner = object && typeof object === 'object' ? object : null;3397 let innerType = inner && inner.type;3398 if (Array.isArray(innerType)) innerType = innerType.find((t) => typeof t === 'string');3399 const innerTarget = c2sIdOf(inner && inner.object);3400 if (innerType === 'Follow') { await unfollowActor(site, innerTarget); return { status: 202, url: innerTarget }; }3401 if (innerType === 'Block') {3402 if (!innerTarget) return { status: 400, error: 'missing_object' };3403 unblock(site, innerTarget).catch(() => {}); // release from Orbit3404 return { status: 202, url: innerTarget };3405 }3406 if (innerType === 'Like' || innerType === 'Announce') {3407 const kind = innerType === 'Announce' ? 'unboost' : 'unlike';3408 const note = await resolveRemoteNote(innerTarget, { asSlug: site.slug }).catch(() => null);3409 const objUri = (note && note.object_uri) || innerTarget;3410 await sendInteraction(site, kind, objUri, note && note.actor_uri);3411 try { setReaction(site.slug, innerTarget, innerType === 'Announce' ? 'boost' : 'like', false, { flagUri: objUri }); }3412 catch { /* non-fatal */ }3413 return { status: 202, url: objUri };3414 }3415 return { status: 400, error: 'unsupported_undo' };3416 }3417 // Delete your OWN note (Robins verzoek, 30-7: long-press delete in de3418 // app). Scope stays narrow: this account's posts and outbound replies,3419 // nothing else. The web delete route is the model: Tombstone to the3420 // followers first, then the cascade, so nobody keeps a live copy of a3421 // post the child took back.3422 case 'Delete': {3423 const targetUri = c2sIdOf(object);3424 if (!targetUri) return { status: 400, error: 'missing_object' };3425 const pid = postIdFromNoteUrl(targetUri, base);3426 if (pid) {3427 const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(pid);3428 if (post) {3429 if (post.site_id !== site.id) return { status: 403, error: 'not_your_note' };3430 if (post.status === 'published') deliverDelete(site, post).catch(() => { /* best-effort */ });3431 db.transaction(() => {3432 db.prepare('DELETE FROM comments WHERE post_id = ?').run(post.id);3433 try { db.prepare('DELETE FROM posts_fts WHERE post_id = ?').run(post.id); } catch { /* FTS optional */ }3434 db.prepare('DELETE FROM posts WHERE id = ?').run(post.id);3435 })();3436 return { status: 202, url: targetUri };3437 }3438 // Same /ap/notes/ namespace: one of our outbound replies/messages.3439 // deliverOutboxDelete checks the site itself and tombstones too.3440 if (await deliverOutboxDelete(site, pid)) return { status: 202, url: targetUri };3441 }3442 return { status: 404, error: 'not_your_note' };3443 }3444 // Update of arbitrary objects needs the post-edit pipeline; tracked3445 // separately (klonkt-demo-c2s-del). Reject clearly rather than half-doing it.3446 default:3447 return { status: 400, error: 'unsupported_type', detail: String(type || 'none') };3448 }3449 } catch (e) {3450 console.warn('[AP] C2S ingest failed:', e && e.message);3451 return { status: 500, error: 'ingest_error' };3452 }3453 }3454 3455 // Create a top-level microblog post from a C2S Note and federate it. Minimal3456 // sibling of the /posts/create route: sanitized HTML content, no title/cover.3457 async function c2sCreatePost(base, site, user, object) {3458 const html = HtmlSanitizerService.sanitize(object.content || (object.source && object.source.content) || '');3459 // Media on a top-level post (shaer-j3uh/-oqxk/-df3i): same rules as3460 // deliverReply — only our OWN uploads, image/audio/video, max 4. They used3461 // to be silently dropped here, so a photo post from the app arrived naked.3462 const media = (Array.isArray(object.attachment) ? object.attachment : [])3463 .filter((a) => a && typeof a.url === 'string' && /^\/media\/[\w./-]+$/.test(a.url)3464 && /^(image|audio|video)\//.test(String(a.mediaType || '')))3465 .slice(0, 4)3466 .map((a) => {3467 const entry = { url: a.url, mediaType: String(a.mediaType), name: String(a.name || '').slice(0, 120) };3468 // The poster the upload leg made, when it did: a video's still frame3469 // (shaer-zowq, .poster.jpg) or an audio's waveform (Robins vraag 30-7,3470 // .poster.png). Rides along so the tag, the federated attachment and3471 // the apps all have something to show instead of a bare box.3472 const posterExt = entry.mediaType.startsWith('video/') ? '.poster.jpg'3473 : entry.mediaType.startsWith('audio/') ? '.poster.png' : null;3474 if (posterExt) {3475 try {3476 const mediaRoot = path.resolve(process.env.MEDIA_PATH || './storage/media');3477 const rel = entry.url.replace(/^\/media\//, '');3478 if (fs.existsSync(path.join(mediaRoot, rel + posterExt))) entry.poster = entry.url + posterExt;3479 } catch { /* no poster is fine */ }3480 }3481 return entry;3482 });3483 if (!html.trim() && !media.length) return { status: 400, error: 'empty_note' };3484 // The web reads the post's content, so the media goes IN it (we build these3485 // tags ourselves from validated paths, after the sanitizer). buildNote3486 // strips <img> back out into AS2 attachments; audio/video tags stay for the3487 // web player and federate via c2s_attachments below.3488 const esc = (t) => String(t).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');3489 const mediaHtml = media.map((a) => {3490 if (a.mediaType.startsWith('image/')) return `<p><img src="${a.url}" alt="${esc(a.name)}"></p>`;3491 // data-poster: <audio> has no poster attribute, but the tile derivation3492 // reads this one to show the waveform (post-tile/post-card).3493 if (a.mediaType.startsWith('audio/')) return `<p><audio controls preload="metadata"${a.poster ? ` data-poster="${a.poster}"` : ''} src="${a.url}"></audio></p>`;3494 const poster = a.poster ? ` poster="${a.poster}"` : '';3495 return `<p><video controls playsinline preload="metadata"${poster} src="${a.url}"></video></p>`;3496 }).join('');3497 // De titel (shaer-uply): AS2 zet hem in `name`, en die werd hier nooit3498 // gelezen -- een client kon hem zetten en hij verdween geruisloos, het3499 // slechtste van de drie mogelijke gedragingen. Platte tekst, want dat is wat3500 // `name` per AS2 is en wat de titelkolom overal verwacht; wie er toch HTML3501 // in stopt houdt de tekst over. De grens van 200 is de huisregel voor korte3502 // vrije tekst hier (content warning, sitetitel) -- de posteditor op het web3503 // heeft geen eigen grens, dus strenger dan het web zijn we hiermee niet3504 // op een manier die iemand merkt.3505 // Vanaf de kolom doet de bestaande machinerie de rest: het web toont hem,3506 // en buildNote vouwt hem als vetgedrukte eerste regel in de content3507 // (Mastodon negeert `name` op een Note).3508 const title = HtmlSanitizerService.toPlainText(typeof object.name === 'string' ? object.name : '').trim().slice(0, 200);3509 const postId = crypto.randomUUID();3510 const slug = 'n-' + postId.slice(0, 8);3511 const now = new Date().toISOString();3512 // Visibility from the note's addressing (shaer-60b): Public in `to` = loud3513 // public, Public in `cc` = quiet public (unlisted), followers-only = friends3514 // (rides the existing fan_only pipeline: followers-only AP delivery + web3515 // gating), neither = participants-only (kept local until mention addressing3516 // lands; still followers-gated on the web).3517 const vis = c2sVisibility(object);3518 const fanOnly = (vis === 'friends' || vis === 'direct') ? 1 : 0;3519 // Deliberately NO cover (Robins besluit, 30-7): the media lives in the3520 // content, and a cover next to it showed the same video twice on the post3521 // page. The tiles derive their picture from the content instead.3522 db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, excerpt, status, type, language, fan_only, ap_visibility, created_at, updated_at, published_at)3523 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)3524 .run(postId, site.id, slug, user.id, title, html + mediaHtml, '', 'published', 'post', object.language || 'nl', fanOnly, vis, now, now, now);3525 if (media.length) { try { db.prepare('UPDATE posts SET c2s_attachments = ? WHERE id = ?').run(JSON.stringify(media), postId); } catch { /* column exists via ensureColumn */ } }3526 try { db.prepare('UPDATE posts SET content_rendered = ? WHERE id = ?').run(bakePostContent(html + mediaHtml), postId); } catch { /* render fallback covers it */ }3527 bakePostContentWithMentions(html + mediaHtml).then((h) => { try { db.prepare('UPDATE posts SET content_rendered = ? WHERE id = ?').run(h, postId); } catch { /* keep sync bake */ } }).catch(() => {});3528 // Ook in de zoekindex, en niet alleen in de kolom (shaer-uply): anders is3529 // een getitelde C2S-post wel te zien maar niet op zijn titel te vinden.3530 try { db.prepare('INSERT INTO posts_fts(content, title, author, post_id) VALUES (?,?,?,?)').run(HtmlSanitizerService.toPlainText(html), title, user.username || '', postId); } catch { /* FTS non-fatal */ }3531 if (vis !== 'direct') {3532 deliverCreate(site, { id: postId, slug, title, content: html + mediaHtml, published_at: now, created_at: now, fan_only: fanOnly, ap_visibility: vis, c2s_attachments: media.length ? JSON.stringify(media) : null }).catch(() => { /* best-effort */ });3533 }3534 return { status: 201, id: postId, url: `${base}/ap/notes/${postId}` };3535 }3536 3537 3153 // The direct-note leg (ward call-for-help) lives in the guardianship module 3538 3154 // (src/services/guardianship/delivery.js); wired with our AP helpers at the … … 6679 6295 }); 6680 6296 6297 // De C2S-inname zijn werktuigen geven (stap 4, shaer-drc). Onderaan, zodat 6298 // elke const hierboven al bestaat; een verzoek kan pas na deze evaluatie 6299 // binnenkomen, dus de koppeling is altijd eerder dan de eerste aanroep. 6300 wireC2S({ 6301 proposeGate, deriveHandle, resolveRemoteNote, deliverReply, markRead, 6302 postIdFromNoteUrl, sendInteraction, setReaction, gateOutgoingFollow, 6303 followActor, unfollowActor, blockTarget, unblock, deliverDelete, 6304 deliverOutboxDelete, bakePostContent, bakePostContentWithMentions, 6305 deliverCreate, 6306 }); 6307 6681 6308 export default { 6682 6309 movedLock,
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)