Changeset bfa6fa1 in Klonkt
- Timestamp:
- 07/31/2026 06:02:01 PM (6 weeks ago)
- Branches:
- main
- Children:
- 72ec6a4
- Parents:
- 0ca7e9a4
- Files:
-
- 6 edited
-
src/assets/css/style.css (modified) (1 diff)
-
src/routes/posts.js (modified) (1 diff)
-
src/services/ActivityPubService.js (modified) (2 diffs)
-
src/services/i18n.js (modified) (3 diffs)
-
src/views/pages/home.ejs (modified) (1 diff)
-
test/move-actor.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/style.css
r0ca7e9a4 rbfa6fa1 559 559 560 560 .empty { text-align: center; padding: 4rem 1rem; } 561 /* Moved account (FEP-7628 slice 3): the signpost above the feed. Loud on 562 purpose — a visitor must not read a stale timeline thinking it is current. */ 563 .moved-banner { 564 text-align: center; 565 margin: 1.5rem 0 2rem; 566 padding: 1.75rem 1.25rem; 567 border: 2px solid var(--accent, #e8b04b); 568 border-radius: var(--radius, 10px); 569 background: color-mix(in srgb, var(--accent, #e8b04b) 10%, var(--paper-2, var(--paper))); 570 } 571 .moved-banner-lead { margin: 0 0 .5rem; font-size: 1rem; opacity: .85; } 572 .moved-banner-link { 573 display: inline-block; 574 font-family: var(--font-display); 575 font-size: clamp(1.5rem, 5vw, 2.4rem); 576 font-weight: 700; 577 line-height: 1.15; 578 overflow-wrap: anywhere; 579 } 580 .moved-banner-hint { margin: .75rem 0 0; font-size: .85rem; color: var(--ink-soft); } 561 581 .empty h1 { font-size: 2rem; } 562 582 .empty p { color: var(--ink-muted); } -
src/routes/posts.js
r0ca7e9a4 rbfa6fa1 232 232 recordPageview(site.id, req); 233 233 234 // FEP-7628 slice 3: this account moved. A visitor who lands here deserves 235 // the same signpost the fediverse gets — one big link to the new address. 236 const movedTo = site.moved_to && /^https?:\/\//i.test(String(site.moved_to)) ? String(site.moved_to) : null; 234 237 renderPage(req, res, 'pages/home', { 235 238 pinnedPosts, 236 239 posts, 237 240 hasMore, nextOffset: offset + FEED_PAGE, moreBase, 241 movedTo, 242 movedToLabel: movedTo ? (ActivityPubService.actorDisplay(site.slug, movedTo).handle || movedTo) : null, 238 243 pageTitle: site.title, 239 244 socialDescr: site.description || site.tagline || '', -
src/services/ActivityPubService.js
r0ca7e9a4 rbfa6fa1 234 234 } 235 235 } catch { /* skip malformed ap_aliases */ } 236 // FEP-7628 slice 3: this account moved. The old actor stays online AS A 237 // SIGNPOST — that is the whole point of keeping it: whoever missed the Move 238 // activity (offline server, later visitor) still learns where we went by 239 // fetching us. Per the FEP the moved actor "should be considered inactive", 240 // and publishers should stop delivering here. 241 if (site.moved_to && /^https?:\/\//i.test(String(site.moved_to))) actor.movedTo = String(site.moved_to); 236 242 // Profile links → PropertyValue rows: Mastodon/PeerTube/WordPress-ActivityPub render these as 237 243 // profile metadata (rel=me enables link-back verification). Additive; ignored by simpler receivers. … … 3892 3898 to: [`${me}/followers`], 3893 3899 }; 3900 // FEP-7628: after setting movedTo, notify the followers with an Update of 3901 // the actor, so their servers hold the signpost even if the Move itself is 3902 // lost. Built from the FRESH row: `site` still carries the pre-move values. 3903 const movedSite = db.prepare('SELECT * FROM sites WHERE slug = ?').get(site.slug) || { ...site, moved_to: target.id }; 3904 const update = { 3905 '@context': AP_CONTEXT, 3906 id: `${me}#update-${Date.now()}-${rid()}`, 3907 type: 'Update', actor: me, to: [PUBLIC], cc: [`${me}/followers`], 3908 object: buildActor(base, movedSite), 3909 published: new Date().toISOString(), 3910 }; 3894 3911 const inboxes = [...new Set(fStmts().list.all(site.slug).map((f) => f.shared_inbox || f.inbox).filter(Boolean))]; 3912 const send = deliverFn || deliverWithRetry; 3895 3913 for (const inbox of inboxes) { 3896 await (deliverFn || deliverWithRetry)(site.slug, inbox, move, `${me}#main-key`, keys.private_pem); 3914 await send(site.slug, inbox, update, `${me}#main-key`, keys.private_pem); 3915 await send(site.slug, inbox, move, `${me}#main-key`, keys.private_pem); 3897 3916 } 3898 3917 console.log('[AP] MOVE announced:', site.slug, '→', target.id, 'naar', inboxes.length, 'inbox(en)'); -
src/services/i18n.js
r0ca7e9a4 rbfa6fa1 912 912 'vblk.back': 'Terug', 913 913 'vblk.to_home': 'Naar de hoofdpagina', 914 'phome.moved_lead': 'Dit account is verhuisd. Je vindt me nu hier:', 915 'phome.moved_hint': 'Volg je me al? Dan verhuist je server je waarschijnlijk vanzelf mee. Zo niet: volg het nieuwe adres.', 914 916 'phome.empty_title': 'Hier is het nog stil.', 915 917 'phome.empty_sub': 'Nog geen posts. Spannend.', … … 1854 1856 'vblk.back': 'Back', 1855 1857 'vblk.to_home': 'Go to the main page', 1858 'phome.moved_lead': 'This account has moved. You can find me here now:', 1859 'phome.moved_hint': 'Already following me? Your server most likely moves you along by itself. If not: follow the new address.', 1856 1860 'phome.empty_title': "It's quiet here for now.", 1857 1861 'phome.empty_sub': 'No posts yet. Exciting.', … … 2796 2800 'vblk.back': 'Zurück', 2797 2801 'vblk.to_home': 'Zur Hauptseite', 2802 'phome.moved_lead': 'Dieses Konto ist umgezogen. Du findest mich jetzt hier:', 2803 'phome.moved_hint': 'Folgst du mir schon? Dann zieht dein Server dich wahrscheinlich automatisch mit. Falls nicht: folge der neuen Adresse.', 2798 2804 'phome.empty_title': 'Hier ist es noch still.', 2799 2805 'phome.empty_sub': 'Noch keine Beiträge. Spannend.', -
src/views/pages/home.ejs
r0ca7e9a4 rbfa6fa1 17 17 #pcms-main) so it survives HTMX swaps and is visible on every 18 18 non-admin page. %> 19 20 <%# FEP-7628 slice 3: this account moved. The signpost comes FIRST, before 21 any post: a visitor should not scroll a dead feed to find out we left. 22 typeof-guard because an old route may render this template for one 23 request right after a deploy (view cache fills on first hit). %> 24 <% if (typeof movedTo !== 'undefined' && movedTo) { %> 25 <div class="container"> 26 <aside class="moved-banner"> 27 <p class="moved-banner-lead"><%= t('phome.moved_lead') %></p> 28 <a class="moved-banner-link" href="<%= movedTo %>" rel="me noopener"><%= movedToLabel || movedTo %></a> 29 <p class="moved-banner-hint"><%= t('phome.moved_hint') %></p> 30 </aside> 31 </div> 32 <% } %> 19 33 20 34 <!-- ========== TIMELINE ========== --> -
test/move-actor.test.js
r0ca7e9a4 rbfa6fa1 163 163 assert.equal(r.target, 'https://elders.example/users/nieuw'); 164 164 assert.equal(db.prepare("SELECT moved_to FROM sites WHERE slug = 'radio'").get().moved_to, 'https://elders.example/users/nieuw'); 165 assert.equal(delivered.length, 2, 'both follower inboxes'); 166 for (const d of delivered) { 167 assert.equal(d.activity.type, 'Move'); 165 // Slice 3 added the Update(actor) alongside it, so count the Moves. 166 const moves = delivered.filter((d) => d.activity.type === 'Move'); 167 assert.equal(moves.length, 2, 'one Move per follower inbox'); 168 for (const d of moves) { 168 169 assert.equal(d.activity.object, ME_RADIO); 169 170 assert.equal(d.activity.target, 'https://elders.example/users/nieuw'); 170 171 } 171 assert.ok(delivered.some((d) => d.inbox === 'https://b.example/shared'), 'shared inbox preferred'); 172 }); 172 assert.ok(moves.some((d) => d.inbox === 'https://b.example/shared'), 'shared inbox preferred'); 173 }); 174 175 // ── Slice 3: the signpost ────────────────────────────────────────── 176 // The old actor stays online and SAYS where it went (FEP-7628 movedTo), and 177 // the followers are told with an Update alongside the Move. 178 179 test('a moved account carries movedTo on its actor; an unmoved one does not', () => { 180 const fresh = db.prepare("SELECT * FROM sites WHERE slug = 'blog'").get(); 181 assert.equal('movedTo' in buildActor('https://test.example', fresh), false, 'no move, no signpost'); 182 const moved = { ...fresh, moved_to: 'https://elders.example/users/nieuw' }; 183 assert.equal(buildActor('https://test.example', moved).movedTo, 'https://elders.example/users/nieuw'); 184 // Junk in the column never reaches the wire. 185 assert.equal('movedTo' in buildActor('https://test.example', { ...fresh, moved_to: 'niet-een-url' }), false); 186 }); 187 188 test('the move tells the followers twice: an Update carrying movedTo, and the Move', async () => { 189 db.prepare("DELETE FROM ap_followers WHERE slug = 'radio'").run(); 190 db.prepare("UPDATE sites SET moved_to = NULL WHERE slug = 'radio'").run(); 191 db.prepare("INSERT INTO ap_followers (slug, actor_uri, inbox) VALUES ('radio', 'https://c.example/u/c', 'https://c.example/inbox')").run(); 192 const sent = []; 193 const r = await moveAccount(radioSite(), 'https://elders.example/users/nieuw', { 194 fetchActorFn: async () => ({ id: 'https://elders.example/users/nieuw', inbox: 'https://elders.example/inbox', alsoKnownAs: [ME_RADIO] }), 195 deliverFn: async (slug, inbox, activity) => { sent.push(activity); }, 196 }); 197 assert.equal(r.ok, true); 198 const update = sent.find((a) => a.type === 'Update'); 199 const move = sent.find((a) => a.type === 'Move'); 200 assert.ok(update, 'an Update is sent'); 201 assert.ok(move, 'the Move is still sent'); 202 assert.equal(update.object.movedTo, 'https://elders.example/users/nieuw', 'the Update carries the signpost'); 203 assert.equal(update.object.id, ME_RADIO, 'and it is OUR actor being updated'); 204 });
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)