source: Klonkt/src/views/pages/connect.ejs@ 4766720

main
Last change on this file since 4766720 was 4766720, checked in by Robin <roboburr@…>, 4 weeks ago

Een verhuisd account gaat op slot aan de uitgaande kant

Robins vraag: "nu kan je er nog alles mee". Klopt. moved_to deed maar twee dingen,
de actor-wegwijzer en een banner op de homepage. Posten, volgen, liken, boosten en
reageren kon gewoon door, en dat federeerde de wereld in.

Drie dingen gingen daar mis. Nieuwe posts kregen een object-URI op een adres dat je
hebt opgezegd, en die URI's overleven het domein niet. Je volgers waren al verhuisd,
dus je postte in het niets terwijl het lijkt of je post. En een server die je
movedTo ziet én tegelijk verse activiteit van dat adres krijgt, krijgt
tegenstrijdige signalen over de verhuizing.

DE POORT STAAT IN DE SERVICE, NIET OP DE KNOPPEN. Een C2S-client (Shaer) praat
rechtstreeks met deze functies en zou langs een verborgen knop lopen. De UI volgt
de poort.

Dicht: posten, reageren, volgen, liken, boosten, stemmen, en een tweede verhuizing.
Open: de actor met movedTo, webfinger, bestaande posts, de outbox, alles inkomend
(reacties op oude posts blijven binnenkomen en leesbaar), je eigen beheer (archief
en volglijst exporteren), ontvolgen, en rapporteren. Dat laatste is een
veiligheidsklep, geen inhoud maken.

Omkeerbaar: moved_to leegmaken heft het slot op. Een verhuizing kan mislukken.

setReaction is APART gepoort, want die schrijft alleen de lokale vlag; het
versturen gebeurt elders. Zonder die tweede poort zag je een like staan die nooit
de deur uit was: dezelfde halve toestand die vandaag al twee keer opdook.

Changed files:
src/services/ActivityPubService.js

  • movedLock() en movedRefusal(), met de scheidslijn in het commentaar
  • poorten in deliverCreate, deliverReply, followActor, sendInteraction, voteOnRemotePoll, setReaction en moveAccount

src/routes/posts.js

  • 'moved' krijgt een eigen melding: het is een weigering met een reden, niet een mislukking, en "Volgen mislukt" laat je zoeken naar een storing die er niet is
  • connect krijgt movedTo mee

src/views/pages/connect.ejs

  • banner in plaats van het volgformulier; het volglijst-blok BLIJFT, want je archief meenemen moet je juist kunnen op een verhuisd account

src/services/i18n.js

  • drie teksten in nl, en en de

New file:
test/moved-lockdown.test.js

  • 11 tests die per soort vastleggen wat dicht gaat en wat open blijft
  • inclusief: een half ingevuld moved_to zet niets op slot, ontvolgen mag nog, en de wegwijzer plus de inbox blijven staan

remarks: tegenproef gedaan. Zonder de poort vallen 8 van de 11 om en blijven de
drie "blijft open"-tests groen. Suite 949 in UTC en Europe/Amsterdam. De pagina in
beide standen gerenderd: formulier weg, banner er, exportblok blijft, geen
onvertaalde sleutels.

Doorsturen van reacties op oude posts is bewust NIET gebouwd. Zo'n reply wijst naar
een object-URI met de oude base, en findThreadTarget matcht alleen op onze eigen
base: hij zou als wees binnenkomen. Om het te laten werken zou de nieuwe instance
objecten moeten claimen op een domein dat hij niet beheert, en dat is precies waar
de sleutelbinding van vanmiddag tegen beschermt. Antwoorden doe je vanaf het nieuwe
account via de bestaande bladwijzerroute; dan blijft elke URI bij wie hem hoort.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 11.9 KB
Line 
1<div class="tl-wrap">
2 <%- include('../partials/fedi-tabs', { active: 'connect' }) %>
3 <h1 class="tl-title"><%= t('tl.connect') %></h1>
4 <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
5 <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
6
7 <%# FEP-633c §2 + §3.6: who guards this account, and how available each of
8 them is. Connect is the "who am I connected to" page, and a guardian is
9 the one connection a ward should never have to hunt for. Availability in
10 buddy-list language on the responsibility axis; the labels are UI, the
11 wire keeps the spec terms. Never public: this page is the owner's. %>
12 <% if (typeof myGuardians !== 'undefined' && myGuardians.length) { %>
13 <section class="conn-guardians">
14 <h2 class="conn-gtitle">&#128737; <%= t('msg.guardians_label') %></h2>
15 <ul class="conn-glist">
16 <% myGuardians.forEach(function(g){
17 var _st = g.availability === 'away' ? 'away' : (g.availability === 'dormant' ? 'dormant' : 'active');
18 var _lbl = _st === 'away' ? t('msg.g_away', { date: g.awayUntil ? formatDate(g.awayUntil) : '?' })
19 : (_st === 'dormant' ? t('msg.g_offline') : t('msg.g_available')); %>
20 <li class="conn-guardian">
21 <span class="conn-gdot conn-gdot--<%= _st %>" aria-hidden="true"></span>
22 <span class="conn-ghandle"><%= g.handle %></span>
23 <span class="conn-gstate"><%= _lbl %></span>
24 </li>
25 <% }); %>
26 </ul>
27 </section>
28 <% } %>
29
30 <% var _moved = (typeof movedTo !== 'undefined' && movedTo) ? movedTo : null; %>
31 <% if (_moved) { %>
32 <%# De uitgaande kant staat op slot (FEP-7628). Dat hoort te blijken voordat
33 je op een knop drukt, niet daarna uit een foutmelding. Lezen blijft wel
34 gewoon werken, dus de lijst hieronder staat er nog. %>
35 <div class="alert alert-error tl-moved">
36 <strong><%= t('tl.moved_title') %></strong>
37 <p><%= t('tl.moved_lead') %> <a href="<%= _moved %>"><%= _moved %></a></p>
38 <p class="form-hint"><%= t('tl.moved_hint') %></p>
39 </div>
40 <% } else { %>
41 <form method="post" action="/news/follow" class="tl-follow">
42 <input type="text" name="handle" placeholder="@naam@server.social of site.com" autocomplete="off" spellcheck="false" required>
43 <button type="submit" class="btn btn-primary"><%= t('tl.follow_btn') %></button>
44 <label class="tl-follow-ab" title="<%= t('tl.autoboost_hint') %>"><input type="checkbox" name="auto_boost" value="1"> &#10024; <%= t('tl.autoboost_follow') %></label>
45 </form>
46 <% } %>
47
48 <%# Je volglijst meenemen. Hoort HIER en niet op /following: de tabbalk stuurt
49 'following' door naar Connect en er linkt niets meer naar die oude pagina.
50 Wie verhuist moet dit kunnen vinden zonder een URL uit zijn hoofd te typen. %>
51 <details class="tl-move">
52 <summary><%= t('tl.move_title') %></summary>
53 <p class="tl-move-hint"><%= t('tl.move_hint') %></p>
54 <p><a class="btn" href="/news/following.csv" download>&#11015; <%= t('tl.move_export') %></a></p>
55 <form method="post" action="/news/following/import" enctype="multipart/form-data">
56 <label class="tl-move-lbl" for="tl-move-file"><%= t('tl.move_import_file') %></label>
57 <input type="file" id="tl-move-file" name="csvfile" accept=".csv,text/csv,text/plain">
58
59 <label class="tl-move-lbl" for="tl-move-csv"><%= t('tl.move_import_lbl') %></label>
60 <textarea id="tl-move-csv" name="csv" rows="5" spellcheck="false"
61 placeholder="Account address,Show boosts,Notify on new posts,Languages,Featured"></textarea>
62
63 <button type="submit" class="btn btn-primary"><%= t('tl.move_import') %></button>
64 </form>
65 </details>
66
67 <%
68 var conns = connections || [];
69 var reachable = conns.filter(function (c) { return !c.unreachable; });
70 var unreachable = conns.filter(function (c) { return c.unreachable; });
71 // Display fields for one connection: name/handle (fallback: derive from actor_uri).
72 function connView(c) {
73 var host = '', user = '';
74 try { var u = new URL(c.actor_uri); host = u.hostname; user = (u.pathname.split('/').filter(Boolean).pop() || ''); } catch (e) {}
75 var handle = c.handle || (user ? ('@' + user + '@' + host) : c.actor_uri);
76 var display = c.name || handle;
77 return { handle: handle, display: display,
78 initial: String(display || '?').replace(/^@/, '').charAt(0).toUpperCase(),
79 profileUrl: c.url || c.actor_uri };
80 }
81 function arrow(dir) { return dir === 'mutual' ? '↔' : (dir === 'following' ? '→' : '←'); }
82 %>
83
84 <% if (reachable.length) { %>
85 <ul class="tl-foll-list">
86 <% reachable.forEach(function (c) { var v = connView(c); %>
87 <li class="tl-foll">
88 <span class="tl-foll-av"><% if (c.icon) { %><img src="<%= avatar(c.icon, 96) %>" alt=""><% } else { %><%= v.initial %><% } %></span>
89 <span class="tl-foll-meta">
90 <a href="<%= v.profileUrl %>" target="_blank" rel="nofollow noopener"><%= v.display %></a>
91 <span class="conn-sub">
92 <span class="conn-dir conn-dir--<%= c.direction %>"><%= arrow(c.direction) %> <%= t('tl.dir_' + c.direction) %></span>
93 <% if (c.follower && c.last_delivery_at) { %><span class="conn-deliv">&middot; <%= t('tl.last_delivery') %>: <%= formatDateTime(c.last_delivery_at) %></span><% } %>
94 </span>
95 </span>
96 <% if (c.following) { %>
97 <form method="post" action="/news/autoboost" class="tl-foll-ab" title="<%= t('tl.autoboost_hint') %>">
98 <input type="hidden" name="actor_uri" value="<%= c.actor_uri %>">
99 <label><input type="checkbox" name="auto_boost" value="1" <%= c.auto_boost ? 'checked' : '' %> data-autosubmit> &#10024; <%= t('tl.autoboost') %></label>
100 </form>
101 <form method="post" action="/news/unfollow"><input type="hidden" name="actor_uri" value="<%= c.actor_uri %>"><button type="submit" class="tl-unfollow"><%= t('tl.unfollow') %></button></form>
102 <% } %>
103 </li>
104 <% }); %>
105 </ul>
106 <% } else { %>
107 <p class="tl-empty"><%= t('tl.connect_empty') %></p>
108 <% } %>
109
110 <% if (unreachable.length) { %>
111 <details class="conn-unreach">
112 <summary>&#9888; <%= t('tl.unreachable') %> (<%= unreachable.length %>)</summary>
113 <p class="conn-unreach-lead"><%= t('tl.unreachable_lead') %></p>
114 <ul class="tl-foll-list">
115 <% unreachable.forEach(function (c) { var v = connView(c); %>
116 <li class="tl-foll is-stale">
117 <span class="tl-foll-av"><% if (c.icon) { %><img src="<%= avatar(c.icon, 96) %>" alt=""><% } else { %><%= v.initial %><% } %></span>
118 <span class="tl-foll-meta">
119 <a href="<%= v.profileUrl %>" target="_blank" rel="nofollow noopener"><%= v.display %></a>
120 <span class="conn-sub">
121 <span class="conn-dir conn-dir--<%= c.direction %>"><%= arrow(c.direction) %> <%= t('tl.dir_' + c.direction) %></span>
122 <% if (c.last_delivery_at) { %><span class="conn-deliv">&middot; <%= t('tl.last_delivery') %>: <%= formatDateTime(c.last_delivery_at) %></span><% } else { %><span class="conn-never">&middot; <%= t('tl.never_delivered') %></span><% } %>
123 <% if (c.last_error_at) { %><span class="conn-err">&middot; <%= t('tl.delivery_failed') %>: <%= formatDateTime(c.last_error_at) %></span><% } %>
124 </span>
125 </span>
126 <% if (c.follower_id) { %>
127 <form method="post" action="/followers/<%= c.follower_id %>/remove" onsubmit="return confirm('<%= t('tl.remove_confirm') %>')">
128 <button type="submit" class="tl-unfollow"><%= t('tl.remove_follower') %></button>
129 </form>
130 <% } %>
131 </li>
132 <% }); %>
133 </ul>
134 </details>
135 <% } %>
136</div>
137
138<style>
139 /* Guardians (FEP-633c 3.6): the buddy-list dot on the responsibility axis.
140 Green available, yellow declared away with an end, grey observed dormant
141 (one answer restores). Set apart from the follow list on purpose: these
142 are not people you chose to follow, they are the ones looking after you. */
143 .conn-guardians { margin: 0 0 1.25rem; padding: .8rem 1rem; border-radius: 12px;
144 border: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent);
145 background: color-mix(in srgb, var(--ink, #000) 2.5%, transparent);
146 border-left: 3px solid #35a2ff; }
147 .conn-gtitle { margin: 0 0 .5rem; font-size: .95rem; }
148 .conn-glist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .35rem; }
149 .conn-guardian { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
150 .conn-gdot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
151 .conn-gdot--active { background: #4caf7d; }
152 .conn-gdot--away { background: #e0a83a; }
153 .conn-gdot--dormant { background: #8a93a3; }
154 .conn-ghandle { font-weight: 600; }
155 .conn-gstate { color: var(--ink-soft, #888); font-size: .82rem; }
156 .tl-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
157 .tl-title { margin: 0 0 .75rem; }
158 .tl-empty { color: var(--ink-soft, #888); }
159 .tl-follow { display: flex; gap: .5rem; margin: 0 0 1.25rem; flex-wrap: wrap; align-items: center; }
160 .tl-follow input[type="text"] { flex: 1; min-width: 200px; height: 2.4rem; padding: 0 .9rem; border-radius: 999px; font: inherit;
161 border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, #000); }
162 .tl-follow-ab { font-size: .85rem; color: var(--ink-soft, #888); display: inline-flex; align-items: center; gap: .3rem; cursor: pointer; }
163 .tl-foll-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .4rem; }
164 .tl-foll { display: flex; align-items: center; gap: .6rem; padding: .45rem .6rem; border-radius: 12px; background: color-mix(in srgb, var(--ink, #000) 4%, transparent); }
165 .tl-foll.is-stale { background: color-mix(in srgb, #c0392b 10%, transparent); }
166 .tl-foll-av { flex: 0 0 36px; width: 36px; height: 36px; border-radius: 50%; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; font-weight: 700; background: color-mix(in srgb, var(--accent, #888) 20%, transparent); color: var(--accent, #555); }
167 .tl-foll-av img { width: 100%; height: 100%; object-fit: cover; background: #fff; }
168 .tl-foll-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.3; }
169 .tl-foll-meta a { color: var(--ink, inherit); text-decoration: none; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
170 .tl-foll-meta a:hover { text-decoration: underline; }
171 .conn-sub { font-size: .78rem; color: var(--ink-soft, #888); display: inline-flex; flex-wrap: wrap; gap: .35rem; align-items: baseline; }
172 .conn-dir { font-weight: 600; }
173 .conn-dir--mutual { color: var(--accent, #06c); }
174 .conn-dir--following { color: color-mix(in srgb, var(--ink, #000) 62%, transparent); }
175 .conn-dir--follower { color: color-mix(in srgb, var(--ink, #000) 62%, transparent); }
176 .conn-never, .conn-err { color: #c0392b; }
177 .tl-foll-ab { margin: 0; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; }
178 .tl-foll-ab label { display: inline-flex; align-items: center; gap: .25rem; cursor: pointer; }
179 .tl-unfollow { background: none; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); border-radius: 8px; padding: .25rem .6rem; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; white-space: nowrap; }
180 .tl-unfollow:hover { color: var(--ink, #000); }
181 .conn-unreach { margin: 1.25rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .75rem; }
182 .conn-unreach summary { cursor: pointer; font-weight: 600; color: #c0392b; }
183 .conn-unreach-lead { font-size: .8rem; color: var(--ink-soft, #888); margin: .5rem 0 .75rem; }
184</style>
Note: See TracBrowser for help on using the repository browser.