add encrypted Nostr session sharing (#8922)
Signed-off-by: callebtc <93376500+callebtc@users.noreply.github.com> Signed-off-by: Douwe Osinga <douwe@squareup.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Generated
+363
-1
@@ -17,6 +17,16 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aead"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aes"
|
name = "aes"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
@@ -379,6 +389,37 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-utility"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"gloo-timers",
|
||||||
|
"tokio",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-wsocket"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069"
|
||||||
|
dependencies = [
|
||||||
|
"async-utility",
|
||||||
|
"futures",
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tokio-socks",
|
||||||
|
"tokio-tungstenite 0.26.2",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atoi"
|
name = "atoi"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -388,6 +429,12 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-destructor"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -1014,6 +1061,12 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bech32"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "better_scoped_tls"
|
name = "better_scoped_tls"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -1315,6 +1368,17 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bip39"
|
||||||
|
version = "2.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc"
|
||||||
|
dependencies = [
|
||||||
|
"bitcoin_hashes",
|
||||||
|
"serde",
|
||||||
|
"unicode-normalization",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -1336,6 +1400,23 @@ version = "0.10.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
|
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitcoin-io"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitcoin_hashes"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
|
||||||
|
dependencies = [
|
||||||
|
"bitcoin-io",
|
||||||
|
"hex-conservative",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -1793,6 +1874,17 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -1804,6 +1896,19 @@ dependencies = [
|
|||||||
"rand_core 0.10.0",
|
"rand_core 0.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20poly1305"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"chacha20 0.9.1",
|
||||||
|
"cipher",
|
||||||
|
"poly1305",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.44"
|
version = "0.4.44"
|
||||||
@@ -1826,6 +1931,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
"inout",
|
"inout",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2409,6 +2515,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
|
"rand_core 0.6.4",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4362,6 +4469,18 @@ dependencies = [
|
|||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-timers"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "goose"
|
name = "goose"
|
||||||
version = "1.34.0"
|
version = "1.34.0"
|
||||||
@@ -4417,6 +4536,8 @@ dependencies = [
|
|||||||
"minijinja",
|
"minijinja",
|
||||||
"mockall",
|
"mockall",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
|
"nostr",
|
||||||
|
"nostr-sdk",
|
||||||
"oauth2",
|
"oauth2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
@@ -4437,6 +4558,7 @@ dependencies = [
|
|||||||
"reqwest 0.13.2",
|
"reqwest 0.13.2",
|
||||||
"rmcp",
|
"rmcp",
|
||||||
"rubato",
|
"rubato",
|
||||||
|
"rustls",
|
||||||
"schemars 1.2.1",
|
"schemars 1.2.1",
|
||||||
"sec1",
|
"sec1",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4803,6 +4925,15 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex-conservative"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hipstr"
|
name = "hipstr"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -5405,6 +5536,18 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
@@ -6208,6 +6351,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "negentropy"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "new_debug_unreachable"
|
name = "new_debug_unreachable"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -6296,6 +6445,83 @@ dependencies = [
|
|||||||
"nom 8.0.0",
|
"nom 8.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nostr"
|
||||||
|
version = "0.44.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bech32",
|
||||||
|
"bip39",
|
||||||
|
"bitcoin_hashes",
|
||||||
|
"cbc",
|
||||||
|
"chacha20 0.9.1",
|
||||||
|
"chacha20poly1305",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"hex",
|
||||||
|
"instant",
|
||||||
|
"scrypt",
|
||||||
|
"secp256k1",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"unicode-normalization",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nostr-database"
|
||||||
|
version = "0.44.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1"
|
||||||
|
dependencies = [
|
||||||
|
"lru",
|
||||||
|
"nostr",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nostr-gossip"
|
||||||
|
version = "0.44.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6"
|
||||||
|
dependencies = [
|
||||||
|
"nostr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nostr-relay-pool"
|
||||||
|
version = "0.44.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b"
|
||||||
|
dependencies = [
|
||||||
|
"async-utility",
|
||||||
|
"async-wsocket",
|
||||||
|
"atomic-destructor",
|
||||||
|
"hex",
|
||||||
|
"lru",
|
||||||
|
"negentropy",
|
||||||
|
"nostr",
|
||||||
|
"nostr-database",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nostr-sdk"
|
||||||
|
version = "0.44.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393"
|
||||||
|
dependencies = [
|
||||||
|
"async-utility",
|
||||||
|
"nostr",
|
||||||
|
"nostr-database",
|
||||||
|
"nostr-gossip",
|
||||||
|
"nostr-relay-pool",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
@@ -6616,6 +6842,12 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-debug"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "open"
|
name = "open"
|
||||||
version = "5.3.4"
|
version = "5.3.4"
|
||||||
@@ -6857,6 +7089,17 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@@ -6890,6 +7133,16 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbkdf2"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pctx_code_execution_runtime"
|
name = "pctx_code_execution_runtime"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -7257,6 +7510,17 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "poly1305"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||||
|
dependencies = [
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
"opaque-debug",
|
||||||
|
"universal-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.13.1"
|
version = "1.13.1"
|
||||||
@@ -7693,7 +7957,7 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chacha20",
|
"chacha20 0.10.0",
|
||||||
"getrandom 0.4.2",
|
"getrandom 0.4.2",
|
||||||
"rand_core 0.10.0",
|
"rand_core 0.10.0",
|
||||||
]
|
]
|
||||||
@@ -8398,6 +8662,15 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "salsa20"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -8500,6 +8773,18 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scrypt"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
|
||||||
|
dependencies = [
|
||||||
|
"password-hash",
|
||||||
|
"pbkdf2",
|
||||||
|
"salsa20",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sdd"
|
name = "sdd"
|
||||||
version = "3.0.10"
|
version = "3.0.10"
|
||||||
@@ -8520,6 +8805,26 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "secp256k1"
|
||||||
|
version = "0.29.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
|
||||||
|
dependencies = [
|
||||||
|
"rand 0.8.5",
|
||||||
|
"secp256k1-sys",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "secp256k1-sys"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@@ -10723,6 +11028,18 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-socks"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"futures-util",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.18"
|
version = "0.1.18"
|
||||||
@@ -10734,6 +11051,22 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.26.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tungstenite 0.26.2",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tungstenite"
|
name = "tokio-tungstenite"
|
||||||
version = "0.28.0"
|
version = "0.28.0"
|
||||||
@@ -11204,6 +11537,25 @@ version = "0.25.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.26.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http 1.4.0",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.28.0"
|
version = "0.28.0"
|
||||||
@@ -11443,6 +11795,16 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
|
checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "universal-hash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
|||||||
@@ -543,6 +543,28 @@ enum SessionCommand {
|
|||||||
default_value = "markdown"
|
default_value = "markdown"
|
||||||
)]
|
)]
|
||||||
format: String,
|
format: String,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long = "nostr",
|
||||||
|
help = "Publish the JSON session export as an encrypted Nostr event and print a Goose share link"
|
||||||
|
)]
|
||||||
|
nostr: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long = "relay",
|
||||||
|
value_name = "RELAY",
|
||||||
|
help = "Nostr relay URL to publish to (can be specified multiple times)",
|
||||||
|
action = clap::ArgAction::Append
|
||||||
|
)]
|
||||||
|
relays: Vec<String>,
|
||||||
|
},
|
||||||
|
#[command(about = "Import a session from JSON or an encrypted Nostr share link")]
|
||||||
|
Import {
|
||||||
|
#[arg(help = "Path to a JSON session export, or a goose://sessions/nostr share link")]
|
||||||
|
input: String,
|
||||||
|
|
||||||
|
#[arg(long = "nostr", help = "Treat input as an encrypted Nostr share link")]
|
||||||
|
nostr: bool,
|
||||||
},
|
},
|
||||||
#[command(name = "diagnostics")]
|
#[command(name = "diagnostics")]
|
||||||
Diagnostics {
|
Diagnostics {
|
||||||
@@ -1227,6 +1249,8 @@ async fn handle_session_subcommand(command: SessionCommand) -> Result<()> {
|
|||||||
identifier,
|
identifier,
|
||||||
output,
|
output,
|
||||||
format,
|
format,
|
||||||
|
nostr,
|
||||||
|
relays,
|
||||||
} => {
|
} => {
|
||||||
let session_manager = SessionManager::instance();
|
let session_manager = SessionManager::instance();
|
||||||
let session_identifier = if let Some(id) = identifier {
|
let session_identifier = if let Some(id) = identifier {
|
||||||
@@ -1244,8 +1268,17 @@ async fn handle_session_subcommand(command: SessionCommand) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
crate::commands::session::handle_session_export(session_identifier, output, format)
|
crate::commands::session::handle_session_export(
|
||||||
.await?;
|
session_identifier,
|
||||||
|
output,
|
||||||
|
format,
|
||||||
|
nostr,
|
||||||
|
relays,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
SessionCommand::Import { input, nostr } => {
|
||||||
|
crate::commands::session::handle_session_import(input, nostr).await?;
|
||||||
}
|
}
|
||||||
SessionCommand::Diagnostics { identifier, output } => {
|
SessionCommand::Diagnostics { identifier, output } => {
|
||||||
let session_manager = SessionManager::instance();
|
let session_manager = SessionManager::instance();
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use anyhow::{Context, Result};
|
|||||||
|
|
||||||
use cliclack::{confirm, multiselect, select};
|
use cliclack::{confirm, multiselect, select};
|
||||||
use etcetera::home_dir;
|
use etcetera::home_dir;
|
||||||
use goose::session::{generate_diagnostics, Session, SessionManager};
|
use goose::config::Config;
|
||||||
|
use goose::session::{generate_diagnostics, nostr_share, Session, SessionManager, SessionType};
|
||||||
use goose::utils::safe_truncate;
|
use goose::utils::safe_truncate;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -216,6 +217,8 @@ pub async fn handle_session_export(
|
|||||||
session_id: String,
|
session_id: String,
|
||||||
output_path: Option<PathBuf>,
|
output_path: Option<PathBuf>,
|
||||||
format: String,
|
format: String,
|
||||||
|
nostr: bool,
|
||||||
|
relays: Vec<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let session_manager = SessionManager::instance();
|
let session_manager = SessionManager::instance();
|
||||||
let session = match session_manager.get_session(&session_id, true).await {
|
let session = match session_manager.get_session(&session_id, true).await {
|
||||||
@@ -241,6 +244,29 @@ pub async fn handle_session_export(
|
|||||||
_ => return Err(anyhow::anyhow!("Unsupported format: {}", format)),
|
_ => return Err(anyhow::anyhow!("Unsupported format: {}", format)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if nostr {
|
||||||
|
if format != "json" {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Nostr session sharing only supports --format json"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if output_path.is_some() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Nostr session sharing cannot be combined with --output"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let relays = nostr_share::resolve_relays(relays, Config::global());
|
||||||
|
let share = nostr_share::publish_session_json(&output, relays).await?;
|
||||||
|
println!("Session published to Nostr relays:");
|
||||||
|
for relay in &share.relays {
|
||||||
|
println!("- {}", relay);
|
||||||
|
}
|
||||||
|
println!("\nShare link:");
|
||||||
|
println!("{}", share.deeplink);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(output_path) = output_path {
|
if let Some(output_path) = output_path {
|
||||||
fs::write(&output_path, output).with_context(|| {
|
fs::write(&output_path, output).with_context(|| {
|
||||||
format!("Failed to write to output file: {}", output_path.display())
|
format!("Failed to write to output file: {}", output_path.display())
|
||||||
@@ -253,6 +279,25 @@ pub async fn handle_session_export(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_session_import(input: String, nostr: bool) -> Result<()> {
|
||||||
|
let json = if nostr || input.starts_with("goose://sessions/nostr") {
|
||||||
|
nostr_share::import_session_json_from_deeplink(&input).await?
|
||||||
|
} else {
|
||||||
|
fs::read_to_string(&input)
|
||||||
|
.with_context(|| format!("Failed to read session import file: {input}"))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let session_manager = SessionManager::instance();
|
||||||
|
let session = session_manager
|
||||||
|
.import_session(&json, Some(SessionType::User))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Session imported:");
|
||||||
|
println!("{} - {}", session.id, session.name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_diagnostics(session_id: &str, output_path: Option<PathBuf>) -> Result<()> {
|
pub async fn handle_diagnostics(session_id: &str, output_path: Option<PathBuf>) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"Generating diagnostics bundle for session '{}'...",
|
"Generating diagnostics bundle for session '{}'...",
|
||||||
|
|||||||
@@ -443,6 +443,8 @@ derive_utoipa!(IconTheme as IconThemeSchema);
|
|||||||
super::routes::session::delete_session,
|
super::routes::session::delete_session,
|
||||||
super::routes::session::export_session,
|
super::routes::session::export_session,
|
||||||
super::routes::session::import_session,
|
super::routes::session::import_session,
|
||||||
|
super::routes::session::share_session_nostr,
|
||||||
|
super::routes::session::import_session_nostr,
|
||||||
super::routes::session::update_session_user_recipe_values,
|
super::routes::session::update_session_user_recipe_values,
|
||||||
super::routes::session::fork_session,
|
super::routes::session::fork_session,
|
||||||
super::routes::session::get_session_extensions,
|
super::routes::session::get_session_extensions,
|
||||||
@@ -512,6 +514,9 @@ derive_utoipa!(IconTheme as IconThemeSchema);
|
|||||||
super::routes::session_events::SessionReplyResponse,
|
super::routes::session_events::SessionReplyResponse,
|
||||||
super::routes::session_events::CancelRequest,
|
super::routes::session_events::CancelRequest,
|
||||||
super::routes::session::ImportSessionRequest,
|
super::routes::session::ImportSessionRequest,
|
||||||
|
super::routes::session::ShareSessionNostrRequest,
|
||||||
|
super::routes::session::ShareSessionNostrResponse,
|
||||||
|
super::routes::session::ImportSessionNostrRequest,
|
||||||
super::routes::session::SessionListResponse,
|
super::routes::session::SessionListResponse,
|
||||||
super::routes::session::UpdateSessionNameRequest,
|
super::routes::session::UpdateSessionNameRequest,
|
||||||
super::routes::session::UpdateSessionUserRecipeValuesRequest,
|
super::routes::session::UpdateSessionUserRecipeValuesRequest,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use goose::agents::ExtensionConfig;
|
use goose::agents::ExtensionConfig;
|
||||||
use goose::recipe::Recipe;
|
use goose::recipe::Recipe;
|
||||||
|
use goose::session::nostr_share;
|
||||||
use goose::session::session_manager::{SessionInsights, SessionType};
|
use goose::session::session_manager::{SessionInsights, SessionType};
|
||||||
use goose::session::{EnabledExtensionsState, Session};
|
use goose::session::{EnabledExtensionsState, Session};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -50,6 +51,28 @@ pub struct ImportSessionRequest {
|
|||||||
json: String,
|
json: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ShareSessionNostrRequest {
|
||||||
|
#[serde(default)]
|
||||||
|
relays: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ShareSessionNostrResponse {
|
||||||
|
deeplink: String,
|
||||||
|
nevent: String,
|
||||||
|
event_id: String,
|
||||||
|
relays: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ImportSessionNostrRequest {
|
||||||
|
deeplink: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, ToSchema)]
|
#[derive(Deserialize, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ForkRequest {
|
pub struct ForkRequest {
|
||||||
@@ -364,6 +387,79 @@ async fn import_session(
|
|||||||
Ok(Json(session))
|
Ok(Json(session))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/sessions/{session_id}/share/nostr",
|
||||||
|
request_body = ShareSessionNostrRequest,
|
||||||
|
params(
|
||||||
|
("session_id" = String, Path, description = "Unique identifier for the session")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Session shared to Nostr successfully", body = ShareSessionNostrResponse),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
||||||
|
(status = 404, description = "Session not found"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("api_key" = [])
|
||||||
|
),
|
||||||
|
tag = "Session Management"
|
||||||
|
)]
|
||||||
|
async fn share_session_nostr(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(session_id): Path<String>,
|
||||||
|
Json(request): Json<ShareSessionNostrRequest>,
|
||||||
|
) -> Result<Json<ShareSessionNostrResponse>, StatusCode> {
|
||||||
|
let exported = state
|
||||||
|
.session_manager()
|
||||||
|
.export_session(&session_id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
let relays = nostr_share::resolve_relays(request.relays, goose::config::Config::global());
|
||||||
|
let share = nostr_share::publish_session_json(&exported, relays)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
Ok(Json(ShareSessionNostrResponse {
|
||||||
|
deeplink: share.deeplink,
|
||||||
|
nevent: share.nevent,
|
||||||
|
event_id: share.event_id,
|
||||||
|
relays: share.relays,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/sessions/import/nostr",
|
||||||
|
request_body = ImportSessionNostrRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Nostr shared session imported successfully", body = Session),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
||||||
|
(status = 400, description = "Bad request - Invalid Nostr share link"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("api_key" = [])
|
||||||
|
),
|
||||||
|
tag = "Session Management"
|
||||||
|
)]
|
||||||
|
async fn import_session_nostr(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(request): Json<ImportSessionNostrRequest>,
|
||||||
|
) -> Result<Json<Session>, StatusCode> {
|
||||||
|
let json = nostr_share::import_session_json_from_deeplink(&request.deeplink)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||||
|
let session = state
|
||||||
|
.session_manager()
|
||||||
|
.import_session(&json, Some(SessionType::User))
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||||
|
|
||||||
|
Ok(Json(session))
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/sessions/{session_id}/fork",
|
path = "/sessions/{session_id}/fork",
|
||||||
@@ -505,10 +601,18 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
|||||||
.route("/sessions/{session_id}", get(get_session))
|
.route("/sessions/{session_id}", get(get_session))
|
||||||
.route("/sessions/{session_id}", delete(delete_session))
|
.route("/sessions/{session_id}", delete(delete_session))
|
||||||
.route("/sessions/{session_id}/export", get(export_session))
|
.route("/sessions/{session_id}/export", get(export_session))
|
||||||
|
.route(
|
||||||
|
"/sessions/{session_id}/share/nostr",
|
||||||
|
post(share_session_nostr).layer(DefaultBodyLimit::max(25 * 1024 * 1024)),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/sessions/import",
|
"/sessions/import",
|
||||||
post(import_session).layer(DefaultBodyLimit::max(25 * 1024 * 1024)),
|
post(import_session).layer(DefaultBodyLimit::max(25 * 1024 * 1024)),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/sessions/import/nostr",
|
||||||
|
post(import_session_nostr).layer(DefaultBodyLimit::max(25 * 1024 * 1024)),
|
||||||
|
)
|
||||||
.route("/sessions/insights", get(get_session_insights))
|
.route("/sessions/insights", get(get_session_insights))
|
||||||
.route("/sessions/{session_id}/name", put(update_session_name))
|
.route("/sessions/{session_id}/name", put(update_session_name))
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ aws-providers = [
|
|||||||
cuda = ["local-inference", "candle-core/cuda", "candle-nn/cuda", "llama-cpp-2/cuda"]
|
cuda = ["local-inference", "candle-core/cuda", "candle-nn/cuda", "llama-cpp-2/cuda"]
|
||||||
vulkan = ["local-inference", "llama-cpp-2/vulkan"]
|
vulkan = ["local-inference", "llama-cpp-2/vulkan"]
|
||||||
rustls-tls = [
|
rustls-tls = [
|
||||||
|
"dep:rustls",
|
||||||
"reqwest/rustls",
|
"reqwest/rustls",
|
||||||
"rmcp/reqwest",
|
"rmcp/reqwest",
|
||||||
"sqlx/runtime-tokio-rustls",
|
"sqlx/runtime-tokio-rustls",
|
||||||
@@ -59,6 +60,7 @@ native-tls = [
|
|||||||
"oauth2/native-tls",
|
"oauth2/native-tls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
@@ -194,6 +196,9 @@ goose-acp-macros = { path = "../goose-acp-macros" }
|
|||||||
tower-http = { workspace = true, features = ["cors"] }
|
tower-http = { workspace = true, features = ["cors"] }
|
||||||
http-body-util = "0.1.3"
|
http-body-util = "0.1.3"
|
||||||
process-wrap = { version = "9.1.0", features = ["std"] }
|
process-wrap = { version = "9.1.0", features = ["std"] }
|
||||||
|
nostr = { version = "0.44.2", features = ["nip44"] }
|
||||||
|
nostr-sdk = { version = "0.44.1", features = ["nip44"] }
|
||||||
|
rustls = { version = "0.23", features = ["aws_lc_rs"], optional = true }
|
||||||
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod chat_history_search;
|
|||||||
mod diagnostics;
|
mod diagnostics;
|
||||||
pub mod extension_data;
|
pub mod extension_data;
|
||||||
mod legacy;
|
mod legacy;
|
||||||
|
pub mod nostr_share;
|
||||||
pub mod session_manager;
|
pub mod session_manager;
|
||||||
|
|
||||||
pub use diagnostics::{
|
pub use diagnostics::{
|
||||||
|
|||||||
@@ -0,0 +1,370 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use nostr::nips::nip19::{FromBech32, Nip19Event, ToBech32};
|
||||||
|
use nostr::nips::nip44;
|
||||||
|
use nostr::prelude::*;
|
||||||
|
use nostr_sdk::Client;
|
||||||
|
|
||||||
|
use crate::config::{Config, ConfigError};
|
||||||
|
|
||||||
|
pub const EVENT_KIND: u16 = 30278;
|
||||||
|
pub const CONFIG_RELAYS_KEY: &str = "GOOSE_NOSTR_RELAYS";
|
||||||
|
|
||||||
|
const DEFAULT_RELAYS: &[&str] = &[
|
||||||
|
"wss://relay.damus.io",
|
||||||
|
"wss://relay.primal.net",
|
||||||
|
"wss://nos.lol",
|
||||||
|
"wss://relay.nostr.band",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct NostrShare {
|
||||||
|
pub deeplink: String,
|
||||||
|
pub nevent: String,
|
||||||
|
pub event_id: String,
|
||||||
|
pub relays: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ParsedShareLink {
|
||||||
|
pub nevent: String,
|
||||||
|
pub decryption_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait NostrPublisher {
|
||||||
|
async fn publish(&self, event: Event, relays: &[String]) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait NostrFetcher {
|
||||||
|
async fn fetch(&self, event_id: EventId, relays: &[String]) -> Result<Event>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LiveNostrClient;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl NostrPublisher for LiveNostrClient {
|
||||||
|
async fn publish(&self, event: Event, relays: &[String]) -> Result<()> {
|
||||||
|
install_rustls_crypto_provider();
|
||||||
|
let client = Client::default();
|
||||||
|
for relay in relays {
|
||||||
|
client
|
||||||
|
.add_relay(relay)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed to add relay {relay}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.try_connect(Duration::from_secs(8)).await;
|
||||||
|
let output = client
|
||||||
|
.send_event_to(relays.iter().map(String::as_str), &event)
|
||||||
|
.await
|
||||||
|
.context("Failed to publish session to Nostr relays")?;
|
||||||
|
client.shutdown().await;
|
||||||
|
|
||||||
|
if output.success.is_empty() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Failed to publish session to any Nostr relay: {:?}",
|
||||||
|
output.failed
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl NostrFetcher for LiveNostrClient {
|
||||||
|
async fn fetch(&self, event_id: EventId, relays: &[String]) -> Result<Event> {
|
||||||
|
install_rustls_crypto_provider();
|
||||||
|
let client = Client::default();
|
||||||
|
for relay in relays {
|
||||||
|
client
|
||||||
|
.add_relay(relay)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed to add relay {relay}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.try_connect(Duration::from_secs(8)).await;
|
||||||
|
let filter = Filter::new()
|
||||||
|
.id(event_id)
|
||||||
|
.kind(Kind::Custom(EVENT_KIND))
|
||||||
|
.limit(1);
|
||||||
|
let events = client
|
||||||
|
.fetch_events_from(
|
||||||
|
relays.iter().map(String::as_str),
|
||||||
|
filter,
|
||||||
|
Duration::from_secs(10),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to fetch shared session from Nostr relays")?;
|
||||||
|
client.shutdown().await;
|
||||||
|
|
||||||
|
events
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("Shared session event not found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rustls-tls")]
|
||||||
|
fn install_rustls_crypto_provider() {
|
||||||
|
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "rustls-tls"))]
|
||||||
|
fn install_rustls_crypto_provider() {}
|
||||||
|
|
||||||
|
pub fn default_relays() -> Vec<String> {
|
||||||
|
DEFAULT_RELAYS
|
||||||
|
.iter()
|
||||||
|
.map(|relay| relay.to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn relays_from_config(config: &Config) -> Vec<String> {
|
||||||
|
match config.get_param::<Vec<String>>(CONFIG_RELAYS_KEY) {
|
||||||
|
Ok(relays) if !relays.is_empty() => normalize_relays(relays),
|
||||||
|
Err(ConfigError::NotFound(_)) => default_relays(),
|
||||||
|
_ => default_relays(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_relays(cli_relays: Vec<String>, config: &Config) -> Vec<String> {
|
||||||
|
if cli_relays.is_empty() {
|
||||||
|
relays_from_config(config)
|
||||||
|
} else {
|
||||||
|
normalize_relays(cli_relays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_session_json(session_json: &str, relays: Vec<String>) -> Result<NostrShare> {
|
||||||
|
publish_session_json_with(session_json, relays, &LiveNostrClient).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_session_json_with<P>(
|
||||||
|
session_json: &str,
|
||||||
|
relays: Vec<String>,
|
||||||
|
publisher: &P,
|
||||||
|
) -> Result<NostrShare>
|
||||||
|
where
|
||||||
|
P: NostrPublisher + Sync,
|
||||||
|
{
|
||||||
|
let relays = normalize_relays(relays);
|
||||||
|
if relays.is_empty() {
|
||||||
|
return Err(anyhow!("At least one Nostr relay is required"));
|
||||||
|
}
|
||||||
|
let relay_urls = relays
|
||||||
|
.iter()
|
||||||
|
.map(|relay| RelayUrl::parse(relay))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
let publish_keys = Keys::generate();
|
||||||
|
let encryption_key = SecretKey::generate();
|
||||||
|
let encryption_keys = Keys::new(encryption_key.clone());
|
||||||
|
let encrypted = nip44::encrypt(
|
||||||
|
&encryption_key,
|
||||||
|
&encryption_keys.public_key(),
|
||||||
|
session_json,
|
||||||
|
nip44::Version::V2,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let event = EventBuilder::new(Kind::Custom(EVENT_KIND), encrypted)
|
||||||
|
.tag(Tag::identifier(format!(
|
||||||
|
"goose-session-{}",
|
||||||
|
uuid::Uuid::now_v7()
|
||||||
|
)))
|
||||||
|
.tag(Tag::parse(["client", "goose"])?)
|
||||||
|
.sign_with_keys(&publish_keys)?;
|
||||||
|
|
||||||
|
publisher.publish(event.clone(), &relays).await?;
|
||||||
|
|
||||||
|
let nevent = Nip19Event::new(event.id)
|
||||||
|
.author(event.pubkey)
|
||||||
|
.kind(Kind::Custom(EVENT_KIND))
|
||||||
|
.relays(relay_urls)
|
||||||
|
.to_bech32()?;
|
||||||
|
let decryption_key = encryption_key.to_secret_hex();
|
||||||
|
let deeplink = build_deeplink(&nevent, &decryption_key);
|
||||||
|
|
||||||
|
Ok(NostrShare {
|
||||||
|
deeplink,
|
||||||
|
nevent,
|
||||||
|
event_id: event.id.to_hex(),
|
||||||
|
relays,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn import_session_json_from_deeplink(deeplink: &str) -> Result<String> {
|
||||||
|
import_session_json_from_deeplink_with(deeplink, &LiveNostrClient).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn import_session_json_from_deeplink_with<F>(
|
||||||
|
deeplink: &str,
|
||||||
|
fetcher: &F,
|
||||||
|
) -> Result<String>
|
||||||
|
where
|
||||||
|
F: NostrFetcher + Sync,
|
||||||
|
{
|
||||||
|
let ParsedShareLink {
|
||||||
|
nevent,
|
||||||
|
decryption_key,
|
||||||
|
} = parse_deeplink(deeplink)?;
|
||||||
|
let event_ref = Nip19Event::from_bech32(&nevent)?;
|
||||||
|
let relays = event_ref
|
||||||
|
.relays
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if relays.is_empty() {
|
||||||
|
return Err(anyhow!("Shared session link does not include any relays"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = fetcher.fetch(event_ref.event_id, &relays).await?;
|
||||||
|
if event.kind != Kind::Custom(EVENT_KIND) {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Unexpected Nostr event kind: {}",
|
||||||
|
u16::from(event.kind)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let secret_key = SecretKey::parse(&decryption_key)?;
|
||||||
|
let encryption_keys = Keys::new(secret_key.clone());
|
||||||
|
nip44::decrypt(&secret_key, &encryption_keys.public_key(), event.content).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_deeplink(nevent: &str, decryption_key: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"goose://sessions/nostr?nevent={}&key={}",
|
||||||
|
urlencoding::encode(nevent),
|
||||||
|
urlencoding::encode(decryption_key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_deeplink(deeplink: &str) -> Result<ParsedShareLink> {
|
||||||
|
let parsed = url::Url::parse(deeplink).context("Invalid Goose session share link")?;
|
||||||
|
if parsed.scheme() != "goose"
|
||||||
|
|| parsed.host_str() != Some("sessions")
|
||||||
|
|| parsed.path() != "/nostr"
|
||||||
|
{
|
||||||
|
return Err(anyhow!("Invalid Goose Nostr session share link"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let nevent = parsed
|
||||||
|
.query_pairs()
|
||||||
|
.find_map(|(key, value)| (key == "nevent").then(|| value.into_owned()))
|
||||||
|
.ok_or_else(|| anyhow!("Missing nevent parameter"))?;
|
||||||
|
let decryption_key = parsed
|
||||||
|
.query_pairs()
|
||||||
|
.find_map(|(key, value)| (key == "key").then(|| value.into_owned()))
|
||||||
|
.ok_or_else(|| anyhow!("Missing decryption key parameter"))?;
|
||||||
|
|
||||||
|
Ok(ParsedShareLink {
|
||||||
|
nevent,
|
||||||
|
decryption_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_relays(relays: Vec<String>) -> Vec<String> {
|
||||||
|
let mut normalized = Vec::new();
|
||||||
|
for relay in relays {
|
||||||
|
let relay = relay.trim();
|
||||||
|
if relay.is_empty() || normalized.iter().any(|existing| existing == relay) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalized.push(relay.to_string());
|
||||||
|
}
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
struct RecordingPublisher {
|
||||||
|
event: Arc<Mutex<Option<Event>>>,
|
||||||
|
relays: Arc<Mutex<Vec<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl NostrPublisher for RecordingPublisher {
|
||||||
|
async fn publish(&self, event: Event, relays: &[String]) -> Result<()> {
|
||||||
|
*self.event.lock().unwrap() = Some(event);
|
||||||
|
*self.relays.lock().unwrap() = relays.to_vec();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StaticFetcher(Event);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl NostrFetcher for StaticFetcher {
|
||||||
|
async fn fetch(&self, _event_id: EventId, _relays: &[String]) -> Result<Event> {
|
||||||
|
Ok(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn publish_builds_deeplink_and_encrypted_kind_30278_event() {
|
||||||
|
let event = Arc::new(Mutex::new(None));
|
||||||
|
let relays = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let publisher = RecordingPublisher {
|
||||||
|
event: event.clone(),
|
||||||
|
relays: relays.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let share = publish_session_json_with(
|
||||||
|
r#"{"id":"session-id","conversation":{"messages":[]}}"#,
|
||||||
|
vec!["wss://relay.example".to_string()],
|
||||||
|
&publisher,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(share.deeplink.starts_with("goose://sessions/nostr?"));
|
||||||
|
assert!(share.nevent.starts_with("nevent1"));
|
||||||
|
assert_eq!(share.relays, vec!["wss://relay.example"]);
|
||||||
|
assert_eq!(*relays.lock().unwrap(), vec!["wss://relay.example"]);
|
||||||
|
|
||||||
|
let event = event.lock().unwrap().clone().unwrap();
|
||||||
|
assert_eq!(event.kind, Kind::Custom(EVENT_KIND));
|
||||||
|
assert_ne!(
|
||||||
|
event.content,
|
||||||
|
r#"{"id":"session-id","conversation":{"messages":[]}}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn publish_and_import_round_trips_session_json() {
|
||||||
|
let event = Arc::new(Mutex::new(None));
|
||||||
|
let publisher = RecordingPublisher {
|
||||||
|
event: event.clone(),
|
||||||
|
relays: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
};
|
||||||
|
let json = r#"{"id":"session-id","name":"shared"}"#;
|
||||||
|
|
||||||
|
let share =
|
||||||
|
publish_session_json_with(json, vec!["wss://relay.example".to_string()], &publisher)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let fetched_event = event.lock().unwrap().clone().unwrap();
|
||||||
|
let imported =
|
||||||
|
import_session_json_from_deeplink_with(&share.deeplink, &StaticFetcher(fetched_event))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(imported, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_deeplink() {
|
||||||
|
let parsed = parse_deeplink("goose://sessions/nostr?nevent=abc&key=def").unwrap();
|
||||||
|
assert_eq!(parsed.nevent, "abc");
|
||||||
|
assert_eq!(parsed.decryption_key, "def");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3145,6 +3145,50 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/sessions/import/nostr": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Session Management"
|
||||||
|
],
|
||||||
|
"operationId": "import_session_nostr",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ImportSessionNostrRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Nostr shared session imported successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Session"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid Nostr share link"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - Invalid or missing API key"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/sessions/insights": {
|
"/sessions/insights": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -3656,6 +3700,61 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/sessions/{session_id}/share/nostr": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Session Management"
|
||||||
|
],
|
||||||
|
"operationId": "share_session_nostr",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "session_id",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Unique identifier for the session",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ShareSessionNostrRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Session shared to Nostr successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ShareSessionNostrResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - Invalid or missing API key"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Session not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/sessions/{session_id}/user_recipe_values": {
|
"/sessions/{session_id}/user_recipe_values": {
|
||||||
"put": {
|
"put": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -5625,6 +5724,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ImportSessionNostrRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"deeplink"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"deeplink": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ImportSessionRequest": {
|
"ImportSessionRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -8105,6 +8215,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ShareSessionNostrRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"relays": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ShareSessionNostrResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"deeplink",
|
||||||
|
"nevent",
|
||||||
|
"eventId",
|
||||||
|
"relays"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"deeplink": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"eventId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"nevent": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"relays": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SlashCommand": {
|
"SlashCommand": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
+18
-9
@@ -8,7 +8,7 @@ import {
|
|||||||
useLocation,
|
useLocation,
|
||||||
useSearchParams,
|
useSearchParams,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { openSharedSessionFromDeepLink } from './sessionLinks';
|
import { openSharedSessionFromDeepLink, importNostrSessionFromDeepLink } from './sessionLinks';
|
||||||
import { type SharedSessionDetails } from './sharedSessions';
|
import { type SharedSessionDetails } from './sharedSessions';
|
||||||
import { ErrorUI } from './components/ErrorBoundary';
|
import { ErrorUI } from './components/ErrorBoundary';
|
||||||
import { ExtensionInstallModal } from './components/ExtensionInstallModal';
|
import { ExtensionInstallModal } from './components/ExtensionInstallModal';
|
||||||
@@ -428,6 +428,11 @@ export function AppInner() {
|
|||||||
setIsLoadingSharedSession(true);
|
setIsLoadingSharedSession(true);
|
||||||
setSharedSessionError(null);
|
setSharedSessionError(null);
|
||||||
try {
|
try {
|
||||||
|
if (link.startsWith('goose://sessions/nostr')) {
|
||||||
|
await importNostrSessionFromDeepLink(link);
|
||||||
|
navigate('/sessions');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await openSharedSessionFromDeepLink(link, (_view: View, options?: ViewOptions) => {
|
await openSharedSessionFromDeepLink(link, (_view: View, options?: ViewOptions) => {
|
||||||
navigate('/shared-session', { state: options });
|
navigate('/shared-session', { state: options });
|
||||||
});
|
});
|
||||||
@@ -438,14 +443,18 @@ export function AppInner() {
|
|||||||
action: 'open_shared_session',
|
action: 'open_shared_session',
|
||||||
recoverable: true,
|
recoverable: true,
|
||||||
});
|
});
|
||||||
// Navigate to shared session view with error
|
if (link.startsWith('goose://sessions/nostr')) {
|
||||||
const shareToken = link.replace('goose://sessions/', '');
|
toast.error(`Failed to import Nostr session: ${errorMessage(error, 'Unknown error')}`);
|
||||||
const options = {
|
navigate('/sessions');
|
||||||
sessionDetails: null,
|
} else {
|
||||||
error: errorMessage(error, 'Unknown error'),
|
const shareToken = link.replace('goose://sessions/', '');
|
||||||
shareToken,
|
const options = {
|
||||||
};
|
sessionDetails: null,
|
||||||
navigate('/shared-session', { state: options });
|
error: errorMessage(error, 'Unknown error'),
|
||||||
|
shareToken,
|
||||||
|
};
|
||||||
|
navigate('/shared-session', { state: options });
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingSharedSession(false);
|
setIsLoadingSharedSession(false);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -593,6 +593,10 @@ export type ImportAppResponse = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ImportSessionNostrRequest = {
|
||||||
|
deeplink: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ImportSessionRequest = {
|
export type ImportSessionRequest = {
|
||||||
json: string;
|
json: string;
|
||||||
};
|
};
|
||||||
@@ -1368,6 +1372,17 @@ export type SetupResponse = {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ShareSessionNostrRequest = {
|
||||||
|
relays?: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShareSessionNostrResponse = {
|
||||||
|
deeplink: string;
|
||||||
|
eventId: string;
|
||||||
|
nevent: string;
|
||||||
|
relays: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
export type SlashCommand = {
|
export type SlashCommand = {
|
||||||
command: string;
|
command: string;
|
||||||
command_type: CommandType;
|
command_type: CommandType;
|
||||||
@@ -4091,6 +4106,37 @@ export type ImportSessionResponses = {
|
|||||||
|
|
||||||
export type ImportSessionResponse = ImportSessionResponses[keyof ImportSessionResponses];
|
export type ImportSessionResponse = ImportSessionResponses[keyof ImportSessionResponses];
|
||||||
|
|
||||||
|
export type ImportSessionNostrData = {
|
||||||
|
body: ImportSessionNostrRequest;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/sessions/import/nostr';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImportSessionNostrErrors = {
|
||||||
|
/**
|
||||||
|
* Bad request - Invalid Nostr share link
|
||||||
|
*/
|
||||||
|
400: unknown;
|
||||||
|
/**
|
||||||
|
* Unauthorized - Invalid or missing API key
|
||||||
|
*/
|
||||||
|
401: unknown;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImportSessionNostrResponses = {
|
||||||
|
/**
|
||||||
|
* Nostr shared session imported successfully
|
||||||
|
*/
|
||||||
|
200: Session;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImportSessionNostrResponse = ImportSessionNostrResponses[keyof ImportSessionNostrResponses];
|
||||||
|
|
||||||
export type GetSessionInsightsData = {
|
export type GetSessionInsightsData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
@@ -4473,6 +4519,42 @@ export type UpdateSessionNameResponses = {
|
|||||||
200: unknown;
|
200: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ShareSessionNostrData = {
|
||||||
|
body: ShareSessionNostrRequest;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Unique identifier for the session
|
||||||
|
*/
|
||||||
|
session_id: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/sessions/{session_id}/share/nostr';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShareSessionNostrErrors = {
|
||||||
|
/**
|
||||||
|
* Unauthorized - Invalid or missing API key
|
||||||
|
*/
|
||||||
|
401: unknown;
|
||||||
|
/**
|
||||||
|
* Session not found
|
||||||
|
*/
|
||||||
|
404: unknown;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShareSessionNostrResponses = {
|
||||||
|
/**
|
||||||
|
* Session shared to Nostr successfully
|
||||||
|
*/
|
||||||
|
200: ShareSessionNostrResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShareSessionNostrResponse2 = ShareSessionNostrResponses[keyof ShareSessionNostrResponses];
|
||||||
|
|
||||||
export type UpdateSessionUserRecipeValuesData = {
|
export type UpdateSessionUserRecipeValuesData = {
|
||||||
body: UpdateSessionUserRecipeValuesRequest;
|
body: UpdateSessionUserRecipeValuesRequest;
|
||||||
path: {
|
path: {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Download,
|
Download,
|
||||||
Upload,
|
Upload,
|
||||||
|
Share2,
|
||||||
|
LoaderCircle,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Copy,
|
Copy,
|
||||||
Puzzle,
|
Puzzle,
|
||||||
@@ -28,18 +30,29 @@ import { Skeleton } from '../ui/skeleton';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { ConfirmationModal } from '../ui/ConfirmationModal';
|
import { ConfirmationModal } from '../ui/ConfirmationModal';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '../ui/dialog';
|
||||||
import {
|
import {
|
||||||
deleteSession,
|
deleteSession,
|
||||||
exportSession,
|
exportSession,
|
||||||
forkSession,
|
forkSession,
|
||||||
importSession,
|
importSession,
|
||||||
|
importSessionNostr,
|
||||||
listSessions,
|
listSessions,
|
||||||
searchSessions,
|
searchSessions,
|
||||||
|
shareSessionNostr,
|
||||||
Session,
|
Session,
|
||||||
updateSessionName,
|
updateSessionName,
|
||||||
ExtensionConfig,
|
ExtensionConfig,
|
||||||
ExtensionData,
|
ExtensionData,
|
||||||
} from '../../api';
|
} from '../../api';
|
||||||
|
import { getTunnelStatus } from '../../api/sdk.gen';
|
||||||
import { formatExtensionName } from '../settings/extensions/subcomponents/ExtensionList';
|
import { formatExtensionName } from '../settings/extensions/subcomponents/ExtensionList';
|
||||||
import { getSearchShortcutText } from '../../utils/keyboardShortcuts';
|
import { getSearchShortcutText } from '../../utils/keyboardShortcuts';
|
||||||
import { shouldShowNewChatTitle } from '../../sessions';
|
import { shouldShowNewChatTitle } from '../../sessions';
|
||||||
@@ -55,6 +68,11 @@ const i18n = defineMessages({
|
|||||||
sessionUpdateFailed: { id: 'sessions.toast.updateFailed', defaultMessage: 'Failed to update session description: {error}' },
|
sessionUpdateFailed: { id: 'sessions.toast.updateFailed', defaultMessage: 'Failed to update session description: {error}' },
|
||||||
chatHistory: { id: 'sessions.chatHistory', defaultMessage: 'Chat history' },
|
chatHistory: { id: 'sessions.chatHistory', defaultMessage: 'Chat history' },
|
||||||
importSession: { id: 'sessions.import', defaultMessage: 'Import Session' },
|
importSession: { id: 'sessions.import', defaultMessage: 'Import Session' },
|
||||||
|
importNostrSession: { id: 'sessions.importNostr', defaultMessage: 'Import Link' },
|
||||||
|
importNostrTitle: { id: 'sessions.importNostr.title', defaultMessage: 'Import Nostr Session' },
|
||||||
|
importNostrDesc: { id: 'sessions.importNostr.description', defaultMessage: 'Paste a Goose Nostr share link to fetch, decrypt, and import the session.' },
|
||||||
|
importNostrPlaceholder: { id: 'sessions.importNostr.placeholder', defaultMessage: 'goose://sessions/nostr?nevent=...&key=...' },
|
||||||
|
importing: { id: 'sessions.importing', defaultMessage: 'Importing...' },
|
||||||
chatHistoryDesc: { id: 'sessions.chatHistoryDesc', defaultMessage: 'View and search your past conversations with Goose. {shortcut} to search.' },
|
chatHistoryDesc: { id: 'sessions.chatHistoryDesc', defaultMessage: 'View and search your past conversations with Goose. {shortcut} to search.' },
|
||||||
searchPlaceholder: { id: 'sessions.searchPlaceholder', defaultMessage: 'Search history...' },
|
searchPlaceholder: { id: 'sessions.searchPlaceholder', defaultMessage: 'Search history...' },
|
||||||
errorLoading: { id: 'sessions.error.loading', defaultMessage: 'Error Loading Sessions' },
|
errorLoading: { id: 'sessions.error.loading', defaultMessage: 'Error Loading Sessions' },
|
||||||
@@ -73,12 +91,19 @@ const i18n = defineMessages({
|
|||||||
importSuccess: { id: 'sessions.toast.imported', defaultMessage: 'Session imported successfully' },
|
importSuccess: { id: 'sessions.toast.imported', defaultMessage: 'Session imported successfully' },
|
||||||
importFailed: { id: 'sessions.toast.importFailed', defaultMessage: 'Failed to import session: {error}' },
|
importFailed: { id: 'sessions.toast.importFailed', defaultMessage: 'Failed to import session: {error}' },
|
||||||
exportSuccess: { id: 'sessions.toast.exported', defaultMessage: 'Session exported successfully' },
|
exportSuccess: { id: 'sessions.toast.exported', defaultMessage: 'Session exported successfully' },
|
||||||
|
shareNostrSuccess: { id: 'sessions.toast.shareNostr', defaultMessage: 'Encrypted Nostr share link created' },
|
||||||
|
shareNostrFailed: { id: 'sessions.toast.shareNostrFailed', defaultMessage: 'Failed to create Nostr share link: {error}' },
|
||||||
|
copied: { id: 'sessions.toast.copied', defaultMessage: 'Copied to clipboard' },
|
||||||
openInNewWindow: { id: 'sessions.action.openNewWindow', defaultMessage: 'Open in new window' },
|
openInNewWindow: { id: 'sessions.action.openNewWindow', defaultMessage: 'Open in new window' },
|
||||||
editSessionName: { id: 'sessions.action.editName', defaultMessage: 'Edit session name' },
|
editSessionName: { id: 'sessions.action.editName', defaultMessage: 'Edit session name' },
|
||||||
duplicateSession: { id: 'sessions.action.duplicate', defaultMessage: 'Duplicate session' },
|
duplicateSession: { id: 'sessions.action.duplicate', defaultMessage: 'Duplicate session' },
|
||||||
deleteSession: { id: 'sessions.action.delete', defaultMessage: 'Delete session' },
|
deleteSession: { id: 'sessions.action.delete', defaultMessage: 'Delete session' },
|
||||||
exportSession: { id: 'sessions.action.export', defaultMessage: 'Export session' },
|
exportSession: { id: 'sessions.action.export', defaultMessage: 'Export session' },
|
||||||
|
shareNostrSession: { id: 'sessions.action.shareNostr', defaultMessage: 'Share encrypted Nostr link' },
|
||||||
extensions: { id: 'sessions.extensions', defaultMessage: 'Extensions:' },
|
extensions: { id: 'sessions.extensions', defaultMessage: 'Extensions:' },
|
||||||
|
shareNostrTitle: { id: 'sessions.shareNostr.title', defaultMessage: 'Encrypted Nostr Share Link' },
|
||||||
|
shareNostrDesc: { id: 'sessions.shareNostr.description', defaultMessage: 'Anyone with this link can fetch and decrypt the session. Treat it like a secret.' },
|
||||||
|
close: { id: 'sessions.close', defaultMessage: 'Close' },
|
||||||
});
|
});
|
||||||
|
|
||||||
function getSessionExtensionNames(extensionData: ExtensionData): string[] {
|
function getSessionExtensionNames(extensionData: ExtensionData): string[] {
|
||||||
@@ -265,6 +290,14 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
||||||
const [sessionToDelete, setSessionToDelete] = useState<Session | null>(null);
|
const [sessionToDelete, setSessionToDelete] = useState<Session | null>(null);
|
||||||
|
|
||||||
|
const [showImportLinkModal, setShowImportLinkModal] = useState(false);
|
||||||
|
const [nostrImportLink, setNostrImportLink] = useState('');
|
||||||
|
const [isImportingNostr, setIsImportingNostr] = useState(false);
|
||||||
|
const [shareLink, setShareLink] = useState('');
|
||||||
|
const [showShareLinkModal, setShowShareLinkModal] = useState(false);
|
||||||
|
const [sharingSessionId, setSharingSessionId] = useState<string | null>(null);
|
||||||
|
const [nostrEnabled, setNostrEnabled] = useState(true);
|
||||||
|
|
||||||
// Search state for debouncing
|
// Search state for debouncing
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||||
@@ -338,6 +371,17 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
loadSessions();
|
loadSessions();
|
||||||
}, [loadSessions]);
|
}, [loadSessions]);
|
||||||
|
|
||||||
|
// Hide Nostr sharing when tunnel is disabled (restricted/enterprise bundles)
|
||||||
|
useEffect(() => {
|
||||||
|
getTunnelStatus()
|
||||||
|
.then(({ data }) => {
|
||||||
|
if (data?.state === 'disabled') {
|
||||||
|
setNostrEnabled(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Timing logic to prevent flicker between skeleton and content on initial load
|
// Timing logic to prevent flicker between skeleton and content on initial load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && showSkeleton) {
|
if (!isLoading && showSkeleton) {
|
||||||
@@ -542,10 +586,62 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
toast.success(intl.formatMessage(i18n.exportSuccess));
|
toast.success(intl.formatMessage(i18n.exportSuccess));
|
||||||
}, [intl]);
|
}, [intl]);
|
||||||
|
|
||||||
|
const handleShareSessionNostr = useCallback(
|
||||||
|
async (session: Session, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSharingSessionId(session.id);
|
||||||
|
try {
|
||||||
|
const response = await shareSessionNostr({
|
||||||
|
path: { session_id: session.id },
|
||||||
|
body: {},
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
setShareLink(response.data.deeplink);
|
||||||
|
setShowShareLinkModal(true);
|
||||||
|
toast.success(intl.formatMessage(i18n.shareNostrSuccess));
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(intl.formatMessage(i18n.shareNostrFailed, { error: errorMessage(error, 'Unknown error') }));
|
||||||
|
} finally {
|
||||||
|
setSharingSessionId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[intl]
|
||||||
|
);
|
||||||
|
|
||||||
const handleImportClick = useCallback(() => {
|
const handleImportClick = useCallback(() => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleImportNostrLink = useCallback(async () => {
|
||||||
|
const deeplink = nostrImportLink.trim();
|
||||||
|
if (!deeplink) return;
|
||||||
|
|
||||||
|
setIsImportingNostr(true);
|
||||||
|
try {
|
||||||
|
await importSessionNostr({
|
||||||
|
body: { deeplink },
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
setNostrImportLink('');
|
||||||
|
setShowImportLinkModal(false);
|
||||||
|
toast.success(intl.formatMessage(i18n.importSuccess));
|
||||||
|
await loadSessions();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(intl.formatMessage(i18n.importFailed, { error: errorMessage(error, 'Unknown error') }));
|
||||||
|
} finally {
|
||||||
|
setIsImportingNostr(false);
|
||||||
|
}
|
||||||
|
}, [intl, loadSessions, nostrImportLink]);
|
||||||
|
|
||||||
|
const handleCopyShareLink = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareLink);
|
||||||
|
toast.success(intl.formatMessage(i18n.copied));
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`Failed to copy: ${errorMessage(error, 'Unknown error')}`);
|
||||||
|
}
|
||||||
|
}, [intl, shareLink]);
|
||||||
|
|
||||||
const handleImportSession = useCallback(
|
const handleImportSession = useCallback(
|
||||||
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@@ -586,14 +682,18 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
onDuplicateClick,
|
onDuplicateClick,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
onExportClick,
|
onExportClick,
|
||||||
|
onShareClick,
|
||||||
onOpenInNewWindow,
|
onOpenInNewWindow,
|
||||||
|
isSharing,
|
||||||
}: {
|
}: {
|
||||||
session: Session;
|
session: Session;
|
||||||
onEditClick: (session: Session) => void;
|
onEditClick: (session: Session) => void;
|
||||||
onDuplicateClick: (session: Session) => void;
|
onDuplicateClick: (session: Session) => void;
|
||||||
onDeleteClick: (session: Session) => void;
|
onDeleteClick: (session: Session) => void;
|
||||||
onExportClick: (session: Session, e: React.MouseEvent) => void;
|
onExportClick: (session: Session, e: React.MouseEvent) => void;
|
||||||
|
onShareClick: (session: Session, e: React.MouseEvent) => void;
|
||||||
onOpenInNewWindow: (session: Session, e: React.MouseEvent) => void;
|
onOpenInNewWindow: (session: Session, e: React.MouseEvent) => void;
|
||||||
|
isSharing: boolean;
|
||||||
}) {
|
}) {
|
||||||
const handleEditClick = useCallback(
|
const handleEditClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
@@ -630,6 +730,13 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
[onExportClick, session]
|
[onExportClick, session]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleShareClick = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
onShareClick(session, e);
|
||||||
|
},
|
||||||
|
[onShareClick, session]
|
||||||
|
);
|
||||||
|
|
||||||
const handleOpenInNewWindowClick = useCallback(
|
const handleOpenInNewWindowClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
onOpenInNewWindow(session, e);
|
onOpenInNewWindow(session, e);
|
||||||
@@ -736,6 +843,20 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
>
|
>
|
||||||
<Download className="w-3 h-3 text-text-secondary hover:text-text-primary" />
|
<Download className="w-3 h-3 text-text-secondary hover:text-text-primary" />
|
||||||
</button>
|
</button>
|
||||||
|
{nostrEnabled && (
|
||||||
|
<button
|
||||||
|
onClick={handleShareClick}
|
||||||
|
disabled={isSharing}
|
||||||
|
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer disabled:cursor-wait disabled:opacity-60"
|
||||||
|
title={intl.formatMessage(i18n.shareNostrSession)}
|
||||||
|
>
|
||||||
|
{isSharing ? (
|
||||||
|
<LoaderCircle className="w-3 h-3 text-text-secondary animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Share2 className="w-3 h-3 text-text-secondary hover:text-text-primary" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -828,7 +949,9 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
onDuplicateClick={handleDuplicateSession}
|
onDuplicateClick={handleDuplicateSession}
|
||||||
onDeleteClick={handleDeleteSession}
|
onDeleteClick={handleDeleteSession}
|
||||||
onExportClick={handleExportSession}
|
onExportClick={handleExportSession}
|
||||||
|
onShareClick={handleShareSessionNostr}
|
||||||
onOpenInNewWindow={handleOpenInNewWindow}
|
onOpenInNewWindow={handleOpenInNewWindow}
|
||||||
|
isSharing={sharingSessionId === session.id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -855,15 +978,28 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
<div className="flex flex-col page-transition">
|
<div className="flex flex-col page-transition">
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<h1 className="text-4xl font-light">{intl.formatMessage(i18n.chatHistory)}</h1>
|
<h1 className="text-4xl font-light">{intl.formatMessage(i18n.chatHistory)}</h1>
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
onClick={handleImportClick}
|
{nostrEnabled && (
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
onClick={() => setShowImportLinkModal(true)}
|
||||||
className="flex items-center gap-2"
|
variant="outline"
|
||||||
>
|
size="sm"
|
||||||
<Upload className="w-4 h-4" />
|
className="flex items-center gap-2"
|
||||||
{intl.formatMessage(i18n.importSession)}
|
>
|
||||||
</Button>
|
<Share2 className="w-4 h-4" />
|
||||||
|
{intl.formatMessage(i18n.importNostrSession)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={handleImportClick}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
{intl.formatMessage(i18n.importSession)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-text-secondary mb-4">
|
<p className="text-sm text-text-secondary mb-4">
|
||||||
{intl.formatMessage(i18n.chatHistoryDesc, { shortcut: getSearchShortcutText() })}
|
{intl.formatMessage(i18n.chatHistoryDesc, { shortcut: getSearchShortcutText() })}
|
||||||
@@ -957,6 +1093,83 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
onSave={handleModalSave}
|
onSave={handleModalSave}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Dialog open={showImportLinkModal} onOpenChange={setShowImportLinkModal}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Share2 className="w-5 h-5" />
|
||||||
|
{intl.formatMessage(i18n.importNostrTitle)}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>{intl.formatMessage(i18n.importNostrDesc)}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
value={nostrImportLink}
|
||||||
|
onChange={(event) => setNostrImportLink(event.target.value)}
|
||||||
|
placeholder={intl.formatMessage(i18n.importNostrPlaceholder)}
|
||||||
|
className="min-h-28 w-full resize-none rounded-lg border border-border-primary bg-background-primary p-3 text-sm text-text-primary outline-none focus:ring-2 focus:ring-border-active"
|
||||||
|
disabled={isImportingNostr}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowImportLinkModal(false)}
|
||||||
|
disabled={isImportingNostr}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(i18n.cancel)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleImportNostrLink}
|
||||||
|
disabled={isImportingNostr || !nostrImportLink.trim()}
|
||||||
|
>
|
||||||
|
{isImportingNostr ? (
|
||||||
|
<>
|
||||||
|
<LoaderCircle className="w-4 h-4 animate-spin" />
|
||||||
|
{intl.formatMessage(i18n.importing)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
intl.formatMessage(i18n.importSession)
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={showShareLinkModal} onOpenChange={setShowShareLinkModal}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Share2 className="w-5 h-5" />
|
||||||
|
{intl.formatMessage(i18n.shareNostrTitle)}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>{intl.formatMessage(i18n.shareNostrDesc)}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="relative rounded-lg border border-border-primary bg-background-secondary p-3 pr-12">
|
||||||
|
<code className="block max-h-36 overflow-y-auto break-all text-sm text-text-primary">
|
||||||
|
{shareLink}
|
||||||
|
</code>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="absolute right-2 top-2"
|
||||||
|
onClick={handleCopyShareLink}
|
||||||
|
disabled={!shareLink}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
<span className="sr-only">{intl.formatMessage(i18n.copied)}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setShowShareLinkModal(false)}>
|
||||||
|
{intl.formatMessage(i18n.close)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={showDeleteConfirmation}
|
isOpen={showDeleteConfirmation}
|
||||||
title={intl.formatMessage(i18n.deleteTitle)}
|
title={intl.formatMessage(i18n.deleteTitle)}
|
||||||
|
|||||||
@@ -3908,6 +3908,9 @@
|
|||||||
"sessions.action.openNewWindow": {
|
"sessions.action.openNewWindow": {
|
||||||
"defaultMessage": "Open in new window"
|
"defaultMessage": "Open in new window"
|
||||||
},
|
},
|
||||||
|
"sessions.action.shareNostr": {
|
||||||
|
"defaultMessage": "Share encrypted Nostr link"
|
||||||
|
},
|
||||||
"sessions.cancel": {
|
"sessions.cancel": {
|
||||||
"defaultMessage": "Cancel"
|
"defaultMessage": "Cancel"
|
||||||
},
|
},
|
||||||
@@ -3917,6 +3920,9 @@
|
|||||||
"sessions.chatHistoryDesc": {
|
"sessions.chatHistoryDesc": {
|
||||||
"defaultMessage": "View and search your past conversations with Goose. {shortcut} to search."
|
"defaultMessage": "View and search your past conversations with Goose. {shortcut} to search."
|
||||||
},
|
},
|
||||||
|
"sessions.close": {
|
||||||
|
"defaultMessage": "Close"
|
||||||
|
},
|
||||||
"sessions.delete.message": {
|
"sessions.delete.message": {
|
||||||
"defaultMessage": "Are you sure you want to delete the session \"{name}\"? This action cannot be undone."
|
"defaultMessage": "Are you sure you want to delete the session \"{name}\"? This action cannot be undone."
|
||||||
},
|
},
|
||||||
@@ -3947,6 +3953,21 @@
|
|||||||
"sessions.import": {
|
"sessions.import": {
|
||||||
"defaultMessage": "Import Session"
|
"defaultMessage": "Import Session"
|
||||||
},
|
},
|
||||||
|
"sessions.importNostr": {
|
||||||
|
"defaultMessage": "Import Link"
|
||||||
|
},
|
||||||
|
"sessions.importNostr.description": {
|
||||||
|
"defaultMessage": "Paste a Goose Nostr share link to fetch, decrypt, and import the session."
|
||||||
|
},
|
||||||
|
"sessions.importNostr.placeholder": {
|
||||||
|
"defaultMessage": "goose://sessions/nostr?nevent=...&key=..."
|
||||||
|
},
|
||||||
|
"sessions.importNostr.title": {
|
||||||
|
"defaultMessage": "Import Nostr Session"
|
||||||
|
},
|
||||||
|
"sessions.importing": {
|
||||||
|
"defaultMessage": "Importing..."
|
||||||
|
},
|
||||||
"sessions.loadingMore": {
|
"sessions.loadingMore": {
|
||||||
"defaultMessage": "Loading more sessions..."
|
"defaultMessage": "Loading more sessions..."
|
||||||
},
|
},
|
||||||
@@ -3965,6 +3986,15 @@
|
|||||||
"sessions.searchPlaceholder": {
|
"sessions.searchPlaceholder": {
|
||||||
"defaultMessage": "Search history..."
|
"defaultMessage": "Search history..."
|
||||||
},
|
},
|
||||||
|
"sessions.shareNostr.description": {
|
||||||
|
"defaultMessage": "Anyone with this link can fetch and decrypt the session. Treat it like a secret."
|
||||||
|
},
|
||||||
|
"sessions.shareNostr.title": {
|
||||||
|
"defaultMessage": "Encrypted Nostr Share Link"
|
||||||
|
},
|
||||||
|
"sessions.toast.copied": {
|
||||||
|
"defaultMessage": "Copied to clipboard"
|
||||||
|
},
|
||||||
"sessions.toast.deleteFailed": {
|
"sessions.toast.deleteFailed": {
|
||||||
"defaultMessage": "Failed to delete session \"{name}\": {error}"
|
"defaultMessage": "Failed to delete session \"{name}\": {error}"
|
||||||
},
|
},
|
||||||
@@ -3986,6 +4016,12 @@
|
|||||||
"sessions.toast.imported": {
|
"sessions.toast.imported": {
|
||||||
"defaultMessage": "Session imported successfully"
|
"defaultMessage": "Session imported successfully"
|
||||||
},
|
},
|
||||||
|
"sessions.toast.shareNostr": {
|
||||||
|
"defaultMessage": "Encrypted Nostr share link created"
|
||||||
|
},
|
||||||
|
"sessions.toast.shareNostrFailed": {
|
||||||
|
"defaultMessage": "Failed to create Nostr share link: {error}"
|
||||||
|
},
|
||||||
"sessions.toast.updateFailed": {
|
"sessions.toast.updateFailed": {
|
||||||
"defaultMessage": "Failed to update session description: {error}"
|
"defaultMessage": "Failed to update session description: {error}"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -511,7 +511,7 @@ app.on('open-url', async (_event, url) => {
|
|||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
|
|
||||||
log.info('[Main] Received open-url event:', url);
|
log.info('[Main] Received open-url event:', url.includes('key=') ? url.replace(/key=[^&]+/, 'key=REDACTED') : url);
|
||||||
|
|
||||||
await app.whenReady();
|
await app.whenReady();
|
||||||
|
|
||||||
@@ -540,7 +540,7 @@ app.on('open-url', async (_event, url) => {
|
|||||||
|
|
||||||
// For extension/session URLs, store the deep link for processing after React is ready
|
// For extension/session URLs, store the deep link for processing after React is ready
|
||||||
pendingDeepLink = url;
|
pendingDeepLink = url;
|
||||||
log.info('[Main] Stored pending deep link for processing after React ready:', url);
|
log.info('[Main] Stored pending deep link for processing after React ready:', url.includes('key=') ? url.replace(/key=[^&]+/, 'key=REDACTED') : url);
|
||||||
|
|
||||||
const existingWindows = BrowserWindow.getAllWindows();
|
const existingWindows = BrowserWindow.getAllWindows();
|
||||||
if (existingWindows.length > 0) {
|
if (existingWindows.length > 0) {
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import { fetchSharedSessionDetails, SharedSessionDetails } from './sharedSessions';
|
import { fetchSharedSessionDetails, SharedSessionDetails } from './sharedSessions';
|
||||||
import { View, ViewOptions } from './utils/navigationUtils';
|
import { View, ViewOptions } from './utils/navigationUtils';
|
||||||
import { errorMessage } from './utils/conversionUtils';
|
import { errorMessage } from './utils/conversionUtils';
|
||||||
|
import { importSessionNostr } from './api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports a session from an encrypted Nostr deep link.
|
||||||
|
* Separated from shared-session handling so callers can route independently.
|
||||||
|
*/
|
||||||
|
export async function importNostrSessionFromDeepLink(url: string): Promise<void> {
|
||||||
|
await importSessionNostr({
|
||||||
|
body: { deeplink: url },
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles opening a shared session from a deep link
|
* Handles opening a shared session from a deep link
|
||||||
|
|||||||
Reference in New Issue
Block a user