fedipod-server 0.11.0 → 0.13.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 +22 -6
- package/dist/claims.d.ts +8 -0
- package/dist/claims.js +10 -0
- package/dist/handler.d.ts +13 -0
- package/dist/handler.js +56 -11
- package/dist/handler.jsonld +8 -0
- package/dist/store-pod.js +18 -4
- package/lib/{c2s.mjs → client/c2s.mjs} +10 -3
- package/lib/{localapi.mjs → client/localapi.mjs} +2 -2
- package/lib/client/masto/accounts.mjs +264 -0
- package/lib/client/masto/body.mjs +69 -0
- package/lib/client/masto/index.mjs +183 -0
- package/lib/client/masto/instance.mjs +104 -0
- package/lib/client/masto/media.mjs +133 -0
- package/lib/client/masto/oauth.mjs +599 -0
- package/lib/client/masto/render.mjs +459 -0
- package/lib/client/masto/statuses.mjs +331 -0
- package/lib/client/masto/timelines.mjs +316 -0
- package/lib/{streaming.mjs → client/streaming.mjs} +1 -1
- package/lib/{acctfeed.mjs → connections/acctfeed.mjs} +1 -1
- package/lib/{atproto.mjs → connections/atproto.mjs} +15 -16
- package/lib/{bskygroup.mjs → connections/bskygroup.mjs} +1 -1
- package/lib/{fediacct.mjs → connections/fediacct.mjs} +31 -35
- package/lib/{import.mjs → connections/import.mjs} +1 -1
- package/lib/{tagfeed.mjs → connections/tagfeed.mjs} +3 -3
- package/lib/connections/vault.mjs +114 -0
- package/lib/core/as2.mjs +170 -0
- package/lib/core/contexts/activitystreams.json +379 -0
- package/lib/core/contexts/did-v1.json +57 -0
- package/lib/core/contexts/fep-5711.json +36 -0
- package/lib/core/contexts/gotosocial.json +86 -0
- package/lib/core/contexts/identity-v1.json +152 -0
- package/lib/core/contexts/index.mjs +45 -0
- package/lib/core/contexts/join-lemmy.json +33 -0
- package/lib/core/contexts/joinmastodon.json +28 -0
- package/lib/core/contexts/map.json +16 -0
- package/lib/core/contexts/miscellany.json +19 -0
- package/lib/core/contexts/schemaorg.json +8845 -0
- package/lib/core/contexts/security-data-integrity-v1.json +78 -0
- package/lib/core/contexts/security-data-integrity-v2.json +81 -0
- package/lib/core/contexts/security-multikey-v1.json +35 -0
- package/lib/core/contexts/security-v1.json +74 -0
- package/lib/core/contexts/webfinger.json +10 -0
- package/lib/{deliver.mjs → core/deliver.mjs} +2 -2
- package/lib/core/graphview.mjs +269 -0
- package/lib/core/intake/activities.mjs +437 -0
- package/lib/core/intake/activity.mjs +240 -0
- package/lib/core/intake/channel.mjs +144 -0
- package/lib/core/intake/group.mjs +222 -0
- package/lib/core/intake/index.mjs +629 -0
- package/lib/core/intake/notes.mjs +288 -0
- package/lib/core/intake/verify.mjs +142 -0
- package/lib/{keys.mjs → core/keys.mjs} +1 -1
- package/lib/core/publisher/collections.mjs +229 -0
- package/lib/core/publisher/index.mjs +421 -0
- package/lib/core/publisher/notes.mjs +188 -0
- package/lib/core/publisher/questions.mjs +233 -0
- package/lib/core/publisher/restore.mjs +199 -0
- package/lib/core/shapes/activitystreams.ttl +129 -0
- package/lib/core/shapes/index.mjs +107 -0
- package/lib/core/shapes/shapes-text.mjs +13 -0
- package/lib/{social.mjs → core/social.mjs} +2 -2
- package/lib/{store.mjs → core/store.mjs} +4 -0
- package/lib/{wire.mjs → core/wire.mjs} +2 -2
- package/lib/device/admin/index.mjs +13 -0
- package/lib/device/admin/origins.mjs +35 -0
- package/lib/device/admin/routes/connections.mjs +144 -0
- package/lib/device/admin/routes/gateway.mjs +199 -0
- package/lib/device/admin/routes/lifecycle.mjs +191 -0
- package/lib/device/admin/routes/owner.mjs +322 -0
- package/lib/device/admin/routes/setup.mjs +393 -0
- package/lib/device/admin/routes/social.mjs +188 -0
- package/lib/device/admin/server.mjs +95 -0
- package/lib/device/admin/static.mjs +244 -0
- package/lib/device/admin/surface.mjs +274 -0
- package/lib/device/cli/commands/account.mjs +586 -0
- package/lib/device/cli/commands/run.mjs +278 -0
- package/lib/device/cli/commands/service.mjs +221 -0
- package/lib/device/cli/commands/setup.mjs +410 -0
- package/lib/device/cli/commands/state.mjs +559 -0
- package/lib/device/cli/context.mjs +288 -0
- package/lib/{migrate.mjs → device/migrate.mjs} +1 -1
- package/lib/{remote.mjs → device/remote.mjs} +3 -3
- package/lib/{setup.mjs → device/setup.mjs} +3 -3
- package/lib/{update.mjs → device/update.mjs} +1 -1
- package/lib/{directory.mjs → gateway/directory.mjs} +1 -1
- package/lib/{front-core.mjs → gateway/front-core.mjs} +3 -3
- package/lib/{gateway-core.mjs → gateway/gateway-core.mjs} +1 -1
- package/lib/{httpsig.mjs → gateway/httpsig.mjs} +1 -1
- package/lib/server/embed.mjs +405 -0
- package/lib/{links.mjs → shared/links.mjs} +1 -1
- package/lib/{ua.mjs → shared/ua.mjs} +1 -1
- package/package.json +1 -1
- package/run-agent.mjs +33 -25
- package/web/admin/actors.js +145 -0
- package/web/admin/common.js +23 -0
- package/web/admin/connections.js +112 -0
- package/web/admin/gateway.js +111 -0
- package/web/admin/group.js +258 -0
- package/web/admin/index.html +7 -1
- package/web/admin/record.js +378 -0
- package/web/admin/setup/index.html +1 -0
- package/web/admin/setup/setup.js +2 -13
- package/web/admin/upkeep.js +170 -0
- package/web/app/README.md +6 -6
- package/web/app/admin-facade.mjs +3 -3
- package/web/app/agent.mjs +14 -16
- package/web/app/atproto-browser.mjs +1 -1
- package/web/app/boot.mjs +2 -3
- package/web/app/deliver-relay.mjs +1 -1
- package/web/app/dist/boot.js +22 -3
- package/web/app/dist/boot.js.map +2 -2
- package/web/app/dist/sw.js +21913 -5446
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/fediacct-browser.mjs +1 -1
- package/web/app/keys-browser.mjs +27 -4
- package/web/app/shims/shapes-text.mjs +8 -0
- package/web/app/signup.mjs +2 -3
- package/web/app/site/admin/actors.js +145 -0
- package/web/app/site/admin/common.js +23 -0
- package/web/app/site/admin/connections.js +112 -0
- package/web/app/site/admin/gateway.js +111 -0
- package/web/app/site/admin/group.js +258 -0
- package/web/app/site/admin/index.html +7 -1
- package/web/app/site/admin/record.js +378 -0
- package/web/app/site/admin/setup/index.html +1 -0
- package/web/app/site/admin/setup/setup.js +2 -13
- package/web/app/site/admin/upkeep.js +170 -0
- package/web/app/site/boot.js +22 -3
- package/web/app/site/sw.js +21913 -5446
- package/web/app/sw-src.mjs +17 -2
- package/lib/admin.mjs +0 -1913
- package/lib/embed.mjs +0 -220
- package/lib/intake.mjs +0 -1981
- package/lib/mastoapi.mjs +0 -2284
- package/lib/publisher.mjs +0 -1192
- package/web/admin/admin.js +0 -1181
- package/web/app/site/admin/admin.js +0 -1181
- /package/lib/{oidc-auth.mjs → client/oidc-auth.mjs} +0 -0
- /package/lib/{webpush.mjs → client/webpush.mjs} +0 -0
- /package/lib/{bskyfeed.mjs → connections/bskyfeed.mjs} +0 -0
- /package/lib/{lease.mjs → core/lease.mjs} +0 -0
- /package/lib/{polls.mjs → core/polls.mjs} +0 -0
- /package/lib/{proof.mjs → core/proof.mjs} +0 -0
- /package/lib/{storage.mjs → core/storage.mjs} +0 -0
- /package/lib/{account.mjs → device/account.mjs} +0 -0
- /package/lib/{certs.mjs → device/certs.mjs} +0 -0
- /package/lib/{export-collections.mjs → device/export-collections.mjs} +0 -0
- /package/lib/{home.mjs → device/home.mjs} +0 -0
- /package/lib/{ports.mjs → device/ports.mjs} +0 -0
- /package/lib/{guard.mjs → shared/guard.mjs} +0 -0
- /package/lib/{safefetch.mjs → shared/safefetch.mjs} +0 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
// account.mjs — the account over its life, through the running agent's
|
|
2
|
+
// admin API: park, revive, retire, gateway, describe, alias, import, rebuild,
|
|
3
|
+
// the group operator's verbs, archive, bsky.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { localFetch } from '../../../client/localapi.mjs';
|
|
8
|
+
import { args, cmd, flag, has, HOME, PORT, requireIdentity, ask, endAsking, finish } from '../context.mjs';
|
|
9
|
+
|
|
10
|
+
export async function parkRevive() {
|
|
11
|
+
// Park is quiesce plus a snapshot of the follow graph, because unfollowing is
|
|
12
|
+
// what stops the traffic and also what destroys the record needed to come back.
|
|
13
|
+
const { Agent } = await import(new URL('../../../../run-agent.mjs', import.meta.url));
|
|
14
|
+
const agent = new Agent({ home: HOME, log: (...a) => console.log(`[${cmd}]`, ...a) });
|
|
15
|
+
// Read-only until you say yes: connecting for real acquires the lease and
|
|
16
|
+
// starts the whole active agent — a destructive inbox drain, a channel
|
|
17
|
+
// subscription, ACL probes and a tag-feed sweep — before the prompt.
|
|
18
|
+
if (!await agent.connect({ act: false })) {
|
|
19
|
+
console.error(`nothing to ${cmd} — no configured, un-retired agent in this AP_HOME`);
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
const cfg = agent.store.getConfig();
|
|
23
|
+
const host = new URL(cfg.remotePod).host;
|
|
24
|
+
|
|
25
|
+
if (cmd === 'park') {
|
|
26
|
+
const contacts = agent.store.getContacts();
|
|
27
|
+
console.log(`\nParking @${cfg.handle}@${host}\n`);
|
|
28
|
+
console.log(` · ${contacts.following.length} account(s) unfollowed, and remembered so revive can undo it`);
|
|
29
|
+
console.log(' · the inbox is closed: deliveries are refused outright, not stored');
|
|
30
|
+
console.log(` · ${contacts.followers.length} follower(s) keep following you — nothing is told you left`);
|
|
31
|
+
console.log(' · the handle keeps resolving; posts and RDF stay where they are');
|
|
32
|
+
console.log(' · a parked agent that gets started will not drain or poll\n');
|
|
33
|
+
console.log('Quietest state short of retiring. Undo with: fedipod revive\n');
|
|
34
|
+
const ans = has('yes') ? 'y' : await ask('park this actor? (y/n)', 'n');
|
|
35
|
+
endAsking();
|
|
36
|
+
if (!/^y/i.test(ans)) { console.log('nothing changed'); process.exit(0); }
|
|
37
|
+
await agent.connect(); // now it may act
|
|
38
|
+
const r = await agent.park();
|
|
39
|
+
console.log(`parked: unfollowed ${r.unfollowed}/${r.following}, ${r.snapshot} remembered, inbox closed`);
|
|
40
|
+
} else {
|
|
41
|
+
const parked = agent.store.read('parked.json', null);
|
|
42
|
+
console.log(`\nReviving @${cfg.handle}@${host}${parked ? ` (parked ${parked.parkedAt})` : ''}\n`);
|
|
43
|
+
console.log(` · the inbox re-opens`);
|
|
44
|
+
console.log(` · ${parked?.following?.length || 0} Follow(s) are re-sent — each needs the far side to accept\n`);
|
|
45
|
+
const ans = has('yes') ? 'y' : await ask('revive this actor? (y/n)', 'y');
|
|
46
|
+
endAsking();
|
|
47
|
+
if (!/^y/i.test(ans)) { console.log('nothing changed'); process.exit(0); }
|
|
48
|
+
await agent.connect(); // now it may act
|
|
49
|
+
const r = await agent.revive();
|
|
50
|
+
console.log(`revived: inbox open, ${r.refollowed}/${r.of} follow(s) re-sent`);
|
|
51
|
+
}
|
|
52
|
+
await finish(agent);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function retire() {
|
|
56
|
+
// Without this an abandoned pod accepts fediverse deliveries forever into a
|
|
57
|
+
// container nobody will ever drain, and no remote server can tell.
|
|
58
|
+
const { Agent } = await import(new URL('../../../../run-agent.mjs', import.meta.url));
|
|
59
|
+
const { resolveHandle } = await import(new URL('../../../../lib/core/social.mjs', import.meta.url));
|
|
60
|
+
const agent = new Agent({ home: HOME, log: (...a) => console.log('[retire]', ...a) });
|
|
61
|
+
// Read-only until you say yes: connecting for real acquires the lease and
|
|
62
|
+
// starts the whole active agent — a destructive inbox drain, a channel
|
|
63
|
+
// subscription, ACL probes and a tag-feed sweep — before the prompt.
|
|
64
|
+
if (!await agent.connect({ act: false })) {
|
|
65
|
+
console.error('nothing to retire — no configured, un-retired agent in this AP_HOME');
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
const cfg = agent.store.getConfig();
|
|
69
|
+
const host = new URL(cfg.remotePod).host;
|
|
70
|
+
const contacts = agent.store.getContacts();
|
|
71
|
+
const followers = contacts.followers.length;
|
|
72
|
+
const following = contacts.following.length;
|
|
73
|
+
const moveTo = flag('move-to');
|
|
74
|
+
const keep = has('keep-handle') || !!moveTo;
|
|
75
|
+
|
|
76
|
+
console.log(`\n${keep ? 'Standing down' : 'Retiring'} @${cfg.handle}@${host}\n`);
|
|
77
|
+
if (moveTo) {
|
|
78
|
+
console.log(` · a Move goes to ${followers} follower inbox(es); their servers migrate them to ${moveTo}`);
|
|
79
|
+
console.log(` · the actor advertises movedTo, so the old handle resolves as a redirect`);
|
|
80
|
+
} else if (keep) {
|
|
81
|
+
console.log(' · the handle keeps resolving — webfinger, host-meta and the actor stay published');
|
|
82
|
+
} else {
|
|
83
|
+
console.log(` · a Delete goes to ${followers} follower inbox(es), telling those servers to drop the account`);
|
|
84
|
+
console.log(' · the actor document is replaced with a Tombstone');
|
|
85
|
+
console.log(' · this agent will refuse to start again for this pod');
|
|
86
|
+
}
|
|
87
|
+
if (keep) {
|
|
88
|
+
console.log(` · ${following} account(s) get unfollowed — that is what stops posts arriving`);
|
|
89
|
+
console.log(' · the inbox is closed, so anything else is refused rather than stored forever');
|
|
90
|
+
}
|
|
91
|
+
// revive() opens the inbox first and only then replays parked.json, which a
|
|
92
|
+
// stand-down never wrote — so it is the right undo here, minus the re-follows.
|
|
93
|
+
console.log(keep
|
|
94
|
+
? '\nReversible: fedipod revive re-opens the inbox. Standing down keeps no snapshot\n'
|
|
95
|
+
+ 'of the follow graph, unlike park, so following people again is on you.\n'
|
|
96
|
+
: '\nYour posts and RDF stay on the pod; the identity does not come back.\n');
|
|
97
|
+
|
|
98
|
+
const ans = has('yes') ? 'y' : await ask(
|
|
99
|
+
keep ? 'stand this actor down? (y/n)' : 'retire this actor? this cannot be undone (y/n)', 'n');
|
|
100
|
+
endAsking();
|
|
101
|
+
if (!/^y/i.test(ans)) { console.log('nothing changed'); process.exit(0); }
|
|
102
|
+
await agent.connect(); // now it may act
|
|
103
|
+
|
|
104
|
+
if (moveTo) {
|
|
105
|
+
// Accept either a full actor URL or @user@host.
|
|
106
|
+
const target = /^https?:\/\//.test(moveTo) ? moveTo : await resolveHandle(agent, moveTo);
|
|
107
|
+
const r = await agent.moveTo(target);
|
|
108
|
+
console.log(`moved to ${r.target}: Move delivered to ${r.inboxes} inbox(es), unfollowed ${r.unfollowed}/${r.following}`);
|
|
109
|
+
} else if (keep) {
|
|
110
|
+
const r = await agent.park(); // same thing, and revivable
|
|
111
|
+
console.log(`stood down ${r.quiescedAt}: unfollowed ${r.unfollowed}/${r.following}, inbox closed`);
|
|
112
|
+
console.log('undo with: fedipod revive');
|
|
113
|
+
} else {
|
|
114
|
+
const r = await agent.publisher.retireActor();
|
|
115
|
+
console.log(`retired ${r.deletedAt}: Delete delivered to ${r.inboxes} inbox(es)`);
|
|
116
|
+
}
|
|
117
|
+
await finish(agent);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function gateway() {
|
|
121
|
+
// Attach this identity to a gateway ('front' kept as an alias). Shapes:
|
|
122
|
+
// fedipod gateway --attach <origin> [--name N] [--fronted] ask the gateway
|
|
123
|
+
// itself: the agent proves the pod with its own credential and stores
|
|
124
|
+
// the door + secret the gateway answers with.
|
|
125
|
+
// fedipod gateway <.../ap/inbox/> --secret S --inbox-only paste-in form:
|
|
126
|
+
// identity stays on the pod; only the advertised inbox moves to the
|
|
127
|
+
// gateway's door. Leaving is one republish with the pod inbox.
|
|
128
|
+
// fedipod gateway <.../ap/actor> --secret S fronted identity
|
|
129
|
+
// fedipod gateway --detach back to the pod inbox
|
|
130
|
+
requireIdentity();
|
|
131
|
+
if (has('attach')) {
|
|
132
|
+
const front = flag('attach');
|
|
133
|
+
if (!/^https?:\/\/\S+$/.test(String(front || ''))) {
|
|
134
|
+
console.error('usage: fedipod gateway --attach <https://gateway-origin> [--name yourname] [--fronted]');
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const res = await localFetch(HOME, PORT, `/gateway`, {
|
|
139
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({ action: 'attach', front,
|
|
141
|
+
...(flag('name') ? { handle: flag('name') } : {}),
|
|
142
|
+
...(has('fronted') ? { fronted: true } : {}) }),
|
|
143
|
+
});
|
|
144
|
+
const body = await res.json();
|
|
145
|
+
if (res.status >= 400) { console.error(body.error || `HTTP ${res.status}`); process.exit(1); }
|
|
146
|
+
if (body.frontActor) {
|
|
147
|
+
console.log(`attached — this identity now publishes as ${body.address || body.frontActor}`);
|
|
148
|
+
console.log('The agent is restarting itself to publish under the front.');
|
|
149
|
+
} else {
|
|
150
|
+
console.log(`attached — your mail now arrives through ${body.url}, filtered; your name has not moved.`);
|
|
151
|
+
console.log('Starting in shadow: the door filters, and the agent measures how much');
|
|
152
|
+
console.log('verifies before it believes any receipt. Move to trust when you are ready.');
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.error(`agent not reachable on :${PORT} (${e.message}) — start it, then attach`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
if (has('detach')) {
|
|
161
|
+
try {
|
|
162
|
+
const res = await localFetch(HOME, PORT, `/gateway`, {
|
|
163
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
164
|
+
body: JSON.stringify({ action: 'forget' }),
|
|
165
|
+
});
|
|
166
|
+
const body = await res.json();
|
|
167
|
+
if (res.status >= 400) { console.error(body.error || `HTTP ${res.status}`); process.exit(1); }
|
|
168
|
+
console.log('detached — the actor was republished advertising your pod\'s own inbox');
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.error(`agent not reachable on :${PORT} (${e.message}) — start it, then detach`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
const target = args[1];
|
|
176
|
+
const secret = flag('secret');
|
|
177
|
+
const inboxOnly = has('inbox-only');
|
|
178
|
+
const okActor = /^https:\/\/\S+\/ap\/actor$/.test(String(target || ''));
|
|
179
|
+
const okInbox = /^https:\/\/\S+\/ap\/inbox\/?$/.test(String(target || ''));
|
|
180
|
+
if (inboxOnly ? !okInbox : !okActor) {
|
|
181
|
+
console.error(inboxOnly
|
|
182
|
+
? 'usage: fedipod gateway <https://host/u/<name>/ap/inbox/> --secret <hmac> --inbox-only'
|
|
183
|
+
: 'usage: fedipod gateway <https://host/u/<name>/ap/actor> --secret <hmac> [--inbox-only]');
|
|
184
|
+
console.error(' (the URL and secret come from the gateway\'s signup page)');
|
|
185
|
+
process.exit(2);
|
|
186
|
+
}
|
|
187
|
+
const { Agent } = await import(new URL('../../../../run-agent.mjs', import.meta.url));
|
|
188
|
+
const { RemotePod } = await import(new URL('../../../../lib/device/remote.mjs', import.meta.url));
|
|
189
|
+
const { apUrls } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
|
|
190
|
+
const agent = new Agent({ home: HOME, log: () => {} });
|
|
191
|
+
const cred = agent.readCredential();
|
|
192
|
+
if (!cred) { console.error('no credential — run setup first'); process.exit(2); }
|
|
193
|
+
agent.remote = new RemotePod(cred);
|
|
194
|
+
await agent.remote.warmup();
|
|
195
|
+
agent.urls = apUrls(cred.remotePod, cred.root);
|
|
196
|
+
agent.store.attach(agent.privateStorage(cred, 'state'));
|
|
197
|
+
await agent.store.load();
|
|
198
|
+
const config = agent.store.getConfig();
|
|
199
|
+
if (!config) { console.error('pod state empty — run setup first'); process.exit(2); }
|
|
200
|
+
if (config.gateway?.frontActor && (inboxOnly || config.gateway.frontActor !== target)) {
|
|
201
|
+
console.error(`this identity already fronts through ${config.gateway.frontActor}.`);
|
|
202
|
+
console.error('Changing a published front renames every id — that is a move, not an edit. Detach first if you mean it.');
|
|
203
|
+
process.exit(2);
|
|
204
|
+
}
|
|
205
|
+
const gateway = inboxOnly
|
|
206
|
+
? { ...(config.gateway || {}), url: target.replace(/\/?$/, '/'), mode: 'shadow',
|
|
207
|
+
...(secret ? { hmacSecret: secret } : {}) }
|
|
208
|
+
: { ...(config.gateway || {}), url: target.replace(/ap\/actor$/, 'ap/inbox/'),
|
|
209
|
+
frontActor: target, mode: 'shadow', ...(secret ? { hmacSecret: secret } : {}) };
|
|
210
|
+
agent.store.setConfig({ ...config, gateway });
|
|
211
|
+
await agent.store.flush();
|
|
212
|
+
if (inboxOnly) {
|
|
213
|
+
console.log('attached to the shared filter — your identity stays on your pod;');
|
|
214
|
+
console.log(`the actor will advertise ${gateway.url} as its inbox.`);
|
|
215
|
+
console.log('Starting in shadow: the door filters, and the agent measures how much');
|
|
216
|
+
console.log('verifies before it believes any receipt. Move to trust when you are ready.');
|
|
217
|
+
} else {
|
|
218
|
+
console.log(`attached to the front — this identity now publishes as ${target}`);
|
|
219
|
+
}
|
|
220
|
+
console.log('Restart the agent (or `fedipod up`) to republish the actor.');
|
|
221
|
+
if (!secret) console.log('No --secret given: pass the front\'s receipt secret to trust its verification.');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function describe() {
|
|
225
|
+
// The bio and the avatar. Both live in the actor document, so this republishes.
|
|
226
|
+
if (!flag('summary') && !flag('icon')) {
|
|
227
|
+
console.error('usage: fedipod describe --summary "what this is" --icon <url>');
|
|
228
|
+
process.exit(2);
|
|
229
|
+
}
|
|
230
|
+
const payload = {};
|
|
231
|
+
if (flag('summary') !== undefined) payload.summary = flag('summary');
|
|
232
|
+
if (flag('icon') !== undefined) payload.icon = flag('icon');
|
|
233
|
+
try {
|
|
234
|
+
const res = await localFetch(HOME, PORT, `/describe`, {
|
|
235
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload),
|
|
236
|
+
});
|
|
237
|
+
const body = await res.json();
|
|
238
|
+
if (res.status >= 400) { console.error(body.error || `HTTP ${res.status}`); process.exit(1); }
|
|
239
|
+
console.log(`described and republished — summary: ${body.summary ? '"' + body.summary + '"' : '(none)'}, icon: ${body.icon || '(none)'}`);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.error(`agent not reachable on :${PORT} (${e.message})`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function alias() {
|
|
247
|
+
// The migration landing pad: old accounts listed in alsoKnownAs, which their
|
|
248
|
+
// servers check before sending a Move here. Served by the running agent
|
|
249
|
+
// because adding one resolves the old account and republishes the actor.
|
|
250
|
+
requireIdentity();
|
|
251
|
+
try {
|
|
252
|
+
if (flag('add')) {
|
|
253
|
+
const res = await localFetch(HOME, PORT, `/alias`, {
|
|
254
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
255
|
+
body: JSON.stringify({ add: flag('add') }),
|
|
256
|
+
});
|
|
257
|
+
const body = await res.json();
|
|
258
|
+
if (res.status >= 400) { console.error(body.error || `HTTP ${res.status}`); process.exit(1); }
|
|
259
|
+
console.log('alias added and the actor republished — aliases now:');
|
|
260
|
+
for (const a of body.aliases) console.log(` ${a}`);
|
|
261
|
+
console.log('on the old server, Account → Move to a different account will now accept this one');
|
|
262
|
+
} else if (flag('remove')) {
|
|
263
|
+
const res = await localFetch(HOME, PORT, `/alias`, {
|
|
264
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({ remove: flag('remove'), ...(has('yes') ? { confirm: true } : {}) }),
|
|
266
|
+
});
|
|
267
|
+
const body = await res.json();
|
|
268
|
+
if (res.status === 409) { console.error(body.error); console.error('add --yes to remove it anyway'); process.exit(1); }
|
|
269
|
+
if (res.status >= 400) { console.error(body.error || `HTTP ${res.status}`); process.exit(1); }
|
|
270
|
+
console.log(body.aliases.length ? 'removed — aliases now:' : 'removed — no aliases left');
|
|
271
|
+
for (const a of body.aliases) console.log(` ${a}`);
|
|
272
|
+
} else {
|
|
273
|
+
const res = await localFetch(HOME, PORT, `/config`);
|
|
274
|
+
const body = await res.json();
|
|
275
|
+
if (res.status >= 400) { console.error(body.error || `HTTP ${res.status}`); process.exit(1); }
|
|
276
|
+
const aliases = body.aliases || [];
|
|
277
|
+
if (!aliases.length) console.log('no aliases — fedipod alias --add @you@old.server');
|
|
278
|
+
for (const a of aliases) console.log(a);
|
|
279
|
+
}
|
|
280
|
+
} catch (e) {
|
|
281
|
+
console.error(`agent not reachable on :${PORT} (${e.message})`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function importCmd() {
|
|
287
|
+
// The CSV exports the old account's server hands its leaver, staged with the
|
|
288
|
+
// running agent and applied by its paced worker. File names tell the kinds —
|
|
289
|
+
// the names Mastodon's export uses, which the rest of the family copies.
|
|
290
|
+
requireIdentity();
|
|
291
|
+
const NOVAL = new Set(['--clear', '--status']);
|
|
292
|
+
const files = [];
|
|
293
|
+
for (let i = 1; i < args.length; i++) {
|
|
294
|
+
const a = args[i];
|
|
295
|
+
if (a.startsWith('--')) { if (!NOVAL.has(a)) i++; continue; }
|
|
296
|
+
files.push(a);
|
|
297
|
+
}
|
|
298
|
+
const jpost = (body) => localFetch(HOME, PORT, `/import`, {
|
|
299
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
|
|
300
|
+
}).then(async r => ({ status: r.status, json: await r.json().catch(() => null) }));
|
|
301
|
+
const jget = () => localFetch(HOME, PORT, `/import`)
|
|
302
|
+
.then(async r => ({ status: r.status, json: await r.json().catch(() => null) }));
|
|
303
|
+
const showProgress = (p) => {
|
|
304
|
+
const parts = Object.entries(p.byKind || {}).map(([k, c]) =>
|
|
305
|
+
`${k} ${c.done}/${c.done + c.pending + c.failed}${c.failed ? ` (${c.failed} failed)` : ''}`);
|
|
306
|
+
console.log(parts.join(', ') || 'nothing staged');
|
|
307
|
+
};
|
|
308
|
+
const kindOf = (file) => {
|
|
309
|
+
const b = path.basename(file).toLowerCase();
|
|
310
|
+
if (b.includes('following')) return 'follow';
|
|
311
|
+
if (b.includes('domain')) return 'domain';
|
|
312
|
+
if (b.includes('block')) return 'block';
|
|
313
|
+
if (b.includes('mute')) return 'mute';
|
|
314
|
+
if (b.includes('list')) return 'list';
|
|
315
|
+
if (b.includes('bookmark')) return 'bookmark';
|
|
316
|
+
return null;
|
|
317
|
+
};
|
|
318
|
+
try {
|
|
319
|
+
if (has('clear')) {
|
|
320
|
+
const r = await jpost({ clear: true });
|
|
321
|
+
if (r.status >= 400) { console.error(r.json?.error || `HTTP ${r.status}`); process.exit(1); }
|
|
322
|
+
console.log('import record cleared');
|
|
323
|
+
} else if (!files.length) {
|
|
324
|
+
const r = await jget();
|
|
325
|
+
if (r.status >= 400) { console.error(r.json?.error || `HTTP ${r.status}`); process.exit(1); }
|
|
326
|
+
if (!r.json.rows) {
|
|
327
|
+
console.log('nothing imported yet — usage: fedipod import <csv-file…> [--kind follow|block|mute|list|domain]');
|
|
328
|
+
} else {
|
|
329
|
+
showProgress(r.json);
|
|
330
|
+
for (const f of r.json.failures || []) console.log(` failed ${f.kind} ${f.value}: ${f.reason}`);
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
for (const file of files) {
|
|
334
|
+
const kind = flag('kind') || kindOf(file);
|
|
335
|
+
if (kind === 'bookmark') { console.log(`${file}: bookmarks are not imported — skipped`); continue; }
|
|
336
|
+
if (!['follow', 'block', 'mute', 'list', 'domain'].includes(kind)) {
|
|
337
|
+
console.error(`${file}: cannot tell what this is from its name — pass --kind follow|block|mute|list|domain`);
|
|
338
|
+
process.exit(2);
|
|
339
|
+
}
|
|
340
|
+
let text;
|
|
341
|
+
try { text = fs.readFileSync(file, 'utf8'); }
|
|
342
|
+
catch (e) { console.error(`${file}: ${e.message}`); process.exit(2); }
|
|
343
|
+
// Stay under the agent's request cap, splitting on line boundaries —
|
|
344
|
+
// but never inside a quoted field, where a newline is field content:
|
|
345
|
+
// a boundary is only taken while the quotes seen so far are balanced.
|
|
346
|
+
const chunks = [];
|
|
347
|
+
let chunk = [], size = 0, quotes = 0;
|
|
348
|
+
for (const line of text.split('\n')) {
|
|
349
|
+
if (size + line.length > 800_000 && chunk.length && quotes % 2 === 0) {
|
|
350
|
+
chunks.push(chunk.join('\n')); chunk = []; size = 0;
|
|
351
|
+
}
|
|
352
|
+
chunk.push(line);
|
|
353
|
+
size += line.length + 1;
|
|
354
|
+
quotes += (line.match(/"/g) || []).length;
|
|
355
|
+
}
|
|
356
|
+
if (chunk.length) chunks.push(chunk.join('\n'));
|
|
357
|
+
let staged = 0, duplicate = 0, already = 0, invalid = 0;
|
|
358
|
+
for (const c of chunks) {
|
|
359
|
+
const r = await jpost({ kind, text: c });
|
|
360
|
+
if (r.status >= 400) { console.error(`${file}: ${r.json?.error || `HTTP ${r.status}`}`); process.exit(1); }
|
|
361
|
+
staged += r.json.staged; duplicate += r.json.duplicate;
|
|
362
|
+
already += r.json.already; invalid += r.json.invalid;
|
|
363
|
+
}
|
|
364
|
+
console.log(`${file}: staged ${staged} ${kind} row(s)`
|
|
365
|
+
+ (already ? `, ${already} already applied` : '')
|
|
366
|
+
+ (duplicate ? `, ${duplicate} already staged` : '')
|
|
367
|
+
+ (invalid ? `, ${invalid} unreadable` : ''));
|
|
368
|
+
}
|
|
369
|
+
// Watch it run. The worker paces itself, so a big list takes a while;
|
|
370
|
+
// ctrl-c leaves it running and `fedipod import` shows where it is.
|
|
371
|
+
let last = '';
|
|
372
|
+
for (;;) {
|
|
373
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
374
|
+
const r = await jget();
|
|
375
|
+
if (r.status >= 400) break;
|
|
376
|
+
const p = r.json;
|
|
377
|
+
const line = `applied ${p.done} of ${p.rows}${p.failed ? ` (${p.failed} failed)` : ''}`;
|
|
378
|
+
if (line !== last) { console.log(line); last = line; }
|
|
379
|
+
if (!p.pending) {
|
|
380
|
+
for (const f of (p.failures || []).slice(0, 20)) console.log(` failed ${f.kind} ${f.value}: ${f.reason}`);
|
|
381
|
+
if ((p.failures || []).length > 20) {
|
|
382
|
+
console.log(` … and ${p.failures.length - 20} more — \`fedipod import\` lists them`);
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} catch (e) {
|
|
389
|
+
console.error(`agent not reachable on :${PORT} (${e.message})`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function rebuild() {
|
|
395
|
+
requireIdentity();
|
|
396
|
+
// Served by the running agent because it needs the lease: it writes the
|
|
397
|
+
// statuses store, and two agents writing it is the thing the lease prevents.
|
|
398
|
+
try {
|
|
399
|
+
const res = await localFetch(HOME, PORT, `/rebuild`, {
|
|
400
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
401
|
+
body: JSON.stringify({ fromNotes: has('from-notes') }),
|
|
402
|
+
});
|
|
403
|
+
const body = await res.json();
|
|
404
|
+
if (res.status >= 400) { console.error(body.error || `HTTP ${res.status}`); process.exit(1); }
|
|
405
|
+
if (body.why) { console.error(body.why); process.exit(1); }
|
|
406
|
+
console.log(`the pod indexed ${body.indexed} post(s); recovered ${body.recovered}`
|
|
407
|
+
+ `${body.reblogs ? `, and marked ${body.reblogs} of them boosted` : ''}`);
|
|
408
|
+
if (body.dropped) console.log(`${body.dropped} fell past the 1000-status cap`);
|
|
409
|
+
if (!body.landed) { console.error('the state write did NOT land — nothing is saved'); process.exit(1); }
|
|
410
|
+
if (!body.recovered && !has('from-notes')) {
|
|
411
|
+
console.log('Nothing was missing. `--from-notes` looks past the outbox, at every note the pod still holds.');
|
|
412
|
+
}
|
|
413
|
+
} catch (e) {
|
|
414
|
+
console.error(`agent not reachable on :${PORT} (${e.message})`);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export async function group() {
|
|
420
|
+
// Group operator commands, served by the running agent's admin API.
|
|
421
|
+
const GETS = ['members', 'announced', 'pending', 'requests', 'modqueue'];
|
|
422
|
+
const BY_ACTOR = ['mute', 'unmute', 'eject', 'admit', 'refuse'];
|
|
423
|
+
const TOGGLES = { review: ['on', 'off'], joins: ['open', 'approve'] };
|
|
424
|
+
const post = !GETS.includes(cmd);
|
|
425
|
+
const admitAll = cmd === 'admit' && has('all');
|
|
426
|
+
const modAct = cmd === 'modqueue' ? (flag('apply') || flag('dismiss') || null) : null;
|
|
427
|
+
const modVerb = cmd === 'modqueue' && flag('dismiss') ? 'dismiss' : 'apply';
|
|
428
|
+
const arg = post ? (flag('actor') || flag('note') || args[1]) : null;
|
|
429
|
+
if (post && !TOGGLES[cmd] && !arg && !admitAll) {
|
|
430
|
+
console.error(`usage: fedipod ${cmd} <${BY_ACTOR.includes(cmd) ? 'actor' : 'note'}-url>`
|
|
431
|
+
+ (cmd === 'admit' ? ' (or: fedipod admit --all)' : ''));
|
|
432
|
+
process.exit(2);
|
|
433
|
+
}
|
|
434
|
+
if (TOGGLES[cmd] && !TOGGLES[cmd].includes(arg)) {
|
|
435
|
+
console.error(`usage: fedipod ${cmd} <${TOGGLES[cmd].join('|')}>`);
|
|
436
|
+
process.exit(2);
|
|
437
|
+
}
|
|
438
|
+
const payload = modAct ? { id: modAct, action: modVerb }
|
|
439
|
+
: cmd === 'review' ? { on: arg === 'on' }
|
|
440
|
+
: cmd === 'joins' ? { approve: arg === 'approve' }
|
|
441
|
+
: admitAll ? { all: true }
|
|
442
|
+
: BY_ACTOR.includes(cmd) ? { actor: arg } : { noteId: arg };
|
|
443
|
+
let body;
|
|
444
|
+
try {
|
|
445
|
+
const asPost = post || !!modAct;
|
|
446
|
+
const res = await localFetch(HOME, PORT, `/${cmd}`, asPost
|
|
447
|
+
? { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) }
|
|
448
|
+
: undefined);
|
|
449
|
+
body = await res.json();
|
|
450
|
+
if (res.status === 404 && body.error === 'not a group') {
|
|
451
|
+
console.error(`the agent on :${PORT} is not a group — these commands only apply to one`);
|
|
452
|
+
process.exit(2);
|
|
453
|
+
}
|
|
454
|
+
if (res.status >= 400) { console.error(body.error || `HTTP ${res.status}`); process.exit(1); }
|
|
455
|
+
} catch (e) {
|
|
456
|
+
console.error(`agent not reachable on :${PORT} (${e.message})`);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
if (cmd === 'modqueue' && modAct) {
|
|
460
|
+
console.log(`${body.action === 'apply' ? 'applied' : 'dismissed'}: ${body.type} from ${body.moderator}`);
|
|
461
|
+
console.log(`${body.remaining} left in the queue`);
|
|
462
|
+
} else if (cmd === 'modqueue') {
|
|
463
|
+
// Moderation asked for over federation. A delivery does not prove who sent
|
|
464
|
+
// it, so a moderator's request waits here instead of acting on arrival.
|
|
465
|
+
const q = Array.isArray(body) ? body : (body.queue || []);
|
|
466
|
+
if (!q.length) console.log('nothing waiting — no moderator has asked for anything');
|
|
467
|
+
for (const e of q) console.log(`${e.id} ${e.at} ${e.type} from ${e.moderator}`);
|
|
468
|
+
if (q.length) {
|
|
469
|
+
console.log('\ncarry one out: fedipod modqueue --apply <id>');
|
|
470
|
+
console.log('turn one down: fedipod modqueue --dismiss <id>');
|
|
471
|
+
}
|
|
472
|
+
} else if (cmd === 'members') {
|
|
473
|
+
if (!body.members.length) console.log('no members yet — nobody has followed this group');
|
|
474
|
+
for (const m of body.members) console.log(`${m.muted ? 'muted ' : ' '}${m.actor}`);
|
|
475
|
+
console.log('\nstop carrying someone: fedipod mute <actor-url> (undo: unmute)');
|
|
476
|
+
console.log('remove them entirely: fedipod eject <actor-url>');
|
|
477
|
+
} else if (cmd === 'announced') {
|
|
478
|
+
if (!body.announced.length) console.log('nothing carried yet');
|
|
479
|
+
for (const a of body.announced) console.log(`${a.announcedAt} ${a.actor} ${a.noteId}`);
|
|
480
|
+
console.log('\nunsay one: fedipod retract <note-url>');
|
|
481
|
+
} else if (cmd === 'pending') {
|
|
482
|
+
console.log(`review is ${body.review ? 'ON' : 'off'}`);
|
|
483
|
+
if (!body.pending.length) console.log('nothing held');
|
|
484
|
+
for (const q of body.pending) console.log(`${q.at} ${q.actor} ${q.noteId}`);
|
|
485
|
+
if (body.pending.length) console.log('\nfedipod approve <note-url> (or decline)');
|
|
486
|
+
} else if (cmd === 'requests') {
|
|
487
|
+
console.log(`joins ${body.approveJoins ? 'need approval' : 'are open — anyone can join'}`);
|
|
488
|
+
if (!body.requests.length) console.log('nobody waiting');
|
|
489
|
+
for (const q of body.requests) console.log(`${q.at} ${q.actor}`);
|
|
490
|
+
if (body.requests.length) console.log('\nfedipod admit <actor-url> (or refuse)');
|
|
491
|
+
} else if (cmd === 'joins') {
|
|
492
|
+
console.log(body.approveJoins
|
|
493
|
+
? 'joins now need approval — the actor advertises manuallyApprovesFollowers and was republished'
|
|
494
|
+
: 'joins are now open — anyone who follows is admitted at once');
|
|
495
|
+
} else if (cmd === 'admit' || cmd === 'refuse') {
|
|
496
|
+
if (admitAll) console.log(`admitted ${body.admitted} — ${body.requests} still waiting`);
|
|
497
|
+
else console.log(`${cmd === 'admit' ? 'admitted' : 'refused'} ${arg} — ${body.requests} still waiting`);
|
|
498
|
+
} else if (cmd === 'mute' || cmd === 'unmute') {
|
|
499
|
+
console.log(`${cmd}d ${arg} — ${body.actors.length} muted member(s)`);
|
|
500
|
+
} else if (cmd === 'eject') {
|
|
501
|
+
console.log(`ejected ${arg}${body.told ? ' — their server was told' : ' (no inbox on record; not told)'}`);
|
|
502
|
+
console.log('they are muted too, so a re-follow rejoins but nothing of theirs is carried');
|
|
503
|
+
} else if (cmd === 'retract') {
|
|
504
|
+
console.log(`retracted ${arg} — Undo sent to ${body.inboxes} inbox(es)`);
|
|
505
|
+
} else if (cmd === 'review') {
|
|
506
|
+
console.log(`review is now ${body.review ? 'ON — nothing is carried until approved' : 'off'}`);
|
|
507
|
+
} else {
|
|
508
|
+
console.log(`${cmd}d ${arg} — ${body.pending} still held`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export async function archive() {
|
|
513
|
+
// Whether drained mail's original bytes are kept in the private half's
|
|
514
|
+
// inbox-archive/. On by default; served by the running agent.
|
|
515
|
+
requireIdentity();
|
|
516
|
+
if (!['on', 'off'].includes(args[1])) {
|
|
517
|
+
console.error('usage: fedipod archive <on|off>');
|
|
518
|
+
process.exit(2);
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const res = await localFetch(HOME, PORT, `/archive`, {
|
|
522
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
523
|
+
body: JSON.stringify({ on: args[1] === 'on' }),
|
|
524
|
+
});
|
|
525
|
+
const body = await res.json();
|
|
526
|
+
if (res.status >= 400) { console.error(body.error || `HTTP ${res.status}`); process.exit(1); }
|
|
527
|
+
console.log(body.archiveInbox
|
|
528
|
+
? 'inbox archive is on — drained mail keeps its original bytes in the private half'
|
|
529
|
+
: 'inbox archive is off — drained mail leaves no copy behind');
|
|
530
|
+
} catch (e) {
|
|
531
|
+
console.error(`agent not reachable on :${PORT} (${e.message})`);
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function bsky() {
|
|
537
|
+
// Bluesky account commands, served by the running agent's admin API.
|
|
538
|
+
const sub = args[1];
|
|
539
|
+
let out;
|
|
540
|
+
try {
|
|
541
|
+
if (sub === 'connect') {
|
|
542
|
+
const identifier = flag('handle') || args[2];
|
|
543
|
+
const appPassword = flag('app-password') || args[3];
|
|
544
|
+
if (!identifier || !appPassword) {
|
|
545
|
+
console.error('usage: fedipod bsky connect <handle> <app-password> [--service https://bsky.social]');
|
|
546
|
+
process.exit(2);
|
|
547
|
+
}
|
|
548
|
+
const res = await localFetch(HOME, PORT, `/atproto/connect`, {
|
|
549
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
550
|
+
body: JSON.stringify({ identifier, appPassword, service: flag('service') }),
|
|
551
|
+
});
|
|
552
|
+
out = await res.json();
|
|
553
|
+
if (res.status >= 400) { console.error(out.error || `HTTP ${res.status}`); process.exit(1); }
|
|
554
|
+
console.log(`connected: @${out.handle} on ${out.service}`);
|
|
555
|
+
console.log('public posts will cross-post; turn off: fedipod bsky crosspost off');
|
|
556
|
+
} else if (sub === 'disconnect') {
|
|
557
|
+
const res = await localFetch(HOME, PORT, `/atproto/disconnect`, {
|
|
558
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}',
|
|
559
|
+
});
|
|
560
|
+
out = await res.json();
|
|
561
|
+
if (res.status >= 400) { console.error(out.error || `HTTP ${res.status}`); process.exit(1); }
|
|
562
|
+
console.log('disconnected — the local credential is gone');
|
|
563
|
+
} else if (sub === 'crosspost') {
|
|
564
|
+
if (!['on', 'off'].includes(args[2])) {
|
|
565
|
+
console.error('usage: fedipod bsky crosspost <on|off>');
|
|
566
|
+
process.exit(2);
|
|
567
|
+
}
|
|
568
|
+
const res = await localFetch(HOME, PORT, `/atproto`, {
|
|
569
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
570
|
+
body: JSON.stringify({ crossPost: args[2] === 'on' }),
|
|
571
|
+
});
|
|
572
|
+
out = await res.json();
|
|
573
|
+
if (res.status >= 400) { console.error(out.error || `HTTP ${res.status}`); process.exit(1); }
|
|
574
|
+
console.log(`cross-posting is ${out.atproto.crossPost ? 'on' : 'off'}`);
|
|
575
|
+
} else {
|
|
576
|
+
const res = await localFetch(HOME, PORT, `/status`);
|
|
577
|
+
out = await res.json();
|
|
578
|
+
const a = out.atproto;
|
|
579
|
+
if (!a?.connected) console.log('no bluesky account connected — fedipod bsky connect <handle> <app-password>');
|
|
580
|
+
else console.log(`@${a.handle} (${a.did}) on ${a.service}${a.lastError ? `\nlast error: ${a.lastError}` : ''}`);
|
|
581
|
+
}
|
|
582
|
+
} catch (e) {
|
|
583
|
+
console.error(`agent not reachable on :${PORT} (${e.message})`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
}
|