| … | … | @@ -2139,12 +2139,54 @@ export function blueskySection (did, reach) { |
| 2139 | 2139 | '<p class="who">Leave empty to publish under the identity above. Nothing is sent to Bluesky until ' + |
| 2140 | 2140 | 'one is chosen.</p><button>save</button></form></details>' |
| 2141 | 2141 | |
| 2142 | // ⛔ THE FOURTH STEP, AND IT IS NOT IMPLIED BY THE THIRD. The switch records CONSENT; it does not | |
| 2143 | // author anything. The fediverse bridge HOLDS your feed and projects from it server-side, so | |
| 2144 | // switching that on is genuinely all there is to do — but Bluesky's membrane is keyless and serves | |
| 2145 | // only what this device SIGNED, so until the device authors them the repo stays empty. Measured on a | |
| 2146 | // handset: identity correct, handle resolving, relay active, switch on, and an atproto repo holding | |
| 2147 | // one bootstrap record. Every step reported success and nothing had ever been published. | |
| 2148 | // | |
| 2149 | // ⚠ Only offered once there is somewhere for it to go. Publishing into an identity nobody carries | |
| 2150 | // writes records no one can resolve. | |
| 2151 | const publish = (on && did.handle) | |
| 2152 | ? '<p><strong>4. Publish what you have written</strong></p>' + | |
| 2153 | '<p class="who">The switch above says posts MAY go to Bluesky. This is what sends them: your ' + | |
| 2154 | 'profile, and every post whose room is public enough to leave. Posts already there are left ' + | |
| 2155 | 'alone, so running it twice is safe.</p>' + | |
| 2156 | '<form method="post" action="/did/project">' + | |
| 2157 | '<button name="send" value="">see what would be published</button> ' + | |
| 2158 | '<button name="send" value="1">publish now</button></form>' + | |
| 2159 | '<p class="warn">Publishing sends your existing posts, not just new ones. A post on Bluesky ' + | |
| 2160 | 'federates and cannot be recalled.</p>' | |
| 2161 | : '' | |
| 2162 | ||
| 2142 | 2163 | return '<h2>Bluesky</h2>' + identity + carried + |
| 2143 | 2164 | '<p><strong>3. Send posts there</strong></p>' + designate + other + |
| 2144 | 2165 | switchFor('bsky', 'Send my public posts to Bluesky', on, |
| 2145 | 2166 | !did.handle ? 'No bridge carries this identity yet, so nothing will be sent even with this on.' : null) + |
| 2146 | 2167 | '<p class="who">Plain text only, up to 300 characters — longer posts are withheld, not shortened. ' + |
| 2147 | 'A post sent there cannot be recalled.</p>' | |
| 2168 | 'A post sent there cannot be recalled.</p>' + publish | |
| 2169 | } | |
| 2170 | ||
| 2171 | /** | |
| 2172 | * What a projection run did, in its own words. | |
| 2173 | * | |
| 2174 | * ⛔ THE LOG VERBATIM, not a summary. Every line is a decision about one record — sent, already there, | |
| 2175 | * withheld and why — and the withheld ones are the interesting half: "too long for bsky", "a members | |
| 2176 | * room reaches nothing". A count cannot express those, and the question a person actually has after | |
| 2177 | * pressing this is "what happened to the one I meant". | |
| 2178 | */ | |
| 2179 | export function projectResult (r, message = null) { | |
| 2180 | const warn = message ? '<p class="warn">' + esc(message) + '</p>' : '' | |
| 2181 | const head = r.refused | |
| 2182 | ? '<p class="warn">Nothing was published: ' + esc(r.refused) + '</p>' | |
| 2183 | : '<p><strong>' + (r.sent || 0) + '</strong> published · ' + (r.skipped || 0) + ' already there · ' + | |
| 2184 | (r.withheld || 0) + ' withheld' + (r.held ? ' · ' + r.held + ' held back' : '') + '</p>' | |
| 2185 | const log = Array.isArray(r.log) && r.log.length | |
| 2186 | ? '<pre>' + esc(r.log.join('\n')) + '</pre>' | |
| 2187 | : '<p class="who">The run produced no output.</p>' | |
| 2188 | return layout('publish to bluesky', warn + '<h1>Publishing to Bluesky</h1>' + head + log + | |
| 2189 | '<p><a href="/settings">back to settings</a></p>') | |
| 2148 | 2190 | } |
| 2149 | 2191 | |
| 2150 | 2192 | /** |
| … | … | @@ -887,6 +887,30 @@ export async function route (req, res, ctx) { |
| 887 | 887 | } |
| 888 | 888 | } |
| 889 | 889 | |
| 890 | // ⛔ THE STEP NOTHING ELSE PERFORMS. Every other verb on the Bluesky card is about the IDENTITY — | |
| 891 | // minting it, publishing it, having a bridge carry it, consenting to use it. None of them authors a | |
| 892 | // record, and the keyless membrane serves only what this device signed, so all four can succeed | |
| 893 | // while the repo stays empty. That is not a hypothetical: a handset reached "carried by | |
| 894 | // fleeky.7rnx.net", relay active, switch on, with one bootstrap record in its repo and no profile. | |
| 895 | // | |
| 896 | // ⚠ The daemon's own log is rendered rather than summarised — the withheld lines say WHY a post did | |
| 897 | // not go, and that is the question a person has after pressing this. | |
| 898 | if (req.method === 'POST' && path === '/did/project') { | |
| 899 | const form = await body(req) | |
| 900 | // ⛔ SEND IS OPT-IN PER PRESS. The dry-run button submits an empty value, so anything other than an | |
| 901 | // explicit "1" plans and publishes nothing — publication onto a network that does not forget is | |
| 902 | // not a thing to do because a form field was missing. | |
| 903 | // ⚠ NOT named `send` — that is this router's own response helper, and shadowing it here turns | |
| 904 | // every successful run into "send is not a function" at the moment it reports what it published. | |
| 905 | const doSend = String(form.send || '') === '1' | |
| 906 | try { | |
| 907 | const r = await client.call('bsky-project', { send: doSend }) | |
| 908 | return send(res, pages.projectResult(r)) | |
| 909 | } catch (err) { | |
| 910 | return seeOther(res, '/settings?m=' + encodeURIComponent(err.message)) | |
| 911 | } | |
| 912 | } | |
| 913 | ||
| 890 | 914 | if (req.method === 'GET' && path === '/bridge/release') { |
| 891 | 915 | const reach = await client.call('reach').catch(() => null) |
| 892 | 916 | if (!reach || !reach.fediverse) return seeOther(res, '/settings') |
| … | … | @@ -0,0 +1,63 @@ |
| 1 | // The step that actually publishes, and why the card needs a fourth one. | |
| 2 | // | |
| 3 | // ⛔ CONSENT IS NOT AUTHORSHIP. Step 3's switch records that posts MAY go to Bluesky. Nothing about it | |
| 4 | // writes a record. The fediverse bridge HOLDS the feed and projects from it server-side, so for that | |
| 5 | // network the switch really is the last step — but Bluesky's membrane is keyless and serves only what | |
| 6 | // the device SIGNED, so until the device authors them the repo stays empty. | |
| 7 | // | |
| 8 | // Measured on a handset, with every visible step reporting success: identity correct, handle | |
| 9 | // resolving, relay active, switch on — and an atproto repo holding one bootstrap record and no | |
| 10 | // profile. The card had no way to say "nothing has been published", because it had nothing to press. | |
| 11 | ||
| 12 | import test from 'brittle' | |
| 13 | import { blueskySection, projectResult } from '../lib/pages.js' | |
| 14 | ||
| 15 | const DID = { did: 'did:plc:example', handle: 'fleeky.7rnx.net', pdsUrl: 'https://pds.7rnx.net', sealed: true } | |
| 16 | const ON = { networks: ['bsky'], credential: 'did:plc:example' } | |
| 17 | ||
| 18 | test('★★★ the publish step appears once there is somewhere for it to go', (t) => { | |
| 19 | const html = blueskySection(DID, ON) | |
| 20 | t.ok(/4\. Publish what you have written/.test(html), 'the step is offered') | |
| 21 | t.ok(/action="\/did\/project"/.test(html), 'and posts to the verb that authors') | |
| 22 | t.ok(/name="send" value="1"/.test(html), 'with an explicit send') | |
| 23 | t.ok(/name="send" value=""/.test(html), 'and a dry run beside it, because this cannot be recalled') | |
| 24 | }) | |
| 25 | ||
| 26 | test('★★★ it is NOT offered when nothing could resolve what was published', (t) => { | |
| 27 | // ⛔ Publishing into an identity nobody carries writes records no one can resolve. | |
| 28 | const uncarried = blueskySection({ ...DID, handle: null }, ON) | |
| 29 | t.absent(/4\. Publish what you have written/.test(uncarried), 'no bridge carries it yet') | |
| 30 | ||
| 31 | // ⛔ And never against withheld consent — the button would be publishing something the switch says | |
| 32 | // must not be published. | |
| 33 | const off = blueskySection(DID, { networks: [], credential: 'did:plc:example' }) | |
| 34 | t.absent(/4\. Publish what you have written/.test(off), 'the switch is off') | |
| 35 | }) | |
| 36 | ||
| 37 | test('★★ it says that existing posts go too, not only new ones', (t) => { | |
| 38 | const html = blueskySection(DID, ON) | |
| 39 | t.ok(/existing posts, not just new ones/.test(html), | |
| 40 | 'a person pressing this is publishing their history, and must be told so before they do') | |
| 41 | }) | |
| 42 | ||
| 43 | test('★★★ the result shows the daemon\'s own lines, not a count', (t) => { | |
| 44 | // The withheld lines are the interesting half: "too long for bsky", "a members room reaches | |
| 45 | // nothing". A summary cannot answer "what happened to the one I meant". | |
| 46 | const html = projectResult({ | |
| 47 | sent: 1, | |
| 48 | skipped: 2, | |
| 49 | withheld: 1, | |
| 50 | log: [' 1788 [favela,bsky] bsky: SENT 3mvb36hp7mc22', ' 1789 [favela] bsky: withheld — too long for bsky'] | |
| 51 | }) | |
| 52 | t.ok(/1<\/strong> published/.test(html)) | |
| 53 | t.ok(/already there/.test(html)) | |
| 54 | t.ok(/withheld — too long for bsky/.test(html), 'the reason survives to the page') | |
| 55 | }) | |
| 56 | ||
| 57 | test('★★ a refusal is reported as publishing nothing, not as a zero', (t) => { | |
| 58 | // ⛔ "could not read what the repo holds" and "there was nothing to send" are different facts, and | |
| 59 | // reporting the first as `0 published` invites pressing it again until it duplicates everything. | |
| 60 | const html = projectResult({ refused: 'could not read what the bsky sink already holds', log: [] }) | |
| 61 | t.ok(/Nothing was published/.test(html)) | |
| 62 | t.ok(/could not read what the bsky sink already holds/.test(html)) | |
| 63 | }) |