feat: custom gossip implementation (#181)
* . * rename global to app_state * refactor event tracker * gossip * . * .
This commit is contained in:
186
Cargo.lock
generated
186
Cargo.lock
generated
@@ -91,6 +91,21 @@ version = "1.0.100"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "app_state"
|
||||||
|
version = "0.2.11"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dirs 5.0.1",
|
||||||
|
"flume",
|
||||||
|
"log",
|
||||||
|
"nostr-lmdb",
|
||||||
|
"nostr-sdk",
|
||||||
|
"rustls",
|
||||||
|
"smol",
|
||||||
|
"whoami",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbitrary"
|
name = "arbitrary"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
@@ -167,6 +182,9 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"url",
|
"url",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols 0.32.9",
|
||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -498,9 +516,9 @@ name = "auto_update"
|
|||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"app_state",
|
||||||
"cargo-packager-updater",
|
"cargo-packager-updater",
|
||||||
"common",
|
"common",
|
||||||
"global",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
@@ -609,7 +627,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.4",
|
"bitflags 2.9.4",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.11.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -629,7 +647,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.4",
|
"bitflags 2.9.4",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.11.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1101,7 +1119,7 @@ name = "client_keys"
|
|||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"global",
|
"app_state",
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
@@ -1218,9 +1236,9 @@ name = "common"
|
|||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"app_state",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"global",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
@@ -1292,6 +1310,7 @@ name = "coop"
|
|||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"app_state",
|
||||||
"assets",
|
"assets",
|
||||||
"auto_update",
|
"auto_update",
|
||||||
"client_keys",
|
"client_keys",
|
||||||
@@ -1299,7 +1318,6 @@ dependencies = [
|
|||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"flume",
|
"flume",
|
||||||
"futures",
|
"futures",
|
||||||
"global",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui_tokio",
|
"gpui_tokio",
|
||||||
"i18n",
|
"i18n",
|
||||||
@@ -1785,7 +1803,7 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 0.9.7",
|
"toml 0.9.8",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
@@ -2434,21 +2452,6 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "global"
|
|
||||||
version = "0.2.11"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"dirs 5.0.1",
|
|
||||||
"flume",
|
|
||||||
"log",
|
|
||||||
"nostr-lmdb",
|
|
||||||
"nostr-sdk",
|
|
||||||
"rustls",
|
|
||||||
"smol",
|
|
||||||
"whoami",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "globset"
|
name = "globset"
|
||||||
version = "0.4.16"
|
version = "0.4.16"
|
||||||
@@ -2529,8 +2532,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2598,7 +2601,7 @@ dependencies = [
|
|||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-cursor",
|
"wayland-cursor",
|
||||||
"wayland-protocols",
|
"wayland-protocols 0.31.2",
|
||||||
"wayland-protocols-plasma",
|
"wayland-protocols-plasma",
|
||||||
"windows 0.61.3",
|
"windows 0.61.3",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
@@ -2624,7 +2627,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-macros"
|
name = "gpui-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2636,7 +2639,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_tokio"
|
name = "gpui_tokio"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2672,13 +2675,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "half"
|
name = "half"
|
||||||
version = "2.6.0"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
|
checksum = "e54c115d4f30f52c67202f079c5f9d8b49db4691f460fdb0b4c2e838261b2ba5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"crunchy",
|
"crunchy",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2705,6 +2709,12 @@ dependencies = [
|
|||||||
"foldhash",
|
"foldhash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -2859,7 +2869,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client_tls"
|
name = "http_client_tls"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3167,7 +3177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.15.5",
|
"hashbrown 0.16.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -3427,9 +3437,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.176"
|
version = "0.2.177"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libfuzzer-sys"
|
name = "libfuzzer-sys"
|
||||||
@@ -3448,7 +3458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.53.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3904,7 +3914,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d"
|
source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -3928,7 +3938,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d"
|
source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3940,7 +3950,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d"
|
source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -3951,7 +3961,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d"
|
source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flume",
|
"flume",
|
||||||
@@ -3965,7 +3975,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-relay-pool"
|
name = "nostr-relay-pool"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d"
|
source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -3982,7 +3992,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ca8eda4903f34ca3d948493257bb96489dcd4b3d"
|
source = "git+https://github.com/rust-nostr/nostr#293f5d6747c57681e3c1e37670a59ca0f6802382"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4706,7 +4716,7 @@ version = "3.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
|
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"toml_edit 0.23.6",
|
"toml_edit 0.23.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4770,9 +4780,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pxfm"
|
name = "pxfm"
|
||||||
version = "0.1.24"
|
version = "0.1.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde"
|
checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
@@ -5135,9 +5145,9 @@ name = "registry"
|
|||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"app_state",
|
||||||
"common",
|
"common",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"global",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
@@ -5200,7 +5210,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest_client"
|
name = "reqwest_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5255,7 +5265,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rope"
|
name = "rope"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -5831,9 +5841,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
|
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -5868,7 +5878,7 @@ name = "settings"
|
|||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"global",
|
"app_state",
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
@@ -5935,10 +5945,10 @@ name = "signer_proxy"
|
|||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"app_state",
|
||||||
"atomic-destructor",
|
"atomic-destructor",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
"futures",
|
||||||
"global",
|
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
@@ -6066,9 +6076,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stacker"
|
name = "stacker"
|
||||||
@@ -6747,14 +6757,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.9.7"
|
version = "0.9.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
|
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned 1.0.2",
|
"serde_spanned 1.0.3",
|
||||||
"toml_datetime 0.7.2",
|
"toml_datetime 0.7.3",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow",
|
"winnow",
|
||||||
@@ -6771,9 +6781,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
|
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -6794,21 +6804,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.23.6"
|
version = "0.23.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b"
|
checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime 0.7.2",
|
"toml_datetime 0.7.3",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
|
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
@@ -6821,9 +6831,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
|
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
@@ -7490,6 +7500,18 @@ dependencies = [
|
|||||||
"wayland-scanner",
|
"wayland-scanner",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-protocols"
|
||||||
|
version = "0.32.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.4",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-scanner",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-protocols-plasma"
|
name = "wayland-protocols-plasma"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -7499,7 +7521,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.4",
|
"bitflags 2.9.4",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-protocols",
|
"wayland-protocols 0.31.2",
|
||||||
"wayland-scanner",
|
"wayland-scanner",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7568,14 +7590,14 @@ version = "0.26.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
|
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webpki-root-certs 1.0.2",
|
"webpki-root-certs 1.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-root-certs"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a"
|
checksum = "05d651ec480de84b762e7be71e6efa7461699c19d9e2c272c8d93455f567786e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@@ -7586,14 +7608,14 @@ version = "0.26.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webpki-roots 1.0.2",
|
"webpki-roots 1.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
|
checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@@ -8473,7 +8495,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zed-collections"
|
name = "zed-collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -8483,7 +8505,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zed-derive-refineable"
|
name = "zed-derive-refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -8518,7 +8540,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zed-http-client"
|
name = "zed-http-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -8544,7 +8566,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zed-media"
|
name = "zed-media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -8560,7 +8582,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zed-perf"
|
name = "zed-perf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -8571,7 +8593,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zed-refineable"
|
name = "zed-refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"zed-derive-refineable",
|
"zed-derive-refineable",
|
||||||
@@ -8650,7 +8672,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zed-semantic-version"
|
name = "zed-semantic-version"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -8660,7 +8682,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zed-sum-tree"
|
name = "zed-sum-tree"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -8671,7 +8693,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zed-util"
|
name = "zed-util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
@@ -8707,7 +8729,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zed-util-macros"
|
name = "zed-util-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#963204c99ddb08bd1dacbb5168194b0a39fa5ad2"
|
source = "git+https://github.com/zed-industries/zed#a4ec693e3471f87aea89bc4c8132330b0fde3a9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "global"
|
name = "app_state"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
44
crates/app_state/src/lib.rs
Normal file
44
crates/app_state/src/lib.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use nostr_lmdb::NostrLMDB;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use paths::nostr_file;
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
pub mod constants;
|
||||||
|
pub mod paths;
|
||||||
|
pub mod state;
|
||||||
|
|
||||||
|
static APP_STATE: OnceLock<AppState> = OnceLock::new();
|
||||||
|
static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Initialize the application state.
|
||||||
|
pub fn app_state() -> &'static AppState {
|
||||||
|
APP_STATE.get_or_init(AppState::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the nostr client.
|
||||||
|
pub fn nostr_client() -> &'static Client {
|
||||||
|
NOSTR_CLIENT.get_or_init(|| {
|
||||||
|
// rustls uses the `aws_lc_rs` provider by default
|
||||||
|
// This only errors if the default provider has already
|
||||||
|
// been installed. We can ignore this `Result`.
|
||||||
|
rustls::crypto::aws_lc_rs::default_provider()
|
||||||
|
.install_default()
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized");
|
||||||
|
|
||||||
|
let opts = ClientOptions::new()
|
||||||
|
.gossip(false)
|
||||||
|
.automatic_authentication(false)
|
||||||
|
.verify_subscriptions(false)
|
||||||
|
.sleep_when_idle(SleepWhenIdle::Enabled {
|
||||||
|
timeout: Duration::from_secs(600),
|
||||||
|
});
|
||||||
|
|
||||||
|
ClientBuilder::default().database(lmdb).opts(opts).build()
|
||||||
|
})
|
||||||
|
}
|
||||||
217
crates/app_state/src/state/gossip.rs
Normal file
217
crates/app_state/src/state/gossip.rs
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
use crate::constants::BOOTSTRAP_RELAYS;
|
||||||
|
use crate::state::SignalKind;
|
||||||
|
use crate::{app_state, nostr_client};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Gossip {
|
||||||
|
pub nip17: HashMap<PublicKey, HashSet<RelayUrl>>,
|
||||||
|
pub nip65: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gossip {
|
||||||
|
pub fn insert(&mut self, event: &Event) {
|
||||||
|
match event.kind {
|
||||||
|
Kind::InboxRelays => {
|
||||||
|
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).cloned().collect();
|
||||||
|
|
||||||
|
if !urls.is_empty() {
|
||||||
|
self.nip17.entry(event.pubkey).or_default().extend(urls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kind::RelayList => {
|
||||||
|
let urls: Vec<(RelayUrl, Option<RelayMetadata>)> = nip65::extract_relay_list(event)
|
||||||
|
.map(|(url, metadata)| (url.to_owned(), metadata.to_owned()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !urls.is_empty() {
|
||||||
|
self.nip65.entry(event.pubkey).or_default().extend(urls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_relays(&self, public_key: &PublicKey) -> Vec<&RelayUrl> {
|
||||||
|
self.nip65
|
||||||
|
.get(public_key)
|
||||||
|
.map(|relays| {
|
||||||
|
relays
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, metadata)| metadata.as_ref() != Some(&RelayMetadata::Write))
|
||||||
|
.map(|(url, _)| url)
|
||||||
|
.take(3)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_relays(&self, public_key: &PublicKey) -> Vec<&RelayUrl> {
|
||||||
|
self.nip65
|
||||||
|
.get(public_key)
|
||||||
|
.map(|relays| {
|
||||||
|
relays
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, metadata)| metadata.as_ref() != Some(&RelayMetadata::Read))
|
||||||
|
.map(|(url, _)| url)
|
||||||
|
.take(3)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec<&RelayUrl> {
|
||||||
|
self.nip17
|
||||||
|
.get(public_key)
|
||||||
|
.map(|relays| relays.iter().collect())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_nip65(&mut self, public_key: PublicKey) -> Result<(), Error> {
|
||||||
|
let client = nostr_client();
|
||||||
|
let timeout = Duration::from_secs(5);
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::RelayList)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Subscribe to events from the bootstrapping relays
|
||||||
|
client
|
||||||
|
.subscribe_to(BOOTSTRAP_RELAYS, filter.clone(), Some(opts))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Verify the received data after a timeout
|
||||||
|
smol::spawn(async move {
|
||||||
|
smol::Timer::after(timeout).await;
|
||||||
|
|
||||||
|
if client.database().count(filter).await.unwrap_or(0) < 1 {
|
||||||
|
app_state()
|
||||||
|
.signal
|
||||||
|
.send(SignalKind::GossipRelaysNotFound)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_nip17(&mut self, public_key: PublicKey) -> Result<(), Error> {
|
||||||
|
let client = nostr_client();
|
||||||
|
let timeout = Duration::from_secs(5);
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let urls = self.write_relays(&public_key);
|
||||||
|
|
||||||
|
// Ensure user's have at least one write relay
|
||||||
|
if urls.is_empty() {
|
||||||
|
return Err(anyhow!("NIP-17 relays are empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure connection to relays
|
||||||
|
for url in urls.iter().cloned() {
|
||||||
|
client.add_relay(url).await?;
|
||||||
|
client.connect_relay(url).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to events from the bootstrapping relays
|
||||||
|
client
|
||||||
|
.subscribe_to(urls, filter.clone(), Some(opts))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Verify the received data after a timeout
|
||||||
|
smol::spawn(async move {
|
||||||
|
smol::Timer::after(timeout).await;
|
||||||
|
|
||||||
|
if client.database().count(filter).await.unwrap_or(0) < 1 {
|
||||||
|
app_state()
|
||||||
|
.signal
|
||||||
|
.send(SignalKind::MessagingRelaysNotFound)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn subscribe(&mut self, public_key: PublicKey, kind: Kind) -> Result<(), Error> {
|
||||||
|
let client = nostr_client();
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
let filter = Filter::new().author(public_key).kind(kind).limit(1);
|
||||||
|
let urls = self.write_relays(&public_key);
|
||||||
|
|
||||||
|
// Ensure user's have at least one write relay
|
||||||
|
if urls.is_empty() {
|
||||||
|
return Err(anyhow!("NIP-65 relays are empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure connection to relays
|
||||||
|
for url in urls.iter().cloned() {
|
||||||
|
client.add_relay(url).await?;
|
||||||
|
client.connect_relay(url).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to filters to user's write relays
|
||||||
|
client.subscribe_to(urls, filter, Some(opts)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bulk_subscribe(&mut self, public_keys: HashSet<PublicKey>) -> Result<(), Error> {
|
||||||
|
if public_keys.is_empty() {
|
||||||
|
return Err(anyhow!("You need at least one public key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = nostr_client();
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||||
|
let limit = public_keys.len() * kinds.len() + 20;
|
||||||
|
|
||||||
|
let filter = Filter::new().authors(public_keys).kinds(kinds).limit(limit);
|
||||||
|
let urls = BOOTSTRAP_RELAYS;
|
||||||
|
|
||||||
|
// Subscribe to filters to the bootstrap relays
|
||||||
|
client.subscribe_to(urls, filter, Some(opts)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Monitor all gift wrap events in the messaging relays for a given public key
|
||||||
|
pub async fn monitor_inbox(&mut self, public_key: PublicKey) -> Result<(), Error> {
|
||||||
|
let client = nostr_client();
|
||||||
|
let id = SubscriptionId::new("inbox");
|
||||||
|
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
|
let urls = self.messaging_relays(&public_key);
|
||||||
|
|
||||||
|
// Ensure user's have at least one messaging relay
|
||||||
|
if urls.is_empty() {
|
||||||
|
return Err(anyhow!("Messaging relays are empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure connection to relays
|
||||||
|
for url in urls.iter().cloned() {
|
||||||
|
client.add_relay(url).await?;
|
||||||
|
client.connect_relay(url).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to filters to user's messaging relays
|
||||||
|
client.subscribe_with_id_to(urls, id, filter, None).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
516
crates/app_state/src/state/mod.rs
Normal file
516
crates/app_state/src/state/mod.rs
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error};
|
||||||
|
use flume::{Receiver, Sender};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smol::lock::RwLock;
|
||||||
|
|
||||||
|
use crate::constants::{
|
||||||
|
BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, SEARCH_RELAYS,
|
||||||
|
};
|
||||||
|
use crate::nostr_client;
|
||||||
|
use crate::paths::support_dir;
|
||||||
|
use crate::state::gossip::Gossip;
|
||||||
|
|
||||||
|
pub mod gossip;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct AuthRequest {
|
||||||
|
pub url: RelayUrl,
|
||||||
|
pub challenge: String,
|
||||||
|
pub sending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthRequest {
|
||||||
|
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
|
||||||
|
Self {
|
||||||
|
challenge: challenge.into(),
|
||||||
|
sending: false,
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum UnwrappingStatus {
|
||||||
|
#[default]
|
||||||
|
Initialized,
|
||||||
|
Processing,
|
||||||
|
Complete,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signals sent through the global event channel to notify UI
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SignalKind {
|
||||||
|
/// A signal to notify UI that the client's signer has been set
|
||||||
|
SignerSet(PublicKey),
|
||||||
|
|
||||||
|
/// A signal to notify UI that the client's signer has been unset
|
||||||
|
SignerUnset,
|
||||||
|
|
||||||
|
/// A signal to notify UI that the relay requires authentication
|
||||||
|
Auth(AuthRequest),
|
||||||
|
|
||||||
|
/// A signal to notify UI that the browser proxy service is down
|
||||||
|
ProxyDown,
|
||||||
|
|
||||||
|
/// A signal to notify UI that a new profile has been received
|
||||||
|
NewProfile(Profile),
|
||||||
|
|
||||||
|
/// A signal to notify UI that a new gift wrap event has been received
|
||||||
|
NewMessage((EventId, Event)),
|
||||||
|
|
||||||
|
/// A signal to notify UI that no messaging relays for current user was found
|
||||||
|
MessagingRelaysNotFound,
|
||||||
|
|
||||||
|
/// A signal to notify UI that no gossip relays for current user was found
|
||||||
|
GossipRelaysNotFound,
|
||||||
|
|
||||||
|
/// A signal to notify UI that gift wrap status has changed
|
||||||
|
GiftWrapStatus(UnwrappingStatus),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Signal {
|
||||||
|
rx: Receiver<SignalKind>,
|
||||||
|
tx: Sender<SignalKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Signal {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Signal {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (tx, rx) = flume::bounded::<SignalKind>(2048);
|
||||||
|
Self { rx, tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receiver(&self) -> &Receiver<SignalKind> {
|
||||||
|
&self.rx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send(&self, kind: SignalKind) {
|
||||||
|
if let Err(e) = self.tx.send_async(kind).await {
|
||||||
|
log::error!("Failed to send signal: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Ingester {
|
||||||
|
rx: Receiver<PublicKey>,
|
||||||
|
tx: Sender<PublicKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Ingester {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ingester {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (tx, rx) = flume::bounded::<PublicKey>(1024);
|
||||||
|
Self { rx, tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receiver(&self) -> &Receiver<PublicKey> {
|
||||||
|
&self.rx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send(&self, public_key: PublicKey) {
|
||||||
|
if let Err(e) = self.tx.send_async(public_key).await {
|
||||||
|
log::error!("Failed to send public key: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct EventTracker {
|
||||||
|
/// Tracking events that have been resent by Coop in the current session
|
||||||
|
pub resent_ids: Vec<Output<EventId>>,
|
||||||
|
|
||||||
|
/// Temporarily store events that need to be resent later
|
||||||
|
pub resend_queue: HashMap<EventId, RelayUrl>,
|
||||||
|
|
||||||
|
/// Tracking events sent by Coop in the current session
|
||||||
|
pub sent_ids: HashSet<EventId>,
|
||||||
|
|
||||||
|
/// Tracking events seen on which relays in the current session
|
||||||
|
pub seen_on_relays: HashMap<EventId, HashSet<RelayUrl>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventTracker {
|
||||||
|
pub fn resent_ids(&self) -> &Vec<Output<EventId>> {
|
||||||
|
&self.resent_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resend_queue(&self) -> &HashMap<EventId, RelayUrl> {
|
||||||
|
&self.resend_queue
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sent_ids(&self) -> &HashSet<EventId> {
|
||||||
|
&self.sent_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seen_on_relays(&self) -> &HashMap<EventId, HashSet<RelayUrl>> {
|
||||||
|
&self.seen_on_relays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple storage to store all states that using across the application.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AppState {
|
||||||
|
/// The timestamp when the application was initialized.
|
||||||
|
pub initialized_at: Timestamp,
|
||||||
|
|
||||||
|
/// Whether this is the first run of the application.
|
||||||
|
pub is_first_run: AtomicBool,
|
||||||
|
|
||||||
|
/// Whether gift wrap processing is in progress.
|
||||||
|
pub gift_wrap_processing: AtomicBool,
|
||||||
|
|
||||||
|
/// Subscription ID for listening to gift wrap events from relays.
|
||||||
|
pub gift_wrap_sub_id: SubscriptionId,
|
||||||
|
|
||||||
|
/// Auto-close options for relay subscriptions
|
||||||
|
pub auto_close_opts: Option<SubscribeAutoCloseOptions>,
|
||||||
|
|
||||||
|
/// NIP-65: https://github.com/nostr-protocol/nips/blob/master/65.md
|
||||||
|
pub gossip: RwLock<Gossip>,
|
||||||
|
|
||||||
|
/// Tracks activity related to Nostr events
|
||||||
|
pub event_tracker: RwLock<EventTracker>,
|
||||||
|
|
||||||
|
/// Signal channel for communication between Nostr and GPUI
|
||||||
|
pub signal: Signal,
|
||||||
|
|
||||||
|
/// Ingester channel for processing public keys
|
||||||
|
pub ingester: Ingester,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let first_run = Self::first_run();
|
||||||
|
let initialized_at = Timestamp::now();
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
let signal = Signal::default();
|
||||||
|
let ingester = Ingester::default();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
initialized_at,
|
||||||
|
signal,
|
||||||
|
ingester,
|
||||||
|
is_first_run: AtomicBool::new(first_run),
|
||||||
|
gift_wrap_sub_id: SubscriptionId::new("inbox"),
|
||||||
|
gift_wrap_processing: AtomicBool::new(false),
|
||||||
|
auto_close_opts: Some(opts),
|
||||||
|
gossip: RwLock::new(Gossip::default()),
|
||||||
|
event_tracker: RwLock::new(EventTracker::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_notifications(&self) -> Result<(), Error> {
|
||||||
|
let client = nostr_client();
|
||||||
|
|
||||||
|
// Get all bootstrapping relays
|
||||||
|
let mut urls = vec![];
|
||||||
|
urls.extend(BOOTSTRAP_RELAYS);
|
||||||
|
urls.extend(SEARCH_RELAYS);
|
||||||
|
|
||||||
|
// Add relay to the relay pool
|
||||||
|
for url in urls.into_iter() {
|
||||||
|
client.add_relay(url).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establish connection to relays
|
||||||
|
client.connect().await;
|
||||||
|
|
||||||
|
let mut processed_events: HashSet<EventId> = HashSet::new();
|
||||||
|
let mut challenges: HashSet<Cow<'_, str>> = HashSet::new();
|
||||||
|
let mut notifications = client.notifications();
|
||||||
|
|
||||||
|
while let Ok(notification) = notifications.recv().await {
|
||||||
|
let RelayPoolNotification::Message { message, relay_url } = notification else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match message {
|
||||||
|
RelayMessage::Event { event, .. } => {
|
||||||
|
// Keep track of which relays have seen this event
|
||||||
|
{
|
||||||
|
let mut event_tracker = self.event_tracker.write().await;
|
||||||
|
event_tracker
|
||||||
|
.seen_on_relays
|
||||||
|
.entry(event.id)
|
||||||
|
.or_default()
|
||||||
|
.insert(relay_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip events that have already been processed
|
||||||
|
if !processed_events.insert(event.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match event.kind {
|
||||||
|
Kind::RelayList => {
|
||||||
|
let mut gossip = self.gossip.write().await;
|
||||||
|
let is_self_authored = Self::is_self_authored(&event).await;
|
||||||
|
|
||||||
|
// Update NIP-65 relays for event's public key
|
||||||
|
gossip.insert(&event);
|
||||||
|
|
||||||
|
// Get events if relay list belongs to current user
|
||||||
|
if is_self_authored {
|
||||||
|
// Fetch user's metadata event
|
||||||
|
gossip.subscribe(event.pubkey, Kind::Metadata).await.ok();
|
||||||
|
|
||||||
|
// Fetch user's contact list event
|
||||||
|
gossip.subscribe(event.pubkey, Kind::ContactList).await.ok();
|
||||||
|
|
||||||
|
// Fetch user's messaging relays event
|
||||||
|
gossip.get_nip17(event.pubkey).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kind::InboxRelays => {
|
||||||
|
let mut gossip = self.gossip.write().await;
|
||||||
|
let is_self_authored = Self::is_self_authored(&event).await;
|
||||||
|
|
||||||
|
// Update NIP-17 relays for event's public key
|
||||||
|
gossip.insert(&event);
|
||||||
|
|
||||||
|
// Subscribe to gift wrap events if messaging relays belong to the current user
|
||||||
|
if is_self_authored {
|
||||||
|
if let Err(e) = gossip.monitor_inbox(event.pubkey).await {
|
||||||
|
log::error!("Error: {e}");
|
||||||
|
self.signal.send(SignalKind::MessagingRelaysNotFound).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kind::ContactList => {
|
||||||
|
let is_self_authored = Self::is_self_authored(&event).await;
|
||||||
|
|
||||||
|
if is_self_authored {
|
||||||
|
let mut gossip = self.gossip.write().await;
|
||||||
|
let public_keys: HashSet<PublicKey> =
|
||||||
|
event.tags.public_keys().copied().collect();
|
||||||
|
|
||||||
|
gossip.bulk_subscribe(public_keys).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kind::Metadata => {
|
||||||
|
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
||||||
|
let profile = Profile::new(event.pubkey, metadata);
|
||||||
|
|
||||||
|
self.signal.send(SignalKind::NewProfile(profile)).await;
|
||||||
|
}
|
||||||
|
Kind::GiftWrap => {
|
||||||
|
self.extract_rumor(&event).await;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
||||||
|
if *subscription_id == self.gift_wrap_sub_id {
|
||||||
|
self.signal
|
||||||
|
.send(SignalKind::GiftWrapStatus(UnwrappingStatus::Processing))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RelayMessage::Auth { challenge } => {
|
||||||
|
if challenges.insert(challenge.clone()) {
|
||||||
|
// Send a signal to the ingester to handle the auth request
|
||||||
|
self.signal
|
||||||
|
.send(SignalKind::Auth(AuthRequest::new(challenge, relay_url)))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RelayMessage::Ok {
|
||||||
|
event_id, message, ..
|
||||||
|
} => {
|
||||||
|
let msg = MachineReadablePrefix::parse(&message);
|
||||||
|
let mut event_tracker = self.event_tracker.write().await;
|
||||||
|
|
||||||
|
// Keep track of events sent by Coop
|
||||||
|
event_tracker.sent_ids.insert(event_id);
|
||||||
|
|
||||||
|
// Keep track of events that need to be resend after auth
|
||||||
|
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
||||||
|
event_tracker.resend_queue.insert(event_id, relay_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_metadata_batching(&self) {
|
||||||
|
let timeout = Duration::from_millis(METADATA_BATCH_TIMEOUT);
|
||||||
|
let mut processed_pubkeys: HashSet<PublicKey> = HashSet::new();
|
||||||
|
let mut batch: HashSet<PublicKey> = HashSet::new();
|
||||||
|
|
||||||
|
/// Internal events for the metadata batching system
|
||||||
|
enum BatchEvent {
|
||||||
|
PublicKey(PublicKey),
|
||||||
|
Timeout,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let futs = smol::future::or(
|
||||||
|
async move {
|
||||||
|
if let Ok(public_key) = self.ingester.receiver().recv_async().await {
|
||||||
|
BatchEvent::PublicKey(public_key)
|
||||||
|
} else {
|
||||||
|
BatchEvent::Closed
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async move {
|
||||||
|
smol::Timer::after(timeout).await;
|
||||||
|
BatchEvent::Timeout
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match futs.await {
|
||||||
|
BatchEvent::PublicKey(public_key) => {
|
||||||
|
// Prevent duplicate keys from being processed
|
||||||
|
if processed_pubkeys.insert(public_key) {
|
||||||
|
batch.insert(public_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the batch if it's full
|
||||||
|
if batch.len() >= METADATA_BATCH_LIMIT {
|
||||||
|
let mut gossip = self.gossip.write().await;
|
||||||
|
gossip.bulk_subscribe(std::mem::take(&mut batch)).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BatchEvent::Timeout => {
|
||||||
|
let mut gossip = self.gossip.write().await;
|
||||||
|
gossip.bulk_subscribe(std::mem::take(&mut batch)).await.ok();
|
||||||
|
}
|
||||||
|
BatchEvent::Closed => {
|
||||||
|
let mut gossip = self.gossip.write().await;
|
||||||
|
gossip.bulk_subscribe(std::mem::take(&mut batch)).await.ok();
|
||||||
|
|
||||||
|
// Exit the current loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_self_authored(event: &Event) -> bool {
|
||||||
|
let client = nostr_client();
|
||||||
|
|
||||||
|
let Ok(signer) = client.signer().await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(public_key) = signer.get_public_key().await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
public_key == event.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores an unwrapped event in local database with reference to original
|
||||||
|
async fn set_rumor(&self, id: EventId, rumor: &Event) -> Result<(), Error> {
|
||||||
|
let client = nostr_client();
|
||||||
|
|
||||||
|
// Save unwrapped event
|
||||||
|
client.database().save_event(rumor).await?;
|
||||||
|
|
||||||
|
// Create a reference event pointing to the unwrapped event
|
||||||
|
let event = EventBuilder::new(Kind::ApplicationSpecificData, "")
|
||||||
|
.tags(vec![Tag::identifier(id), Tag::event(rumor.id)])
|
||||||
|
.sign(&Keys::generate())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Save reference event
|
||||||
|
client.database().save_event(&event).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves a previously unwrapped event from local database
|
||||||
|
async fn get_rumor(&self, id: EventId) -> Result<Event, Error> {
|
||||||
|
let client = nostr_client();
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::ApplicationSpecificData)
|
||||||
|
.identifier(id)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||||
|
let target_id = event.tags.event_ids().collect::<Vec<_>>()[0];
|
||||||
|
|
||||||
|
if let Some(event) = client.database().event_by_id(target_id).await? {
|
||||||
|
Ok(event)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Event not found."))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Event is not cached yet."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwraps a gift-wrapped event and processes its contents.
|
||||||
|
async fn extract_rumor(&self, gift_wrap: &Event) {
|
||||||
|
let client = nostr_client();
|
||||||
|
|
||||||
|
let mut rumor: Option<Event> = None;
|
||||||
|
|
||||||
|
if let Ok(event) = self.get_rumor(gift_wrap.id).await {
|
||||||
|
rumor = Some(event);
|
||||||
|
} else if let Ok(unwrapped) = client.unwrap_gift_wrap(gift_wrap).await {
|
||||||
|
// Sign the unwrapped event with a RANDOM KEYS
|
||||||
|
if let Ok(event) = unwrapped.rumor.sign_with_keys(&Keys::generate()) {
|
||||||
|
// Save this event to the database for future use.
|
||||||
|
if let Err(e) = self.set_rumor(gift_wrap.id, &event).await {
|
||||||
|
log::warn!("Failed to cache unwrapped event: {e}")
|
||||||
|
}
|
||||||
|
|
||||||
|
rumor = Some(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(event) = rumor {
|
||||||
|
// Send all pubkeys to the metadata batch to sync data
|
||||||
|
for public_key in event.tags.public_keys().copied() {
|
||||||
|
self.ingester.send(public_key).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
match event.created_at >= self.initialized_at {
|
||||||
|
// New message: send a signal to notify the UI
|
||||||
|
true => {
|
||||||
|
self.signal
|
||||||
|
.send(SignalKind::NewMessage((gift_wrap.id, event)))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
// Old message: Coop is probably processing the user's messages during initial load
|
||||||
|
false => {
|
||||||
|
self.gift_wrap_processing.store(true, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_run() -> bool {
|
||||||
|
let flag = support_dir().join(".first_run");
|
||||||
|
!flag.exists() && std::fs::write(&flag, "").is_ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
app_state = { path = "../app_state" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use app_state::constants::{APP_PUBKEY, APP_UPDATER_ENDPOINT};
|
||||||
use cargo_packager_updater::semver::Version;
|
use cargo_packager_updater::semver::Version;
|
||||||
use cargo_packager_updater::{check_update, Config, Update};
|
use cargo_packager_updater::{check_update, Config, Update};
|
||||||
use global::constants::{APP_PUBKEY, APP_UPDATER_ENDPOINT};
|
|
||||||
use gpui::http_client::Url;
|
use gpui::http_client::Url;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
|
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
global = { path = "../global" }
|
app_state = { path = "../app_state" }
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
use global::app_state;
|
use app_state::app_state;
|
||||||
use global::constants::KEYRING_URL;
|
use app_state::constants::KEYRING_URL;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Window};
|
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Window};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
global = { path = "../global" }
|
app_state = { path = "../app_state" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
|
use app_state::constants::IMAGE_RESIZE_SERVICE;
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use global::constants::IMAGE_RESIZE_SERVICE;
|
|
||||||
use gpui::{Image, ImageFormat, SharedString, SharedUri};
|
use gpui::{Image, ImageFormat, SharedString, SharedUri};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use qrcode::render::svg;
|
use qrcode::render::svg;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ ui = { path = "../ui" }
|
|||||||
title_bar = { path = "../title_bar" }
|
title_bar = { path = "../title_bar" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
app_state = { path = "../app_state" }
|
||||||
registry = { path = "../registry" }
|
registry = { path = "../registry" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
client_keys = { path = "../client_keys" }
|
client_keys = { path = "../client_keys" }
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
|
use app_state::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
|
||||||
|
use app_state::state::{AuthRequest, SignalKind, UnwrappingStatus};
|
||||||
|
use app_state::{app_state, nostr_client};
|
||||||
use auto_update::AutoUpdater;
|
use auto_update::AutoUpdater;
|
||||||
use client_keys::ClientKeys;
|
use client_keys::ClientKeys;
|
||||||
use common::display::RenderedProfile;
|
use common::display::RenderedProfile;
|
||||||
use common::event::EventUtils;
|
use common::event::EventUtils;
|
||||||
use global::constants::{
|
|
||||||
ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH, METADATA_BATCH_LIMIT,
|
|
||||||
METADATA_BATCH_TIMEOUT, SEARCH_RELAYS,
|
|
||||||
};
|
|
||||||
use global::{app_state, nostr_client, AuthRequest, Notice, SignalKind, UnwrappingStatus};
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
deferred, div, px, rems, App, AppContext, AsyncWindowContext, Axis, ClipboardItem, Context,
|
deferred, div, px, rems, App, AppContext, AsyncWindowContext, Axis, ClipboardItem, Context,
|
||||||
@@ -139,23 +137,43 @@ impl ChatSpace {
|
|||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe to open chat room requests
|
// Handle registry events
|
||||||
cx.subscribe_in(®istry, window, move |this, _, event, window, cx| {
|
cx.subscribe_in(®istry, window, move |this, _, ev, window, cx| {
|
||||||
this.process_registry_event(event, window, cx);
|
match ev {
|
||||||
|
RegistryEvent::Open(room) => {
|
||||||
|
if let Some(room) = room.upgrade() {
|
||||||
|
this.dock.update(cx, |this, cx| {
|
||||||
|
let panel = chat::init(room, window, cx);
|
||||||
|
this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RegistryEvent::Close(..) => {
|
||||||
|
this.dock.update(cx, |this, cx| {
|
||||||
|
this.focus_tab_panel(window, cx);
|
||||||
|
|
||||||
|
cx.defer_in(window, |_, window, cx| {
|
||||||
|
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||||
|
window.close_all_modals(cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
// Connect to the bootstrap relays
|
// Handle nostr events in the background
|
||||||
// Then handle nostr events in the background
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
Self::connect()
|
app_state().handle_notifications().await.ok();
|
||||||
.await
|
}),
|
||||||
.expect("Failed connect the bootstrap relays. Please restart the application.");
|
);
|
||||||
|
|
||||||
Self::process_nostr_events()
|
tasks.push(
|
||||||
.await
|
// Listen all metadata requests then batch them into single subscription
|
||||||
.expect("Failed to handle nostr events. Please restart the application.");
|
cx.background_spawn(async move {
|
||||||
|
app_state().handle_metadata_batching().await;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -174,17 +192,10 @@ impl ChatSpace {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
tasks.push(
|
|
||||||
// Listen all metadata requests then batch them into single subscription
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
Self::process_batching_metadata().await;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
// Continuously handle signals from the Nostr channel
|
// Continuously handle signals from the Nostr channel
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
Self::process_nostr_signals(this, cx).await
|
Self::handle_signals(this, cx).await
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -198,100 +209,26 @@ impl ChatSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect() -> Result<(), Error> {
|
|
||||||
let client = nostr_client();
|
|
||||||
|
|
||||||
for relay in BOOTSTRAP_RELAYS.into_iter() {
|
|
||||||
client.add_relay(relay).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Connected to bootstrap relays");
|
|
||||||
|
|
||||||
for relay in SEARCH_RELAYS.into_iter() {
|
|
||||||
client.add_relay(relay).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Connected to search relays");
|
|
||||||
|
|
||||||
// Establish connection to relays
|
|
||||||
client.connect().await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn observe_signer() {
|
async fn observe_signer() {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
let app_state = app_state();
|
let app_state = app_state();
|
||||||
let stream_timeout = Duration::from_secs(5);
|
let loop_duration = Duration::from_millis(800);
|
||||||
let loop_duration = Duration::from_secs(1);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let Ok(signer) = client.signer().await else {
|
if let Ok(signer) = client.signer().await {
|
||||||
smol::Timer::after(loop_duration).await;
|
if let Ok(pk) = signer.get_public_key().await {
|
||||||
continue;
|
// Notify the app that the signer has been set
|
||||||
};
|
app_state.signal.send(SignalKind::SignerSet(pk)).await;
|
||||||
|
|
||||||
let Ok(public_key) = signer.get_public_key().await else {
|
// Get user's gossip relays
|
||||||
smol::Timer::after(loop_duration).await;
|
app_state.gossip.write().await.get_nip65(pk).await.ok();
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Notify the app that the signer has been set.
|
// Exit the current loop
|
||||||
app_state
|
|
||||||
.signal
|
|
||||||
.send(SignalKind::SignerSet(public_key))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Subscribe to the NIP-65 relays for the public key.
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::RelayList)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
let mut nip65_found = false;
|
|
||||||
|
|
||||||
match client
|
|
||||||
.stream_events_from(BOOTSTRAP_RELAYS, filter, stream_timeout)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(mut stream) => {
|
|
||||||
if stream.next().await.is_some() {
|
|
||||||
nip65_found = true;
|
|
||||||
} else {
|
|
||||||
// Timeout
|
|
||||||
app_state.signal.send(SignalKind::RelaysNotFound).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Error fetching NIP-65 Relay: {e:?}");
|
|
||||||
app_state.signal.send(SignalKind::RelaysNotFound).await;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if nip65_found {
|
|
||||||
// Subscribe to the NIP-17 relays for the public key.
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::InboxRelays)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
match client.stream_events(filter, stream_timeout).await {
|
|
||||||
Ok(mut stream) => {
|
|
||||||
if stream.next().await.is_some() {
|
|
||||||
break;
|
break;
|
||||||
} else {
|
|
||||||
// Timeout
|
|
||||||
app_state.signal.send(SignalKind::RelaysNotFound).await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
log::error!("Error fetching NIP-17 Relay: {e:?}");
|
|
||||||
app_state.signal.send(SignalKind::RelaysNotFound).await;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
smol::Timer::after(loop_duration).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,189 +274,7 @@ impl ChatSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_batching_metadata() {
|
async fn handle_signals(view: WeakEntity<ChatSpace>, cx: &mut AsyncWindowContext) {
|
||||||
let app_state = app_state();
|
|
||||||
let timeout = Duration::from_millis(METADATA_BATCH_TIMEOUT);
|
|
||||||
let mut processed_pubkeys: HashSet<PublicKey> = HashSet::new();
|
|
||||||
let mut batch: HashSet<PublicKey> = HashSet::new();
|
|
||||||
|
|
||||||
/// Internal events for the metadata batching system
|
|
||||||
enum BatchEvent {
|
|
||||||
PublicKey(PublicKey),
|
|
||||||
Timeout,
|
|
||||||
Closed,
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let futs = smol::future::or(
|
|
||||||
async move {
|
|
||||||
if let Ok(public_key) = app_state.ingester.receiver().recv_async().await {
|
|
||||||
BatchEvent::PublicKey(public_key)
|
|
||||||
} else {
|
|
||||||
BatchEvent::Closed
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async move {
|
|
||||||
smol::Timer::after(timeout).await;
|
|
||||||
BatchEvent::Timeout
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
match futs.await {
|
|
||||||
BatchEvent::PublicKey(public_key) => {
|
|
||||||
// Prevent duplicate keys from being processed
|
|
||||||
if processed_pubkeys.insert(public_key) {
|
|
||||||
batch.insert(public_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the batch if it's full
|
|
||||||
if batch.len() >= METADATA_BATCH_LIMIT {
|
|
||||||
Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BatchEvent::Timeout => {
|
|
||||||
Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await;
|
|
||||||
}
|
|
||||||
BatchEvent::Closed => {
|
|
||||||
Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn process_nostr_events() -> Result<(), Error> {
|
|
||||||
let client = nostr_client();
|
|
||||||
let app_state = app_state();
|
|
||||||
|
|
||||||
let mut processed_events: HashSet<EventId> = HashSet::new();
|
|
||||||
let mut challenges: HashSet<Cow<'_, str>> = HashSet::new();
|
|
||||||
let mut notifications = client.notifications();
|
|
||||||
|
|
||||||
while let Ok(notification) = notifications.recv().await {
|
|
||||||
let RelayPoolNotification::Message { message, relay_url } = notification else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
match message {
|
|
||||||
RelayMessage::Event { event, .. } => {
|
|
||||||
// Keep track of which relays have seen this event
|
|
||||||
app_state
|
|
||||||
.seen_on_relays
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.entry(event.id)
|
|
||||||
.or_insert_with(HashSet::new)
|
|
||||||
.insert(relay_url);
|
|
||||||
|
|
||||||
// Skip events that have already been processed
|
|
||||||
if !processed_events.insert(event.id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
match event.kind {
|
|
||||||
Kind::RelayList => {
|
|
||||||
if let Ok(true) = Self::is_self_event(&event).await {
|
|
||||||
// Fetch user's metadata event
|
|
||||||
Self::fetch_single_event(Kind::Metadata, event.pubkey).await;
|
|
||||||
|
|
||||||
// Fetch user's contact list event
|
|
||||||
Self::fetch_single_event(Kind::ContactList, event.pubkey).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Kind::InboxRelays => {
|
|
||||||
if let Ok(true) = Self::is_self_event(&event).await {
|
|
||||||
let relays = nip17::extract_relay_list(&event).collect_vec();
|
|
||||||
|
|
||||||
if !relays.is_empty() {
|
|
||||||
for relay in relays.clone().into_iter() {
|
|
||||||
if client.add_relay(relay).await.is_err() {
|
|
||||||
let notice = Notice::RelayFailed(relay.clone());
|
|
||||||
app_state.signal.send(SignalKind::Notice(notice)).await;
|
|
||||||
}
|
|
||||||
if client.connect_relay(relay).await.is_err() {
|
|
||||||
let notice = Notice::RelayFailed(relay.clone());
|
|
||||||
app_state.signal.send(SignalKind::Notice(notice)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to gift wrap events only in the current user's NIP-17 relays
|
|
||||||
Self::fetch_gift_wrap(relays, event.pubkey).await;
|
|
||||||
} else {
|
|
||||||
app_state.signal.send(SignalKind::RelaysNotFound).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Kind::ContactList => {
|
|
||||||
if let Ok(true) = Self::is_self_event(&event).await {
|
|
||||||
let public_keys = event.tags.public_keys().copied().collect_vec();
|
|
||||||
let kinds = vec![Kind::Metadata, Kind::ContactList];
|
|
||||||
let limit = public_keys.len() * kinds.len();
|
|
||||||
let filter =
|
|
||||||
Filter::new().limit(limit).authors(public_keys).kinds(kinds);
|
|
||||||
|
|
||||||
client
|
|
||||||
.subscribe_to(
|
|
||||||
BOOTSTRAP_RELAYS,
|
|
||||||
filter,
|
|
||||||
app_state.auto_close_opts,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Kind::Metadata => {
|
|
||||||
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
|
||||||
let profile = Profile::new(event.pubkey, metadata);
|
|
||||||
|
|
||||||
app_state.signal.send(SignalKind::NewProfile(profile)).await;
|
|
||||||
}
|
|
||||||
Kind::GiftWrap => {
|
|
||||||
Self::unwrap_gift_wrap(&event).await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
|
||||||
if *subscription_id == app_state.gift_wrap_sub_id {
|
|
||||||
let signal = SignalKind::GiftWrapStatus(UnwrappingStatus::Processing);
|
|
||||||
app_state.signal.send(signal).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RelayMessage::Auth { challenge } => {
|
|
||||||
if challenges.insert(challenge.clone()) {
|
|
||||||
let req = AuthRequest::new(challenge, relay_url);
|
|
||||||
// Send a signal to the ingester to handle the auth request
|
|
||||||
app_state.signal.send(SignalKind::Auth(req)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RelayMessage::Ok {
|
|
||||||
event_id, message, ..
|
|
||||||
} => {
|
|
||||||
// Keep track of events sent by Coop
|
|
||||||
app_state.sent_ids.write().await.insert(event_id);
|
|
||||||
|
|
||||||
// Keep track of events that need to be resent
|
|
||||||
match MachineReadablePrefix::parse(&message) {
|
|
||||||
Some(MachineReadablePrefix::AuthRequired) => {
|
|
||||||
app_state
|
|
||||||
.resend_queue
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.insert(event_id, relay_url);
|
|
||||||
}
|
|
||||||
Some(_) => {}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn process_nostr_signals(view: WeakEntity<ChatSpace>, cx: &mut AsyncWindowContext) {
|
|
||||||
let app_state = app_state();
|
let app_state = app_state();
|
||||||
let mut is_open_proxy_modal = false;
|
let mut is_open_proxy_modal = false;
|
||||||
|
|
||||||
@@ -604,14 +359,17 @@ impl ChatSpace {
|
|||||||
this.event_to_message(gift_wrap_id, event, window, cx);
|
this.event_to_message(gift_wrap_id, event, window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
SignalKind::RelaysNotFound => {
|
SignalKind::GossipRelaysNotFound => {
|
||||||
view.update(cx, |this, cx| {
|
view.update(cx, |this, cx| {
|
||||||
this.set_required_relays(cx);
|
this.set_required_relays(cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
SignalKind::Notice(msg) => {
|
SignalKind::MessagingRelaysNotFound => {
|
||||||
window.push_notification(msg.as_str(), cx);
|
view.update(cx, |this, cx| {
|
||||||
|
this.set_required_relays(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -619,177 +377,6 @@ impl ChatSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if an event is belong to the current user
|
|
||||||
async fn is_self_event(event: &Event) -> Result<bool, Error> {
|
|
||||||
let client = nostr_client();
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
Ok(public_key == event.pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetches a single event by kind and public key
|
|
||||||
pub async fn fetch_single_event(kind: Kind, public_key: PublicKey) {
|
|
||||||
let client = nostr_client();
|
|
||||||
let app_state = app_state();
|
|
||||||
let filter = Filter::new().kind(kind).author(public_key).limit(1);
|
|
||||||
|
|
||||||
if let Err(e) = client.subscribe(filter, app_state.auto_close_opts).await {
|
|
||||||
log::info!("Failed to subscribe: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetches gift wrap events for a given public key and relays
|
|
||||||
pub async fn fetch_gift_wrap(relays: Vec<&RelayUrl>, public_key: PublicKey) {
|
|
||||||
let client = nostr_client();
|
|
||||||
let id = app_state().gift_wrap_sub_id.clone();
|
|
||||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
|
||||||
|
|
||||||
if client
|
|
||||||
.subscribe_with_id_to(relays.clone(), id, filter, None)
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
log::info!("Subscribed to messages in: {relays:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetches metadata for a list of public keys
|
|
||||||
async fn fetch_metadata_for_pubkeys(public_keys: HashSet<PublicKey>) {
|
|
||||||
if public_keys.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = nostr_client();
|
|
||||||
let app_state = app_state();
|
|
||||||
|
|
||||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
|
||||||
let limit = public_keys.len() * kinds.len() + 20;
|
|
||||||
|
|
||||||
// A filter to fetch metadata
|
|
||||||
let filter = Filter::new().authors(public_keys).kinds(kinds).limit(limit);
|
|
||||||
|
|
||||||
client
|
|
||||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, app_state.auto_close_opts)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores an unwrapped event in local database with reference to original
|
|
||||||
async fn set_unwrapped_event(gift_wrap: EventId, unwrapped: &Event) -> Result<(), Error> {
|
|
||||||
let client = nostr_client();
|
|
||||||
|
|
||||||
// Save unwrapped event
|
|
||||||
client.database().save_event(unwrapped).await?;
|
|
||||||
|
|
||||||
// Create a reference event pointing to the unwrapped event
|
|
||||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, "")
|
|
||||||
.tags(vec![Tag::identifier(gift_wrap), Tag::event(unwrapped.id)])
|
|
||||||
.sign(&Keys::generate())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Save reference event
|
|
||||||
client.database().save_event(&event).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieves a previously unwrapped event from local database
|
|
||||||
async fn get_unwrapped_event(root: EventId) -> Result<Event, Error> {
|
|
||||||
let client = nostr_client();
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::ApplicationSpecificData)
|
|
||||||
.identifier(root)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
|
||||||
let target_id = event.tags.event_ids().collect_vec()[0];
|
|
||||||
|
|
||||||
if let Some(event) = client.database().event_by_id(target_id).await? {
|
|
||||||
Ok(event)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Event not found."))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Event is not cached yet."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unwraps a gift-wrapped event and processes its contents.
|
|
||||||
async fn unwrap_gift_wrap(target: &Event) {
|
|
||||||
let client = nostr_client();
|
|
||||||
let app_state = app_state();
|
|
||||||
let mut message: Option<Event> = None;
|
|
||||||
|
|
||||||
if let Ok(event) = Self::get_unwrapped_event(target.id).await {
|
|
||||||
message = Some(event);
|
|
||||||
} else if let Ok(unwrapped) = client.unwrap_gift_wrap(target).await {
|
|
||||||
// Sign the unwrapped event with a RANDOM KEYS
|
|
||||||
if let Ok(event) = unwrapped.rumor.sign_with_keys(&Keys::generate()) {
|
|
||||||
// Save this event to the database for future use.
|
|
||||||
if let Err(e) = Self::set_unwrapped_event(target.id, &event).await {
|
|
||||||
log::warn!("Failed to cache unwrapped event: {e}")
|
|
||||||
}
|
|
||||||
|
|
||||||
message = Some(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(event) = message {
|
|
||||||
// Send all pubkeys to the metadata batch to sync data
|
|
||||||
for public_key in event.all_pubkeys() {
|
|
||||||
app_state.ingester.send(public_key).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
match event.created_at >= app_state.init_at {
|
|
||||||
// New message: send a signal to notify the UI
|
|
||||||
true => {
|
|
||||||
app_state
|
|
||||||
.signal
|
|
||||||
.send(SignalKind::NewMessage((target.id, event)))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
// Old message: Coop is probably processing the user's messages during initial load
|
|
||||||
false => {
|
|
||||||
app_state
|
|
||||||
.gift_wrap_processing
|
|
||||||
.store(true, Ordering::Release);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_registry_event(
|
|
||||||
&mut self,
|
|
||||||
event: &RegistryEvent,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
RegistryEvent::Open(room) => {
|
|
||||||
if let Some(room) = room.upgrade() {
|
|
||||||
self.dock.update(cx, |this, cx| {
|
|
||||||
let panel = chat::init(room, window, cx);
|
|
||||||
this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.push_notification(t!("common.room_error"), cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RegistryEvent::Close(..) => {
|
|
||||||
self.dock.update(cx, |this, cx| {
|
|
||||||
this.focus_tab_panel(window, cx);
|
|
||||||
|
|
||||||
cx.defer_in(window, |_, window, cx| {
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
window.close_all_modals(cx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auth(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context<Self>) {
|
fn auth(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let settings = AppSettings::global(cx);
|
let settings = AppSettings::global(cx);
|
||||||
|
|
||||||
@@ -835,16 +422,17 @@ impl ChatSpace {
|
|||||||
relay.resubscribe().await?;
|
relay.resubscribe().await?;
|
||||||
|
|
||||||
// Get all failed events that need to be resent
|
// Get all failed events that need to be resent
|
||||||
let mut queue = app_state.resend_queue.write().await;
|
let mut event_tracker = app_state.event_tracker.write().await;
|
||||||
|
|
||||||
let ids: Vec<EventId> = queue
|
let ids: Vec<EventId> = event_tracker
|
||||||
|
.resend_queue
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, url)| relay_url == *url)
|
.filter(|(_, url)| relay_url == *url)
|
||||||
.map(|(id, _)| *id)
|
.map(|(id, _)| *id)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for id in ids.into_iter() {
|
for id in ids.into_iter() {
|
||||||
if let Some(relay_url) = queue.remove(&id) {
|
if let Some(relay_url) = event_tracker.resend_queue.remove(&id) {
|
||||||
if let Some(event) = client.database().event_by_id(&id).await? {
|
if let Some(event) = client.database().event_by_id(&id).await? {
|
||||||
let event_id = relay.send_event(&event).await?;
|
let event_id = relay.send_event(&event).await?;
|
||||||
|
|
||||||
@@ -854,8 +442,8 @@ impl ChatSpace {
|
|||||||
success: HashSet::from([relay_url]),
|
success: HashSet::from([relay_url]),
|
||||||
};
|
};
|
||||||
|
|
||||||
app_state.sent_ids.write().await.insert(event_id);
|
event_tracker.sent_ids.insert(event_id);
|
||||||
app_state.resent_ids.write().await.push(output);
|
event_tracker.resent_ids.push(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use app_state::constants::{APP_ID, APP_NAME};
|
||||||
|
use app_state::{app_state, nostr_client};
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use global::constants::{APP_ID, APP_NAME};
|
|
||||||
use global::{app_state, nostr_client};
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
|
point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
|
||||||
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use app_state::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
||||||
|
use app_state::state::SignalKind;
|
||||||
|
use app_state::{app_state, nostr_client};
|
||||||
use client_keys::ClientKeys;
|
use client_keys::ClientKeys;
|
||||||
use common::display::RenderedProfile;
|
use common::display::RenderedProfile;
|
||||||
use global::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
|
||||||
use global::{app_state, nostr_client, SignalKind};
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use app_state::{app_state, nostr_client};
|
||||||
use common::display::{RenderedProfile, RenderedTimestamp};
|
use common::display::{RenderedProfile, RenderedTimestamp};
|
||||||
use common::nip96::nip96_upload;
|
use common::nip96::nip96_upload;
|
||||||
use global::{app_state, nostr_client};
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext,
|
div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext,
|
||||||
@@ -170,7 +170,8 @@ impl Chat {
|
|||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let app_state = app_state();
|
let app_state = app_state();
|
||||||
let sent_ids = app_state.sent_ids.read().await;
|
let event_tracker = app_state.event_tracker.read().await;
|
||||||
|
let sent_ids = event_tracker.sent_ids();
|
||||||
|
|
||||||
this.update_in(cx, |this, _window, cx| {
|
this.update_in(cx, |this, _window, cx| {
|
||||||
if !sent_ids.contains(&gift_wrap_id) {
|
if !sent_ids.contains(&gift_wrap_id) {
|
||||||
@@ -1240,6 +1241,7 @@ impl Chat {
|
|||||||
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
let app_state = app_state();
|
let app_state = app_state();
|
||||||
|
let event_tracker = app_state.event_tracker.read().await;
|
||||||
let mut relays: Vec<RelayUrl> = vec![];
|
let mut relays: Vec<RelayUrl> = vec![];
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
@@ -1249,7 +1251,7 @@ impl Chat {
|
|||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||||
if let Some(Ok(id)) = event.tags.identifier().map(EventId::parse) {
|
if let Some(Ok(id)) = event.tags.identifier().map(EventId::parse) {
|
||||||
if let Some(urls) = app_state.seen_on_relays.read().await.get(&id).cloned() {
|
if let Some(urls) = event_tracker.seen_on_relays.get(&id).cloned() {
|
||||||
relays.extend(urls);
|
relays.extend(urls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ use std::ops::Range;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
|
use app_state::constants::BOOTSTRAP_RELAYS;
|
||||||
|
use app_state::{app_state, nostr_client};
|
||||||
use common::display::{RenderedProfile, TextUtils};
|
use common::display::{RenderedProfile, TextUtils};
|
||||||
use common::nip05::nip05_profile;
|
use common::nip05::nip05_profile;
|
||||||
use global::constants::BOOTSTRAP_RELAYS;
|
|
||||||
use global::{app_state, nostr_client};
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
|
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use app_state::nostr_client;
|
||||||
use common::nip96::nip96_upload;
|
use common::nip96::nip96_upload;
|
||||||
use global::nostr_client;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use app_state::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
||||||
|
use app_state::nostr_client;
|
||||||
use client_keys::ClientKeys;
|
use client_keys::ClientKeys;
|
||||||
use global::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
|
||||||
use global::nostr_client;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use app_state::constants::{ACCOUNT_IDENTIFIER, NIP17_RELAYS, NIP65_RELAYS};
|
||||||
|
use app_state::nostr_client;
|
||||||
use common::nip96::nip96_upload;
|
use common::nip96::nip96_upload;
|
||||||
use global::constants::{ACCOUNT_IDENTIFIER, NIP17_RELAYS, NIP65_RELAYS};
|
|
||||||
use global::nostr_client;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
|
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
|
||||||
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
|
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use app_state::constants::{
|
||||||
|
ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT,
|
||||||
|
};
|
||||||
|
use app_state::nostr_client;
|
||||||
use client_keys::ClientKeys;
|
use client_keys::ClientKeys;
|
||||||
use common::display::TextUtils;
|
use common::display::TextUtils;
|
||||||
use global::constants::{ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
|
||||||
use global::nostr_client;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, px, relative, svg, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
|
div, img, px, relative, svg, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use app_state::constants::BOOTSTRAP_RELAYS;
|
||||||
|
use app_state::nostr_client;
|
||||||
use common::display::{shorten_pubkey, RenderedProfile, RenderedTimestamp};
|
use common::display::{shorten_pubkey, RenderedProfile, RenderedTimestamp};
|
||||||
use common::nip05::nip05_verify;
|
use common::nip05::nip05_verify;
|
||||||
use global::constants::BOOTSTRAP_RELAYS;
|
|
||||||
use global::nostr_client;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity,
|
div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use global::constants::NIP17_RELAYS;
|
use app_state::constants::NIP17_RELAYS;
|
||||||
use global::{app_state, nostr_client};
|
use app_state::{app_state, nostr_client};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, uniform_list, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
div, px, uniform_list, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ use std::ops::Range;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
|
use app_state::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||||
|
use app_state::state::UnwrappingStatus;
|
||||||
|
use app_state::{app_state, nostr_client};
|
||||||
use common::debounced_delay::DebouncedDelay;
|
use common::debounced_delay::DebouncedDelay;
|
||||||
use common::display::{RenderedTimestamp, TextUtils};
|
use common::display::{RenderedTimestamp, TextUtils};
|
||||||
use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
|
||||||
use global::{app_state, nostr_client, UnwrappingStatus};
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity,
|
deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use app_state::nostr_client;
|
||||||
use common::display::RenderedProfile;
|
use common::display::RenderedProfile;
|
||||||
use common::nip05::nip05_verify;
|
use common::nip05::nip05_verify;
|
||||||
use global::nostr_client;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
|
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
|
||||||
|
|||||||
@@ -1,261 +0,0 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use flume::{Receiver, Sender};
|
|
||||||
use nostr_lmdb::NostrLMDB;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use paths::nostr_file;
|
|
||||||
use smol::lock::RwLock;
|
|
||||||
|
|
||||||
use crate::paths::support_dir;
|
|
||||||
|
|
||||||
pub mod constants;
|
|
||||||
pub mod paths;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct AuthRequest {
|
|
||||||
pub url: RelayUrl,
|
|
||||||
pub challenge: String,
|
|
||||||
pub sending: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthRequest {
|
|
||||||
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
|
|
||||||
Self {
|
|
||||||
challenge: challenge.into(),
|
|
||||||
sending: false,
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Notice {
|
|
||||||
RelayFailed(RelayUrl),
|
|
||||||
AuthFailed(RelayUrl),
|
|
||||||
Custom(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Notice {
|
|
||||||
pub fn as_str(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Notice::AuthFailed(url) => format!("Authenticate failed for relay {url}"),
|
|
||||||
Notice::RelayFailed(url) => format!("Failed to connect the relay {url}"),
|
|
||||||
Notice::Custom(msg) => msg.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum UnwrappingStatus {
|
|
||||||
#[default]
|
|
||||||
Initialized,
|
|
||||||
Processing,
|
|
||||||
Complete,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signals sent through the global event channel to notify UI
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum SignalKind {
|
|
||||||
/// A signal to notify UI that the client's signer has been set
|
|
||||||
SignerSet(PublicKey),
|
|
||||||
|
|
||||||
/// A signal to notify UI that the client's signer has been unset
|
|
||||||
SignerUnset,
|
|
||||||
|
|
||||||
/// A signal to notify UI that the relay requires authentication
|
|
||||||
Auth(AuthRequest),
|
|
||||||
|
|
||||||
/// A signal to notify UI that the browser proxy service is down
|
|
||||||
ProxyDown,
|
|
||||||
|
|
||||||
/// A signal to notify UI that a new profile has been received
|
|
||||||
NewProfile(Profile),
|
|
||||||
|
|
||||||
/// A signal to notify UI that a new gift wrap event has been received
|
|
||||||
NewMessage((EventId, Event)),
|
|
||||||
|
|
||||||
/// A signal to notify UI that no DM relays for current user was found
|
|
||||||
RelaysNotFound,
|
|
||||||
|
|
||||||
/// A signal to notify UI that gift wrap status has changed
|
|
||||||
GiftWrapStatus(UnwrappingStatus),
|
|
||||||
|
|
||||||
/// A signal to notify UI that there are errors or notices occurred
|
|
||||||
Notice(Notice),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Signal {
|
|
||||||
rx: Receiver<SignalKind>,
|
|
||||||
tx: Sender<SignalKind>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Signal {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Signal {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (tx, rx) = flume::bounded::<SignalKind>(2048);
|
|
||||||
Self { rx, tx }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn receiver(&self) -> &Receiver<SignalKind> {
|
|
||||||
&self.rx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send(&self, kind: SignalKind) {
|
|
||||||
if let Err(e) = self.tx.send_async(kind).await {
|
|
||||||
log::error!("Failed to send signal: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Ingester {
|
|
||||||
rx: Receiver<PublicKey>,
|
|
||||||
tx: Sender<PublicKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Ingester {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ingester {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (tx, rx) = flume::bounded::<PublicKey>(1024);
|
|
||||||
Self { rx, tx }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn receiver(&self) -> &Receiver<PublicKey> {
|
|
||||||
&self.rx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send(&self, public_key: PublicKey) {
|
|
||||||
if let Err(e) = self.tx.send_async(public_key).await {
|
|
||||||
log::error!("Failed to send public key: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A simple storage to store all states that using across the application.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AppState {
|
|
||||||
/// The timestamp when the application was initialized.
|
|
||||||
pub init_at: Timestamp,
|
|
||||||
|
|
||||||
/// The timestamp when the application was last used.
|
|
||||||
pub last_used_at: Option<Timestamp>,
|
|
||||||
|
|
||||||
/// Whether this is the first run of the application.
|
|
||||||
pub is_first_run: AtomicBool,
|
|
||||||
|
|
||||||
/// Subscription ID for listening to gift wrap events from relays.
|
|
||||||
pub gift_wrap_sub_id: SubscriptionId,
|
|
||||||
|
|
||||||
/// Auto-close options for relay subscriptions
|
|
||||||
pub auto_close_opts: Option<SubscribeAutoCloseOptions>,
|
|
||||||
|
|
||||||
/// Whether gift wrap processing is in progress.
|
|
||||||
pub gift_wrap_processing: AtomicBool,
|
|
||||||
|
|
||||||
/// Tracking events sent by Coop in the current session
|
|
||||||
pub sent_ids: RwLock<HashSet<EventId>>,
|
|
||||||
|
|
||||||
/// Tracking events seen on which relays in the current session
|
|
||||||
pub seen_on_relays: RwLock<HashMap<EventId, HashSet<RelayUrl>>>,
|
|
||||||
|
|
||||||
/// Tracking events that have been resent by Coop in the current session
|
|
||||||
pub resent_ids: RwLock<Vec<Output<EventId>>>,
|
|
||||||
|
|
||||||
/// Temporarily store events that need to be resent later
|
|
||||||
pub resend_queue: RwLock<HashMap<EventId, RelayUrl>>,
|
|
||||||
|
|
||||||
/// Signal channel for communication between Nostr and GPUI
|
|
||||||
pub signal: Signal,
|
|
||||||
|
|
||||||
/// Ingester channel for processing public keys
|
|
||||||
pub ingester: Ingester,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AppState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let init_at = Timestamp::now();
|
|
||||||
let first_run = first_run();
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
|
||||||
|
|
||||||
let signal = Signal::default();
|
|
||||||
let ingester = Ingester::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
init_at,
|
|
||||||
signal,
|
|
||||||
ingester,
|
|
||||||
last_used_at: None,
|
|
||||||
is_first_run: AtomicBool::new(first_run),
|
|
||||||
gift_wrap_sub_id: SubscriptionId::new("inbox"),
|
|
||||||
gift_wrap_processing: AtomicBool::new(false),
|
|
||||||
auto_close_opts: Some(opts),
|
|
||||||
sent_ids: RwLock::new(HashSet::new()),
|
|
||||||
seen_on_relays: RwLock::new(HashMap::new()),
|
|
||||||
resent_ids: RwLock::new(Vec::new()),
|
|
||||||
resend_queue: RwLock::new(HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
|
|
||||||
static APP_STATE: OnceLock<AppState> = OnceLock::new();
|
|
||||||
|
|
||||||
pub fn nostr_client() -> &'static Client {
|
|
||||||
NOSTR_CLIENT.get_or_init(|| {
|
|
||||||
// rustls uses the `aws_lc_rs` provider by default
|
|
||||||
// This only errors if the default provider has already
|
|
||||||
// been installed. We can ignore this `Result`.
|
|
||||||
rustls::crypto::aws_lc_rs::default_provider()
|
|
||||||
.install_default()
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized");
|
|
||||||
|
|
||||||
let opts = ClientOptions::new()
|
|
||||||
.gossip(true)
|
|
||||||
.automatic_authentication(false)
|
|
||||||
.verify_subscriptions(false)
|
|
||||||
.sleep_when_idle(SleepWhenIdle::Enabled {
|
|
||||||
timeout: Duration::from_secs(600),
|
|
||||||
});
|
|
||||||
|
|
||||||
ClientBuilder::default().database(lmdb).opts(opts).build()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn app_state() -> &'static AppState {
|
|
||||||
APP_STATE.get_or_init(AppState::new)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn first_run() -> bool {
|
|
||||||
let flag = support_dir().join(format!(".{}-first_run", env!("CARGO_PKG_VERSION")));
|
|
||||||
|
|
||||||
if !flag.exists() {
|
|
||||||
if std::fs::write(&flag, "").is_err() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true // First run
|
|
||||||
} else {
|
|
||||||
false // Not first run
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
app_state = { path = "../app_state" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ use std::cmp::Reverse;
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use app_state::nostr_client;
|
||||||
|
use app_state::state::UnwrappingStatus;
|
||||||
use common::event::EventUtils;
|
use common::event::EventUtils;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
use global::{nostr_client, UnwrappingStatus};
|
|
||||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window};
|
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ use std::hash::{Hash, Hasher};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
|
use app_state::constants::SEND_RETRY;
|
||||||
|
use app_state::{app_state, nostr_client};
|
||||||
use common::display::RenderedProfile;
|
use common::display::RenderedProfile;
|
||||||
use common::event::EventUtils;
|
use common::event::EventUtils;
|
||||||
use global::constants::SEND_RETRY;
|
|
||||||
use global::{app_state, nostr_client};
|
|
||||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task};
|
use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task};
|
||||||
use itertools::Itertools;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
use crate::Registry;
|
use crate::Registry;
|
||||||
@@ -358,18 +357,6 @@ impl Room {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(&self, relays: Vec<RelayUrl>, cx: &App) -> Task<Result<(), Error>> {
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let client = nostr_client();
|
|
||||||
|
|
||||||
for relay in relays.into_iter() {
|
|
||||||
client.disconnect_relay(relay).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads all messages for this room from the database
|
/// Loads all messages for this room from the database
|
||||||
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Event>, Error>> {
|
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Event>, Error>> {
|
||||||
let members = self.members.clone();
|
let members = self.members.clone();
|
||||||
@@ -378,13 +365,14 @@ impl Room {
|
|||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
let sent_ids = app_state()
|
let sent_ids: Vec<EventId> = app_state()
|
||||||
.sent_ids
|
.event_tracker
|
||||||
.read()
|
.read()
|
||||||
.await
|
.await
|
||||||
|
.sent_ids()
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.collect_vec();
|
.collect();
|
||||||
|
|
||||||
// Get seen events from database
|
// Get seen events from database
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
@@ -508,12 +496,17 @@ impl Room {
|
|||||||
let rumor = rumor.clone();
|
let rumor = rumor.clone();
|
||||||
let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, vec![]).await?;
|
let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, vec![]).await?;
|
||||||
|
|
||||||
let Ok(relay_urls) = Self::messaging_relays(receiver).await else {
|
let gossip = app_state.gossip.read().await;
|
||||||
|
let urls = gossip.messaging_relays(&receiver);
|
||||||
|
|
||||||
|
// Check if there are any relays to send the event to
|
||||||
|
if urls.is_empty() {
|
||||||
reports.push(SendReport::new(receiver).not_found());
|
reports.push(SendReport::new(receiver).not_found());
|
||||||
continue;
|
continue;
|
||||||
};
|
}
|
||||||
|
|
||||||
match client.send_event_to(relay_urls, &event).await {
|
// Send the event to the relays
|
||||||
|
match client.send_event_to(urls, &event).await {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
let id = output.id().to_owned();
|
let id = output.id().to_owned();
|
||||||
let auth_required = output.failed.iter().any(|m| m.1.starts_with("auth-"));
|
let auth_required = output.failed.iter().any(|m| m.1.starts_with("auth-"));
|
||||||
@@ -522,7 +515,8 @@ impl Room {
|
|||||||
if auth_required {
|
if auth_required {
|
||||||
// Wait for authenticated and resent event successfully
|
// Wait for authenticated and resent event successfully
|
||||||
for attempt in 0..=SEND_RETRY {
|
for attempt in 0..=SEND_RETRY {
|
||||||
let ids = app_state.resent_ids.read().await;
|
let retry_manager = app_state.event_tracker.read().await;
|
||||||
|
let ids = retry_manager.resent_ids();
|
||||||
|
|
||||||
// Check if event was successfully resent
|
// Check if event was successfully resent
|
||||||
if let Some(output) = ids.iter().find(|e| e.id() == &id).cloned() {
|
if let Some(output) = ids.iter().find(|e| e.id() == &id).cloned() {
|
||||||
@@ -555,8 +549,15 @@ impl Room {
|
|||||||
|
|
||||||
// Only send a backup message to current user if sent successfully to others
|
// Only send a backup message to current user if sent successfully to others
|
||||||
if reports.iter().all(|r| r.is_sent_success()) && backup {
|
if reports.iter().all(|r| r.is_sent_success()) && backup {
|
||||||
if let Ok(relay_urls) = Self::messaging_relays(public_key).await {
|
let gossip = app_state.gossip.read().await;
|
||||||
match client.send_event_to(relay_urls, &event).await {
|
let urls = gossip.messaging_relays(&public_key);
|
||||||
|
|
||||||
|
// Check if there are any relays to send the event to
|
||||||
|
if urls.is_empty() {
|
||||||
|
reports.push(SendReport::new(public_key).not_found());
|
||||||
|
} else {
|
||||||
|
// Send the event to the relays
|
||||||
|
match client.send_event_to(urls, &event).await {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
reports.push(SendReport::new(public_key).status(output));
|
reports.push(SendReport::new(public_key).status(output));
|
||||||
}
|
}
|
||||||
@@ -564,8 +565,6 @@ impl Room {
|
|||||||
reports.push(SendReport::new(public_key).error(e.to_string()));
|
reports.push(SendReport::new(public_key).error(e.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
reports.push(SendReport::new(public_key).not_found());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reports.push(SendReport::new(public_key).on_hold(event));
|
reports.push(SendReport::new(public_key).on_hold(event));
|
||||||
@@ -583,6 +582,8 @@ impl Room {
|
|||||||
) -> Task<Result<Vec<SendReport>, Error>> {
|
) -> Task<Result<Vec<SendReport>, Error>> {
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
|
let app_state = app_state();
|
||||||
|
|
||||||
let mut resend_reports = vec![];
|
let mut resend_reports = vec![];
|
||||||
|
|
||||||
for report in reports.into_iter() {
|
for report in reports.into_iter() {
|
||||||
@@ -611,8 +612,15 @@ impl Room {
|
|||||||
|
|
||||||
// Process the on hold event if it exists
|
// Process the on hold event if it exists
|
||||||
if let Some(event) = report.on_hold {
|
if let Some(event) = report.on_hold {
|
||||||
if let Ok(relay_urls) = Self::messaging_relays(receiver).await {
|
let gossip = app_state.gossip.read().await;
|
||||||
match client.send_event_to(relay_urls, &event).await {
|
let urls = gossip.messaging_relays(&receiver);
|
||||||
|
|
||||||
|
// Check if there are any relays to send the event to
|
||||||
|
if urls.is_empty() {
|
||||||
|
resend_reports.push(SendReport::new(receiver).not_found());
|
||||||
|
} else {
|
||||||
|
// Send the event to the relays
|
||||||
|
match client.send_event_to(urls, &event).await {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
resend_reports.push(SendReport::new(receiver).status(output));
|
resend_reports.push(SendReport::new(receiver).status(output));
|
||||||
}
|
}
|
||||||
@@ -620,8 +628,6 @@ impl Room {
|
|||||||
resend_reports.push(SendReport::new(receiver).error(e.to_string()));
|
resend_reports.push(SendReport::new(receiver).error(e.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
resend_reports.push(SendReport::new(receiver).not_found());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -629,36 +635,4 @@ impl Room {
|
|||||||
Ok(resend_reports)
|
Ok(resend_reports)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets messaging relays for public key
|
|
||||||
async fn messaging_relays(public_key: PublicKey) -> Result<Vec<RelayUrl>, Error> {
|
|
||||||
let client = nostr_client();
|
|
||||||
let mut relay_urls = vec![];
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::InboxRelays)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
|
||||||
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
|
||||||
|
|
||||||
// Check if at least one URL exists
|
|
||||||
if urls.is_empty() {
|
|
||||||
return Err(anyhow!("Not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to relays
|
|
||||||
for url in urls.iter() {
|
|
||||||
client.add_relay(url).await?;
|
|
||||||
client.connect_relay(url).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
relay_urls.extend(urls.into_iter().take(3).unique());
|
|
||||||
} else {
|
|
||||||
return Err(anyhow!("Not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(relay_urls)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
global = { path = "../global" }
|
app_state = { path = "../app_state" }
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use global::constants::SETTINGS_IDENTIFIER;
|
use app_state::constants::SETTINGS_IDENTIFIER;
|
||||||
use global::nostr_client;
|
use app_state::nostr_client;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
global = { path = "../global" }
|
app_state = { path = "../app_state" }
|
||||||
|
|
||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
|||||||
Reference in New Issue
Block a user