commit ea69f478e3f6631449f3cd27f72388c7d0c62c8e
parent 932ea04384e60fb50f317d26aa100b1ea4441559
Author: triesap <tyson@radroots.org>
Date: Fri, 22 Aug 2025 17:15:16 -0700
nostr: add `radroots-nostr` crate
Diffstat:
16 files changed, 1322 insertions(+), 6 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -28,6 +28,17 @@ dependencies = [
]
[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -149,6 +160,49 @@ dependencies = [
]
[[package]]
+name = "async-utility"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151"
+dependencies = [
+ "futures-util",
+ "gloo-timers",
+ "tokio",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-wsocket"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a7d8c7d34a225ba919dd9ba44d4b9106d20142da545e086be8ae21d1897e043"
+dependencies = [
+ "async-utility",
+ "futures",
+ "futures-util",
+ "js-sys",
+ "tokio",
+ "tokio-rustls",
+ "tokio-socks",
+ "tokio-tungstenite",
+ "url",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "atomic-destructor"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -271,6 +325,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -295,6 +355,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -480,11 +546,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
- "rand_core",
+ "rand_core 0.6.4",
"typenum",
]
[[package]]
+name = "data-encoding"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
+[[package]]
name = "deranged"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -525,6 +597,12 @@ dependencies = [
]
[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -556,6 +634,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -565,6 +649,71 @@ dependencies = [
]
[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -594,9 +743,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
+ "wasm-bindgen",
]
[[package]]
@@ -606,6 +757,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -661,6 +824,108 @@ dependencies = [
]
[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots 1.0.2",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2 0.6.0",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -835,6 +1100,22 @@ dependencies = [
]
[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -898,6 +1179,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
+name = "lru"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -939,6 +1232,12 @@ dependencies = [
]
[[package]]
+name = "negentropy"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
+
+[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -954,6 +1253,7 @@ version = "0.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62a97d745f1bd8d5e05a978632bbb87b0614567d5142906fe7c86fb2440faac6"
dependencies = [
+ "aes",
"base64 0.22.1",
"bech32",
"bip39",
@@ -972,6 +1272,47 @@ dependencies = [
]
[[package]]
+name = "nostr-database"
+version = "0.43.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c75a8c2175d2785ba73cfddef21d1e30da5fbbdf158569b6808ba44973a15b"
+dependencies = [
+ "lru",
+ "nostr",
+ "tokio",
+]
+
+[[package]]
+name = "nostr-relay-pool"
+version = "0.43.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "265d9b44771ed15db93b183a0c93dbb703b2b0d0b74dffb5c2a081be52373a5a"
+dependencies = [
+ "async-utility",
+ "async-wsocket",
+ "atomic-destructor",
+ "lru",
+ "negentropy",
+ "nostr",
+ "nostr-database",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "nostr-sdk"
+version = "0.43.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "599f8963d6a1522a13b1a2b0ea6e168acfc367706606f1d33fa595e91fa22db0"
+dependencies = [
+ "async-utility",
+ "nostr",
+ "nostr-database",
+ "nostr-relay-pool",
+ "tokio",
+]
+
+[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1046,7 +1387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
- "rand_core",
+ "rand_core 0.6.4",
"subtle",
]
@@ -1123,6 +1464,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1167,6 +1514,61 @@ dependencies = [
]
[[package]]
+name = "quinn"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2 0.5.10",
+ "thiserror 2.0.16",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.3",
+ "lru-slab",
+ "rand 0.9.2",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.16",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2 0.5.10",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1221,6 +1623,20 @@ dependencies = [
]
[[package]]
+name = "radroots-nostr"
+version = "0.1.0"
+dependencies = [
+ "nostr",
+ "nostr-sdk",
+ "radroots-events",
+ "radroots-events-codec",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
name = "radroots-runtime"
version = "0.1.0"
dependencies = [
@@ -1256,8 +1672,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
]
[[package]]
@@ -1267,7 +1693,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
]
[[package]]
@@ -1280,6 +1716,15 @@ dependencies = [
]
[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
+[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1324,6 +1769,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
+name = "reqwest"
+version = "0.12.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots 1.0.2",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
name = "ron"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1373,6 +1870,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
name = "rustix"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1386,6 +1889,41 @@ dependencies = [
]
[[package]]
+name = "rustls"
+version = "0.23.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1424,7 +1962,7 @@ version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
dependencies = [
- "rand",
+ "rand 0.8.5",
"secp256k1-sys",
"serde",
]
@@ -1480,6 +2018,29 @@ dependencies = [
]
[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1527,6 +2088,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1556,6 +2137,15 @@ dependencies = [
]
[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1700,12 +2290,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
dependencies = [
"backtrace",
+ "bytes",
"io-uring",
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"slab",
+ "socket2 0.6.0",
"tokio-macros",
"windows-sys 0.59.0",
]
@@ -1722,6 +2314,44 @@ dependencies = [
]
[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-socks"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
+dependencies = [
+ "either",
+ "futures-util",
+ "thiserror 1.0.69",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
+dependencies = [
+ "futures-util",
+ "log",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tungstenite",
+ "webpki-roots 0.26.11",
+]
+
+[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1763,6 +2393,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1836,6 +2511,31 @@ dependencies = [
]
[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.9.2",
+ "rustls",
+ "rustls-pki-types",
+ "sha1",
+ "thiserror 2.0.16",
+ "utf-8",
+]
+
+[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1901,6 +2601,12 @@ dependencies = [
]
[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
name = "url"
version = "2.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1913,6 +2619,12 @@ dependencies = [
]
[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1937,6 +2649,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1978,6 +2699,19 @@ dependencies = [
]
[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2020,6 +2754,34 @@ dependencies = [
]
[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.2",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2102,6 +2864,15 @@ dependencies = [
[[package]]
name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
diff --git a/Cargo.toml b/Cargo.toml
@@ -14,14 +14,18 @@ license = "AGPL-3.0"
radroots-core = { path = "crates/core", version = "0.1.0", default-features = false }
radroots-events = { path = "crates/events", version = "0.1.0", default-features = false }
radroots-events-codec = { path = "crates/events-codec", version = "0.1.0", default-features = false }
+radroots-events-indexed = { path = "crates/events-indexed", version = "0.1.0", default-features = false }
+radroots-nostr = { path = "crates/nostr", version = "0.1.0", default-features = false }
radroots-trade = { path = "crates/trade", version = "0.1.0", default-features = false }
anyhow = { version = "1" }
clap = { version = "4" }
config = { version = "0.14" }
+nostr-sdk = { version = "0.43.0" }
nostr = { version = "0.43.0" }
serde = { version = "1", default-features = false }
serde_json = { version = "1", default-features = false }
+reqwest = { version = "0.12", default-features = false }
rust_decimal = { version = "1", default-features = false }
rust_decimal_macros = { version = "1" }
tempfile = { version = "3" }
diff --git a/crates/nostr/Cargo.toml b/crates/nostr/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "radroots-nostr"
+version.workspace = true
+edition.workspace = true
+authors = ["Radroots Authors"]
+rust-version.workspace = true
+license.workspace = true
+
+[features]
+default = ["std", "sdk"]
+std = []
+sdk = ["dep:nostr-sdk"]
+codec = ["dep:radroots-events", "dep:radroots-events-codec"]
+http = ["dep:reqwest"]
+
+[dependencies]
+radroots-events = { workspace = true, optional = true, default-features = false }
+radroots-events-codec = { workspace = true, optional = true, default-features = false }
+nostr = { workspace = true, features = ["nip04"] }
+nostr-sdk = { workspace = true, optional = true }
+reqwest = { workspace = true, optional = true, default-features = false, features = ["json", "rustls-tls"] }
+serde = { workspace = true }
+serde_json = { workspace = true }
+thiserror = { workspace = true }
diff --git a/crates/nostr/src/client.rs b/crates/nostr/src/client.rs
@@ -0,0 +1,22 @@
+use core::time::Duration;
+use nostr::{event::Event, event::EventBuilder, event::EventId, filter::Filter};
+use nostr_sdk::{Client, prelude::*};
+
+use crate::error::NostrUtilsError;
+
+pub async fn nostr_send_event(
+ client: &Client,
+ event: EventBuilder,
+) -> Result<Output<EventId>, NostrUtilsError> {
+ Ok(client.send_event_builder(event).await?)
+}
+
+pub async fn nostr_fetch_event_by_id(client: Client, id: &str) -> Result<Event, NostrUtilsError> {
+ let event_id = EventId::parse(id)?;
+ let filter = Filter::new().id(event_id);
+ let events = client.fetch_events(filter, Duration::from_secs(10)).await?;
+ let event = events
+ .first()
+ .ok_or_else(|| NostrUtilsError::EventNotFound(event_id.to_hex()))?;
+ Ok(event.clone())
+}
diff --git a/crates/nostr/src/codec_adapters.rs b/crates/nostr/src/codec_adapters.rs
@@ -0,0 +1,118 @@
+extern crate alloc;
+use alloc::{string::String, vec::Vec};
+
+use nostr::event::Event;
+
+use radroots_events_codec::job::{
+ error::JobParseError, feedback::decode as fb_decode, request::decode as req_decode,
+ result::decode as res_decode,
+};
+
+fn event_id(e: &Event) -> String {
+ e.id.to_hex()
+}
+
+fn author(e: &Event) -> String {
+ e.pubkey.to_hex()
+}
+
+fn published_at(e: &Event) -> u32 {
+ e.created_at.as_u64() as u32
+}
+
+fn kind_u32(e: &Event) -> u32 {
+ e.kind.as_u16() as u32
+}
+
+fn content(e: &Event) -> String {
+ e.content.clone()
+}
+
+fn tags_vec(e: &Event) -> Vec<Vec<String>> {
+ e.tags.iter().map(|t| t.as_slice().to_vec()).collect()
+}
+
+fn sig_hex(e: &Event) -> String {
+ e.sig.to_string()
+}
+
+pub fn to_job_request_metadata(
+ e: &Event,
+) -> Result<radroots_events::job::request::models::RadrootsJobRequestEventMetadata, JobParseError> {
+ req_decode::metadata_from_event(
+ event_id(e),
+ author(e),
+ published_at(e),
+ kind_u32(e),
+ tags_vec(e),
+ )
+}
+
+pub fn to_job_result_metadata(
+ e: &Event,
+) -> Result<radroots_events::job::result::models::RadrootsJobResultEventMetadata, JobParseError> {
+ res_decode::metadata_from_event(
+ event_id(e),
+ author(e),
+ published_at(e),
+ kind_u32(e),
+ content(e),
+ tags_vec(e),
+ )
+}
+
+pub fn to_job_feedback_metadata(
+ e: &Event,
+) -> Result<radroots_events::job::feedback::models::RadrootsJobFeedbackEventMetadata, JobParseError>
+{
+ fb_decode::metadata_from_event(
+ event_id(e),
+ author(e),
+ published_at(e),
+ kind_u32(e),
+ content(e),
+ tags_vec(e),
+ )
+}
+
+pub fn to_job_request_index(
+ e: &Event,
+) -> Result<radroots_events::job::request::models::RadrootsJobRequestEventIndex, JobParseError> {
+ req_decode::index_from_event(
+ event_id(e),
+ author(e),
+ published_at(e),
+ kind_u32(e),
+ content(e),
+ tags_vec(e),
+ sig_hex(e),
+ )
+}
+
+pub fn to_job_result_index(
+ e: &Event,
+) -> Result<radroots_events::job::result::models::RadrootsJobResultEventIndex, JobParseError> {
+ res_decode::index_from_event(
+ event_id(e),
+ author(e),
+ published_at(e),
+ kind_u32(e),
+ content(e),
+ tags_vec(e),
+ sig_hex(e),
+ )
+}
+
+pub fn to_job_feedback_index(
+ e: &Event,
+) -> Result<radroots_events::job::feedback::models::RadrootsJobFeedbackEventIndex, JobParseError> {
+ fb_decode::index_from_event(
+ event_id(e),
+ author(e),
+ published_at(e),
+ kind_u32(e),
+ content(e),
+ tags_vec(e),
+ sig_hex(e),
+ )
+}
diff --git a/crates/nostr/src/error.rs b/crates/nostr/src/error.rs
@@ -0,0 +1,36 @@
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum NostrUtilsError {
+ #[cfg(feature = "sdk")]
+ #[error("Client error: {0}")]
+ ClientError(#[from] nostr_sdk::client::Error),
+
+ #[cfg(feature = "sdk")]
+ #[error("Database error: {0}")]
+ DatabaseError(#[from] nostr_sdk::prelude::DatabaseError),
+
+ #[error("Event error: {0}")]
+ EventError(#[from] nostr::event::Error),
+
+ #[error("Event not found: {0}")]
+ EventNotFound(String),
+
+ #[error("Event builder failure: {0}")]
+ EventBuildError(#[from] nostr::event::builder::Error),
+}
+
+#[derive(Debug, Error)]
+pub enum NostrTagsResolveError {
+ #[error("Missing public key 'p' tag in encrypted event: {0:?}")]
+ MissingPTag(nostr::Event),
+
+ #[error("Encrypted event recipient mismatch")]
+ NotRecipient,
+
+ #[error("Decryption error: {0}")]
+ DecryptionError(String),
+
+ #[error("Failed to parse decrypted tag JSON: {0}")]
+ ParseError(#[from] serde_json::Error),
+}
diff --git a/crates/nostr/src/events/jobs.rs b/crates/nostr/src/events/jobs.rs
@@ -0,0 +1,33 @@
+use nostr::{
+ event::{Event, EventBuilder, Tag},
+ nips::nip90::{DataVendingMachineStatus, JobFeedbackData},
+};
+
+use crate::error::NostrUtilsError;
+
+pub fn nostr_build_event_job_result(
+ job_request: &Event,
+ payload: impl Into<String>,
+ millisats: u64,
+ bolt11: Option<String>,
+ tags: Option<Vec<Tag>>,
+) -> Result<EventBuilder, NostrUtilsError> {
+ let builder = EventBuilder::job_result(job_request.clone(), payload, millisats, bolt11)?
+ .tags(tags.unwrap_or_default());
+ Ok(builder)
+}
+
+pub fn nostr_build_event_job_feedback(
+ job_request: &Event,
+ status: &str,
+ extra_info: Option<String>,
+ tags: Option<Vec<Tag>>,
+) -> Result<EventBuilder, NostrUtilsError> {
+ let status = status
+ .parse::<DataVendingMachineStatus>()
+ .unwrap_or(DataVendingMachineStatus::Error);
+ let feedback_data = JobFeedbackData::new(&job_request.clone(), status)
+ .extra_info(extra_info.unwrap_or_default());
+ let builder = EventBuilder::job_feedback(feedback_data).tags(tags.unwrap_or_default());
+ Ok(builder)
+}
diff --git a/crates/nostr/src/events/mod.rs b/crates/nostr/src/events/mod.rs
@@ -0,0 +1,26 @@
+pub mod jobs;
+
+extern crate alloc;
+use alloc::{string::String, vec::Vec};
+
+use nostr::event::{EventBuilder, Kind, Tag, TagKind};
+
+use crate::error::NostrUtilsError;
+
+pub fn nostr_build_events(
+ kind_u32: u32,
+ content: impl Into<String>,
+ tag_slices: Vec<Vec<String>>,
+) -> Result<EventBuilder, NostrUtilsError> {
+ let mut tags: Vec<Tag> = Vec::new();
+ for mut s in tag_slices {
+ if s.is_empty() {
+ continue;
+ }
+ let key = s.remove(0);
+ let values = s;
+ tags.push(Tag::custom(TagKind::Custom(key.into()), values));
+ }
+ let builder = EventBuilder::new(Kind::Custom(kind_u32 as u16), content.into()).tags(tags);
+ Ok(builder)
+}
diff --git a/crates/nostr/src/filter.rs b/crates/nostr/src/filter.rs
@@ -0,0 +1,13 @@
+use nostr::{event::Kind, filter::Filter, types::Timestamp};
+
+pub fn nostr_kind(kind: u16) -> Kind {
+ Kind::Custom(kind)
+}
+
+pub fn nostr_filter_kind(kind: u16) -> Filter {
+ Filter::new().kind(Kind::Custom(kind))
+}
+
+pub fn nostr_filter_new_events(filter: Filter) -> Filter {
+ filter.since(Timestamp::now())
+}
diff --git a/crates/nostr/src/lib.rs b/crates/nostr/src/lib.rs
@@ -0,0 +1,49 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+
+extern crate alloc;
+
+#[cfg(feature = "sdk")]
+pub mod client;
+
+pub mod error;
+pub mod events;
+pub mod filter;
+pub mod metadata;
+pub mod parse;
+pub mod relays;
+pub mod tags;
+pub mod util;
+
+#[cfg(feature = "codec")]
+pub mod codec_adapters;
+
+#[cfg(feature = "http")]
+pub mod nip11;
+
+pub mod prelude {
+ pub use crate::events::nostr_build_events;
+
+ #[cfg(feature = "sdk")]
+ pub use crate::client::{nostr_fetch_event_by_id, nostr_send_event};
+
+ pub use crate::error::{NostrTagsResolveError, NostrUtilsError};
+
+ pub use crate::filter::{nostr_filter_kind, nostr_filter_new_events, nostr_kind};
+
+ pub use crate::events::jobs::{nostr_build_event_job_feedback, nostr_build_event_job_result};
+
+ pub use crate::relays::{add_relay, connect, remove_relay};
+
+ pub use crate::metadata::{
+ build_metadata_event, fetch_latest_metadata_for_author, set_metadata,
+ };
+
+ pub use crate::parse::{parse_pubkey, parse_pubkeys};
+
+ pub use crate::tags::*;
+
+ pub use crate::util::npub_string;
+
+ #[cfg(feature = "http")]
+ pub use crate::nip11::fetch_nip11;
+}
diff --git a/crates/nostr/src/metadata.rs b/crates/nostr/src/metadata.rs
@@ -0,0 +1,41 @@
+use crate::error::NostrUtilsError;
+use core::time::Duration;
+use nostr::{
+ Kind, Metadata, event::Event, event::EventBuilder, event::EventId, filter::Filter,
+ key::PublicKey,
+};
+use nostr_sdk::{Client, prelude::Output};
+
+pub fn build_metadata_event(md: &Metadata) -> EventBuilder {
+ EventBuilder::metadata(md)
+}
+
+pub async fn set_metadata(
+ client: &Client,
+ md: &Metadata,
+) -> Result<Output<EventId>, NostrUtilsError> {
+ let builder = build_metadata_event(md);
+ Ok(client.send_event_builder(builder).await?)
+}
+
+pub async fn fetch_latest_metadata_for_author(
+ client: &Client,
+ author: PublicKey,
+ timeout: Duration,
+) -> Result<Option<Event>, NostrUtilsError> {
+ let filter = Filter::new().authors(vec![author]).kind(Kind::Metadata);
+ let stored = client.database().query(filter.clone()).await?;
+ let fetched = client.fetch_events(filter, timeout).await?;
+
+ let mut latest: Option<Event> = None;
+ for ev in stored.into_iter().chain(fetched.into_iter()) {
+ if ev.kind != Kind::Metadata {
+ continue;
+ }
+ match &latest {
+ Some(cur) if ev.created_at <= cur.created_at => {}
+ _ => latest = Some(ev),
+ }
+ }
+ Ok(latest)
+}
diff --git a/crates/nostr/src/nip11.rs b/crates/nostr/src/nip11.rs
@@ -0,0 +1,20 @@
+#[cfg(all(feature = "http", feature = "codec"))]
+use radroots_events::relay_document::models::RadrootsRelayDocument;
+
+#[cfg(all(feature = "http", feature = "codec"))]
+use crate::util::ws_to_http;
+
+#[cfg(all(feature = "http", feature = "codec"))]
+pub async fn fetch_nip11(ws_url: &str) -> Option<RadrootsRelayDocument> {
+ let http_url = ws_to_http(ws_url)?;
+ let client = reqwest::Client::new();
+ client
+ .get(&http_url)
+ .header("Accept", "application/nostr+json")
+ .send()
+ .await
+ .ok()?
+ .json::<RadrootsRelayDocument>()
+ .await
+ .ok()
+}
diff --git a/crates/nostr/src/parse.rs b/crates/nostr/src/parse.rs
@@ -0,0 +1,17 @@
+use nostr::{key::PublicKey, nips::nip19::FromBech32};
+
+#[derive(Debug, thiserror::Error)]
+pub enum ParseError {
+ #[error("invalid pubkey format: {0}")]
+ Invalid(String),
+}
+
+pub fn parse_pubkey(s: &str) -> Result<PublicKey, ParseError> {
+ PublicKey::from_bech32(s)
+ .or_else(|_| PublicKey::from_hex(s))
+ .map_err(|_| ParseError::Invalid(s.to_string()))
+}
+
+pub fn parse_pubkeys(input: &[String]) -> Result<Vec<PublicKey>, ParseError> {
+ input.iter().map(|s| parse_pubkey(s)).collect()
+}
diff --git a/crates/nostr/src/relays.rs b/crates/nostr/src/relays.rs
@@ -0,0 +1,16 @@
+use crate::error::NostrUtilsError;
+use nostr_sdk::Client;
+
+pub async fn add_relay(client: &Client, url: &str) -> Result<(), NostrUtilsError> {
+ client.add_relay(url).await?;
+ Ok(())
+}
+
+pub async fn remove_relay(client: &Client, url: &str) -> Result<(), NostrUtilsError> {
+ client.force_remove_relay(url).await?;
+ Ok(())
+}
+
+pub async fn connect(client: &Client) {
+ client.connect().await;
+}
diff --git a/crates/nostr/src/tags.rs b/crates/nostr/src/tags.rs
@@ -0,0 +1,106 @@
+extern crate alloc;
+use alloc::{borrow::Cow, string::String, vec::Vec};
+
+use nostr::{
+ event::{Event, Tag, TagKind, TagStandard},
+ key::{Keys, PublicKey},
+ nips::nip04,
+ types::RelayUrl,
+};
+
+use crate::error::NostrTagsResolveError;
+
+pub fn nostr_tag_first_value(tag: &Tag, key: &str) -> Option<String> {
+ if tag.kind() == TagKind::custom(key) {
+ tag.content().map(|v| v.to_string())
+ } else {
+ None
+ }
+}
+
+pub fn nostr_tag_at_value(tag: &Tag, index: usize) -> Option<String> {
+ tag.as_slice().get(index).cloned()
+}
+
+pub fn nostr_tag_slice(tag: &Tag, start: usize) -> Option<Vec<String>> {
+ tag.as_slice().get(start..).map(|s| s.to_vec())
+}
+
+pub fn nostr_tag_relays_parse(tag: &Tag) -> Option<&Vec<RelayUrl>> {
+ match tag.as_standardized()? {
+ TagStandard::Relays(urls) => Some(urls),
+ _ => None,
+ }
+}
+
+pub fn nostr_tags_match<'a>(tag: &'a Tag) -> Option<(&'a str, &'a [String])> {
+ if let TagKind::Custom(Cow::Borrowed(key)) = tag.kind() {
+ Some((key, &tag.as_slice()[1..]))
+ } else {
+ None
+ }
+}
+
+pub fn nostr_tag_match_l(tag: &Tag) -> Option<(&str, f64)> {
+ let values = tag.as_slice();
+ if values.len() >= 3 && values[0].eq_ignore_ascii_case("l") {
+ if let Ok(value) = values[1].parse::<f64>() {
+ return Some((values[2].as_str(), value));
+ }
+ }
+ None
+}
+
+pub fn nostr_tag_match_location(tag: &Tag) -> Option<(&str, &str, &str)> {
+ let values = tag.as_slice();
+ if values.len() >= 4 && values[0] == "location" {
+ Some((values[1].as_str(), values[2].as_str(), values[3].as_str()))
+ } else {
+ None
+ }
+}
+
+pub fn nostr_tag_match_geohash(tag: &Tag) -> Option<String> {
+ match tag.as_standardized()? {
+ TagStandard::Geohash(geohash) => Some(geohash.clone()),
+ _ => None,
+ }
+}
+
+pub fn nostr_tag_match_title(tag: &Tag) -> Option<String> {
+ match tag.as_standardized()? {
+ TagStandard::Title(title) => Some(title.clone()),
+ _ => None,
+ }
+}
+
+pub fn nostr_tag_match_summary(tag: &Tag) -> Option<String> {
+ match tag.as_standardized()? {
+ TagStandard::Summary(summary) => Some(summary.clone()),
+ _ => None,
+ }
+}
+
+pub fn nostr_tags_resolve(event: &Event, keys: &Keys) -> Result<Vec<Tag>, NostrTagsResolveError> {
+ if !event.tags.iter().any(|t| t.kind() == TagKind::Encrypted) {
+ return Ok(event.clone().tags.to_vec());
+ }
+ let recipient = event
+ .tags
+ .iter()
+ .find_map(|tag| {
+ if tag.kind() == TagKind::p() {
+ tag.content()?.parse::<PublicKey>().ok()
+ } else {
+ None
+ }
+ })
+ .ok_or_else(|| NostrTagsResolveError::MissingPTag(event.clone()))?;
+ if recipient != keys.public_key() {
+ return Err(NostrTagsResolveError::NotRecipient);
+ }
+ let cleartext = nip04::decrypt(keys.secret_key(), &event.pubkey, &event.content)
+ .map_err(|e| NostrTagsResolveError::DecryptionError(e.to_string()))?;
+ let decrypted_tags: nostr::event::tag::list::Tags = serde_json::from_str(&cleartext)?;
+ Ok(decrypted_tags.to_vec())
+}
diff --git a/crates/nostr/src/util.rs b/crates/nostr/src/util.rs
@@ -0,0 +1,20 @@
+use nostr::{key::PublicKey, nips::nip19::ToBech32};
+
+pub fn npub_string(pk: &PublicKey) -> Option<String> {
+ pk.to_bech32().ok()
+}
+
+#[cfg(feature = "http")]
+pub fn ws_to_http(ws: &str) -> Option<String> {
+ let mut u = reqwest::Url::parse(ws).ok()?;
+ let scheme = u.scheme().to_owned();
+
+ let new_scheme = match scheme.as_str() {
+ "wss" => "https",
+ "ws" => "http",
+ other => other,
+ };
+
+ u.set_scheme(new_scheme).ok()?;
+ Some(u.into())
+}