Changeset 2086ea2 in Klonkt for src/routes
- Timestamp:
- 08/12/2026 09:16:09 PM (3 months ago)
- Children:
- 39dab6b
- Parents:
- 5412cbb
- File:
-
- 1 edited
-
src/routes/ward.js (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/ward.js
r5412cbb r2086ea2 14 14 */ 15 15 import express from 'express'; 16 import multer from 'multer'; 17 import fs from 'fs'; 18 import { randomUUID } from 'crypto'; 16 19 import path from 'path'; 17 20 import { fileURLToPath } from 'url'; 21 import { mediaDir } from '../config/paths.js'; 18 22 import db from '../config/database.js'; 19 23 import { requireAuth } from '../middleware/auth.js'; … … 112 116 newTitle: i18nT(L, 'ward.new_title'), 113 117 noFriends: i18nT(L, 'ward.no_friends'), 118 write: i18nT(L, 'ward.write'), 119 post: i18nT(L, 'ward.post'), 120 visibility: i18nT(L, 'ward.visibility'), 121 visFriends: i18nT(L, 'ward.vis_friends'), 122 visPublic: i18nT(L, 'ward.vis_public'), 123 addPhoto: i18nT(L, 'ward.add_photo'), 124 addAudio: i18nT(L, 'ward.add_audio'), 125 addVideo: i18nT(L, 'ward.add_video'), 126 posted: i18nT(L, 'ward.posted'), 127 uploading: i18nT(L, 'ward.uploading'), 128 tooBig: i18nT(L, 'ward.too_big'), 114 129 }, 115 130 }; … … 361 376 }); 362 377 378 // Dezelfde media-map en dezelfde filters als de AP C2S-upload (/uploadMedia): 379 // een ward die via de app post en een ward die via de browser post moeten in 380 // dezelfde bak landen, anders federeren hun foto's van twee plekken. 381 const WARD_MEDIA_DIR = mediaDir('REPLY_MEDIA_PATH', 'reply-media'); 382 fs.mkdirSync(WARD_MEDIA_DIR, { recursive: true }); 383 const WARD_MEDIA_EXT = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif', '.mp3', '.m4a', '.ogg', '.opus', '.flac', '.wav', '.mp4', '.webm', '.mov']); 384 const wardUpload = multer({ 385 storage: multer.diskStorage({ 386 destination: (req, file, cb) => cb(null, WARD_MEDIA_DIR), 387 filename: (req, file, cb) => cb(null, `${randomUUID()}${path.extname(file.originalname || '').toLowerCase()}`), 388 }), 389 limits: { fileSize: 32 * 1024 * 1024 }, 390 fileFilter: (req, file, cb) => { 391 const ext = path.extname(file.originalname || '').toLowerCase(); 392 if (!WARD_MEDIA_EXT.has(ext)) return cb(new Error('media_type')); 393 cb(null, true); 394 }, 395 }); 396 397 /** 398 * Media uploaden voor de composer. Sessie-geauthenticeerd, want /ward draait 399 * op je eigen ingelogde Klonkt; de AP-upload eromheen eist een bearer die de 400 * browser niet heeft. Geeft { url, mediaType, name } terug, precies de vorm 401 * die een note als attachment draagt. 402 */ 403 router.post('/api/upload', requireAuth, (req, res) => { 404 const site = siteForUser(req); 405 if (!site) return res.status(404).json({ error: 'no_site' }); 406 wardUpload.single('file')(req, res, (err) => { 407 if (err) return res.status(400).json({ error: err.message === 'media_type' ? 'media_type' : 'upload_failed' }); 408 if (!req.file) return res.status(400).json({ error: 'no_file' }); 409 const mime = String(req.file.mimetype || ''); 410 if (!/^(image|audio|video)\//.test(mime)) { 411 try { fs.unlinkSync(req.file.path); } catch { /* best effort */ } 412 return res.status(400).json({ error: 'media_type' }); 413 } 414 res.status(201).json({ 415 url: '/media/reply-media/' + req.file.filename, 416 mediaType: mime, 417 name: String(req.file.originalname || '').slice(0, 120), 418 }); 419 }); 420 }); 421 422 /** 423 * Een post maken. Bouwt precies de AS2 Create die Shaer over C2S stuurt en 424 * geeft hem aan ingestOutboxActivity: die doet de compose-poort (§5), de 425 * attachment-validatie en de zichtbaarheid. GEEN eigen posting-pad, want dan 426 * zou de web-ward langs een gate kunnen die de app-ward tegenhoudt. 427 * 428 * De zichtbaarheid volgt Shaers eigen keuzes: 'public' luidop, 'friends' 429 * alleen naar volgers, 'direct' naar genoemde mensen. Standaard 'friends', 430 * want een besloten post is de veiligere default voor een kind. 431 */ 432 router.post('/api/post', requireAuth, express.json({ limit: '16kb' }), async (req, res) => { 433 const site = siteForUser(req); 434 if (!site) return res.status(404).json({ error: 'no_site' }); 435 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 436 const me = AP.actorId(base, site.slug); 437 438 const text = String(req.body?.text || '').trim().slice(0, 5000); 439 const media = Array.isArray(req.body?.media) ? req.body.media.slice(0, 4) : []; 440 if (!text && !media.length) return res.status(400).json({ error: 'empty' }); 441 442 const vis = ['public', 'friends'].includes(req.body?.visibility) ? req.body.visibility : 'friends'; 443 const followers = `${me}/followers`; 444 // Publiek luidop = Public in to; vrienden = alleen volgers, geen Public. 445 const to = vis === 'public' ? ['https://www.w3.org/ns/activitystreams#Public'] : [followers]; 446 447 const attachment = media 448 .filter((m) => m && typeof m.url === 'string' && /^\/media\//.test(m.url) && /^(image|audio|video)\//.test(String(m.mediaType || ''))) 449 .map((m) => ({ 450 type: m.mediaType.startsWith('image/') ? 'Image' : m.mediaType.startsWith('audio/') ? 'Audio' : 'Video', 451 mediaType: m.mediaType, 452 url: m.url, 453 name: String(m.name || '').slice(0, 120), 454 })); 455 456 const escaped = text.replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c])); 457 const content = escaped ? `<p>${escaped.replace(/\n/g, '<br>')}</p>` : ''; 458 459 const r = await AP.ingestOutboxActivity(site, req.session.user, { 460 type: 'Create', 461 object: { 462 type: 'Note', 463 content, 464 source: { content: text, mediaType: 'text/plain' }, 465 to, cc: [], 466 attachment: attachment.length ? attachment : undefined, 467 }, 468 }).catch((e) => ({ status: 500, error: String(e && e.message || e) })); 469 470 if (!r || r.status >= 400) return res.status(r?.status || 502).json({ error: r?.error || 'post_failed' }); 471 res.status(201).json({ ok: true, url: r.url || null }); 472 }); 473 363 474 function pwaAsset(rel, type) { 364 475 return (req, res) => {
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)