@ghostlytools/cli 1.2.1 → 1.2.2
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 +4 -2
- package/SKILL.md +2 -1
- package/dist/assets/{arkade-CtCpFaF7.js → arkade-_gS9wShF.js} +2 -2
- package/dist/assets/{arkade-CtCpFaF7.js.map → arkade-_gS9wShF.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-C0XRWySP.js → hyperdhtRelay-CgYLaQz3.js} +3 -3
- package/dist/assets/{hyperdhtRelay-C0XRWySP.js.map → hyperdhtRelay-CgYLaQz3.js.map} +1 -1
- package/dist/assets/{mainline-DVYdrus6.js → mainline-1v8OTYvp.js} +2 -2
- package/dist/assets/{mainline-DVYdrus6.js.map → mainline-1v8OTYvp.js.map} +1 -1
- package/dist/assets/{server-D5FBZYW8.js → server-Cy4ox7oD.js} +315 -78
- package/dist/assets/server-Cy4ox7oD.js.map +1 -0
- package/dist/assets/{src-aKGjb_yz.js → src-XzcHzneo.js} +4974 -4795
- package/dist/assets/src-XzcHzneo.js.map +1 -0
- package/dist/assets/{usdt-CUfucwZf.js → usdt-Deo3g0Nq.js} +2 -2
- package/dist/assets/{usdt-CUfucwZf.js.map → usdt-Deo3g0Nq.js.map} +1 -1
- package/dist/ghostly.mjs +361 -76
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/server-D5FBZYW8.js.map +0 -1
- package/dist/assets/src-aKGjb_yz.js.map +0 -1
package/dist/ghostly.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as __exportAll } from "./assets/rolldown-runtime-8H4AJuhK.js";
|
|
3
|
-
import { $ as
|
|
3
|
+
import { $ as groupName, $a as APP_PREFIXES, $c as sanitizeAvatar, $n as readGroupWantNo, At as checkDidSignature, Ba as RUN_RESULTS, Bo as signalHasVideo, Bt as parseDid, Ci as recordsFromWellKnown, Cn as pubkyProofFile, Ct as AtprotoProofError, Dn as requestLnurlInvoice, Dr as ChatFiles, Ei as wellKnownUrl, En as pubkyHomeserverOf, Et as DID_FILE_MAX_BYTES, Fa as replySnippet, Fo as buildSdpFromSignal, Ft as didWebFile, Ga as checkStatusCard, Gc as openRelayPayload, Gt as strictBase64Url, Ha as buttonLabelClash, Hc as RELAY_PAYLOAD_MAX_BYTES, Hn as engineText, Ht as parseDidSignaturePaste, Ic as setLinkTraceSink, Ii as boundVerifiedIdentity, Io as callRtcConfig, It as didWebFileNames, Jn as groupFileFallback, Kn as GROUP_WANT_NO_FRAME, La as ITEM_STATES, Lo as callSignalHeardAt, Lt as didWebFileUrl, Mo as CALL_SIGNAL_MAX_AGE_MS, Mr as TYPING_REFRESH_MS, Mt as didJwsAlg, No as RTC_CONFIG, Nt as didSignedMessages, On as resolveLightningDestination, Ot as DidError, Pi as IdentityCheckUnavailable, Po as answersOffer, Pr as sanitizeTypingStatus, Pt as didWebDocumentUrl, Qc as MAX_AVATAR_LENGTH, Qn as readGroupWant, Ra as PR_CHECKS, Ro as extractParamsFromSdp, Rt as didWebHost, Si as recordsFromTxt, Sn as pubkyProofCapability, St as shortAtprotoDid, Ti as txtValue, Tn as homeserverWebEndpoint, To as GhostlyHttpError, Tt as DID_DOCUMENT_MAX_BYTES$1, Ua as cardLine, Un as GROUP_FILE_LIMITS, Ut as signingKeys, Va as STATUS_CARD_LIMITS, Vn as engineError, Vt as parseDidDocument, Wa as cardUrl, Wn as GROUP_HAVE_FRAME, Wt as staticDidDocument, Xi as irohRelayUrl, Xr as checkPushEndpoint, Xt as resolveDidDht, Yi as irohDescriptor, Yn as isGroupMessageId, Zn as readGroupHave, _ as checkAppManifest, _a as RateWindow, _i as domainWellKnownFile, _n as PUBKY_PROOF_MAX_BYTES, _t as atprotoProofRkey, ac as inviteLink, al as utf8Decode, at as sameSshKey, bi as nip05RootKey, bn as newPubkyProofFolder, bt as normalizeAtprotoHandle, c as readAppStore, ca as PLAYABLE_AUDIO, co as signAppObject, ct as verifyBitcoinMessage, d as APP_BUNDLE_LIMITS, da as fileMessageText, dn as walletNetworkOf, do as canonicalJsonBytes, dt as ATPROTO_DECLARED_SCOPE, eo as APP_STATEMENT_LIMITS, fa as parseVoiceMeta, ft as ATPROTO_FULL_SCOPE, g as assembleAppBundle, gc as seedSigner, gi as domainTxtRecord, gt as atprotoProofRecord, hi as dnsProofName, ht as atprotoDidMethod, il as toZ32, it as parseSshSignature, jr as TYPING_KINDS, jt as didGhostlyServiceEntry, kn as decodeBolt11, ko as LIMITS, kr as transferEnded, kt as checkDidJws, la as VOICE_LIMITS, lo as signAppRevocation, lt as BitcoinAddressError, mi as decodeDnsResponse, mt as ATPROTO_PROOF_SCOPE, n as appStoreDecision, nl as randomBytes$1, no as appRef, nt as SshSigError, oa as sanitizeFileName, ol as utf8Encode, on as WALLET_NETWORKS, ot as sshSignCommand, pa as voicePeaksOf, pi as DOMAIN_FILE_MAX_BYTES, pt as ATPROTO_PROOF_COLLECTION, qn as fetchesByItself, ra as readImageMeta, rl as toBase64Url, ro as checkAppRevokeStatement, rt as parseSshPublicKey, s as checkAppStoreIndex, sa as sanitizeMime, so as readAppRevocations, st as verifySshSignature, ti as BUTTONS_CAPABILITY, tl as fromBase64Url$1, to as appFingerprint, tt as SSHSIG_NAMESPACE, u as signAppStore, ua as baseMime, uo as canonicalJson, ut as decodeAnyBitcoinAddress, v as isAppUrl, vi as encodeDnsQuery, vn as isPubkyKey, vt as checkAtprotoProofRecord, wi as recordsName, wn as pubkyProofPath, wt as verifyAtprotoRecordProof, xi as normalizeDomain, xn as normalizePubkyKey, xt as parseAtprotoDidDocument, y as readAppBundle, yi as isPublicAddress, yn as isPubkyProofFolder, yt as isAtprotoDid, za as PR_STATES, zi as identityStatement, zo as parseCallSignal, zt as normalizeDid } from "./assets/src-XzcHzneo.js";
|
|
4
4
|
import { c as fileStore, f as store$1, i as STORES, m as wrap, o as databaseName, p as transact, u as openDb } from "./assets/idb-CTF6LF5B.js";
|
|
5
5
|
import { a as fileBytesOf, i as fileBytes, l as FILE_BYTES_STEP } from "./assets/fileBytes-BcYZhNgF.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
@@ -35,7 +35,7 @@ import { IDBFactory, IDBKeyRange as IDBKeyRange$1, IDBTransaction } from "fake-i
|
|
|
35
35
|
import { format } from "node:util";
|
|
36
36
|
var package_default = {
|
|
37
37
|
name: "@ghostlytools/cli",
|
|
38
|
-
version: "1.2.
|
|
38
|
+
version: "1.2.2",
|
|
39
39
|
type: "module",
|
|
40
40
|
description: "Ghostly for bots and terminals: the app's own engine, headless on Node, behind a daemon and a `ghostly` command with JSON output.",
|
|
41
41
|
license: "MIT",
|
|
@@ -285,15 +285,21 @@ function parseArgs(argv, spec, idSlot = () => false) {
|
|
|
285
285
|
}
|
|
286
286
|
/** The global option a word names (`--profile`, `--profile=bot`, `-p`), else undefined. */
|
|
287
287
|
function globalOption(arg) {
|
|
288
|
+
const name = globalName(arg);
|
|
289
|
+
return name ? GLOBAL_OPTIONS[name] : void 0;
|
|
290
|
+
}
|
|
291
|
+
/** The name of the global option a word names (`profile` for `-p`), else undefined. */
|
|
292
|
+
function globalName(arg) {
|
|
288
293
|
const eq = arg.indexOf("=");
|
|
289
294
|
const name = arg.startsWith("--") ? arg.slice(2, eq === -1 ? void 0 : eq) : arg.length === 2 && arg.startsWith("-") ? Object.entries(GLOBAL_OPTIONS).find(([, o]) => o.short === arg.slice(1))?.[0] : void 0;
|
|
290
|
-
return name && Object.hasOwn(GLOBAL_OPTIONS, name) ?
|
|
295
|
+
return name && Object.hasOwn(GLOBAL_OPTIONS, name) ? name : void 0;
|
|
291
296
|
}
|
|
292
297
|
/**
|
|
293
298
|
* Global options given before the command (`ghostly --profile bot send …`) move after it, where every command's
|
|
294
299
|
* parser reads them. Stops at `--` and at the first word that is not a global option. They go before the first word
|
|
295
300
|
* after the command's that starts with a dash (its first option, or the `--` that ends them), else at the end: never
|
|
296
|
-
* between an option and its value, which may itself be `--` (`--pretty profile set --name --`).
|
|
301
|
+
* between an option and its value, which may itself be `--` (`--pretty profile set --name --`). One that takes a value
|
|
302
|
+
* and is the last word has none: a usage error.
|
|
297
303
|
*/
|
|
298
304
|
function liftGlobals(argv) {
|
|
299
305
|
const lifted = [];
|
|
@@ -304,6 +310,7 @@ function liftGlobals(argv) {
|
|
|
304
310
|
const option = globalOption(arg);
|
|
305
311
|
if (!option || arg === "--") break;
|
|
306
312
|
const takesValue = option.type !== "boolean" && eq === -1;
|
|
313
|
+
if (takesValue && i + 1 >= argv.length) throw new CliError("usage", `--${globalName(arg)} needs a value`);
|
|
307
314
|
lifted.push(...argv.slice(i, i + (takesValue ? 2 : 1)));
|
|
308
315
|
i += takesValue ? 2 : 1;
|
|
309
316
|
}
|
|
@@ -335,14 +342,26 @@ var SECRET_CONTEXT = /priv(?:ate)?[\s_-]*key|secret|seed|mnemonic|nsec|xprv|\bwi
|
|
|
335
342
|
/** How far from a 64-hex string a word like "seed" still counts as saying what it is. */
|
|
336
343
|
var CONTEXT_REACH = 48;
|
|
337
344
|
var CASHU_TOKEN = /\bcashu([AB])([A-Za-z0-9_\-+/]{20,}={0,2})/g;
|
|
345
|
+
/**
|
|
346
|
+
* Characters that take no room on screen: format characters (zero-width space, soft hyphen, word joiner, the joiners,
|
|
347
|
+
* direction marks), control characters that are not white space, variation selectors, the grapheme joiner and the
|
|
348
|
+
* Hangul fillers. A page, a PDF or a terminal puts some of them into long strings as places to break a line.
|
|
349
|
+
*/
|
|
350
|
+
var INVISIBLE = /(?![\t\n\v\f\r\x85])[\p{Cc}\p{Cf}]|\u034f|[\u115f\u1160\u3164\uffa0]|[\ufe00-\ufe0f]|[\u{e0100}-\u{e01ef}]/gu;
|
|
338
351
|
var base58check = createBase58check(sha256);
|
|
339
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* The first secret in `text`, keys and seeds before ecash; null for anything else. The text is read as it is and as
|
|
354
|
+
* it reads without its invisible characters: a key with one inside is whole again once the reader takes it out.
|
|
355
|
+
*/
|
|
340
356
|
function findSecret(text) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
if (
|
|
345
|
-
|
|
357
|
+
const visible = text.replace(INVISIBLE, "");
|
|
358
|
+
const readings = visible === text ? [text] : [text, visible];
|
|
359
|
+
if (readings.some(hasMnemonic)) return { kind: "mnemonic" };
|
|
360
|
+
if (readings.some((reading) => NSEC.test(reading))) return { kind: "nsec" };
|
|
361
|
+
if (readings.some((reading) => EXTENDED_PRIVATE.test(reading) || hasWif(reading))) return { kind: "bitcoin-key" };
|
|
362
|
+
if (readings.some(hasHexSecret)) return { kind: "hex-key" };
|
|
363
|
+
const tokens = readings.map(findCashu);
|
|
364
|
+
return tokens.find((found) => found?.kind === "cashu" && found.amount !== null) ?? tokens.find((found) => found) ?? null;
|
|
346
365
|
}
|
|
347
366
|
/**
|
|
348
367
|
* 12 to 24 words in a row from the English BIP39 list whose checksum holds, anywhere in the text; or a message that
|
|
@@ -875,7 +894,8 @@ function findGroup(groups, ref) {
|
|
|
875
894
|
* A chat or a group, where a command takes either: `group:<id>` is a group; anything else a chat first (`findChat`),
|
|
876
895
|
* then a group. A chat found only by the name its contact gave itself never takes what also names a group: a group's
|
|
877
896
|
* own id is that group, and a prefix or the name of a group this profile is in is refused, with both ids. With neither,
|
|
878
|
-
*
|
|
897
|
+
* one `not_found` names both and where to look (a contact's key: no chat with it); a name that matches more than one
|
|
898
|
+
* chat or group stays that error.
|
|
879
899
|
*/
|
|
880
900
|
function findChatOrGroup(links, groups, ref) {
|
|
881
901
|
if (ref.startsWith("group:")) return { group: findGroup(groups, ref.slice(6)) };
|
|
@@ -887,7 +907,9 @@ function findChatOrGroup(links, groups, ref) {
|
|
|
887
907
|
try {
|
|
888
908
|
return { group: findGroup(groups, ref) };
|
|
889
909
|
} catch (inner) {
|
|
890
|
-
|
|
910
|
+
if (!(inner instanceof CliError) || inner.code !== "not_found") throw inner;
|
|
911
|
+
if (CONTACT_KEY.test(ref)) throw error;
|
|
912
|
+
throw new CliError("not_found", `No chat or group ${JSON.stringify(ref)} (ghostly chat list, ghostly group list)`);
|
|
891
913
|
}
|
|
892
914
|
}
|
|
893
915
|
if (!chat.theirName) return { link: chat.link };
|
|
@@ -916,6 +938,16 @@ function chatOrGroup(ctx, ref) {
|
|
|
916
938
|
};
|
|
917
939
|
}
|
|
918
940
|
var groupOf = (ctx, params) => findGroup(state(ctx).groups, str(params, "group", true));
|
|
941
|
+
/**
|
|
942
|
+
* A message of a chat or a group (`group:<id>`) named by its id here, read as one row; only a name that is no such
|
|
943
|
+
* row's key (the id both sides know, `wireId`) reads the whole history. `keep`: only a message it accepts (mine, or
|
|
944
|
+
* someone else's). The history is synchronous work on the daemon's one thread, about 120 ms at 10,000 messages.
|
|
945
|
+
*/
|
|
946
|
+
async function messageOf(ctx, linkId, ref, keep = () => true) {
|
|
947
|
+
const found = await node(ctx).getMessage(linkId, ref);
|
|
948
|
+
if (found && keep(found)) return found;
|
|
949
|
+
return (linkId.startsWith("group:") ? await node(ctx).groupMessages({ groupId: linkId.slice(6) }) : await node(ctx).getMessages(linkId)).find((m) => keep(m) && m.wireId === ref);
|
|
950
|
+
}
|
|
919
951
|
/** Resolves with `check`'s first defined answer: now, or on a later state; `timeout` error after `ms`. */
|
|
920
952
|
function waitForState(ctx, check, ms, what) {
|
|
921
953
|
const now = check(state(ctx));
|
|
@@ -1131,6 +1163,12 @@ var storedGroupFiles = {
|
|
|
1131
1163
|
read: (file, offset, length) => readStored(file, offset, length),
|
|
1132
1164
|
room: async () => (await fileBytes()).room()
|
|
1133
1165
|
};
|
|
1166
|
+
/**
|
|
1167
|
+
* Ended transfers an edge keeps the record of. A late frame for one of them is still answered as files/3 says (an abort
|
|
1168
|
+
* to an asker going on with a transfer cancelled here); older ones are forgotten, so an edge's records stay few
|
|
1169
|
+
* however many transfers it has seen.
|
|
1170
|
+
*/
|
|
1171
|
+
var ENDED_KEPT = 8;
|
|
1134
1172
|
/** Why a want waits, as the person reads it (the app shows its own words for each `wait`). */
|
|
1135
1173
|
var WAITING_NOTES = {
|
|
1136
1174
|
nobody: "Nobody you are connected to has this file yet",
|
|
@@ -1164,7 +1202,10 @@ var GroupFileDesk = class {
|
|
|
1164
1202
|
constructor(deps) {
|
|
1165
1203
|
this.deps = deps;
|
|
1166
1204
|
}
|
|
1167
|
-
/**
|
|
1205
|
+
/**
|
|
1206
|
+
* At start: each group's files as they were kept. One fetched by itself, or asked for, and not here yet is asked for
|
|
1207
|
+
* again; with automatic downloads off, one chosen by itself before waits for a Download instead.
|
|
1208
|
+
*/
|
|
1168
1209
|
async load(groupIds) {
|
|
1169
1210
|
for (const groupId of groupIds) for (const file of await this.deps.store.list(groupId).catch(() => [])) {
|
|
1170
1211
|
if (!file.group || !file.metadata || !file.digest || this.kept.has(file.id)) continue;
|
|
@@ -1205,7 +1246,8 @@ var GroupFileDesk = class {
|
|
|
1205
1246
|
retry: true,
|
|
1206
1247
|
error: file.transfer.error ?? DAMAGED$1
|
|
1207
1248
|
});
|
|
1208
|
-
else if (kept.
|
|
1249
|
+
else if (kept.asked || kept.auto && this.deps.settings().autoDownloads) this.wants.set(kept.id, this.newWant());
|
|
1250
|
+
else if (kept.auto) await this.stop(kept, false);
|
|
1209
1251
|
else this.show(kept, {
|
|
1210
1252
|
state: "transferring",
|
|
1211
1253
|
stage: "asking",
|
|
@@ -1230,6 +1272,56 @@ var GroupFileDesk = class {
|
|
|
1230
1272
|
}
|
|
1231
1273
|
}
|
|
1232
1274
|
/**
|
|
1275
|
+
* The person stops a file that is coming, or that this device would fetch (Cancel, Decline): nobody is asked for it
|
|
1276
|
+
* any more, the transfer under way ends, what came of it is deleted, and it waits for a Download. Paused (`keep`),
|
|
1277
|
+
* what came stays, and Resume (`download`) goes on from there.
|
|
1278
|
+
*/
|
|
1279
|
+
async halt(fileId, keep = false) {
|
|
1280
|
+
const kept = this.kept.get(fileId);
|
|
1281
|
+
if (!kept) throw new Error("No such file");
|
|
1282
|
+
if (keep && !this.wants.has(fileId)) return;
|
|
1283
|
+
await this.stop(kept, keep);
|
|
1284
|
+
}
|
|
1285
|
+
/** Automatic downloads were turned off: files chosen by themselves and not here yet stop coming, and wait for a Download. */
|
|
1286
|
+
async autoDownloadsOff() {
|
|
1287
|
+
for (const kept of [...this.kept.values()]) if (kept.auto && !kept.asked && this.wants.has(kept.id)) await this.stop(kept, false);
|
|
1288
|
+
}
|
|
1289
|
+
async stop(kept, keep) {
|
|
1290
|
+
if (kept.held || kept.mine) return;
|
|
1291
|
+
const want = this.wants.get(kept.id);
|
|
1292
|
+
this.wants.delete(kept.id);
|
|
1293
|
+
const edge = want?.wire && want.linkId ? this.edgesById.get(want.linkId) : void 0;
|
|
1294
|
+
const record = edge?.files.get("in", want.wire);
|
|
1295
|
+
const { size } = kept.meta;
|
|
1296
|
+
if (!keep) {
|
|
1297
|
+
kept.stored = 0;
|
|
1298
|
+
kept.fresh = true;
|
|
1299
|
+
}
|
|
1300
|
+
if (edge && record && !transferEnded(record)) {
|
|
1301
|
+
if (!keep) kept.dropped = record.id;
|
|
1302
|
+
edge.files.cancel("in", record.id);
|
|
1303
|
+
edge.taking.delete(record.id);
|
|
1304
|
+
} else if (!keep) kept.idle = (kept.idle ?? Promise.resolve()).then(() => this.deps.store.write(kept.id)).then((writer) => writer.discard()).catch(() => {});
|
|
1305
|
+
this.show(kept, keep ? {
|
|
1306
|
+
state: "transferring",
|
|
1307
|
+
stage: "paused",
|
|
1308
|
+
pausedBy: "me",
|
|
1309
|
+
transferred: kept.stored ?? 0,
|
|
1310
|
+
size,
|
|
1311
|
+
direction: "in"
|
|
1312
|
+
} : {
|
|
1313
|
+
state: "transferring",
|
|
1314
|
+
stage: "asking",
|
|
1315
|
+
transferred: 0,
|
|
1316
|
+
size,
|
|
1317
|
+
direction: "in"
|
|
1318
|
+
});
|
|
1319
|
+
if (kept.auto || kept.asked) {
|
|
1320
|
+
kept.auto = kept.asked = false;
|
|
1321
|
+
await this.deps.store.patch(kept.id, { group: this.groupField(kept) }).catch(() => {});
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1233
1325
|
* An error when this group took as many announcements of mine as it takes in a minute, or as its members take from
|
|
1234
1326
|
* one member in `announceWindowMs` (past that they would get the text, not the file); null when one may go now.
|
|
1235
1327
|
*/
|
|
@@ -1696,20 +1788,35 @@ var GroupFileDesk = class {
|
|
|
1696
1788
|
const reserved = active.reduce((sum, a) => sum + a.size, 0);
|
|
1697
1789
|
return this.served.bytes + reserved + kept.meta.size > GROUP_FILE_LIMITS.serveBytesPerDay ? "busy" : null;
|
|
1698
1790
|
}
|
|
1699
|
-
/**
|
|
1791
|
+
/**
|
|
1792
|
+
* Transfers served now, all groups together: to whom, what each takes of the day (its size, or what it sent when that
|
|
1793
|
+
* is more), and whether bytes are still to go.
|
|
1794
|
+
*/
|
|
1700
1795
|
servingNow() {
|
|
1701
1796
|
const active = [];
|
|
1702
1797
|
for (const edge of this.edgesById.values()) for (const wire of edge.serving.keys()) {
|
|
1703
1798
|
const record = edge.files.get("out", wire);
|
|
1704
1799
|
if (record && !transferEnded(record)) active.push({
|
|
1705
1800
|
peer: edge.peer,
|
|
1706
|
-
size: record.file.size,
|
|
1801
|
+
size: Math.max(record.file.size, edge.files.sent(wire)),
|
|
1707
1802
|
sending: record.confirmed < record.file.size
|
|
1708
1803
|
});
|
|
1709
1804
|
}
|
|
1710
1805
|
return active;
|
|
1711
1806
|
}
|
|
1712
1807
|
/**
|
|
1808
|
+
* An asker has a served transfer send parts again (WISP 503 § Serving limits): every byte sent counts toward the day,
|
|
1809
|
+
* so it goes on only while the day has room for it.
|
|
1810
|
+
*/
|
|
1811
|
+
mayResend() {
|
|
1812
|
+
const today = Math.floor(this.deps.now() / 864e5);
|
|
1813
|
+
if (this.served.day !== today) this.served = {
|
|
1814
|
+
day: today,
|
|
1815
|
+
bytes: 0
|
|
1816
|
+
};
|
|
1817
|
+
return this.served.bytes + this.servingNow().reduce((sum, a) => sum + a.size, 0) < GROUP_FILE_LIMITS.serveBytesPerDay;
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1713
1820
|
* Served transfers that no longer count end, their places free for others (WISP 503 § Serving limits): the asker's
|
|
1714
1821
|
* session closed, it is not in the roster any more, or its bytes have not moved for `wantWaitMs` (the asker has moved
|
|
1715
1822
|
* on by then).
|
|
@@ -1762,6 +1869,7 @@ var GroupFileDesk = class {
|
|
|
1762
1869
|
serving: /* @__PURE__ */ new Map(),
|
|
1763
1870
|
servedMoved: /* @__PURE__ */ new Map(),
|
|
1764
1871
|
taking: /* @__PURE__ */ new Map(),
|
|
1872
|
+
ended: [],
|
|
1765
1873
|
have: /* @__PURE__ */ new Set(),
|
|
1766
1874
|
haveAt: 0,
|
|
1767
1875
|
busy: /* @__PURE__ */ new Set()
|
|
@@ -1779,7 +1887,8 @@ var GroupFileDesk = class {
|
|
|
1779
1887
|
openSource: (record) => this.openSource(edge, record),
|
|
1780
1888
|
changed: (record, transferred) => this.transferChanged(edge, record, transferred),
|
|
1781
1889
|
room: () => this.deps.store.room(),
|
|
1782
|
-
writable: () => this.deps.writable?.(linkId)
|
|
1890
|
+
writable: () => this.deps.writable?.(linkId),
|
|
1891
|
+
resend: (record) => !edge.serving.has(record.id) || this.mayResend()
|
|
1783
1892
|
});
|
|
1784
1893
|
this.edgesById.set(linkId, edge);
|
|
1785
1894
|
return edge;
|
|
@@ -1818,7 +1927,7 @@ var GroupFileDesk = class {
|
|
|
1818
1927
|
if (resumed) kept.fresh = kept.unclear = true;
|
|
1819
1928
|
else kept.damaged.add(edge.peer);
|
|
1820
1929
|
}
|
|
1821
|
-
return kept.idle = (damaged ? writer.discard() : writer.close()).catch(() => {});
|
|
1930
|
+
return kept.idle = (damaged || kept.dropped === record.id ? writer.discard() : writer.close()).catch(() => {});
|
|
1822
1931
|
}
|
|
1823
1932
|
};
|
|
1824
1933
|
}
|
|
@@ -1831,7 +1940,14 @@ var GroupFileDesk = class {
|
|
|
1831
1940
|
digest: async () => kept.meta.d
|
|
1832
1941
|
};
|
|
1833
1942
|
}
|
|
1943
|
+
/** A transfer on the edge ended: its record stays among the last `ENDED_KEPT`, and the oldest past that is forgotten. */
|
|
1944
|
+
transferOver(edge, record) {
|
|
1945
|
+
if (edge.ended.some(([direction, id]) => direction === record.direction && id === record.id)) return;
|
|
1946
|
+
edge.ended.push([record.direction, record.id]);
|
|
1947
|
+
while (edge.ended.length > ENDED_KEPT) edge.files.forget(...edge.ended.shift());
|
|
1948
|
+
}
|
|
1834
1949
|
transferChanged(edge, record, transferred) {
|
|
1950
|
+
if (transferEnded(record)) this.transferOver(edge, record);
|
|
1835
1951
|
if (record.direction === "out") {
|
|
1836
1952
|
if (transferEnded(record) && edge.serving.has(record.id)) {
|
|
1837
1953
|
const today = Math.floor(this.deps.now() / 864e5);
|
|
@@ -1839,7 +1955,7 @@ var GroupFileDesk = class {
|
|
|
1839
1955
|
day: today,
|
|
1840
1956
|
bytes: 0
|
|
1841
1957
|
};
|
|
1842
|
-
this.served.bytes += record.
|
|
1958
|
+
this.served.bytes += edge.files.sent(record.id);
|
|
1843
1959
|
edge.serving.delete(record.id);
|
|
1844
1960
|
edge.servedMoved.delete(record.id);
|
|
1845
1961
|
const serving = this.deps.settings().serveFiles;
|
|
@@ -2671,15 +2787,16 @@ var FILE_METHODS = {
|
|
|
2671
2787
|
/**
|
|
2672
2788
|
* files/3: answer an offer, or pause, resume or cancel a transfer, either way; `resend` a file sent from here that
|
|
2673
2789
|
* stopped moving (or failed), `request` again one that stopped arriving. Without a chat, the file's id names it.
|
|
2674
|
-
* A group's file (WISP 503) takes `accept` (download one this device did not fetch by itself) and `
|
|
2790
|
+
* A group's file (WISP 503) takes `accept` (download one this device did not fetch by itself), `request`, and `cancel`,
|
|
2791
|
+
* `decline`, `pause` and `resume` for one that is coming.
|
|
2675
2792
|
*/
|
|
2676
2793
|
async "file.action"(ctx, params) {
|
|
2677
2794
|
const fileId = str(params, "file", true);
|
|
2678
2795
|
const { linkId, where } = fileChat(ctx, params, fileId);
|
|
2679
2796
|
const action = oneOf(params, "action", ACTIONS, "accept");
|
|
2680
2797
|
if ("group" in where) {
|
|
2681
|
-
if (action
|
|
2682
|
-
if (state(ctx).transfers[fileId]?.state === "done") throw new CliError("refused", "Nothing to ask for: the file is all here");
|
|
2798
|
+
if (action === "resend") throw new CliError("bad_request", "A group's file is downloaded (file accept) or asked for again (file request)");
|
|
2799
|
+
if (state(ctx).transfers[fileId]?.state === "done") throw new CliError("refused", action === "accept" || action === "request" || action === "resume" ? "Nothing to ask for: the file is all here" : "Nothing to stop: the file is all here");
|
|
2683
2800
|
try {
|
|
2684
2801
|
await node(ctx).fileAction({
|
|
2685
2802
|
linkId,
|
|
@@ -3346,8 +3463,7 @@ function pressable(link) {
|
|
|
3346
3463
|
return peer ? peer.includes(BUTTONS_CAPABILITY) : null;
|
|
3347
3464
|
}
|
|
3348
3465
|
/** The message of mine with buttons that `button update` names, by its id here (or the id both sides know). */
|
|
3349
|
-
function buttonsMessage(
|
|
3350
|
-
const message = messages.find((m) => m.sender === "me" && m.id === ref) ?? messages.find((m) => m.sender === "me" && m.wireId === ref);
|
|
3466
|
+
function buttonsMessage(message, ref, group) {
|
|
3351
3467
|
if (!message) throw new CliError("not_found", `No message ${JSON.stringify(ref)} of yours in this ${group ? "group" : "chat"}: the messageId send gave`);
|
|
3352
3468
|
if (message.card?.kind !== "buttons") throw new CliError("bad_request", "That message has no buttons: send one with send --button id:Label");
|
|
3353
3469
|
return message;
|
|
@@ -3367,7 +3483,7 @@ async function updateButtons(ctx, params) {
|
|
|
3367
3483
|
const secret = findSecret(newText);
|
|
3368
3484
|
if (secret) throw new CliError("confirm", `The text looks like ${secret.kind === "cashu" ? "a Cashu token (money anyone who reads it can take)" : "a secret (a seed or a private key)"}; update with --force if you mean to`, { kind: secret.kind });
|
|
3369
3485
|
}
|
|
3370
|
-
const message = buttonsMessage(
|
|
3486
|
+
const message = buttonsMessage(await messageOf(ctx, target.linkId, ref, (m) => m.sender === "me"), ref, target.group);
|
|
3371
3487
|
if ((message.edit?.seq ?? 0) >= STATUS_CARD_LIMITS.edits) throw new CliError("refused", `This message took ${STATUS_CARD_LIMITS.edits} updates, the most one takes: ask again with a new send`, {
|
|
3372
3488
|
messageId: message.id,
|
|
3373
3489
|
edits: message.edit.seq
|
|
@@ -3392,7 +3508,7 @@ async function updateButtons(ctx, params) {
|
|
|
3392
3508
|
card
|
|
3393
3509
|
});
|
|
3394
3510
|
if (result.error) throw new CliError(result.refused ? "refused" : "unavailable", result.error);
|
|
3395
|
-
let edited =
|
|
3511
|
+
let edited = await node(ctx).getMessage(target.linkId, message.id);
|
|
3396
3512
|
if (wait === "confirmed" && edited?.edit?.pending) edited = await waitForEdit(ctx, target.linkId, message.id, ms);
|
|
3397
3513
|
const edges = wait === "sent" && edited?.edit ? await waitForGroupFrame(ctx, target.id, message.id, edited.edit.seq, ms) : void 0;
|
|
3398
3514
|
return {
|
|
@@ -3415,8 +3531,7 @@ async function pressButton(ctx, params) {
|
|
|
3415
3531
|
const ref = str(params, "message", true);
|
|
3416
3532
|
const buttonId = str(params, "button", true);
|
|
3417
3533
|
const where = target.group ? "group" : "chat";
|
|
3418
|
-
const
|
|
3419
|
-
const message = theirs.find((m) => m.id === ref) ?? theirs.find((m) => m.wireId === ref);
|
|
3534
|
+
const message = await messageOf(ctx, target.linkId, ref, (m) => m.sender !== "me");
|
|
3420
3535
|
if (!message) throw new CliError("not_found", `No message ${JSON.stringify(ref)} from someone else in this ${where}: the id its event or history gave`);
|
|
3421
3536
|
if (message.card?.kind !== "buttons") throw new CliError("bad_request", "That message has no buttons");
|
|
3422
3537
|
const button = message.card.buttons.find((b) => b.id === buttonId);
|
|
@@ -3575,7 +3690,7 @@ function isLive(link) {
|
|
|
3575
3690
|
if (link.textDelivery !== "stream" || link.dataLink !== "open") return false;
|
|
3576
3691
|
return !link.profile || link.pairing?.status === "ready" && !!link.pairing.transport;
|
|
3577
3692
|
}
|
|
3578
|
-
function chatJson(link) {
|
|
3693
|
+
function chatJson(link, showSecret) {
|
|
3579
3694
|
const label = link.label?.trim() || null;
|
|
3580
3695
|
const peerName = link.peerNick?.trim() || null;
|
|
3581
3696
|
const live = isLive(link);
|
|
@@ -3593,15 +3708,15 @@ function chatJson(link) {
|
|
|
3593
3708
|
pairing: link.pairing?.status ?? null,
|
|
3594
3709
|
stage: link.pairingProgress?.stage ?? null,
|
|
3595
3710
|
verified: !!link.peerVerified,
|
|
3596
|
-
invite: link.pairing?.status === "ready" ? null : link.inviteCode
|
|
3711
|
+
invite: link.pairing?.status === "ready" || !link.inviteCode ? null : showSecret === true ? link.inviteCode : "<hidden>",
|
|
3597
3712
|
compatibility: !link.profile,
|
|
3598
3713
|
peerPicture: !!link.peerAvatar
|
|
3599
3714
|
};
|
|
3600
3715
|
}
|
|
3601
3716
|
/** A chat's details: the summary plus what its connection panel shows. */
|
|
3602
|
-
function chatDetailsJson(link) {
|
|
3717
|
+
function chatDetailsJson(link, showSecret) {
|
|
3603
3718
|
return {
|
|
3604
|
-
...chatJson(link),
|
|
3719
|
+
...chatJson(link, showSecret),
|
|
3605
3720
|
progress: link.pairingProgress ?? null,
|
|
3606
3721
|
transports: {
|
|
3607
3722
|
mine: link.availableTransports ?? [],
|
|
@@ -12533,6 +12648,25 @@ function checkTextSize(text) {
|
|
|
12533
12648
|
max: LIMITS.maxChatMessageBytes
|
|
12534
12649
|
});
|
|
12535
12650
|
}
|
|
12651
|
+
/**
|
|
12652
|
+
* The engine's state as `engine getState` prints it without `--show-secret`: the settings' credentials, a group's
|
|
12653
|
+
* entry link (anyone who reads it can join) and a pending chat's invite code (whoever reads it can take the chat)
|
|
12654
|
+
* are `<hidden>`.
|
|
12655
|
+
*/
|
|
12656
|
+
function hideStateSecrets(state) {
|
|
12657
|
+
return {
|
|
12658
|
+
...state,
|
|
12659
|
+
settings: redactSettings(state.settings),
|
|
12660
|
+
...state.groups ? { groups: state.groups.map((g) => g.entryLink ? {
|
|
12661
|
+
...g,
|
|
12662
|
+
entryLink: "<hidden>"
|
|
12663
|
+
} : g) } : {},
|
|
12664
|
+
...state.links ? { links: state.links.map((l) => l.inviteCode ? {
|
|
12665
|
+
...l,
|
|
12666
|
+
inviteCode: "<hidden>"
|
|
12667
|
+
} : l) } : {}
|
|
12668
|
+
};
|
|
12669
|
+
}
|
|
12536
12670
|
/** Settings as a bot may read them: storage credentials and other secrets masked unless asked for. */
|
|
12537
12671
|
function redactSettings(settings, showSecret = false) {
|
|
12538
12672
|
if (showSecret) return settings;
|
|
@@ -12560,7 +12694,9 @@ var SETTABLE = {
|
|
|
12560
12694
|
online: "boolean",
|
|
12561
12695
|
shareProfile: "boolean",
|
|
12562
12696
|
sendTyping: "boolean",
|
|
12563
|
-
nick: "string"
|
|
12697
|
+
nick: "string",
|
|
12698
|
+
autoDownloads: "boolean",
|
|
12699
|
+
serveFiles: "boolean"
|
|
12564
12700
|
};
|
|
12565
12701
|
var TRANSPORT_NAMES = {
|
|
12566
12702
|
auto: "auto",
|
|
@@ -12723,13 +12859,14 @@ var METHODS = {
|
|
|
12723
12859
|
node(ctx).setActiveLink({ linkId });
|
|
12724
12860
|
return { chat: linkId };
|
|
12725
12861
|
},
|
|
12726
|
-
async "chat.list"(ctx) {
|
|
12727
|
-
|
|
12862
|
+
async "chat.list"(ctx, params) {
|
|
12863
|
+
const showSecret = bool(params, "showSecret");
|
|
12864
|
+
return { chats: state(ctx).links.map((link) => chatJson(link, showSecret)).sort((a, b) => b.lastMessageAt - a.lastMessageAt || b.createdAt - a.createdAt) };
|
|
12728
12865
|
},
|
|
12729
12866
|
async "chat.get"(ctx, params) {
|
|
12730
12867
|
const link = chatOf(ctx, params);
|
|
12731
12868
|
return {
|
|
12732
|
-
...chatDetailsJson(link),
|
|
12869
|
+
...chatDetailsJson(link, bool(params, "showSecret")),
|
|
12733
12870
|
heldUntil: holdOf(ctx, link.id)
|
|
12734
12871
|
};
|
|
12735
12872
|
},
|
|
@@ -12795,7 +12932,7 @@ var METHODS = {
|
|
|
12795
12932
|
messageId,
|
|
12796
12933
|
text
|
|
12797
12934
|
});
|
|
12798
|
-
if (result.error) throw editRefused(await
|
|
12935
|
+
if (result.error) throw editRefused(await messageOf(ctx, link.id, messageId), messageId, "chat", result.error, result.refused);
|
|
12799
12936
|
const id = result.messageId ?? messageId;
|
|
12800
12937
|
let message = await node(ctx).getMessage(link.id, id);
|
|
12801
12938
|
if (wait === "confirmed" && message?.edit?.pending) message = await waitForEdit(ctx, link.id, id, num(params, "timeout", 30, {
|
|
@@ -12849,7 +12986,7 @@ var METHODS = {
|
|
|
12849
12986
|
},
|
|
12850
12987
|
/** WISP 400 § Reactions: one emoji per person per message, a new one replaces mine; `remove` takes mine back. */
|
|
12851
12988
|
async "chat.react"(ctx, params) {
|
|
12852
|
-
const link = chatOf(ctx, params);
|
|
12989
|
+
const link = notAGroup(ctx, params, () => chatOf(ctx, params), "group react", "<message> <emoji>");
|
|
12853
12990
|
return {
|
|
12854
12991
|
chat: link.id,
|
|
12855
12992
|
...await react(ctx, link.id, params)
|
|
@@ -12886,10 +13023,12 @@ var METHODS = {
|
|
|
12886
13023
|
if (!messageIds.length) throw new CliError("bad_request", "Name the messages to forward (their ids, from history or an event)");
|
|
12887
13024
|
const to = list(params, "to").map((ref) => chatOrGroup(ctx, ref));
|
|
12888
13025
|
if (!to.length) throw new CliError("bad_request", "Forward to which chat? Name one or more with --to");
|
|
12889
|
-
const
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
13026
|
+
const chosen = [];
|
|
13027
|
+
for (const id of messageIds) {
|
|
13028
|
+
const message = await node(ctx).getMessage(from.linkId, id);
|
|
13029
|
+
if (!message) throw new CliError("not_found", `No message ${id} in this ${from.group ? "group" : "chat"}`);
|
|
13030
|
+
chosen.push(message);
|
|
13031
|
+
}
|
|
12893
13032
|
if (!bool(params, "force")) for (const message of chosen) {
|
|
12894
13033
|
const secret = message.file ? null : findSecret(message.text);
|
|
12895
13034
|
if (secret) throw new CliError("confirm", `A message looks like ${secret.kind === "cashu" ? "a Cashu token (money anyone who reads it can take)" : "a secret (a seed or a private key)"}; forward it with --force if you mean to`, {
|
|
@@ -12923,7 +13062,7 @@ var METHODS = {
|
|
|
12923
13062
|
await waitForGroupFrame(ctx, to[i].id, messageId, void 0, ms);
|
|
12924
13063
|
continue;
|
|
12925
13064
|
}
|
|
12926
|
-
const sent =
|
|
13065
|
+
const sent = await node(ctx).getMessage(to[i].id, messageId);
|
|
12927
13066
|
if (sent?.file) await FILE_METHODS["file.wait"](ctx, {
|
|
12928
13067
|
chat: to[i].id,
|
|
12929
13068
|
file: sent.file.id,
|
|
@@ -12955,7 +13094,7 @@ var METHODS = {
|
|
|
12955
13094
|
async "chat.delete"(ctx, params) {
|
|
12956
13095
|
const link = chatOf(ctx, params);
|
|
12957
13096
|
const messageId = str(params, "message", true);
|
|
12958
|
-
if (!
|
|
13097
|
+
if (!await node(ctx).getMessage(link.id, messageId)) throw new CliError("not_found", `No message ${messageId} in this chat`);
|
|
12959
13098
|
await node(ctx).deleteMessage({
|
|
12960
13099
|
linkId: link.id,
|
|
12961
13100
|
messageId
|
|
@@ -13071,7 +13210,13 @@ var METHODS = {
|
|
|
13071
13210
|
};
|
|
13072
13211
|
},
|
|
13073
13212
|
async "group.join"(ctx, params) {
|
|
13074
|
-
|
|
13213
|
+
let groupId;
|
|
13214
|
+
try {
|
|
13215
|
+
({groupId} = await node(ctx).joinGroupByLink({ link: str(params, "link", true) }));
|
|
13216
|
+
} catch (error) {
|
|
13217
|
+
if (error instanceof Error && error.message === "This is not a link to a group") throw new CliError("bad_request", error.message);
|
|
13218
|
+
throw error;
|
|
13219
|
+
}
|
|
13075
13220
|
return { group: groupId };
|
|
13076
13221
|
},
|
|
13077
13222
|
async "group.list"(ctx, params) {
|
|
@@ -13140,15 +13285,16 @@ var METHODS = {
|
|
|
13140
13285
|
}
|
|
13141
13286
|
const mentions = mentionsFor(text, list(params, "mentions"), group);
|
|
13142
13287
|
const wait = oneOf(params, "wait", ["none", "sent"], "none");
|
|
13143
|
-
const
|
|
13288
|
+
const linkId = `group:${group.id}`;
|
|
13289
|
+
const before = (await node(ctx).getMessage(linkId, messageId))?.edit?.seq ?? 0;
|
|
13144
13290
|
const result = await node(ctx).editMessage({
|
|
13145
|
-
linkId
|
|
13291
|
+
linkId,
|
|
13146
13292
|
messageId,
|
|
13147
13293
|
text,
|
|
13148
13294
|
...mentions.length ? { mentions } : {}
|
|
13149
13295
|
});
|
|
13150
|
-
if (result.error) throw editRefused(await
|
|
13151
|
-
const message =
|
|
13296
|
+
if (result.error) throw editRefused(await messageOf(ctx, linkId, messageId), messageId, "group", result.error, result.refused);
|
|
13297
|
+
const message = await node(ctx).getMessage(linkId, messageId);
|
|
13152
13298
|
const edits = message?.edit?.seq ?? 0;
|
|
13153
13299
|
if (edits === before) return {
|
|
13154
13300
|
group: group.id,
|
|
@@ -13314,12 +13460,32 @@ function groupSendRefused(error) {
|
|
|
13314
13460
|
return new CliError("unavailable", error);
|
|
13315
13461
|
}
|
|
13316
13462
|
/**
|
|
13463
|
+
* A chat command given a group's id, prefix or name: `not_found` that says it is a group, with the group command that
|
|
13464
|
+
* takes it (`react` on a group, where `group react` is meant). A name no chat and no group has stays the chat's error.
|
|
13465
|
+
*/
|
|
13466
|
+
function notAGroup(ctx, params, chat, command, rest) {
|
|
13467
|
+
try {
|
|
13468
|
+
return chat();
|
|
13469
|
+
} catch (error) {
|
|
13470
|
+
if (!(error instanceof CliError) || error.code !== "not_found") throw error;
|
|
13471
|
+
const ref = str(params, "chat", true);
|
|
13472
|
+
let group;
|
|
13473
|
+
try {
|
|
13474
|
+
group = findGroup(state(ctx).groups, ref);
|
|
13475
|
+
} catch {
|
|
13476
|
+
throw error;
|
|
13477
|
+
}
|
|
13478
|
+
const shown = /^[\w.:-]+$/.test(ref) ? ref : JSON.stringify(ref);
|
|
13479
|
+
throw new CliError("not_found", `${JSON.stringify(ref)} is a group, not a chat: ghostly ${command} ${shown} ${rest}`, { group: group.id });
|
|
13480
|
+
}
|
|
13481
|
+
}
|
|
13482
|
+
/**
|
|
13317
13483
|
* An edit the engine refused. A message that is not in the chat or group at all is `not_found` (exit 3), naming the id
|
|
13318
13484
|
* to give, as `button update` and `task update` do; before, a mistyped or lost id read "Only your own text messages
|
|
13319
13485
|
* can be edited" (`refused`, exit 1), as if the message were someone else's.
|
|
13320
13486
|
*/
|
|
13321
|
-
function editRefused(
|
|
13322
|
-
if (refused && !
|
|
13487
|
+
function editRefused(message, ref, where, error, refused) {
|
|
13488
|
+
if (refused && !message) return new CliError("not_found", `No message ${JSON.stringify(ref)} in this ${where}: give the messageId ${where === "group" ? "group send" : "send"} printed`);
|
|
13323
13489
|
return new CliError(refused ? "refused" : "unavailable", error);
|
|
13324
13490
|
}
|
|
13325
13491
|
/**
|
|
@@ -14482,6 +14648,11 @@ var showSecret = { "show-secret": {
|
|
|
14482
14648
|
type: "boolean",
|
|
14483
14649
|
description: "Print the group's entry link (anyone who has it can join)"
|
|
14484
14650
|
} };
|
|
14651
|
+
/** A pending chat's invite code lets whoever reads it take the chat: printed only when asked for. */
|
|
14652
|
+
var showInvite = { "show-secret": {
|
|
14653
|
+
type: "boolean",
|
|
14654
|
+
description: "Print a pending chat's invite code (whoever has it can take the chat)"
|
|
14655
|
+
} };
|
|
14485
14656
|
var secret = (options) => options["show-secret"] === true ? { showSecret: true } : {};
|
|
14486
14657
|
/**
|
|
14487
14658
|
* `--for` of the typing commands: the seconds it is kept on, 1 to 600, and never with `--stop`. A usage error (exit 2)
|
|
@@ -15050,8 +15221,10 @@ var commands$6 = {
|
|
|
15050
15221
|
},
|
|
15051
15222
|
"chat list": {
|
|
15052
15223
|
method: "chat.list",
|
|
15053
|
-
usage: "chat list",
|
|
15054
|
-
summary: "Chats, newest first"
|
|
15224
|
+
usage: "chat list [--show-secret]",
|
|
15225
|
+
summary: "Chats, newest first (a pending invite code only with --show-secret)",
|
|
15226
|
+
options: showInvite,
|
|
15227
|
+
params: ({ options }) => secret(options)
|
|
15055
15228
|
},
|
|
15056
15229
|
"chat remove": {
|
|
15057
15230
|
method: "chat.remove",
|
|
@@ -15079,10 +15252,14 @@ var commands$6 = {
|
|
|
15079
15252
|
},
|
|
15080
15253
|
"chat show": {
|
|
15081
15254
|
method: "chat.get",
|
|
15082
|
-
usage: "chat show <chat>",
|
|
15083
|
-
summary: "One chat and its connection",
|
|
15255
|
+
usage: "chat show <chat> [--show-secret]",
|
|
15256
|
+
summary: "One chat and its connection (its pending invite code only with --show-secret)",
|
|
15084
15257
|
args: ["chat"],
|
|
15085
|
-
|
|
15258
|
+
options: showInvite,
|
|
15259
|
+
params: ({ options }, { chat }) => ({
|
|
15260
|
+
chat,
|
|
15261
|
+
...secret(options)
|
|
15262
|
+
})
|
|
15086
15263
|
},
|
|
15087
15264
|
"chat transport": {
|
|
15088
15265
|
method: "chat.transport",
|
|
@@ -19348,6 +19525,7 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19348
19525
|
const factory = new IDBFactory();
|
|
19349
19526
|
const raw = factory;
|
|
19350
19527
|
const classes = await probeClasses(factory);
|
|
19528
|
+
dropIndexEntriesByRecord(classes, (a, b) => factory.cmp(a, b));
|
|
19351
19529
|
let writing = true;
|
|
19352
19530
|
let restoring = true;
|
|
19353
19531
|
let journalFd = null;
|
|
@@ -19535,7 +19713,8 @@ async function probeClasses(factory) {
|
|
|
19535
19713
|
const classes = {
|
|
19536
19714
|
Database: rawDb.constructor,
|
|
19537
19715
|
ObjectStore: rawStore.constructor,
|
|
19538
|
-
Index: rawStore.rawIndexes.get("i").constructor
|
|
19716
|
+
Index: rawStore.rawIndexes.get("i").constructor,
|
|
19717
|
+
RecordStore: rawStore.records.constructor
|
|
19539
19718
|
};
|
|
19540
19719
|
db.close();
|
|
19541
19720
|
await new Promise((resolve) => {
|
|
@@ -19544,6 +19723,76 @@ async function probeClasses(factory) {
|
|
|
19544
19723
|
});
|
|
19545
19724
|
return classes;
|
|
19546
19725
|
}
|
|
19726
|
+
/** The classes whose index upkeep is already by record: they are fake-indexeddb's own, shared by every store opened. */
|
|
19727
|
+
var byRecord = /* @__PURE__ */ new WeakSet();
|
|
19728
|
+
/**
|
|
19729
|
+
* fake-indexeddb drops an overwritten or deleted record from an index by walking every entry of it (an index is in
|
|
19730
|
+
* its own key's order, not the record's): each delivery mark, edit or reaction cost time with every message of the
|
|
19731
|
+
* profile, 170 ms at 10,000, and a long chat slowed every other one. The store has the records it just replaced or
|
|
19732
|
+
* removed in hand, and they say which entries to drop; a call that comes any other way still walks.
|
|
19733
|
+
*/
|
|
19734
|
+
function dropIndexEntriesByRecord(classes, cmp) {
|
|
19735
|
+
const storeProto = classes.ObjectStore.prototype;
|
|
19736
|
+
const recordsProto = classes.RecordStore.prototype;
|
|
19737
|
+
if (byRecord.has(storeProto)) return;
|
|
19738
|
+
byRecord.add(storeProto);
|
|
19739
|
+
const { storeRecord, deleteRecord } = storeProto;
|
|
19740
|
+
const { put, delete: remove, deleteByValue } = recordsProto;
|
|
19741
|
+
let gone = null;
|
|
19742
|
+
storeProto.storeRecord = function(record, noOverwrite, rollbackLog) {
|
|
19743
|
+
if (!this.rawIndexes.size) return storeRecord.call(this, record, noOverwrite, rollbackLog);
|
|
19744
|
+
const records = this.records, indexes = this.rawIndexes, outer = gone;
|
|
19745
|
+
records.put = (next, keep) => {
|
|
19746
|
+
const replaced = put.call(this.records, next, keep);
|
|
19747
|
+
gone = {
|
|
19748
|
+
indexes,
|
|
19749
|
+
records: replaced ? [replaced] : []
|
|
19750
|
+
};
|
|
19751
|
+
return replaced;
|
|
19752
|
+
};
|
|
19753
|
+
try {
|
|
19754
|
+
return storeRecord.call(this, record, noOverwrite, rollbackLog);
|
|
19755
|
+
} finally {
|
|
19756
|
+
delete records.put;
|
|
19757
|
+
gone = outer;
|
|
19758
|
+
}
|
|
19759
|
+
};
|
|
19760
|
+
storeProto.deleteRecord = function(key, rollbackLog) {
|
|
19761
|
+
if (!this.rawIndexes.size) return deleteRecord.call(this, key, rollbackLog);
|
|
19762
|
+
const records = this.records, indexes = this.rawIndexes, outer = gone;
|
|
19763
|
+
records.delete = (range) => {
|
|
19764
|
+
const removed = remove.call(this.records, range);
|
|
19765
|
+
gone = {
|
|
19766
|
+
indexes,
|
|
19767
|
+
records: removed
|
|
19768
|
+
};
|
|
19769
|
+
return removed;
|
|
19770
|
+
};
|
|
19771
|
+
try {
|
|
19772
|
+
deleteRecord.call(this, key, rollbackLog);
|
|
19773
|
+
} finally {
|
|
19774
|
+
delete records.delete;
|
|
19775
|
+
gone = outer;
|
|
19776
|
+
}
|
|
19777
|
+
};
|
|
19778
|
+
recordsProto.deleteByValue = function(key) {
|
|
19779
|
+
let index;
|
|
19780
|
+
if (gone) {
|
|
19781
|
+
for (const candidate of gone.indexes.values()) if (candidate.records === this) index = candidate;
|
|
19782
|
+
}
|
|
19783
|
+
if (!gone || !index) return deleteByValue.call(this, key);
|
|
19784
|
+
const dropped = [];
|
|
19785
|
+
for (const entry of indexEntries(index, gone.records, cmp)) {
|
|
19786
|
+
if (this.keysAreUnique) {
|
|
19787
|
+
const held = this.get(entry.key);
|
|
19788
|
+
if (!held || cmp(held.value, entry.value) !== 0) continue;
|
|
19789
|
+
}
|
|
19790
|
+
this.records.delete(entry);
|
|
19791
|
+
dropped.push(entry);
|
|
19792
|
+
}
|
|
19793
|
+
return dropped;
|
|
19794
|
+
};
|
|
19795
|
+
}
|
|
19547
19796
|
/**
|
|
19548
19797
|
* The snapshot's records come in key order (as `writeSnapshot` walks them), so each store's tree and each index's are
|
|
19549
19798
|
* built whole from sorted arrays: storing record by record compared every key down the tree, a 70,000-message profile
|
|
@@ -19939,14 +20188,14 @@ async function startRuntime(paths, options = {}) {
|
|
|
19939
20188
|
(await import("./assets/ghostly_iroh_web-Dvzh3Y6E.js")).initSync({ module: irohWasmBytes() });
|
|
19940
20189
|
const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([import("./assets/fileBytes-BcYZhNgF.js").then((n) => n.o), import("./assets/fileBytes-Dbifu5I5.js")]);
|
|
19941
20190
|
registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
|
|
19942
|
-
const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-
|
|
20191
|
+
const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-Cy4ox7oD.js").then((n) => n.t), import("./assets/endpoint-pgFkR2AS.js")]);
|
|
19943
20192
|
const network = hyperdhtNetwork();
|
|
19944
20193
|
const dht = mainlineNetwork();
|
|
19945
|
-
const mainline = dht.off ? null : new (await (import("./assets/mainline-
|
|
20194
|
+
const mainline = dht.off ? null : new (await (import("./assets/mainline-1v8OTYvp.js"))).Mainline({
|
|
19946
20195
|
bootstrap: dht.bootstrap,
|
|
19947
20196
|
host: dht.host
|
|
19948
20197
|
});
|
|
19949
|
-
const transport = mainline || pinned ? new (await (import("./assets/mainline-
|
|
20198
|
+
const transport = mainline || pinned ? new (await (import("./assets/mainline-1v8OTYvp.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
|
|
19950
20199
|
const server = new EngineServer({
|
|
19951
20200
|
...transport ? { transport } : {},
|
|
19952
20201
|
irohWeb: true,
|
|
@@ -20603,7 +20852,7 @@ var SPECIAL = [
|
|
|
20603
20852
|
"The profile's settings",
|
|
20604
20853
|
{ "show-secret": o("boolean", "Show credentials too") }
|
|
20605
20854
|
],
|
|
20606
|
-
["settings set <key> <json-value>", "Change one: relays, irohRelays, hyperdhtRelay, readRelays, iceServers, publicProfiles, online, shareProfile, sendTyping, nick"],
|
|
20855
|
+
["settings set <key> <json-value>", "Change one: relays, irohRelays, hyperdhtRelay, readRelays, iceServers, publicProfiles, online, shareProfile, sendTyping, autoDownloads, serveFiles, nick"],
|
|
20607
20856
|
[
|
|
20608
20857
|
"engine <method> [json-params | -] [--confirm-real] [--show-secret]",
|
|
20609
20858
|
"Any call of the app's engine, with its own parameters",
|
|
@@ -20649,6 +20898,14 @@ function help() {
|
|
|
20649
20898
|
"2 usage, 3 not found, 4 timed out, 5 needs --force/--yes/--confirm-real. See packages/cli/README.md."
|
|
20650
20899
|
].join("\n");
|
|
20651
20900
|
}
|
|
20901
|
+
/** What `ghostly help help` answers. */
|
|
20902
|
+
var HELP_USAGE = [
|
|
20903
|
+
"Usage: ghostly help [<command>...]",
|
|
20904
|
+
"",
|
|
20905
|
+
" Help for every command, or for one command or a group of them (also ghostly <command> --help)",
|
|
20906
|
+
"",
|
|
20907
|
+
"Options for every command: --profile, -p <name> --home <dir> --pretty --help, -h"
|
|
20908
|
+
].join("\n");
|
|
20652
20909
|
/** Every command's help row: usage, summary and, for the table and text commands, their options. */
|
|
20653
20910
|
function helpRows() {
|
|
20654
20911
|
return [
|
|
@@ -20675,6 +20932,7 @@ function commandWords(usage) {
|
|
|
20675
20932
|
* options; for a group (`ghostly help file`), the group's commands.
|
|
20676
20933
|
*/
|
|
20677
20934
|
function commandHelp(words) {
|
|
20935
|
+
if (words[0] === "help") return HELP_USAGE;
|
|
20678
20936
|
const named = (list) => helpRows().filter((row) => {
|
|
20679
20937
|
const own = commandWords(row.usage);
|
|
20680
20938
|
return list.every((word, i) => own[i] === word);
|
|
@@ -20954,32 +21212,42 @@ async function daemonCommand(argv) {
|
|
|
20954
21212
|
}
|
|
20955
21213
|
if (sub === "stop") {
|
|
20956
21214
|
knownProfile(g);
|
|
21215
|
+
const stopFor = seconds(parsed, 20);
|
|
20957
21216
|
const client = await connectDaemon(g.paths.socket);
|
|
20958
21217
|
if (!client) throw new CliError("not_found", `No daemon runs profile ${g.profile}`);
|
|
20959
21218
|
print({
|
|
20960
21219
|
stopped: true,
|
|
20961
|
-
pid: await stopDaemon(g, client,
|
|
21220
|
+
pid: await stopDaemon(g, client, stopFor)
|
|
20962
21221
|
});
|
|
20963
21222
|
return;
|
|
20964
21223
|
}
|
|
20965
21224
|
if (sub === "restart") {
|
|
21225
|
+
const stopFor = seconds(parsed, 20), startFor = seconds(parsed, 60);
|
|
20966
21226
|
const client = await connectDaemon(g.paths.socket);
|
|
20967
21227
|
const before = client ? (await client.call("status").catch(() => null))?.version ?? null : null;
|
|
20968
21228
|
print({
|
|
20969
21229
|
restarted: true,
|
|
20970
|
-
stopped: client ? await stopDaemon(g, client,
|
|
21230
|
+
stopped: client ? await stopDaemon(g, client, stopFor) : null,
|
|
20971
21231
|
before,
|
|
20972
21232
|
version: VERSION,
|
|
20973
|
-
...await startDetached(g,
|
|
21233
|
+
...await startDetached(g, startFor)
|
|
20974
21234
|
});
|
|
20975
21235
|
return;
|
|
20976
21236
|
}
|
|
20977
21237
|
if (sub) throw new CliError("usage", "ghostly daemon [--detach] | daemon status | daemon stop | daemon restart");
|
|
21238
|
+
const startFor = seconds(parsed, 60);
|
|
20978
21239
|
if (!parsed.options.detach) {
|
|
20979
21240
|
await runDaemon(g);
|
|
20980
21241
|
return;
|
|
20981
21242
|
}
|
|
20982
|
-
print(await startDetached(g,
|
|
21243
|
+
print(await startDetached(g, startFor));
|
|
21244
|
+
}
|
|
21245
|
+
/** `--timeout <s>`, or `fallback` without one: seconds above 0 (0 or less would not wait at all). */
|
|
21246
|
+
function seconds(parsed, fallback) {
|
|
21247
|
+
const value = parsed.options.timeout;
|
|
21248
|
+
if (value === void 0) return fallback;
|
|
21249
|
+
if (typeof value !== "number" || !(value > 0)) throw new CliError("usage", "--timeout takes a number of seconds above 0");
|
|
21250
|
+
return value;
|
|
20983
21251
|
}
|
|
20984
21252
|
/**
|
|
20985
21253
|
* Asks the daemon to stop and waits until its process is gone, not only until it let go of the profile (it releases
|
|
@@ -21039,6 +21307,10 @@ async function startDetached(g, seconds) {
|
|
|
21039
21307
|
}
|
|
21040
21308
|
if (!client) {
|
|
21041
21309
|
heldBy(g, child.pid);
|
|
21310
|
+
if (child.exitCode === null && child.pid !== void 0 && processAlive(child.pid)) throw new CliError("timeout", `The daemon (process ${child.pid}) is still starting; ghostly daemon status shows when it is up`, {
|
|
21311
|
+
pid: child.pid,
|
|
21312
|
+
log: g.paths.log
|
|
21313
|
+
});
|
|
21042
21314
|
throw new CliError("engine", `The daemon did not start; see ${g.paths.log}`);
|
|
21043
21315
|
}
|
|
21044
21316
|
client.close();
|
|
@@ -21226,17 +21498,7 @@ async function engineCommand(argv) {
|
|
|
21226
21498
|
params,
|
|
21227
21499
|
confirmReal
|
|
21228
21500
|
}));
|
|
21229
|
-
if (method === "getState" && !showSecret && result && typeof result === "object")
|
|
21230
|
-
const state = result;
|
|
21231
|
-
result = {
|
|
21232
|
-
...state,
|
|
21233
|
-
settings: redactSettings(state.settings),
|
|
21234
|
-
...state.groups ? { groups: state.groups.map((g) => g.entryLink ? {
|
|
21235
|
-
...g,
|
|
21236
|
-
entryLink: "<hidden>"
|
|
21237
|
-
} : g) } : {}
|
|
21238
|
-
};
|
|
21239
|
-
}
|
|
21501
|
+
if (method === "getState" && !showSecret && result && typeof result === "object") result = hideStateSecrets(result);
|
|
21240
21502
|
print(result);
|
|
21241
21503
|
}
|
|
21242
21504
|
async function settingsCommand(sub, argv) {
|
|
@@ -21469,6 +21731,7 @@ async function main(input) {
|
|
|
21469
21731
|
return 0;
|
|
21470
21732
|
}
|
|
21471
21733
|
if (argv[0] === "--version" || argv[0] === "version") {
|
|
21734
|
+
pretty = argv.slice(1).some((word) => word === "--pretty" || word === "--pretty=true");
|
|
21472
21735
|
print({ version: VERSION });
|
|
21473
21736
|
return 0;
|
|
21474
21737
|
}
|
|
@@ -21564,7 +21827,29 @@ async function run(argv) {
|
|
|
21564
21827
|
}
|
|
21565
21828
|
}
|
|
21566
21829
|
//#endregion
|
|
21830
|
+
//#region src/runtime/webStorage.ts
|
|
21831
|
+
/**
|
|
21832
|
+
* Node 25 and newer define `globalThis.localStorage` as a getter that, without `--localstorage-file`, answers
|
|
21833
|
+
* `undefined` and writes an ExperimentalWarning on stderr the first time it is read. The engine reads it where a
|
|
21834
|
+
* browser keeps a setting (`globalThis.localStorage?.getItem(...)`), so every command that opens a profile printed the
|
|
21835
|
+
* warning. Without the flag the CLI has no localStorage, as on Node 22: the getter is replaced by `undefined` before
|
|
21836
|
+
* anything reads it. With the flag, Node's storage stays.
|
|
21837
|
+
*/
|
|
21838
|
+
function quietWebStorage(scope = globalThis, execArgv = process.execArgv, nodeOptions = process.env.NODE_OPTIONS) {
|
|
21839
|
+
const own = Object.getOwnPropertyDescriptor(scope, "localStorage");
|
|
21840
|
+
if (!own?.get || !own.configurable) return;
|
|
21841
|
+
const flag = (arg) => arg === "--localstorage-file" || arg.startsWith("--localstorage-file=");
|
|
21842
|
+
if (execArgv.some(flag) || (nodeOptions ?? "").split(/\s+/).some(flag)) return;
|
|
21843
|
+
Object.defineProperty(scope, "localStorage", {
|
|
21844
|
+
value: void 0,
|
|
21845
|
+
writable: true,
|
|
21846
|
+
configurable: true,
|
|
21847
|
+
enumerable: false
|
|
21848
|
+
});
|
|
21849
|
+
}
|
|
21850
|
+
//#endregion
|
|
21567
21851
|
//#region src/bin.ts
|
|
21852
|
+
quietWebStorage();
|
|
21568
21853
|
run(process.argv.slice(2));
|
|
21569
21854
|
//#endregion
|
|
21570
21855
|
export { BACKUP_MEDIA_TYPE as $, requireTurn as A, paymentWireId as At, networkLabel as B, breezDatabasesFor as C, storedGroupFiles as Ct, sourceKey as D, replyRef as Dt, ProviderSources as E, receivedPairedReply as Et, sealSeed as F, streamStored as Ft, BITCOIN_INVOICE_ON_TESTNET as G, cashuMint as H, unsealSeed as I, fakesLightning as J, DEFAULT_MINTS as K, ModeChanged as L, setSingleWriterGate as M, readStored as Mt, intentRepository as N, removeStored as Nt, awayFrom as O, replyTo as Ot, newDeviceKey as P, storedSize as Pt, paysItsOwnInvoices as Q, ModeGate as R, breezDatabaseInUse as S, groupOfFile as St, lightningPaying as T, pairedWireReply as Tt, MESSAGE_DETAILS_MAX_BYTES as U, CASHU_MINT_SOURCE as V, WALLET_TYPES as W, isWorthlessMint as X, isTestMint as Y, mintNetwork as Z, LightningCards as _, verifyIdentity as _t, irohRelayProblem as a, reserveAdapterIds as at, normalizePhrase as b, groupFileId as bt, createFailure as c, SourceConfigError as ct, paymentNetwork as d, networkMode as dt, MAX_BACKUP_BYTES as et, paymentNetworksOf as f, redact as ft, CASHU_CARD as g, boundedIdentityFetch as gt, rpcHost as h, setOwnDidSource as ht, createIrohWebEndpoint as i, registeredOnchainProviders as it, setAwayWallets as j, FileAppender as jt, refuseAway as k, paymentAlias as kt, createTiming as l, SourceUnreachableError as lt, networkReason as m, resolveAtprotoDid as mt, loadFedimintSdk as n, onAdaptersChanged as nt, WALLET_NAMES as o, NothingSpentError as ot, walletInstances as p, assertPublicServer as pt, TEST_MINT as q, DEFAULT_IROH_RELAYS as r, registeredLightningProviders as rt, assertConfirmedReal as s, PROVIDER_PLATFORMS as st, fedimintDatabase as t, identityProviders as tt, crossNetwork as u, isNothingSpentError as ut, cardsKey as v, chosenResolver as vt, dropBreezDatabase as w, groupReply as wt, breezDatabase as x, groupFileText as xt, isRecoveryPhrase as y, GroupFileDesk as yt, WrongNetworkError as z };
|