diff --git a/Cargo.lock b/Cargo.lock index 04c95e8..34f42c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,7 +102,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -346,7 +346,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -422,7 +422,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -595,7 +595,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -615,7 +615,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -747,7 +747,7 @@ checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -847,7 +847,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -952,7 +952,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.106", + "syn 2.0.107", "tempfile", "toml 0.8.23", ] @@ -1074,18 +1074,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "client_keys" -version = "0.2.11" -dependencies = [ - "anyhow", - "gpui", - "log", - "nostr-sdk", - "smallvec", - "states", -] - [[package]] name = "cmake" version = "0.1.54" @@ -1169,7 +1157,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1282,7 +1270,6 @@ dependencies = [ "anyhow", "assets", "auto_update", - "client_keys", "common", "dirs 5.0.1", "flume", @@ -1547,7 +1534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1603,17 +1590,17 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1692,7 +1679,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1829,7 +1816,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1849,7 +1836,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1972,7 +1959,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -2166,7 +2153,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -2320,7 +2307,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -2506,7 +2493,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.1" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2601,18 +2588,18 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "anyhow", "gpui", @@ -2841,7 +2828,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "anyhow", "async-compression", @@ -2866,7 +2853,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3210,7 +3197,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3660,7 +3647,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -3907,7 +3894,7 @@ dependencies = [ [[package]] name = "nostr" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435" +source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" dependencies = [ "aes", "base64", @@ -3931,7 +3918,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435" +source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" dependencies = [ "async-utility", "nostr", @@ -3943,7 +3930,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435" +source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" dependencies = [ "flatbuffers", "lru", @@ -3954,7 +3941,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435" +source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" dependencies = [ "async-utility", "flume", @@ -3968,7 +3955,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435" +source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" dependencies = [ "async-utility", "async-wsocket", @@ -3985,7 +3972,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435" +source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2" dependencies = [ "async-utility", "nostr", @@ -4078,7 +4065,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4346,7 +4333,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4493,7 +4480,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "collections", "serde", @@ -4530,7 +4517,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4565,7 +4552,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4702,7 +4689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4733,7 +4720,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4761,7 +4748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -5103,13 +5090,13 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "derive_refineable", ] @@ -5150,6 +5137,7 @@ dependencies = [ "anyhow", "common", "flume", + "futures", "fuzzy-matcher", "gpui", "itertools 0.13.0", @@ -5158,6 +5146,8 @@ dependencies = [ "nostr-lmdb", "nostr-sdk", "rustls", + "serde", + "serde_json", "settings", "smallvec", "smol", @@ -5216,7 +5206,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "anyhow", "bytes", @@ -5270,7 +5260,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "arrayvec", "log", @@ -5289,9 +5279,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rust-embed" -version = "8.7.2" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -5300,22 +5290,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.2" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.106", + "syn 2.0.107", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.7.2" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185" dependencies = [ "globset", "sha2", @@ -5350,7 +5340,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -5616,7 +5606,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -5737,7 +5727,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "anyhow", "serde", @@ -5776,7 +5766,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -5787,7 +5777,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -5834,7 +5824,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -6096,7 +6086,7 @@ checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" dependencies = [ "proc-macro-error2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -6157,7 +6147,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -6169,7 +6159,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -6181,7 +6171,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "arrayvec", "log", @@ -6306,9 +6296,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" dependencies = [ "proc-macro2", "quote", @@ -6341,7 +6331,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -6516,7 +6506,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -6527,7 +6517,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -6686,7 +6676,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -6906,7 +6896,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -7217,7 +7207,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "anyhow", "async-fs", @@ -7252,11 +7242,11 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2" +source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2" dependencies = [ "perf", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -7432,7 +7422,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "wasm-bindgen-shared", ] @@ -7467,7 +7457,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7825,7 +7815,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -7836,7 +7826,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -7847,7 +7837,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -7858,7 +7848,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -8465,7 +8455,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "synstructure", ] @@ -8512,7 +8502,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "zbus_names", "zvariant", "zvariant_utils", @@ -8660,7 +8650,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -8680,7 +8670,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "synstructure", ] @@ -8701,7 +8691,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -8734,7 +8724,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -8785,7 +8775,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "zvariant_utils", ] @@ -8798,6 +8788,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", + "syn 2.0.107", "winnow", ] diff --git a/crates/client_keys/Cargo.toml b/crates/client_keys/Cargo.toml deleted file mode 100644 index dafecba..0000000 --- a/crates/client_keys/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "client_keys" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -states = { path = "../states" } - -nostr-sdk.workspace = true -gpui.workspace = true -anyhow.workspace = true -log.workspace = true -smallvec.workspace = true diff --git a/crates/client_keys/src/lib.rs b/crates/client_keys/src/lib.rs deleted file mode 100644 index 5428fe3..0000000 --- a/crates/client_keys/src/lib.rs +++ /dev/null @@ -1,144 +0,0 @@ -use gpui::{App, AppContext, Context, Entity, Global, Subscription, Window}; -use nostr_sdk::prelude::*; -use smallvec::{smallvec, SmallVec}; -use states::constants::KEYRING_URL; -use states::paths::config_dir; - -pub fn init(cx: &mut App) { - ClientKeys::set_global(cx.new(ClientKeys::new), cx); -} - -struct GlobalClientKeys(Entity); - -impl Global for GlobalClientKeys {} - -pub struct ClientKeys { - keys: Option, - #[allow(dead_code)] - subscriptions: SmallVec<[Subscription; 1]>, -} - -impl ClientKeys { - /// Retrieve the Global Client Keys instance - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - /// Retrieve the Client Keys instance - pub fn read_global(cx: &App) -> &Self { - cx.global::().0.read(cx) - } - - /// Set the Global Client Keys instance - pub(crate) fn set_global(state: Entity, cx: &mut App) { - cx.set_global(GlobalClientKeys(state)); - } - - pub(crate) fn new(cx: &mut Context) -> Self { - let mut subscriptions = smallvec![]; - - subscriptions.push(cx.observe_new::(|this, window, cx| { - if let Some(window) = window { - this.load(window, cx); - } - })); - - Self { - keys: None, - subscriptions, - } - } - - pub fn load(&mut self, window: &mut Window, cx: &mut Context) { - // Prevent macOS from asking for password every time - // Only for debug builds - if cfg!(debug_assertions) && cfg!(target_os = "macos") { - log::warn!("Running debug build on macOS"); - log::warn!("Skipping keychain access, generating new client keys"); - self.new_keys(cx); - return; - } - - let read_client_keys = cx.read_credentials(KEYRING_URL); - - cx.spawn_in(window, async move |this, cx| { - if let Ok(Some((_, secret))) = read_client_keys.await { - // Update the client keys with the stored secret key from the keychain - this.update(cx, |this, cx| { - let Ok(secret_key) = SecretKey::from_slice(&secret) else { - this.set_keys(None, false, true, cx); - return; - }; - let keys = Keys::new(secret_key); - this.set_keys(Some(keys), false, true, cx); - }) - .ok(); - } else if Self::first_run() { - // If this is the first run, generate new keys and use them for the client keys - this.update(cx, |this, cx| { - this.new_keys(cx); - }) - .ok(); - } else { - this.update(cx, |this, cx| { - this.set_keys(None, false, true, cx); - }) - .ok(); - } - }) - .detach(); - } - - pub(crate) fn set_keys( - &mut self, - keys: Option, - persist: bool, - notify: bool, - cx: &mut Context, - ) { - if persist { - if let Some(keys) = keys.as_ref() { - let username = keys.public_key().to_hex(); - let password = keys.secret_key().secret_bytes(); - let write_keys = cx.write_credentials(KEYRING_URL, &username, &password); - - cx.background_spawn(async move { - if let Err(e) = write_keys.await { - log::error!("Failed to save the client keys: {e}") - } - }) - .detach(); - } - } - - self.keys = keys; - - // Notify GPUI to reload UI - if notify { - cx.notify(); - } - } - - pub fn new_keys(&mut self, cx: &mut Context) { - self.set_keys(Some(Keys::generate()), true, true, cx); - } - - pub fn force_new_keys(&mut self, cx: &mut Context) { - self.set_keys(Some(Keys::generate()), true, false, cx); - } - - pub fn keys(&self) -> Keys { - self.keys - .clone() - .expect("Keys should always be initialized") - } - - pub fn has_keys(&self) -> bool { - self.keys.is_some() - } - - fn first_run() -> bool { - let flag = config_dir().join(".first_run"); - !flag.exists() && std::fs::write(&flag, "").is_ok() - } -} diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index be90563..d267aff 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -35,7 +35,6 @@ common = { path = "../common" } states = { path = "../states" } registry = { path = "../registry" } settings = { path = "../settings" } -client_keys = { path = "../client_keys" } auto_update = { path = "../auto_update" } rust-i18n.workspace = true diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs index 7c51dfb..ea6d43d 100644 --- a/crates/coop/src/actions.rs +++ b/crates/coop/src/actions.rs @@ -1,7 +1,10 @@ use std::sync::Mutex; -use gpui::{actions, App}; +use gpui::{actions, App, AppContext}; use nostr_connect::prelude::*; +use registry::keystore::KeyItem; +use registry::Registry; +use states::app_state; actions!(coop, [ReloadMetadata, DarkMode, Settings, Logout, Quit]); actions!(sidebar, [Reload, RelayStatus]); @@ -43,6 +46,36 @@ pub fn load_embedded_fonts(cx: &App) { .unwrap(); } +pub fn reset(cx: &mut App) { + let registry = Registry::global(cx); + let keystore = registry.read(cx).keystore(); + + cx.spawn(async move |cx| { + cx.background_spawn(async move { + let client = app_state().client(); + client.unset_signer().await; + }) + .await; + + keystore + .delete_credentials(&KeyItem::User.to_string(), cx) + .await + .ok(); + + keystore + .delete_credentials(&KeyItem::Bunker.to_string(), cx) + .await + .ok(); + + registry + .update(cx, |this, cx| { + this.reset(cx); + }) + .ok(); + }) + .detach(); +} + pub fn quit(_: &Quit, cx: &mut App) { log::info!("Gracefully quitting the application . . ."); cx.quit(); diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index cb68e08..064d810 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use anyhow::{anyhow, Error}; use auto_update::AutoUpdater; -use client_keys::ClientKeys; use common::display::RenderedProfile; use common::event::EventUtils; use gpui::prelude::FluentBuilder; @@ -17,10 +16,11 @@ use i18n::{shared_t, t}; use itertools::Itertools; use nostr_connect::prelude::*; use nostr_sdk::prelude::*; +use registry::keystore::KeyItem; use registry::{Registry, RegistryEvent}; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; -use states::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH}; +use states::constants::{BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH}; use states::state::{AuthRequest, SignalKind, UnwrappingStatus}; use states::{app_state, default_nip17_relays, default_nip65_relays}; use theme::{ActiveTheme, Theme, ThemeMode}; @@ -36,7 +36,7 @@ use ui::notification::Notification; use ui::popup_menu::PopupMenuExt; use ui::{h_flex, v_flex, ContextModal, Disableable, IconName, Root, Sizable, StyledExt}; -use crate::actions::{DarkMode, Logout, ReloadMetadata, Settings}; +use crate::actions::{reset, DarkMode, Logout, ReloadMetadata, Settings}; use crate::views::compose::compose_button; use crate::views::setup_relay::SetupRelay; use crate::views::{ @@ -82,7 +82,6 @@ pub struct ChatSpace { impl ChatSpace { pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let client_keys = ClientKeys::global(cx); let registry = Registry::global(cx); let status = registry.read(cx).unwrapping_status.clone(); @@ -101,20 +100,46 @@ impl ChatSpace { ); subscriptions.push( - // Observe the client keys and show an alert modal if they fail to initialize - cx.observe_in(&client_keys, window, |this, keys, window, cx| { - if !keys.read(cx).has_keys() { - this.render_client_keys_modal(window, cx); - } else { - this.load_local_account(window, cx); + // Observe the keystore + cx.observe_in(®istry, window, |this, registry, window, cx| { + let has_keyring = registry.read(cx).initialized_keystore; + let use_filestore = registry.read(cx).is_using_file_keystore(); + let not_logged_in = registry.read(cx).signer_pubkey().is_none(); + + if use_filestore && not_logged_in { + this.render_keyring_installation(window, cx); + } + + if has_keyring && not_logged_in { + let keystore = registry.read(cx).keystore(); + + cx.spawn_in(window, async move |this, cx| { + let result = keystore + .read_credentials(&KeyItem::User.to_string(), cx) + .await; + + this.update_in(cx, |this, window, cx| { + match result { + Ok(Some((user, secret))) => { + let public_key = PublicKey::parse(&user).unwrap(); + let secret = String::from_utf8(secret).unwrap(); + this.set_account_layout(public_key, secret, window, cx); + } + _ => { + this.set_onboarding_layout(window, cx); + } + }; + }) + .ok(); + }) + .detach(); } }), ); subscriptions.push( - // Observe the global registry + // Observe the global registry's events cx.observe_in(&status, window, move |this, status, window, cx| { - let registry = Registry::global(cx); let status = status.read(cx); let all_panels = this.get_all_panel_ids(cx); @@ -122,7 +147,7 @@ impl ChatSpace { status, UnwrappingStatus::Processing | UnwrappingStatus::Complete ) { - registry.update(cx, |this, cx| { + Registry::global(cx).update(cx, |this, cx| { this.load_rooms(window, cx); this.refresh_rooms(all_panels, cx); }); @@ -510,12 +535,12 @@ impl ChatSpace { fn set_account_layout( &mut self, + public_key: PublicKey, secret: String, - profile: Profile, window: &mut Window, cx: &mut Context, ) { - let panel = Arc::new(account::init(profile, secret, window, cx)); + let panel = Arc::new(account::init(public_key, secret, window, cx)); let center = DockItem::panel(panel); self.dock.update(cx, |this, cx| { @@ -556,44 +581,6 @@ impl ChatSpace { cx.notify(); } - fn load_local_account(&mut self, window: &mut Window, cx: &mut Context) { - let task = cx.background_spawn(async move { - let client = app_state().client(); - - let filter = Filter::new() - .kind(Kind::ApplicationSpecificData) - .identifier(ACCOUNT_IDENTIFIER) - .limit(1); - - if let Some(event) = client.database().query(filter).await?.first_owned() { - let metadata = client - .database() - .metadata(event.pubkey) - .await? - .unwrap_or_default(); - - Ok((event.content, Profile::new(event.pubkey, metadata))) - } else { - Err(anyhow!("Empty")) - } - }); - - cx.spawn_in(window, async move |this, cx| { - if let Ok((secret, profile)) = task.await { - this.update_in(cx, |this, window, cx| { - this.set_account_layout(secret, profile, window, cx); - }) - .ok(); - } else { - this.update_in(cx, |this, window, cx| { - this.set_onboarding_layout(window, cx); - }) - .ok(); - } - }) - .detach(); - } - fn on_settings(&mut self, _ev: &Settings, window: &mut Window, cx: &mut Context) { let view = preferences::init(window, cx); @@ -659,24 +646,7 @@ impl ChatSpace { } fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context) { - cx.background_spawn(async move { - let states = app_state(); - let client = states.client(); - - let filter = Filter::new() - .kind(Kind::ApplicationSpecificData) - .identifier(ACCOUNT_IDENTIFIER); - - // Delete account - client.database().delete(filter).await.ok(); - - // Reset the nostr client - client.reset().await; - - // Notify the channel about the signer being unset - states.signal().send(SignalKind::SignerUnset).await; - }) - .detach(); + reset(cx); } fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context) { @@ -734,6 +704,32 @@ impl ChatSpace { } } + fn render_keyring_installation(&mut self, window: &mut Window, cx: &mut App) { + window.open_modal(cx, move |this, _window, cx| { + this.overlay_closable(false) + .show_close(false) + .keyboard(false) + .alert() + .button_props(ModalButtonProps::default().ok_text(t!("common.continue"))) + .title(shared_t!("keyring_disable.label")) + .child( + v_flex() + .gap_2() + .text_sm() + .child(shared_t!("keyring_disable.body_1")) + .child(shared_t!("keyring_disable.body_2")) + .child(shared_t!("keyring_disable.body_3")) + .child(shared_t!("keyring_disable.body_4")) + .child( + div() + .text_xs() + .text_color(cx.theme().danger_foreground) + .child(shared_t!("keyring_disable.body_5")), + ), + ) + }); + } + fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) { let relays = default_nip65_relays(); @@ -936,53 +932,6 @@ impl ChatSpace { }) } - fn render_client_keys_modal(&mut self, window: &mut Window, cx: &mut Context) { - window.open_modal(cx, move |this, _window, cx| { - this.overlay_closable(false) - .show_close(false) - .keyboard(false) - .confirm() - .button_props( - ModalButtonProps::default() - .cancel_text(t!("startup.create_new_keys")) - .ok_text(t!("common.allow")), - ) - .child( - div() - .w_full() - .h_40() - .flex() - .flex_col() - .gap_1() - .items_center() - .justify_center() - .text_center() - .text_sm() - .child( - div() - .font_semibold() - .text_color(cx.theme().text_muted) - .child(shared_t!("startup.client_keys_warning")), - ) - .child(shared_t!("startup.client_keys_desc")), - ) - .on_cancel(|_, _window, cx| { - ClientKeys::global(cx).update(cx, |this, cx| { - this.new_keys(cx); - }); - // true: Close modal - true - }) - .on_ok(|_, window, cx| { - ClientKeys::global(cx).update(cx, |this, cx| { - this.load(window, cx); - }); - // true: Close modal - true - }) - }); - } - fn render_titlebar_left_side( &mut self, _window: &mut Window, diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index a19229a..d5df347 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -83,9 +83,6 @@ fn main() { // Initialize components ui::init(cx); - // Initialize client keys - client_keys::init(cx); - // Initialize app registry registry::init(cx); diff --git a/crates/coop/src/views/account.rs b/crates/coop/src/views/account.rs index 7b0ec63..acaa7ca 100644 --- a/crates/coop/src/views/account.rs +++ b/crates/coop/src/views/account.rs @@ -1,66 +1,68 @@ use std::time::Duration; -use anyhow::Error; -use client_keys::ClientKeys; use common::display::RenderedProfile; use gpui::prelude::FluentBuilder; use gpui::{ div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, - WeakEntity, Window, + Window, }; use i18n::{shared_t, t}; use nostr_connect::prelude::*; -use nostr_sdk::prelude::*; +use registry::keystore::KeyItem; +use registry::Registry; use smallvec::{smallvec, SmallVec}; use states::app_state; -use states::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT}; -use states::state::SignalKind; +use states::constants::BUNKER_TIMEOUT; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::indicator::Indicator; -use ui::input::{InputState, TextInput}; -use ui::notification::Notification; use ui::popup_menu::PopupMenu; use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt}; -use crate::actions::CoopAuthUrlHandler; +use crate::actions::{reset, CoopAuthUrlHandler}; pub fn init( - profile: Profile, + public_key: PublicKey, secret: String, window: &mut Window, cx: &mut App, ) -> Entity { - cx.new(|cx| Account::new(secret, profile, window, cx)) + cx.new(|cx| Account::new(public_key, secret, window, cx)) } pub struct Account { - profile: Profile, - stored_secret: String, - is_bunker: bool, + public_key: PublicKey, + secret: String, loading: bool, name: SharedString, focus_handle: FocusHandle, image_cache: Entity, + /// Event subscriptions _subscriptions: SmallVec<[Subscription; 1]>, + + /// Background tasks _tasks: SmallVec<[Task<()>; 1]>, } impl Account { - fn new(secret: String, profile: Profile, window: &mut Window, cx: &mut Context) -> Self { - let is_bunker = secret.starts_with("bunker://"); + fn new( + public_key: PublicKey, + secret: String, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let tasks = smallvec![]; let mut subscriptions = smallvec![]; subscriptions.push( // Clear the local state when user closes the account panel cx.on_release_in(window, move |this, window, cx| { - this.stored_secret.clear(); this.image_cache.update(cx, |this, cx| { this.clear(window, cx); }); @@ -68,212 +70,116 @@ impl Account { ); Self { - profile, - is_bunker, - stored_secret: secret, + public_key, + secret, loading: false, name: "Account".into(), focus_handle: cx.focus_handle(), image_cache: RetainAllImageCache::new(cx), _subscriptions: subscriptions, - _tasks: smallvec![], + _tasks: tasks, } } fn login(&mut self, window: &mut Window, cx: &mut Context) { self.set_loading(true, cx); - if self.is_bunker { - if let Ok(uri) = NostrConnectURI::parse(&self.stored_secret) { - self.nostr_connect(uri, window, cx); - } - } else if let Ok(enc) = EncryptedSecretKey::from_bech32(&self.stored_secret) { - self.keys(enc, window, cx); - } else { - window.push_notification("Cannot continue with current account", cx); - self.set_loading(false, cx); - } - } - - fn nostr_connect(&mut self, uri: NostrConnectURI, window: &mut Window, cx: &mut Context) { - let client_keys = ClientKeys::global(cx); - let app_keys = client_keys.read(cx).keys(); - - let timeout = Duration::from_secs(BUNKER_TIMEOUT); - let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap(); - - // Handle auth url with the default browser - signer.auth_url_handler(CoopAuthUrlHandler); - - self._tasks.push( - // Handle connection in the background - cx.spawn_in(window, async move |this, cx| { - let client = app_state().client(); - - match signer.bunker_uri().await { - Ok(_) => { - // Set the client's signer with the current nostr connect instance - client.set_signer(signer).await; - } - Err(e) => { - this.update_in(cx, |this, window, cx| { - this.set_loading(false, cx); - window.push_notification(Notification::error(e.to_string()), cx); - }) - .ok(); - } + // Try to login with bunker + if self.secret.starts_with("bunker://") { + match NostrConnectURI::parse(&self.secret) { + Ok(uri) => { + self.login_with_bunker(uri, window, cx); } - }), - ); - } - - fn keys(&mut self, enc: EncryptedSecretKey, window: &mut Window, cx: &mut Context) { - let pwd_input: Entity = cx.new(|cx| InputState::new(window, cx).masked(true)); - let weak_input = pwd_input.downgrade(); - - let error: Entity> = cx.new(|_| None); - let weak_error = error.downgrade(); - - let entity = cx.weak_entity(); - - window.open_modal(cx, move |this, _window, cx| { - let entity = entity.clone(); - let entity_clone = entity.clone(); - let weak_input = weak_input.clone(); - let weak_error = weak_error.clone(); - - this.overlay_closable(false) - .show_close(false) - .keyboard(false) - .confirm() - .on_cancel(move |_, _window, cx| { - entity - .update(cx, |this, cx| { - this.set_loading(false, cx); - }) - .ok(); - - // true to close the modal - true - }) - .on_ok(move |_, window, cx| { - let weak_error = weak_error.clone(); - let password = weak_input - .read_with(cx, |state, _cx| state.value().to_owned()) - .ok(); - - entity_clone - .update(cx, |this, cx| { - this.verify_keys(enc, password, weak_error, window, cx); - }) - .ok(); - - // false to keep the modal open - false - }) - .child( - div() - .w_full() - .flex() - .flex_col() - .gap_1() - .text_sm() - .child(shared_t!("login.password_to_decrypt")) - .child(TextInput::new(&pwd_input).small()) - .when_some(error.read(cx).as_ref(), |this, error| { - this.child( - div() - .text_xs() - .italic() - .text_color(cx.theme().danger_foreground) - .child(error.clone()), - ) - }), - ) - }); - } - - fn verify_keys( - &mut self, - enc: EncryptedSecretKey, - password: Option, - error: WeakEntity>, - window: &mut Window, - cx: &mut Context, - ) { - let Some(password) = password else { - error - .update(cx, |this, cx| { - *this = Some("Password is required".into()); - cx.notify(); - }) - .ok(); + Err(e) => { + window.push_notification(e.to_string(), cx); + self.set_loading(false, cx); + } + } return; }; - if password.is_empty() { - error - .update(cx, |this, cx| { - *this = Some("Password cannot be empty".into()); - cx.notify(); - }) - .ok(); - return; + // Fall back to login with keys + match SecretKey::parse(&self.secret) { + Ok(secret) => { + self.login_with_keys(secret, cx); + } + Err(e) => { + window.push_notification(e.to_string(), cx); + self.set_loading(false, cx); + } } + } - let task: Task> = cx.background_spawn(async move { - let secret = enc.decrypt(&password)?; - Ok(secret) - }); + fn login_with_bunker( + &mut self, + uri: NostrConnectURI, + window: &mut Window, + cx: &mut Context, + ) { + let keystore = Registry::global(cx).read(cx).keystore(); - cx.spawn_in(window, async move |_this, cx| { - match task.await { - Ok(secret) => { - cx.update(|window, cx| { - window.close_all_modals(cx); - }) - .ok(); + // Handle connection in the background + cx.spawn_in(window, async move |this, cx| { + let result = keystore + .read_credentials(&KeyItem::Bunker.to_string(), cx) + .await; - let client = app_state().client(); - let keys = Keys::new(secret); + this.update_in(cx, |this, window, cx| { + match result { + Ok(Some((_, content))) => { + let secret = SecretKey::from_slice(&content).unwrap(); + let keys = Keys::new(secret); + let timeout = Duration::from_secs(BUNKER_TIMEOUT); + let mut signer = NostrConnect::new(uri, keys, timeout, None).unwrap(); - // Set the client's signer with the current keys - client.set_signer(keys).await - } - Err(e) => { - error - .update(cx, |this, cx| { - *this = Some(e.to_string().into()); - cx.notify(); - }) - .ok(); - } - }; + // Handle auth url with the default browser + signer.auth_url_handler(CoopAuthUrlHandler); + + // Connect to the remote signer + this._tasks.push( + // Handle connection in the background + cx.spawn_in(window, async move |this, cx| { + let client = app_state().client(); + + match signer.bunker_uri().await { + Ok(_) => { + client.set_signer(signer).await; + } + Err(e) => { + this.update_in(cx, |this, window, cx| { + window.push_notification(e.to_string(), cx); + this.set_loading(false, cx); + }) + .ok(); + } + } + }), + ) + } + Ok(None) => { + window.push_notification(t!("login.keyring_required"), cx); + this.set_loading(false, cx); + } + Err(e) => { + window.push_notification(e.to_string(), cx); + this.set_loading(false, cx); + } + }; + }) + .ok(); }) .detach(); } - fn logout(&mut self, _window: &mut Window, cx: &mut Context) { - self._tasks.push( - // Reset the nostr client in the background - cx.background_spawn(async move { - let states = app_state(); - let client = states.client(); + fn login_with_keys(&mut self, secret: SecretKey, cx: &mut Context) { + let keys = Keys::new(secret); - let filter = Filter::new() - .kind(Kind::ApplicationSpecificData) - .identifier(ACCOUNT_IDENTIFIER); - - // Delete account - client.database().delete(filter).await.ok(); - - // Unset the client's signer - client.unset_signer().await; - - // Notify the channel about the signer being unset - states.signal().send(SignalKind::SignerUnset).await; - }), - ); + // Update the signer + cx.background_spawn(async move { + let client = app_state().client(); + client.set_signer(keys).await; + }) + .detach(); } fn set_loading(&mut self, status: bool, cx: &mut Context) { @@ -310,6 +216,10 @@ impl Focusable for Account { impl Render for Account { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { + let registry = Registry::global(cx); + let profile = registry.read(cx).get_person(&self.public_key, cx); + let bunker = self.secret.starts_with("bunker://"); + v_flex() .image_cache(self.image_cache.clone()) .relative() @@ -367,8 +277,8 @@ impl Render for Account { ) }) .when(!self.loading, |this| { - let avatar = self.profile.avatar(true); - let name = self.profile.display_name(); + let avatar = profile.avatar(true); + let name = profile.display_name(); this.child( h_flex() @@ -381,7 +291,7 @@ impl Render for Account { .child(Avatar::new(avatar).size(rems(1.5))) .child(div().pb_px().font_semibold().child(name)), ) - .child(div().when(self.is_bunker, |this| { + .child(div().when(bunker, |this| { let label = SharedString::from("Nostr Connect"); this.child( @@ -407,9 +317,9 @@ impl Render for Account { Button::new("logout") .label(t!("user.sign_out")) .ghost() - .on_click(cx.listener(move |this, _e, window, cx| { - this.logout(window, cx); - })), + .on_click(|_, _window, cx| { + reset(cx); + }), ), ) } diff --git a/crates/coop/src/views/backup_keys.rs b/crates/coop/src/views/backup_keys.rs index fc7dc11..03e5181 100644 --- a/crates/coop/src/views/backup_keys.rs +++ b/crates/coop/src/views/backup_keys.rs @@ -2,7 +2,6 @@ use std::fs; use std::time::Duration; use dirs::document_dir; -use gpui::prelude::FluentBuilder; use gpui::{ div, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, @@ -15,7 +14,6 @@ use ui::input::{InputState, TextInput}; use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable}; pub struct BackupKeys { - password: Entity, pubkey_input: Entity, secret_input: Entity, error: Option, @@ -27,8 +25,6 @@ impl BackupKeys { let Ok(npub) = keys.public_key.to_bech32(); let Ok(nsec) = keys.secret_key().to_bech32(); - let password = cx.new(|cx| InputState::new(window, cx).masked(true)); - let pubkey_input = cx.new(|cx| { InputState::new(window, cx) .disabled(true) @@ -42,7 +38,6 @@ impl BackupKeys { }); Self { - password, pubkey_input, secret_input, error: None, @@ -50,18 +45,8 @@ impl BackupKeys { } } - pub fn password(&self, cx: &Context) -> String { - self.password.read(cx).value().to_string() - } - pub fn backup(&mut self, window: &mut Window, cx: &mut Context) -> Option> { let document_dir = document_dir().expect("Failed to get document directory"); - let password = self.password.read(cx).value().to_string(); - - if password.is_empty() { - self.set_error(t!("login.password_is_required"), window, cx); - return None; - }; let path = cx.prompt_for_new_path(&document_dir, Some("My Nostr Account")); let nsec = self.secret_input.read(cx).value().to_string(); @@ -190,21 +175,5 @@ impl Render for BackupKeys { .child(shared_t!("new_account.backup_secret_note")), ), ) - .child(divider(cx)) - .child( - v_flex() - .gap_1() - .child(shared_t!("login.set_password")) - .child(TextInput::new(&self.password).small()) - .when_some(self.error.as_ref(), |this, error| { - this.child( - div() - .italic() - .text_xs() - .text_color(cx.theme().danger_foreground) - .child(error.clone()), - ) - }), - ) } } diff --git a/crates/coop/src/views/login.rs b/crates/coop/src/views/login.rs index c863e66..030e453 100644 --- a/crates/coop/src/views/login.rs +++ b/crates/coop/src/views/login.rs @@ -1,23 +1,25 @@ use std::time::Duration; -use client_keys::ClientKeys; +use anyhow::anyhow; use gpui::prelude::FluentBuilder; use gpui::{ div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, - Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, - Window, + Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, }; use i18n::{shared_t, t}; use nostr_connect::prelude::*; +use registry::keystore::KeyItem; +use registry::Registry; use smallvec::{smallvec, SmallVec}; use states::app_state; -use states::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT}; +use states::constants::BUNKER_TIMEOUT; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputEvent, InputState, TextInput}; +use ui::notification::Notification; use ui::popup_menu::PopupMenu; -use ui::{v_flex, ContextModal, Disableable, Sizable, StyledExt}; +use ui::{v_flex, ContextModal, Disableable, StyledExt}; use crate::actions::CoopAuthUrlHandler; @@ -26,15 +28,19 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { } pub struct Login { - input: Entity, + key_input: Entity, + pass_input: Entity, error: Entity>, countdown: Entity>, + require_password: bool, logging_in: bool, - // Panel + + /// Panel name: SharedString, focus_handle: FocusHandle, - #[allow(unused)] - subscriptions: SmallVec<[Subscription; 1]>, + + /// Event subscriptions + _subscriptions: SmallVec<[Subscription; 1]>, } impl Login { @@ -43,29 +49,42 @@ impl Login { } fn view(window: &mut Window, cx: &mut Context) -> Self { - let input = cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://...")); + let key_input = cx.new(|cx| InputState::new(window, cx)); + let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true)); + let error = cx.new(|_| None); let countdown = cx.new(|_| None); let mut subscriptions = smallvec![]; - // Subscribe to key input events and process login when the user presses enter subscriptions.push( - cx.subscribe_in(&input, window, |this, _e, event, window, cx| { - if let InputEvent::PressEnter { .. } = event { - this.login(window, cx); - } + // Subscribe to key input events and process login when the user presses enter + cx.subscribe_in(&key_input, window, |this, input, event, window, cx| { + match event { + InputEvent::PressEnter { .. } => { + this.login(window, cx); + } + InputEvent::Change => { + if input.read(cx).value().starts_with("ncryptsec1") { + this.require_password = true; + cx.notify(); + } + } + _ => {} + }; }), ); Self { - input, + key_input, + pass_input, error, countdown, - subscriptions, name: "Login".into(), focus_handle: cx.focus_handle(), logging_in: false, + require_password: false, + _subscriptions: subscriptions, } } @@ -77,197 +96,34 @@ impl Login { // Prevent duplicate login requests self.set_logging_in(true, cx); - // Disable the input - self.input.update(cx, |this, cx| { - this.set_loading(true, cx); - this.set_disabled(true, cx); - }); + let value = self.key_input.read(cx).value(); + let password = self.pass_input.read(cx).value(); - // Content can be secret key or bunker:// - match self.input.read(cx).value().to_string() { - s if s.starts_with("nsec1") => self.ask_for_password(s, window, cx), - s if s.starts_with("ncryptsec1") => self.ask_for_password(s, window, cx), - s if s.starts_with("bunker://") => self.login_with_bunker(s, window, cx), - _ => self.set_error(t!("login.invalid_key"), window, cx), - }; - } - - fn ask_for_password(&mut self, content: String, window: &mut Window, cx: &mut Context) { - let current_view = cx.entity().downgrade(); - let is_ncryptsec = content.starts_with("ncryptsec1"); - - let pwd_input = cx.new(|cx| InputState::new(window, cx).masked(true)); - let weak_pwd_input = pwd_input.downgrade(); - - let confirm_input = cx.new(|cx| InputState::new(window, cx).masked(true)); - let weak_confirm_input = confirm_input.downgrade(); - - window.open_modal(cx, move |this, _window, cx| { - let weak_pwd_input = weak_pwd_input.clone(); - let weak_confirm_input = weak_confirm_input.clone(); - - let view_cancel = current_view.clone(); - let view_ok = current_view.clone(); - - let label: SharedString = if !is_ncryptsec { - t!("login.set_password").into() - } else { - t!("login.password_to_decrypt").into() - }; - - let description: SharedString = if is_ncryptsec { - t!("login.password_description").into() - } else { - t!("login.password_description_full").into() - }; - - this.overlay_closable(false) - .show_close(false) - .keyboard(false) - .confirm() - .on_cancel(move |_, window, cx| { - view_cancel - .update(cx, |this, cx| { - this.set_error(t!("login.password_is_required"), window, cx); - }) - .ok(); - true - }) - .on_ok(move |_, window, cx| { - let value = weak_pwd_input - .read_with(cx, |state, _cx| state.value().to_owned()) - .ok(); - - let confirm = weak_confirm_input - .read_with(cx, |state, _cx| state.value().to_owned()) - .ok(); - - view_ok - .update(cx, |this, cx| { - this.verify_password(value, confirm, is_ncryptsec, window, cx); - }) - .ok(); - true - }) - .child( - div() - .w_full() - .flex() - .flex_col() - .gap_2() - .text_sm() - .child( - div() - .flex() - .flex_col() - .gap_1() - .child(label) - .child(TextInput::new(&pwd_input).small()), - ) - .when(content.starts_with("nsec1"), |this| { - this.child( - div() - .flex() - .flex_col() - .gap_1() - .child(SharedString::new(t!("login.confirm_password"))) - .child(TextInput::new(&confirm_input).small()), - ) - }) - .child( - div() - .text_xs() - .italic() - .text_color(cx.theme().text_placeholder) - .child(description), - ), - ) - }); - } - - fn verify_password( - &mut self, - password: Option, - confirm: Option, - is_ncryptsec: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(password) = password else { - self.set_error(t!("login.password_is_required"), window, cx); - return; - }; - - if password.is_empty() { - self.set_error(t!("login.password_is_required"), window, cx); - return; - } - - // Skip verification if key is ncryptsec - if is_ncryptsec { - self.login_with_keys(password.to_string(), window, cx); - return; - } - - let Some(confirm) = confirm else { - self.set_error(t!("login.must_confirm_password"), window, cx); - return; - }; - - if confirm.is_empty() { - self.set_error(t!("login.must_confirm_password"), window, cx); - return; - } - - if password != confirm { - self.set_error(t!("login.password_not_match"), window, cx); - return; - } - - self.login_with_keys(password.to_string(), window, cx); - } - - fn login_with_keys(&mut self, password: String, window: &mut Window, cx: &mut Context) { - let value = self.input.read(cx).value().to_string(); - - let secret_key = if value.starts_with("nsec1") { - SecretKey::parse(&value).ok() + if value.starts_with("bunker://") { + self.login_with_bunker(&value, window, cx); } else if value.starts_with("ncryptsec1") { - EncryptedSecretKey::from_bech32(&value) - .map(|enc| enc.decrypt(&password).ok()) - .unwrap_or_default() + self.login_with_password(&value, &password, cx); + } else if value.starts_with("nsec1") { + if let Ok(secret) = SecretKey::parse(&value) { + let keys = Keys::new(secret); + self.login_with_keys(keys, cx); + } else { + self.set_error("Invalid", cx); + } } else { - None - }; - - if let Some(secret_key) = secret_key { - let keys = Keys::new(secret_key); - - // Encrypt and save user secret key to disk - self.write_keys_to_disk(&keys, password, cx); - - // Set the client's signer with the current keys - cx.background_spawn(async move { - let client = app_state().client(); - client.set_signer(keys).await; - }) - .detach(); - } else { - self.set_error(t!("login.key_invalid"), window, cx); + self.set_error("Invalid", cx); } } - fn login_with_bunker(&mut self, content: String, window: &mut Window, cx: &mut Context) { + fn login_with_bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context) { let Ok(uri) = NostrConnectURI::parse(content) else { - self.set_error(t!("login.bunker_invalid"), window, cx); + self.set_error(t!("login.bunker_invalid"), cx); return; }; - let client_keys = ClientKeys::global(cx); - let app_keys = client_keys.read(cx).keys(); - + let app_keys = Keys::generate(); let timeout = Duration::from_secs(BUNKER_TIMEOUT); - let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap(); + let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap(); // Handle auth url with the default browser signer.auth_url_handler(CoopAuthUrlHandler); @@ -293,103 +149,152 @@ impl Login { // Handle connection cx.spawn_in(window, async move |this, cx| { - match signer.bunker_uri().await { - Ok(uri) => { - this.update(cx, |this, cx| { - this.write_uri_to_disk(signer, uri, cx); - }) - .ok(); - } - Err(error) => { - this.update_in(cx, |this, window, cx| { - this.set_error(error.to_string(), window, cx); - // Force reset the client keys - // - // This step is necessary to ensure that user can retry the connection - client_keys.update(cx, |this, cx| { - this.force_new_keys(cx); - }); - }) - .ok(); - } - } + let result = signer.bunker_uri().await; + + this.update_in(cx, |this, window, cx| { + match result { + Ok(uri) => { + this.save_connection(&app_keys, &uri, window, cx); + this.connect(signer, cx); + } + Err(e) => { + window.push_notification(Notification::error(e.to_string()), cx); + } + }; + }) + .ok(); }) .detach(); } - fn write_uri_to_disk( + fn save_connection( &mut self, - signer: NostrConnect, - uri: NostrConnectURI, - cx: &mut Context, - ) { - let mut uri_without_secret = uri.to_string(); - - // Clear the secret parameter in the URI if it exists - if let Some(secret) = uri.secret() { - uri_without_secret = uri_without_secret.replace(secret, ""); - } - - let task: Task> = cx.background_spawn(async move { - let client = app_state().client(); - - // Update the client's signer - client.set_signer(signer).await; - - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - let event = EventBuilder::new(Kind::ApplicationSpecificData, uri_without_secret) - .tags(vec![Tag::identifier(ACCOUNT_IDENTIFIER)]) - .build(public_key) - .sign(&Keys::generate()) - .await?; - - // Save the event to the database - client.database().save_event(&event).await?; - - Ok(()) - }); - - task.detach(); - } - - pub fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context) { - let keys = keys.to_owned(); - let public_key = keys.public_key(); - - cx.background_spawn(async move { - if let Ok(enc_key) = - EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown) - { - let client = app_state().client(); - let value = enc_key.to_bech32().unwrap(); - let keys = Keys::generate(); - let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)]; - let kind = Kind::ApplicationSpecificData; - - let builder = EventBuilder::new(kind, value) - .tags(tags) - .build(public_key) - .sign(&keys) - .await; - - if let Ok(event) = builder { - if let Err(e) = client.database().save_event(&event).await { - log::error!("Failed to save event: {e}"); - }; - } - } - }) - .detach(); - } - - fn set_error( - &mut self, - message: impl Into, + keys: &Keys, + uri: &NostrConnectURI, window: &mut Window, cx: &mut Context, ) { + let keystore = Registry::global(cx).read(cx).keystore(); + let username = keys.public_key().to_hex(); + let secret = keys.secret_key().to_secret_bytes(); + let mut clean_uri = uri.to_string(); + + // Clear the secret parameter in the URI if it exists + if let Some(s) = uri.secret() { + clean_uri = clean_uri.replace(s, ""); + } + + cx.spawn_in(window, async move |this, cx| { + let user_url = KeyItem::User.to_string(); + let bunker_url = KeyItem::Bunker.to_string(); + let user_password = clean_uri.into_bytes(); + + // Write bunker uri to keyring for further connection + if let Err(e) = keystore + .write_credentials(&user_url, "bunker", &user_password, cx) + .await + { + this.update_in(cx, |_, window, cx| { + window.push_notification(e.to_string(), cx); + }) + .ok(); + } + + // Write the app keys for further connection + if let Err(e) = keystore + .write_credentials(&bunker_url, &username, &secret, cx) + .await + { + this.update_in(cx, |_, window, cx| { + window.push_notification(e.to_string(), cx); + }) + .ok(); + } + }) + .detach(); + } + + fn connect(&mut self, signer: NostrConnect, cx: &mut Context) { + cx.background_spawn(async move { + let client = app_state().client(); + client.set_signer(signer).await; + }) + .detach(); + } + + pub fn login_with_password(&mut self, content: &str, pwd: &str, cx: &mut Context) { + if pwd.is_empty() { + self.set_error("Password is required", cx); + return; + } + + let Ok(enc) = EncryptedSecretKey::from_bech32(content) else { + self.set_error("Secret Key is invalid", cx); + return; + }; + + let password = pwd.to_owned(); + + // Decrypt in the background to ensure it doesn't block the UI + let task = cx.background_spawn(async move { + if let Ok(content) = enc.decrypt(&password) { + Ok(Keys::new(content)) + } else { + Err(anyhow!("Invalid password")) + } + }); + + cx.spawn(async move |this, cx| { + let result = task.await; + + this.update(cx, |this, cx| { + match result { + Ok(keys) => { + this.login_with_keys(keys, cx); + } + Err(e) => { + this.set_error(e.to_string(), cx); + } + }; + }) + .ok(); + }) + .detach(); + } + + pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context) { + let keystore = Registry::global(cx).read(cx).keystore(); + let username = keys.public_key().to_hex(); + let secret = keys.secret_key().to_secret_hex().into_bytes(); + + cx.spawn(async move |this, cx| { + let bunker_url = KeyItem::User.to_string(); + + // Write the app keys for further connection + if let Err(e) = keystore + .write_credentials(&bunker_url, &username, &secret, cx) + .await + { + this.update(cx, |this, cx| { + this.set_error(e.to_string(), cx); + }) + .ok(); + } + + // Update the signer + cx.background_spawn(async move { + let client = app_state().client(); + client.set_signer(keys).await; + }) + .detach(); + }) + .detach(); + } + + fn set_error(&mut self, message: S, cx: &mut Context) + where + S: Into, + { // Reset the log in state self.set_logging_in(false, cx); @@ -402,13 +307,6 @@ impl Login { cx.notify(); }); - // Re enable the input - self.input.update(cx, |this, cx| { - this.set_value("", window, cx); - this.set_loading(false, cx); - this.set_disabled(false, cx); - }); - // Clear the error message after 3 secs cx.spawn(async move |this, cx| { cx.background_executor().timer(Duration::from_secs(3)).await; @@ -493,7 +391,25 @@ impl Render for Login { .child( v_flex() .gap_3() - .child(TextInput::new(&self.input)) + .text_sm() + .child( + v_flex() + .gap_1() + .text_sm() + .text_color(cx.theme().text_muted) + .child("nsec or bunker://") + .child(TextInput::new(&self.key_input)), + ) + .when(self.require_password, |this| { + this.child( + v_flex() + .gap_1() + .text_sm() + .text_color(cx.theme().text_muted) + .child("Password:") + .child(TextInput::new(&self.pass_input)), + ) + }) .child( Button::new("login") .label(t!("common.continue")) @@ -513,13 +429,13 @@ impl Render for Login { .child(shared_t!("login.approve_message", i = i)), ) }) - .when_some(self.error.read(cx).clone(), |this, error| { + .when_some(self.error.read(cx).as_ref(), |this, error| { this.child( div() .text_xs() .text_center() .text_color(cx.theme().danger_foreground) - .child(error), + .child(error.clone()), ) }), ), diff --git a/crates/coop/src/views/new_account.rs b/crates/coop/src/views/new_account.rs index d845c22..941877c 100644 --- a/crates/coop/src/views/new_account.rs +++ b/crates/coop/src/views/new_account.rs @@ -8,9 +8,11 @@ use gpui::{ use gpui_tokio::Tokio; use i18n::{shared_t, t}; use nostr_sdk::prelude::*; +use registry::keystore::KeyItem; +use registry::Registry; use settings::AppSettings; use smol::fs; -use states::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS}; +use states::constants::BOOTSTRAP_RELAYS; use states::{app_state, default_nip17_relays, default_nip65_relays}; use theme::ActiveTheme; use ui::avatar::Avatar; @@ -81,21 +83,17 @@ impl NewAccount { .on_ok(move |_, window, cx| { weak_view .update(cx, |this, cx| { - let password = this.password(cx); let current_view = current_view.clone(); if let Some(task) = this.backup(window, cx) { cx.spawn_in(window, async move |_, cx| { task.await; - cx.update(|window, cx| { - current_view - .update(cx, |this, cx| { - this.set_signer(password, window, cx); - }) - .ok(); - }) - .ok() + current_view + .update(cx, |this, cx| { + this.set_signer(cx); + }) + .ok(); }) .detach(); } @@ -107,10 +105,13 @@ impl NewAccount { }) } - fn set_signer(&mut self, password: String, window: &mut Window, cx: &mut Context) { - window.close_modal(cx); + pub fn set_signer(&mut self, cx: &mut Context) { + let keystore = Registry::global(cx).read(cx).keystore(); let keys = self.temp_keys.read(cx).clone(); + let username = keys.public_key().to_hex(); + let secret = keys.secret_key().to_secret_hex().into_bytes(); + let avatar = self.avatar_input.read(cx).value().to_string(); let name = self.name_input.read(cx).value().to_string(); let mut metadata = Metadata::new().display_name(name.clone()).name(name); @@ -119,81 +120,59 @@ impl NewAccount { metadata = metadata.picture(url); }; - // Encrypt and save user secret key to disk - self.write_keys_to_disk(&keys, password, cx); + cx.spawn(async move |_, cx| { + let url = KeyItem::User.to_string(); - // Set the client's signer with the current keys - let task: Task> = cx.background_spawn(async move { - let client = app_state().client(); + // Write the app keys for further connection + keystore + .write_credentials(&url, &username, &secret, cx) + .await + .ok(); + // Update the signer // Set the client's signer with the current keys - client.set_signer(keys).await; - - // Verify the signer - let signer = client.signer().await?; - - // Construct a NIP-65 event - let event = EventBuilder::new(Kind::RelayList, "") - .tags(default_nip65_relays().iter().map(|(url, metadata)| { - Tag::relay_metadata(url.to_owned(), metadata.to_owned()) - })) - .sign(&signer) - .await?; - - // Set NIP-65 relays - client.send_event_to(BOOTSTRAP_RELAYS, &event).await?; - - // Construct a NIP-17 event - let event = EventBuilder::new(Kind::InboxRelays, "") - .tags( - default_nip17_relays() - .iter() - .map(|url| Tag::relay(url.to_owned())), - ) - .sign(&signer) - .await?; - - // Set NIP-17 relays - client.send_event(&event).await?; - - // Construct a metadata event - let event = EventBuilder::metadata(&metadata).sign(&signer).await?; - - // Set metadata - client.send_event(&event).await?; - - Ok(()) - }); - - task.detach(); - } - - fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context) { - let keys = keys.to_owned(); - let public_key = keys.public_key(); - - cx.background_spawn(async move { - if let Ok(enc_key) = - EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown) - { + let task: Task> = cx.background_spawn(async move { let client = app_state().client(); - let value = enc_key.to_bech32().unwrap(); - let keys = Keys::generate(); - let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)]; - let kind = Kind::ApplicationSpecificData; - let builder = EventBuilder::new(kind, value) - .tags(tags) - .build(public_key) - .sign(&keys) - .await; + // Set the client's signer with the current keys + client.set_signer(keys).await; - if let Ok(event) = builder { - if let Err(e) = client.database().save_event(&event).await { - log::error!("Failed to save event: {e}"); - }; - } - } + // Verify the signer + let signer = client.signer().await?; + + // Construct a NIP-65 event + let event = EventBuilder::new(Kind::RelayList, "") + .tags( + default_nip65_relays() + .iter() + .cloned() + .map(|(url, metadata)| Tag::relay_metadata(url, metadata)), + ) + .sign(&signer) + .await?; + + // Set NIP-65 relays + client.send_event_to(BOOTSTRAP_RELAYS, &event).await?; + + // Construct a NIP-17 event + let event = EventBuilder::new(Kind::InboxRelays, "") + .tags(default_nip17_relays().iter().cloned().map(Tag::relay)) + .sign(&signer) + .await?; + + // Set NIP-17 relays + client.send_event(&event).await?; + + // Construct a metadata event + let event = EventBuilder::metadata(&metadata).sign(&signer).await?; + + // Set metadata + client.send_event(&event).await?; + + Ok(()) + }); + + task.detach(); }) .detach(); } diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs index 658b832..f64dbd4 100644 --- a/crates/coop/src/views/onboarding.rs +++ b/crates/coop/src/views/onboarding.rs @@ -1,19 +1,20 @@ use std::sync::Arc; use std::time::Duration; -use client_keys::ClientKeys; use common::display::TextUtils; use gpui::prelude::FluentBuilder; use gpui::{ - div, img, px, relative, svg, AnyElement, App, AppContext, ClipboardItem, Context, Entity, - EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement, - Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, + div, img, px, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, + FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement, Render, + SharedString, StatefulInteractiveElement, Styled, Task, Window, }; use i18n::{shared_t, t}; use nostr_connect::prelude::*; +use registry::keystore::KeyItem; +use registry::Registry; use smallvec::{smallvec, SmallVec}; use states::app_state; -use states::constants::{ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT}; +use states::constants::{APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; @@ -21,7 +22,7 @@ use ui::notification::Notification; use ui::popup_menu::PopupMenu; use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; -use crate::chatspace; +use crate::chatspace::{self}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { Onboarding::new(window, cx) @@ -59,14 +60,14 @@ impl NostrConnectApp { } pub struct Onboarding { - nostr_connect_uri: Entity, - nostr_connect: Entity>, - qr_code: Entity>>, - connecting: bool, - // Panel + app_keys: Keys, + qr_code: Option>, + + /// Panel name: SharedString, focus_handle: FocusHandle, - _subscriptions: SmallVec<[Subscription; 2]>, + + /// Background tasks _tasks: SmallVec<[Task<()>; 1]>, } @@ -76,145 +77,101 @@ impl Onboarding { } fn view(window: &mut Window, cx: &mut Context) -> Self { - let nostr_connect = cx.new(|_| None); - let qr_code = cx.new(|_| None); + let app_keys = Keys::generate(); + let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); + + let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(); + let uri = NostrConnectURI::client(app_keys.public_key(), vec![relay], APP_NAME); + let qr_code = uri.to_string().to_qr(); // NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md // // Direct connection initiated by the client - let nostr_connect_uri = cx.new(|cx| { - let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(); - let app_keys = ClientKeys::read_global(cx).keys(); - NostrConnectURI::client(app_keys.public_key(), vec![relay], APP_NAME) - }); + let signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap(); - let mut subscriptions = smallvec![]; + let mut tasks = smallvec![]; - // Clean up when the current view is released - subscriptions.push(cx.on_release_in(window, |this, window, cx| { - this.shutdown_nostr_connect(window, cx); - })); - - // Set Nostr Connect after the view is initialized - cx.defer_in(window, |this, window, cx| { - this.set_connect(window, cx); - }); - - Self { - nostr_connect, - nostr_connect_uri, - qr_code, - connecting: false, - name: "Onboarding".into(), - focus_handle: cx.focus_handle(), - _subscriptions: subscriptions, - _tasks: smallvec![], - } - } - - fn set_connecting(&mut self, cx: &mut Context) { - self.connecting = true; - cx.notify(); - } - - fn set_connect(&mut self, window: &mut Window, cx: &mut Context) { - let uri = self.nostr_connect_uri.read(cx).clone(); - let app_keys = ClientKeys::read_global(cx).keys(); - let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - - self.qr_code.update(cx, |this, cx| { - *this = uri.to_string().to_qr(); - cx.notify(); - }); - - self.nostr_connect.update(cx, |this, cx| { - *this = NostrConnect::new(uri, app_keys, timeout, None).ok(); - cx.notify(); - }); - - self._tasks.push( - // Wait for Nostr Connect approval + tasks.push( + // Wait for nostr connect cx.spawn_in(window, async move |this, cx| { - let connect = this.read_with(cx, |this, cx| this.nostr_connect.read(cx).clone()); + let result = signer.bunker_uri().await; - if let Ok(Some(signer)) = connect { - match signer.bunker_uri().await { + this.update_in(cx, |this, window, cx| { + match result { Ok(uri) => { - this.update(cx, |this, cx| { - this.set_connecting(cx); - this.write_uri_to_disk(signer, uri, cx); - }) - .ok(); + this.save_connection(&uri, window, cx); + this.connect(signer, cx); } Err(e) => { - this.update_in(cx, |_, window, cx| { - window.push_notification( - Notification::error(e.to_string()).title("Nostr Connect"), - cx, - ); - }) - .ok(); + window.push_notification(Notification::error(e.to_string()), cx); } }; - } + }) + .ok(); }), - ) + ); + + Self { + qr_code, + app_keys, + name: "Onboarding".into(), + focus_handle: cx.focus_handle(), + _tasks: tasks, + } } - fn write_uri_to_disk( + fn save_connection( &mut self, - signer: NostrConnect, - uri: NostrConnectURI, + uri: &NostrConnectURI, + window: &mut Window, cx: &mut Context, ) { - let mut uri_without_secret = uri.to_string(); + let keystore = Registry::global(cx).read(cx).keystore(); + let username = self.app_keys.public_key().to_hex(); + let secret = self.app_keys.secret_key().to_secret_bytes(); + let mut clean_uri = uri.to_string(); // Clear the secret parameter in the URI if it exists - if let Some(secret) = uri.secret() { - uri_without_secret = uri_without_secret.replace(secret, ""); + if let Some(s) = uri.secret() { + clean_uri = clean_uri.replace(s, ""); } - let task: Task> = cx.background_spawn(async move { - let client = app_state().client(); + cx.spawn_in(window, async move |this, cx| { + let user_url = KeyItem::User.to_string(); + let bunker_url = KeyItem::Bunker.to_string(); + let user_password = clean_uri.into_bytes(); - // Update the client's signer - client.set_signer(signer).await; - - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - let event = EventBuilder::new(Kind::ApplicationSpecificData, uri_without_secret) - .tags(vec![Tag::identifier(ACCOUNT_IDENTIFIER)]) - .build(public_key) - .sign(&Keys::generate()) - .await?; - - // Save the event to the database - client.database().save_event(&event).await?; - - Ok(()) - }); - - task.detach(); - } - - fn copy_uri(&mut self, window: &mut Window, cx: &mut Context) { - cx.write_to_clipboard(ClipboardItem::new_string( - self.nostr_connect_uri.read(cx).to_string(), - )); - window.push_notification(t!("common.copied"), cx); - } - - fn shutdown_nostr_connect(&mut self, _window: &mut Window, cx: &mut App) { - if !self.connecting { - if let Some(signer) = self.nostr_connect.read(cx).clone() { - cx.background_spawn(async move { - log::info!("Shutting down Nostr Connect"); - signer.shutdown().await; + // Write bunker uri to keyring for further connection + if let Err(e) = keystore + .write_credentials(&user_url, "bunker", &user_password, cx) + .await + { + this.update_in(cx, |_, window, cx| { + window.push_notification(e.to_string(), cx); }) - .detach(); + .ok(); } - } + + // Write the app keys for further connection + if let Err(e) = keystore + .write_credentials(&bunker_url, &username, &secret, cx) + .await + { + this.update_in(cx, |_, window, cx| { + window.push_notification(e.to_string(), cx); + }) + .ok(); + } + }) + .detach(); + } + + fn connect(&mut self, signer: NostrConnect, cx: &mut Context) { + cx.background_spawn(async move { + let client = app_state().client(); + client.set_signer(signer).await; + }) + .detach(); } fn render_apps(&self, cx: &Context) -> impl IntoIterator { @@ -368,23 +325,14 @@ impl Render for Onboarding { .gap_5() .items_center() .justify_center() - .when_some(self.qr_code.read(cx).as_ref(), |this, qr| { + .when_some(self.qr_code.as_ref(), |this, qr| { this.child( - div() - .id("") - .child( - img(qr.clone()) - .size(px(256.)) - .rounded_xl() - .shadow_lg() - .border_1() - .border_color(cx.theme().element_active), - ) - .on_click(cx.listener( - move |this, _e, window, cx| { - this.copy_uri(window, cx) - }, - )), + img(qr.clone()) + .size(px(256.)) + .rounded_xl() + .shadow_lg() + .border_1() + .border_color(cx.theme().element_active), ) }) .child( diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index eeebbd6..158757c 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -19,6 +19,9 @@ smallvec.workspace = true smol.workspace = true log.workspace = true flume.workspace = true +futures.workspace = true +serde.workspace = true +serde_json.workspace = true fuzzy-matcher = "0.3.7" rustls = "0.23.23" diff --git a/crates/registry/src/keystore.rs b/crates/registry/src/keystore.rs new file mode 100644 index 0000000..4217c96 --- /dev/null +++ b/crates/registry/src/keystore.rs @@ -0,0 +1,191 @@ +use std::any::Any; +use std::collections::HashMap; +use std::fmt::Display; +use std::future::Future; +use std::path::PathBuf; +use std::pin::Pin; + +use anyhow::Result; +use futures::FutureExt as _; +use gpui::AsyncApp; +use states::paths::config_dir; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum KeyItem { + User, + Bunker, + Client, + Encryption, +} + +impl Display for KeyItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::User => write!(f, "coop-user"), + Self::Bunker => write!(f, "coop-bunker"), + Self::Client => write!(f, "coop-client"), + Self::Encryption => write!(f, "coop-encryption"), + } + } +} + +impl From for String { + fn from(item: KeyItem) -> Self { + item.to_string() + } +} + +pub trait KeyStore: Any + Send + Sync { + fn name(&self) -> &str; + + /// Reads the credentials from the provider. + #[allow(clippy::type_complexity)] + fn read_credentials<'a>( + &'a self, + url: &'a str, + cx: &'a AsyncApp, + ) -> Pin)>>> + 'a>>; + + /// Writes the credentials to the provider. + fn write_credentials<'a>( + &'a self, + url: &'a str, + username: &'a str, + password: &'a [u8], + cx: &'a AsyncApp, + ) -> Pin> + 'a>>; + + /// Deletes the credentials from the provider. + fn delete_credentials<'a>( + &'a self, + url: &'a str, + cx: &'a AsyncApp, + ) -> Pin> + 'a>>; +} + +/// A credentials provider that stores credentials in the system keychain. +pub struct KeyringProvider; + +impl KeyStore for KeyringProvider { + fn name(&self) -> &str { + "keyring" + } + + fn read_credentials<'a>( + &'a self, + url: &'a str, + cx: &'a AsyncApp, + ) -> Pin)>>> + 'a>> { + async move { cx.update(|cx| cx.read_credentials(url))?.await }.boxed_local() + } + + fn write_credentials<'a>( + &'a self, + url: &'a str, + username: &'a str, + password: &'a [u8], + cx: &'a AsyncApp, + ) -> Pin> + 'a>> { + async move { + cx.update(move |cx| cx.write_credentials(url, username, password))? + .await + } + .boxed_local() + } + + fn delete_credentials<'a>( + &'a self, + url: &'a str, + cx: &'a AsyncApp, + ) -> Pin> + 'a>> { + async move { cx.update(move |cx| cx.delete_credentials(url))?.await }.boxed_local() + } +} + +/// A credentials provider that stores credentials in a local file. +pub struct FileProvider { + path: PathBuf, +} + +impl FileProvider { + pub fn new() -> Self { + let path = config_dir().join(".keys"); + + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + + Self { path } + } + + pub fn load_credentials(&self) -> Result)>> { + let json = std::fs::read(&self.path)?; + let credentials: HashMap)> = serde_json::from_slice(&json)?; + + Ok(credentials) + } + + pub fn save_credentials(&self, credentials: &HashMap)>) -> Result<()> { + let json = serde_json::to_string(credentials)?; + std::fs::write(&self.path, json)?; + + Ok(()) + } +} + +impl Default for FileProvider { + fn default() -> Self { + Self::new() + } +} + +impl KeyStore for FileProvider { + fn name(&self) -> &str { + "file" + } + + fn read_credentials<'a>( + &'a self, + url: &'a str, + _cx: &'a AsyncApp, + ) -> Pin)>>> + 'a>> { + async move { + Ok(self + .load_credentials() + .unwrap_or_default() + .get(url) + .cloned()) + } + .boxed_local() + } + + fn write_credentials<'a>( + &'a self, + url: &'a str, + username: &'a str, + password: &'a [u8], + _cx: &'a AsyncApp, + ) -> Pin> + 'a>> { + async move { + let mut credentials = self.load_credentials().unwrap_or_default(); + credentials.insert(url.to_string(), (username.to_string(), password.to_vec())); + + self.save_credentials(&credentials) + } + .boxed_local() + } + + fn delete_credentials<'a>( + &'a self, + url: &'a str, + _cx: &'a AsyncApp, + ) -> Pin> + 'a>> { + async move { + let mut credentials = self.load_credentials()?; + credentials.remove(url); + + self.save_credentials(&credentials) + } + .boxed_local() + } +} diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs index cb53f77..70bf62f 100644 --- a/crates/registry/src/lib.rs +++ b/crates/registry/src/lib.rs @@ -1,5 +1,6 @@ use std::cmp::Reverse; use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, LazyLock}; use anyhow::Error; use common::event::EventUtils; @@ -14,13 +15,19 @@ use room::RoomKind; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use states::app_state; +use states::constants::KEYRING_URL; use states::state::UnwrappingStatus; +use crate::keystore::{FileProvider, KeyStore, KeyringProvider}; use crate::room::Room; +pub mod keystore; pub mod message; pub mod room; +pub static DISABLE_KEYRING: LazyLock = + LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty())); + pub fn init(cx: &mut App) { Registry::set_global(cx.new(Registry::new), cx); } @@ -36,7 +43,6 @@ pub enum RegistryEvent { NewRequest(RoomKind), } -#[derive(Debug)] pub struct Registry { /// Collection of all chat rooms pub rooms: Vec>, @@ -47,11 +53,17 @@ pub struct Registry { /// Status of the unwrapping process pub unwrapping_status: Entity, + /// Key Store for storing credentials + pub keystore: Arc, + + /// Whether the keystore has been initialized + pub initialized_keystore: bool, + /// Public Key of the currently activated signer signer_pubkey: Option, /// Tasks for asynchronous operations - _tasks: SmallVec<[Task<()>; 1]>, + _tasks: SmallVec<[Task<()>; 2]>, } impl EventEmitter for Registry {} @@ -75,8 +87,38 @@ impl Registry { /// Create a new registry instance pub(crate) fn new(cx: &mut Context) -> Self { let unwrapping_status = cx.new(|_| UnwrappingStatus::default()); + let read_credential = cx.read_credentials(KEYRING_URL); + let initialized_keystore = cfg!(debug_assertions) || *DISABLE_KEYRING; + let keystore: Arc = if cfg!(debug_assertions) || *DISABLE_KEYRING { + Arc::new(FileProvider::default()) + } else { + Arc::new(KeyringProvider) + }; + let mut tasks = smallvec![]; + if !(cfg!(debug_assertions) || *DISABLE_KEYRING) { + tasks.push( + // Verify the keyring access + cx.spawn(async move |this, cx| { + let result = read_credential.await; + + this.update(cx, |this, cx| { + if let Err(e) = result { + log::error!("Keyring error: {e}"); + // For Linux: + // The user has not installed secret service on their system + // Fall back to the file provider + this.keystore = Arc::new(FileProvider::default()); + } + this.initialized_keystore = true; + cx.notify(); + }) + .ok(); + }), + ); + } + tasks.push( // Load all user profiles from the database cx.spawn(async move |this, cx| { @@ -96,6 +138,8 @@ impl Registry { Self { unwrapping_status, + keystore, + initialized_keystore, rooms: vec![], persons: HashMap::new(), signer_pubkey: None, @@ -122,6 +166,16 @@ impl Registry { }) } + /// Returns the keystore. + pub fn keystore(&self) -> Arc { + Arc::clone(&self.keystore) + } + + /// Returns true if the keystore is a file keystore. + pub fn is_using_file_keystore(&self) -> bool { + self.keystore.name() == "file" + } + /// Returns the public key of the currently activated signer. pub fn signer_pubkey(&self) -> Option { self.signer_pubkey diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 9f8524d..b9d5405 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -50,6 +50,7 @@ setting_accessors! { pub contact_bypass: bool, pub auto_login: bool, pub auto_auth: bool, + pub disable_keyring: bool, } #[derive(Serialize, Deserialize)] @@ -62,6 +63,7 @@ pub struct Settings { pub contact_bypass: bool, pub auto_login: bool, pub auto_auth: bool, + pub disable_keyring: bool, pub authenticated_relays: Vec, } @@ -76,6 +78,7 @@ impl Default for Settings { contact_bypass: true, auto_login: false, auto_auth: true, + disable_keyring: false, authenticated_relays: vec![], } } diff --git a/crates/states/src/constants.rs b/crates/states/src/constants.rs index 1cd8873..99c47c7 100644 --- a/crates/states/src/constants.rs +++ b/crates/states/src/constants.rs @@ -2,9 +2,8 @@ pub const APP_NAME: &str = "Coop"; pub const APP_ID: &str = "su.reya.coop"; pub const APP_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc4MkNFRkQ2RkVGQURGNzUKUldSMTMvcisxdThzZUZraHc4Vno3NVNJek81VkJFUEV3MkJweGFxQXhpekdSU1JIekpqMG4yemMK"; pub const APP_UPDATER_ENDPOINT: &str = "https://coop-updater.reya.su/"; -pub const KEYRING_URL: &str = "Coop Safe Storage"; -pub const ACCOUNT_IDENTIFIER: &str = "coop:user"; +pub const KEYRING_URL: &str = "Coop Safe Storage"; pub const SETTINGS_IDENTIFIER: &str = "coop:settings"; /// Bootstrap Relays. diff --git a/locales/app.yml b/locales/app.yml index 47706a2..607978b 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -60,6 +60,20 @@ common: configure: en: "Configure" +keyring_disable: + label: + en: "Keyring is disabled" + body_1: + en: "Coop cannot access the Keyring Service on your system." + body_2: + en: "By design, Coop uses Keyring to store your credentials." + body_3: + en: "Without access to Keyring, Coop will store your credentials as plain text." + body_4: + en: "If you want to store your credentials in the Keyring, please enable Keyring and allow Coop to access it." + body_5: + en: "By clicking continue, you agree to store your credentials as plain text." + auto_update: updating: en: "Installing the new update..." @@ -102,12 +116,6 @@ onboarding: ext_login_note: en: "You will need to keep your default browser open." -proxy: - label: - en: "Waiting for approval" - description: - en: "Open your default browser and approve the connection request in your Nostr Signer extension" - auth: label: en: "Authentication Required" @@ -116,20 +124,6 @@ auth: requests: en: "You have %{u} pending authentication requests" -startup: - client_keys_warning: - en: "Warning" - client_keys_desc: - en: "Allow Coop to read the client keys stored in Keychain to continue" - create_new_keys: - en: "Create New Keys" - auto_login_in_progress: - en: "Auto login in progress" - stuck: - en: "Stuck?" - reset: - en: "Reset" - new_account: title: en: "Create a new identity" @@ -181,6 +175,8 @@ login: en: "Bunker is not valid" logging_in: en: "Logging in..." + keyring_required: + en: "You must allow Coop access to the keyring to continue." mailbox: modal: