Changeset 67f7150 in Klonkt
- Timestamp:
- 07/30/2026 11:26:35 AM (6 weeks ago)
- Branches:
- main
- Children:
- 6dd26e5
- Parents:
- a9da2c0
- Files:
-
- 3 edited
-
src/routes/activitypub.js (modified) (1 diff)
-
src/services/ActivityPubService.js (modified) (3 diffs)
-
test/c2s-compose.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
ra9da2c0 r67f7150 110 110 ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20` 111 111 ).all(site.id); 112 AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, posts), audience === 'friend' ? 'private, no-store' : undefined); 112 const ob = AP.buildOutbox(baseUrl(req), site, posts); 113 if (audience === 'friend') { 114 // The owner's app builds its feed from this leg, and every note here is 115 // by the site itself: give it the same `shaer:author` byline the timeline 116 // entries carry, so your own cards get a header too (avatar + name). 117 const me = AP.selfAuthor(baseUrl(req), site); 118 for (const it of ob.orderedItems) { 119 if (it && it.object && typeof it.object === 'object') it.object['shaer:author'] = me; 120 } 121 } 122 AP.sendAP(res, ob, audience === 'friend' ? 'private, no-store' : undefined); 113 123 }); 114 124 -
src/services/ActivityPubService.js
ra9da2c0 r67f7150 760 760 if (user) out.preferredUsername = user; 761 761 if (d.icon) out.icon = { type: 'Image', url: d.icon }; 762 return out; 763 } 764 765 // The site's OWN display info in the same shape as `shaer:author` on timeline 766 // entries. The owner's app reads its own posts from the outbox, which carried 767 // no author info, so every card but your own had a byline (Robins melding, 768 // 30-7: geen header van self op eigen posts). 769 export function selfAuthor(base, site) { 770 const out = { 771 name: site.title || site.slug, 772 handle: `@${site.slug}@${String(base).replace(/^https?:\/\//, '')}`, 773 url: `${base}/${site.slug === site.primary_slug ? '' : 'user/' + encodeURIComponent(site.slug)}`, 774 }; 775 if (site.profile_photo) { 776 out.icon = /^https?:/.test(site.profile_photo) ? site.profile_photo : `${base}${site.profile_photo.startsWith('/') ? '' : '/'}${site.profile_photo}`; 777 } 762 778 return out; 763 779 } … … 2374 2390 return { status: 400, error: 'unsupported_undo' }; 2375 2391 } 2376 // Delete/Update of arbitrary objects need the post-edit pipeline; tracked 2392 // Delete your OWN note (Robins verzoek, 30-7: long-press delete in de 2393 // app). Scope stays narrow: this account's posts and outbound replies, 2394 // nothing else. The web delete route is the model: Tombstone to the 2395 // followers first, then the cascade, so nobody keeps a live copy of a 2396 // post the child took back. 2397 case 'Delete': { 2398 const targetUri = c2sIdOf(object); 2399 if (!targetUri) return { status: 400, error: 'missing_object' }; 2400 const pid = postIdFromNoteUrl(targetUri, base); 2401 if (pid) { 2402 const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(pid); 2403 if (post) { 2404 if (post.site_id !== site.id) return { status: 403, error: 'not_your_note' }; 2405 if (post.status === 'published') deliverDelete(site, post).catch(() => { /* best-effort */ }); 2406 db.transaction(() => { 2407 db.prepare('DELETE FROM comments WHERE post_id = ?').run(post.id); 2408 try { db.prepare('DELETE FROM posts_fts WHERE post_id = ?').run(post.id); } catch { /* FTS optional */ } 2409 db.prepare('DELETE FROM posts WHERE id = ?').run(post.id); 2410 })(); 2411 return { status: 202, url: targetUri }; 2412 } 2413 // Same /ap/notes/ namespace: one of our outbound replies/messages. 2414 // deliverOutboxDelete checks the site itself and tombstones too. 2415 if (await deliverOutboxDelete(site, pid)) return { status: 202, url: targetUri }; 2416 } 2417 return { status: 404, error: 'not_your_note' }; 2418 } 2419 // Update of arbitrary objects needs the post-edit pipeline; tracked 2377 2420 // separately (klonkt-demo-c2s-del). Reject clearly rather than half-doing it. 2378 2421 default: … … 4114 4157 linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections, 4115 4158 noteVisibility, belongsInTimeline, playerUrlFor, isRejectedObject, rejectInteraction, interactionReportTarget, 4116 getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility, actorDisplay, buildActorRef, prefersEnriched, 4159 getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility, actorDisplay, buildActorRef, prefersEnriched, selfAuthor, 4117 4160 }; -
test/c2s-compose.test.js
ra9da2c0 r67f7150 144 144 }); 145 145 146 test('C2S Delete takes an own post back, and only an own post', async () => { 147 // Long-press delete in the app (Robins verzoek, 30-7): the child changes 148 // their mind, the post goes, followers get the Tombstone. 149 const r = await AP.ingestOutboxActivity(site, user, { 150 type: 'Create', 151 object: { 152 type: 'Note', content: '<p>weg hiermee</p>', 153 to: ['https://test.example/ap/users/kid/followers'], 154 cc: ['https://www.w3.org/ns/activitystreams#Public'], 155 }, 156 }); 157 assert.equal(r.status, 201); 158 const del = await AP.ingestOutboxActivity(site, user, { type: 'Delete', object: r.url }); 159 assert.equal(del.status, 202, 'an own note deletes'); 160 assert.equal(db.prepare('SELECT COUNT(*) c FROM posts WHERE id = ?').get(r.id).c, 0, 'and the row is gone'); 161 162 const unknown = await AP.ingestOutboxActivity(site, user, { type: 'Delete', object: 'https://test.example/ap/notes/bestaat-niet' }); 163 assert.equal(unknown.status, 404, 'an unknown note is a clear no'); 164 165 // Another account's post on this server: refused, row untouched. 166 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u2', 'ander', 'u2@t', 'x', 'user'); 167 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,0)').run('s2', 'ander', 'ander', 'u2'); 168 db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at, created_at, updated_at) 169 VALUES ('p-ander', 's2', 'u2', 'n-ander', '', '<p>van een ander</p>', 'published', datetime('now'), datetime('now'), datetime('now'))`).run(); 170 const foreign = await AP.ingestOutboxActivity(site, user, { type: 'Delete', object: 'https://test.example/ap/notes/p-ander' }); 171 assert.equal(foreign.status, 403, "someone else's post is not yours to take back"); 172 assert.equal(db.prepare('SELECT COUNT(*) c FROM posts WHERE id = ?').get('p-ander').c, 1, 'and it stays'); 173 }); 174 175 test('selfAuthor: the byline for your own outbox notes', () => { 176 // The owner's app reads its own posts from the outbox, which carried no 177 // author info: every card but your own had a header (Robins melding, 30-7). 178 const me = AP.selfAuthor('https://test.example', site); 179 assert.equal(me.name, 'kid'); 180 assert.equal(me.handle, '@kid@test.example'); 181 }); 182 146 183 test('a media-only post is a post, not an empty-note error', async () => { 147 184 const r = await AP.ingestOutboxActivity(site, user, {
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)