fedipod 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/bin/fedipod.mjs +4 -4
- package/groups.md +3 -3
- package/gui.md +28 -2
- package/lib/acctfeed.mjs +257 -0
- package/lib/admin.mjs +145 -3
- package/lib/bskygroup.mjs +1 -1
- package/lib/fediacct.mjs +257 -0
- package/lib/mastoapi.mjs +101 -2
- package/lib/store.mjs +26 -1
- package/lib/wire.mjs +2 -2
- package/package.json +1 -1
- package/phanpy/dist/assets/main-BdqNbG-a.js +1 -1
- package/phanpy/dist/compose/index.html +1 -1
- package/phanpy/dist/index.html +1 -1
- package/phanpy/dist/sw.js +2 -25
- package/phanpy/dist/sw.js.map +1 -0
- package/run-agent.mjs +31 -0
- package/scripts/verify-ui.mjs +7 -4
- package/web/admin/admin.js +81 -22
- package/web/admin/index.html +37 -8
package/web/admin/admin.js
CHANGED
|
@@ -28,7 +28,7 @@ const MODERATION = $('moderation');
|
|
|
28
28
|
const STATUS = $('status-ctl');
|
|
29
29
|
const STATUS_PICK = $('status-pick'); // held: getElementById can't see it mid-render
|
|
30
30
|
const BSKY_CTL = $('bsky-ctl');
|
|
31
|
-
const
|
|
31
|
+
const IDENT_CTL = $('ident-ctl');
|
|
32
32
|
const FOLLOWS_CTL = $('follows-ctl');
|
|
33
33
|
const FOLLOWS_PICK = $('follows-pick');
|
|
34
34
|
const UPDATE_CTL = $('update-ctl');
|
|
@@ -113,6 +113,49 @@ document.getElementById('win').addEventListener('win:closed', () => {
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
// One row per identity connected elsewhere, indented under the row that adds
|
|
117
|
+
// them. Each carries its own way out: disconnecting one has nothing to do with
|
|
118
|
+
// the others.
|
|
119
|
+
function identityRows() {
|
|
120
|
+
const out = [];
|
|
121
|
+
const row = (label, ...nodes) => {
|
|
122
|
+
const dt = document.createElement('dt');
|
|
123
|
+
dt.className = 'under';
|
|
124
|
+
dt.textContent = label;
|
|
125
|
+
const dd = document.createElement('dd');
|
|
126
|
+
dd.append(...nodes);
|
|
127
|
+
out.push([dt, dd]);
|
|
128
|
+
};
|
|
129
|
+
if (config.atproto?.connected) {
|
|
130
|
+
const a = document.createElement('a');
|
|
131
|
+
a.href = `https://bsky.app/profile/${config.atproto.handle}`;
|
|
132
|
+
a.textContent = `@${config.atproto.handle}`;
|
|
133
|
+
a.target = '_blank';
|
|
134
|
+
a.rel = 'noopener';
|
|
135
|
+
a.title = ` Open this account's Bluesky page in a new tab`;
|
|
136
|
+
BSKY_CTL.hidden = false;
|
|
137
|
+
BSKY_CROSSPOST.value = config.atproto.crossPost ? 'on' : 'off';
|
|
138
|
+
row('Bluesky', a, ' ', BSKY_CTL);
|
|
139
|
+
} else {
|
|
140
|
+
BSKY_CTL.hidden = true;
|
|
141
|
+
}
|
|
142
|
+
for (const acct of config.fediAccounts || []) {
|
|
143
|
+
const name = document.createElement('span');
|
|
144
|
+
name.textContent = acct.needsReconnect ? `${acct.handle} — sign in again` : acct.handle;
|
|
145
|
+
if (acct.needsReconnect) name.className = 'warn';
|
|
146
|
+
const off = document.createElement('button');
|
|
147
|
+
off.type = 'button';
|
|
148
|
+
off.className = 'inline danger';
|
|
149
|
+
off.textContent = 'Disconnect';
|
|
150
|
+
off.title = ` Stop reading ${acct.handle} and remove its stored token`;
|
|
151
|
+
off.addEventListener('click', async () => {
|
|
152
|
+
if (await write('/fediacct/disconnect', { id: acct.id }, `${acct.handle} disconnected`)) await load();
|
|
153
|
+
});
|
|
154
|
+
row(acct.host, name, ' ', off);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
116
159
|
function render() {
|
|
117
160
|
// The bar names the actor, the same way on every page. Only the tab title is
|
|
118
161
|
// this page's own business.
|
|
@@ -134,7 +177,7 @@ function render() {
|
|
|
134
177
|
['Fediverse identity', config.address || `@${config.handle} — no resolvable address`,
|
|
135
178
|
config.accountId && config.address ? { href: `/admin/client/#/a/${config.accountId}` } : null],
|
|
136
179
|
['Solid identity', config.webId, config.remotePod ? { href: config.remotePod, blank: true } : null],
|
|
137
|
-
['
|
|
180
|
+
['Other identities', 'ctl'],
|
|
138
181
|
['local store', config.home],
|
|
139
182
|
// The address you actually open, not the bare number — the named origin when
|
|
140
183
|
// there is one, since that is what the client and the OAuth redirect use.
|
|
@@ -148,6 +191,8 @@ function render() {
|
|
|
148
191
|
// A person gates followers here; a group's gate is the joins control on its
|
|
149
192
|
// kind row, so the row would be a second switch for the same thing.
|
|
150
193
|
if (config.kind !== 'group') rows.splice(2, 0, ['new followers', 'ctl']);
|
|
194
|
+
// Rows that belong under the one being written, appended after it.
|
|
195
|
+
let kids = null;
|
|
151
196
|
for (const [k, v, link] of rows) {
|
|
152
197
|
if (!v) continue;
|
|
153
198
|
const dt = document.createElement('dt');
|
|
@@ -207,27 +252,19 @@ function render() {
|
|
|
207
252
|
UPDATE_WORD.textContent = words.join('; ');
|
|
208
253
|
UPDATE_GO.hidden = !u?.available;
|
|
209
254
|
}
|
|
210
|
-
//
|
|
211
|
-
|
|
255
|
+
// The way to add one. Each account already connected is a row of its own
|
|
256
|
+
// underneath, so this row stays the action and never becomes a list.
|
|
257
|
+
if (k === 'Other identities') {
|
|
212
258
|
dd.textContent = '';
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
a.textContent = `@${config.atproto.handle}`;
|
|
217
|
-
a.target = '_blank';
|
|
218
|
-
a.rel = 'noopener';
|
|
219
|
-
a.title = ` Open this account's Bluesky page in a new tab`;
|
|
220
|
-
dd.append(a, ' ', BSKY_CTL);
|
|
221
|
-
BSKY_CTL.hidden = false;
|
|
222
|
-
BSKY_CONNECT_CTL.hidden = true;
|
|
223
|
-
BSKY_CROSSPOST.value = config.atproto.crossPost ? 'on' : 'off';
|
|
224
|
-
} else {
|
|
225
|
-
dd.append(BSKY_CONNECT_CTL);
|
|
226
|
-
BSKY_CONNECT_CTL.hidden = false;
|
|
227
|
-
BSKY_CTL.hidden = true;
|
|
228
|
-
}
|
|
259
|
+
dd.append(IDENT_CTL);
|
|
260
|
+
IDENT_CTL.hidden = false;
|
|
261
|
+
kids = identityRows();
|
|
229
262
|
}
|
|
230
263
|
facts.append(dt, dd);
|
|
264
|
+
if (kids) {
|
|
265
|
+
for (const [ckt, ckd] of kids) facts.append(ckt, ckd);
|
|
266
|
+
kids = null;
|
|
267
|
+
}
|
|
231
268
|
}
|
|
232
269
|
if (!config.address) {
|
|
233
270
|
const p = document.createElement('p');
|
|
@@ -420,9 +457,17 @@ function setRail(closed) {
|
|
|
420
457
|
$('rail-toggle').addEventListener('click', () => setRail(!$('rail').classList.contains('closed')));
|
|
421
458
|
if (matchMedia('(max-width: 47rem)').matches) setRail(true);
|
|
422
459
|
|
|
423
|
-
|
|
460
|
+
// One way in for both networks, because from the record they are the same
|
|
461
|
+
// thing: an account elsewhere whose timeline joins the one you read here.
|
|
462
|
+
$('ident-open').addEventListener('click', () => {
|
|
424
463
|
closePanels();
|
|
425
|
-
solWindow.show('
|
|
464
|
+
solWindow.show('ident-form', 'Connect an identity');
|
|
465
|
+
});
|
|
466
|
+
$('ident-pick-fedi').addEventListener('click', () => {
|
|
467
|
+
solWindow.show('fediacct-form', 'Connect a Fediverse account');
|
|
468
|
+
});
|
|
469
|
+
$('ident-pick-bsky').addEventListener('click', () => {
|
|
470
|
+
solWindow.show('bsky-form', 'Connect a Bluesky account');
|
|
426
471
|
});
|
|
427
472
|
|
|
428
473
|
let bskyBusy = false;
|
|
@@ -443,6 +488,20 @@ $('bsky-form').addEventListener('submit', async (ev) => {
|
|
|
443
488
|
}
|
|
444
489
|
} finally { bskyBusy = false; }
|
|
445
490
|
});
|
|
491
|
+
// The sign-in happens at the other server, so this hands the browser over
|
|
492
|
+
// rather than taking a password. What comes back lands on /fediacct/callback,
|
|
493
|
+
// which this machine answers and nowhere else does.
|
|
494
|
+
let fediBusy = false;
|
|
495
|
+
$('fediacct-form').addEventListener('submit', async (ev) => {
|
|
496
|
+
ev.preventDefault();
|
|
497
|
+
if (fediBusy) return;
|
|
498
|
+
fediBusy = true;
|
|
499
|
+
try {
|
|
500
|
+
const r = await write('/fediacct/connect', { host: $('fediacct-host').value.trim() }, '');
|
|
501
|
+
if (r?.authorize) location.href = r.authorize;
|
|
502
|
+
} finally { fediBusy = false; }
|
|
503
|
+
});
|
|
504
|
+
|
|
446
505
|
$('bsky-disconnect').addEventListener('click', async () => {
|
|
447
506
|
if (bskyBusy) return;
|
|
448
507
|
bskyBusy = true;
|
package/web/admin/index.html
CHANGED
|
@@ -133,6 +133,9 @@ dl { margin: .5rem 0; } /* grid only above 34rem — see the media query
|
|
|
133
133
|
gap: .6rem .9rem; align-items: baseline; }
|
|
134
134
|
}
|
|
135
135
|
dt { color: var(--muted); }
|
|
136
|
+
/* An identity connected under "Other identities" belongs to the row above it,
|
|
137
|
+
and the indent is what says so. */
|
|
138
|
+
dt.under { padding-left: 1.25rem; }
|
|
136
139
|
dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
137
140
|
.rows { list-style: none; padding: 0; margin: .3rem 0; }
|
|
138
141
|
.rows li { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
|
|
@@ -216,16 +219,16 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
216
219
|
<label class="choice"><input type="radio" name="newKind" value="person" checked>
|
|
217
220
|
A <strong>person</strong> — an account you read and post from</label>
|
|
218
221
|
<label class="choice"><input type="radio" name="newKind" value="group">
|
|
219
|
-
A <strong>group</strong> of
|
|
222
|
+
A <strong>group</strong> of Fediverse users you manage</label>
|
|
220
223
|
</fieldset>
|
|
221
224
|
|
|
222
225
|
<fieldset>
|
|
223
226
|
<legend>Actor's Handle</legend>
|
|
224
227
|
<label for="new-handle">Handle</label>
|
|
225
228
|
<input type="text" id="new-handle" autocomplete="off"
|
|
226
|
-
placeholder="handle part of
|
|
229
|
+
placeholder="handle part of Fediverse address: @HANDLE@server"
|
|
227
230
|
aria-describedby="new-handle-hint">
|
|
228
|
-
<p class="hint" id="new-handle-hint">The name in your
|
|
231
|
+
<p class="hint" id="new-handle-hint">The name in your Fediverse address. This is permanent.
|
|
229
232
|
Letters, digits, hyphens, and underscores only.</p>
|
|
230
233
|
</fieldset>
|
|
231
234
|
|
|
@@ -303,8 +306,8 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
303
306
|
<option value="auto">accepted automatically</option>
|
|
304
307
|
</select>
|
|
305
308
|
</span>
|
|
306
|
-
<span id="
|
|
307
|
-
<button type="button" id="
|
|
309
|
+
<span id="ident-ctl" hidden>
|
|
310
|
+
<button type="button" id="ident-open" class="inline" title=" Connect an account you hold on another network, so its timeline joins the one you read here">Connect an identity</button>
|
|
308
311
|
</span>
|
|
309
312
|
<span id="bsky-ctl" hidden>
|
|
310
313
|
<select id="bsky-crosspost" aria-label="Bluesky crossposting" title=" Whether your public posts also go to the Bluesky account">
|
|
@@ -344,7 +347,7 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
344
347
|
on arrival. Shown only when something is actually waiting. -->
|
|
345
348
|
<div id="block-modqueue" hidden>
|
|
346
349
|
<h3>Moderation requests <span class="muted" id="modqueue-count"></span></h3>
|
|
347
|
-
<p class="hint">Asked for by a moderator over the
|
|
350
|
+
<p class="hint">Asked for by a moderator over the Fediverse. A delivery cannot prove who
|
|
348
351
|
sent it, so nothing here has been acted on.</p>
|
|
349
352
|
<ul class="rows" role="list" id="modqueue"></ul>
|
|
350
353
|
</div>
|
|
@@ -423,20 +426,46 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
423
426
|
<button id="bsky-connect" type="submit" class="primary" title=" Sign in to the Bluesky account with the app password">Connect</button>
|
|
424
427
|
</p>
|
|
425
428
|
</form>
|
|
429
|
+
<form id="ident-form" hidden>
|
|
430
|
+
<p class="hint">Either kind brings that account's timeline and notifications
|
|
431
|
+
into the feed you read here, and acts as that account when you reply to,
|
|
432
|
+
favourite or boost one of its posts.</p>
|
|
433
|
+
<p>
|
|
434
|
+
<button type="button" id="ident-pick-fedi" class="primary" title=" Sign in at a Mastodon-API server: Mastodon, GoToSocial, Pleroma, Akkoma, Pixelfed or Friendica">A Fediverse account</button>
|
|
435
|
+
<button type="button" id="ident-pick-bsky" title=" Sign in to Bluesky or another ATProto server with an app password">A Bluesky account</button>
|
|
436
|
+
</p>
|
|
437
|
+
</form>
|
|
438
|
+
<form id="fediacct-form" hidden>
|
|
439
|
+
<ol class="hint">
|
|
440
|
+
<li>Name the server your other account is on.</li>
|
|
441
|
+
<li>That server asks you to sign in and to allow FediPod. It is the only
|
|
442
|
+
place your password is typed; this agent never sees or stores one.</li>
|
|
443
|
+
<li>Its timeline and notifications then join the ones you already read here,
|
|
444
|
+
and a like or a reply on one of its posts goes out as that account.</li>
|
|
445
|
+
<li>What arrives is held on this machine for reading. None of it is written
|
|
446
|
+
to your pod.</li>
|
|
447
|
+
</ol>
|
|
448
|
+
<label for="fediacct-host">server</label>
|
|
449
|
+
<input type="text" id="fediacct-host" placeholder="mastodon.social" autocomplete="off"
|
|
450
|
+
title=" The server your other account lives on — Mastodon, GoToSocial, Pleroma, Akkoma, Pixelfed and Friendica all speak the API this needs">
|
|
451
|
+
<p>
|
|
452
|
+
<button id="fediacct-connect" type="submit" class="primary" title=" Go to that server to sign in and allow FediPod">Continue to that server</button>
|
|
453
|
+
</p>
|
|
454
|
+
</form>
|
|
426
455
|
<!-- Every consequence is written here rather than assembled in script: the
|
|
427
456
|
page asking "are you sure" has to say what it is sure about, and this is
|
|
428
457
|
the only record of it. Script picks which block to show, nothing else. -->
|
|
429
458
|
<!-- The gateway popups: attach proves the pod with this agent's own credential;
|
|
430
459
|
detach republishes the pod's own inbox. -->
|
|
431
460
|
<form id="gateway-attach-form" hidden>
|
|
432
|
-
<p class="hint">A gateway filters your
|
|
461
|
+
<p class="hint">A gateway filters your Fediverse mail before it reaches your pod: each
|
|
433
462
|
delivery is verified at its door, spam is dropped, and the rest lands in your pod as
|
|
434
463
|
before. Attaching proves the pod with this agent's own credential — no password
|
|
435
464
|
leaves this machine.</p>
|
|
436
465
|
<label for="gw-front">gateway</label>
|
|
437
466
|
<input type="url" id="gw-front" placeholder="https://fedipod.net" autocomplete="off">
|
|
438
467
|
<fieldset>
|
|
439
|
-
<legend>Your
|
|
468
|
+
<legend>Your Fediverse handle</legend>
|
|
440
469
|
<label class="choice"><input type="radio" name="gwShape" value="pod" checked>
|
|
441
470
|
Keep my pod-based handle: <strong id="gw-pod-preview"></strong><br>
|
|
442
471
|
<span class="hint">Your address stays tied to your own pod — you can drop the
|