@ghostlytools/cli 1.2.0 → 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 +15 -10
- package/SKILL.md +15 -6
- package/dist/assets/{arkade-3szZ1IpP.js → arkade-_gS9wShF.js} +2 -2
- package/dist/assets/{arkade-3szZ1IpP.js.map → arkade-_gS9wShF.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-DmTrpmba.js → hyperdhtRelay-CgYLaQz3.js} +3 -3
- package/dist/assets/{hyperdhtRelay-DmTrpmba.js.map → hyperdhtRelay-CgYLaQz3.js.map} +1 -1
- package/dist/assets/{mainline-BueFJx3f.js → mainline-1v8OTYvp.js} +2 -2
- package/dist/assets/{mainline-BueFJx3f.js.map → mainline-1v8OTYvp.js.map} +1 -1
- package/dist/assets/{server-BGXmSYx_.js → server-Cy4ox7oD.js} +722 -147
- package/dist/assets/server-Cy4ox7oD.js.map +1 -0
- package/dist/assets/{src-BMUcYQ7b.js → src-XzcHzneo.js} +1016 -752
- package/dist/assets/src-XzcHzneo.js.map +1 -0
- package/dist/assets/{usdt-DklGBSeq.js → usdt-Deo3g0Nq.js} +2 -2
- package/dist/assets/{usdt-DklGBSeq.js.map → usdt-Deo3g0Nq.js.map} +1 -1
- package/dist/ghostly.mjs +831 -203
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/server-BGXmSYx_.js.map +0 -1
- package/dist/assets/src-BMUcYQ7b.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";
|
|
@@ -14,11 +14,11 @@ import { schnorr } from "@noble/curves/secp256k1.js";
|
|
|
14
14
|
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
15
15
|
import { access, constants, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
16
16
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
17
|
+
import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual } from "node:crypto";
|
|
17
18
|
import { decode, npubEncode } from "nostr-tools/nip19";
|
|
18
19
|
import { BunkerSigner } from "nostr-tools/nip46";
|
|
19
20
|
import { SimplePool } from "nostr-tools/pool";
|
|
20
21
|
import { generateSecretKey } from "nostr-tools/pure";
|
|
21
|
-
import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual } from "node:crypto";
|
|
22
22
|
import { createServer as createServer$1 } from "node:http";
|
|
23
23
|
import { Readable } from "node:stream";
|
|
24
24
|
import { pipeline } from "node:stream/promises";
|
|
@@ -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",
|
|
@@ -139,7 +139,7 @@ var CliError = class extends Error {
|
|
|
139
139
|
/** Anything thrown, as the error a bot sees: the engine's own refusals keep their words. */
|
|
140
140
|
function asCliError(error) {
|
|
141
141
|
if (error instanceof CliError) return error;
|
|
142
|
-
if (error && typeof error === "object" && "code" in error && "message" in error && typeof error.code === "string" && error.code
|
|
142
|
+
if (error && typeof error === "object" && "code" in error && "message" in error && typeof error.code === "string" && Object.hasOwn(EXIT, error.code)) return new CliError(error.code, String(error.message));
|
|
143
143
|
const local = localFileError(error);
|
|
144
144
|
if (local) return local;
|
|
145
145
|
const unopened = profileOpenError(error);
|
|
@@ -285,13 +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
|
-
* parser reads them. Stops at `--` and at the first word that is not a global option.
|
|
299
|
+
* parser reads them. Stops at `--` and at the first word that is not a global option. They go before the first word
|
|
300
|
+
* after the command's that starts with a dash (its first option, or the `--` that ends them), else at the end: never
|
|
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.
|
|
295
303
|
*/
|
|
296
304
|
function liftGlobals(argv) {
|
|
297
305
|
const lifted = [];
|
|
@@ -302,15 +310,16 @@ function liftGlobals(argv) {
|
|
|
302
310
|
const option = globalOption(arg);
|
|
303
311
|
if (!option || arg === "--") break;
|
|
304
312
|
const takesValue = option.type !== "boolean" && eq === -1;
|
|
313
|
+
if (takesValue && i + 1 >= argv.length) throw new CliError("usage", `--${globalName(arg)} needs a value`);
|
|
305
314
|
lifted.push(...argv.slice(i, i + (takesValue ? 2 : 1)));
|
|
306
315
|
i += takesValue ? 2 : 1;
|
|
307
316
|
}
|
|
308
317
|
const rest = argv.slice(i);
|
|
309
|
-
const
|
|
310
|
-
return
|
|
311
|
-
...rest.slice(0,
|
|
318
|
+
const dash = rest[0] === "--" ? 0 : rest.findIndex((word, i) => i > 0 && word.startsWith("-"));
|
|
319
|
+
return dash === -1 ? [...rest, ...lifted] : [
|
|
320
|
+
...rest.slice(0, dash),
|
|
312
321
|
...lifted,
|
|
313
|
-
...rest.slice(
|
|
322
|
+
...rest.slice(dash)
|
|
314
323
|
];
|
|
315
324
|
}
|
|
316
325
|
//#endregion
|
|
@@ -333,14 +342,26 @@ var SECRET_CONTEXT = /priv(?:ate)?[\s_-]*key|secret|seed|mnemonic|nsec|xprv|\bwi
|
|
|
333
342
|
/** How far from a 64-hex string a word like "seed" still counts as saying what it is. */
|
|
334
343
|
var CONTEXT_REACH = 48;
|
|
335
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;
|
|
336
351
|
var base58check = createBase58check(sha256);
|
|
337
|
-
/**
|
|
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
|
+
*/
|
|
338
356
|
function findSecret(text) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
if (
|
|
343
|
-
|
|
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;
|
|
344
365
|
}
|
|
345
366
|
/**
|
|
346
367
|
* 12 to 24 words in a row from the English BIP39 list whose checksum holds, anywhere in the text; or a message that
|
|
@@ -814,66 +835,119 @@ var CONTACT_KEY = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
|
|
|
814
835
|
* itself (which the contact controls, so it never wins over a label).
|
|
815
836
|
*/
|
|
816
837
|
function findChat(links, ref) {
|
|
838
|
+
return chatBy(links, ref).link;
|
|
839
|
+
}
|
|
840
|
+
/** `findChat`, and whether only the name the contact gave itself (`theirName`) found the chat. */
|
|
841
|
+
function chatBy(links, ref) {
|
|
817
842
|
const exact = links.find((link) => link.id === ref);
|
|
818
|
-
if (exact) return
|
|
843
|
+
if (exact) return {
|
|
844
|
+
link: exact,
|
|
845
|
+
theirName: false
|
|
846
|
+
};
|
|
819
847
|
if (CONTACT_KEY.test(ref)) {
|
|
820
848
|
const byKey = links.filter((link) => link.peerPubKeyZ32 === ref);
|
|
821
|
-
if (byKey.length === 1) return
|
|
849
|
+
if (byKey.length === 1) return {
|
|
850
|
+
link: byKey[0],
|
|
851
|
+
theirName: false
|
|
852
|
+
};
|
|
822
853
|
if (byKey.length > 1) throw new CliError("bad_request", `${JSON.stringify(ref)} names more than one chat: use its id`, { matches: byKey.map((link) => link.id) });
|
|
823
854
|
throw new CliError("not_found", `No chat with ${JSON.stringify(ref)}`);
|
|
824
855
|
}
|
|
825
856
|
const byPrefix = links.filter((link) => link.id.startsWith(ref));
|
|
826
|
-
if (byPrefix.length === 1) return
|
|
857
|
+
if (byPrefix.length === 1) return {
|
|
858
|
+
link: byPrefix[0],
|
|
859
|
+
theirName: false
|
|
860
|
+
};
|
|
827
861
|
const lower = ref.toLowerCase();
|
|
828
862
|
const named = (name) => name?.trim().toLowerCase() === lower;
|
|
829
863
|
const byLabel = links.filter((link) => named(link.label));
|
|
830
864
|
const byName = byLabel.length ? byLabel : links.filter((link) => named(link.peerNick));
|
|
831
|
-
if (byName.length === 1) return
|
|
865
|
+
if (byName.length === 1) return {
|
|
866
|
+
link: byName[0],
|
|
867
|
+
theirName: !byLabel.length
|
|
868
|
+
};
|
|
832
869
|
if (byPrefix.length > 1 || byName.length > 1) throw new CliError("bad_request", `${JSON.stringify(ref)} names more than one chat: use its id`, { matches: [...byPrefix, ...byName].map((link) => link.id) });
|
|
833
870
|
throw new CliError("not_found", `No chat ${JSON.stringify(ref)}`);
|
|
834
871
|
}
|
|
872
|
+
/** Only an invitation so far: this profile is not in the group (yet), and whoever invited chose its name. */
|
|
873
|
+
var invitedOnly = (group) => group.status === void 0 && !!group.invitation;
|
|
874
|
+
/**
|
|
875
|
+
* A group by its id, by a unique prefix of its id, or by its name. A group this profile is in wins over an invitation
|
|
876
|
+
* with the same prefix or name (the one who invites chooses those); an invitation is still found by its id, and by
|
|
877
|
+
* prefix or name where no group of this profile has it.
|
|
878
|
+
*/
|
|
835
879
|
function findGroup(groups, ref) {
|
|
836
880
|
const exact = groups.find((group) => group.id === ref);
|
|
837
881
|
if (exact) return exact;
|
|
838
|
-
const
|
|
882
|
+
const mineFirst = (found) => {
|
|
883
|
+
const mine = found.filter((group) => !invitedOnly(group));
|
|
884
|
+
return mine.length ? mine : found;
|
|
885
|
+
};
|
|
886
|
+
const byPrefix = mineFirst(groups.filter((group) => group.id.startsWith(ref)));
|
|
839
887
|
if (byPrefix.length === 1) return byPrefix[0];
|
|
840
|
-
const byName = groups.filter((group) => group.name.trim().toLowerCase() === ref.toLowerCase());
|
|
888
|
+
const byName = mineFirst(groups.filter((group) => group.name.trim().toLowerCase() === ref.toLowerCase()));
|
|
841
889
|
if (byName.length === 1) return byName[0];
|
|
842
890
|
if (byPrefix.length > 1 || byName.length > 1) throw new CliError("bad_request", `${JSON.stringify(ref)} names more than one group: use its id`, { matches: [...byPrefix, ...byName].map((group) => group.id) });
|
|
843
891
|
throw new CliError("not_found", `No group ${JSON.stringify(ref)}`);
|
|
844
892
|
}
|
|
845
|
-
var chatOf = (ctx, params) => findChat(state(ctx).links, str(params, "chat", true));
|
|
846
893
|
/**
|
|
847
|
-
* A chat or a group,
|
|
848
|
-
* then a group.
|
|
894
|
+
* A chat or a group, where a command takes either: `group:<id>` is a group; anything else a chat first (`findChat`),
|
|
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
|
|
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,
|
|
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.
|
|
849
899
|
*/
|
|
850
|
-
function
|
|
851
|
-
if (ref.startsWith("group:")) {
|
|
852
|
-
|
|
853
|
-
return {
|
|
854
|
-
id: group.id,
|
|
855
|
-
linkId: `group:${group.id}`,
|
|
856
|
-
group: true
|
|
857
|
-
};
|
|
858
|
-
}
|
|
900
|
+
function findChatOrGroup(links, groups, ref) {
|
|
901
|
+
if (ref.startsWith("group:")) return { group: findGroup(groups, ref.slice(6)) };
|
|
902
|
+
let chat;
|
|
859
903
|
try {
|
|
860
|
-
|
|
861
|
-
return {
|
|
862
|
-
id: link.id,
|
|
863
|
-
linkId: link.id,
|
|
864
|
-
group: false
|
|
865
|
-
};
|
|
904
|
+
chat = chatBy(links, ref);
|
|
866
905
|
} catch (error) {
|
|
867
906
|
if (!(error instanceof CliError) || error.code !== "not_found") throw error;
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
907
|
+
try {
|
|
908
|
+
return { group: findGroup(groups, ref) };
|
|
909
|
+
} catch (inner) {
|
|
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)`);
|
|
913
|
+
}
|
|
874
914
|
}
|
|
915
|
+
if (!chat.theirName) return { link: chat.link };
|
|
916
|
+
const exact = groups.find((group) => group.id === ref);
|
|
917
|
+
if (exact) return { group: exact };
|
|
918
|
+
const lower = ref.toLowerCase();
|
|
919
|
+
const also = groups.filter((group) => !invitedOnly(group) && (group.id.startsWith(ref) || group.name.trim().toLowerCase() === lower));
|
|
920
|
+
if (also.length) throw new CliError("bad_request", `${JSON.stringify(ref)} names a chat (the name its contact chose) and a group: use the chat's id or group:<id>`, {
|
|
921
|
+
chat: chat.link.id,
|
|
922
|
+
groups: also.map((group) => group.id)
|
|
923
|
+
});
|
|
924
|
+
return { link: chat.link };
|
|
925
|
+
}
|
|
926
|
+
var chatOf = (ctx, params) => findChat(state(ctx).links, str(params, "chat", true));
|
|
927
|
+
/** A chat or a group as `findChatOrGroup` reads it. `linkId` is what the engine calls it. */
|
|
928
|
+
function chatOrGroup(ctx, ref) {
|
|
929
|
+
const found = findChatOrGroup(state(ctx).links, state(ctx).groups, ref);
|
|
930
|
+
return "group" in found ? {
|
|
931
|
+
id: found.group.id,
|
|
932
|
+
linkId: `group:${found.group.id}`,
|
|
933
|
+
group: true
|
|
934
|
+
} : {
|
|
935
|
+
id: found.link.id,
|
|
936
|
+
linkId: found.link.id,
|
|
937
|
+
group: false
|
|
938
|
+
};
|
|
875
939
|
}
|
|
876
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
|
+
}
|
|
877
951
|
/** Resolves with `check`'s first defined answer: now, or on a later state; `timeout` error after `ms`. */
|
|
878
952
|
function waitForState(ctx, check, ms, what) {
|
|
879
953
|
const now = check(state(ctx));
|
|
@@ -1089,6 +1163,12 @@ var storedGroupFiles = {
|
|
|
1089
1163
|
read: (file, offset, length) => readStored(file, offset, length),
|
|
1090
1164
|
room: async () => (await fileBytes()).room()
|
|
1091
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;
|
|
1092
1172
|
/** Why a want waits, as the person reads it (the app shows its own words for each `wait`). */
|
|
1093
1173
|
var WAITING_NOTES = {
|
|
1094
1174
|
nobody: "Nobody you are connected to has this file yet",
|
|
@@ -1122,7 +1202,10 @@ var GroupFileDesk = class {
|
|
|
1122
1202
|
constructor(deps) {
|
|
1123
1203
|
this.deps = deps;
|
|
1124
1204
|
}
|
|
1125
|
-
/**
|
|
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
|
+
*/
|
|
1126
1209
|
async load(groupIds) {
|
|
1127
1210
|
for (const groupId of groupIds) for (const file of await this.deps.store.list(groupId).catch(() => [])) {
|
|
1128
1211
|
if (!file.group || !file.metadata || !file.digest || this.kept.has(file.id)) continue;
|
|
@@ -1163,7 +1246,8 @@ var GroupFileDesk = class {
|
|
|
1163
1246
|
retry: true,
|
|
1164
1247
|
error: file.transfer.error ?? DAMAGED$1
|
|
1165
1248
|
});
|
|
1166
|
-
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);
|
|
1167
1251
|
else this.show(kept, {
|
|
1168
1252
|
state: "transferring",
|
|
1169
1253
|
stage: "asking",
|
|
@@ -1188,6 +1272,56 @@ var GroupFileDesk = class {
|
|
|
1188
1272
|
}
|
|
1189
1273
|
}
|
|
1190
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
|
+
/**
|
|
1191
1325
|
* An error when this group took as many announcements of mine as it takes in a minute, or as its members take from
|
|
1192
1326
|
* one member in `announceWindowMs` (past that they would get the text, not the file); null when one may go now.
|
|
1193
1327
|
*/
|
|
@@ -1654,20 +1788,35 @@ var GroupFileDesk = class {
|
|
|
1654
1788
|
const reserved = active.reduce((sum, a) => sum + a.size, 0);
|
|
1655
1789
|
return this.served.bytes + reserved + kept.meta.size > GROUP_FILE_LIMITS.serveBytesPerDay ? "busy" : null;
|
|
1656
1790
|
}
|
|
1657
|
-
/**
|
|
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
|
+
*/
|
|
1658
1795
|
servingNow() {
|
|
1659
1796
|
const active = [];
|
|
1660
1797
|
for (const edge of this.edgesById.values()) for (const wire of edge.serving.keys()) {
|
|
1661
1798
|
const record = edge.files.get("out", wire);
|
|
1662
1799
|
if (record && !transferEnded(record)) active.push({
|
|
1663
1800
|
peer: edge.peer,
|
|
1664
|
-
size: record.file.size,
|
|
1801
|
+
size: Math.max(record.file.size, edge.files.sent(wire)),
|
|
1665
1802
|
sending: record.confirmed < record.file.size
|
|
1666
1803
|
});
|
|
1667
1804
|
}
|
|
1668
1805
|
return active;
|
|
1669
1806
|
}
|
|
1670
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
|
+
/**
|
|
1671
1820
|
* Served transfers that no longer count end, their places free for others (WISP 503 § Serving limits): the asker's
|
|
1672
1821
|
* session closed, it is not in the roster any more, or its bytes have not moved for `wantWaitMs` (the asker has moved
|
|
1673
1822
|
* on by then).
|
|
@@ -1720,6 +1869,7 @@ var GroupFileDesk = class {
|
|
|
1720
1869
|
serving: /* @__PURE__ */ new Map(),
|
|
1721
1870
|
servedMoved: /* @__PURE__ */ new Map(),
|
|
1722
1871
|
taking: /* @__PURE__ */ new Map(),
|
|
1872
|
+
ended: [],
|
|
1723
1873
|
have: /* @__PURE__ */ new Set(),
|
|
1724
1874
|
haveAt: 0,
|
|
1725
1875
|
busy: /* @__PURE__ */ new Set()
|
|
@@ -1737,7 +1887,8 @@ var GroupFileDesk = class {
|
|
|
1737
1887
|
openSource: (record) => this.openSource(edge, record),
|
|
1738
1888
|
changed: (record, transferred) => this.transferChanged(edge, record, transferred),
|
|
1739
1889
|
room: () => this.deps.store.room(),
|
|
1740
|
-
writable: () => this.deps.writable?.(linkId)
|
|
1890
|
+
writable: () => this.deps.writable?.(linkId),
|
|
1891
|
+
resend: (record) => !edge.serving.has(record.id) || this.mayResend()
|
|
1741
1892
|
});
|
|
1742
1893
|
this.edgesById.set(linkId, edge);
|
|
1743
1894
|
return edge;
|
|
@@ -1776,7 +1927,7 @@ var GroupFileDesk = class {
|
|
|
1776
1927
|
if (resumed) kept.fresh = kept.unclear = true;
|
|
1777
1928
|
else kept.damaged.add(edge.peer);
|
|
1778
1929
|
}
|
|
1779
|
-
return kept.idle = (damaged ? writer.discard() : writer.close()).catch(() => {});
|
|
1930
|
+
return kept.idle = (damaged || kept.dropped === record.id ? writer.discard() : writer.close()).catch(() => {});
|
|
1780
1931
|
}
|
|
1781
1932
|
};
|
|
1782
1933
|
}
|
|
@@ -1789,7 +1940,14 @@ var GroupFileDesk = class {
|
|
|
1789
1940
|
digest: async () => kept.meta.d
|
|
1790
1941
|
};
|
|
1791
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
|
+
}
|
|
1792
1949
|
transferChanged(edge, record, transferred) {
|
|
1950
|
+
if (transferEnded(record)) this.transferOver(edge, record);
|
|
1793
1951
|
if (record.direction === "out") {
|
|
1794
1952
|
if (transferEnded(record) && edge.serving.has(record.id)) {
|
|
1795
1953
|
const today = Math.floor(this.deps.now() / 864e5);
|
|
@@ -1797,7 +1955,7 @@ var GroupFileDesk = class {
|
|
|
1797
1955
|
day: today,
|
|
1798
1956
|
bytes: 0
|
|
1799
1957
|
};
|
|
1800
|
-
this.served.bytes += record.
|
|
1958
|
+
this.served.bytes += edge.files.sent(record.id);
|
|
1801
1959
|
edge.serving.delete(record.id);
|
|
1802
1960
|
edge.servedMoved.delete(record.id);
|
|
1803
1961
|
const serving = this.deps.settings().serveFiles;
|
|
@@ -2375,24 +2533,8 @@ function filesOf(messages, transfers) {
|
|
|
2375
2533
|
transfer: transfers[m.file.id] ?? null
|
|
2376
2534
|
}));
|
|
2377
2535
|
}
|
|
2378
|
-
/**
|
|
2379
|
-
|
|
2380
|
-
* as `forward` reads them. A name that is neither says what a chat would.
|
|
2381
|
-
*/
|
|
2382
|
-
function chatOrGroupOf(ctx, params) {
|
|
2383
|
-
const ref = str(params, "chat", true);
|
|
2384
|
-
if (ref.startsWith("group:")) return { group: findGroup(state(ctx).groups, ref.slice(6)) };
|
|
2385
|
-
try {
|
|
2386
|
-
return { link: chatOf(ctx, params) };
|
|
2387
|
-
} catch (error) {
|
|
2388
|
-
if (!(error instanceof CliError) || error.code !== "not_found") throw error;
|
|
2389
|
-
try {
|
|
2390
|
-
return { group: findGroup(state(ctx).groups, ref) };
|
|
2391
|
-
} catch {
|
|
2392
|
-
throw error;
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2536
|
+
/** The chat or the group a file command names (`findChatOrGroup`). */
|
|
2537
|
+
var chatOrGroupOf = (ctx, params) => findChatOrGroup(state(ctx).links, state(ctx).groups, str(params, "chat", true));
|
|
2396
2538
|
/**
|
|
2397
2539
|
* The chat or group a file belongs to: a file id starts with its chat's id (a group's file with `group-<id>-`), so it
|
|
2398
2540
|
* may be left out; given, it must be the file's own. `where` is what the answer names it by.
|
|
@@ -2529,6 +2671,15 @@ function fitName(name) {
|
|
|
2529
2671
|
while (stem.length && !fits(stem.join("").trimEnd() + extension)) stem.pop();
|
|
2530
2672
|
return (stem.join("").trimEnd() || "file") + extension;
|
|
2531
2673
|
}
|
|
2674
|
+
/**
|
|
2675
|
+
* The name `file save --dir` writes a file under. Its sender chose it, and the folder is one scripts and people run
|
|
2676
|
+
* tools in: a name that starts with a dash is an option to the tool a `*` hands it to ("-rf", "--checkpoint-action=…"),
|
|
2677
|
+
* so it gets a `_` first, as a Windows device name does in `sanitizeFileName`.
|
|
2678
|
+
*/
|
|
2679
|
+
function saveName(name) {
|
|
2680
|
+
const clean = sanitizeFileName(name);
|
|
2681
|
+
return fitName(clean.startsWith("-") ? `_${clean}` : clean);
|
|
2682
|
+
}
|
|
2532
2683
|
var ACTIONS = [
|
|
2533
2684
|
"accept",
|
|
2534
2685
|
"decline",
|
|
@@ -2636,15 +2787,16 @@ var FILE_METHODS = {
|
|
|
2636
2787
|
/**
|
|
2637
2788
|
* files/3: answer an offer, or pause, resume or cancel a transfer, either way; `resend` a file sent from here that
|
|
2638
2789
|
* stopped moving (or failed), `request` again one that stopped arriving. Without a chat, the file's id names it.
|
|
2639
|
-
* 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.
|
|
2640
2792
|
*/
|
|
2641
2793
|
async "file.action"(ctx, params) {
|
|
2642
2794
|
const fileId = str(params, "file", true);
|
|
2643
2795
|
const { linkId, where } = fileChat(ctx, params, fileId);
|
|
2644
2796
|
const action = oneOf(params, "action", ACTIONS, "accept");
|
|
2645
2797
|
if ("group" in where) {
|
|
2646
|
-
if (action
|
|
2647
|
-
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");
|
|
2648
2800
|
try {
|
|
2649
2801
|
await node(ctx).fileAction({
|
|
2650
2802
|
linkId,
|
|
@@ -2717,7 +2869,7 @@ var FILE_METHODS = {
|
|
|
2717
2869
|
size: transfer.size
|
|
2718
2870
|
};
|
|
2719
2871
|
},
|
|
2720
|
-
/** Writes a received (or sent) file to disk: into `dir` under its own name, or to `path`. Never over a file unless `force`. */
|
|
2872
|
+
/** Writes a received (or sent) file to disk: into `dir` under its own name (`saveName`), or to `path`. Never over a file unless `force`. */
|
|
2721
2873
|
async "file.save"(ctx, params) {
|
|
2722
2874
|
const fileId = str(params, "file", true);
|
|
2723
2875
|
if (params.chat !== void 0) fileChat(ctx, params, fileId);
|
|
@@ -2730,7 +2882,7 @@ var FILE_METHODS = {
|
|
|
2730
2882
|
if (stored.leftOut) throw new CliError("not_found", "Not in this backup: the light backup this profile was restored from left the file out");
|
|
2731
2883
|
const transfer = state(ctx).transfers[fileId] ?? stored.transfer;
|
|
2732
2884
|
if (transfer && transfer.state !== "done") throw new CliError("unavailable", `The file is not all here yet (${transfer.transferred} of ${transfer.size} bytes)`);
|
|
2733
|
-
const target = str(params, "path") ? resolve(str(params, "path", true)) : join(resolve(str(params, "dir") ?? "."),
|
|
2885
|
+
const target = str(params, "path") ? resolve(str(params, "path", true)) : join(resolve(str(params, "dir") ?? "."), saveName(stored.metadata.name));
|
|
2734
2886
|
const flags = bool(params, "force") ? "w" : "wx";
|
|
2735
2887
|
let out;
|
|
2736
2888
|
try {
|
|
@@ -2922,12 +3074,74 @@ function checked$1(card) {
|
|
|
2922
3074
|
if ("error" in result) throw new CliError("bad_request", `Status card: ${result.error}`, { card });
|
|
2923
3075
|
return result.card;
|
|
2924
3076
|
}
|
|
2925
|
-
/**
|
|
3077
|
+
/** Every text in a card's fields (title, step, items, runs, links…), as the contact's app may show it. */
|
|
3078
|
+
function cardTexts(value, out = []) {
|
|
3079
|
+
if (typeof value === "string") out.push(value);
|
|
3080
|
+
else if (Array.isArray(value)) for (const item of value) cardTexts(item, out);
|
|
3081
|
+
else if (isObject(value)) for (const item of Object.values(value)) cardTexts(item, out);
|
|
3082
|
+
return out;
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* The secret guard `send` has (docs/CLI.md § Safety): a card whose text looks like a seed, a private key or a Cashu
|
|
3086
|
+
* token goes only with `--force`. `fields` are what this command gives (an update's own, not the card it merges over).
|
|
3087
|
+
* A card's text is read as given and as the card carries it (`cardLine`): a line loses its invisible characters on the
|
|
3088
|
+
* way, so a key one of them broke in two is whole again on the card. The fallback text goes as it is, as `send`'s.
|
|
3089
|
+
*/
|
|
3090
|
+
function guardSecrets(fields, params, verb) {
|
|
3091
|
+
if (bool(params, "force")) return;
|
|
3092
|
+
const check = (text) => {
|
|
3093
|
+
const secret = findSecret(text);
|
|
3094
|
+
if (secret) throw new CliError("confirm", `The card's text looks like ${secret.kind === "cashu" ? "a Cashu token (money anyone who reads it can take)" : "a secret (a seed or a private key)"}; ${verb} it with --force if you mean to`, { kind: secret.kind });
|
|
3095
|
+
};
|
|
3096
|
+
for (const text of cardTexts([fields, params.run])) {
|
|
3097
|
+
check(text);
|
|
3098
|
+
check(cardLine(text, Infinity) ?? "");
|
|
3099
|
+
}
|
|
3100
|
+
if (typeof params.text === "string") check(params.text);
|
|
3101
|
+
}
|
|
3102
|
+
/** Per card (its chat or group, its kind and its id): the message of mine that carries it, as this process last saw it. */
|
|
3103
|
+
var carriers = /* @__PURE__ */ new WeakMap();
|
|
3104
|
+
var carrierKey = (linkId, kind, id) => `${linkId}\n${kind}\n${id}`;
|
|
3105
|
+
function carriersOf(ctx) {
|
|
3106
|
+
let map = carriers.get(ctx);
|
|
3107
|
+
if (map) return map;
|
|
3108
|
+
carriers.set(ctx, map = /* @__PURE__ */ new Map());
|
|
3109
|
+
const cards = map;
|
|
3110
|
+
ctx.hub.onEvent((event) => {
|
|
3111
|
+
if (event.type !== "message.sent" && event.type !== "group.sent") return;
|
|
3112
|
+
const message = event.message;
|
|
3113
|
+
const card = message?.card;
|
|
3114
|
+
if (typeof message?.id !== "string" || typeof card?.id !== "string" || card.kind !== "task" && card.kind !== "routine" && card.kind !== "usage") return;
|
|
3115
|
+
carried(cards, carrierKey(event.type === "group.sent" ? `group:${String(event.group)}` : String(event.chat), card.kind, card.id), message.id);
|
|
3116
|
+
});
|
|
3117
|
+
return map;
|
|
3118
|
+
}
|
|
3119
|
+
function carried(cards, key, messageId) {
|
|
3120
|
+
cards.delete(key);
|
|
3121
|
+
cards.set(key, messageId);
|
|
3122
|
+
if (cards.size > 1e3) cards.delete(cards.keys().next().value);
|
|
3123
|
+
}
|
|
3124
|
+
/**
|
|
3125
|
+
* The latest message of mine in this chat or group with a card of this kind and id: the one an update edits. A card
|
|
3126
|
+
* this process sent or found before is read by its message's key, so its updates cost the same in a long chat as in a
|
|
3127
|
+
* short one; only a card not seen yet (the first update after a start, a one-shot) is looked for in the whole history.
|
|
3128
|
+
*/
|
|
2926
3129
|
async function cardMessage(ctx, target, kind, id) {
|
|
3130
|
+
const cards = carriersOf(ctx), key = carrierKey(target.linkId, kind, id);
|
|
3131
|
+
const carries = (message) => message?.sender === "me" && message.card?.kind === kind && message.card.id === id;
|
|
3132
|
+
const known = cards.get(key);
|
|
3133
|
+
if (known !== void 0) {
|
|
3134
|
+
const message = await node(ctx).getMessage(target.linkId, known);
|
|
3135
|
+
if (carries(message)) return message;
|
|
3136
|
+
cards.delete(key);
|
|
3137
|
+
}
|
|
2927
3138
|
const messages = target.group ? await node(ctx).groupMessages({ groupId: target.id }) : await node(ctx).getMessages(target.linkId);
|
|
3139
|
+
if (cards.has(key)) return cardMessage(ctx, target, kind, id);
|
|
2928
3140
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2929
3141
|
const message = messages[i];
|
|
2930
|
-
if (message
|
|
3142
|
+
if (!carries(message)) continue;
|
|
3143
|
+
carried(cards, key, message.id);
|
|
3144
|
+
return message;
|
|
2931
3145
|
}
|
|
2932
3146
|
throw new CliError("not_found", `No ${kind} ${JSON.stringify(id)} of yours in this ${target.group ? "group" : "chat"}: send it first with ghostly ${kind} send`);
|
|
2933
3147
|
}
|
|
@@ -2955,6 +3169,7 @@ async function parentThere(ctx, target, card) {
|
|
|
2955
3169
|
async function sendCard(ctx, params, kind) {
|
|
2956
3170
|
const target = chatOrGroup(ctx, str(params, "chat", true));
|
|
2957
3171
|
const fields = fieldsOf(params);
|
|
3172
|
+
guardSecrets(fields, params, "send");
|
|
2958
3173
|
const now = Date.now();
|
|
2959
3174
|
const id = typeof fields.id === "string" && fields.id ? fields.id : `${kind}-${toBase64Url(randomBytes$1(6))}`;
|
|
2960
3175
|
const card = checked$1(withRun({
|
|
@@ -2985,6 +3200,7 @@ async function sendCard(ctx, params, kind) {
|
|
|
2985
3200
|
card
|
|
2986
3201
|
});
|
|
2987
3202
|
if (result.error || !result.messageId) throw new CliError(result.refused ? "refused" : "unavailable", result.error ?? "Nothing to send");
|
|
3203
|
+
carried(carriersOf(ctx), carrierKey(target.linkId, kind, id), result.messageId);
|
|
2988
3204
|
const edges = wait === "sent" ? await waitForGroupFrame(ctx, target.id, result.messageId, void 0, ms) : void 0;
|
|
2989
3205
|
return answer(target, kind, id, result.messageId, {
|
|
2990
3206
|
card,
|
|
@@ -2998,6 +3214,7 @@ async function sendCard(ctx, params, kind) {
|
|
|
2998
3214
|
});
|
|
2999
3215
|
if (result.error) throw new CliError(result.refused ? "refused" : "unavailable", result.error);
|
|
3000
3216
|
if (!result.messageId) throw new CliError("bad_request", "Nothing to send");
|
|
3217
|
+
carried(carriersOf(ctx), carrierKey(target.linkId, kind, id), result.messageId);
|
|
3001
3218
|
let message = await node(ctx).getMessage(target.linkId, result.messageId);
|
|
3002
3219
|
if (wait !== "none") message = await waitForMessage(ctx, target.linkId, result.messageId, wait, ms);
|
|
3003
3220
|
return answer(target, kind, id, result.messageId, {
|
|
@@ -3024,6 +3241,7 @@ async function updateCard(ctx, params, kind) {
|
|
|
3024
3241
|
const target = chatOrGroup(ctx, str(params, "chat", true));
|
|
3025
3242
|
const id = str(params, kind, true);
|
|
3026
3243
|
const patch = fieldsOf(params);
|
|
3244
|
+
guardSecrets(patch, params, "update");
|
|
3027
3245
|
const message = await cardMessage(ctx, target, kind, id);
|
|
3028
3246
|
if ((message.edit?.seq ?? 0) >= STATUS_CARD_LIMITS.edits) throw new CliError("refused", `This ${kind}'s message took ${STATUS_CARD_LIMITS.edits} updates, the most one takes: start a new card with ghostly ${kind} send (a new --id, or the same one: the newest card of an id stands for it)`, {
|
|
3029
3247
|
messageId: message.id,
|
|
@@ -3065,7 +3283,7 @@ async function updateCard(ctx, params, kind) {
|
|
|
3065
3283
|
pace.last = Date.now();
|
|
3066
3284
|
await editCard(ctx, target, message.id, card, text);
|
|
3067
3285
|
}
|
|
3068
|
-
let edited =
|
|
3286
|
+
let edited = await node(ctx).getMessage(target.linkId, message.id);
|
|
3069
3287
|
if (wait === "confirmed" && edited?.edit?.pending) edited = await waitForEdit(ctx, target.linkId, message.id, ms);
|
|
3070
3288
|
const edges = wait === "sent" && edited?.edit ? await waitForGroupFrame(ctx, target.id, message.id, edited.edit.seq, ms) : void 0;
|
|
3071
3289
|
return answer(target, kind, id, message.id, {
|
|
@@ -3245,8 +3463,7 @@ function pressable(link) {
|
|
|
3245
3463
|
return peer ? peer.includes(BUTTONS_CAPABILITY) : null;
|
|
3246
3464
|
}
|
|
3247
3465
|
/** The message of mine with buttons that `button update` names, by its id here (or the id both sides know). */
|
|
3248
|
-
function buttonsMessage(
|
|
3249
|
-
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) {
|
|
3250
3467
|
if (!message) throw new CliError("not_found", `No message ${JSON.stringify(ref)} of yours in this ${group ? "group" : "chat"}: the messageId send gave`);
|
|
3251
3468
|
if (message.card?.kind !== "buttons") throw new CliError("bad_request", "That message has no buttons: send one with send --button id:Label");
|
|
3252
3469
|
return message;
|
|
@@ -3266,12 +3483,13 @@ async function updateButtons(ctx, params) {
|
|
|
3266
3483
|
const secret = findSecret(newText);
|
|
3267
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 });
|
|
3268
3485
|
}
|
|
3269
|
-
const message = buttonsMessage(
|
|
3486
|
+
const message = buttonsMessage(await messageOf(ctx, target.linkId, ref, (m) => m.sender === "me"), ref, target.group);
|
|
3270
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`, {
|
|
3271
3488
|
messageId: message.id,
|
|
3272
3489
|
edits: message.edit.seq
|
|
3273
3490
|
});
|
|
3274
3491
|
const before = message.card;
|
|
3492
|
+
if (chosen !== void 0 && !before.buttons.some((b) => b.id === chosen)) throw new CliError("bad_request", `That message has no button ${JSON.stringify(chosen)}; its buttons: ${before.buttons.map((b) => b.id).join(", ")}`, { card: before });
|
|
3275
3493
|
const card = checked({
|
|
3276
3494
|
...before,
|
|
3277
3495
|
...chosen !== void 0 && { chosen },
|
|
@@ -3290,7 +3508,7 @@ async function updateButtons(ctx, params) {
|
|
|
3290
3508
|
card
|
|
3291
3509
|
});
|
|
3292
3510
|
if (result.error) throw new CliError(result.refused ? "refused" : "unavailable", result.error);
|
|
3293
|
-
let edited =
|
|
3511
|
+
let edited = await node(ctx).getMessage(target.linkId, message.id);
|
|
3294
3512
|
if (wait === "confirmed" && edited?.edit?.pending) edited = await waitForEdit(ctx, target.linkId, message.id, ms);
|
|
3295
3513
|
const edges = wait === "sent" && edited?.edit ? await waitForGroupFrame(ctx, target.id, message.id, edited.edit.seq, ms) : void 0;
|
|
3296
3514
|
return {
|
|
@@ -3313,8 +3531,7 @@ async function pressButton(ctx, params) {
|
|
|
3313
3531
|
const ref = str(params, "message", true);
|
|
3314
3532
|
const buttonId = str(params, "button", true);
|
|
3315
3533
|
const where = target.group ? "group" : "chat";
|
|
3316
|
-
const
|
|
3317
|
-
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");
|
|
3318
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`);
|
|
3319
3536
|
if (message.card?.kind !== "buttons") throw new CliError("bad_request", "That message has no buttons");
|
|
3320
3537
|
const button = message.card.buttons.find((b) => b.id === buttonId);
|
|
@@ -3473,7 +3690,7 @@ function isLive(link) {
|
|
|
3473
3690
|
if (link.textDelivery !== "stream" || link.dataLink !== "open") return false;
|
|
3474
3691
|
return !link.profile || link.pairing?.status === "ready" && !!link.pairing.transport;
|
|
3475
3692
|
}
|
|
3476
|
-
function chatJson(link) {
|
|
3693
|
+
function chatJson(link, showSecret) {
|
|
3477
3694
|
const label = link.label?.trim() || null;
|
|
3478
3695
|
const peerName = link.peerNick?.trim() || null;
|
|
3479
3696
|
const live = isLive(link);
|
|
@@ -3491,15 +3708,15 @@ function chatJson(link) {
|
|
|
3491
3708
|
pairing: link.pairing?.status ?? null,
|
|
3492
3709
|
stage: link.pairingProgress?.stage ?? null,
|
|
3493
3710
|
verified: !!link.peerVerified,
|
|
3494
|
-
invite: link.pairing?.status === "ready" ? null : link.inviteCode
|
|
3711
|
+
invite: link.pairing?.status === "ready" || !link.inviteCode ? null : showSecret === true ? link.inviteCode : "<hidden>",
|
|
3495
3712
|
compatibility: !link.profile,
|
|
3496
3713
|
peerPicture: !!link.peerAvatar
|
|
3497
3714
|
};
|
|
3498
3715
|
}
|
|
3499
3716
|
/** A chat's details: the summary plus what its connection panel shows. */
|
|
3500
|
-
function chatDetailsJson(link) {
|
|
3717
|
+
function chatDetailsJson(link, showSecret) {
|
|
3501
3718
|
return {
|
|
3502
|
-
...chatJson(link),
|
|
3719
|
+
...chatJson(link, showSecret),
|
|
3503
3720
|
progress: link.pairingProgress ?? null,
|
|
3504
3721
|
transports: {
|
|
3505
3722
|
mine: link.availableTransports ?? [],
|
|
@@ -3669,10 +3886,15 @@ function groupJson(group, showSecret = false) {
|
|
|
3669
3886
|
* Group administration and pictures (WISP 1100, phase 3): what the app's group menu and profile screen do. The
|
|
3670
3887
|
* engine enforces who may do what (the admin); these check the arguments and name members.
|
|
3671
3888
|
*/
|
|
3672
|
-
/**
|
|
3889
|
+
/**
|
|
3890
|
+
* A member by key, unique key prefix or name; never this profile itself. A whole key is that member, whatever another
|
|
3891
|
+
* member calls itself; a prefix and a name stand side by side (two members is an error, not a guess).
|
|
3892
|
+
*/
|
|
3673
3893
|
function findMember(group, ref) {
|
|
3674
3894
|
const lower = ref.toLowerCase();
|
|
3675
|
-
const
|
|
3895
|
+
const others = group.members.filter((m) => !m.me);
|
|
3896
|
+
const byKey = others.filter((m) => m.key === ref);
|
|
3897
|
+
const matches = byKey.length ? byKey : others.filter((m) => m.key.startsWith(ref) || m.nick?.trim().toLowerCase() === lower);
|
|
3676
3898
|
if (matches.length === 1) return matches[0];
|
|
3677
3899
|
throw new CliError(matches.length ? "bad_request" : "not_found", matches.length ? `${JSON.stringify(ref)} names more than one member` : `No member ${JSON.stringify(ref)} in ${group.name}`);
|
|
3678
3900
|
}
|
|
@@ -3789,6 +4011,63 @@ var GROUP_ADMIN_METHODS = {
|
|
|
3789
4011
|
}
|
|
3790
4012
|
};
|
|
3791
4013
|
//#endregion
|
|
4014
|
+
//#region src/turnRefs.ts
|
|
4015
|
+
/**
|
|
4016
|
+
* The id an agent turn names its message by (docs/CLI.md § Agent turns). A message is stored under an id its sender
|
|
4017
|
+
* chose most of (`peer_<the id they sent>`), so a turn carries this one instead, made here from the chat (or group) and
|
|
4018
|
+
* that id: `t_` and 32 hex digits, in which nobody can write a word. Every command that takes a message's id takes it.
|
|
4019
|
+
*/
|
|
4020
|
+
function turnRef(where, messageId) {
|
|
4021
|
+
return "t_" + createHash("sha256").update(`${where}\n${messageId}`).digest("hex").slice(0, 32);
|
|
4022
|
+
}
|
|
4023
|
+
var TURN_REF = /^t_[0-9a-f]{32}$/;
|
|
4024
|
+
var isRef = (value) => typeof value === "string" && TURN_REF.test(value);
|
|
4025
|
+
/**
|
|
4026
|
+
* `params` with each turn's id in `message`, `reply` and `messages` as the id its message is stored under: looked for
|
|
4027
|
+
* among the chat's (or group's) newest messages first, where a turn's almost always is, then in all of them. One that
|
|
4028
|
+
* names no message stays as given, and its command says so as for any unknown id.
|
|
4029
|
+
*/
|
|
4030
|
+
async function withStoredIds(ctx, params) {
|
|
4031
|
+
const wanted = new Set([
|
|
4032
|
+
params.message,
|
|
4033
|
+
params.reply,
|
|
4034
|
+
...Array.isArray(params.messages) ? params.messages : []
|
|
4035
|
+
].filter(isRef));
|
|
4036
|
+
if (!wanted.size) return params;
|
|
4037
|
+
let target = null;
|
|
4038
|
+
try {
|
|
4039
|
+
if (typeof params.group === "string") {
|
|
4040
|
+
const group = findGroup(state(ctx).groups, params.group);
|
|
4041
|
+
target = {
|
|
4042
|
+
id: group.id,
|
|
4043
|
+
linkId: `group:${group.id}`,
|
|
4044
|
+
group: true
|
|
4045
|
+
};
|
|
4046
|
+
} else if (typeof params.chat === "string") target = chatOrGroup(ctx, params.chat);
|
|
4047
|
+
} catch {}
|
|
4048
|
+
if (!target) return params;
|
|
4049
|
+
const { id, linkId, group } = target;
|
|
4050
|
+
const found = /* @__PURE__ */ new Map();
|
|
4051
|
+
const look = (messages) => {
|
|
4052
|
+
for (const message of messages) {
|
|
4053
|
+
const ref = turnRef(id, message.id);
|
|
4054
|
+
if (wanted.has(ref)) found.set(ref, message.id);
|
|
4055
|
+
}
|
|
4056
|
+
return found.size === wanted.size;
|
|
4057
|
+
};
|
|
4058
|
+
if (!look((await node(ctx).messagePage({
|
|
4059
|
+
linkId,
|
|
4060
|
+
limit: 200
|
|
4061
|
+
})).messages)) look(group ? await node(ctx).groupMessages({ groupId: id }) : await node(ctx).getMessages(linkId));
|
|
4062
|
+
const stored = (value) => isRef(value) ? found.get(value) ?? value : value;
|
|
4063
|
+
return {
|
|
4064
|
+
...params,
|
|
4065
|
+
...params.message !== void 0 && { message: stored(params.message) },
|
|
4066
|
+
...params.reply !== void 0 && { reply: stored(params.reply) },
|
|
4067
|
+
...Array.isArray(params.messages) && { messages: params.messages.map(stored) }
|
|
4068
|
+
};
|
|
4069
|
+
}
|
|
4070
|
+
//#endregion
|
|
3792
4071
|
//#region ../browser/src/proofs/bitcoinWallets.ts
|
|
3793
4072
|
/** What a proof proves and what it does not; shown next to every Bitcoin proof, given or received. */
|
|
3794
4073
|
var BITCOIN_PROOF_LIMITS = [
|
|
@@ -12369,6 +12648,25 @@ function checkTextSize(text) {
|
|
|
12369
12648
|
max: LIMITS.maxChatMessageBytes
|
|
12370
12649
|
});
|
|
12371
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
|
+
}
|
|
12372
12670
|
/** Settings as a bot may read them: storage credentials and other secrets masked unless asked for. */
|
|
12373
12671
|
function redactSettings(settings, showSecret = false) {
|
|
12374
12672
|
if (showSecret) return settings;
|
|
@@ -12396,7 +12694,9 @@ var SETTABLE = {
|
|
|
12396
12694
|
online: "boolean",
|
|
12397
12695
|
shareProfile: "boolean",
|
|
12398
12696
|
sendTyping: "boolean",
|
|
12399
|
-
nick: "string"
|
|
12697
|
+
nick: "string",
|
|
12698
|
+
autoDownloads: "boolean",
|
|
12699
|
+
serveFiles: "boolean"
|
|
12400
12700
|
};
|
|
12401
12701
|
var TRANSPORT_NAMES = {
|
|
12402
12702
|
auto: "auto",
|
|
@@ -12504,7 +12804,7 @@ var METHODS = {
|
|
|
12504
12804
|
async "settings.set"(ctx, params) {
|
|
12505
12805
|
const patch = {};
|
|
12506
12806
|
for (const [key, value] of Object.entries(params)) {
|
|
12507
|
-
const kind = SETTABLE[key];
|
|
12807
|
+
const kind = Object.hasOwn(SETTABLE, key) ? SETTABLE[key] : void 0;
|
|
12508
12808
|
if (!kind) throw new CliError("bad_request", `${key} is not a setting the CLI changes (${Object.keys(SETTABLE).join(", ")})`);
|
|
12509
12809
|
if (kind === "strings" && !(Array.isArray(value) && value.every((v) => typeof v === "string"))) throw new CliError("bad_request", `${key} must be a list of strings`);
|
|
12510
12810
|
if (kind === "string" && typeof value !== "string") throw new CliError("bad_request", `${key} must be a string`);
|
|
@@ -12559,13 +12859,14 @@ var METHODS = {
|
|
|
12559
12859
|
node(ctx).setActiveLink({ linkId });
|
|
12560
12860
|
return { chat: linkId };
|
|
12561
12861
|
},
|
|
12562
|
-
async "chat.list"(ctx) {
|
|
12563
|
-
|
|
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) };
|
|
12564
12865
|
},
|
|
12565
12866
|
async "chat.get"(ctx, params) {
|
|
12566
12867
|
const link = chatOf(ctx, params);
|
|
12567
12868
|
return {
|
|
12568
|
-
...chatDetailsJson(link),
|
|
12869
|
+
...chatDetailsJson(link, bool(params, "showSecret")),
|
|
12569
12870
|
heldUntil: holdOf(ctx, link.id)
|
|
12570
12871
|
};
|
|
12571
12872
|
},
|
|
@@ -12631,7 +12932,7 @@ var METHODS = {
|
|
|
12631
12932
|
messageId,
|
|
12632
12933
|
text
|
|
12633
12934
|
});
|
|
12634
|
-
if (result.error) throw editRefused(await
|
|
12935
|
+
if (result.error) throw editRefused(await messageOf(ctx, link.id, messageId), messageId, "chat", result.error, result.refused);
|
|
12635
12936
|
const id = result.messageId ?? messageId;
|
|
12636
12937
|
let message = await node(ctx).getMessage(link.id, id);
|
|
12637
12938
|
if (wait === "confirmed" && message?.edit?.pending) message = await waitForEdit(ctx, link.id, id, num(params, "timeout", 30, {
|
|
@@ -12685,7 +12986,7 @@ var METHODS = {
|
|
|
12685
12986
|
},
|
|
12686
12987
|
/** WISP 400 § Reactions: one emoji per person per message, a new one replaces mine; `remove` takes mine back. */
|
|
12687
12988
|
async "chat.react"(ctx, params) {
|
|
12688
|
-
const link = chatOf(ctx, params);
|
|
12989
|
+
const link = notAGroup(ctx, params, () => chatOf(ctx, params), "group react", "<message> <emoji>");
|
|
12689
12990
|
return {
|
|
12690
12991
|
chat: link.id,
|
|
12691
12992
|
...await react(ctx, link.id, params)
|
|
@@ -12722,10 +13023,12 @@ var METHODS = {
|
|
|
12722
13023
|
if (!messageIds.length) throw new CliError("bad_request", "Name the messages to forward (their ids, from history or an event)");
|
|
12723
13024
|
const to = list(params, "to").map((ref) => chatOrGroup(ctx, ref));
|
|
12724
13025
|
if (!to.length) throw new CliError("bad_request", "Forward to which chat? Name one or more with --to");
|
|
12725
|
-
const
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
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
|
+
}
|
|
12729
13032
|
if (!bool(params, "force")) for (const message of chosen) {
|
|
12730
13033
|
const secret = message.file ? null : findSecret(message.text);
|
|
12731
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`, {
|
|
@@ -12759,7 +13062,7 @@ var METHODS = {
|
|
|
12759
13062
|
await waitForGroupFrame(ctx, to[i].id, messageId, void 0, ms);
|
|
12760
13063
|
continue;
|
|
12761
13064
|
}
|
|
12762
|
-
const sent =
|
|
13065
|
+
const sent = await node(ctx).getMessage(to[i].id, messageId);
|
|
12763
13066
|
if (sent?.file) await FILE_METHODS["file.wait"](ctx, {
|
|
12764
13067
|
chat: to[i].id,
|
|
12765
13068
|
file: sent.file.id,
|
|
@@ -12791,7 +13094,7 @@ var METHODS = {
|
|
|
12791
13094
|
async "chat.delete"(ctx, params) {
|
|
12792
13095
|
const link = chatOf(ctx, params);
|
|
12793
13096
|
const messageId = str(params, "message", true);
|
|
12794
|
-
if (!
|
|
13097
|
+
if (!await node(ctx).getMessage(link.id, messageId)) throw new CliError("not_found", `No message ${messageId} in this chat`);
|
|
12795
13098
|
await node(ctx).deleteMessage({
|
|
12796
13099
|
linkId: link.id,
|
|
12797
13100
|
messageId
|
|
@@ -12826,7 +13129,8 @@ var METHODS = {
|
|
|
12826
13129
|
},
|
|
12827
13130
|
async "chat.transport"(ctx, params) {
|
|
12828
13131
|
const link = chatOf(ctx, params);
|
|
12829
|
-
const
|
|
13132
|
+
const name = str(params, "transport", true);
|
|
13133
|
+
const transport = Object.hasOwn(TRANSPORT_NAMES, name) ? TRANSPORT_NAMES[name] : void 0;
|
|
12830
13134
|
if (!transport) throw new CliError("bad_request", `transport must be one of auto, dht, webrtc, iroh, hyperdht`);
|
|
12831
13135
|
await node(ctx).setChatTransport({
|
|
12832
13136
|
linkId: link.id,
|
|
@@ -12906,7 +13210,13 @@ var METHODS = {
|
|
|
12906
13210
|
};
|
|
12907
13211
|
},
|
|
12908
13212
|
async "group.join"(ctx, params) {
|
|
12909
|
-
|
|
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
|
+
}
|
|
12910
13220
|
return { group: groupId };
|
|
12911
13221
|
},
|
|
12912
13222
|
async "group.list"(ctx, params) {
|
|
@@ -12975,15 +13285,16 @@ var METHODS = {
|
|
|
12975
13285
|
}
|
|
12976
13286
|
const mentions = mentionsFor(text, list(params, "mentions"), group);
|
|
12977
13287
|
const wait = oneOf(params, "wait", ["none", "sent"], "none");
|
|
12978
|
-
const
|
|
13288
|
+
const linkId = `group:${group.id}`;
|
|
13289
|
+
const before = (await node(ctx).getMessage(linkId, messageId))?.edit?.seq ?? 0;
|
|
12979
13290
|
const result = await node(ctx).editMessage({
|
|
12980
|
-
linkId
|
|
13291
|
+
linkId,
|
|
12981
13292
|
messageId,
|
|
12982
13293
|
text,
|
|
12983
13294
|
...mentions.length ? { mentions } : {}
|
|
12984
13295
|
});
|
|
12985
|
-
if (result.error) throw editRefused(await
|
|
12986
|
-
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);
|
|
12987
13298
|
const edits = message?.edit?.seq ?? 0;
|
|
12988
13299
|
if (edits === before) return {
|
|
12989
13300
|
group: group.id,
|
|
@@ -13149,12 +13460,32 @@ function groupSendRefused(error) {
|
|
|
13149
13460
|
return new CliError("unavailable", error);
|
|
13150
13461
|
}
|
|
13151
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
|
+
/**
|
|
13152
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
|
|
13153
13484
|
* to give, as `button update` and `task update` do; before, a mistyped or lost id read "Only your own text messages
|
|
13154
13485
|
* can be edited" (`refused`, exit 1), as if the message were someone else's.
|
|
13155
13486
|
*/
|
|
13156
|
-
function editRefused(
|
|
13157
|
-
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`);
|
|
13158
13489
|
return new CliError(refused ? "refused" : "unavailable", error);
|
|
13159
13490
|
}
|
|
13160
13491
|
/**
|
|
@@ -13209,12 +13540,12 @@ function history(messages, params, json = messageJson) {
|
|
|
13209
13540
|
};
|
|
13210
13541
|
}
|
|
13211
13542
|
Object.keys(METHODS);
|
|
13212
|
-
/** Runs one method of the API. */
|
|
13543
|
+
/** Runs one method of the API: one the table lists itself (`constructor`, which every object has, is none). */
|
|
13213
13544
|
async function callApi(ctx, method, params = {}) {
|
|
13214
|
-
const run = METHODS[method];
|
|
13545
|
+
const run = Object.hasOwn(METHODS, method) ? METHODS[method] : void 0;
|
|
13215
13546
|
if (!run) throw new CliError("not_found", `No method ${method}`);
|
|
13216
13547
|
if (params === null || typeof params !== "object" || Array.isArray(params)) throw new CliError("bad_request", "params must be an object");
|
|
13217
|
-
return run(ctx, params);
|
|
13548
|
+
return run(ctx, await withStoredIds(ctx, params));
|
|
13218
13549
|
}
|
|
13219
13550
|
//#endregion
|
|
13220
13551
|
//#region src/privateFolder.ts
|
|
@@ -14317,7 +14648,23 @@ var showSecret = { "show-secret": {
|
|
|
14317
14648
|
type: "boolean",
|
|
14318
14649
|
description: "Print the group's entry link (anyone who has it can join)"
|
|
14319
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
|
+
} };
|
|
14320
14656
|
var secret = (options) => options["show-secret"] === true ? { showSecret: true } : {};
|
|
14657
|
+
/**
|
|
14658
|
+
* `--for` of the typing commands: the seconds it is kept on, 1 to 600, and never with `--stop`. A usage error (exit 2)
|
|
14659
|
+
* that names the flag: before, the API's own check answered ("for must be a number from 1 to 600", exit 1).
|
|
14660
|
+
*/
|
|
14661
|
+
function typingFor(options) {
|
|
14662
|
+
const seconds = options.for;
|
|
14663
|
+
if (seconds === void 0) return void 0;
|
|
14664
|
+
if (typeof seconds !== "number" || !(seconds >= 1 && seconds <= 600)) throw new CliError("usage", `--for takes a number of seconds from 1 to 600, not ${JSON.stringify(seconds)}`);
|
|
14665
|
+
if (options.stop === true) throw new CliError("usage", "--for keeps it on: not with --stop");
|
|
14666
|
+
return seconds;
|
|
14667
|
+
}
|
|
14321
14668
|
/** A history cursor: a timestamp when it is all digits, else a message id. */
|
|
14322
14669
|
function cursor(value) {
|
|
14323
14670
|
if (value === void 0) return void 0;
|
|
@@ -14359,6 +14706,10 @@ var cardOptions = {
|
|
|
14359
14706
|
text: {
|
|
14360
14707
|
type: "string",
|
|
14361
14708
|
description: "The text older apps show instead (default: written from the card)"
|
|
14709
|
+
},
|
|
14710
|
+
force: {
|
|
14711
|
+
type: "boolean",
|
|
14712
|
+
description: "Send even if the card's text looks like a seed, a key or ecash"
|
|
14362
14713
|
}
|
|
14363
14714
|
};
|
|
14364
14715
|
/** `--link label=https://…` or a bare link, as a card's links. */
|
|
@@ -14392,7 +14743,26 @@ function itemOf(value) {
|
|
|
14392
14743
|
text: value.slice(colon + 1).trim()
|
|
14393
14744
|
};
|
|
14394
14745
|
}
|
|
14395
|
-
/**
|
|
14746
|
+
/** `--progress`, `--left`: a percent, or a usage error that names the flag. */
|
|
14747
|
+
function percent(options, flag) {
|
|
14748
|
+
const value = options[flag];
|
|
14749
|
+
if (value !== void 0 && !(typeof value === "number" && value >= 0 && value <= 100)) throw new CliError("usage", `--${flag} takes a percent from 0 to 100, not ${JSON.stringify(value)}`);
|
|
14750
|
+
}
|
|
14751
|
+
/** The flags of a task's pull request, and the field of `pr` each one gives. */
|
|
14752
|
+
var PR_FLAGS = [
|
|
14753
|
+
["pr-url", "url"],
|
|
14754
|
+
["pr-number", "number"],
|
|
14755
|
+
["additions", "additions"],
|
|
14756
|
+
["deletions", "deletions"],
|
|
14757
|
+
["files", "files"],
|
|
14758
|
+
["pr-state", "state"],
|
|
14759
|
+
["pr-checks", "checks"]
|
|
14760
|
+
];
|
|
14761
|
+
/**
|
|
14762
|
+
* A task's fields from its flags; what was not given is left out, so an update changes only what it names. What a flag
|
|
14763
|
+
* got wrong is a usage error (exit 2) that names the flag: before, it reached the card's own check and failed as
|
|
14764
|
+
* `bad_request` (exit 1) in the card's words ("Status card: done is at most total").
|
|
14765
|
+
*/
|
|
14396
14766
|
function taskFields(options) {
|
|
14397
14767
|
const fields = { ...cardJson(options.json) };
|
|
14398
14768
|
const set = (key, value) => {
|
|
@@ -14401,6 +14771,7 @@ function taskFields(options) {
|
|
|
14401
14771
|
set("id", options.id);
|
|
14402
14772
|
set("title", options.title);
|
|
14403
14773
|
set("status", options.status);
|
|
14774
|
+
percent(options, "progress");
|
|
14404
14775
|
set("progress", options.progress);
|
|
14405
14776
|
set("step", options.step);
|
|
14406
14777
|
set("branch", options.branch);
|
|
@@ -14409,22 +14780,18 @@ function taskFields(options) {
|
|
|
14409
14780
|
if (!match) throw new CliError("usage", `--steps takes done/total, like 2/5, not ${JSON.stringify(options.steps)}`);
|
|
14410
14781
|
fields.done = Number(match[1]);
|
|
14411
14782
|
fields.total = Number(match[2]);
|
|
14783
|
+
if (Number(match[2]) < 1 || Number(match[1]) > Number(match[2])) throw new CliError("usage", `--steps takes done/total with done at most total, like 2/5, not ${JSON.stringify(options.steps)}`);
|
|
14412
14784
|
}
|
|
14785
|
+
if (options["pr-url"] !== void 0 && !cardUrl(options["pr-url"])) throw new CliError("usage", `--pr-url takes an https link of at most ${STATUS_CARD_LIMITS.url} characters, not ${JSON.stringify(options["pr-url"])}`);
|
|
14786
|
+
for (const [flag, words] of [["pr-state", PR_STATES], ["pr-checks", PR_CHECKS]]) if (options[flag] !== void 0 && !words.includes(options[flag])) throw new CliError("usage", `--${flag} takes ${listed(words)}, not ${JSON.stringify(options[flag])}`);
|
|
14413
14787
|
const pr = {};
|
|
14414
|
-
for (const [flag, key] of [
|
|
14415
|
-
["pr-url", "url"],
|
|
14416
|
-
["pr-number", "number"],
|
|
14417
|
-
["additions", "additions"],
|
|
14418
|
-
["deletions", "deletions"],
|
|
14419
|
-
["files", "files"],
|
|
14420
|
-
["pr-state", "state"],
|
|
14421
|
-
["pr-checks", "checks"]
|
|
14422
|
-
]) if (options[flag] !== void 0) pr[key] = options[flag];
|
|
14788
|
+
for (const [flag, key] of PR_FLAGS) if (options[flag] !== void 0) pr[key] = options[flag];
|
|
14423
14789
|
if (Object.keys(pr).length) fields.pr = {
|
|
14424
14790
|
...fields.pr && typeof fields.pr === "object" ? fields.pr : {},
|
|
14425
14791
|
...pr
|
|
14426
14792
|
};
|
|
14427
14793
|
if (Array.isArray(options.item) && options.item.length) fields.items = options.item.map(itemOf);
|
|
14794
|
+
if (Array.isArray(options.tag) && options.tag.length > STATUS_CARD_LIMITS.tags) throw new CliError("usage", `--tag is given at most ${STATUS_CARD_LIMITS.tags} times, not ${options.tag.length}`);
|
|
14428
14795
|
if (Array.isArray(options.tag) && options.tag.length) fields.tags = options.tag;
|
|
14429
14796
|
set("parent", options.parent);
|
|
14430
14797
|
const links = cardLinks(options.link);
|
|
@@ -14441,6 +14808,25 @@ function needs(card, fields, usage) {
|
|
|
14441
14808
|
if (missing.length) throw new CliError("usage", `${missing.join(" and ")} ${missing.length > 1 ? "are" : "is"} needed: ghostly ${usage}`);
|
|
14442
14809
|
return card;
|
|
14443
14810
|
}
|
|
14811
|
+
/**
|
|
14812
|
+
* A new task's pull request goes by its link: `--pr-state` and the like without `--pr-url` (or `pr.url` in `--json`)
|
|
14813
|
+
* are a usage error that names them. An update's card may have the link already, so only a send asks.
|
|
14814
|
+
*/
|
|
14815
|
+
function prLinked(card, options, usage) {
|
|
14816
|
+
const given = PR_FLAGS.filter(([flag]) => options[flag] !== void 0).map(([flag]) => `--${flag}`);
|
|
14817
|
+
if (!given.length || card.pr.url !== void 0) return card;
|
|
14818
|
+
throw new CliError("usage", `${given.join(" and ")} ${given.length > 1 ? "go" : "goes"} with --pr-url: ghostly ${usage}`);
|
|
14819
|
+
}
|
|
14820
|
+
/**
|
|
14821
|
+
* A usage card says how much is left: `--left`, or `--used` with `--limit`, as flags or in `--json`. The one that is
|
|
14822
|
+
* missing is a usage error that names the flag.
|
|
14823
|
+
*/
|
|
14824
|
+
function quota(card, usage) {
|
|
14825
|
+
const used = card.used !== void 0;
|
|
14826
|
+
if (used !== (card.limit !== void 0)) throw new CliError("usage", `--${used ? "used" : "limit"} goes with --${used ? "limit" : "used"}: ghostly ${usage}`);
|
|
14827
|
+
if (card.left === void 0 && !used) throw new CliError("usage", `--left is needed (or --used with --limit): ghostly ${usage}`);
|
|
14828
|
+
return card;
|
|
14829
|
+
}
|
|
14444
14830
|
/** A time a command names: milliseconds, or a date the JavaScript Date reads (`2026-09-30T01:00:00Z`). */
|
|
14445
14831
|
function cardTime(value, flag) {
|
|
14446
14832
|
if (value === void 0) return void 0;
|
|
@@ -14589,6 +14975,7 @@ function usageWindowOf(value) {
|
|
|
14589
14975
|
/** A usage card's fields from its flags (WISP 405 § Usage); `--json` gives any of them, the flags win over it. */
|
|
14590
14976
|
function usageFields(options) {
|
|
14591
14977
|
const fields = { ...cardJson(options.json) };
|
|
14978
|
+
percent(options, "left");
|
|
14592
14979
|
for (const key of [
|
|
14593
14980
|
"id",
|
|
14594
14981
|
"left",
|
|
@@ -14646,7 +15033,8 @@ var usageOptions = {
|
|
|
14646
15033
|
description: "Every 1:1 chat with a contact, instead of one chat"
|
|
14647
15034
|
},
|
|
14648
15035
|
json: cardOptions.json,
|
|
14649
|
-
text: cardOptions.text
|
|
15036
|
+
text: cardOptions.text,
|
|
15037
|
+
force: cardOptions.force
|
|
14650
15038
|
};
|
|
14651
15039
|
//#endregion
|
|
14652
15040
|
//#region src/commands/calls.ts
|
|
@@ -14718,6 +15106,7 @@ var commands$7 = {
|
|
|
14718
15106
|
};
|
|
14719
15107
|
//#endregion
|
|
14720
15108
|
//#region src/commands/chats.ts
|
|
15109
|
+
var BUTTON_UPDATE = "button update <chat|group> <message> [--chosen <button>] [--close] [--text <text>] [--force] [--wait none|confirmed|sent] [--timeout s]";
|
|
14721
15110
|
/** Invites, chats and messages: one entry per command, in alphabetical order (test/commands.test.ts checks). */
|
|
14722
15111
|
var commands$6 = {
|
|
14723
15112
|
"button press": {
|
|
@@ -14746,7 +15135,7 @@ var commands$6 = {
|
|
|
14746
15135
|
},
|
|
14747
15136
|
"button update": {
|
|
14748
15137
|
method: "button.update",
|
|
14749
|
-
usage:
|
|
15138
|
+
usage: BUTTON_UPDATE,
|
|
14750
15139
|
summary: "Show the answer on a message's buttons (--chosen), close them, or change its text (the messageId send --button gave)",
|
|
14751
15140
|
args: ["chat", "message"],
|
|
14752
15141
|
options: {
|
|
@@ -14769,16 +15158,19 @@ var commands$6 = {
|
|
|
14769
15158
|
},
|
|
14770
15159
|
timeout: groupWait.timeout
|
|
14771
15160
|
},
|
|
14772
|
-
params: ({ options }, a) =>
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
15161
|
+
params: ({ options }, a) => {
|
|
15162
|
+
if (!options.chosen && options.close !== true && !options.text) throw new CliError("usage", `Give --chosen, --close or --text: ghostly ${BUTTON_UPDATE}`);
|
|
15163
|
+
return {
|
|
15164
|
+
chat: a.chat,
|
|
15165
|
+
message: a.message,
|
|
15166
|
+
chosen: options.chosen,
|
|
15167
|
+
close: options.close === true,
|
|
15168
|
+
text: options.text,
|
|
15169
|
+
force: options.force === true,
|
|
15170
|
+
wait: options.wait,
|
|
15171
|
+
timeout: options.timeout
|
|
15172
|
+
};
|
|
15173
|
+
}
|
|
14782
15174
|
},
|
|
14783
15175
|
"chat connect": {
|
|
14784
15176
|
method: "chat.connect",
|
|
@@ -14829,8 +15221,10 @@ var commands$6 = {
|
|
|
14829
15221
|
},
|
|
14830
15222
|
"chat list": {
|
|
14831
15223
|
method: "chat.list",
|
|
14832
|
-
usage: "chat list",
|
|
14833
|
-
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)
|
|
14834
15228
|
},
|
|
14835
15229
|
"chat remove": {
|
|
14836
15230
|
method: "chat.remove",
|
|
@@ -14858,10 +15252,14 @@ var commands$6 = {
|
|
|
14858
15252
|
},
|
|
14859
15253
|
"chat show": {
|
|
14860
15254
|
method: "chat.get",
|
|
14861
|
-
usage: "chat show <chat>",
|
|
14862
|
-
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)",
|
|
14863
15257
|
args: ["chat"],
|
|
14864
|
-
|
|
15258
|
+
options: showInvite,
|
|
15259
|
+
params: ({ options }, { chat }) => ({
|
|
15260
|
+
chat,
|
|
15261
|
+
...secret(options)
|
|
15262
|
+
})
|
|
14865
15263
|
},
|
|
14866
15264
|
"chat transport": {
|
|
14867
15265
|
method: "chat.transport",
|
|
@@ -14882,10 +15280,13 @@ var commands$6 = {
|
|
|
14882
15280
|
type: "string",
|
|
14883
15281
|
description: "The code the contact reads out"
|
|
14884
15282
|
} },
|
|
14885
|
-
params: ({ options }, { chat }) =>
|
|
14886
|
-
chat
|
|
14887
|
-
|
|
14888
|
-
|
|
15283
|
+
params: ({ options }, { chat }) => {
|
|
15284
|
+
if (typeof options.code !== "string" || !options.code.trim()) throw new CliError("usage", "--code is needed: ghostly chat verify <chat> --code <code>");
|
|
15285
|
+
return {
|
|
15286
|
+
chat,
|
|
15287
|
+
code: options.code
|
|
15288
|
+
};
|
|
15289
|
+
}
|
|
14889
15290
|
},
|
|
14890
15291
|
"chat wait": {
|
|
14891
15292
|
method: "chat.wait",
|
|
@@ -14996,18 +15397,22 @@ var commands$6 = {
|
|
|
14996
15397
|
},
|
|
14997
15398
|
"pin": {
|
|
14998
15399
|
method: "chat.pin",
|
|
14999
|
-
usage: "pin <chat|group> <message>
|
|
15400
|
+
usage: "pin <chat|group> <message> | --remove",
|
|
15000
15401
|
summary: "Pin a message at the top of a chat or group (one per chat: it replaces the pinned one); --remove unpins",
|
|
15001
|
-
args: ["chat", "message"],
|
|
15402
|
+
args: ["chat", "message...?"],
|
|
15002
15403
|
options: { remove: {
|
|
15003
15404
|
type: "boolean",
|
|
15004
|
-
description: "Unpin the chat's pinned message"
|
|
15405
|
+
description: "Unpin the chat's pinned message (it names no message)"
|
|
15005
15406
|
} },
|
|
15006
|
-
params: ({ options }, a) =>
|
|
15007
|
-
|
|
15008
|
-
|
|
15009
|
-
|
|
15010
|
-
|
|
15407
|
+
params: ({ options }, a) => {
|
|
15408
|
+
if (a.message === void 0 && options.remove !== true) throw new CliError("usage", "Missing <message>: ghostly pin <chat|group> <message> | --remove");
|
|
15409
|
+
if (a.message?.includes(" ")) throw new CliError("usage", "Too many arguments: ghostly pin <chat|group> <message> | --remove");
|
|
15410
|
+
return {
|
|
15411
|
+
chat: a.chat,
|
|
15412
|
+
message: a.message,
|
|
15413
|
+
remove: options.remove === true
|
|
15414
|
+
};
|
|
15415
|
+
}
|
|
15011
15416
|
},
|
|
15012
15417
|
"react": {
|
|
15013
15418
|
method: "chat.react",
|
|
@@ -15031,7 +15436,7 @@ var commands$6 = {
|
|
|
15031
15436
|
},
|
|
15032
15437
|
"routine send": {
|
|
15033
15438
|
method: "routine.send",
|
|
15034
|
-
usage: "routine send <chat|group> --name <name> --schedule \"every day 01:00\" [--cron \"0 1 * * *\"] [--state active|paused] [--next <time>] [--run ok|failed|skipped[:summary]] [--link url]... [--id id] [--json json|-|file] [--wait none|sent|delivered] [--timeout s]",
|
|
15439
|
+
usage: "routine send <chat|group> --name <name> --schedule \"every day 01:00\" [--cron \"0 1 * * *\"] [--state active|paused] [--next <time>] [--run ok|failed|skipped[:summary]] [--link url]... [--id id] [--json json|-|file] [--force] [--wait none|sent|delivered] [--timeout s]",
|
|
15035
15440
|
summary: "Send a routine card (something a bot runs on a schedule: its last run and the next) to a chat or group",
|
|
15036
15441
|
args: ["chat"],
|
|
15037
15442
|
options: {
|
|
@@ -15048,13 +15453,14 @@ var commands$6 = {
|
|
|
15048
15453
|
card: needs(routineFields(options), [["name", "name"], ["schedule", "schedule"]], "routine send <chat|group> --name <name> --schedule \"every day 01:00\""),
|
|
15049
15454
|
run: runOf(options.run),
|
|
15050
15455
|
text: options.text,
|
|
15456
|
+
force: options.force === true,
|
|
15051
15457
|
wait: options.wait,
|
|
15052
15458
|
timeout: options.timeout
|
|
15053
15459
|
})
|
|
15054
15460
|
},
|
|
15055
15461
|
"routine update": {
|
|
15056
15462
|
method: "routine.update",
|
|
15057
|
-
usage: "routine update <chat|group> <routine> [--run ok|failed|skipped[:summary]] [--next <time>] [--state active|paused] [--schedule ...] [--json json|-|file] [--wait none|confirmed|sent] [--timeout s]",
|
|
15463
|
+
usage: "routine update <chat|group> <routine> [--run ok|failed|skipped[:summary]] [--next <time>] [--state active|paused] [--schedule ...] [--json json|-|file] [--force] [--wait none|confirmed|sent] [--timeout s]",
|
|
15058
15464
|
summary: "Update a routine card you sent: record a run, the next one, a pause (at most one update per 2.5 s; sooner ones merge)",
|
|
15059
15465
|
args: ["chat", "routine"],
|
|
15060
15466
|
options: {
|
|
@@ -15071,13 +15477,14 @@ var commands$6 = {
|
|
|
15071
15477
|
card: routineFields(options),
|
|
15072
15478
|
run: runOf(options.run),
|
|
15073
15479
|
text: options.text,
|
|
15480
|
+
force: options.force === true,
|
|
15074
15481
|
wait: options.wait,
|
|
15075
15482
|
timeout: options.timeout
|
|
15076
15483
|
})
|
|
15077
15484
|
},
|
|
15078
15485
|
"task send": {
|
|
15079
15486
|
method: "task.send",
|
|
15080
|
-
usage: "task send <chat|group> --title <title> [--status s] [--progress n | --steps a/b] [--step \"...\"] [--item state:text]... [--pr-url url --pr-number n --additions n --deletions n --files n --pr-state s --pr-checks c] [--branch b] [--tag label]... [--parent task] [--link url]... [--id id] [--json json|-|file] [--wait none|sent|delivered] [--timeout s]",
|
|
15487
|
+
usage: "task send <chat|group> --title <title> [--status s] [--progress n | --steps a/b] [--step \"...\"] [--item state:text]... [--pr-url url --pr-number n --additions n --deletions n --files n --pr-state s --pr-checks c] [--branch b] [--tag label]... [--parent task] [--link url]... [--id id] [--json json|-|file] [--force] [--wait none|sent|delivered] [--timeout s]",
|
|
15081
15488
|
summary: "Send a task card (a bot's work: status, progress, its PR) to a chat or group; prints its id and message",
|
|
15082
15489
|
args: ["chat"],
|
|
15083
15490
|
options: {
|
|
@@ -15091,15 +15498,16 @@ var commands$6 = {
|
|
|
15091
15498
|
},
|
|
15092
15499
|
params: ({ options }, { chat }) => ({
|
|
15093
15500
|
chat,
|
|
15094
|
-
card: needs(taskFields(options), [["title", "title"]], "task send <chat|group> --title <title>"),
|
|
15501
|
+
card: prLinked(needs(taskFields(options), [["title", "title"]], "task send <chat|group> --title <title>"), options, "task send <chat|group> --title <title> --pr-url <url>"),
|
|
15095
15502
|
text: options.text,
|
|
15503
|
+
force: options.force === true,
|
|
15096
15504
|
wait: options.wait,
|
|
15097
15505
|
timeout: options.timeout
|
|
15098
15506
|
})
|
|
15099
15507
|
},
|
|
15100
15508
|
"task update": {
|
|
15101
15509
|
method: "task.update",
|
|
15102
|
-
usage: "task update <chat|group> <task> [--status s] [--progress n | --steps a/b] [--step \"...\"] [--item state:text]... [--pr-url ...] [--pr-state s] [--pr-checks c] [--tag label]... [--parent task] [--json json|-|file] [--wait none|confirmed|sent] [--timeout s]",
|
|
15510
|
+
usage: "task update <chat|group> <task> [--status s] [--progress n | --steps a/b] [--step \"...\"] [--item state:text]... [--pr-url ...] [--pr-state s] [--pr-checks c] [--tag label]... [--parent task] [--json json|-|file] [--force] [--wait none|confirmed|sent] [--timeout s]",
|
|
15103
15511
|
summary: "Update a task card you sent: the fields given, merged over it (at most one update per 2.5 s; sooner ones merge)",
|
|
15104
15512
|
args: ["chat", "task"],
|
|
15105
15513
|
options: {
|
|
@@ -15115,6 +15523,7 @@ var commands$6 = {
|
|
|
15115
15523
|
task: a.task,
|
|
15116
15524
|
card: taskFields(options),
|
|
15117
15525
|
text: options.text,
|
|
15526
|
+
force: options.force === true,
|
|
15118
15527
|
wait: options.wait,
|
|
15119
15528
|
timeout: options.timeout
|
|
15120
15529
|
})
|
|
@@ -15147,12 +15556,12 @@ var commands$6 = {
|
|
|
15147
15556
|
stop: options.stop === true,
|
|
15148
15557
|
kind: options.kind,
|
|
15149
15558
|
status: options.status,
|
|
15150
|
-
for: options
|
|
15559
|
+
for: typingFor(options)
|
|
15151
15560
|
})
|
|
15152
15561
|
},
|
|
15153
15562
|
"usage send": {
|
|
15154
15563
|
method: "usage.send",
|
|
15155
|
-
usage: "usage send [<chat|group>] [--all] --left <percent> [--window \"5 h\"] [--resets <time>] [--label Claude] [--account <name>] [--also week=80@<time>]... [--used n --limit n] [--id id] [--json json|-|file] [--wait none|sent] [--timeout s]",
|
|
15564
|
+
usage: "usage send [<chat|group>] [--all] --left <percent> [--window \"5 h\"] [--resets <time>] [--label Claude] [--account <name>] [--also week=80@<time>]... [--used n --limit n] [--id id] [--json json|-|file] [--force] [--wait none|sent] [--timeout s]",
|
|
15156
15565
|
summary: "Say how much of your quota is left (a bot's usage card, one per chat, updated in place); --all for every 1:1 chat",
|
|
15157
15566
|
args: ["chat?"],
|
|
15158
15567
|
options: {
|
|
@@ -15164,8 +15573,9 @@ var commands$6 = {
|
|
|
15164
15573
|
return {
|
|
15165
15574
|
chat,
|
|
15166
15575
|
all: options.all === true,
|
|
15167
|
-
card: usageFields(options),
|
|
15576
|
+
card: quota(usageFields(options), "usage send <chat|--all> --left <percent>"),
|
|
15168
15577
|
text: options.text,
|
|
15578
|
+
force: options.force === true,
|
|
15169
15579
|
wait: options.wait,
|
|
15170
15580
|
timeout: options.timeout
|
|
15171
15581
|
};
|
|
@@ -15662,7 +16072,7 @@ var commands$3 = {
|
|
|
15662
16072
|
stop: options.stop === true,
|
|
15663
16073
|
kind: options.kind,
|
|
15664
16074
|
status: options.status,
|
|
15665
|
-
for: options
|
|
16075
|
+
for: typingFor(options)
|
|
15666
16076
|
})
|
|
15667
16077
|
}
|
|
15668
16078
|
};
|
|
@@ -16455,6 +16865,12 @@ var PlaybackQueue = class {
|
|
|
16455
16865
|
this.length = 0;
|
|
16456
16866
|
return ms;
|
|
16457
16867
|
}
|
|
16868
|
+
/** Drops up to that much from the front, in whole frames (audio whose time went by); returns how much, in milliseconds. */
|
|
16869
|
+
skip(ms) {
|
|
16870
|
+
const frames = Math.min(Math.floor(ms / 20), Math.floor(this.length / this.frame));
|
|
16871
|
+
if (frames > 0) this.take(frames * this.frame);
|
|
16872
|
+
return Math.max(0, frames) * 20;
|
|
16873
|
+
}
|
|
16458
16874
|
take(bytes) {
|
|
16459
16875
|
const out = Buffer.allocUnsafe(bytes);
|
|
16460
16876
|
let filled = 0;
|
|
@@ -16682,6 +17098,10 @@ var ReorderBuffer = class {
|
|
|
16682
17098
|
}
|
|
16683
17099
|
}
|
|
16684
17100
|
};
|
|
17101
|
+
//#endregion
|
|
17102
|
+
//#region src/calls/audio.ts
|
|
17103
|
+
/** The longest stall of the sender whose frames are still sent after it (see `catchUp`). */
|
|
17104
|
+
var MAX_CATCH_UP_MS = 1e3;
|
|
16685
17105
|
/**
|
|
16686
17106
|
* One call's audio: every 20 ms a frame from the queue (the program's audio, or silence) is encoded and sent; what
|
|
16687
17107
|
* comes in is put back in order, decoded, and handed on in 20 ms frames as soon as it is. Nothing waits on a clock
|
|
@@ -16699,7 +17119,17 @@ var CallAudio = class {
|
|
|
16699
17119
|
timer = null;
|
|
16700
17120
|
startedAt = 0;
|
|
16701
17121
|
ticks = 0;
|
|
17122
|
+
/** What was left in the queue after the last frame sent: the program's audio written ahead of the call. */
|
|
17123
|
+
ahead = 0;
|
|
17124
|
+
/** After a stall that was not replayed: how much of its audio is still to drop, down to what queue, and until when. */
|
|
17125
|
+
owed = 0;
|
|
17126
|
+
kept = 0;
|
|
17127
|
+
owedUntil = 0;
|
|
16702
17128
|
talking = false;
|
|
17129
|
+
/** The last silent frame's packet, how many silent frames in a row gave it, and that packet once it is kept (see `settle`). */
|
|
17130
|
+
lastQuiet = null;
|
|
17131
|
+
same = 0;
|
|
17132
|
+
quiet = null;
|
|
16703
17133
|
stopped = false;
|
|
16704
17134
|
/** Frames sent and received, for `call list`. */
|
|
16705
17135
|
sent = 0;
|
|
@@ -16734,16 +17164,40 @@ var CallAudio = class {
|
|
|
16734
17164
|
tick() {
|
|
16735
17165
|
if (this.stopped) return;
|
|
16736
17166
|
const { frame, silent } = this.queue.next();
|
|
17167
|
+
const still = silent || frame.equals(this.silence);
|
|
16737
17168
|
let payload;
|
|
16738
|
-
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
17169
|
+
if (still && this.quiet) payload = this.quiet;
|
|
17170
|
+
else {
|
|
17171
|
+
try {
|
|
17172
|
+
payload = this.options.encoder.encode(frame, this.samples);
|
|
17173
|
+
} catch {
|
|
17174
|
+
return;
|
|
17175
|
+
}
|
|
17176
|
+
this.settle(still, payload);
|
|
16742
17177
|
}
|
|
16743
17178
|
const marker = !silent && !this.talking;
|
|
16744
17179
|
this.talking = !silent;
|
|
16745
17180
|
this.options.track.send(this.writer.packet(payload, marker));
|
|
16746
17181
|
this.sent++;
|
|
17182
|
+
this.ahead = this.queue.queuedMs;
|
|
17183
|
+
}
|
|
17184
|
+
/**
|
|
17185
|
+
* Keeps the packet silence encodes to once the encoder gave it SETTLED_FRAMES times in a row: it is sent again for
|
|
17186
|
+
* the silent frames that follow, without the encoder, until the program's audio is back.
|
|
17187
|
+
*/
|
|
17188
|
+
settle(still, payload) {
|
|
17189
|
+
if (!still) {
|
|
17190
|
+
this.lastQuiet = null;
|
|
17191
|
+
this.quiet = null;
|
|
17192
|
+
this.same = 0;
|
|
17193
|
+
return;
|
|
17194
|
+
}
|
|
17195
|
+
if (this.lastQuiet?.equals(payload)) {
|
|
17196
|
+
if (++this.same >= 25) this.quiet = this.lastQuiet;
|
|
17197
|
+
return;
|
|
17198
|
+
}
|
|
17199
|
+
this.lastQuiet = Buffer.from(payload);
|
|
17200
|
+
this.same = 1;
|
|
16747
17201
|
}
|
|
16748
17202
|
/** Handles one packet from the track. Exposed for tests. */
|
|
16749
17203
|
receive(packet) {
|
|
@@ -16778,14 +17232,57 @@ var CallAudio = class {
|
|
|
16778
17232
|
const due = this.startedAt + (this.ticks + 1) * 20;
|
|
16779
17233
|
this.timer = setTimeout(() => {
|
|
16780
17234
|
if (this.stopped) return;
|
|
16781
|
-
if (now() - due
|
|
16782
|
-
this.
|
|
16783
|
-
this.
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
17235
|
+
if (now() - due < 20) {
|
|
17236
|
+
this.ticks++;
|
|
17237
|
+
this.drop();
|
|
17238
|
+
this.tick();
|
|
17239
|
+
this.schedule();
|
|
17240
|
+
return;
|
|
17241
|
+
}
|
|
17242
|
+
setImmediate(() => {
|
|
17243
|
+
if (this.stopped) return;
|
|
17244
|
+
this.catchUp(now() - due);
|
|
17245
|
+
this.schedule();
|
|
17246
|
+
});
|
|
16787
17247
|
}, Math.max(0, due - now()));
|
|
16788
17248
|
}
|
|
17249
|
+
/**
|
|
17250
|
+
* Meets the clock again after the loop was busy: every frame that is due goes out at once, so a loop that turns
|
|
17251
|
+
* slower than a frame for a while (every 40 ms under a large file) keeps up, and a program writing at real time
|
|
17252
|
+
* stays there. The contact's jitter buffer takes the short burst. A stall past MAX_CATCH_UP_MS is not replayed: the
|
|
17253
|
+
* clock starts again and what the program wrote during it is dropped, or it would be heard that much late for the
|
|
17254
|
+
* rest of the call.
|
|
17255
|
+
*/
|
|
17256
|
+
catchUp(late) {
|
|
17257
|
+
const now = this.options.now ?? Date.now;
|
|
17258
|
+
if (late > 1e3) {
|
|
17259
|
+
this.owed = late;
|
|
17260
|
+
this.kept = this.ahead;
|
|
17261
|
+
this.owedUntil = now() + MAX_CATCH_UP_MS;
|
|
17262
|
+
this.startedAt = now();
|
|
17263
|
+
this.ticks = 0;
|
|
17264
|
+
this.drop();
|
|
17265
|
+
this.tick();
|
|
17266
|
+
return;
|
|
17267
|
+
}
|
|
17268
|
+
do {
|
|
17269
|
+
this.ticks++;
|
|
17270
|
+
this.drop();
|
|
17271
|
+
this.tick();
|
|
17272
|
+
} while (this.startedAt + (this.ticks + 1) * 20 <= now());
|
|
17273
|
+
}
|
|
17274
|
+
/**
|
|
17275
|
+
* Drops the audio of a stall that was not replayed, as it is read: a long one does not fit the program's socket, so
|
|
17276
|
+
* the rest of it comes over the next turns. What was queued before the stall (a clip written ahead) is kept.
|
|
17277
|
+
*/
|
|
17278
|
+
drop() {
|
|
17279
|
+
if (this.owed <= 0) return;
|
|
17280
|
+
if ((this.options.now ?? Date.now)() > this.owedUntil) {
|
|
17281
|
+
this.owed = 0;
|
|
17282
|
+
return;
|
|
17283
|
+
}
|
|
17284
|
+
this.owed -= this.queue.skip(Math.min(this.owed, this.queue.queuedMs - this.kept));
|
|
17285
|
+
}
|
|
16789
17286
|
};
|
|
16790
17287
|
//#endregion
|
|
16791
17288
|
//#region src/calls/media.ts
|
|
@@ -17258,6 +17755,8 @@ var CallManager = class {
|
|
|
17258
17755
|
this.callable(call.chat);
|
|
17259
17756
|
call.rate = rateOf(options.rate, call.rate);
|
|
17260
17757
|
const stack = await this.stack();
|
|
17758
|
+
if (call.ended) throw new CliError("unavailable", "The call ended before it was answered");
|
|
17759
|
+
if (call.state !== "ringing") throw new CliError("refused", "That call is not ringing here");
|
|
17261
17760
|
call.state = "connecting";
|
|
17262
17761
|
this.disarm(call);
|
|
17263
17762
|
try {
|
|
@@ -17364,10 +17863,18 @@ var CallManager = class {
|
|
|
17364
17863
|
this.calls.set(call.id, call);
|
|
17365
17864
|
return call;
|
|
17366
17865
|
}
|
|
17367
|
-
/**
|
|
17866
|
+
/**
|
|
17867
|
+
* The call's socket (open from now on: a program may connect before the media is up) and its media. A call that
|
|
17868
|
+
* ended while its socket opened closes it here: `end` found none to close, and nothing lists the call any more.
|
|
17869
|
+
*/
|
|
17368
17870
|
async attach(call, create) {
|
|
17369
17871
|
const queue = call.queue = new PlaybackQueue(call.rate);
|
|
17370
|
-
|
|
17872
|
+
const socket = await AudioSocket.open(audioSocketPath(this.host.profileDir, call.id), call.rate, { onAudio: (chunk) => queue.push(chunk) });
|
|
17873
|
+
if (call.ended) {
|
|
17874
|
+
await socket.close();
|
|
17875
|
+
return;
|
|
17876
|
+
}
|
|
17877
|
+
call.socket = socket;
|
|
17371
17878
|
await this.connect(call, create);
|
|
17372
17879
|
}
|
|
17373
17880
|
/**
|
|
@@ -17375,6 +17882,7 @@ var CallManager = class {
|
|
|
17375
17882
|
* the call ended, or asked for a newer connection, while this one was made: then it is closed.
|
|
17376
17883
|
*/
|
|
17377
17884
|
async connect(call, create) {
|
|
17885
|
+
if (call.ended) return false;
|
|
17378
17886
|
const attempt = ++call.attempt;
|
|
17379
17887
|
call.media?.close();
|
|
17380
17888
|
call.media = null;
|
|
@@ -19017,6 +19525,7 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19017
19525
|
const factory = new IDBFactory();
|
|
19018
19526
|
const raw = factory;
|
|
19019
19527
|
const classes = await probeClasses(factory);
|
|
19528
|
+
dropIndexEntriesByRecord(classes, (a, b) => factory.cmp(a, b));
|
|
19020
19529
|
let writing = true;
|
|
19021
19530
|
let restoring = true;
|
|
19022
19531
|
let journalFd = null;
|
|
@@ -19204,7 +19713,8 @@ async function probeClasses(factory) {
|
|
|
19204
19713
|
const classes = {
|
|
19205
19714
|
Database: rawDb.constructor,
|
|
19206
19715
|
ObjectStore: rawStore.constructor,
|
|
19207
|
-
Index: rawStore.rawIndexes.get("i").constructor
|
|
19716
|
+
Index: rawStore.rawIndexes.get("i").constructor,
|
|
19717
|
+
RecordStore: rawStore.records.constructor
|
|
19208
19718
|
};
|
|
19209
19719
|
db.close();
|
|
19210
19720
|
await new Promise((resolve) => {
|
|
@@ -19213,6 +19723,76 @@ async function probeClasses(factory) {
|
|
|
19213
19723
|
});
|
|
19214
19724
|
return classes;
|
|
19215
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
|
+
}
|
|
19216
19796
|
/**
|
|
19217
19797
|
* The snapshot's records come in key order (as `writeSnapshot` walks them), so each store's tree and each index's are
|
|
19218
19798
|
* built whole from sorted arrays: storing record by record compared every key down the tree, a 70,000-message profile
|
|
@@ -19608,14 +20188,14 @@ async function startRuntime(paths, options = {}) {
|
|
|
19608
20188
|
(await import("./assets/ghostly_iroh_web-Dvzh3Y6E.js")).initSync({ module: irohWasmBytes() });
|
|
19609
20189
|
const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([import("./assets/fileBytes-BcYZhNgF.js").then((n) => n.o), import("./assets/fileBytes-Dbifu5I5.js")]);
|
|
19610
20190
|
registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
|
|
19611
|
-
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")]);
|
|
19612
20192
|
const network = hyperdhtNetwork();
|
|
19613
20193
|
const dht = mainlineNetwork();
|
|
19614
|
-
const mainline = dht.off ? null : new (await (import("./assets/mainline-
|
|
20194
|
+
const mainline = dht.off ? null : new (await (import("./assets/mainline-1v8OTYvp.js"))).Mainline({
|
|
19615
20195
|
bootstrap: dht.bootstrap,
|
|
19616
20196
|
host: dht.host
|
|
19617
20197
|
});
|
|
19618
|
-
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;
|
|
19619
20199
|
const server = new EngineServer({
|
|
19620
20200
|
...transport ? { transport } : {},
|
|
19621
20201
|
irohWeb: true,
|
|
@@ -19926,19 +20506,21 @@ function toTurn(event) {
|
|
|
19926
20506
|
}
|
|
19927
20507
|
function turn(event, source, where, message) {
|
|
19928
20508
|
const file = message.file;
|
|
20509
|
+
const place = where.chat ?? where.group ?? "", messageId = turnRef(place, message.id);
|
|
19929
20510
|
return {
|
|
19930
20511
|
seq: event.seq,
|
|
19931
|
-
id: `agent.turn:${
|
|
20512
|
+
id: `agent.turn:${source}:${place}:${messageId}`,
|
|
19932
20513
|
type: "agent.turn",
|
|
19933
20514
|
at: event.at,
|
|
19934
20515
|
source,
|
|
19935
20516
|
...where,
|
|
19936
|
-
messageId
|
|
20517
|
+
messageId,
|
|
19937
20518
|
timestamp: message.timestamp,
|
|
19938
20519
|
...message.press ? { press: message.press } : {},
|
|
19939
20520
|
untrusted: {
|
|
19940
20521
|
text: message.text,
|
|
19941
20522
|
name: message.nick ?? null,
|
|
20523
|
+
messageId: message.id,
|
|
19942
20524
|
...message.replyTo ? { replyTo: {
|
|
19943
20525
|
id: message.replyTo.id,
|
|
19944
20526
|
snippet: message.replyTo.snippet
|
|
@@ -20127,7 +20709,14 @@ function requireProfile(g) {
|
|
|
20127
20709
|
createProfile(g.home, g.profile);
|
|
20128
20710
|
return;
|
|
20129
20711
|
}
|
|
20130
|
-
throw
|
|
20712
|
+
throw noProfile(g);
|
|
20713
|
+
}
|
|
20714
|
+
function noProfile(g) {
|
|
20715
|
+
return new CliError("not_found", `No profile ${g.profile}: make it with \`ghostly profile create ${g.profile}\``);
|
|
20716
|
+
}
|
|
20717
|
+
/** For a command that only reads: a mistyped profile is named as one, and `default` is not made by asking about it. */
|
|
20718
|
+
function knownProfile(g) {
|
|
20719
|
+
if (g.profile !== "default" && !profileExists(g.home, g.profile)) throw noProfile(g);
|
|
20131
20720
|
}
|
|
20132
20721
|
/** Said on stderr when the daemon runs another release than this command (it started before an upgrade). */
|
|
20133
20722
|
function versionWarning(daemon, cli) {
|
|
@@ -20263,7 +20852,7 @@ var SPECIAL = [
|
|
|
20263
20852
|
"The profile's settings",
|
|
20264
20853
|
{ "show-secret": o("boolean", "Show credentials too") }
|
|
20265
20854
|
],
|
|
20266
|
-
["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"],
|
|
20267
20856
|
[
|
|
20268
20857
|
"engine <method> [json-params | -] [--confirm-real] [--show-secret]",
|
|
20269
20858
|
"Any call of the app's engine, with its own parameters",
|
|
@@ -20309,6 +20898,14 @@ function help() {
|
|
|
20309
20898
|
"2 usage, 3 not found, 4 timed out, 5 needs --force/--yes/--confirm-real. See packages/cli/README.md."
|
|
20310
20899
|
].join("\n");
|
|
20311
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");
|
|
20312
20909
|
/** Every command's help row: usage, summary and, for the table and text commands, their options. */
|
|
20313
20910
|
function helpRows() {
|
|
20314
20911
|
return [
|
|
@@ -20335,6 +20932,7 @@ function commandWords(usage) {
|
|
|
20335
20932
|
* options; for a group (`ghostly help file`), the group's commands.
|
|
20336
20933
|
*/
|
|
20337
20934
|
function commandHelp(words) {
|
|
20935
|
+
if (words[0] === "help") return HELP_USAGE;
|
|
20338
20936
|
const named = (list) => helpRows().filter((row) => {
|
|
20339
20937
|
const own = commandWords(row.usage);
|
|
20340
20938
|
return list.every((word, i) => own[i] === word);
|
|
@@ -20591,6 +21189,7 @@ async function daemonCommand(argv) {
|
|
|
20591
21189
|
"restart"
|
|
20592
21190
|
].includes(sub)) noMoreThan(0, parsed.positionals, sub ? `daemon ${sub}` : "daemon [--detach]");
|
|
20593
21191
|
if (sub === "status") {
|
|
21192
|
+
knownProfile(g);
|
|
20594
21193
|
const client = await connectDaemon(g.paths.socket);
|
|
20595
21194
|
if (!client) {
|
|
20596
21195
|
print({
|
|
@@ -20612,32 +21211,43 @@ async function daemonCommand(argv) {
|
|
|
20612
21211
|
return;
|
|
20613
21212
|
}
|
|
20614
21213
|
if (sub === "stop") {
|
|
21214
|
+
knownProfile(g);
|
|
21215
|
+
const stopFor = seconds(parsed, 20);
|
|
20615
21216
|
const client = await connectDaemon(g.paths.socket);
|
|
20616
21217
|
if (!client) throw new CliError("not_found", `No daemon runs profile ${g.profile}`);
|
|
20617
21218
|
print({
|
|
20618
21219
|
stopped: true,
|
|
20619
|
-
pid: await stopDaemon(g, client,
|
|
21220
|
+
pid: await stopDaemon(g, client, stopFor)
|
|
20620
21221
|
});
|
|
20621
21222
|
return;
|
|
20622
21223
|
}
|
|
20623
21224
|
if (sub === "restart") {
|
|
21225
|
+
const stopFor = seconds(parsed, 20), startFor = seconds(parsed, 60);
|
|
20624
21226
|
const client = await connectDaemon(g.paths.socket);
|
|
20625
21227
|
const before = client ? (await client.call("status").catch(() => null))?.version ?? null : null;
|
|
20626
21228
|
print({
|
|
20627
21229
|
restarted: true,
|
|
20628
|
-
stopped: client ? await stopDaemon(g, client,
|
|
21230
|
+
stopped: client ? await stopDaemon(g, client, stopFor) : null,
|
|
20629
21231
|
before,
|
|
20630
21232
|
version: VERSION,
|
|
20631
|
-
...await startDetached(g,
|
|
21233
|
+
...await startDetached(g, startFor)
|
|
20632
21234
|
});
|
|
20633
21235
|
return;
|
|
20634
21236
|
}
|
|
20635
21237
|
if (sub) throw new CliError("usage", "ghostly daemon [--detach] | daemon status | daemon stop | daemon restart");
|
|
21238
|
+
const startFor = seconds(parsed, 60);
|
|
20636
21239
|
if (!parsed.options.detach) {
|
|
20637
21240
|
await runDaemon(g);
|
|
20638
21241
|
return;
|
|
20639
21242
|
}
|
|
20640
|
-
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;
|
|
20641
21251
|
}
|
|
20642
21252
|
/**
|
|
20643
21253
|
* Asks the daemon to stop and waits until its process is gone, not only until it let go of the profile (it releases
|
|
@@ -20697,6 +21307,10 @@ async function startDetached(g, seconds) {
|
|
|
20697
21307
|
}
|
|
20698
21308
|
if (!client) {
|
|
20699
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
|
+
});
|
|
20700
21314
|
throw new CliError("engine", `The daemon did not start; see ${g.paths.log}`);
|
|
20701
21315
|
}
|
|
20702
21316
|
client.close();
|
|
@@ -20884,17 +21498,7 @@ async function engineCommand(argv) {
|
|
|
20884
21498
|
params,
|
|
20885
21499
|
confirmReal
|
|
20886
21500
|
}));
|
|
20887
|
-
if (method === "getState" && !showSecret && result && typeof result === "object")
|
|
20888
|
-
const state = result;
|
|
20889
|
-
result = {
|
|
20890
|
-
...state,
|
|
20891
|
-
settings: redactSettings(state.settings),
|
|
20892
|
-
...state.groups ? { groups: state.groups.map((g) => g.entryLink ? {
|
|
20893
|
-
...g,
|
|
20894
|
-
entryLink: "<hidden>"
|
|
20895
|
-
} : g) } : {}
|
|
20896
|
-
};
|
|
20897
|
-
}
|
|
21501
|
+
if (method === "getState" && !showSecret && result && typeof result === "object") result = hideStateSecrets(result);
|
|
20898
21502
|
print(result);
|
|
20899
21503
|
}
|
|
20900
21504
|
async function settingsCommand(sub, argv) {
|
|
@@ -21127,12 +21731,14 @@ async function main(input) {
|
|
|
21127
21731
|
return 0;
|
|
21128
21732
|
}
|
|
21129
21733
|
if (argv[0] === "--version" || argv[0] === "version") {
|
|
21734
|
+
pretty = argv.slice(1).some((word) => word === "--pretty" || word === "--pretty=true");
|
|
21130
21735
|
print({ version: VERSION });
|
|
21131
21736
|
return 0;
|
|
21132
21737
|
}
|
|
21133
21738
|
const [first, second] = argv;
|
|
21134
21739
|
const two = second && !second.startsWith("-") ? `${first} ${second}` : "";
|
|
21135
|
-
|
|
21740
|
+
const text = (name) => Object.hasOwn(TEXT_COMMANDS, name), table = (name) => Object.hasOwn(COMMANDS, name);
|
|
21741
|
+
if (first === "profile" && !table(two)) {
|
|
21136
21742
|
await profileCommand(second, argv.slice(2));
|
|
21137
21743
|
return 0;
|
|
21138
21744
|
}
|
|
@@ -21160,19 +21766,19 @@ async function main(input) {
|
|
|
21160
21766
|
await callPipeCommand(argv.slice(2));
|
|
21161
21767
|
return -1;
|
|
21162
21768
|
}
|
|
21163
|
-
if (two &&
|
|
21769
|
+
if (two && text(two)) {
|
|
21164
21770
|
await textCommand(two, argv.slice(2));
|
|
21165
21771
|
return 0;
|
|
21166
21772
|
}
|
|
21167
|
-
if (two &&
|
|
21773
|
+
if (two && table(two)) {
|
|
21168
21774
|
await tableCommand(two, argv.slice(2));
|
|
21169
21775
|
return 0;
|
|
21170
21776
|
}
|
|
21171
|
-
if (
|
|
21777
|
+
if (text(first)) {
|
|
21172
21778
|
await textCommand(first, argv.slice(1));
|
|
21173
21779
|
return 0;
|
|
21174
21780
|
}
|
|
21175
|
-
if (
|
|
21781
|
+
if (table(first)) {
|
|
21176
21782
|
await tableCommand(first, argv.slice(1));
|
|
21177
21783
|
return 0;
|
|
21178
21784
|
}
|
|
@@ -21221,7 +21827,29 @@ async function run(argv) {
|
|
|
21221
21827
|
}
|
|
21222
21828
|
}
|
|
21223
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
|
|
21224
21851
|
//#region src/bin.ts
|
|
21852
|
+
quietWebStorage();
|
|
21225
21853
|
run(process.argv.slice(2));
|
|
21226
21854
|
//#endregion
|
|
21227
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 };
|