@ghostlytools/cli 1.2.4 → 1.3.1
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 +25 -9
- package/dist/assets/api-Dq2QXQDk.js +4172 -0
- package/dist/assets/api-Dq2QXQDk.js.map +1 -0
- package/dist/assets/api-TiB5QSG9.js +3 -0
- package/dist/assets/apiKit-CA2x5GJF.js +200 -0
- package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
- package/dist/assets/apps-BxV9i_Y4.js +786 -0
- package/dist/assets/apps-BxV9i_Y4.js.map +1 -0
- package/dist/assets/apps-C-T8jeji.js +2 -0
- package/dist/assets/{arkade-CdnKGDbg.js → arkade-C_qDmHnm.js} +2 -2
- package/dist/assets/{arkade-CdnKGDbg.js.map → arkade-C_qDmHnm.js.map} +1 -1
- package/dist/assets/backup-Bugnn3Ue.js +2 -0
- package/dist/assets/backup-CnMN2oUK.js +731 -0
- package/dist/assets/backup-CnMN2oUK.js.map +1 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoind-CZchoh43.js +349 -0
- package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
- package/dist/assets/buttons-CvNBzh2S.js +496 -0
- package/dist/assets/buttons-CvNBzh2S.js.map +1 -0
- package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-BOTFMpfS.js} +56 -18
- package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
- package/dist/assets/envelope-WjingYSc.js +90 -0
- package/dist/assets/envelope-WjingYSc.js.map +1 -0
- package/dist/assets/errors-DO5z6h_l.js +105 -0
- package/dist/assets/errors-DO5z6h_l.js.map +1 -0
- package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
- package/dist/assets/{fileBytes-Dbifu5I5.js → fileBytes-CfSXjTFa.js} +2 -2
- package/dist/assets/{fileBytes-Dbifu5I5.js.map → fileBytes-CfSXjTFa.js.map} +1 -1
- package/dist/assets/fileBytes-CgbibpbV.js +2 -0
- package/dist/assets/{fileBytes-BcYZhNgF.js → fileBytes-Di7VZtmU.js} +6 -19
- package/dist/assets/fileBytes-Di7VZtmU.js.map +1 -0
- package/dist/assets/fileBytesIdb-CFP-DSkZ.js +2 -0
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js → fileBytesIdb-D5u6H8X_.js} +4 -4
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js.map → fileBytesIdb-D5u6H8X_.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js → fileBytesOpfs-Dd1N4ERH.js} +3 -3
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js.map → fileBytesOpfs-Dd1N4ERH.js.map} +1 -1
- package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
- package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
- package/dist/assets/host-CWt-jtqz.js +4673 -0
- package/dist/assets/host-CWt-jtqz.js.map +1 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js +21 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-C1p0nkle.js → hyperdhtRelay-CPYwSLLS.js} +25 -10
- package/dist/assets/hyperdhtRelay-CPYwSLLS.js.map +1 -0
- package/dist/assets/{idb-CTF6LF5B.js → idb-B5L8wuOm.js} +7 -5
- package/dist/assets/idb-B5L8wuOm.js.map +1 -0
- package/dist/assets/irohWeb-4oChIOIp.js +544 -0
- package/dist/assets/irohWeb-4oChIOIp.js.map +1 -0
- package/dist/assets/{joinTrace-CO8mlhNO.js → joinTrace-CjRDFy85.js} +2 -7
- package/dist/assets/{joinTrace-CO8mlhNO.js.map → joinTrace-CjRDFy85.js.map} +1 -1
- package/dist/assets/joinTrace-D-fnt2p3.js +2 -0
- package/dist/assets/main-DbqcdShN.js +4409 -0
- package/dist/assets/main-DbqcdShN.js.map +1 -0
- package/dist/assets/{mainline-BH6s8HVi.js → mainline-ZkiOnevs.js} +11 -6
- package/dist/assets/mainline-ZkiOnevs.js.map +1 -0
- package/dist/assets/mints-BrlY773j.js +74 -0
- package/dist/assets/mints-BrlY773j.js.map +1 -0
- package/dist/assets/mints-CqTdA4Gk.js +2 -0
- package/dist/assets/networkReason-CetKkTPo.js +154 -0
- package/dist/assets/networkReason-CetKkTPo.js.map +1 -0
- package/dist/assets/nwc-CgkqyvsE.js +612 -0
- package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js +38 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js.map +1 -0
- package/dist/assets/profiles-BHc9pEYg.js +139 -0
- package/dist/assets/profiles-BHc9pEYg.js.map +1 -0
- package/dist/assets/registry-DlAKO0Fy.js +3793 -0
- package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
- package/dist/assets/registry-OBJBnHVQ.js +2 -0
- package/dist/assets/resolve-CouHhCpJ.js +591 -0
- package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
- package/dist/assets/{restoredRows-C_84q0Pd.js → restoredRows-Db3oUTko.js} +2 -2
- package/dist/assets/{restoredRows-C_84q0Pd.js.map → restoredRows-Db3oUTko.js.map} +1 -1
- package/dist/assets/{server-0zfNpABQ.js → server-Ss0gJ-nw.js} +1275 -1404
- package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
- package/dist/assets/{src-CbLkHfz8.js → src-DDzSERyc.js} +882 -134
- package/dist/assets/src-DDzSERyc.js.map +1 -0
- package/dist/assets/turnRefs-B87dtrHM.js +323 -0
- package/dist/assets/turnRefs-B87dtrHM.js.map +1 -0
- package/dist/assets/types-C4_KafL_.js +122 -0
- package/dist/assets/types-C4_KafL_.js.map +1 -0
- package/dist/assets/{usdt-Qf4UuygS.js → usdt-Fj8CQO2z.js} +3 -3
- package/dist/assets/{usdt-Qf4UuygS.js.map → usdt-Fj8CQO2z.js.map} +1 -1
- package/dist/ghostly.mjs +4 -22077
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/endpoint-pgFkR2AS.js.map +0 -1
- package/dist/assets/fileBytes-BcYZhNgF.js.map +0 -1
- package/dist/assets/hyperdhtRelay-C1p0nkle.js.map +0 -1
- package/dist/assets/idb-CTF6LF5B.js.map +0 -1
- package/dist/assets/mainline-BH6s8HVi.js.map +0 -1
- package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
- package/dist/assets/server-0zfNpABQ.js.map +0 -1
- package/dist/assets/src-CbLkHfz8.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitcoindProvider-DGw3xBSa.js","names":[],"sources":["../../../browser/src/engine/paymentAdapters/providers/bitcoindProvider.ts"],"sourcesContent":["import type { OnchainProviderDescriptor } from \"./onchain\";\nimport type { ProviderSettings } from \"./types\";\n\n/**\n * Bitcoin Core as the source picker lists it, and its settings. The client (`./bitcoind`) and btc-signer's\n * transaction code load when a wallet connects, not with the app.\n */\n\nconst WALLET_NAME = /^[A-Za-z0-9._-]{0,64}$/;\n\n/** `user:password` in the password field (the .cookie file's contents) when no user is given. */\nexport function credentials({ config, secrets }: ProviderSettings) {\n const password = secrets.password ?? \"\";\n const user = config.user?.trim() ?? \"\";\n if (!user && password.includes(\":\")) {\n const at = password.indexOf(\":\");\n return { user: password.slice(0, at), password: password.slice(at + 1) };\n }\n return { user, password };\n}\n\nexport const bitcoindRpc: OnchainProviderDescriptor = {\n id: \"bitcoind\",\n label: \"Bitcoin Core\",\n kind: \"onchain\",\n description: \"A wallet on your own Bitcoin Core node, over its RPC. Your node holds the keys and signs. Needs the desktop app.\",\n networks: [\"bitcoin\", \"testnet\", \"signet\", \"regtest\"],\n platforms: [\"desktop\"],\n experimental: true,\n fields: [\n { name: \"url\", label: \"RPC address\", kind: \"url\", placeholder: \"http://127.0.0.1:8332\", defaults: { mainnet: \"http://127.0.0.1:8332\", testnet: \"http://127.0.0.1:38332\" },\n help: \"Your node's rpcbind and rpcport: 8332 mainnet, 38332 signet, 48332 testnet4, 18443 regtest.\" },\n { name: \"wallet\", label: \"Wallet name\", kind: \"text\", optional: true, placeholder: \"ghostly\", help: \"A wallet loaded in the node (bitcoin-cli listwallets). Empty: its default wallet.\" },\n { name: \"user\", label: \"RPC user\", kind: \"text\", optional: true, help: \"Leave empty to paste the .cookie file below.\" },\n { name: \"password\", label: \"RPC password or cookie\", kind: \"secret\", help: \"rpcpassword, or the contents of the node's .cookie file (it changes when bitcoind restarts).\" },\n ],\n validate(settings) {\n const url = (() => { try { return new URL(settings.config.url?.trim() ?? \"\"); } catch { return undefined; } })();\n if (!url || ![\"http:\", \"https:\"].includes(url.protocol)) throw new Error(\"Enter the node's RPC address, like http://127.0.0.1:8332\");\n if (url.username || url.password) throw new Error(\"Put the RPC user and password in their own fields, not in the address\");\n if (url.search || url.hash) throw new Error(\"The RPC address cannot have a query or fragment\");\n if (!WALLET_NAME.test(settings.config.wallet?.trim() ?? \"\") || [\".\", \"..\"].includes(settings.config.wallet?.trim() ?? \"\")) throw new Error(\"A wallet name has only letters, digits, '-', '_' and '.'\");\n const { user, password } = credentials(settings);\n if (!user || !password) throw new Error(\"Enter the RPC user and password, or paste the .cookie file\");\n },\n async create(settings, host) {\n const { BitcoindOnchain, tauriTransport } = await import(\"./bitcoind\");\n return BitcoindOnchain.connect({ url: settings.config.url.trim(), wallet: settings.config.wallet?.trim() ?? \"\", ...credentials(settings) }, host.mode, tauriTransport(host.invoke), host.signal);\n },\n};\n"],"mappings":";;;;;AAQA,IAAM,cAAc;;AAGpB,SAAgB,YAAY,EAAE,QAAQ,WAA6B;CACjE,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,OAAO,OAAO,MAAM,KAAK,KAAK;CACpC,IAAI,CAAC,QAAQ,SAAS,SAAS,GAAG,GAAG;EACnC,MAAM,KAAK,SAAS,QAAQ,GAAG;EAC/B,OAAO;GAAE,MAAM,SAAS,MAAM,GAAG,EAAE;GAAG,UAAU,SAAS,MAAM,KAAK,CAAC;EAAE;CACzE;CACA,OAAO;EAAE;EAAM;CAAS;AAC1B;AAEA,IAAa,cAAyC;CACpD,IAAI;CACJ,OAAO;CACP,MAAM;CACN,aAAa;CACb,UAAU;EAAC;EAAW;EAAW;EAAU;CAAS;CACpD,WAAW,CAAC,SAAS;CACrB,cAAc;CACd,QAAQ;EACN;GAAE,MAAM;GAAO,OAAO;GAAe,MAAM;GAAO,aAAa;GAAyB,UAAU;IAAE,SAAS;IAAyB,SAAS;GAAyB;GACtK,MAAM;EAA8F;EACtG;GAAE,MAAM;GAAU,OAAO;GAAe,MAAM;GAAQ,UAAU;GAAM,aAAa;GAAW,MAAM;EAAoF;EACxL;GAAE,MAAM;GAAQ,OAAO;GAAY,MAAM;GAAQ,UAAU;GAAM,MAAM;EAA+C;EACtH;GAAE,MAAM;GAAY,OAAO;GAA0B,MAAM;GAAU,MAAM;EAA+F;CAC5K;CACA,SAAS,UAAU;EACjB,MAAM,aAAa;GAAE,IAAI;IAAE,OAAO,IAAI,IAAI,SAAS,OAAO,KAAK,KAAK,KAAK,EAAE;GAAG,QAAQ;IAAE;GAAkB;EAAE,EAAA,CAAG;EAC/G,IAAI,CAAC,OAAO,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,SAAS,IAAI,QAAQ,GAAG,MAAM,IAAI,MAAM,0DAA0D;EACnI,IAAI,IAAI,YAAY,IAAI,UAAU,MAAM,IAAI,MAAM,uEAAuE;EACzH,IAAI,IAAI,UAAU,IAAI,MAAM,MAAM,IAAI,MAAM,iDAAiD;EAC7F,IAAI,CAAC,YAAY,KAAK,SAAS,OAAO,QAAQ,KAAK,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,SAAS,OAAO,QAAQ,KAAK,KAAK,EAAE,GAAG,MAAM,IAAI,MAAM,0DAA0D;EACrM,MAAM,EAAE,MAAM,aAAa,YAAY,QAAQ;EAC/C,IAAI,CAAC,QAAQ,CAAC,UAAU,MAAM,IAAI,MAAM,4DAA4D;CACtG;CACA,MAAM,OAAO,UAAU,MAAM;EAC3B,MAAM,EAAE,iBAAiB,mBAAmB,MAAM,OAAO;EACzD,OAAO,gBAAgB,QAAQ;GAAE,KAAK,SAAS,OAAO,IAAI,KAAK;GAAG,QAAQ,SAAS,OAAO,QAAQ,KAAK,KAAK;GAAI,GAAG,YAAY,QAAQ;EAAE,GAAG,KAAK,MAAM,eAAe,KAAK,MAAM,GAAG,KAAK,MAAM;CACjM;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bitcoindevkit_bg-B-iLWrmX.js","names":["import.meta.
|
|
1
|
+
{"version":3,"file":"bitcoindevkit_bg-B-iLWrmX.js","names":["import.meta.ROLLDOWN_FILE_URL_FKNdLzKnlftW3t_gzo81yw"],"sources":["../../../../node_modules/@bitcoindevkit/bdk-wallet-web/bitcoindevkit_bg.wasm?url"],"sourcesContent":["export default import.meta.ROLLDOWN_FILE_URL_FKNdLzKnlftW3t_gzo81yw"],"ignoreList":[0],"mappings":";AAAA,IAAA,2BAAe,IAAA,IAAA,oCAAA,YAAA,GAAmD,CAAA,CAAnDA"}
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { Ya as STATUS_CARD_LIMITS, Za as buttonLabelClash, _l as randomBytes, eo as checkStatusCard, ui as BUTTONS_CAPABILITY, vl as toBase64Url } from "./src-DDzSERyc.js";
|
|
2
|
+
import { t as CliError } from "./errors-DO5z6h_l.js";
|
|
3
|
+
import { _ as str, d as messageOf, f as node, i as chatOrGroup, m as oneOf, n as bool, p as num } from "./apiKit-CA2x5GJF.js";
|
|
4
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
5
|
+
import { createBase58check } from "@scure/base";
|
|
6
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
7
|
+
//#region ../../apps/ui/src/lib/parse/secrets.ts
|
|
8
|
+
var WORD_INDEX = new Map(wordlist.map((word, index) => [word, index]));
|
|
9
|
+
var MNEMONIC_LENGTHS = [
|
|
10
|
+
12,
|
|
11
|
+
15,
|
|
12
|
+
18,
|
|
13
|
+
21,
|
|
14
|
+
24
|
|
15
|
+
];
|
|
16
|
+
/** Checksums tried per message at most: a 16 KiB run of seed words stays a few milliseconds. */
|
|
17
|
+
var CHECKSUM_BUDGET = 4096;
|
|
18
|
+
var NSEC = /\bnsec1[02-9ac-hj-np-z]{58}(?![02-9ac-hj-np-z])/i;
|
|
19
|
+
var EXTENDED_PRIVATE = /\b[xtyzuvYZUV]prv[1-9A-HJ-NP-Za-km-z]{100,}/;
|
|
20
|
+
var WIF = /\b[59KLc][1-9A-HJ-NP-Za-km-z]{50,51}\b/g;
|
|
21
|
+
var HEX_SECRET = /\b(?:0x)?(?:[0-9a-fA-F]{128}|[0-9a-fA-F]{64})\b/g;
|
|
22
|
+
var SECRET_CONTEXT = /priv(?:ate)?[\s_-]*key|secret|seed|mnemonic|nsec|xprv|\bwif\b|recovery|passphrase/i;
|
|
23
|
+
/** How far from a 64-hex string a word like "seed" still counts as saying what it is. */
|
|
24
|
+
var CONTEXT_REACH = 48;
|
|
25
|
+
var CASHU_TOKEN = /\bcashu([AB])([A-Za-z0-9_\-+/]{20,}={0,2})/g;
|
|
26
|
+
/**
|
|
27
|
+
* Characters that take no room on screen: format characters (zero-width space, soft hyphen, word joiner, the joiners,
|
|
28
|
+
* direction marks), control characters that are not white space, variation selectors, the grapheme joiner and the
|
|
29
|
+
* Hangul fillers. A page, a PDF or a terminal puts some of them into long strings as places to break a line.
|
|
30
|
+
*/
|
|
31
|
+
var INVISIBLE = /(?![\t\n\v\f\r\x85])[\p{Cc}\p{Cf}]|\u034f|[\u115f\u1160\u3164\uffa0]|[\ufe00-\ufe0f]|[\u{e0100}-\u{e01ef}]/gu;
|
|
32
|
+
var base58check = createBase58check(sha256);
|
|
33
|
+
/**
|
|
34
|
+
* The first secret in `text`, keys and seeds before ecash; null for anything else. The text is read as it is and as
|
|
35
|
+
* it reads without its invisible characters: a key with one inside is whole again once the reader takes it out.
|
|
36
|
+
*/
|
|
37
|
+
function findSecret(text) {
|
|
38
|
+
const visible = text.replace(INVISIBLE, "");
|
|
39
|
+
const readings = visible === text ? [text] : [text, visible];
|
|
40
|
+
if (readings.some(hasMnemonic)) return { kind: "mnemonic" };
|
|
41
|
+
if (readings.some((reading) => NSEC.test(reading))) return { kind: "nsec" };
|
|
42
|
+
if (readings.some((reading) => EXTENDED_PRIVATE.test(reading) || hasWif(reading))) return { kind: "bitcoin-key" };
|
|
43
|
+
if (readings.some(hasHexSecret)) return { kind: "hex-key" };
|
|
44
|
+
const tokens = readings.map(findCashu);
|
|
45
|
+
return tokens.find((found) => found?.kind === "cashu" && found.amount !== null) ?? tokens.find((found) => found) ?? null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 12 to 24 words in a row from the English BIP39 list whose checksum holds, anywhere in the text; or a message that
|
|
49
|
+
* is nothing but 12, 15, 18, 21 or 24 of those words (a seed with one word mistyped is still most of a seed). Prose
|
|
50
|
+
* breaks a run at once: "the", "a", "is", "to", "and", "of" and "I" are not on the list.
|
|
51
|
+
*
|
|
52
|
+
* One word said over and over is not a seed, though one in 16 such runs passes a 12-word checksum ("word" does): a
|
|
53
|
+
* window of a single word never counts, and a window mostly of one word ("abandon … about") counts only when that
|
|
54
|
+
* word does not go on past it on either side.
|
|
55
|
+
*/
|
|
56
|
+
function hasMnemonic(text) {
|
|
57
|
+
const tokens = text.toLowerCase().match(/[a-z]+/g);
|
|
58
|
+
if (!tokens || tokens.length < 12) return false;
|
|
59
|
+
let budget = CHECKSUM_BUDGET;
|
|
60
|
+
let run = [];
|
|
61
|
+
const seedLike = (start, length) => {
|
|
62
|
+
const window = run.slice(start, start + length);
|
|
63
|
+
const counts = /* @__PURE__ */ new Map();
|
|
64
|
+
for (const word of window) counts.set(word, (counts.get(word) ?? 0) + 1);
|
|
65
|
+
if (counts.size === 1) return false;
|
|
66
|
+
const [most, times] = [...counts].reduce((a, b) => b[1] > a[1] ? b : a);
|
|
67
|
+
if (times > length / 2 && (run[start - 1] === most || run[start + length] === most)) return false;
|
|
68
|
+
return checksumHolds(window);
|
|
69
|
+
};
|
|
70
|
+
const check = () => {
|
|
71
|
+
if (run.length >= 12) {
|
|
72
|
+
for (const length of MNEMONIC_LENGTHS) for (let start = 0; start + length <= run.length && budget > 0; start++, budget--) if (seedLike(start, length)) return true;
|
|
73
|
+
if (run.length === tokens.length && MNEMONIC_LENGTHS.includes(run.length) && new Set(run).size > 1) return true;
|
|
74
|
+
}
|
|
75
|
+
run = [];
|
|
76
|
+
return false;
|
|
77
|
+
};
|
|
78
|
+
for (const token of tokens) {
|
|
79
|
+
const index = WORD_INDEX.get(token);
|
|
80
|
+
if (index !== void 0) run.push(index);
|
|
81
|
+
else if (check()) return true;
|
|
82
|
+
}
|
|
83
|
+
return check();
|
|
84
|
+
}
|
|
85
|
+
/** BIP39: the words' 11-bit indices are the entropy followed by the first bits of its SHA-256. */
|
|
86
|
+
function checksumHolds(indices) {
|
|
87
|
+
const bits = indices.length * 11, checksumBits = bits / 33, entropyBytes = (bits - checksumBits) / 8;
|
|
88
|
+
const bytes = new Uint8Array(Math.ceil(bits / 8));
|
|
89
|
+
indices.forEach((index, i) => {
|
|
90
|
+
for (let b = 0; b < 11; b++) if (index & 1 << 10 - b) {
|
|
91
|
+
const at = i * 11 + b;
|
|
92
|
+
bytes[at >> 3] |= 128 >> (at & 7);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return sha256(bytes.subarray(0, entropyBytes))[0] >> 8 - checksumBits === bytes[entropyBytes] >> 8 - checksumBits;
|
|
96
|
+
}
|
|
97
|
+
/** A WIF private key: base58check with version 0x80 (mainnet) or 0xef (testnet), 32 bytes and an optional 0x01. */
|
|
98
|
+
function hasWif(text) {
|
|
99
|
+
for (const [candidate] of text.matchAll(WIF)) try {
|
|
100
|
+
const payload = base58check.decode(candidate);
|
|
101
|
+
if ((payload[0] === 128 || payload[0] === 239) && (payload.length === 33 || payload.length === 34 && payload[33] === 1)) return true;
|
|
102
|
+
} catch {}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
/** 64 or 128 hex digits with "private key", "secret", "seed" or the like close by. A bare hash is not asked about. */
|
|
106
|
+
function hasHexSecret(text) {
|
|
107
|
+
for (const match of text.matchAll(HEX_SECRET)) {
|
|
108
|
+
const before = text.slice(Math.max(0, match.index - CONTEXT_REACH), match.index);
|
|
109
|
+
const after = text.slice(match.index + match[0].length, match.index + match[0].length + CONTEXT_REACH);
|
|
110
|
+
if (SECRET_CONTEXT.test(before) || SECRET_CONTEXT.test(after)) return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
/** Every Cashu token in the text, with their amounts added up when all of them decode and share one unit. */
|
|
115
|
+
function findCashu(text) {
|
|
116
|
+
const tokens = [...text.matchAll(CASHU_TOKEN)].map(([, version, body]) => decodeCashu(version, body));
|
|
117
|
+
if (!tokens.length) return null;
|
|
118
|
+
const unit = tokens.find((token) => token)?.unit ?? "sat";
|
|
119
|
+
return {
|
|
120
|
+
kind: "cashu",
|
|
121
|
+
amount: tokens.every((token) => token && token.unit === unit) ? tokens.reduce((sum, token) => sum + token.amount, 0) : null,
|
|
122
|
+
unit
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** What a Cashu token is worth, read from the token itself (NUT-00): nothing is asked of the mint. */
|
|
126
|
+
function decodeCashu(version, body) {
|
|
127
|
+
try {
|
|
128
|
+
const bytes = fromBase64(body);
|
|
129
|
+
if (version === "A") {
|
|
130
|
+
const token = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
131
|
+
return total(token.token?.flatMap((entry) => entry.proofs ?? []).map((proof) => proof.amount), token.unit);
|
|
132
|
+
}
|
|
133
|
+
const token = decodeCbor(bytes);
|
|
134
|
+
return total(token?.t?.flatMap((entry) => entry.p ?? []).map((proof) => proof.a), token?.u);
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function total(amounts, unit) {
|
|
140
|
+
if (!amounts?.length || !amounts.every((a) => Number.isSafeInteger(a) && a >= 0)) return null;
|
|
141
|
+
const amount = amounts.reduce((sum, a) => sum + a, 0);
|
|
142
|
+
return Number.isSafeInteger(amount) ? {
|
|
143
|
+
amount,
|
|
144
|
+
unit: typeof unit === "string" && unit ? unit : "sat"
|
|
145
|
+
} : null;
|
|
146
|
+
}
|
|
147
|
+
function fromBase64(text) {
|
|
148
|
+
const standard = text.replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
|
|
149
|
+
const binary = atob(standard + "=".repeat((4 - standard.length % 4) % 4));
|
|
150
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
151
|
+
}
|
|
152
|
+
/** Enough CBOR (RFC 8949) for a cashuB token: definite lengths, integers, bytes, text, arrays, maps, tags, simple values. */
|
|
153
|
+
function decodeCbor(bytes) {
|
|
154
|
+
let at = 0, depth = 0;
|
|
155
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
156
|
+
const length = (info) => {
|
|
157
|
+
if (info < 24) return info;
|
|
158
|
+
const size = info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : info === 27 ? 8 : 0;
|
|
159
|
+
if (!size || at + size > bytes.length) throw new Error("cbor");
|
|
160
|
+
let value = 0;
|
|
161
|
+
for (let i = 0; i < size; i++) value = value * 256 + bytes[at + i];
|
|
162
|
+
at += size;
|
|
163
|
+
return value;
|
|
164
|
+
};
|
|
165
|
+
const item = () => {
|
|
166
|
+
if (at >= bytes.length || ++depth > 32) throw new Error("cbor");
|
|
167
|
+
const head = bytes[at++], major = head >> 5, info = head & 31;
|
|
168
|
+
try {
|
|
169
|
+
if (major === 7) {
|
|
170
|
+
if (info === 20) return false;
|
|
171
|
+
if (info === 21) return true;
|
|
172
|
+
if (info === 22 || info === 23) return null;
|
|
173
|
+
if (info === 25) {
|
|
174
|
+
at += 2;
|
|
175
|
+
return NaN;
|
|
176
|
+
}
|
|
177
|
+
if (info === 26) {
|
|
178
|
+
at += 4;
|
|
179
|
+
return view.getFloat32(at - 4);
|
|
180
|
+
}
|
|
181
|
+
if (info === 27) {
|
|
182
|
+
at += 8;
|
|
183
|
+
return view.getFloat64(at - 8);
|
|
184
|
+
}
|
|
185
|
+
throw new Error("cbor");
|
|
186
|
+
}
|
|
187
|
+
const n = length(info);
|
|
188
|
+
switch (major) {
|
|
189
|
+
case 0: return n;
|
|
190
|
+
case 1: return -1 - n;
|
|
191
|
+
case 2:
|
|
192
|
+
case 3: {
|
|
193
|
+
if (at + n > bytes.length) throw new Error("cbor");
|
|
194
|
+
const slice = bytes.subarray(at, at += n);
|
|
195
|
+
return major === 2 ? slice : new TextDecoder("utf-8", { fatal: true }).decode(slice);
|
|
196
|
+
}
|
|
197
|
+
case 4:
|
|
198
|
+
if (n > bytes.length - at) throw new Error("cbor");
|
|
199
|
+
return Array.from({ length: n }, item);
|
|
200
|
+
case 5: {
|
|
201
|
+
if (n > bytes.length - at) throw new Error("cbor");
|
|
202
|
+
const map = Object.create(null);
|
|
203
|
+
for (let i = 0; i < n; i++) {
|
|
204
|
+
const key = item();
|
|
205
|
+
map[String(key)] = item();
|
|
206
|
+
}
|
|
207
|
+
return map;
|
|
208
|
+
}
|
|
209
|
+
default: return item();
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
depth--;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return item();
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/waits.ts
|
|
219
|
+
var DELIVERY_RANK = {
|
|
220
|
+
sending: 0,
|
|
221
|
+
waiting: 1,
|
|
222
|
+
queued: 1,
|
|
223
|
+
held: 2,
|
|
224
|
+
sent: 2,
|
|
225
|
+
delivered: 3
|
|
226
|
+
};
|
|
227
|
+
/** Waits until the contact confirmed the latest edit of one of my messages (it is no longer pending). */
|
|
228
|
+
async function waitForEdit(ctx, chat, messageId, ms) {
|
|
229
|
+
const until = Date.now() + ms;
|
|
230
|
+
for (;;) {
|
|
231
|
+
const message = await node(ctx).getMessage(chat, messageId);
|
|
232
|
+
if (!message) throw new CliError("not_found", `No message ${messageId} in this chat`);
|
|
233
|
+
if (!message.edit?.pending) return message;
|
|
234
|
+
if (Date.now() >= until) throw new CliError("timeout", `Timed out after ${Math.round(ms / 1e3)} s: the contact has not confirmed the edit yet. It goes by itself once the chat is live and the contact's app shows edits, while this profile is online`, {
|
|
235
|
+
messageId,
|
|
236
|
+
edits: message.edit.seq
|
|
237
|
+
});
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Waits until an edge took my group message, or its edit number `edit` (WISP 902: a group has no receipts, so this is
|
|
243
|
+
* as far as the author sees): a member's edge in a private group, an edge to one of my hubs in a community.
|
|
244
|
+
*/
|
|
245
|
+
async function waitForGroupFrame(ctx, groupId, messageId, edit, ms) {
|
|
246
|
+
const until = Date.now() + ms;
|
|
247
|
+
for (;;) {
|
|
248
|
+
const taken = node(ctx).groupTaken({
|
|
249
|
+
groupId,
|
|
250
|
+
messageId,
|
|
251
|
+
...edit ? { edit } : {}
|
|
252
|
+
});
|
|
253
|
+
if (taken > 0) return taken;
|
|
254
|
+
if (Date.now() >= until) throw new CliError("timeout", `Timed out after ${Math.round(ms / 1e3)} s: no member's edge took it yet. It stays in the group and goes when one opens, while this profile is online`, {
|
|
255
|
+
messageId,
|
|
256
|
+
...edit ? { edits: edit } : {}
|
|
257
|
+
});
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Waits until a message of mine reached `target` (`sent`: on its way to the contact; `delivered`: acknowledged). */
|
|
262
|
+
async function waitForMessage(ctx, chat, messageId, target, ms) {
|
|
263
|
+
const want = DELIVERY_RANK[target];
|
|
264
|
+
const look = (message) => {
|
|
265
|
+
if (!message) return void 0;
|
|
266
|
+
const delivery = message.delivery ?? "sent";
|
|
267
|
+
if (delivery === "failed") throw new CliError("engine", message.deliveryError ?? "The message could not be sent", {
|
|
268
|
+
messageId,
|
|
269
|
+
delivery
|
|
270
|
+
});
|
|
271
|
+
return (DELIVERY_RANK[delivery] ?? 0) >= want ? message : void 0;
|
|
272
|
+
};
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
let settled = false;
|
|
275
|
+
const finish = (error, message) => {
|
|
276
|
+
if (settled) return;
|
|
277
|
+
settled = true;
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
off();
|
|
280
|
+
if (error) reject(error);
|
|
281
|
+
else resolve(message);
|
|
282
|
+
};
|
|
283
|
+
const check = (last = false) => void node(ctx).getMessage(chat, messageId).then((message) => {
|
|
284
|
+
try {
|
|
285
|
+
const done = look(message);
|
|
286
|
+
if (done) finish(null, done);
|
|
287
|
+
else if (last) finish(new CliError("timeout", `Timed out after ${Math.round(ms / 1e3)} s: the message is ${message?.delivery ?? "not sent yet"} and still goes by itself while this profile is online`, {
|
|
288
|
+
messageId,
|
|
289
|
+
delivery: message?.delivery ?? null
|
|
290
|
+
}));
|
|
291
|
+
} catch (error) {
|
|
292
|
+
finish(error);
|
|
293
|
+
}
|
|
294
|
+
}, finish);
|
|
295
|
+
const timer = setTimeout(() => check(true), ms);
|
|
296
|
+
const off = ctx.hub.onEvent((event) => {
|
|
297
|
+
if (event.chat === chat && (event.messageId === messageId || event.message?.id === messageId)) check();
|
|
298
|
+
});
|
|
299
|
+
check();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/buttons.ts
|
|
304
|
+
/**
|
|
305
|
+
* `--button id:Label` (again for each, 1 to 6), `--style id=primary|neutral|danger`, `--once` and `--id`, as the
|
|
306
|
+
* method's parameters: `buttons` and `buttonsId`. Nothing when no `--button` is given. The label may hold colons: the
|
|
307
|
+
* id ends at the first one. Two labels that read the same (ignoring case and spaces at the ends), or a label that is
|
|
308
|
+
* another button's id, are a usage error here; other bounds (ids, labels, how many) are the card's, checked when it is sent.
|
|
309
|
+
*/
|
|
310
|
+
function buttonsOf(options) {
|
|
311
|
+
const given = Array.isArray(options.button) ? options.button : [];
|
|
312
|
+
if (!given.length) {
|
|
313
|
+
for (const flag of [
|
|
314
|
+
"style",
|
|
315
|
+
"once",
|
|
316
|
+
"id"
|
|
317
|
+
]) if (options[flag] !== void 0) throw new CliError("usage", `--${flag} goes with --button id:Label`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const buttons = given.map((value) => {
|
|
321
|
+
const colon = value.indexOf(":");
|
|
322
|
+
if (colon < 1 || colon === value.length - 1) throw new CliError("usage", `--button takes id:Label, like yes:Yes, not ${JSON.stringify(value)}`);
|
|
323
|
+
return {
|
|
324
|
+
id: value.slice(0, colon),
|
|
325
|
+
label: value.slice(colon + 1).trim()
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
const clash = buttonLabelClash(buttons);
|
|
329
|
+
if (clash) {
|
|
330
|
+
const [at, other] = [JSON.stringify(given[clash.at]), JSON.stringify(given[clash.with])];
|
|
331
|
+
throw new CliError("usage", clash.kind === "label" ? `--button ${at} repeats the label of --button ${other} (ignoring case and spaces): a typed answer could not tell them apart` : `--button ${at} has a label that is the id of --button ${other}: a typed answer could not tell them apart`);
|
|
332
|
+
}
|
|
333
|
+
for (const value of Array.isArray(options.style) ? options.style : []) {
|
|
334
|
+
const eq = value.indexOf("=");
|
|
335
|
+
const button = eq > 0 ? buttons.find((b) => b.id === value.slice(0, eq)) : void 0;
|
|
336
|
+
if (!button) throw new CliError("usage", `--style takes id=primary|neutral|danger for one of the buttons, not ${JSON.stringify(value)}`);
|
|
337
|
+
button.style = value.slice(eq + 1);
|
|
338
|
+
}
|
|
339
|
+
if (options.once === true) for (const button of buttons) button.once = true;
|
|
340
|
+
return {
|
|
341
|
+
buttons,
|
|
342
|
+
...options.id !== void 0 && { buttonsId: String(options.id) }
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/** A buttons card checked by the sender's rule, or a `bad_request` that says what is wrong. */
|
|
346
|
+
function checked(card) {
|
|
347
|
+
const result = checkStatusCard(card);
|
|
348
|
+
if ("error" in result) throw new CliError("bad_request", `Buttons: ${result.error}`, { card });
|
|
349
|
+
return result.card;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The card a send's `buttons` (and `buttonsId`) make, or undefined when it names none. `buttons` is a list of
|
|
353
|
+
* `{id, label, style?, once?}`; the id of the question is made up (`ask-…`) unless given.
|
|
354
|
+
*/
|
|
355
|
+
function buttonsCard(params) {
|
|
356
|
+
const buttons = params.buttons;
|
|
357
|
+
if (buttons === void 0 || buttons === null) {
|
|
358
|
+
if (params.buttonsId !== void 0) throw new CliError("bad_request", "buttonsId goes with buttons");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!Array.isArray(buttons)) throw new CliError("bad_request", "buttons must be a list of {id, label, style?, once?}");
|
|
362
|
+
return checked({
|
|
363
|
+
kind: "buttons",
|
|
364
|
+
id: str(params, "buttonsId") ?? `ask-${toBase64Url(randomBytes(6))}`,
|
|
365
|
+
buttons
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
/** Whether a 1:1 contact's app shows and presses buttons: null until its app said what it offers. */
|
|
369
|
+
function pressable(link) {
|
|
370
|
+
const peer = link.sessionOffers?.peer;
|
|
371
|
+
return peer ? peer.includes(BUTTONS_CAPABILITY) : null;
|
|
372
|
+
}
|
|
373
|
+
/** The message of mine with buttons that `button update` names, by its id here (or the id both sides know). */
|
|
374
|
+
function buttonsMessage(message, ref, group) {
|
|
375
|
+
if (!message) throw new CliError("not_found", `No message ${JSON.stringify(ref)} of yours in this ${group ? "group" : "chat"}: the messageId send gave`);
|
|
376
|
+
if (message.card?.kind !== "buttons") throw new CliError("bad_request", "That message has no buttons: send one with send --button id:Label");
|
|
377
|
+
return message;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* `button update`: the answer shown on a question's buttons (`chosen`), the buttons closed (`close`), or a new text
|
|
381
|
+
* (`text`), by editing the message with the same buttons. What is not given stays as it was.
|
|
382
|
+
*/
|
|
383
|
+
async function updateButtons(ctx, params) {
|
|
384
|
+
const target = chatOrGroup(ctx, str(params, "chat", true));
|
|
385
|
+
const ref = str(params, "message", true);
|
|
386
|
+
const chosen = str(params, "chosen");
|
|
387
|
+
const close = bool(params, "close");
|
|
388
|
+
const newText = str(params, "text");
|
|
389
|
+
if (chosen === void 0 && !close && newText === void 0) throw new CliError("bad_request", "Nothing to update: give chosen, close or text");
|
|
390
|
+
if (newText !== void 0 && !bool(params, "force")) {
|
|
391
|
+
const secret = findSecret(newText);
|
|
392
|
+
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 });
|
|
393
|
+
}
|
|
394
|
+
const message = buttonsMessage(await messageOf(ctx, target.linkId, ref, (m) => m.sender === "me"), ref, target.group);
|
|
395
|
+
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`, {
|
|
396
|
+
messageId: message.id,
|
|
397
|
+
edits: message.edit.seq
|
|
398
|
+
});
|
|
399
|
+
const before = message.card;
|
|
400
|
+
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 });
|
|
401
|
+
const card = checked({
|
|
402
|
+
...before,
|
|
403
|
+
...chosen !== void 0 && { chosen },
|
|
404
|
+
...(close || before.closed) && { closed: true }
|
|
405
|
+
});
|
|
406
|
+
const text = newText ?? message.text;
|
|
407
|
+
const wait = oneOf(params, "wait", target.group ? ["none", "sent"] : ["none", "confirmed"], "none");
|
|
408
|
+
const ms = num(params, "timeout", 30, {
|
|
409
|
+
min: 1,
|
|
410
|
+
max: 3600
|
|
411
|
+
}) * 1e3;
|
|
412
|
+
const result = await node(ctx).editMessage({
|
|
413
|
+
linkId: target.linkId,
|
|
414
|
+
messageId: message.id,
|
|
415
|
+
text,
|
|
416
|
+
card
|
|
417
|
+
});
|
|
418
|
+
if (result.error) throw new CliError(result.refused ? "refused" : "unavailable", result.error);
|
|
419
|
+
let edited = await node(ctx).getMessage(target.linkId, message.id);
|
|
420
|
+
if (wait === "confirmed" && edited?.edit?.pending) edited = await waitForEdit(ctx, target.linkId, message.id, ms);
|
|
421
|
+
const edges = wait === "sent" && edited?.edit ? await waitForGroupFrame(ctx, target.id, message.id, edited.edit.seq, ms) : void 0;
|
|
422
|
+
return {
|
|
423
|
+
[target.group ? "group" : "chat"]: target.id,
|
|
424
|
+
buttons: card.id,
|
|
425
|
+
messageId: message.id,
|
|
426
|
+
card,
|
|
427
|
+
edits: edited?.edit?.seq ?? 0,
|
|
428
|
+
...target.group ? { ...edges !== void 0 && { edges } } : { confirmed: !!edited && !edited.edit?.pending }
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* `button press`: the other side of a question, as the app's tap: a press on a button of someone else's message, by
|
|
433
|
+
* its id here (or the id both sides know). The engine sends a reply whose text is the button's label, and refuses what
|
|
434
|
+
* an app would: a button the message does not have, closed buttons, a second answer past `once`, a second press
|
|
435
|
+
* within a second. A one-shot in a chat waits until the press went out, as `send` does.
|
|
436
|
+
*/
|
|
437
|
+
async function pressButton(ctx, params) {
|
|
438
|
+
const target = chatOrGroup(ctx, str(params, "chat", true));
|
|
439
|
+
const ref = str(params, "message", true);
|
|
440
|
+
const buttonId = str(params, "button", true);
|
|
441
|
+
const where = target.group ? "group" : "chat";
|
|
442
|
+
const message = await messageOf(ctx, target.linkId, ref, (m) => m.sender !== "me");
|
|
443
|
+
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`);
|
|
444
|
+
if (message.card?.kind !== "buttons") throw new CliError("bad_request", "That message has no buttons");
|
|
445
|
+
const button = message.card.buttons.find((b) => b.id === buttonId);
|
|
446
|
+
if (!button) throw new CliError("not_found", `That message has no button ${JSON.stringify(buttonId)}; its buttons: ${message.card.buttons.map((b) => b.id).join(", ")}`);
|
|
447
|
+
const wait = target.group ? oneOf(params, "wait", ["none", "sent"], "none") : oneOf(params, "wait", [
|
|
448
|
+
"none",
|
|
449
|
+
"sent",
|
|
450
|
+
"delivered"
|
|
451
|
+
], ctx.mode === "one-shot" ? "sent" : "none");
|
|
452
|
+
const ms = num(params, "timeout", 30, {
|
|
453
|
+
min: 1,
|
|
454
|
+
max: 3600
|
|
455
|
+
}) * 1e3;
|
|
456
|
+
const result = await node(ctx).pressButton({
|
|
457
|
+
linkId: target.linkId,
|
|
458
|
+
messageId: message.id,
|
|
459
|
+
buttonId
|
|
460
|
+
});
|
|
461
|
+
if (result.error) throw new CliError(result.refused ? "refused" : "unavailable", result.error, result.paced ? { paced: true } : void 0);
|
|
462
|
+
const replyId = result.messageId ?? null;
|
|
463
|
+
const answer = {
|
|
464
|
+
[where]: target.id,
|
|
465
|
+
messageId: message.id,
|
|
466
|
+
button: button.id,
|
|
467
|
+
label: button.label,
|
|
468
|
+
replyId
|
|
469
|
+
};
|
|
470
|
+
if (!replyId) return answer;
|
|
471
|
+
if (target.group) {
|
|
472
|
+
const edges = wait === "sent" ? await waitForGroupFrame(ctx, target.id, replyId, void 0, ms) : node(ctx).groupTaken({
|
|
473
|
+
groupId: target.id,
|
|
474
|
+
messageId: replyId
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
...answer,
|
|
478
|
+
edges
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const reply = wait === "none" ? await node(ctx).getMessage(target.linkId, replyId) : await waitForMessage(ctx, target.linkId, replyId, wait, ms);
|
|
482
|
+
return {
|
|
483
|
+
...answer,
|
|
484
|
+
delivery: reply?.delivery ?? null
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
var BUTTON_METHODS = {
|
|
488
|
+
/** WISP 406 · Message Buttons: a press on a button of someone else's message, as the app's tap. */
|
|
489
|
+
"button.press": pressButton,
|
|
490
|
+
/** WISP 406 · Message Buttons: the answer chosen on a question of mine, its buttons closed, or its text changed. */
|
|
491
|
+
"button.update": updateButtons
|
|
492
|
+
};
|
|
493
|
+
//#endregion
|
|
494
|
+
export { waitForEdit as a, findSecret as c, pressable as i, buttonsCard as n, waitForGroupFrame as o, buttonsOf as r, waitForMessage as s, BUTTON_METHODS as t };
|
|
495
|
+
|
|
496
|
+
//# sourceMappingURL=buttons-CvNBzh2S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buttons-CvNBzh2S.js","names":[],"sources":["../../../../apps/ui/src/lib/parse/secrets.ts","../../src/waits.ts","../../src/buttons.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2.js\";\nimport { createBase58check } from \"@scure/base\";\nimport { wordlist } from \"@scure/bip39/wordlists/english.js\";\n\n/**\n * What the composer asks about before a message goes: text that looks like a secret someone could take funds or an\n * identity with, or a Cashu token (bearer money: the text box would hand it to whoever reads the chat).\n *\n * Detection is local and synchronous, and a finding never carries the text it found: only what kind it is and, for\n * ecash, the amount decoded here. Ark, Spark and Fedimint wallets are BIP39 seeds or raw keys, caught by the rules\n * below; Fedimint ecash notes are plain base64 with no prefix, so they are not recognised.\n */\nexport type SecretFinding =\n | { kind: \"mnemonic\" | \"nsec\" | \"bitcoin-key\" | \"hex-key\" }\n | { kind: \"cashu\"; amount: number | null; unit: string };\n\nexport type SecretKind = SecretFinding[\"kind\"];\n\nconst WORD_INDEX = new Map(wordlist.map((word, index) => [word, index]));\nconst MNEMONIC_LENGTHS = [12, 15, 18, 21, 24];\n/** Checksums tried per message at most: a 16 KiB run of seed words stays a few milliseconds. */\nconst CHECKSUM_BUDGET = 4096;\n\nconst NSEC = /\\bnsec1[02-9ac-hj-np-z]{58}(?![02-9ac-hj-np-z])/i;\nconst EXTENDED_PRIVATE = /\\b[xtyzuvYZUV]prv[1-9A-HJ-NP-Za-km-z]{100,}/;\nconst WIF = /\\b[59KLc][1-9A-HJ-NP-Za-km-z]{50,51}\\b/g;\nconst HEX_SECRET = /\\b(?:0x)?(?:[0-9a-fA-F]{128}|[0-9a-fA-F]{64})\\b/g;\nconst SECRET_CONTEXT = /priv(?:ate)?[\\s_-]*key|secret|seed|mnemonic|nsec|xprv|\\bwif\\b|recovery|passphrase/i;\n/** How far from a 64-hex string a word like \"seed\" still counts as saying what it is. */\nconst CONTEXT_REACH = 48;\nconst CASHU_TOKEN = /\\bcashu([AB])([A-Za-z0-9_\\-+/]{20,}={0,2})/g;\n\n/**\n * Characters that take no room on screen: format characters (zero-width space, soft hyphen, word joiner, the joiners,\n * direction marks), control characters that are not white space, variation selectors, the grapheme joiner and the\n * Hangul fillers. A page, a PDF or a terminal puts some of them into long strings as places to break a line.\n */\nconst INVISIBLE = /(?![\\t\\n\\v\\f\\r\\x85])[\\p{Cc}\\p{Cf}]|\\u034f|[\\u115f\\u1160\\u3164\\uffa0]|[\\ufe00-\\ufe0f]|[\\u{e0100}-\\u{e01ef}]/gu;\n\nconst base58check = createBase58check(sha256);\n\n/**\n * The first secret in `text`, keys and seeds before ecash; null for anything else. The text is read as it is and as\n * it reads without its invisible characters: a key with one inside is whole again once the reader takes it out.\n */\nexport function findSecret(text: string): SecretFinding | null {\n const visible = text.replace(INVISIBLE, \"\");\n const readings = visible === text ? [text] : [text, visible];\n if (readings.some(hasMnemonic)) return { kind: \"mnemonic\" };\n if (readings.some((reading) => NSEC.test(reading))) return { kind: \"nsec\" };\n if (readings.some((reading) => EXTENDED_PRIVATE.test(reading) || hasWif(reading))) return { kind: \"bitcoin-key\" };\n if (readings.some(hasHexSecret)) return { kind: \"hex-key\" };\n const tokens = readings.map(findCashu);\n return tokens.find((found) => found?.kind === \"cashu\" && found.amount !== null) ?? tokens.find((found) => found) ?? null;\n}\n\n/**\n * 12 to 24 words in a row from the English BIP39 list whose checksum holds, anywhere in the text; or a message that\n * is nothing but 12, 15, 18, 21 or 24 of those words (a seed with one word mistyped is still most of a seed). Prose\n * breaks a run at once: \"the\", \"a\", \"is\", \"to\", \"and\", \"of\" and \"I\" are not on the list.\n *\n * One word said over and over is not a seed, though one in 16 such runs passes a 12-word checksum (\"word\" does): a\n * window of a single word never counts, and a window mostly of one word (\"abandon … about\") counts only when that\n * word does not go on past it on either side.\n */\nfunction hasMnemonic(text: string): boolean {\n const tokens = text.toLowerCase().match(/[a-z]+/g);\n if (!tokens || tokens.length < 12) return false;\n let budget = CHECKSUM_BUDGET;\n let run: number[] = [];\n const seedLike = (start: number, length: number) => {\n const window = run.slice(start, start + length);\n const counts = new Map<number, number>();\n for (const word of window) counts.set(word, (counts.get(word) ?? 0) + 1);\n if (counts.size === 1) return false;\n const [most, times] = [...counts].reduce((a, b) => (b[1] > a[1] ? b : a));\n if (times > length / 2 && (run[start - 1] === most || run[start + length] === most)) return false;\n return checksumHolds(window);\n };\n const check = () => {\n if (run.length >= 12) {\n for (const length of MNEMONIC_LENGTHS) {\n for (let start = 0; start + length <= run.length && budget > 0; start++, budget--) {\n if (seedLike(start, length)) return true;\n }\n }\n if (run.length === tokens.length && MNEMONIC_LENGTHS.includes(run.length) && new Set(run).size > 1) return true;\n }\n run = [];\n return false;\n };\n for (const token of tokens) {\n const index = WORD_INDEX.get(token);\n if (index !== undefined) run.push(index);\n else if (check()) return true;\n }\n return check();\n}\n\n/** BIP39: the words' 11-bit indices are the entropy followed by the first bits of its SHA-256. */\nfunction checksumHolds(indices: number[]): boolean {\n const bits = indices.length * 11, checksumBits = bits / 33, entropyBytes = (bits - checksumBits) / 8;\n const bytes = new Uint8Array(Math.ceil(bits / 8));\n indices.forEach((index, i) => {\n for (let b = 0; b < 11; b++) if (index & (1 << (10 - b))) { const at = i * 11 + b; bytes[at >> 3] |= 0x80 >> (at & 7); }\n });\n const expected = sha256(bytes.subarray(0, entropyBytes))[0] >> (8 - checksumBits);\n const got = bytes[entropyBytes] >> (8 - checksumBits);\n return expected === got;\n}\n\n/** A WIF private key: base58check with version 0x80 (mainnet) or 0xef (testnet), 32 bytes and an optional 0x01. */\nfunction hasWif(text: string): boolean {\n for (const [candidate] of text.matchAll(WIF)) {\n try {\n const payload = base58check.decode(candidate);\n if ((payload[0] === 0x80 || payload[0] === 0xef) && (payload.length === 33 || (payload.length === 34 && payload[33] === 1))) return true;\n } catch { /* not base58check: an ordinary word */ }\n }\n return false;\n}\n\n/** 64 or 128 hex digits with \"private key\", \"secret\", \"seed\" or the like close by. A bare hash is not asked about. */\nfunction hasHexSecret(text: string): boolean {\n for (const match of text.matchAll(HEX_SECRET)) {\n const before = text.slice(Math.max(0, match.index - CONTEXT_REACH), match.index);\n const after = text.slice(match.index + match[0].length, match.index + match[0].length + CONTEXT_REACH);\n if (SECRET_CONTEXT.test(before) || SECRET_CONTEXT.test(after)) return true;\n }\n return false;\n}\n\n/** Every Cashu token in the text, with their amounts added up when all of them decode and share one unit. */\nfunction findCashu(text: string): SecretFinding | null {\n const tokens = [...text.matchAll(CASHU_TOKEN)].map(([, version, body]) => decodeCashu(version as \"A\" | \"B\", body));\n if (!tokens.length) return null;\n const unit = tokens.find((token) => token)?.unit ?? \"sat\";\n const readable = tokens.every((token) => token && token.unit === unit);\n return { kind: \"cashu\", amount: readable ? tokens.reduce((sum, token) => sum + token!.amount, 0) : null, unit };\n}\n\n/** What a Cashu token is worth, read from the token itself (NUT-00): nothing is asked of the mint. */\nexport function decodeCashu(version: \"A\" | \"B\", body: string): { amount: number; unit: string } | null {\n try {\n const bytes = fromBase64(body);\n if (version === \"A\") {\n const token = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes)) as { token?: { proofs?: { amount?: unknown }[] }[]; unit?: unknown };\n return total(token.token?.flatMap((entry) => entry.proofs ?? []).map((proof) => proof.amount), token.unit);\n }\n const token = decodeCbor(bytes) as { t?: { p?: { a?: unknown }[] }[]; u?: unknown } | null;\n return total(token?.t?.flatMap((entry) => entry.p ?? []).map((proof) => proof.a), token?.u);\n } catch {\n return null;\n }\n}\n\nfunction total(amounts: unknown[] | undefined, unit: unknown): { amount: number; unit: string } | null {\n if (!amounts?.length || !amounts.every((a): a is number => Number.isSafeInteger(a) && (a as number) >= 0)) return null;\n const amount = amounts.reduce((sum, a) => sum + a, 0);\n return Number.isSafeInteger(amount) ? { amount, unit: typeof unit === \"string\" && unit ? unit : \"sat\" } : null;\n}\n\nfunction fromBase64(text: string): Uint8Array {\n const standard = text.replace(/-/g, \"+\").replace(/_/g, \"/\").replace(/=+$/, \"\");\n const binary = atob(standard + \"=\".repeat((4 - (standard.length % 4)) % 4));\n return Uint8Array.from(binary, (c) => c.charCodeAt(0));\n}\n\n/** Enough CBOR (RFC 8949) for a cashuB token: definite lengths, integers, bytes, text, arrays, maps, tags, simple values. */\nfunction decodeCbor(bytes: Uint8Array): unknown {\n let at = 0, depth = 0;\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const length = (info: number): number => {\n if (info < 24) return info;\n const size = info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : info === 27 ? 8 : 0;\n if (!size || at + size > bytes.length) throw new Error(\"cbor\");\n let value = 0;\n for (let i = 0; i < size; i++) value = value * 256 + bytes[at + i];\n at += size;\n return value;\n };\n const item = (): unknown => {\n if (at >= bytes.length || ++depth > 32) throw new Error(\"cbor\");\n const head = bytes[at++], major = head >> 5, info = head & 31;\n try {\n if (major === 7) {\n if (info === 20) return false;\n if (info === 21) return true;\n if (info === 22 || info === 23) return null;\n if (info === 25) { at += 2; return NaN; } // a half float: never in a token\n if (info === 26) { at += 4; return view.getFloat32(at - 4); }\n if (info === 27) { at += 8; return view.getFloat64(at - 8); }\n throw new Error(\"cbor\");\n }\n const n = length(info);\n switch (major) {\n case 0: return n;\n case 1: return -1 - n;\n case 2: case 3: {\n if (at + n > bytes.length) throw new Error(\"cbor\");\n const slice = bytes.subarray(at, (at += n));\n return major === 2 ? slice : new TextDecoder(\"utf-8\", { fatal: true }).decode(slice);\n }\n case 4: {\n if (n > bytes.length - at) throw new Error(\"cbor\");\n return Array.from({ length: n }, item);\n }\n case 5: {\n if (n > bytes.length - at) throw new Error(\"cbor\");\n const map: Record<string, unknown> = Object.create(null);\n for (let i = 0; i < n; i++) { const key = item(); map[String(key)] = item(); }\n return map;\n }\n default: return item(); // a tag: the value it tags\n }\n } finally { depth--; }\n };\n return item();\n}\n","import type { StoredMessage } from \"@ghostly/browser/shared/types\";\nimport { node, type ApiContext } from \"./apiKit\";\nimport { CliError } from \"./errors\";\nimport type { GhostlyEvent } from \"./events\";\n\n/*\n * Waiting on what a command sent (WISP 1100): a message's delivery, an edit's confirmation, a group frame taken by an\n * edge. Shared by the chat, group and status card methods.\n */\n\nconst DELIVERY_RANK: Record<string, number> = { sending: 0, waiting: 1, queued: 1, held: 2, sent: 2, delivered: 3 };\n\n/** Waits until the contact confirmed the latest edit of one of my messages (it is no longer pending). */\nexport async function waitForEdit(ctx: ApiContext, chat: string, messageId: string, ms: number): Promise<StoredMessage> {\n const until = Date.now() + ms;\n for (;;) {\n const message = await node(ctx).getMessage(chat, messageId);\n if (!message) throw new CliError(\"not_found\", `No message ${messageId} in this chat`);\n if (!message.edit?.pending) return message;\n if (Date.now() >= until) throw new CliError(\"timeout\", `Timed out after ${Math.round(ms / 1000)} s: the contact has not confirmed the edit yet. It goes by itself once the chat is live and the contact's app shows edits, while this profile is online`, { messageId, edits: message.edit.seq });\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n}\n\n/**\n * Waits until an edge took my group message, or its edit number `edit` (WISP 902: a group has no receipts, so this is\n * as far as the author sees): a member's edge in a private group, an edge to one of my hubs in a community.\n */\nexport async function waitForGroupFrame(ctx: ApiContext, groupId: string, messageId: string, edit: number | undefined, ms: number): Promise<number> {\n const until = Date.now() + ms;\n for (;;) {\n const taken = node(ctx).groupTaken({ groupId, messageId, ...(edit ? { edit } : {}) });\n if (taken > 0) return taken;\n if (Date.now() >= until) throw new CliError(\"timeout\", `Timed out after ${Math.round(ms / 1000)} s: no member's edge took it yet. It stays in the group and goes when one opens, while this profile is online`, { messageId, ...(edit ? { edits: edit } : {}) });\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n}\n\n/** Waits until a message of mine reached `target` (`sent`: on its way to the contact; `delivered`: acknowledged). */\nexport async function waitForMessage(ctx: ApiContext, chat: string, messageId: string, target: \"sent\" | \"delivered\", ms: number): Promise<StoredMessage> {\n const want = DELIVERY_RANK[target];\n const look = (message: StoredMessage | undefined) => {\n if (!message) return undefined;\n const delivery = message.delivery ?? \"sent\";\n if (delivery === \"failed\") throw new CliError(\"engine\", message.deliveryError ?? \"The message could not be sent\", { messageId, delivery });\n return (DELIVERY_RANK[delivery] ?? 0) >= want ? message : undefined;\n };\n return new Promise<StoredMessage>((resolve, reject) => {\n let settled = false;\n const finish = (error: unknown, message?: StoredMessage) => {\n if (settled) return;\n settled = true; clearTimeout(timer); off();\n if (error) reject(error); else resolve(message!);\n };\n const check = (last = false) => void node(ctx).getMessage(chat, messageId).then((message) => {\n try {\n const done = look(message);\n if (done) finish(null, done);\n else if (last) finish(new CliError(\"timeout\", `Timed out after ${Math.round(ms / 1000)} s: the message is ${message?.delivery ?? \"not sent yet\"} and still goes by itself while this profile is online`, { messageId, delivery: message?.delivery ?? null }));\n } catch (error) { finish(error); }\n }, finish);\n const timer = setTimeout(() => check(true), ms);\n // Any event naming the message: a message first seen already delivered comes as `message.sent`, never as\n // `message.delivery`. Listening before the first read leaves no gap for a change to slip through.\n const off = ctx.hub.onEvent((event: GhostlyEvent) => {\n if (event.chat === chat && (event.messageId === messageId || (event.message as { id?: string } | undefined)?.id === messageId)) check();\n });\n check();\n });\n}\n","import { BUTTONS_CAPABILITY, STATUS_CARD_LIMITS, buttonLabelClash, checkStatusCard, randomBytes, toBase64Url, type ButtonsCard } from \"@ghostly/core\";\nimport type { LinkView, StoredMessage } from \"@ghostly/browser/shared/types\";\nimport type { Parsed } from \"./args\";\nimport { bool, chatOrGroup, messageOf, node, num, oneOf, str, type Method, type Params } from \"./apiKit\";\nimport { CliError } from \"./errors\";\nimport { findSecret } from \"../../../apps/ui/src/lib/parse/secrets\";\nimport { waitForEdit, waitForGroupFrame, waitForMessage } from \"./waits\";\n\n/*\n * Message buttons (WISP 406 · Message Buttons), the bot's side: a message whose text is the question, with up to six\n * buttons under it (`send --button yes:Yes`). A press comes back as a reply whose text is the button's label; the\n * engine marks it (`press`) and `listen` says `button.pressed`. `button update` then shows the answer (`chosen`) and\n * closes the buttons, by editing the message with the same buttons.\n */\n\n/**\n * `--button id:Label` (again for each, 1 to 6), `--style id=primary|neutral|danger`, `--once` and `--id`, as the\n * method's parameters: `buttons` and `buttonsId`. Nothing when no `--button` is given. The label may hold colons: the\n * id ends at the first one. Two labels that read the same (ignoring case and spaces at the ends), or a label that is\n * another button's id, are a usage error here; other bounds (ids, labels, how many) are the card's, checked when it is sent.\n */\nexport function buttonsOf(options: Parsed[\"options\"]): { buttons: Record<string, unknown>[]; buttonsId?: string } | undefined {\n const given = Array.isArray(options.button) ? options.button as string[] : [];\n if (!given.length) {\n for (const flag of [\"style\", \"once\", \"id\"] as const)\n if (options[flag] !== undefined) throw new CliError(\"usage\", `--${flag} goes with --button id:Label`);\n return undefined;\n }\n const buttons: Record<string, unknown>[] = given.map((value) => {\n const colon = value.indexOf(\":\");\n if (colon < 1 || colon === value.length - 1) throw new CliError(\"usage\", `--button takes id:Label, like yes:Yes, not ${JSON.stringify(value)}`);\n return { id: value.slice(0, colon), label: value.slice(colon + 1).trim() };\n });\n // Two buttons a typed answer could not tell apart: the same label, or a label that is another's id (ignoring case).\n const clash = buttonLabelClash(buttons);\n if (clash) {\n const [at, other] = [JSON.stringify(given[clash.at]), JSON.stringify(given[clash.with])];\n throw new CliError(\"usage\", clash.kind === \"label\"\n ? `--button ${at} repeats the label of --button ${other} (ignoring case and spaces): a typed answer could not tell them apart`\n : `--button ${at} has a label that is the id of --button ${other}: a typed answer could not tell them apart`);\n }\n for (const value of Array.isArray(options.style) ? options.style as string[] : []) {\n const eq = value.indexOf(\"=\");\n const button = eq > 0 ? buttons.find((b) => b.id === value.slice(0, eq)) : undefined;\n if (!button) throw new CliError(\"usage\", `--style takes id=primary|neutral|danger for one of the buttons, not ${JSON.stringify(value)}`);\n button.style = value.slice(eq + 1);\n }\n if (options.once === true) for (const button of buttons) button.once = true;\n return { buttons, ...(options.id !== undefined && { buttonsId: String(options.id) }) };\n}\n\n/** A buttons card checked by the sender's rule, or a `bad_request` that says what is wrong. */\nfunction checked(card: Record<string, unknown>): ButtonsCard {\n const result = checkStatusCard(card);\n if (\"error\" in result) throw new CliError(\"bad_request\", `Buttons: ${result.error}`, { card });\n return result.card as ButtonsCard;\n}\n\n/**\n * The card a send's `buttons` (and `buttonsId`) make, or undefined when it names none. `buttons` is a list of\n * `{id, label, style?, once?}`; the id of the question is made up (`ask-…`) unless given.\n */\nexport function buttonsCard(params: Params): ButtonsCard | undefined {\n const buttons = params.buttons;\n if (buttons === undefined || buttons === null) {\n if (params.buttonsId !== undefined) throw new CliError(\"bad_request\", \"buttonsId goes with buttons\");\n return undefined;\n }\n if (!Array.isArray(buttons)) throw new CliError(\"bad_request\", \"buttons must be a list of {id, label, style?, once?}\");\n const id = str(params, \"buttonsId\") ?? `ask-${toBase64Url(randomBytes(6))}`;\n return checked({ kind: \"buttons\", id, buttons });\n}\n\n/** Whether a 1:1 contact's app shows and presses buttons: null until its app said what it offers. */\nexport function pressable(link: LinkView): boolean | null {\n const peer = link.sessionOffers?.peer;\n return peer ? peer.includes(BUTTONS_CAPABILITY) : null;\n}\n\n/** The message of mine with buttons that `button update` names, by its id here (or the id both sides know). */\nfunction buttonsMessage(message: StoredMessage | undefined, ref: string, group: boolean): StoredMessage & { card: ButtonsCard } {\n if (!message) throw new CliError(\"not_found\", `No message ${JSON.stringify(ref)} of yours in this ${group ? \"group\" : \"chat\"}: the messageId send gave`);\n if (message.card?.kind !== \"buttons\") throw new CliError(\"bad_request\", \"That message has no buttons: send one with send --button id:Label\");\n return message as StoredMessage & { card: ButtonsCard };\n}\n\n/**\n * `button update`: the answer shown on a question's buttons (`chosen`), the buttons closed (`close`), or a new text\n * (`text`), by editing the message with the same buttons. What is not given stays as it was.\n */\nasync function updateButtons(ctx: Parameters<Method>[0], params: Params): Promise<Record<string, unknown>> {\n const target = chatOrGroup(ctx, str(params, \"chat\", true));\n const ref = str(params, \"message\", true);\n const chosen = str(params, \"chosen\");\n const close = bool(params, \"close\");\n const newText = str(params, \"text\");\n if (chosen === undefined && !close && newText === undefined) throw new CliError(\"bad_request\", \"Nothing to update: give chosen, close or text\");\n // A new text is a message like any other: the same guard as `send` and `edit`.\n if (newText !== undefined && !bool(params, \"force\")) {\n const secret = findSecret(newText);\n 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 });\n }\n const message = buttonsMessage(await messageOf(ctx, target.linkId, ref, (m) => m.sender === \"me\"), ref, target.group);\n if ((message.edit?.seq ?? 0) >= STATUS_CARD_LIMITS.edits)\n throw new CliError(\"refused\", `This message took ${STATUS_CARD_LIMITS.edits} updates, the most one takes: ask again with a new send`, { messageId: message.id, edits: message.edit!.seq });\n const before = message.card;\n // Said here, with the buttons it has, as a press says it: the card's own check names only the field.\n if (chosen !== undefined && !before.buttons.some((b) => b.id === chosen))\n throw new CliError(\"bad_request\", `That message has no button ${JSON.stringify(chosen)}; its buttons: ${before.buttons.map((b) => b.id).join(\", \")}`, { card: before });\n const card = checked({ ...before, ...(chosen !== undefined && { chosen }), ...((close || before.closed) && { closed: true }) });\n const text = newText ?? message.text;\n const wait = oneOf(params, \"wait\", target.group ? [\"none\", \"sent\"] as const : [\"none\", \"confirmed\"] as const, \"none\");\n const ms = num(params, \"timeout\", 30, { min: 1, max: 3600 }) * 1000;\n const result = await node(ctx).editMessage({ linkId: target.linkId, messageId: message.id, text, card });\n if (result.error) throw new CliError(result.refused ? \"refused\" : \"unavailable\", result.error);\n let edited = await node(ctx).getMessage(target.linkId, message.id);\n if (wait === \"confirmed\" && edited?.edit?.pending) edited = await waitForEdit(ctx, target.linkId, message.id, ms);\n const edges = wait === \"sent\" && edited?.edit ? await waitForGroupFrame(ctx, target.id, message.id, edited.edit.seq, ms) : undefined;\n return {\n [target.group ? \"group\" : \"chat\"]: target.id, buttons: card.id, messageId: message.id, card, edits: edited?.edit?.seq ?? 0,\n ...(target.group ? { ...(edges !== undefined && { edges }) } : { confirmed: !!edited && !edited.edit?.pending }),\n };\n}\n\n/**\n * `button press`: the other side of a question, as the app's tap: a press on a button of someone else's message, by\n * its id here (or the id both sides know). The engine sends a reply whose text is the button's label, and refuses what\n * an app would: a button the message does not have, closed buttons, a second answer past `once`, a second press\n * within a second. A one-shot in a chat waits until the press went out, as `send` does.\n */\nasync function pressButton(ctx: Parameters<Method>[0], params: Params): Promise<Record<string, unknown>> {\n const target = chatOrGroup(ctx, str(params, \"chat\", true));\n const ref = str(params, \"message\", true);\n const buttonId = str(params, \"button\", true);\n const where = target.group ? \"group\" : \"chat\";\n const message = await messageOf(ctx, target.linkId, ref, (m) => m.sender !== \"me\");\n 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`);\n if (message.card?.kind !== \"buttons\") throw new CliError(\"bad_request\", \"That message has no buttons\");\n const button = message.card.buttons.find((b) => b.id === buttonId);\n if (!button) throw new CliError(\"not_found\", `That message has no button ${JSON.stringify(buttonId)}; its buttons: ${message.card.buttons.map((b) => b.id).join(\", \")}`);\n const wait = target.group\n ? oneOf(params, \"wait\", [\"none\", \"sent\"] as const, \"none\")\n : oneOf(params, \"wait\", [\"none\", \"sent\", \"delivered\"] as const, ctx.mode === \"one-shot\" ? \"sent\" : \"none\");\n const ms = num(params, \"timeout\", 30, { min: 1, max: 3600 }) * 1000;\n const result = await node(ctx).pressButton({ linkId: target.linkId, messageId: message.id, buttonId });\n if (result.error) throw new CliError(result.refused ? \"refused\" : \"unavailable\", result.error, result.paced ? { paced: true } : undefined);\n const replyId = result.messageId ?? null;\n const answer = { [where]: target.id, messageId: message.id, button: button.id, label: button.label, replyId };\n if (!replyId) return answer;\n if (target.group) {\n const edges = wait === \"sent\" ? await waitForGroupFrame(ctx, target.id, replyId, undefined, ms) : node(ctx).groupTaken({ groupId: target.id, messageId: replyId });\n return { ...answer, edges };\n }\n const reply = wait === \"none\" ? await node(ctx).getMessage(target.linkId, replyId) : await waitForMessage(ctx, target.linkId, replyId, wait, ms);\n return { ...answer, delivery: reply?.delivery ?? null };\n}\n\nexport const BUTTON_METHODS: Record<string, Method> = {\n /** WISP 406 · Message Buttons: a press on a button of someone else's message, as the app's tap. */\n \"button.press\": pressButton,\n /** WISP 406 · Message Buttons: the answer chosen on a question of mine, its buttons closed, or its text changed. */\n \"button.update\": updateButtons,\n};\n"],"mappings":"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"}
|