Changeset feced2c in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 07/19/2026 05:25:26 PM (7 weeks ago)
- Branches:
- main
- Children:
- 5190152
- Parents:
- 33e1dbd
- git-author:
- Robin <roboburr@…> (07/19/2026 05:24:58 PM)
- git-committer:
- Robin <roboburr@…> (07/19/2026 05:25:26 PM)
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r33e1dbd rfeced2c 227 227 if (opts.isReply) { 228 228 const meR = actorId(base, site.slug); 229 // Rich replies: attachments column (JSON [{url, mediaType, name}]) → AS2 230 // attachment array with absolute URLs and the matching object type. 231 let replyAtt; 232 try { 233 const list = post.attachments ? JSON.parse(post.attachments) : []; 234 if (Array.isArray(list) && list.length) { 235 replyAtt = list.map((a) => ({ 236 type: a.mediaType.startsWith('image/') ? 'Image' : a.mediaType.startsWith('audio/') ? 'Audio' : 'Video', 237 mediaType: a.mediaType, 238 url: /^https?:/i.test(a.url) ? a.url : `${base}${a.url}`, 239 name: a.name || undefined, 240 })); 241 } 242 } catch { /* malformed attachments never block the Note */ } 229 243 return { 230 244 id: noteId(base, post.id), … … 235 249 // Reply language (rich replies): the AS2 language map next to `content`. 236 250 contentMap: post.language ? { [post.language]: post.content } : undefined, 251 attachment: replyAtt, 237 252 url: post.post_slug ? `${base}/${encodeURIComponent(post.post_slug)}` : undefined, 238 253 published: toISO(post.created_at), … … 712 727 _listI = db.prepare('SELECT id, kind, object_uri, parent_uri, actor_uri, actor_name, actor_handle, actor_url, actor_icon, content, published, created_at, acted_boost, acted_like, visibility FROM ap_interactions WHERE post_id = ? ORDER BY created_at ASC'); 713 728 _getI = db.prepare('SELECT * FROM ap_interactions WHERE id = ?'); 714 _insO = db.prepare('INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, language, created_at) VALUES (?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');729 _insO = db.prepare('INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, language, attachments, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)'); 715 730 _listO = db.prepare('SELECT * FROM ap_outbox WHERE post_id = ? ORDER BY created_at ASC'); 716 731 _getO = db.prepare('SELECT * FROM ap_outbox WHERE id = ?'); … … 817 832 noteId: baseClean ? `${baseClean}/ap/notes/${o.id}` : o.id, parent: o.in_reply_to || null, 818 833 mine: true, outboxId: o.id, content: stripLeadingMentions(o.content), created_at: o.created_at, 834 media: (() => { try { return o.attachments ? JSON.parse(o.attachments) : []; } catch { return []; } })(), 819 835 actor_name: siteName, actor_handle: siteHandle, actor_url: siteUrl, actor_icon: siteIcon, 820 836 children: [], … … 1856 1872 // Send a reply FROM this site to a remote actor (in reply to their inbound reply). 1857 1873 // `parent` = an ap_interactions row (actor_uri, actor_url, actor_handle, object_uri). 1858 export async function deliverReply(site, { postId, postSlug, parent, text, html, language }) {1874 export async function deliverReply(site, { postId, postSlug, parent, text, html, language, attachments }) { 1859 1875 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 1860 1876 // Rich replies: `html` is the reply editor's HTML (sanitized here); `text` is … … 1862 1878 const richClean = html ? HtmlSanitizerService.sanitize(String(html)) : ''; 1863 1879 const rich = richClean && HtmlSanitizerService.toPlainText(richClean).trim() ? richClean : ''; 1864 if (!base || !site || !site.slug || !parent || (!String(text || '').trim() && !rich)) return null; 1880 // Attachments: only OUR OWN uploads (/media/... paths, no remote URLs — the 1881 // upload route is the sole producer), image/audio/video only, max 4. 1882 const media = (Array.isArray(attachments) ? attachments : []) 1883 .filter((a) => a && typeof a.url === 'string' && /^\/media\/[\w./-]+$/.test(a.url) 1884 && /^(image|audio|video)\//.test(String(a.mediaType || ''))) 1885 .slice(0, 4) 1886 .map((a) => ({ url: a.url, mediaType: String(a.mediaType), name: String(a.name || '').slice(0, 120) })); 1887 // A media-only reply (no text) is a valid reply. 1888 if (!base || !site || !site.slug || !parent || (!String(text || '').trim() && !rich && !media.length)) return null; 1865 1889 const me = actorId(base, site.slug); 1866 1890 const handle = parent.actor_handle || deriveHandle(parent.actor_uri); … … 1890 1914 const replyLang = /^[a-z]{2,3}(-[A-Za-z0-9-]+)?$/.test(String(language || '')) ? language : null; 1891 1915 // Dedup: skip if the exact same reply was already sent (double-submit guard). 1892 const dup = db.prepare('SELECT 1 FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? LIMIT 1') 1893 .get(site.slug, parent.object_uri || '', content); 1916 // Attachments count toward "the same": two media-only replies share content. 1917 const mediaJson = media.length ? JSON.stringify(media) : null; 1918 const dup = db.prepare('SELECT 1 FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? AND IFNULL(attachments, \'\') = IFNULL(?, \'\') LIMIT 1') 1919 .get(site.slug, parent.object_uri || '', content, mediaJson); 1894 1920 if (dup) { console.log('[AP] outreply skipped (duplicate)'); return { duplicate: true, delivered: 0 }; } 1895 1921 const id = crypto.randomUUID(); 1896 iStmts().insO.run(id, site.slug, postId, postSlug || null, parent.object_uri || null, parent.actor_uri || null, handle, content, replyLang );1922 iStmts().insO.run(id, site.slug, postId, postSlug || null, parent.object_uri || null, parent.actor_uri || null, handle, content, replyLang, mediaJson); 1897 1923 const row = iStmts().getO.get(id); 1898 1924 const note = buildReplyNote(base, site, row);
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)