Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f276a06b2 | ||
|
|
010c96b05f | ||
|
|
31057b6fd3 | ||
|
|
2238743c59 | ||
|
|
60319999a6 | ||
|
|
96f46866f7 | ||
|
|
c758495de9 | ||
|
|
580342f3a5 | ||
|
|
89d2bcd598 | ||
|
|
753958659d | ||
|
|
c075194c3d | ||
|
|
296857d372 | ||
|
|
03bc1fab18 | ||
|
|
5bc36cc591 | ||
|
|
1a0f66a518 | ||
|
|
2da46501ca | ||
|
|
6411aab7f9 | ||
|
|
03a46e20b0 | ||
|
|
433bb0f130 | ||
|
|
439174417a | ||
|
|
ecf2cf3094 | ||
|
|
a3a83722b2 | ||
|
|
0bac2cbfc8 | ||
|
|
0b7742cfb1 | ||
|
|
4e2ec93584 | ||
|
|
c001726402 | ||
|
|
5e9fca1aeb | ||
|
|
bba66a1bce | ||
|
|
68a69505ac | ||
|
|
010a420465 | ||
|
|
bd7d083107 | ||
|
|
ba19c978a5 | ||
|
|
25ac86cc68 | ||
|
|
854531112d | ||
|
|
5224b9159e | ||
|
|
ae68fdd896 | ||
|
|
5e75370a67 | ||
|
|
d81949c31b | ||
|
|
116feace87 | ||
|
|
2d0efd8db4 | ||
|
|
c2704af0d6 | ||
|
|
0fe2423b22 | ||
|
|
b2ca2f6743 | ||
|
|
cf6171aadc | ||
|
|
dbb535eaaa | ||
|
|
ee27d0c597 | ||
|
|
259c115d67 | ||
|
|
efa14418fc | ||
|
|
097199ae6a | ||
|
|
f9e30c194f | ||
|
|
738e651574 | ||
|
|
506b92f505 | ||
|
|
cd1bc7655c |
@@ -1,2 +1 @@
|
|||||||
/target
|
/target
|
||||||
nostr.db
|
|
||||||
|
|||||||
Generated
+42
-406
@@ -2,17 +2,6 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ahash"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom",
|
|
||||||
"once_cell",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -22,12 +11,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arrayvec"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@@ -62,8 +45,14 @@ name = "bitcoin_hashes"
|
|||||||
version = "0.9.7"
|
version = "0.9.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1"
|
checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitcoin_hashes"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde 1.0.131",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -81,12 +70,6 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bumpalo"
|
|
||||||
version = "3.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.4.3"
|
version = "1.4.3"
|
||||||
@@ -120,22 +103,6 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "config"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b1b9d958c2b1368a663f05538fc1b5975adce1e19f435acceae987aceeeb369"
|
|
||||||
dependencies = [
|
|
||||||
"lazy_static",
|
|
||||||
"nom",
|
|
||||||
"rust-ini",
|
|
||||||
"serde 1.0.131",
|
|
||||||
"serde-hjson",
|
|
||||||
"serde_json",
|
|
||||||
"toml",
|
|
||||||
"yaml-rust",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -145,27 +112,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-utils"
|
|
||||||
version = "0.8.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"lazy_static",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dashmap"
|
|
||||||
version = "5.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b799062aaf67eb976af3bdca031ee6f846d2f0a5710ddbb0d2efee33f3cc4760"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"num_cpus",
|
|
||||||
"parking_lot",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -188,18 +134,6 @@ dependencies = [
|
|||||||
"termcolor",
|
"termcolor",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fallible-iterator"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fallible-streaming-iterator"
|
|
||||||
version = "0.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -293,12 +227,6 @@ version = "0.3.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
|
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-timer"
|
|
||||||
version = "3.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.18"
|
version = "0.3.18"
|
||||||
@@ -338,41 +266,6 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "governor"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac3de427a64787873c3b196285e6684cddbf0ae7d1d8d56eaafbb4120c4cb641"
|
|
||||||
dependencies = [
|
|
||||||
"dashmap",
|
|
||||||
"futures",
|
|
||||||
"futures-timer",
|
|
||||||
"no-std-compat",
|
|
||||||
"nonzero_ext",
|
|
||||||
"parking_lot",
|
|
||||||
"quanta",
|
|
||||||
"rand 0.8.4",
|
|
||||||
"smallvec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.11.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
|
||||||
dependencies = [
|
|
||||||
"ahash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashlink"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
|
|
||||||
dependencies = [
|
|
||||||
"hashbrown",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.1.19"
|
version = "0.1.19"
|
||||||
@@ -382,12 +275,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hex"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -437,55 +324,11 @@ version = "0.4.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "js-sys"
|
|
||||||
version = "0.3.55"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
|
|
||||||
dependencies = [
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy_static"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lexical-core"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
|
|
||||||
dependencies = [
|
|
||||||
"arrayvec",
|
|
||||||
"bitflags",
|
|
||||||
"cfg-if",
|
|
||||||
"ryu",
|
|
||||||
"static_assertions",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.111"
|
version = "0.2.108"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e167738f1866a7ec625567bae89ca0d44477232a4f7c52b1c7f2adc2c98804f"
|
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libsqlite3-sys"
|
|
||||||
version = "0.23.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58"
|
|
||||||
dependencies = [
|
|
||||||
"pkg-config",
|
|
||||||
"vcpkg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "linked-hash-map"
|
|
||||||
version = "0.5.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@@ -505,15 +348,6 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mach"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matches"
|
name = "matches"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -548,47 +382,20 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "no-std-compat"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "5.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
|
|
||||||
dependencies = [
|
|
||||||
"lexical-core",
|
|
||||||
"memchr",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nonzero_ext"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-rs-relay"
|
name = "nostr-rs-relay"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin_hashes",
|
"base64",
|
||||||
"config",
|
"bitcoin_hashes 0.10.0",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"governor",
|
|
||||||
"hex",
|
|
||||||
"lazy_static",
|
|
||||||
"log",
|
"log",
|
||||||
"nonzero_ext",
|
|
||||||
"rusqlite",
|
|
||||||
"secp256k1",
|
"secp256k1",
|
||||||
"serde 1.0.131",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
@@ -605,24 +412,6 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.1.43"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits 0.2.14",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.2.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg 1.0.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
@@ -688,12 +477,6 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pkg-config"
|
|
||||||
version = "0.3.24"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -702,29 +485,13 @@ checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.33"
|
version = "1.0.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a"
|
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quanta"
|
|
||||||
version = "0.9.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils",
|
|
||||||
"libc",
|
|
||||||
"mach",
|
|
||||||
"once_cell",
|
|
||||||
"raw-cpuid",
|
|
||||||
"wasi",
|
|
||||||
"web-sys",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
@@ -880,15 +647,6 @@ dependencies = [
|
|||||||
"rand_core 0.3.1",
|
"rand_core 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "raw-cpuid"
|
|
||||||
version = "10.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "929f54e29691d4e6a9cc558479de70db7aa3d98cd6fe7ab86d7507aa2886b9d2"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rdrand"
|
name = "rdrand"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -924,32 +682,11 @@ version = "0.6.25"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rusqlite"
|
|
||||||
version = "0.26.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"fallible-iterator",
|
|
||||||
"fallible-streaming-iterator",
|
|
||||||
"hashlink",
|
|
||||||
"libsqlite3-sys",
|
|
||||||
"memchr",
|
|
||||||
"smallvec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust-ini"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.7"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "254df5081ce98661a883445175e52efe99d1cb2a5552891d965d2f5d0cad1c16"
|
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
@@ -963,10 +700,10 @@ version = "0.20.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a"
|
checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin_hashes",
|
"bitcoin_hashes 0.9.7",
|
||||||
"rand 0.6.5",
|
"rand 0.6.5",
|
||||||
"secp256k1-sys",
|
"secp256k1-sys",
|
||||||
"serde 1.0.131",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -980,36 +717,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "0.8.23"
|
version = "1.0.130"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8"
|
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.131"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde-hjson"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8"
|
|
||||||
dependencies = [
|
|
||||||
"lazy_static",
|
|
||||||
"num-traits 0.1.43",
|
|
||||||
"regex",
|
|
||||||
"serde 0.8.23",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.131"
|
version = "1.0.130"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2"
|
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1018,13 +737,24 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.72"
|
version = "1.0.71"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
|
checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde 1.0.131",
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_repr"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1061,17 +791,11 @@ version = "1.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "static_assertions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.82"
|
version = "1.0.81"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
|
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1155,9 +879,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tungstenite"
|
name = "tokio-tungstenite"
|
||||||
version = "0.16.1"
|
version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e80b39df6afcc12cdf752398ade96a6b9e99c903dfdc36e53ad10b9c366bca72"
|
checksum = "e057364a4dd37870b33bf8dc1885d29187d90770f488d599d3ee8d9e4916fbd3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
@@ -1165,15 +889,6 @@ dependencies = [
|
|||||||
"tungstenite",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml"
|
|
||||||
version = "0.5.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
|
||||||
dependencies = [
|
|
||||||
"serde 1.0.131",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -1247,12 +962,6 @@ dependencies = [
|
|||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vcpkg"
|
|
||||||
version = "0.2.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
@@ -1265,70 +974,6 @@ version = "0.10.2+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen"
|
|
||||||
version = "0.2.78"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"wasm-bindgen-macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-backend"
|
|
||||||
version = "0.2.78"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
|
|
||||||
dependencies = [
|
|
||||||
"bumpalo",
|
|
||||||
"lazy_static",
|
|
||||||
"log",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"wasm-bindgen-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-macro"
|
|
||||||
version = "0.2.78"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"wasm-bindgen-macro-support",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-macro-support"
|
|
||||||
version = "0.2.78"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"wasm-bindgen-backend",
|
|
||||||
"wasm-bindgen-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-shared"
|
|
||||||
version = "0.2.78"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "web-sys"
|
|
||||||
version = "0.3.55"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -1359,12 +1004,3 @@ name = "winapi-x86_64-pc-windows-gnu"
|
|||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yaml-rust"
|
|
||||||
version = "0.4.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
|
||||||
dependencies = [
|
|
||||||
"linked-hash-map",
|
|
||||||
]
|
|
||||||
|
|||||||
+21
-21
@@ -1,25 +1,25 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nostr-rs-relay"
|
name = "nostr-rs-relay"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "^0.4"
|
log = "0.4.14"
|
||||||
env_logger = "^0.9"
|
env_logger = "0.9.0"
|
||||||
tokio = { version = "^1.14", features = ["full"] }
|
tokio = { version = "1.14.0", features = ["full"] }
|
||||||
futures = "^0.3"
|
futures = "0.3.18"
|
||||||
futures-util = "^0.3"
|
futures-util = "0.3.18"
|
||||||
tokio-tungstenite = "^0.16"
|
tokio-tungstenite = "0.16.0"
|
||||||
tungstenite = "^0.16"
|
tungstenite = "0.16.0"
|
||||||
thiserror = "^1"
|
thiserror = "1.0.30"
|
||||||
uuid = { version = "^0.8", features = ["v4"] }
|
uuid = { version = "0.8", features = ["v4"] }
|
||||||
config = { version = "0.11", features = ["toml"] }
|
|
||||||
bitcoin_hashes = { version = "^0.9", features = ["serde"] }
|
|
||||||
secp256k1 = { version = "^0.20", features = ["rand", "rand-std", "serde", "bitcoin_hashes"] }
|
bitcoin_hashes = { version = "0.10.0", features = ["serde"] }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
secp256k1 = { version = "0.20.3", features = ["rand", "rand-std", "serde", "bitcoin_hashes"] }
|
||||||
serde_json = "^1.0"
|
serde = { version = "1.0.130", features = ["derive"] }
|
||||||
hex = "^0.4"
|
serde_json = "1.0.71"
|
||||||
rusqlite = "^0.26"
|
serde_repr = "0.1.7"
|
||||||
lazy_static = "^1.4"
|
base64 = "0.13.0"
|
||||||
governor = "^0.4"
|
|
||||||
nonzero_ext = "^0.3"
|
|
||||||
|
|||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
FROM rust:1.57 as builder
|
|
||||||
|
|
||||||
RUN USER=root cargo new --bin nostr-rs-relay
|
|
||||||
WORKDIR ./nostr-rs-relay
|
|
||||||
COPY ./Cargo.toml ./Cargo.toml
|
|
||||||
COPY ./Cargo.lock ./Cargo.lock
|
|
||||||
RUN cargo build --release
|
|
||||||
RUN rm src/*.rs
|
|
||||||
|
|
||||||
COPY ./src ./src
|
|
||||||
|
|
||||||
RUN rm ./target/release/deps/nostr*relay*
|
|
||||||
RUN cargo build --release
|
|
||||||
|
|
||||||
FROM debian:buster-slim
|
|
||||||
ARG APP=/usr/src/app
|
|
||||||
ARG APP_DATA=/usr/src/app/db
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y ca-certificates tzdata sqlite3 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
ENV TZ=Etc/UTC \
|
|
||||||
APP_USER=appuser
|
|
||||||
|
|
||||||
RUN groupadd $APP_USER \
|
|
||||||
&& useradd -g $APP_USER $APP_USER \
|
|
||||||
&& mkdir -p ${APP} \
|
|
||||||
&& mkdir -p ${APP_DATA}
|
|
||||||
|
|
||||||
COPY --from=builder /nostr-rs-relay/target/release/nostr-rs-relay ${APP}/nostr-rs-relay
|
|
||||||
|
|
||||||
RUN chown -R $APP_USER:$APP_USER ${APP}
|
|
||||||
|
|
||||||
USER $APP_USER
|
|
||||||
WORKDIR ${APP_DATA}
|
|
||||||
|
|
||||||
ENV RUST_LOG=info
|
|
||||||
|
|
||||||
|
|
||||||
CMD ["../nostr-rs-relay"]
|
|
||||||
@@ -1,36 +1,14 @@
|
|||||||
# [nostr-rs-relay](https://git.sr.ht/~gheartsfield/nostr-rs-relay)
|
[nostr-rs-relay](https://git.sr.ht/~gheartsfield/nostr-rs-relay)
|
||||||
|
===
|
||||||
|
|
||||||
This is a [nostr](https://github.com/fiatjaf/nostr) relay, written in
|
This is a [nostr](https://github.com/fiatjaf/nostr) relay, written in
|
||||||
Rust. It currently supports the entire relay protocol, and has a
|
Rust. Very much a work-in-progress, and a hobby project to learn
|
||||||
SQLite persistence layer.
|
Rust, and tokio/async.
|
||||||
|
|
||||||
The project master repository is available on
|
The project master repository is available on
|
||||||
[sourcehut](https://sr.ht/~gheartsfield/nostr-rs-relay/), and is
|
[sourcehut](https://sr.ht/~gheartsfield/nostr-rs-relay/), and is
|
||||||
mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
|
mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
The provided `Dockerfile` will compile and build the server application. Use a bind mount to store the SQLite database outside of the container image, and map the container's 8080 port to a host port (8090 in the example below).
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ docker build -t nostr-rs-relay .
|
|
||||||
$ docker run -p 8090:8080 --mount src=$(pwd)/nostr_data,target=/usr/src/app/db,type=bind nostr-rs-relay
|
|
||||||
[2021-12-12T04:20:47Z INFO nostr_rs_relay] Listening on: 0.0.0.0:8080
|
|
||||||
[2021-12-12T04:20:47Z INFO nostr_rs_relay::db] Opened database for writing
|
|
||||||
[2021-12-12T04:20:47Z INFO nostr_rs_relay::db] init completed
|
|
||||||
```
|
|
||||||
|
|
||||||
Use a `nostr` client such as [`noscl`](https://github.com/fiatjaf/noscl) to publish and query events.
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ noscl publish "hello world"
|
|
||||||
Sent to 'ws://localhost:8090'.
|
|
||||||
Seen it on 'ws://localhost:8090'.
|
|
||||||
$ noscl home
|
|
||||||
Text Note [81cf...2652] from 296a...9b92 5 seconds ago
|
|
||||||
hello world
|
|
||||||
```
|
|
||||||
|
|
||||||
License
|
License
|
||||||
---
|
---
|
||||||
This project is MIT licensed.
|
This project is MIT licensed.
|
||||||
|
|||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
# Nostr-rs-relay configuration
|
|
||||||
|
|
||||||
[network]
|
|
||||||
# Bind to this network address
|
|
||||||
address = "0.0.0.0"
|
|
||||||
# Listen on this port
|
|
||||||
port = 8080
|
|
||||||
|
|
||||||
[options]
|
|
||||||
# Reject events that have timestamps greater than this many seconds in
|
|
||||||
# the future. Defaults to rejecting anything greater than 30 minutes
|
|
||||||
# from the current time.
|
|
||||||
#reject_future_seconds = 1800
|
|
||||||
|
|
||||||
[limits]
|
|
||||||
# Limit events created per second, averaged over one minute. Must be
|
|
||||||
# an integer. If not set (or set to 0), defaults to unlimited.
|
|
||||||
messages_per_sec = 0
|
|
||||||
|
|
||||||
# Maximum WebSocket message in bytes. Defaults to 128k.
|
|
||||||
#max_ws_message_bytes = 131072
|
|
||||||
|
|
||||||
# Maximum WebSocket frame size in bytes. Defaults to 128k.
|
|
||||||
#max_ws_frame_bytes = 131072
|
|
||||||
|
|
||||||
# Broadcast buffer size, in number of events. This prevents slow
|
|
||||||
# readers from consuming memory. Defaults to 4096.
|
|
||||||
#broadcast_buffer = 4096
|
|
||||||
|
|
||||||
# Event persistence buffer size, in number of events. This provides
|
|
||||||
# backpressure to senders if writes are slow. Defaults to 16.
|
|
||||||
#event_persist_buffer = 16
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
["EVENT","5496183021909538",{"id":"c352327610efbfe08ab49d8452f7827f0d4798d3c28c1143ef5a4c8d531fa1fa","pubkey":"d843184b145ec26c673b77b9eac5321af19195cb48470165d20e56fe7e727cb7","created_at":1612650401,"kind":1,"tags":null,"content":"gdfshbsdfgb","sig":"ebd534a85fc8510dbcf92023ff9012b89e7408b52d347af788fa223f5df83eae6ab5fced5ab2e3c265bfda9cd6f428b0cb4cdcbd1ac5a13a5cc3265df5d7942d"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"1384757da583e6129ce831c3d7afc775a33a090578f888dd0d010328ad047d0c","pubkey":"bbbd9711d357df4f4e498841fd796535c95c8e751fa35355008a911c41265fca","created_at":1612650459,"kind":1,"tags":null,"content":"hello world","sig":"59d0cc47ab566e81f72fe5f430bcfb9b3c688cb0093d1e6daa49201c00d28ecc3651468b7938642869ed98c0f1b262998e49a05a6ed056c0d92b193f4e93bc21"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"e7ade13f425e3c71a7ebd9d5176059f6ce0469f06317e804cb57c35b71e791da","pubkey":"40334685ed272a062c1a3bac85c995c53c69cbfbd0a02f1a134679986fd39af1","created_at":1612651366,"kind":1,"tags":null,"content":"Hello world","sig":"cefb82b57c47e8ed6fda7bf06513d653b2081cc25817f55c8e56005c84390aedc7a7b8464fde46afad890d1d355c63da2c9c4d8b64cccc70df2232f9f4cf86bb"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"dfcdc27e775dc27024689fcdab44cbf7ec5660bcded5beb25f079aa05468fcc9","pubkey":"09488d7a12468dda7839520515c20e6c2a8272c5df8ec0261db2ec65d4420a39","created_at":1612654851,"kind":4,"tags":null,"content":"HOfkGd7fA2vIsbXuZazDrA==?iv=tAfeHJFgxIjuUvxqlxCcLg==","sig":"7176125524d751e810f8523360eb319076e46d97e70928a0bcf3b6bec9efd121f01adbcf196d428a1a7691f5c94df3c4c3478c9dbd3113745bdb96cc2c8a0a49"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"d349109f225a0e1685913a2559a6a9beca0ec0d6865d617e237a320800dd8fc3","pubkey":"09488d7a12468dda7839520515c20e6c2a8272c5df8ec0261db2ec65d4420a39","created_at":1612654966,"kind":4,"tags":null,"content":"esxa2yzo8b/3xR574joX4g==?iv=2LinF8Zd1fs3DCAmt4cLZg==","sig":"927a81962830e6f2d437e1f5d5cf123e0b536ad5492469483373a02e7f3c1ef4367b121a8e58aed062b07c5f634b108a2d67965e24086337b63e395888c9de9b"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"2740f9572b8330565103640e94c608c561150d115f38d1c9230b221222c7ffec","pubkey":"5d6baec9a3dce295e6a2ae16fef2aa1849d2753d047a4abc16f53183d971183d","created_at":1612702779,"kind":1,"tags":null,"content":"Hello world","sig":"37dd4a4beccabb01a9ae4661953807c82d70d92e92495a5647b83670845063505eee8daf0a0a9f845e74f6420911e6a585fe39a94aefb7f2a274a30912178ae3"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"a629291da57e02ecfa72ae4dfab772bb31720756fcd51a0247f1753fbf9afd30","pubkey":"7bb57b0f744317c7300dda43a3e64ebf5e5aff1b180619641de1767bad543ca3","created_at":1612706239,"kind":1,"tags":null,"content":"Hello world","sig":"ae540e301af3b4836d0c6e334076a26de7879cf3a31f28d2599e0c9d71e64f30ba12c3675368eaed324136338c6bb3aaf3f7e6fb62f3e041f6f7e5a6726a4234"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"6ec3918617f694b5dd8bc0a3524a6b341e42425d45dd89784dbc79642cdbb6c0","pubkey":"f8e9ba8fb6f401c6f85ec7671f4b6879ff9ae4d867508d00cd42fcaa43e01974","created_at":1612706419,"kind":1,"tags":null,"content":"hello world","sig":"b8e49754c9dd7f6898a28ed2b5d002026a559f119171a22db7095b0c2fee995475d06dda11a9f482b3b6766c375a2b0c2204ba1fe0a58ef312a90db0a68f9def"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"7a84cab2e6e7ced2ea2bf29d07a739a140c3afe2fefbe83a0578ac6cc7691f66","pubkey":"f56bd7d9e38ef70e5355dd1adcfa2d63b4f8041157b75085a61ad201022067f4","created_at":1612706661,"kind":1,"tags":null,"content":"Hello world","sig":"99fab70b7e51e4bf17eacd246235c06e369c2cfeae1afb76a47f5fc0d9f09ea73da7282a8c3ed0da4d38519159fecd2c06d942afa33f9a44eae5a98922d883ed"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"f01d8c4d834a6c7bd21339f61f42bc282c39c4ec20b9165e197c70ef2ec45bb6","pubkey":"f8e9ba8fb6f401c6f85ec7671f4b6879ff9ae4d867508d00cd42fcaa43e01974","created_at":1612706788,"kind":1,"tags":null,"content":"😘Nostr mooon","sig":"795ce321c1b3f795cd89cd56a6e155d459d1acc49f7d99bfb0c5601be03a6ba6e19a78b7701b2161c0e005ba12ec956693e3b04ab5926b4420137f97044bb62d"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"d424b06c3ab880b4456bb892d824a193c37fec5905127c47e0a58c01f20a8805","pubkey":"f8e9ba8fb6f401c6f85ec7671f4b6879ff9ae4d867508d00cd42fcaa43e01974","created_at":1612706836,"kind":4,"tags":null,"content":"ZDN1DW9WVKy9gtYk8guCXw==?iv=Wni55RkHsIiFr2LDKZGnvA==","sig":"d735906490710367234b0dbd0f656e38862f77b3df3a1b02be240d79c252e21be435c82f943f97af5814135655a052d49ba1a60b623231c71e5aa3e1f05576f3"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"3bc86162407605dfcf629fb1568cdd7b8bef311e4a347230965e3701e0efb550","pubkey":"f56bd7d9e38ef70e5355dd1adcfa2d63b4f8041157b75085a61ad201022067f4","created_at":1612706876,"kind":4,"tags":null,"content":"5hRRuFOeVXk6eDBLtd90DQ==?iv=uvxudX52hqSgoNmLoCa7yw==","sig":"4ad25d91b31c022862d905a3d22cf539aeb0d407e514a495d93271beb87dc8b6d52584c9751e0f2282db3e9ef6d3b954b24fec68b99ccc473efe00eb79902a28"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"03f2d548da33ba1d57f0799ecca65024c22921d401c5664105613084d556e1ca","pubkey":"f56bd7d9e38ef70e5355dd1adcfa2d63b4f8041157b75085a61ad201022067f4","created_at":1612706887,"kind":4,"tags":null,"content":"ijb0cRAp01jqCrsdf6x4XA==?iv=rqfq4NZOYH+aP33PX2u/7g==","sig":"08085386e584a6c35110e32def29325ab82012d3a37074d8144976448d1b15e869af632cf505a2ebe4c2ec944ffc232760f7adb1b9c535122ab3b08e64dff548"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"94010c0e91f5cc089eb92dc13d52a5230ec3b2b853139f8a5e4cb22b329824a3","pubkey":"f8e9ba8fb6f401c6f85ec7671f4b6879ff9ae4d867508d00cd42fcaa43e01974","created_at":1612706924,"kind":4,"tags":null,"content":"LARSxV4ftsCYjdZ6AS3x8g==?iv=mhXHc8fblD1SghCpH9pcYQ==","sig":"f5e4c5f4cdfe0f2f42fbb843a254d225b718a1e3461030b721f6fa3b79cdeab27e4066e0736a8b172af7f4ceced66b62d183f215aa8cd05bd07664cc8c833f47"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"6cc318ed826f77fce5ce0b2c64621693cb6d89ae8671703d3bcecbb49fb2d46e","pubkey":"545423ec685844531c29a2947793f9eb030bb33857651f410e7eee09d7af07d8","created_at":1612707587,"kind":1,"tags":null,"content":"Hello world","sig":"7e0bb2a6da5a5964a933e893767240bdc7d40d419296743775a6bf574c558063884c86912cb5ba50f7037bd12e2b6764dfcaa61c1dd0156f017897311fc4ddd5"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"c2b2675d252f5660d5551bc74d619f9f434388d2d170ee64c8e16ae5e802b663","pubkey":"d6cd8c07574eb86ffe5e3d870f7dd5f77f2284e16dfed0ebe1417264d70bd2bc","created_at":1612712970,"kind":1,"tags":null,"content":"poo","sig":"e1dc366fc7f99cbc976f91382a2165bc0e481570a8d04c2745f82aabc2465c1f9bafaccfd8ff20ae250e5abfcec6dd86a2df5c5d2063d089e7e1100d12e10fc7"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"34ec7e92578415fa9b870ab3dbc914f8cd1a69728d92455ef281719a8ba98d45","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612713213,"kind":1,"tags":null,"content":"Hello world!","sig":"facbe66cac0e1761e48ce5e9874a39013d899e359e82822e91aa8b955febcdd555b2fa201f343bea198912a91eb8cd8a1f05ce6bba7c1fd4b070881eada76403"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"e1dacb8a6ca63fc8a144e3d5e5f18892e21cf6b8dfd7eb019d0443cfceba4bab","pubkey":"2b28d10527292d478c4653402b085d831a3b78759e46f7b46de3b7640e2ad667","created_at":1612713276,"kind":1,"tags":null,"content":"Yo","sig":"b1dd07fb7e97b4822de7f2854a603cc04f4e081427193d8495e8628f7994f0dbbadc367680bb5a2fa0f56699e01862fbf8e452f0698bbf66536d732663d32876"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"1f8cd2db4c7e7153f5b7b9e67953980ace79af78e47bba6b2a5e66ab93c749bb","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612713555,"kind":1,"tags":null,"content":"Follow your mates!","sig":"1727f52b4257f1f6fba8a6fe0b6b0180ee404f76fde18427840fa4d513010ba802b4b8fd261209ab196465b3ffe11e8d7ccbbc1aa557d36a4be753b251eb03d1"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"a94596ffc55baa20157dac4c09544fe2e925d9cc21cb5765a6744762892bbeab","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612713686,"kind":4,"tags":null,"content":"sidnxTMcUmJWv5esjKgdbQ==?iv=k885PbcWY/1Rs4SOVuVBpw==","sig":"0653438b09f25101bd46a118752adda07fa8eebc89b1aab725ca6c4e09d028d9b6d28e16382b4d0bfb583cbcb142fbe8ff59815f898168b287bc288128eda5a7"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"cbe6c74e1a8337ad1c90a4d2cf4602942070735a35330c6603b92cf406e61a93","pubkey":"2b28d10527292d478c4653402b085d831a3b78759e46f7b46de3b7640e2ad667","created_at":1612713689,"kind":4,"tags":null,"content":"GnBL88R3Q8UyM14hqJYM9w==?iv=uqJo8XCW1kFnK6EV/gZAKg==","sig":"95f6273f6be520af9f84ea1249e57b755f6f4b773708abc5c9aed2f46817e92e04012c34e5d9a0f5604774df4bcb1dec2f32dbc7599fb051fd774352c209b9a1"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"90c8b94d5687efa38bf7ab27d6f6d334ad66a6a6ce8a2a85c1ce1a1fcb624938","pubkey":"2b28d10527292d478c4653402b085d831a3b78759e46f7b46de3b7640e2ad667","created_at":1612713727,"kind":4,"tags":null,"content":"9fFEbgB4zpC+w2ebOU+75Q==?iv=Sr0fAFqjt2bKSrg67ExELQ==","sig":"03a224e416b1cdc8c2690905e7dfe7e44c3470e9fbe49e63717d603717900d4c5fb20c129433e8308cd170ea901a10e29833d33df7c8eb00f67da6eec4b24ee1"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"c3f5afc494bba4f3859fba2d89af388b006eaacdcea2085778a852d84dd91d69","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612714060,"kind":4,"tags":null,"content":"PGRihYEwIGDBepMSwSIzAw==?iv=qsiS+2Tpuvm/XjHzfeSZsA==","sig":"55dc0046403e3369fe1dbd05053ccb74d130ad13bbd49fa07a228bbcb0c2a2168b3eae2176179a7ac5470a6af5b2eb3fd22bc2e4f4f5583f26b9affd6175c7c9"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"7d55fca77dc47d50e4b2a36ebf178de9e4e9108e51324ee5ebef101faeddf295","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612717927,"kind":1,"tags":null,"content":"Hello, Dolu there","sig":"df6ef0dcd4f698436102b65c2712e80b8eb8e0af57ddec958e7dc123b7861efc3e52c81ff29a9bd3491e97b43168f5babbed1da3fd552e04e2df50c62f24e987"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"49e822a04f7172c7cae32c1d02191eee78bb741a743c79988a4bc7c972ae1ee1","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612717967,"kind":4,"tags":null,"content":"lZjRPAfWaP4zHB8SRS93KA==?iv=KSj6xm+7L5QcKazYem+OJw==","sig":"58192c278352bf560fd0875a329c64f9270ebe193246ffc3506782e37c52aed449b073f420e1759bc72c536ce5e01a2b948bfdf9cec46b7ebbce393ac4df1fbd"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"dc6ce72faea7c32260726d0d8046cd1365f25ca31d9889cde2fd7a6ecca22865","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718029,"kind":4,"tags":null,"content":"PR0zmMZVkfdzsJriO3ajdQ==?iv=bML7a18IESplBVx2Fo5gsQ==","sig":"23efe1d673d3d6b10dc1dc21f573a1ae925917435a68622121b12bd539301c0ab112c555bab675a71a917cf155d5c39f90fcbcec7a2b74697114e88733376ce0"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"beb313771c25ba6a62f40f24eed6eaf7fea82a7fac8cfbbdda78f3b4c7ef208f","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718038,"kind":4,"tags":null,"content":"8+83lQr6CjtxUV8jmh/sVw==?iv=TBpFUY7D/sNPSbTzToMn6g==","sig":"47c0dcdd9c432fa83f3153cad76359cc74cdceefe7f9c66db0732091f8367b57c6cdec7f1de0d0f395099da9ce2c52069396ad80ff8fab302689ee13217a927e"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"b6efa79297151c0f25948ed586c5fb23c04453043cf24d961e8d0eb8b8c83707","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718072,"kind":1,"tags":null,"content":"Not getting any posts yet?","sig":"70c18033c32b3f95636b15d6013d18360bb734f5bcbb1c8bf5993d8f890e75e4158531a0ab9a48d49bcbc1b94f94163d6c80d9b89e6bf23edbda9d14fcb16d6a"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"8a0f6f93dd51c279527e891dfbd57ab95d02d9fc7d331418d75c7ea79158a0fb","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718144,"kind":1,"tags":null,"content":"Hellooooo","sig":"eb2fff856a92b2518bcc90fc965d527a57ad91a9ce6dabf5c96d35d6c7440036ed9523157521ad7cfc3327a540c392c4a751008cc2ebf3b433d81f535c45ba71"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"f450d18411b503b30d5a8cebaeb0480ba6c8eb67fda572d04c63fb900d95297a","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718156,"kind":1,"tags":null,"content":"no","sig":"9a0c6663fdb527f8dbd1668acda6459814c738918cece209f7fb65f973cc3fb32ee7fb61e5f9e461f443d6f775a795498bcc19db2127803a8588044f07bdca0a"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"9d73afa3e3ea6b4e930504bb34c8066d655d1878fdc1cff39ed9b093fc3324ab","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718170,"kind":1,"tags":null,"content":"I'm fiatjaf, and I can't prove!","sig":"89d217bfc23463fb326507765a35661ee64b2e474656bacf3511423f2784a47c1a163ccc6bcc054440b40310bbd280e712fb6cdeb23c1d486908b5e8b71e55fe"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"c9ae8727e529f4e6e6a24ad476c13b90d550adc648693b764a7ecf0b15b83909","pubkey":"b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9","created_at":1612718177,"kind":1,"tags":null,"content":"I'm akovalenko, is there anybody out there?","sig":"0f31cb6077b169f4316c7db18ea1b8e0e6d6ac408d563ecfd75d26c2cbe6bd0f441f8cb6759d4cc13f4d5f13470a403dd77cfd55a104ce50f975a8a2d0f16a9b"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"a6ac3698abfa3be971d038769abbe4448615ee07ff19c9f727a48cf8ccec993e","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718265,"kind":1,"tags":null,"content":"hello","sig":"bb261862a595791c504ef3930a03d10dc2c784a5f9e9a912d948ec32617e0b2c6699b1bc3d8af3fa195bbe8f9a79d3170fb7b3dd66a1b0d23d43043bbce85b33"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"08a572f46715562dbbd4ba6687786acb9b1c2a4d2e9b627f605f782482c174de","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718310,"kind":1,"tags":null,"content":"Wow, got some data","sig":"f632c4485f8165d2323e2f11bca7a5d08b9b041efcabb1da76382b52fb703f44cf7d6aa4a0730d3e992d94b17237cb12cb87138685d431620a4e5062ef328e03"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"951b0c82b9b4ba64762d948254f6a909f40891ad5535d4520df227ca439b6cfb","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718384,"kind":1,"tags":null,"content":"I just see a ton of gibberish","sig":"8a5778393a927f172a63bbca5125725cab01f73c5d1d5e437c672e183fb9944006ac6d1c453f282e535d1f4c15878400999bf154eba7fd87a135759e07ce95f7"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"643f09d78fda9b1747f701b4d9031a96a0ab8109fc67049d5303a92783e0fac3","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718575,"kind":1,"tags":null,"content":"Yes, the rate limit is great UX","sig":"7784aae1d430c93b20a351a2e603a68913618ab68aaa39c1e05c619b986b69f55cba4552973564b08dbe51223e070ee765bfd377224f60d9278f267dacef52c4"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"9beb6fdad93036d38a8c12cbefc39b4b9867b18e43b4329562be04ad3c292045","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718709,"kind":1,"tags":null,"content":"Anyone tried this bot on telegram @zlibrarybot?","sig":"de19affb1494c650c980b9b7743ba1d90750e4ce6f757c2340eb6d50d7b2ae78d6db508ffd6508cfba256b76aa075085f04b42ce35ffe10434faf5ddee4b1d02"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"36220ea30231dc02aca75151bb22097497cc32d86d091cb4dbfd2a27704ea463","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718724,"kind":1,"tags":null,"content":"Just put in the name of any book","sig":"4eaa22c253de6d40f17b0b1a5fb9055ee2a533f30dfb0354ece8b19913b3231196d66c31cb5ea846629845995afa5340cea894dfed62dc34e21e6bb081598f56"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"45880a8e7effae31debc120bf563894290ec0f6f1cfdfa18c37384297633ce99","pubkey":"4c26f43c256ae19fd025e6a619ae9b3ddc7092568d464613d94d05e6c573d4af","created_at":1612718750,"kind":1,"tags":null,"content":"Hi","sig":"eb9aa9cb0c341c3ed87cbe92b8e532226fd0fff6b93e5de67b4bb9329c479fd9611740cd48ff4e0529263f7a61cb7b48da075020da4cb980b94794083f5ae935"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"e914eb8271ea891b45b9fbf7cdd01b3cf09861b9245d2f9935f1f2a9f1ffa690","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718792,"kind":4,"tags":null,"content":"yQylpih6SrlVVusFQodcpQ==?iv=wl1KlrC5A5qulcTkzA/pWw==","sig":"3347c3d08dbccbcbed7dcc5ecdc7d8000ebf60f2e6f2d15ba85b11ae214703949f1d77614974532bfb2aae51189f0c20b7743b1c65a278cc4060ef9713c4045a"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"3366fa6384aa042317f76656187cf4629225ed3246c5cd69a73e46a5a28760dc","pubkey":"359322282153a422b8f5e727acbd55d53f1e7a3aa76651d903c8f33eed316fce","created_at":1612718848,"kind":1,"tags":null,"content":"Bitcoin","sig":"302e913d3234356a5dde9b508cd9c614310398e267e86b1a77609a51a5a23a2e578a5d2a7f9d66436c62927c23c1272f307896f0a204c2c16285a3f69290e74c"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"128f4f9eb1856893273ac91aa9544f02a9b7bc0e701a16449412f46e9f283db8","pubkey":"76c8658c827c8f4c1680be9f0927882c73000aac1ca4f70cdcc0279e39962309","created_at":1612718889,"kind":4,"tags":null,"content":"H5Fvj/EuZ2sKme8h5wZ+GQ==?iv=gpRePDQtugknxvORJ6PPgw==","sig":"87f1847c98d8776447123075f5139993ac3fd19738a55c359bd64acd2b0ad1d3ce12e1b04703536a488bc061e17719fb709ccc14cc109a95b2746ae527abe090"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"ec39944536c3f9524c434c6d91da99c4dcb553fef16a9cc3ce2563ca47bef081","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612718919,"kind":1,"tags":null,"content":"so far so good","sig":"b1747003555faf4bc5310f20d27f77cbe1877d31c64cd555701153d149b5799ca92bf38efcf89cf336e005627620d4063f497c3f9a9abe278e3a4329bb32494a"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"3fa4586a8d0bc29c236d07a553dc4204a0bd51ae13adaa9539e0ac66b1b64009","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612718994,"kind":4,"tags":null,"content":"PPh2a0C+4VBpPydvH114kw==?iv=pCny9BX3pzh381QMODZfSg==","sig":"b0d4258ca804e1142a9735d075c73457b9ab213eaed1e8336046ae4d9b422cf74178e3b0e640b64ddeb40584f600b236d2c81f19f51b838fdb69938f077e991d"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"a724ac4709629a4554d4a3b392a71beb5a0f365288aa6b1a8057957ec3c9b2ed","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612719055,"kind":1,"tags":null,"content":"ok, I've removed the rate-limit from my POC relay code","sig":"0f908b235639180e4e906c27a5ca0b5efa97a746c05ada7c48341a144bff2919f3bef692b3894ad33dea494c0dd9c15aa83f5011c1f0b77656016dccfd1d0f67"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"8e476e2a1d9c52cc06e6f2ec2d74d5842f84e3e00a2a42191c095f6e62706de5","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612719085,"kind":1,"tags":null,"content":"but I still think clients should be aware that relays are not to be trusted and they may fail for weird reasons and so on","sig":"d9b80145fde32ffbecf0456ce69b1b7a55df0d5a01210dbe3a4c22050171aa61d5cef9e202d121987d07590c84124fb4e4c566235ec9f28f4c5f783c0ea5717a"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"6ccca21b5176945cfc303825ce18aea2e3023680375d8e57788190399b09e8d6","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612719122,"kind":1,"tags":null,"content":"I get a lot of random relay errors","sig":"5c60bcc689fccc32662ae62004c22f81ee6d3ea6dac0e603aca2ef807a2b7206fc0ffb7c793e5930c13f0eeb55de5fca5cf9ff00ba8840bdcc52dfc0248ecbba"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"3c3441e03c9dd125b5b4ea0d456ab731653e888c4a4961958cca832be388cb46","pubkey":"2a6becb02af9a9c16f78473bec828396ff39856e6ebbee8ff9a40450d5585a71","created_at":1612719134,"kind":1,"tags":null,"content":"Hello, World!","sig":"19639912ff30544da35da60674f830888ef2139dc7c9206003d440a859a02704f19287a1f3b23814d0485a69a6342e24583a1b0064c45f32d0563786b3c2fe01"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"d977fc22309d501976ab9a317ec59d88802bbdd8cac2d1e0fae24b0b1993c8c7","pubkey":"8fb2b4dfbbad68a3001d3e25f745de57c75647e8cb9ae0c3cc50f297933e4337","created_at":1612719190,"kind":1,"tags":null,"content":"Hey Nostr!","sig":"3c09e5737d588b498454581dba7a1ff972f3d34bcd39441ba5421338d4b05de16507b07402aa7d2649a7bebc571bb1e9769877df24e56ca7d44732cbd6f2e4bb"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"c3ea79a52feffa76b10f26c5041b2493545b27668bcb9abe0cc4348acf0852f0","pubkey":"8c79b25dda02684757031cc31c05ed8c17f5d6111b2b80f825945b838450481c","created_at":1612719317,"kind":1,"tags":null,"content":"*insert Jack Nicholson nodding meme*","sig":"ddda774b976833aa507422fff17d2090814be63db3580aff8e83822aa9ab892b7bc58a3cdecf06821a8ba37ed9b30bda15682d46cd24e7537c2c4616d8d91fad"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"9eb12cda084c1e6692e9421c803c8c4970a658b223c28eb7af35933a246076f6","pubkey":"6cb51a92f6c1ecc613db13fca5da2dbcc23bde6613eb9ea7cb79686fea6ca221","created_at":1612719361,"kind":1,"tags":null,"content":"Hi","sig":"204ba930638a2ac6ab7c8eaf264a529a589db2f5ff8f54076e55084edd9d65325bef86f5f65ec23aacbb0190bb2ace4624d9fe33b5815a6113325088fecc5f59"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"4de66a382a962f28c755979b85118a5a940bc118742fbc6fd98dc9260ba437c8","pubkey":"8fb2b4dfbbad68a3001d3e25f745de57c75647e8cb9ae0c3cc50f297933e4337","created_at":1612719388,"kind":4,"tags":null,"content":"+jEjChxHpBFL9YtMzMv6Sw==?iv=Fc8TB1CFkOEV3Pd2BoBhSQ==","sig":"a666adcb630fe810a75e523d8e80295355f259c22e5818a1703bee1556217cc932d04d33c7100dffef01dbf120888b5221001d544ed01bbf0cc6f20c572b471b"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"635ebf9aa3adf665bc5fbead6b45e1bd3d8933d2c3fea82989de2729eb352f66","pubkey":"6cb51a92f6c1ecc613db13fca5da2dbcc23bde6613eb9ea7cb79686fea6ca221","created_at":1612719522,"kind":1,"tags":null,"content":"Hello","sig":"3b07c57ddbfb281c39b4d9e238e760f3e7f8c81e9d6b4fb960ed3005bc4de4c501782d8e926ac5039f732d010a462aefaab83b08caddc1fc4b2089e6a56f37cb"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"033e38ba1e59cadcce55434d9d416406f774fead3fce93226c8fbca9e919005c","pubkey":"271cbc18d71f9eeaca6693dcbaf76a54730e80c8dc7d2b11d5d1b242121f1555","created_at":1612719720,"kind":1,"tags":null,"content":"test ","sig":"c3a7ee3a08a948364ba00b490685516683dc6b6b283fb2ed013a6d08c231f82aec4ef6c876e0c46c161a05794c9dabf128bc8e2841b8ce74e20dfd285d648def"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"37ab40788c8ac4e3a6dfe2be16647fb68122fb9d3995e2f39675e0dbfd1bc36d","pubkey":"8fb2b4dfbbad68a3001d3e25f745de57c75647e8cb9ae0c3cc50f297933e4337","created_at":1612719782,"kind":1,"tags":null,"content":"Hi, I'm Elon Musk! We celebrate the TESLA anniversary this year. We give away free BTC just send your BTC to this address and you'll receive the 10x back!!!11!1 Guaranteed!","sig":"89cdb3791e845b42da1326415e0602dfb18551c7a0f8b67f2c250397e52c1b500036c3e8e40e844836eb9b868ff105d065e0cf26b2777e02589a47e1cc88cc76"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"463c161858ef6a6f81a702cba1b2ed18dd8e4f13ca5a02e6abb2d912b7248cae","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612719841,"kind":1,"tags":null,"content":"I deleted local storage, got my feed back, got dms but lost my side of the conversation","sig":"9bb358ba036273dad1bff21d6aec38d47f5026325e849db31224cfc579e2fc3f8c27a3a52dbc85cc2efd86a10e041e2fd8c4868fa842b412dad63f85367ae344"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"21f6b8d34b297e56f8b1df778a146beeb441dbd0209f7eb798e7c0dcf7ff745d","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612719895,"kind":4,"tags":null,"content":"AM20BSQKE8B7S3P93h84KQ==?iv=RTEAOmRoDKdR/pJ08KmJrA==","sig":"fd9a740788fe1bc67de55ccaaafc487cbc879988fa4ebe900f0751f824d75863e1d4c28b29d3054f419a4492fceab17732b1c7311ba8ac2cc2e7106e55b9040b"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"77468731e429b95e5e086f58c9d2a30c5631ea3800b3e2c359890d151e06158e","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612719901,"kind":4,"tags":null,"content":"nSpVaB74d6Fco01YFdLIgg==?iv=3/5tClR6B7ZdDcEGf38Aew==","sig":"84a32b0ab26f5d9d47701ab189af3fc9331ba8df90665bff94c3d222205810c05ece0abdeccbcb26ede41b7bec6f557d057d37f3ce571d3c65541f66deb550fd"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"bed9c3e1734b93e1d6689646fc692f7605e6adeb4bca90ccc161e6fb7557c275","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612719940,"kind":1,"tags":null,"content":"Wow, thats pretty cool","sig":"3207436a396788597d6ca7a2be4bba2b9c480aa208c374526b8001276cbe0dd547fb594224edb848c238724ccb9df76059ffcc6c792653271f1140b470ed2385"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"03e0972dc6a94e88ee7bc1cdb5ba10bb67122f5d9d57bed0d99847d670773214","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612720554,"kind":1,"tags":null,"content":"Test from a new relay","sig":"0d927de5915c12de773e17ef4db3bfd39977c937d97631b8f0b82d723feafef13ce4e96d95992f02e93dea662ff82aca2adba70ca48b1c7c5c780eb01dea5bff"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"38d696d8b96200e10f1335f83cc2ef40393021336cc9cec254b7c74b97c8cf84","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612720654,"kind":4,"tags":null,"content":"TzyTqitK+Adv1cgSe1trDg==?iv=ELXw7Ije8Q074chj+uGS2g==","sig":"1d706b49307a172c8db00b475b7abe9f159b088dae8d054fb40c0fd7aee3763897ecafb4dfeda065c2ad1784078c07de7597b6fa88286a0697c9c74606f6c186"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"35f9708ff0bae02ce4fc3799f50fcaf1affac7ba809cca9e18a2bf70c601a0db","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612720665,"kind":4,"tags":null,"content":"todzDCDEUecb+H4fmkiwzA==?iv=7ecvtK2r/kOyh6fyD79inQ==","sig":"f7dade434bec0c43b68397893a8c06c439f8ab628a347381cb2b509ccbb0ab3af96fa50dcc81166cb0781d408e8328f207ce05e73503164fbca1702d9ac4d837"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"e5130088298114e5b3cda517dbfc76999ddc758e3462db28236b2ec8deaea84d","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612720777,"kind":4,"tags":null,"content":"+sEvKff5h25ieydxbT6V9w==?iv=3IfPlCKjWN1qKACBrikGdg==","sig":"e00183869031142455c10722b6b9c80e7de0b7c32aef743af102ad4621df83583cf68b1686f9ca6cd6ccfc6d9b9dafca632d10ebf7600548ce6af83cd968ae3f"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"3b7444dafaa8ba360028d1ca352558eec138380eab6d41f269a90f53f2d1f76a","pubkey":"6466df2abb1b1d0114305d0a11397411871a2ed1b133b7bc8299f958b65fa491","created_at":1612720778,"kind":4,"tags":null,"content":"KX321Ey2RZN84rro05lxLw==?iv=WK18NePy+Kb+TCYmH+j7Gw==","sig":"0fa034dba846c0a459fc8943c7fd93f72baefb8af9cb8ecc17308e8f9b7a45f2a839dea46f90df6f5cfa048d2e02e9b82de43820dfe6ab4e1cd1611fec4f91cc"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"257ddf06fe141133fc3278ea3caea681983a128b2017c1a7310330b7f3b6685b","pubkey":"da5e9e12f0ee37574fe8875e95f53057a7d9345ad82b692712a0c2c9a2a53c02","created_at":1612721689,"kind":1,"tags":null,"content":"Testing...","sig":"c6a51c19e6f1203801f16a4999dc3e6c172925fd4767d3dce818c38b41cdeeac788b0048fce42b9e09a5bb9bb3687c257a7f72920959187441c2ccb6e6774705"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"f4d3a6d1287a75266c5afc1672544bc3157de1ccecd15bf843202d2bfb218a02","pubkey":"2a6becb02af9a9c16f78473bec828396ff39856e6ebbee8ff9a40450d5585a71","created_at":1612721718,"kind":1,"tags":null,"content":"I didn't find it in the mobile application Add public key. 🧐🧐","sig":"b69226f5b17b1a610e81547f78f117419bcbff4230eb2ed745f5c3bffe3575ebdc170a580c2c4ad055c53a941481ff5906f60167d48cb02f93b673eb950ed919"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"4db57707d52e788a2d5f37fce5c74a7a67e82f102d11e20ea6d315b21e644cbd","pubkey":"46635e6e168d3daa4961f854f688428c5c4dc47faff5b4e2930f8c2394e0b6df","created_at":1612721776,"kind":1,"tags":null,"content":"helloooo","sig":"23f038a45cbdf38b002eca22f7d6e862f2e57c37e2d79b80eb4cd629777afd0e03bc1edd54dc8f8921684e8f2a17870d56c3598fb50e0231f5a481d9c92db3cd"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"12ba7e31b8f7e9aa2f210f03dadfdecea331ab3f8ead3c7a1cbf8f4987ac11d9","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612722130,"kind":1,"tags":null,"content":"my relay is fixed","sig":"7faeee90de41eb67bce29f03d0c1ed98024e9f4f83b9875511dccfad4fe2dc9370ea8099353a13eb54b2411778a49fd3c70b83fbabdb0b4c4f673fda07a8979c"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"c782268e60b28e3b01fc094e59716753aa6d11085b4a0c43c1f7999c9df4fb24","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612722180,"kind":1,"tags":null,"content":"I am `systmectl` clumsy","sig":"230748aeb559d1d9b9b4aae3b74fd9b5e371adfcc9a80d9b67c05d664204207bf1f440eb4a8b8959b43bbe1789bbe2b762e4aaa94355d0258ebea518a6cda0d9"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"762ad4264bcfc5594d495ca737a041c92f83206b8a65d57c33afeaa717697ef7","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612722208,"kind":1,"tags":null,"content":"I think deleting the scroll thing worked","sig":"55138849fd4debabaa9432aab7bd3f71ddf79c1c33a8eaa7549ae42c173ae0a034ce729d6a715092cd79615ffc398dc36d499ced631ff69dfdcf63cea14379c0"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"97c41b68d39f383c5820bea80cfead8066b42cb94d536b75f1601159c999ba3a","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612722277,"kind":4,"tags":null,"content":"oWSXntrVNx4bYaX/jvQ00w==?iv=UP+cLHDTS7FeleAVbn542w==","sig":"8ecc4a01011292e88d9b01917c3934ba1035152edec5b56a33a3bec56c3ab0ba357b3976ccab868df5ef73413962b15c957e70ce6c5e325d0f5ac82202ea4ade"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"1f868ba3e13c97ef0f2d622d2e4e091d53652305aa542ef7f5cb07790e57d62d","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612722401,"kind":4,"tags":null,"content":"3kS0ICsW17CGSrXFD6MjDw==?iv=Cm3hZDxNSMCp6LzgesXU9w==","sig":"3726851fb326210d3f5ad20efc2fb636a8bfef515a41fd55d82842004e2b0e6fb3d79f58af1963311c0e2a753f6373296bd97634779a593e953175716bc4bef7"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"a23bee31c65324a70c1696533040f1ad13a29d49ea7ac3faac6f7d4f490f0015","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612722420,"kind":4,"tags":null,"content":"HaE5TWUEbtI8+gcHcwccNg==?iv=9nE52WRDvEv92RKUt8/iCg==","sig":"3ce2014c6a605ca785c1315a2620b357e19ad8332c119ce392dd260b42c59552d4a157f9e367ee88d0f55994805770cec4c5947b26847d747ba2f7f50613cb43"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"91b6bd660a5c736490585c5035d6b4232723175276fa7fcb20de4d6ff2d488e9","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612722902,"kind":1,"tags":null,"content":"Whar","sig":"b3c0c1c6c2cd5f2acaf2fb5664d514dcb5a7347c9871dc342a3a8db10364c0c18785573b13f8107fa36e336566e2d155da735979c359996c65c9abf40909ad71"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"43938617983503dac8789a5e1328e9e6cba630d0038935130b6a08013199d47c","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612723064,"kind":1,"tags":null,"content":"What next then","sig":"d16d703d7919b8170acb25a7c68e2919b46723575abc017d9cb28273c8387e4fb6a31385db07ff89ee07f2c82018768e0ad052d72a6a6d9f289e45fdba58d140"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"7fb1d4d16fe68d0b1fb5c5c229505e8f581f8f1e24dc19ca87ba712224ce500c","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612723635,"kind":1,"tags":null,"content":"Scammer","sig":"a5247f0b185a50ec1d13a776b52394c24e07a668caf8ce4c20402ef23cf62a9b362cf8c18225fea2b7708680a373855e744fc09933da53ded662dcef68fc4d9c"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"d4223bb3e51a2e79b7e1f71707d7dfa3572fddd97c99e9c231e636e7d217b345","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612723646,"kind":1,"tags":null,"content":"I am on the netlify instance now","sig":"f4aafcc264ae078e5faded7af12f49be4db4fb28beabb48a837fb8b7a9374080b881022784eac187d4736e48b7865c9be2f7e3fa0e5b58a3ec4b02d25e738186"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"7b9db98b22b9d33d8782c2be56265c03fbc5302118cc80634d92abc2412489ce","pubkey":"42142dc712ff8d1a4aa0a3f6990a60dfde6a19fb872e6b98d50383b8f36d6491","created_at":1612723968,"kind":1,"tags":null,"content":"hi bitch","sig":"6cd6bd473c7319a57ec687d77b1f87bf515eeaa70b2559d838b3d7407b576b96b779c73449eef7ebcc63302bec4bc841ad88445ed734eeb1e4b8a122857288c1"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"13586d7130e6cc14a684c4ab1ad578e410183cfed8be34f48dfdc1dd02259ce9","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612724128,"kind":1,"tags":null,"content":"netlify try to not break","sig":"618109dfd3734f7247fc7702f5e6198509785d5da0d9328eb6d42f5880ddc75b056e5f9521a424e3759a44f0b64300c22557fd1e7e9f89c35f0b0e6edc51035f"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"9a0707d478c45ef06eabc412ce05f7e7bffd260d509574bfddc3f55206daac63","pubkey":"34a229681f989d0357a60f89600f150f30b8ca5879a8e4b6fe29b451d3eec3d9","created_at":1612724140,"kind":1,"tags":null,"content":"hola mundo 😎","sig":"db08c883ce6adc74e09eb8eab86f5f013bcbcbfcef247e43c3ba87d06dac504163481e76820574c704ef9414af5b1e39bb1c3907dfadf60f2f79c865763d5d07"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"2f26cb9643e1ac11c4933785c412a15b61267be3f7141817f44df3fa2acea8f0","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612724287,"kind":4,"tags":null,"content":"2g5kgiiGNmetO4p6SdQd4g==?iv=naRaO8xDiGtUyNDeuUuH1Q==","sig":"3a414f0df3f28f0a1a93741b6cc30e4966967adde6fdb8bfbd816051af39d3d5f208d11f610952208917567b276037986187e103458bd8caf42e0a75632b22fb"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"8e7ede8bdd4dc1ce73b242e9769a28af35025261e5c15c0b8e390973bd87619e","pubkey":"97eb9cffb7ed45bddfcfeef4ed158509361f459df5e5dc9a5e20e6e9e7f3292b","created_at":1612724442,"kind":1,"tags":null,"content":"Running #Bitcoin","sig":"74f126533fb2c127e64936923323a9bea47d5835296f45c52f244c3268234a942d6ca94c247585fb94ad375afe711b88a3a7fe75138280e96520d07859aae7dd"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"920a36e97fa1e6efc720eb2d633e9aed1c525bdc90a69b1c9edf4f4473d85d08","pubkey":"38ffee9a31d0ef9d4e34f8af52a63fd569535edf33b29475b17ba8a05406cd5a","created_at":1612724458,"kind":1,"tags":null,"content":"Hey","sig":"c2ec9ed12eaf40d7b12a6973aabb861e23cf90ea6c51ef321544d4d9a405b1f895426a1a3036a6d475f46968539e1e681b8839ad30bed85679c5c4f56cc6d5a9"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"a0cce98db0522ffb2416db0805d91a03834fcd15181a9f449e36485c61fbae80","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612724556,"kind":1,"tags":null,"content":"netlify should be fixed now, mostly","sig":"4dfe55db62725b39054055eb1de844373b7f21ed3274531cf010542bf448ac8b842a719af171db1f12842be441c5cfea2b3b295150e38c7b6e1716fbc20a10ec"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"3b9467414a319b4331c96375e2d1b553508799fd83bc3b83de26066a7aa395be","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612724888,"kind":4,"tags":null,"content":"gxJTObw4DhMe5r/b5CNnPA==?iv=FTPzPR5sbzCqmYMZ5TA4yg==","sig":"4a75cce11b98ccad8eec1b35352931433e48f44b2ea592b7c8dc42003738e19bdd68a72e73996940aab21d2916722b80f61240cd7730ace135c366549f98e102"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"d8683498da8ea9ccabf0a7b713d134fca696e809b5eedd2d2e792139c0fc5693","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612725164,"kind":4,"tags":null,"content":"2bK8qPakfVGmuR5YLQ86qg==?iv=tGyApLaIMpjNhckLINqnCg==","sig":"4e3590ddd80efdeebbd5fd36eb7049447ef1fecf04763be71258dcec0d3e99a6d39f3ef12eaa9479fc1c7e7b38f73d50ad44beee01cfc9ae26e1a0e417a8119c"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"d50f2b453128dcd3f3fb1ab0504d817f5ab657be8105b78fde2bd98b9ef3ec40","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612725187,"kind":4,"tags":null,"content":"IhxR3DUK6cuusvxJaXQjYA==?iv=QywipN395oAmSpPo3v/ESg==","sig":"81d139cf9f9df56f560b92bbbc5c81f4a24b85358986fd05d4008bf4d7e9d505400191adaadfdb81ae0c06b4371b63a21ab09939f6fcdaa2c58bbf43afe13c85"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"e7cd845b72ecea0e409a22526c273982b5db3c724259152c80c704781b0515ae","pubkey":"08cc733d6660e1df8677f17cb149d177a2e19bcc51cb3993234e9ec51eb50a65","created_at":1612725385,"kind":1,"tags":null,"content":"Hey","sig":"11ddd49be7ab14fed60dd7efea317a25fdcac9265407e8865a7956fad56cffa6bf0ceefb8bca5fa86c99e29263f5fad1daecf439ffc03bc6a5819d14782ab6cc"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"6bdd3088c100f71124588dfe4806993e248dc6809f52cf13606f4f331b6e9b42","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612725524,"kind":4,"tags":null,"content":"RVz8i1lgi1iAh0IfQcZdkw==?iv=teFxtIOFeboJkXZl9InubQ==","sig":"a68737ecb7524a9c840face76af714934487564189bdfeb3668f5dea5212c25534a9770319722adab4a4eed9b9be20af62e0208ab71d1f6a456edfdc3136f7a7"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"75e3fafb8cb2b949e67a938cb5edefd2dfc527e47a5b439348130eceb131196b","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612725528,"kind":4,"tags":null,"content":"RiL/bP37fcHeuhkZT5hc7Q==?iv=bJ+5UxnBFDrWMW33TA7L3g==","sig":"bc99ae69cacaf0062ff57d8c66a0f329c5f33bacf741144e467fcd9ddfce8fb4c38f8e60cab3a557f6ee6950d8737e99935fa00c234d47047de3d82672c4e128"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"4cfe3bdb4c706339257710d4312b2ef3825aff4e0bb5cbe3d51e62ff2308fc1f","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612725730,"kind":4,"tags":null,"content":"dYeM0n1w5gClZxvP/+BGiA==?iv=ddKhdGnlsQ6NKUQ+WOaPpA==","sig":"0b25b5696d8e79c758e66129db3de7d1dbc005fd416ecf31decfb8b0ffdc838528fe9cde3d520225f4d4fc963787a42f65e8c474ae6d935a375dc04603cc2636"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"fa7754c8e6d3ce77ec788a585385563582ef8996afb61eb66eaf9ec20bc481b0","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612725784,"kind":1,"tags":null,"content":"Well that was trial by fire 🔥","sig":"27d8ef4439e09eecb52c56886bdfd3882d547f34a7de05db99b174163be15fc016523fd31871ef630c2aa31e0dab0baefc0575be51abf2e373918d1e2ff55973"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"7b32445285e3d59c66ff74031370d506303c1cc254f361e1dd1ba0ac8660f140","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612726042,"kind":4,"tags":null,"content":"vuc8VIl0xz8b5kNIGfs0Ow==?iv=2KKjOb05Cd3kMn56PYEMLw==","sig":"14f9ed8ab8a811f8def79370c23be55738418e380b28b76fae5c09b38a34fb8d98fe724ff78eedcea280aad4b3c1200e9956c1c414abfcc813614d715191e8de"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"a7e8da90926dcaf9fa4a0897cc07c724f27b0f7725d538f807c6ee6108905ddc","pubkey":"901d6d9cfcb94291b8be03d44895224fc26c024c79efca6e6daa04589956887e","created_at":1612726051,"kind":1,"tags":null,"content":"Test nost","sig":"89fe7cea6e2ef532fcbeb1e25cac78c8d6d539c923d08ad75873831f6b281637ba440bf714f62f05e7f2bcc92b247d6b7bd1b61c03bab306ecfc02fd83791554"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"c5a6dcf0a1f9bf5072189b60e8e8b3f46b0ae862232de95b575424db9eb5b536","pubkey":"901d6d9cfcb94291b8be03d44895224fc26c024c79efca6e6daa04589956887e","created_at":1612726088,"kind":1,"tags":null,"content":"relay says rate-limit","sig":"62a0432144584e6dd37d1717f35e9a7d3a1c6db2a2d8e490322009946614c00fff9d7f2d39b0822f0abffab79ab70f15f26d3d0f26e1c742c7d17cadc772811a"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"002afa98c0293fa7b9846934d2fc9d0d4508d9e7767d663d25fd3cd2aecb283b","pubkey":"b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9","created_at":1612726313,"kind":4,"tags":null,"content":"NhLGT83vTqBayskoh6V4Aw==?iv=XGd0VFUVIri7ml9rRJC33A==","sig":"00307cf72b7b467175f5c0d29bd78af1a702589b7ecae0a9634c8f2fcc6d8391017127f46229b637bb44ed91d4e0c29361b70cd1e8c8e5036c30550616860ff9"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"75b58a2b7992fbcad1ce996ac1ad5daaf72b495063db6ca42c0b70c2ced4b5d1","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612726316,"kind":1,"tags":null,"content":"if anyone is DMing me I don't see it","sig":"02f8529deaf5cfa8c717ecd51f421c2ec0fbf58a4f1c66fc8462f678aa22ef49d497dd21bdd414899a8fb355d6808e325097ebe90a4641fc4a3ba8f5b60d1a57"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"e61e00cfee8917f0266b6b51e2888342d66cf2e416f155083a0bdda4b35689a0","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612726363,"kind":1,"tags":null,"content":"Do you see this b09f2...","sig":"90f12736588764f22c0c75aa1f3d6fe58dd4c22b897d32659c5cedbd0b73315e8c41bae01277c43442997f91053c3acf1c968284df0053a3976c56393189138f"}]
|
||||||
|
["EVENT","5496183021909538",{"id":"b1b6fe662f109c4d60512c2e23a7c4e70c9b536a173c850efd68167779a7c0e4","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612726530,"kind":4,"tags":null,"content":"oidImNefkwTgiaCLZ/5veA==?iv=TM30ouqWWuSDIjgnUZvfMA==","sig":"b9be71cf72afb23135534d118abf16eb7cd0c2053159a5bbd9b4bb0e910f82ae8673c1cc4c6f0cbe614e2d30de3620e5337fb7d37edc6273c2916f6ad20af64b"}]
|
||||||
+53
-19
@@ -1,30 +1,64 @@
|
|||||||
//! Subscription close request parsing
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
/// Close command in network format
|
// Container for a request to close a subscription
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||||
|
#[serde(transparent)]
|
||||||
pub struct CloseCmd {
|
pub struct CloseCmd {
|
||||||
/// Protocol command, expected to always be "CLOSE".
|
cmds: Vec<String>,
|
||||||
cmd: String,
|
}
|
||||||
/// The subscription identifier being closed.
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub struct Close {
|
||||||
id: String,
|
id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close command parsed
|
impl<'de> Deserialize<'de> for Close {
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
fn deserialize<D>(deserializer: D) -> Result<Close, D::Error>
|
||||||
pub struct Close {
|
where
|
||||||
/// The subscription identifier being closed.
|
D: Deserializer<'de>,
|
||||||
pub id: String,
|
{
|
||||||
}
|
let mut v: serde_json::Value = Deserialize::deserialize(deserializer)?;
|
||||||
|
// this shoud be an exactly 2-element array
|
||||||
|
// verify the first element is a String, CLOSE
|
||||||
|
// get the subscription from the second element.
|
||||||
|
|
||||||
impl From<CloseCmd> for Result<Close> {
|
// check for array
|
||||||
fn from(cc: CloseCmd) -> Result<Close> {
|
let va = v
|
||||||
// ensure command is correct
|
.as_array_mut()
|
||||||
if cc.cmd != "CLOSE" {
|
.ok_or(serde::de::Error::custom("not array"))?;
|
||||||
Err(Error::CommandUnknownError)
|
|
||||||
} else {
|
// check length
|
||||||
Ok(Close { id: cc.id })
|
if va.len() != 2 {
|
||||||
|
return Err(serde::de::Error::custom("not exactly 2 fields"));
|
||||||
}
|
}
|
||||||
|
let mut i = va.into_iter();
|
||||||
|
// get command ("REQ") and ensure it is a string
|
||||||
|
let req_cmd_str: serde_json::Value = i.next().unwrap().take();
|
||||||
|
let req = req_cmd_str.as_str().ok_or(serde::de::Error::custom(
|
||||||
|
"first element of request was not a string",
|
||||||
|
))?;
|
||||||
|
if req != "CLOSE" {
|
||||||
|
return Err(serde::de::Error::custom("missing CLOSE command"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure sub id is a string
|
||||||
|
let sub_id_str: serde_json::Value = i.next().unwrap().take();
|
||||||
|
let sub_id = sub_id_str
|
||||||
|
.as_str()
|
||||||
|
.ok_or(serde::de::Error::custom("missing subscription id"))?;
|
||||||
|
|
||||||
|
Ok(Close {
|
||||||
|
id: sub_id.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Close {
|
||||||
|
pub fn parse(json: &str) -> Result<Close> {
|
||||||
|
serde_json::from_str(json).map_err(|e| Error::JsonParseFailed(e))
|
||||||
|
}
|
||||||
|
pub fn get_id(&self) -> String {
|
||||||
|
self.id.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-108
@@ -1,108 +0,0 @@
|
|||||||
use lazy_static::lazy_static;
|
|
||||||
use log::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::sync::RwLock;
|
|
||||||
|
|
||||||
// initialize a singleton default configuration
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref SETTINGS: RwLock<Settings> = RwLock::new(Settings::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[allow(unused)]
|
|
||||||
pub struct Network {
|
|
||||||
pub port: u16,
|
|
||||||
pub address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[allow(unused)]
|
|
||||||
pub struct Options {
|
|
||||||
pub reject_future_seconds: Option<usize>, // if defined, reject any events with a timestamp more than X seconds in the future
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[allow(unused)]
|
|
||||||
pub struct Retention {
|
|
||||||
// TODO: implement
|
|
||||||
pub max_events: Option<usize>, // max events
|
|
||||||
pub max_bytes: Option<usize>, // max size
|
|
||||||
pub persist_days: Option<usize>, // oldest message
|
|
||||||
pub whitelist_addresses: Option<Vec<String>>, // whitelisted addresses (never delete)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[allow(unused)]
|
|
||||||
pub struct Limits {
|
|
||||||
pub messages_per_sec: Option<u32>, // Artificially slow down event writing to limit disk consumption (averaged over 1 minute)
|
|
||||||
pub max_event_bytes: Option<usize>,
|
|
||||||
pub max_ws_message_bytes: Option<usize>,
|
|
||||||
pub max_ws_frame_bytes: Option<usize>,
|
|
||||||
pub broadcast_buffer: usize, // events to buffer for subscribers (prevents slow readers from consuming memory)
|
|
||||||
pub event_persist_buffer: usize, // events to buffer for database commits (block senders if database writes are too slow)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[allow(unused)]
|
|
||||||
pub struct Settings {
|
|
||||||
pub network: Network,
|
|
||||||
pub limits: Limits,
|
|
||||||
pub retention: Retention,
|
|
||||||
pub options: Options,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Settings {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let d = Self::default();
|
|
||||||
// attempt to construct settings with file
|
|
||||||
// Self::new_from_default(&d).unwrap_or(d)
|
|
||||||
let from_file = Self::new_from_default(&d);
|
|
||||||
match from_file {
|
|
||||||
Ok(f) => f,
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error reading config file ({:?})", e);
|
|
||||||
d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_from_default(default: &Settings) -> Result<Self, config::ConfigError> {
|
|
||||||
let config: config::Config = config::Config::new();
|
|
||||||
let settings: Settings = config
|
|
||||||
// use defaults
|
|
||||||
.with_merged(config::Config::try_from(default).unwrap())?
|
|
||||||
// override with file contents
|
|
||||||
.with_merged(config::File::with_name("config"))?
|
|
||||||
.try_into()?;
|
|
||||||
Ok(settings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Settings {
|
|
||||||
fn default() -> Self {
|
|
||||||
Settings {
|
|
||||||
network: Network {
|
|
||||||
port: 8080,
|
|
||||||
address: "0.0.0.0".to_owned(),
|
|
||||||
},
|
|
||||||
limits: Limits {
|
|
||||||
messages_per_sec: None,
|
|
||||||
max_event_bytes: Some(2 << 17), // 128K
|
|
||||||
max_ws_message_bytes: Some(2 << 17), // 128K
|
|
||||||
max_ws_frame_bytes: Some(2 << 17), // 128K
|
|
||||||
broadcast_buffer: 4096,
|
|
||||||
event_persist_buffer: 16,
|
|
||||||
},
|
|
||||||
retention: Retention {
|
|
||||||
max_events: None, // max events
|
|
||||||
max_bytes: None, // max size
|
|
||||||
persist_days: None, // oldest message
|
|
||||||
whitelist_addresses: None, // whitelisted addresses (never delete)
|
|
||||||
},
|
|
||||||
options: Options {
|
|
||||||
reject_future_seconds: Some(30 * 60), // Reject events 30min in the future or greater
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-102
@@ -1,102 +0,0 @@
|
|||||||
//! Client connection state
|
|
||||||
use crate::close::Close;
|
|
||||||
use crate::error::Error;
|
|
||||||
use crate::error::Result;
|
|
||||||
use crate::event::Event;
|
|
||||||
|
|
||||||
use crate::subscription::Subscription;
|
|
||||||
use log::*;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
/// A subscription identifier has a maximum length
|
|
||||||
const MAX_SUBSCRIPTION_ID_LEN: usize = 256;
|
|
||||||
|
|
||||||
/// State for a client connection
|
|
||||||
pub struct ClientConn {
|
|
||||||
/// Unique client identifier generated at connection time
|
|
||||||
client_id: Uuid,
|
|
||||||
/// The current set of active client subscriptions
|
|
||||||
subscriptions: HashMap<String, Subscription>,
|
|
||||||
/// Per-connection maximum concurrent subscriptions
|
|
||||||
max_subs: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ClientConn {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientConn {
|
|
||||||
/// Create a new, empty connection state.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let client_id = Uuid::new_v4();
|
|
||||||
ClientConn {
|
|
||||||
client_id,
|
|
||||||
subscriptions: HashMap::new(),
|
|
||||||
max_subs: 32,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a short prefix of the client's unique identifier, suitable
|
|
||||||
/// for logging.
|
|
||||||
pub fn get_client_prefix(&self) -> String {
|
|
||||||
self.client_id.to_string().chars().take(8).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find all matching subscriptions.
|
|
||||||
pub fn get_matching_subscriptions(&self, e: &Event) -> Vec<&str> {
|
|
||||||
let mut v: Vec<&str> = vec![];
|
|
||||||
for (id, sub) in self.subscriptions.iter() {
|
|
||||||
if sub.interested_in_event(e) {
|
|
||||||
v.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new subscription for this connection.
|
|
||||||
pub fn subscribe(&mut self, s: Subscription) -> Result<()> {
|
|
||||||
let k = s.get_id();
|
|
||||||
let sub_id_len = k.len();
|
|
||||||
// prevent arbitrarily long subscription identifiers from
|
|
||||||
// being used.
|
|
||||||
if sub_id_len > MAX_SUBSCRIPTION_ID_LEN {
|
|
||||||
info!(
|
|
||||||
"ignoring sub request with excessive length: ({})",
|
|
||||||
sub_id_len
|
|
||||||
);
|
|
||||||
return Err(Error::SubIdMaxLengthError);
|
|
||||||
}
|
|
||||||
// check if an existing subscription exists, and replace if so
|
|
||||||
if self.subscriptions.contains_key(&k) {
|
|
||||||
self.subscriptions.remove(&k);
|
|
||||||
self.subscriptions.insert(k, s);
|
|
||||||
debug!("replaced existing subscription");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if there is room for another subscription.
|
|
||||||
if self.subscriptions.len() >= self.max_subs {
|
|
||||||
return Err(Error::SubMaxExceededError);
|
|
||||||
}
|
|
||||||
// add subscription
|
|
||||||
self.subscriptions.insert(k, s);
|
|
||||||
debug!(
|
|
||||||
"registered new subscription, currently have {} active subs",
|
|
||||||
self.subscriptions.len()
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove the subscription for this connection.
|
|
||||||
pub fn unsubscribe(&mut self, c: Close) {
|
|
||||||
// TODO: return notice if subscription did not exist.
|
|
||||||
self.subscriptions.remove(&c.id);
|
|
||||||
debug!(
|
|
||||||
"removed subscription, currently have {} active subs",
|
|
||||||
self.subscriptions.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
//! Event persistence and querying
|
|
||||||
use crate::error::Result;
|
|
||||||
use crate::event::Event;
|
|
||||||
use crate::subscription::Subscription;
|
|
||||||
use governor::clock::Clock;
|
|
||||||
use governor::{Quota, RateLimiter};
|
|
||||||
use hex;
|
|
||||||
use log::*;
|
|
||||||
use rusqlite::params;
|
|
||||||
use rusqlite::Connection;
|
|
||||||
use rusqlite::OpenFlags;
|
|
||||||
//use std::num::NonZeroU32;
|
|
||||||
use crate::config::SETTINGS;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::thread;
|
|
||||||
use tokio::task;
|
|
||||||
|
|
||||||
/// Database file
|
|
||||||
const DB_FILE: &str = "nostr.db";
|
|
||||||
|
|
||||||
/// Startup DB Pragmas
|
|
||||||
const STARTUP_SQL: &str = r##"
|
|
||||||
PRAGMA main.synchronous=NORMAL;
|
|
||||||
PRAGMA foreign_keys = ON;
|
|
||||||
pragma mmap_size = 536870912; -- 512MB of mmap
|
|
||||||
"##;
|
|
||||||
|
|
||||||
/// Schema definition
|
|
||||||
const INIT_SQL: &str = r##"
|
|
||||||
-- Database settings
|
|
||||||
PRAGMA encoding = "UTF-8";
|
|
||||||
PRAGMA journal_mode=WAL;
|
|
||||||
PRAGMA main.synchronous=NORMAL;
|
|
||||||
PRAGMA foreign_keys = ON;
|
|
||||||
PRAGMA application_id = 1654008667;
|
|
||||||
PRAGMA user_version = 2;
|
|
||||||
|
|
||||||
-- Event Table
|
|
||||||
CREATE TABLE IF NOT EXISTS event (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
event_hash BLOB NOT NULL, -- 4-byte hash
|
|
||||||
first_seen INTEGER NOT NULL, -- when the event was first seen (not authored!) (seconds since 1970)
|
|
||||||
created_at INTEGER NOT NULL, -- when the event was authored
|
|
||||||
author BLOB NOT NULL, -- author pubkey
|
|
||||||
kind INTEGER NOT NULL, -- event kind
|
|
||||||
hidden INTEGER, -- relevant for queries
|
|
||||||
content TEXT NOT NULL -- serialized json of event object
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Event Indexes
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash);
|
|
||||||
CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at);
|
|
||||||
CREATE INDEX IF NOT EXISTS author_index ON event(author);
|
|
||||||
CREATE INDEX IF NOT EXISTS kind_index ON event(kind);
|
|
||||||
|
|
||||||
-- Event References Table
|
|
||||||
CREATE TABLE IF NOT EXISTS event_ref (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
event_id INTEGER NOT NULL, -- an event ID that contains an #e tag.
|
|
||||||
referenced_event BLOB NOT NULL, -- the event that is referenced.
|
|
||||||
FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Event References Index
|
|
||||||
CREATE INDEX IF NOT EXISTS event_ref_index ON event_ref(referenced_event);
|
|
||||||
|
|
||||||
-- Pubkey References Table
|
|
||||||
CREATE TABLE IF NOT EXISTS pubkey_ref (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
event_id INTEGER NOT NULL, -- an event ID that contains an #p tag.
|
|
||||||
referenced_pubkey BLOB NOT NULL, -- the pubkey that is referenced.
|
|
||||||
FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE RESTRICT ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Pubkey References Index
|
|
||||||
CREATE INDEX IF NOT EXISTS pubkey_ref_index ON pubkey_ref(referenced_pubkey);
|
|
||||||
"##;
|
|
||||||
|
|
||||||
/// Upgrade DB to latest version, and execute pragma settings
|
|
||||||
pub fn upgrade_db(conn: &mut Connection) -> Result<()> {
|
|
||||||
// check the version.
|
|
||||||
let curr_version = db_version(conn)?;
|
|
||||||
info!("DB version = {:?}", curr_version);
|
|
||||||
|
|
||||||
// initialize from scratch
|
|
||||||
if curr_version == 0 {
|
|
||||||
match conn.execute_batch(INIT_SQL) {
|
|
||||||
Ok(()) => info!("database pragma/schema initialized to v2, and ready"),
|
|
||||||
Err(err) => {
|
|
||||||
error!("update failed: {}", err);
|
|
||||||
panic!("database could not be initialized");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if curr_version == 1 {
|
|
||||||
// only change is adding a hidden column to events.
|
|
||||||
let upgrade_sql = r##"
|
|
||||||
ALTER TABLE event ADD hidden INTEGER;
|
|
||||||
UPDATE event SET hidden=FALSE;
|
|
||||||
PRAGMA user_version = 2;
|
|
||||||
"##;
|
|
||||||
match conn.execute_batch(upgrade_sql) {
|
|
||||||
Ok(()) => info!("database schema upgraded v1 -> v2"),
|
|
||||||
Err(err) => {
|
|
||||||
error!("update failed: {}", err);
|
|
||||||
panic!("database could not be upgraded");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if curr_version == 2 {
|
|
||||||
debug!("Database version was already current");
|
|
||||||
} else if curr_version > 2 {
|
|
||||||
panic!("Database version is newer than supported by this executable");
|
|
||||||
}
|
|
||||||
// Setup PRAGMA
|
|
||||||
conn.execute_batch(STARTUP_SQL)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawn a database writer that persists events to the SQLite store.
|
|
||||||
pub async fn db_writer(
|
|
||||||
mut event_rx: tokio::sync::mpsc::Receiver<Event>,
|
|
||||||
bcast_tx: tokio::sync::broadcast::Sender<Event>,
|
|
||||||
mut shutdown: tokio::sync::broadcast::Receiver<()>,
|
|
||||||
) -> tokio::task::JoinHandle<Result<()>> {
|
|
||||||
task::spawn_blocking(move || {
|
|
||||||
let mut conn = Connection::open_with_flags(
|
|
||||||
Path::new(DB_FILE),
|
|
||||||
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
|
|
||||||
)?;
|
|
||||||
info!("opened database for writing");
|
|
||||||
upgrade_db(&mut conn)?;
|
|
||||||
// get rate limit settings
|
|
||||||
let config = SETTINGS.read().unwrap();
|
|
||||||
let rps_setting = config.limits.messages_per_sec;
|
|
||||||
let mut lim_opt = None;
|
|
||||||
let clock = governor::clock::QuantaClock::default();
|
|
||||||
if let Some(rps) = rps_setting {
|
|
||||||
if rps > 0 {
|
|
||||||
info!("Enabling rate limits for event creation ({}/sec)", rps);
|
|
||||||
let quota = core::num::NonZeroU32::new(rps * 60).unwrap();
|
|
||||||
lim_opt = Some(RateLimiter::direct(Quota::per_minute(quota)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loop {
|
|
||||||
if let Ok(_) = shutdown.try_recv() {
|
|
||||||
info!("shutting down database writer");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// call blocking read on channel
|
|
||||||
let next_event = event_rx.blocking_recv();
|
|
||||||
// if the channel has closed, we will never get work
|
|
||||||
if next_event.is_none() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let mut event_write = false;
|
|
||||||
let event = next_event.unwrap();
|
|
||||||
match write_event(&mut conn, &event) {
|
|
||||||
Ok(updated) => {
|
|
||||||
if updated == 0 {
|
|
||||||
debug!("ignoring duplicate event");
|
|
||||||
} else {
|
|
||||||
info!("persisted event: {}", event.get_event_id_prefix());
|
|
||||||
event_write = true;
|
|
||||||
// send this out to all clients
|
|
||||||
bcast_tx.send(event.clone()).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("event insert failed: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// use rate limit, if defined, and if an event was actually written.
|
|
||||||
if event_write {
|
|
||||||
if let Some(ref lim) = lim_opt {
|
|
||||||
if let Err(n) = lim.check() {
|
|
||||||
info!("Rate limiting event creation");
|
|
||||||
thread::sleep(n.wait_time_from(clock.now()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conn.close().ok();
|
|
||||||
info!("database connection closed");
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn db_version(conn: &mut Connection) -> Result<usize> {
|
|
||||||
let query = "PRAGMA user_version;";
|
|
||||||
let curr_version = conn.query_row(query, [], |row| row.get(0))?;
|
|
||||||
Ok(curr_version)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Persist an event to the database.
|
|
||||||
pub fn write_event(conn: &mut Connection, e: &Event) -> Result<usize> {
|
|
||||||
// start transaction
|
|
||||||
let tx = conn.transaction()?;
|
|
||||||
// get relevant fields from event and convert to blobs.
|
|
||||||
let id_blob = hex::decode(&e.id).ok();
|
|
||||||
let pubkey_blob = hex::decode(&e.pubkey).ok();
|
|
||||||
let event_str = serde_json::to_string(&e).ok();
|
|
||||||
// ignore if the event hash is a duplicate.
|
|
||||||
let ins_count = tx.execute(
|
|
||||||
"INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, strftime('%s','now'), FALSE);",
|
|
||||||
params![id_blob, e.created_at, e.kind, pubkey_blob, event_str]
|
|
||||||
)?;
|
|
||||||
if ins_count == 0 {
|
|
||||||
// if the event was a duplicate, no need to insert event or
|
|
||||||
// pubkey references.
|
|
||||||
return Ok(ins_count);
|
|
||||||
}
|
|
||||||
// remember primary key of the event most recently inserted.
|
|
||||||
let ev_id = tx.last_insert_rowid();
|
|
||||||
// add all event tags into the event_ref table
|
|
||||||
let etags = e.get_event_tags();
|
|
||||||
if !etags.is_empty() {
|
|
||||||
for etag in etags.iter() {
|
|
||||||
tx.execute(
|
|
||||||
"INSERT OR IGNORE INTO event_ref (event_id, referenced_event) VALUES (?1, ?2)",
|
|
||||||
params![ev_id, hex::decode(&etag).ok()],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// add all event tags into the pubkey_ref table
|
|
||||||
let ptags = e.get_pubkey_tags();
|
|
||||||
if !ptags.is_empty() {
|
|
||||||
for ptag in ptags.iter() {
|
|
||||||
tx.execute(
|
|
||||||
"INSERT OR IGNORE INTO pubkey_ref (event_id, referenced_pubkey) VALUES (?1, ?2)",
|
|
||||||
params![ev_id, hex::decode(&ptag).ok()],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if this event is for a metadata update, hide every other kind=0
|
|
||||||
// event from the same author that was issued earlier than this.
|
|
||||||
if e.kind == 0 {
|
|
||||||
let update_count = tx.execute(
|
|
||||||
"UPDATE event SET hidden=TRUE WHERE id!=? AND kind=0 AND author=? AND created_at <= ? and hidden!=TRUE",
|
|
||||||
params![ev_id, hex::decode(&e.pubkey).ok(), e.created_at],
|
|
||||||
)?;
|
|
||||||
if update_count > 0 {
|
|
||||||
info!("hid {} older metadata events", update_count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if this event is for a contact update, hide every other kind=3
|
|
||||||
// event from the same author that was issued earlier than this.
|
|
||||||
if e.kind == 3 {
|
|
||||||
let update_count = tx.execute(
|
|
||||||
"UPDATE event SET hidden=TRUE WHERE id!=? AND kind=3 AND author=? AND created_at <= ? and hidden!=TRUE",
|
|
||||||
params![ev_id, hex::decode(&e.pubkey).ok(), e.created_at],
|
|
||||||
)?;
|
|
||||||
if update_count > 0 {
|
|
||||||
info!("hid {} older contact events", update_count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tx.commit()?;
|
|
||||||
Ok(ins_count)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event resulting from a specific subscription request
|
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
|
||||||
pub struct QueryResult {
|
|
||||||
/// Subscription identifier
|
|
||||||
pub sub_id: String,
|
|
||||||
/// Serialized event
|
|
||||||
pub event: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a string contains only hex characters.
|
|
||||||
fn is_hex(s: &str) -> bool {
|
|
||||||
s.chars().all(|x| char::is_ascii_hexdigit(&x))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a dynamic SQL query string from a subscription.
|
|
||||||
fn query_from_sub(sub: &Subscription) -> String {
|
|
||||||
// build a dynamic SQL query. all user-input is either an integer
|
|
||||||
// (sqli-safe), or a string that is filtered to only contain
|
|
||||||
// hexadecimal characters.
|
|
||||||
let mut query =
|
|
||||||
"SELECT DISTINCT(e.content) FROM event e LEFT JOIN event_ref er ON e.id=er.event_id LEFT JOIN pubkey_ref pr ON e.id=pr.event_id "
|
|
||||||
.to_owned();
|
|
||||||
// for every filter in the subscription, generate a where clause
|
|
||||||
let mut filter_clauses: Vec<String> = Vec::new();
|
|
||||||
for f in sub.filters.iter() {
|
|
||||||
// individual filter components
|
|
||||||
let mut filter_components: Vec<String> = Vec::new();
|
|
||||||
// Query for "authors"
|
|
||||||
if f.authors.is_some() {
|
|
||||||
let authors_escaped: Vec<String> = f
|
|
||||||
.authors
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.filter(|&x| is_hex(x))
|
|
||||||
.map(|x| format!("x'{}'", x))
|
|
||||||
.collect();
|
|
||||||
let authors_clause = format!("author IN ({})", authors_escaped.join(", "));
|
|
||||||
filter_components.push(authors_clause);
|
|
||||||
}
|
|
||||||
// Query for Kind
|
|
||||||
if f.kind.is_some() {
|
|
||||||
// kind is number, no escaping needed
|
|
||||||
let kind_clause = format!("kind = {}", f.kind.unwrap());
|
|
||||||
filter_components.push(kind_clause);
|
|
||||||
}
|
|
||||||
// Query for event
|
|
||||||
if f.id.is_some() {
|
|
||||||
let id_str = f.id.as_ref().unwrap();
|
|
||||||
if is_hex(id_str) {
|
|
||||||
let id_clause = format!("event_hash = x'{}'", id_str);
|
|
||||||
filter_components.push(id_clause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Query for referenced event
|
|
||||||
if f.event.is_some() {
|
|
||||||
let ev_str = f.event.as_ref().unwrap();
|
|
||||||
if is_hex(ev_str) {
|
|
||||||
let ev_clause = format!("referenced_event = x'{}'", ev_str);
|
|
||||||
filter_components.push(ev_clause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Query for referenced pet name pubkey
|
|
||||||
if f.pubkey.is_some() {
|
|
||||||
let pet_str = f.pubkey.as_ref().unwrap();
|
|
||||||
if is_hex(pet_str) {
|
|
||||||
let pet_clause = format!("referenced_pubkey = x'{}'", pet_str);
|
|
||||||
filter_components.push(pet_clause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Query for timestamp
|
|
||||||
if f.since.is_some() {
|
|
||||||
let created_clause = format!("created_at > {}", f.since.unwrap());
|
|
||||||
filter_components.push(created_clause);
|
|
||||||
}
|
|
||||||
// Query for timestamp
|
|
||||||
if f.until.is_some() {
|
|
||||||
let until_clause = format!("created_at < {}", f.until.unwrap());
|
|
||||||
filter_components.push(until_clause);
|
|
||||||
}
|
|
||||||
|
|
||||||
// combine all clauses, and add to filter_clauses
|
|
||||||
if !filter_components.is_empty() {
|
|
||||||
let mut fc = "( ".to_owned();
|
|
||||||
fc.push_str(&filter_components.join(" AND "));
|
|
||||||
fc.push_str(" )");
|
|
||||||
filter_clauses.push(fc);
|
|
||||||
} else {
|
|
||||||
// never display hidden events
|
|
||||||
filter_clauses.push("hidden!=FALSE".to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// combine all filters with OR clauses, if any exist
|
|
||||||
if !filter_clauses.is_empty() {
|
|
||||||
query.push_str(" WHERE ");
|
|
||||||
query.push_str(&filter_clauses.join(" OR "));
|
|
||||||
}
|
|
||||||
// add order clause
|
|
||||||
query.push_str(" ORDER BY created_at ASC");
|
|
||||||
debug!("query string: {}", query);
|
|
||||||
query
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform a database query using a subscription.
|
|
||||||
///
|
|
||||||
/// The [`Subscription`] is converted into a SQL query. Each result
|
|
||||||
/// is published on the `query_tx` channel as it is returned. If a
|
|
||||||
/// message becomes available on the `abandon_query_rx` channel, the
|
|
||||||
/// query is immediately aborted.
|
|
||||||
pub async fn db_query(
|
|
||||||
sub: Subscription,
|
|
||||||
query_tx: tokio::sync::mpsc::Sender<QueryResult>,
|
|
||||||
mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>,
|
|
||||||
) {
|
|
||||||
task::spawn_blocking(move || {
|
|
||||||
let conn =
|
|
||||||
Connection::open_with_flags(Path::new(DB_FILE), OpenFlags::SQLITE_OPEN_READ_ONLY)
|
|
||||||
.unwrap();
|
|
||||||
debug!("opened database for reading");
|
|
||||||
debug!("going to query for: {:?}", sub);
|
|
||||||
// generate SQL query
|
|
||||||
let q = query_from_sub(&sub);
|
|
||||||
// execute the query
|
|
||||||
let mut stmt = conn.prepare(&q).unwrap();
|
|
||||||
let mut event_rows = stmt.query([]).unwrap();
|
|
||||||
while let Some(row) = event_rows.next().unwrap() {
|
|
||||||
// check if this is still active (we could do this every N rows)
|
|
||||||
if abandon_query_rx.try_recv().is_ok() {
|
|
||||||
debug!("query aborted");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO: check before unwrapping
|
|
||||||
let event_json = row.get(0).unwrap();
|
|
||||||
query_tx
|
|
||||||
.blocking_send(QueryResult {
|
|
||||||
sub_id: sub.get_id(),
|
|
||||||
event: event_json,
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
debug!("query completed");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+8
-49
@@ -1,67 +1,26 @@
|
|||||||
//! Error handling
|
//! Error handling.
|
||||||
|
|
||||||
use std::result;
|
use std::result;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tungstenite::error::Error as WsError;
|
|
||||||
|
|
||||||
/// Simple `Result` type for errors in this module
|
|
||||||
pub type Result<T, E = Error> = result::Result<T, E>;
|
pub type Result<T, E = Error> = result::Result<T, E>;
|
||||||
|
|
||||||
/// Custom error type for Nostr
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Protocol parse error")]
|
#[error("command from client not recognized")]
|
||||||
ProtoParseError,
|
CommandNotFound,
|
||||||
#[error("Connection error")]
|
#[error("parsing JSON->Event failed")]
|
||||||
ConnError,
|
|
||||||
#[error("Client write error")]
|
|
||||||
ConnWriteError,
|
|
||||||
#[error("EVENT parse failed")]
|
|
||||||
EventParseFailed,
|
EventParseFailed,
|
||||||
#[error("ClOSE message parse failed")]
|
#[error("parsing JSON->Req failed")]
|
||||||
|
ReqParseFailed,
|
||||||
|
#[error("parsing JSON->Close failed")]
|
||||||
CloseParseFailed,
|
CloseParseFailed,
|
||||||
#[error("Event validation failed")]
|
|
||||||
EventInvalid,
|
|
||||||
#[error("Subscription identifier max length exceeded")]
|
|
||||||
SubIdMaxLengthError,
|
|
||||||
#[error("Maximum concurrent subscription count reached")]
|
|
||||||
SubMaxExceededError,
|
|
||||||
// this should be used if the JSON is invalid
|
|
||||||
#[error("JSON parsing failed")]
|
#[error("JSON parsing failed")]
|
||||||
JsonParseFailed(serde_json::Error),
|
JsonParseFailed(serde_json::Error),
|
||||||
#[error("WebSocket proto error")]
|
|
||||||
WebsocketError(WsError),
|
|
||||||
#[error("Command unknown")]
|
|
||||||
CommandUnknownError,
|
|
||||||
#[error("SQL error")]
|
|
||||||
SqlError(rusqlite::Error),
|
|
||||||
#[error("Config error")]
|
|
||||||
ConfigError(config::ConfigError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<rusqlite::Error> for Error {
|
|
||||||
/// Wrap SQL error
|
|
||||||
fn from(r: rusqlite::Error) -> Self {
|
|
||||||
Error::SqlError(r)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<serde_json::Error> for Error {
|
impl From<serde_json::Error> for Error {
|
||||||
/// Wrap JSON error
|
|
||||||
fn from(r: serde_json::Error) -> Self {
|
fn from(r: serde_json::Error) -> Self {
|
||||||
Error::JsonParseFailed(r)
|
Error::JsonParseFailed(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<WsError> for Error {
|
|
||||||
/// Wrap Websocket error
|
|
||||||
fn from(r: WsError) -> Self {
|
|
||||||
Error::WebsocketError(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<config::ConfigError> for Error {
|
|
||||||
/// Wrap Config error
|
|
||||||
fn from(r: config::ConfigError) -> Self {
|
|
||||||
Error::ConfigError(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+34
-213
@@ -1,189 +1,57 @@
|
|||||||
//! Event parsing and validation
|
use crate::error::{Error, Result};
|
||||||
use crate::config;
|
|
||||||
use crate::error::Error::*;
|
|
||||||
use crate::error::Result;
|
|
||||||
use bitcoin_hashes::{sha256, Hash};
|
|
||||||
use log::*;
|
|
||||||
use secp256k1::{schnorrsig, Secp256k1};
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serde_json::value::Value;
|
//use serde_json::json;
|
||||||
use serde_json::Number;
|
//use serde_json::Result;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
/// Event command in network format
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
|
||||||
pub struct EventCmd {
|
|
||||||
cmd: String, // expecting static "EVENT"
|
|
||||||
event: Event,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event parsed
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
pub id: String,
|
pub(crate) id: String,
|
||||||
pub(crate) pubkey: String,
|
pub(crate) pubkey: String,
|
||||||
pub(crate) created_at: u64,
|
pub(crate) created_at: u64,
|
||||||
pub(crate) kind: u64,
|
pub(crate) kind: u8,
|
||||||
#[serde(deserialize_with = "tag_from_string")]
|
#[serde(deserialize_with = "tag_from_string")]
|
||||||
// NOTE: array-of-arrays may need to be more general than a string container
|
|
||||||
pub(crate) tags: Vec<Vec<String>>,
|
pub(crate) tags: Vec<Vec<String>>,
|
||||||
pub(crate) content: String,
|
pub(crate) content: String,
|
||||||
pub(crate) sig: String,
|
pub(crate) sig: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple tag type for array of array of strings.
|
|
||||||
type Tag = Vec<Vec<String>>;
|
type Tag = Vec<Vec<String>>;
|
||||||
|
|
||||||
/// Deserializer that ensures we always have a [`Tag`].
|
// handle a default value (empty vec) for null tags
|
||||||
fn tag_from_string<'de, D>(deserializer: D) -> Result<Tag, D::Error>
|
fn tag_from_string<'de, D>(deserializer: D) -> Result<Tag, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let opt = Option::deserialize(deserializer)?;
|
let opt = Option::deserialize(deserializer)?;
|
||||||
Ok(opt.unwrap_or_else(Vec::new))
|
Ok(opt.unwrap_or_else(|| vec![]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert network event to parsed/validated event.
|
// Goals:
|
||||||
impl From<EventCmd> for Result<Event> {
|
// Roundtrip from JSON-string to Event, and back to string.
|
||||||
fn from(ec: EventCmd) -> Result<Event> {
|
// Perform validation on an Event to ensure the id and signature are correct.
|
||||||
// ensure command is correct
|
|
||||||
if ec.cmd != "EVENT" {
|
|
||||||
Err(CommandUnknownError)
|
|
||||||
} else if ec.event.is_valid() {
|
|
||||||
Ok(ec.event)
|
|
||||||
} else {
|
|
||||||
Err(EventInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Seconds since 1970
|
|
||||||
fn unix_time() -> u64 {
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.map(|x| x.as_secs())
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
/// Create a short event identifier, suitable for logging.
|
pub fn parse(json: &str) -> Result<Event> {
|
||||||
pub fn get_event_id_prefix(&self) -> String {
|
let _e: Event = serde_json::from_str(json)?;
|
||||||
self.id.chars().take(8).collect()
|
Err(Error::EventParseFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this event has a valid signature.
|
// check if this event is valid (should be propagated, stored) based on signature.
|
||||||
fn is_valid(&self) -> bool {
|
pub fn is_valid(&self) -> bool {
|
||||||
// TODO: return a Result with a reason for invalid events
|
false
|
||||||
// don't bother to validate an event with a timestamp in the distant future.
|
|
||||||
let config = config::SETTINGS.read().unwrap();
|
|
||||||
let max_future_sec = config.options.reject_future_seconds;
|
|
||||||
if let Some(allowable_future) = max_future_sec {
|
|
||||||
let curr_time = unix_time();
|
|
||||||
// calculate difference, plus how far future we allow
|
|
||||||
if curr_time + (allowable_future as u64) < self.created_at {
|
|
||||||
let delta = self.created_at - curr_time;
|
|
||||||
debug!(
|
|
||||||
"Event is too far in the future ({} seconds), rejecting",
|
|
||||||
delta
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// validation is performed by:
|
|
||||||
// * parsing JSON string into event fields
|
|
||||||
// * create an array:
|
|
||||||
// ** [0, pubkey-hex-string, created-at-num, kind-num, tags-array-of-arrays, content-string]
|
|
||||||
// * serialize with no spaces/newlines
|
|
||||||
let c_opt = self.to_canonical();
|
|
||||||
if c_opt.is_none() {
|
|
||||||
info!("event could not be canonicalized");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let c = c_opt.unwrap();
|
|
||||||
// * compute the sha256sum.
|
|
||||||
let digest: sha256::Hash = sha256::Hash::hash(c.as_bytes());
|
|
||||||
let hex_digest = format!("{:x}", digest);
|
|
||||||
// * ensure the id matches the computed sha256sum.
|
|
||||||
if self.id != hex_digest {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// * validate the message digest (sig) using the pubkey & computed sha256 message hash.
|
|
||||||
let secp = Secp256k1::new();
|
|
||||||
let sig = schnorrsig::Signature::from_str(&self.sig).unwrap();
|
|
||||||
let message = secp256k1::Message::from(digest);
|
|
||||||
let pubkey = schnorrsig::PublicKey::from_str(&self.pubkey).unwrap();
|
|
||||||
let verify = secp.schnorrsig_verify(&sig, &message, &pubkey);
|
|
||||||
matches!(verify, Ok(()))
|
|
||||||
}
|
}
|
||||||
|
// check if given event is referenced in a tag
|
||||||
/// Convert event to canonical representation for signing.
|
pub fn event_tag_match(&self, event: &str) -> bool {
|
||||||
fn to_canonical(&self) -> Option<String> {
|
|
||||||
// create a JsonValue for each event element
|
|
||||||
let mut c: Vec<Value> = vec![];
|
|
||||||
// id must be set to 0
|
|
||||||
let id = Number::from(0_u64);
|
|
||||||
c.push(serde_json::Value::Number(id));
|
|
||||||
// public key
|
|
||||||
c.push(Value::String(self.pubkey.to_owned()));
|
|
||||||
// creation time
|
|
||||||
let created_at = Number::from(self.created_at);
|
|
||||||
c.push(serde_json::Value::Number(created_at));
|
|
||||||
// kind
|
|
||||||
let kind = Number::from(self.kind);
|
|
||||||
c.push(serde_json::Value::Number(kind));
|
|
||||||
// tags
|
|
||||||
c.push(self.tags_to_canonical());
|
|
||||||
// content
|
|
||||||
c.push(Value::String(self.content.to_owned()));
|
|
||||||
serde_json::to_string(&Value::Array(c)).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert tags to a canonical form for signing.
|
|
||||||
fn tags_to_canonical(&self) -> Value {
|
|
||||||
let mut tags = Vec::<Value>::new();
|
|
||||||
// iterate over self tags,
|
|
||||||
for t in self.tags.iter() {
|
for t in self.tags.iter() {
|
||||||
// each tag is a vec of strings
|
if t.len() == 2 {
|
||||||
let mut a = Vec::<Value>::new();
|
if t.get(0).unwrap() == "#e" {
|
||||||
for v in t.iter() {
|
if t.get(1).unwrap() == event {
|
||||||
a.push(serde_json::Value::String(v.to_owned()));
|
return true;
|
||||||
}
|
}
|
||||||
tags.push(serde_json::Value::Array(a));
|
}
|
||||||
}
|
|
||||||
serde_json::Value::Array(tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a list of event tags.
|
|
||||||
pub fn get_event_tags(&self) -> Vec<&str> {
|
|
||||||
let mut etags = vec![];
|
|
||||||
for t in self.tags.iter() {
|
|
||||||
if t.len() >= 2 && t.get(0).unwrap() == "e" {
|
|
||||||
etags.push(&t.get(1).unwrap()[..]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
etags
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a list of pubkey/petname tags.
|
|
||||||
pub fn get_pubkey_tags(&self) -> Vec<&str> {
|
|
||||||
let mut ptags = vec![];
|
|
||||||
for t in self.tags.iter() {
|
|
||||||
if t.len() >= 2 && t.get(0).unwrap() == "p" {
|
|
||||||
ptags.push(&t.get(1).unwrap()[..]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ptags
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a given event is referenced in an event tag.
|
|
||||||
pub fn event_tag_match(&self, eventid: &str) -> bool {
|
|
||||||
self.get_event_tags().contains(&eventid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a given event is referenced in an event tag.
|
|
||||||
pub fn pubkey_tag_match(&self, pubkey: &str) -> bool {
|
|
||||||
self.get_pubkey_tags().contains(&pubkey)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +59,7 @@ impl Event {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
fn simple_event() -> Event {
|
fn simple_event() -> Event {
|
||||||
Event {
|
super::Event {
|
||||||
id: "0".to_owned(),
|
id: "0".to_owned(),
|
||||||
pubkey: "0".to_owned(),
|
pubkey: "0".to_owned(),
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
@@ -218,21 +86,6 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_event_tag_match() -> Result<()> {
|
|
||||||
let event = simple_event();
|
|
||||||
assert!(!event.event_tag_match("foo"));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_event_tag_match() -> Result<()> {
|
|
||||||
let mut event = simple_event();
|
|
||||||
event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]];
|
|
||||||
assert!(event.event_tag_match("foo"));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn event_tags_serialize() -> Result<()> {
|
fn event_tags_serialize() -> Result<()> {
|
||||||
// serialize an event with tags to JSON string
|
// serialize an event with tags to JSON string
|
||||||
@@ -257,49 +110,17 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn event_deserialize() -> Result<()> {
|
fn event_deserialize() -> Result<()> {
|
||||||
let raw_json = r#"{"id":"1384757da583e6129ce831c3d7afc775a33a090578f888dd0d010328ad047d0c","pubkey":"bbbd9711d357df4f4e498841fd796535c95c8e751fa35355008a911c41265fca","created_at":1612650459,"kind":1,"tags":null,"content":"hello world","sig":"59d0cc47ab566e81f72fe5f430bcfb9b3c688cb0093d1e6daa49201c00d28ecc3651468b7938642869ed98c0f1b262998e49a05a6ed056c0d92b193f4e93bc21"}"#;
|
let raw_json = r#"{"id":"1384757da583e6129ce831c3d7afc775a33a090578f888dd0d010328ad047d0c","pubkey":"bbbd9711d357df4f4e498841fd796535c95c8e751fa35355008a911c41265fca","created_at":1612650459,"kind":1,"tags":null,"content":"hello world","sig":"59d0cc47ab566e81f72fe5f430bcfb9b3c688cb0093d1e6daa49201c00d28ecc3651468b7938642869ed98c0f1b262998e49a05a6ed056c0d92b193f4e93bc21"}"#;
|
||||||
|
// id: 1384757da583e6129ce831c3d7afc775a33a090578f888dd0d010328ad047d0c
|
||||||
|
// pubkey: bbbd9711d357df4f4e498841fd796535c95c8e751fa35355008a911c41265fca",
|
||||||
|
// created_at: 1612650459
|
||||||
|
// kind :1,
|
||||||
|
// tags":null,
|
||||||
|
// "content":"hello world",
|
||||||
|
// "sig":"59d0cc47ab566e81f72fe5f430bcfb9b3c688cb0093d1e6daa49201c00d28ecc3651468b7938642869ed98c0f1b262998e49a05a6ed056c0d92b193f4e93bc21"}]"#;
|
||||||
let e: Event = serde_json::from_str(raw_json)?;
|
let e: Event = serde_json::from_str(raw_json)?;
|
||||||
|
// assert that the kind is 1
|
||||||
assert_eq!(e.kind, 1);
|
assert_eq!(e.kind, 1);
|
||||||
assert_eq!(e.tags.len(), 0);
|
assert_eq!(e.tags.len(), 0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn event_canonical() {
|
|
||||||
let e = Event {
|
|
||||||
id: "999".to_owned(),
|
|
||||||
pubkey: "012345".to_owned(),
|
|
||||||
created_at: 501234,
|
|
||||||
kind: 1,
|
|
||||||
tags: vec![],
|
|
||||||
content: "this is a test".to_owned(),
|
|
||||||
sig: "abcde".to_owned(),
|
|
||||||
};
|
|
||||||
let c = e.to_canonical();
|
|
||||||
let expected = Some(r#"[0,"012345",501234,1,[],"this is a test"]"#.to_owned());
|
|
||||||
assert_eq!(c, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn event_canonical_with_tags() {
|
|
||||||
let e = Event {
|
|
||||||
id: "999".to_owned(),
|
|
||||||
pubkey: "012345".to_owned(),
|
|
||||||
created_at: 501234,
|
|
||||||
kind: 1,
|
|
||||||
tags: vec![
|
|
||||||
vec!["#e".to_owned(), "aoeu".to_owned()],
|
|
||||||
vec![
|
|
||||||
"#p".to_owned(),
|
|
||||||
"aaaa".to_owned(),
|
|
||||||
"ws://example.com".to_owned(),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
content: "this is a test".to_owned(),
|
|
||||||
sig: "abcde".to_owned(),
|
|
||||||
};
|
|
||||||
let c = e.to_canonical();
|
|
||||||
let expected_json = r###"[0,"012345",501234,1,[["#e","aoeu"],["#p","aaaa","ws://example.com"]],"this is a test"]"###;
|
|
||||||
let expected = Some(expected_json.to_owned());
|
|
||||||
assert_eq!(c, expected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -1,8 +1,5 @@
|
|||||||
pub mod close;
|
pub mod close;
|
||||||
pub mod config;
|
|
||||||
pub mod conn;
|
|
||||||
pub mod db;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod protostream;
|
pub mod proto;
|
||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
|
|||||||
+44
-226
@@ -1,248 +1,66 @@
|
|||||||
//! Server process
|
use log::info;
|
||||||
use futures::SinkExt;
|
use nostr_rs_relay::proto::Proto;
|
||||||
use futures::StreamExt;
|
use std::{env, io::Error};
|
||||||
use log::*;
|
|
||||||
use nostr_rs_relay::close::Close;
|
|
||||||
use nostr_rs_relay::config;
|
|
||||||
use nostr_rs_relay::conn;
|
|
||||||
use nostr_rs_relay::db;
|
|
||||||
use nostr_rs_relay::error::{Error, Result};
|
|
||||||
use nostr_rs_relay::event::Event;
|
|
||||||
use nostr_rs_relay::protostream;
|
|
||||||
use nostr_rs_relay::protostream::NostrMessage::*;
|
|
||||||
use nostr_rs_relay::protostream::NostrResponse::*;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio::runtime::Builder;
|
use tokio::runtime::Builder;
|
||||||
use tokio::sync::broadcast;
|
|
||||||
use tokio::sync::broadcast::{Receiver, Sender};
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use tungstenite::protocol::WebSocketConfig;
|
use tungstenite::protocol::WebSocketConfig;
|
||||||
|
|
||||||
/// Start running a Nostr relay server.
|
|
||||||
fn main() -> Result<(), Error> {
|
fn main() -> Result<(), Error> {
|
||||||
// setup logger
|
|
||||||
let _ = env_logger::try_init();
|
let _ = env_logger::try_init();
|
||||||
{
|
let addr = env::args()
|
||||||
let mut settings = config::SETTINGS.write().unwrap();
|
.nth(1)
|
||||||
// replace default settings with those read from config.toml
|
.unwrap_or_else(|| "0.0.0.0:8888".to_string());
|
||||||
let c = config::Settings::new();
|
|
||||||
*settings = c;
|
|
||||||
}
|
|
||||||
let config = config::SETTINGS.read().unwrap();
|
|
||||||
debug!("config: {:?}", config);
|
|
||||||
let addr = format!("{}:{}", config.network.address.trim(), config.network.port);
|
|
||||||
// configure tokio runtime
|
// configure tokio runtime
|
||||||
let rt = Builder::new_multi_thread()
|
let rt = Builder::new_multi_thread()
|
||||||
.enable_all()
|
.worker_threads(2)
|
||||||
|
.enable_io()
|
||||||
.thread_name("tokio-ws")
|
.thread_name("tokio-ws")
|
||||||
|
.on_thread_stop(|| {
|
||||||
|
info!("thread stopping");
|
||||||
|
})
|
||||||
|
.on_thread_start(|| {
|
||||||
|
info!("thread starting");
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// start tokio
|
// start tokio
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let settings = config::SETTINGS.read().unwrap();
|
// Create the event loop and TCP listener we'll accept connections on.
|
||||||
let listener = TcpListener::bind(&addr).await.expect("Failed to bind");
|
let listener = TcpListener::bind(&addr).await.expect("Failed to bind");
|
||||||
info!("listening on: {}", addr);
|
info!("Listening on: {}", addr);
|
||||||
// all client-submitted valid events are broadcast to every
|
while let Ok((stream, _)) = listener.accept().await {
|
||||||
// other client on this channel. This should be large enough
|
tokio::spawn(nostr_server(stream));
|
||||||
// to accomodate slower readers (messages are dropped if
|
|
||||||
// clients can not keep up).
|
|
||||||
let (bcast_tx, _) = broadcast::channel::<Event>(settings.limits.broadcast_buffer);
|
|
||||||
// validated events that need to be persisted are sent to the
|
|
||||||
// database on via this channel.
|
|
||||||
let (event_tx, event_rx) = mpsc::channel::<Event>(settings.limits.event_persist_buffer);
|
|
||||||
// establish a channel for letting all threads now about a
|
|
||||||
// requested server shutdown.
|
|
||||||
let (invoke_shutdown, _) = broadcast::channel::<()>(1);
|
|
||||||
let ctrl_c_shutdown = invoke_shutdown.clone();
|
|
||||||
// listen for ctrl-c interruupts
|
|
||||||
tokio::spawn(async move {
|
|
||||||
tokio::signal::ctrl_c().await.unwrap();
|
|
||||||
info!("shutting down due to SIGINT");
|
|
||||||
ctrl_c_shutdown.send(()).ok();
|
|
||||||
});
|
|
||||||
// start the database writer thread. Give it a channel for
|
|
||||||
// writing events, and for publishing events that have been
|
|
||||||
// written (to all connected clients).
|
|
||||||
db::db_writer(event_rx, bcast_tx.clone(), invoke_shutdown.subscribe()).await;
|
|
||||||
|
|
||||||
// track unique client connection count
|
|
||||||
let mut client_accept_count: usize = 0;
|
|
||||||
let mut stop_listening = invoke_shutdown.subscribe();
|
|
||||||
// handle new client connection requests, or SIGINT signals.
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
_ = stop_listening.recv() => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok((stream, _)) = listener.accept() => {
|
|
||||||
client_accept_count += 1;
|
|
||||||
info!("creating new connection for client #{}",client_accept_count);
|
|
||||||
tokio::spawn(nostr_server(
|
|
||||||
stream,
|
|
||||||
bcast_tx.clone(),
|
|
||||||
event_tx.clone(),
|
|
||||||
invoke_shutdown.subscribe(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle new client connections. This runs through an event loop
|
// Todo: Implement sending messages to all other clients; example:
|
||||||
/// for all client communication.
|
// https://github.com/snapview/tokio-tungstenite/blob/master/examples/server.rs
|
||||||
async fn nostr_server(
|
|
||||||
stream: TcpStream,
|
// Wrap/Upgrade TCP connections in WebSocket Streams, and hand off to Nostr protocol handler.
|
||||||
broadcast: Sender<Event>,
|
async fn nostr_server(stream: TcpStream) {
|
||||||
event_tx: tokio::sync::mpsc::Sender<Event>,
|
let addr = stream
|
||||||
mut shutdown: Receiver<()>,
|
.peer_addr()
|
||||||
) {
|
.expect("connected streams should have a peer address");
|
||||||
// get a broadcast channel for clients to communicate on
|
info!("Peer address: {}", addr);
|
||||||
let mut bcast_rx = broadcast.subscribe();
|
let config = WebSocketConfig {
|
||||||
let mut config = WebSocketConfig::default();
|
max_send_queue: None,
|
||||||
{
|
max_message_size: Some(2 << 16), // 64K
|
||||||
let settings = config::SETTINGS.read().unwrap();
|
max_frame_size: Some(2 << 16), // 64k
|
||||||
config.max_message_size = settings.limits.max_ws_message_bytes;
|
accept_unmasked_frames: false, // follow the spec
|
||||||
config.max_frame_size = settings.limits.max_ws_frame_bytes;
|
};
|
||||||
}
|
|
||||||
// upgrade the TCP connection to WebSocket
|
|
||||||
let conn = tokio_tungstenite::accept_async_with_config(stream, Some(config)).await;
|
let conn = tokio_tungstenite::accept_async_with_config(stream, Some(config)).await;
|
||||||
let ws_stream = conn.expect("websocket handshake error");
|
match conn {
|
||||||
// wrap websocket into a stream & sink of Nostr protocol messages
|
Ok(ws_stream) => {
|
||||||
let mut nostr_stream = protostream::wrap_ws_in_nostr(ws_stream);
|
info!("New WebSocket connection: {}", addr);
|
||||||
// Track internal client state
|
// create a nostr protocol handler, and give it the stream
|
||||||
let mut conn = conn::ClientConn::new();
|
let mut proto = Proto::new(ws_stream);
|
||||||
let cid = conn.get_client_prefix();
|
proto.process_client().await
|
||||||
// Create a channel for receiving query results from the database.
|
|
||||||
// we will send out the tx handle to any query we generate.
|
|
||||||
let (query_tx, mut query_rx) = mpsc::channel::<db::QueryResult>(256);
|
|
||||||
// maintain a hashmap of a oneshot channel for active subscriptions.
|
|
||||||
// when these subscriptions are cancelled, make a message
|
|
||||||
// available to the executing query so it knows to stop.
|
|
||||||
//let (abandon_query_tx, _) = oneshot::channel::<()>();
|
|
||||||
let mut running_queries: HashMap<String, oneshot::Sender<()>> = HashMap::new();
|
|
||||||
// for stats, keep track of how many events the client published,
|
|
||||||
// and how many it received from queries.
|
|
||||||
let mut client_published_event_count: usize = 0;
|
|
||||||
let mut client_received_event_count: usize = 0;
|
|
||||||
info!("new connection for client: {}", cid);
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
_ = shutdown.recv() => {
|
|
||||||
// server shutting down, exit loop
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
Some(query_result) = query_rx.recv() => {
|
|
||||||
// database informed us of a query result we asked for
|
|
||||||
let res = EventRes(query_result.sub_id,query_result.event);
|
|
||||||
client_received_event_count += 1;
|
|
||||||
nostr_stream.send(res).await.ok();
|
|
||||||
},
|
|
||||||
Ok(global_event) = bcast_rx.recv() => {
|
|
||||||
// an event has been broadcast to all clients
|
|
||||||
// first check if there is a subscription for this event.
|
|
||||||
let matching_subs = conn.get_matching_subscriptions(&global_event);
|
|
||||||
for s in matching_subs {
|
|
||||||
// TODO: serialize at broadcast time, instead of
|
|
||||||
// once for each consumer.
|
|
||||||
if let Ok(event_str) = serde_json::to_string(&global_event) {
|
|
||||||
debug!("sub match: client: {}, sub: {}, event: {}",
|
|
||||||
cid, s,
|
|
||||||
global_event.get_event_id_prefix());
|
|
||||||
// create an event response and send it
|
|
||||||
let res = EventRes(s.to_owned(),event_str);
|
|
||||||
nostr_stream.send(res).await.ok();
|
|
||||||
} else {
|
|
||||||
warn!("could not convert event to string");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// check if this client has a subscription
|
|
||||||
proto_next = nostr_stream.next() => {
|
|
||||||
match proto_next {
|
|
||||||
Some(Ok(EventMsg(ec))) => {
|
|
||||||
// An EventCmd needs to be validated to be converted into an Event
|
|
||||||
// handle each type of message
|
|
||||||
let parsed : Result<Event> = Result::<Event>::from(ec);
|
|
||||||
match parsed {
|
|
||||||
Ok(e) => {
|
|
||||||
let id_prefix:String = e.id.chars().take(8).collect();
|
|
||||||
debug!("successfully parsed/validated event: {} from client: {}", id_prefix, cid);
|
|
||||||
// Write this to the database
|
|
||||||
event_tx.send(e.clone()).await.ok();
|
|
||||||
client_published_event_count += 1;
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
info!("client {} sent an invalid event", cid);
|
|
||||||
nostr_stream.send(NoticeRes("event was invalid".to_owned())).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(Ok(SubMsg(s))) => {
|
|
||||||
debug!("client {} requesting a subscription", cid);
|
|
||||||
// subscription handling consists of:
|
|
||||||
// * registering the subscription so future events can be matched
|
|
||||||
// * making a channel to cancel to request later
|
|
||||||
// * sending a request for a SQL query
|
|
||||||
let (abandon_query_tx, abandon_query_rx) = oneshot::channel::<()>();
|
|
||||||
match conn.subscribe(s.clone()) {
|
|
||||||
Ok(()) => {
|
|
||||||
running_queries.insert(s.id.to_owned(), abandon_query_tx);
|
|
||||||
// start a database query
|
|
||||||
db::db_query(s, query_tx.clone(), abandon_query_rx).await;
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
info!("Subscription error: {}", e);
|
|
||||||
nostr_stream.send(NoticeRes(format!("{}",e))).await.ok();
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(Ok(CloseMsg(cc))) => {
|
|
||||||
// closing a request simply removes the subscription.
|
|
||||||
let parsed : Result<Close> = Result::<Close>::from(cc);
|
|
||||||
match parsed {
|
|
||||||
Ok(c) => {
|
|
||||||
// check if a query is currently
|
|
||||||
// running, and remove it if so.
|
|
||||||
let stop_tx = running_queries.remove(&c.id);
|
|
||||||
if let Some(tx) = stop_tx {
|
|
||||||
tx.send(()).ok();
|
|
||||||
}
|
|
||||||
// stop checking new events against
|
|
||||||
// the subscription
|
|
||||||
conn.unsubscribe(c);
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
info!("invalid command ignored");
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
debug!("normal websocket close from client: {}",cid);
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
Some(Err(Error::ConnError)) => {
|
|
||||||
debug!("got connection close/error, disconnecting client: {}",cid);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Some(Err(e)) => {
|
|
||||||
info!("got non-fatal error from client: {}, error: {:?}", cid, e);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
Err(_) => {
|
||||||
// connection cleanup - ensure any still running queries are terminated.
|
println!("Error");
|
||||||
for (_, stop_tx) in running_queries.into_iter() {
|
info!("Error during websocket handshake");
|
||||||
stop_tx.send(()).ok();
|
}
|
||||||
}
|
};
|
||||||
info!(
|
|
||||||
"stopping connection for client: {} (client sent {} event(s), received {})",
|
|
||||||
cid, client_published_event_count, client_received_event_count
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
use crate::close::Close;
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::event::Event;
|
||||||
|
use crate::subscription::Subscription;
|
||||||
|
use futures::SinkExt;
|
||||||
|
use tungstenite::error::Error::*;
|
||||||
|
use tungstenite::protocol::frame::coding::CloseCode;
|
||||||
|
use tungstenite::protocol::frame::CloseFrame;
|
||||||
|
use tungstenite::protocol::Message;
|
||||||
|
|
||||||
|
use futures::StreamExt;
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio_tungstenite::WebSocketStream;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// A protocol handler/helper. Use one per client.
|
||||||
|
pub struct Proto {
|
||||||
|
client_id: Uuid,
|
||||||
|
// current set of subscriptions
|
||||||
|
subscriptions: HashMap<String, Subscription>,
|
||||||
|
// websocket
|
||||||
|
stream: WebSocketStream<TcpStream>,
|
||||||
|
max_subs: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_SUBSCRIPTION_ID_LEN: usize = 256;
|
||||||
|
|
||||||
|
impl Proto {
|
||||||
|
pub fn new(stream: WebSocketStream<TcpStream>) -> Self {
|
||||||
|
let p = Proto {
|
||||||
|
client_id: Uuid::new_v4(),
|
||||||
|
subscriptions: HashMap::new(),
|
||||||
|
max_subs: 128,
|
||||||
|
stream: stream,
|
||||||
|
};
|
||||||
|
debug!("New client: {:?}", p.client_id);
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_client(&mut self) {
|
||||||
|
while let Some(mes_res) = self.stream.next().await {
|
||||||
|
match mes_res {
|
||||||
|
Ok(Message::Text(cmd)) => {
|
||||||
|
info!("Message received");
|
||||||
|
debug!("Message: {}", cmd);
|
||||||
|
let proto_error = self.process_message(cmd);
|
||||||
|
match proto_error {
|
||||||
|
Err(_) => {
|
||||||
|
self.send_notice("failed to process message.").await;
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Message processed successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Everything else below is basically websocket error handling.
|
||||||
|
Ok(Message::Binary(_)) => {
|
||||||
|
info!("Ignoring Binary message");
|
||||||
|
self.send_notice("binary messages not supported.").await;
|
||||||
|
}
|
||||||
|
Ok(Message::Ping(_)) | Ok(Message::Pong(_)) => debug!("Ping/Pong"),
|
||||||
|
Ok(Message::Close(_)) => {
|
||||||
|
info!("Got request to close connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(tungstenite::error::Error::Capacity(
|
||||||
|
tungstenite::error::CapacityError::MessageTooLong { size, max_size },
|
||||||
|
)) => {
|
||||||
|
info!(
|
||||||
|
"Message size too large, disconnecting this client. ({} > {})",
|
||||||
|
size, max_size
|
||||||
|
);
|
||||||
|
self.send_notice("binary messages not supported.").await;
|
||||||
|
self.stream
|
||||||
|
.close(Some(CloseFrame {
|
||||||
|
code: CloseCode::Size,
|
||||||
|
reason: "Exceeded max message size".into(),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.expect("failed to send close frame");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(AlreadyClosed) => {
|
||||||
|
warn!("this connection was already closed, and we tried to operate on it");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(ConnectionClosed) | Err(Io(_)) => {
|
||||||
|
debug!("Closing this connection normally");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(Tls(_)) | Err(Protocol(_)) | Err(Utf8) | Err(Url(_)) | Err(HttpFormat(_))
|
||||||
|
| Err(Http(_)) => {
|
||||||
|
info!("websocket/tls/enc protocol error, dropping connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Some new kind of error, bailing: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sending notice to client. never fails.
|
||||||
|
pub async fn send_notice(&mut self, msg: &str) {
|
||||||
|
// TODO: real escaping
|
||||||
|
let s = msg.replace("\"", "");
|
||||||
|
self.stream
|
||||||
|
.send(Message::Text(format!("[\"NOTICE\",\"{}\"]", s)))
|
||||||
|
.await
|
||||||
|
.err();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error results will be transformed into client NOTICEs
|
||||||
|
pub fn process_message(&mut self, cmd: String) -> Result<()> {
|
||||||
|
info!(
|
||||||
|
"Processing message in proto for client: {:?}",
|
||||||
|
self.client_id
|
||||||
|
);
|
||||||
|
let message = parse_cmd(cmd)?;
|
||||||
|
info!("Parsed message: {:?}", message);
|
||||||
|
match message {
|
||||||
|
NostrRequest::EvReq(_) => {}
|
||||||
|
NostrRequest::SubReq(sub) => self.subscribe(sub),
|
||||||
|
NostrRequest::CloseReq(close) => self.unsubscribe(close),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&mut self, s: Subscription) {
|
||||||
|
// TODO: add NOTICE responses for error conditions. At the
|
||||||
|
// moment, we are silently dropping subscription requests that
|
||||||
|
// aren't perfect.
|
||||||
|
|
||||||
|
// check if the subscription key is reasonable.
|
||||||
|
let k = s.get_id();
|
||||||
|
let sub_id_len = k.len();
|
||||||
|
if sub_id_len > MAX_SUBSCRIPTION_ID_LEN {
|
||||||
|
info!("Dropping subscription with huge ({}) length", sub_id_len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// check if an existing subscription exists, and replace if so
|
||||||
|
if self.subscriptions.contains_key(&k) {
|
||||||
|
self.subscriptions.remove(&k);
|
||||||
|
self.subscriptions.insert(k, s);
|
||||||
|
info!("Replaced existing subscription");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there is room for another subscription.
|
||||||
|
if self.subscriptions.len() >= self.max_subs {
|
||||||
|
info!("Client has reached the maximum number of unique subscriptions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// add subscription
|
||||||
|
self.subscriptions.insert(k, s);
|
||||||
|
info!(
|
||||||
|
"Registered new subscription, currently have {} active subs",
|
||||||
|
self.subscriptions.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsubscribe(&mut self, c: Close) {
|
||||||
|
// TODO: return notice if subscription did not exist.
|
||||||
|
self.subscriptions.remove(&c.get_id());
|
||||||
|
info!(
|
||||||
|
"Removed subscription, currently have {} active subs",
|
||||||
|
self.subscriptions.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A raw message with the expected type
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub enum NostrRawMessage {
|
||||||
|
EvRaw(String),
|
||||||
|
SubRaw(String),
|
||||||
|
CloseRaw(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fully parsed request
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub enum NostrRequest {
|
||||||
|
EvReq(Event),
|
||||||
|
SubReq(Subscription),
|
||||||
|
CloseReq(Close),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the message in the expected request type
|
||||||
|
fn msg_type_wrapper(msg: String) -> Result<NostrRawMessage> {
|
||||||
|
// check prefix.
|
||||||
|
if msg.starts_with(r#"["EVENT","#) {
|
||||||
|
Ok(NostrRawMessage::EvRaw(msg))
|
||||||
|
} else if msg.starts_with(r#"["REQ","#) {
|
||||||
|
Ok(NostrRawMessage::SubRaw(msg))
|
||||||
|
} else if msg.starts_with(r#"["CLOSE","#) {
|
||||||
|
Ok(NostrRawMessage::CloseRaw(msg))
|
||||||
|
} else {
|
||||||
|
Err(Error::CommandNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_cmd(msg: String) -> Result<NostrRequest> {
|
||||||
|
// turn this raw string into a parsed request
|
||||||
|
let typ = msg_type_wrapper(msg)?;
|
||||||
|
match typ {
|
||||||
|
NostrRawMessage::EvRaw(_) => Err(Error::EventParseFailed),
|
||||||
|
NostrRawMessage::SubRaw(m) => Ok(NostrRequest::SubReq(Subscription::parse(&m)?)),
|
||||||
|
NostrRawMessage::CloseRaw(m) => Ok(NostrRequest::CloseReq(Close::parse(&m)?)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the request into a fully deserialized type
|
||||||
|
|
||||||
|
// The protocol-handling process looks something like:
|
||||||
|
// Receive a message (bytes).
|
||||||
|
// Determine the type. We could do this with an untagged deserialization in serde. Or we can peek at the prefix.
|
||||||
|
// Wrap the message string in the client request type (either Event, Req, Close)
|
||||||
|
// For Req/Close, we can fully parse these.
|
||||||
|
|
||||||
|
// For Event, we want to be more cautious.
|
||||||
|
// Before we admit an event, we should reject any duplicates.
|
||||||
|
// duplicates in the datastore will have already been sent out to interested subscribers.
|
||||||
|
// No point in verifying an event that we already have.
|
||||||
|
|
||||||
|
// Event pipeline looks like:
|
||||||
|
// Get message. (./)
|
||||||
|
// Verify it is an event. (./)
|
||||||
|
// Parse into string / number components from JSON.
|
||||||
|
// Perform validation, re-serialize (or can we re-use the original?)
|
||||||
|
// Publish to subscribers.
|
||||||
|
// Push to datastore.
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
//! Nostr protocol layered over WebSocket
|
|
||||||
use crate::close::CloseCmd;
|
|
||||||
use crate::error::{Error, Result};
|
|
||||||
use crate::event::EventCmd;
|
|
||||||
use crate::subscription::Subscription;
|
|
||||||
use core::pin::Pin;
|
|
||||||
use futures::sink::Sink;
|
|
||||||
use futures::stream::Stream;
|
|
||||||
use futures::task::Context;
|
|
||||||
use futures::task::Poll;
|
|
||||||
use log::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio_tungstenite::WebSocketStream;
|
|
||||||
use tungstenite::error::Error as WsError;
|
|
||||||
use tungstenite::protocol::Message;
|
|
||||||
|
|
||||||
/// Nostr protocol messages from a client
|
|
||||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Debug)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum NostrMessage {
|
|
||||||
/// An `EVENT` message
|
|
||||||
EventMsg(EventCmd),
|
|
||||||
/// A `REQ` message
|
|
||||||
SubMsg(Subscription),
|
|
||||||
/// A `CLOSE` message
|
|
||||||
CloseMsg(CloseCmd),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Nostr protocol messages from a relay/server
|
|
||||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Debug)]
|
|
||||||
pub enum NostrResponse {
|
|
||||||
/// A `NOTICE` response
|
|
||||||
NoticeRes(String),
|
|
||||||
/// An `EVENT` response, composed of the subscription identifier,
|
|
||||||
/// and serialized event JSON
|
|
||||||
EventRes(String, String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Nostr protocol stream is layered on top of a Websocket stream.
|
|
||||||
pub struct NostrStream {
|
|
||||||
ws_stream: WebSocketStream<TcpStream>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given a websocket, return a protocol stream wrapper.
|
|
||||||
pub fn wrap_ws_in_nostr(ws: WebSocketStream<TcpStream>) -> NostrStream {
|
|
||||||
NostrStream { ws_stream: ws }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implement the [`Stream`] interface to produce Nostr messages.
|
|
||||||
impl Stream for NostrStream {
|
|
||||||
type Item = Result<NostrMessage>;
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
|
||||||
/// Convert Message to NostrMessage
|
|
||||||
fn convert(msg: String) -> Result<NostrMessage> {
|
|
||||||
debug!("raw msg: {}", msg);
|
|
||||||
let event_size = msg.len();
|
|
||||||
debug!("event size is {} bytes", event_size);
|
|
||||||
let parsed_res: Result<NostrMessage> = serde_json::from_str(&msg).map_err(|e| e.into());
|
|
||||||
match parsed_res {
|
|
||||||
Ok(m) => Ok(m),
|
|
||||||
Err(e) => {
|
|
||||||
debug!("proto parse error: {:?}", e);
|
|
||||||
Err(Error::ProtoParseError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match Pin::new(&mut self.ws_stream).poll_next(cx) {
|
|
||||||
Poll::Pending => Poll::Pending,
|
|
||||||
Poll::Ready(None) => Poll::Ready(None),
|
|
||||||
Poll::Ready(Some(v)) => match v {
|
|
||||||
Ok(Message::Text(vs)) => Poll::Ready(Some(convert(vs))),
|
|
||||||
Ok(Message::Binary(_)) => Poll::Ready(Some(Err(Error::ProtoParseError))),
|
|
||||||
Ok(Message::Pong(_)) | Ok(Message::Ping(_)) => Poll::Pending,
|
|
||||||
Ok(Message::Close(_)) => Poll::Ready(None),
|
|
||||||
Err(WsError::AlreadyClosed) | Err(WsError::ConnectionClosed) => Poll::Ready(None),
|
|
||||||
Err(_) => Poll::Ready(Some(Err(Error::ConnError))),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implement the [`Sink`] interface to produce Nostr responses.
|
|
||||||
impl Sink<NostrResponse> for NostrStream {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
||||||
// map the error type
|
|
||||||
match Pin::new(&mut self.ws_stream).poll_ready(cx) {
|
|
||||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
|
|
||||||
Poll::Ready(Err(_)) => Poll::Ready(Err(Error::ConnWriteError)),
|
|
||||||
Poll::Pending => Poll::Pending,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_send(mut self: Pin<&mut Self>, item: NostrResponse) -> Result<(), Self::Error> {
|
|
||||||
// TODO: do real escaping for these - at least on NOTICE,
|
|
||||||
// which surely has some problems if arbitrary text is sent.
|
|
||||||
let send_str = match item {
|
|
||||||
NostrResponse::NoticeRes(msg) => {
|
|
||||||
let s = msg.replace("\"", "");
|
|
||||||
format!("[\"NOTICE\",\"{}\"]", s)
|
|
||||||
}
|
|
||||||
NostrResponse::EventRes(sub, eventstr) => {
|
|
||||||
let subesc = sub.replace("\"", "");
|
|
||||||
format!("[\"EVENT\",\"{}\",{}]", subesc, eventstr)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match Pin::new(&mut self.ws_stream).start_send(Message::Text(send_str)) {
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(_) => Err(Error::ConnWriteError),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+103
-75
@@ -1,43 +1,37 @@
|
|||||||
//! Subscription and filter parsing
|
use crate::error::{Error, Result};
|
||||||
use crate::error::Result;
|
|
||||||
use crate::event::Event;
|
use crate::event::Event;
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
//use serde_json::json;
|
||||||
|
//use serde_json::Result;
|
||||||
|
|
||||||
/// Subscription identifier and set of request filters
|
// Container for a request filter
|
||||||
#[derive(Serialize, PartialEq, Debug, Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct ReqCmd {
|
||||||
|
cmds: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
pub struct Subscription {
|
pub struct Subscription {
|
||||||
pub id: String,
|
id: String,
|
||||||
pub filters: Vec<ReqFilter>,
|
filters: Vec<ReqFilter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter for requests
|
|
||||||
///
|
|
||||||
/// Corresponds to client-provided subscription request elements. Any
|
|
||||||
/// element can be present if it should be used in filtering, or
|
|
||||||
/// absent ([`None`]) if it should be ignored.
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct ReqFilter {
|
pub struct ReqFilter {
|
||||||
/// Event hash
|
id: Option<String>,
|
||||||
pub id: Option<String>,
|
author: Option<String>,
|
||||||
/// Event kind
|
kind: Option<u8>,
|
||||||
pub kind: Option<u64>,
|
#[serde(rename = "e#")]
|
||||||
/// Referenced event hash
|
event: Option<String>,
|
||||||
#[serde(rename = "#e")]
|
#[serde(rename = "p#")]
|
||||||
pub event: Option<String>,
|
pubkey: Option<String>,
|
||||||
/// Referenced public key for a petname
|
since: Option<u64>,
|
||||||
#[serde(rename = "#p")]
|
authors: Option<Vec<String>>,
|
||||||
pub pubkey: Option<String>,
|
|
||||||
/// Events published after this time
|
|
||||||
pub since: Option<u64>,
|
|
||||||
/// Events published before this time
|
|
||||||
pub until: Option<u64>,
|
|
||||||
/// List of author public keys
|
|
||||||
pub authors: Option<Vec<String>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Subscription {
|
impl<'de> Deserialize<'de> for Subscription {
|
||||||
/// Custom deserializer for subscriptions, which have a more
|
|
||||||
/// complex structure than the other message types.
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Subscription, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Subscription, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
@@ -51,18 +45,18 @@ impl<'de> Deserialize<'de> for Subscription {
|
|||||||
// check for array
|
// check for array
|
||||||
let va = v
|
let va = v
|
||||||
.as_array_mut()
|
.as_array_mut()
|
||||||
.ok_or_else(|| serde::de::Error::custom("not array"))?;
|
.ok_or(serde::de::Error::custom("not array"))?;
|
||||||
|
|
||||||
// check length
|
// check length
|
||||||
if va.len() < 3 {
|
if va.len() < 3 {
|
||||||
return Err(serde::de::Error::custom("not enough fields"));
|
return Err(serde::de::Error::custom("not enough fields"));
|
||||||
}
|
}
|
||||||
let mut i = va.iter_mut();
|
let mut i = va.into_iter();
|
||||||
// get command ("REQ") and ensure it is a string
|
// get command ("REQ") and ensure it is a string
|
||||||
let req_cmd_str: serde_json::Value = i.next().unwrap().take();
|
let req_cmd_str: serde_json::Value = i.next().unwrap().take();
|
||||||
let req = req_cmd_str
|
let req = req_cmd_str.as_str().ok_or(serde::de::Error::custom(
|
||||||
.as_str()
|
"first element of request was not a string",
|
||||||
.ok_or_else(|| serde::de::Error::custom("first element of request was not a string"))?;
|
))?;
|
||||||
if req != "REQ" {
|
if req != "REQ" {
|
||||||
return Err(serde::de::Error::custom("missing REQ command"));
|
return Err(serde::de::Error::custom("missing REQ command"));
|
||||||
}
|
}
|
||||||
@@ -71,7 +65,7 @@ impl<'de> Deserialize<'de> for Subscription {
|
|||||||
let sub_id_str: serde_json::Value = i.next().unwrap().take();
|
let sub_id_str: serde_json::Value = i.next().unwrap().take();
|
||||||
let sub_id = sub_id_str
|
let sub_id = sub_id_str
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| serde::de::Error::custom("missing subscription id"))?;
|
.ok_or(serde::de::Error::custom("missing subscription id"))?;
|
||||||
|
|
||||||
let mut filters = vec![];
|
let mut filters = vec![];
|
||||||
for fv in i {
|
for fv in i {
|
||||||
@@ -87,62 +81,70 @@ impl<'de> Deserialize<'de> for Subscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Subscription {
|
impl Subscription {
|
||||||
/// Get a copy of the subscription identifier.
|
pub fn parse(json: &str) -> Result<Subscription> {
|
||||||
|
serde_json::from_str(json).map_err(|e| Error::JsonParseFailed(e))
|
||||||
|
}
|
||||||
pub fn get_id(&self) -> String {
|
pub fn get_id(&self) -> String {
|
||||||
self.id.clone()
|
self.id.clone()
|
||||||
}
|
}
|
||||||
/// Determine if this subscription matches a given [`Event`]. Any
|
pub fn get_filter_count(&self) -> usize {
|
||||||
/// individual filter match is sufficient.
|
self.filters.len()
|
||||||
|
}
|
||||||
pub fn interested_in_event(&self, event: &Event) -> bool {
|
pub fn interested_in_event(&self, event: &Event) -> bool {
|
||||||
|
// loop through every filter, and return true if any match this event.
|
||||||
for f in self.filters.iter() {
|
for f in self.filters.iter() {
|
||||||
if f.interested_in_event(event) {
|
if f.interested_in_event(event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReqFilter {
|
impl ReqFilter {
|
||||||
/// Check for a match within the authors list.
|
fn authors_match(&self, author: &str) -> bool {
|
||||||
// TODO: Ambiguity; what if the array is empty? Should we
|
if self
|
||||||
// consider that the same as null?
|
.authors
|
||||||
fn authors_match(&self, event: &Event) -> bool {
|
|
||||||
self.authors
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|vs| vs.contains(&event.pubkey.to_owned()))
|
.map(|vs| vs.contains(&author.to_owned()))
|
||||||
.unwrap_or(true)
|
|
||||||
}
|
|
||||||
/// Check if this filter either matches, or does not care about the event tags.
|
|
||||||
fn event_match(&self, event: &Event) -> bool {
|
|
||||||
self.event
|
|
||||||
.as_ref()
|
|
||||||
.map(|t| event.event_tag_match(t))
|
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this filter either matches, or does not care about
|
|
||||||
/// the pubkey/petname tags.
|
|
||||||
fn pubkey_match(&self, event: &Event) -> bool {
|
|
||||||
self.pubkey
|
|
||||||
.as_ref()
|
|
||||||
.map(|t| event.pubkey_tag_match(t))
|
|
||||||
.unwrap_or(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this filter either matches, or does not care about the kind.
|
|
||||||
fn kind_match(&self, kind: u64) -> bool {
|
|
||||||
self.kind.map(|v| v == kind).unwrap_or(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determine if all populated fields in this filter match the provided event.
|
|
||||||
pub fn interested_in_event(&self, event: &Event) -> bool {
|
pub fn interested_in_event(&self, event: &Event) -> bool {
|
||||||
self.id.as_ref().map(|v| v == &event.id).unwrap_or(true)
|
// determine if all populated fields in this filter match the provided event.
|
||||||
&& self.since.map(|t| event.created_at > t).unwrap_or(true)
|
// a filter matches an event if all the populated fields match.
|
||||||
&& self.kind_match(event.kind)
|
// Iterate through each filter field, return false if the field exists and doesn't match the event.
|
||||||
&& self.authors_match(event)
|
// order based on cost; id, time, kind, author, event
|
||||||
&& self.pubkey_match(event)
|
if !self.id.as_ref().map(|v| v == &event.id).unwrap_or(true) {
|
||||||
&& self.event_match(event)
|
false
|
||||||
|
} else if !self.since.map(|t| event.created_at > t).unwrap_or(true) {
|
||||||
|
false
|
||||||
|
} else if !self.kind.map(|v| v == event.kind).unwrap_or(true) {
|
||||||
|
false
|
||||||
|
} else if !self
|
||||||
|
.author
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v == &event.pubkey)
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
false
|
||||||
|
} else if !self.authors_match(&event.pubkey) {
|
||||||
|
false
|
||||||
|
} else if !self
|
||||||
|
.event
|
||||||
|
.as_ref()
|
||||||
|
.map(|e| event.event_tag_match(e))
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +158,17 @@ mod tests {
|
|||||||
let s: Subscription = serde_json::from_str(raw_json)?;
|
let s: Subscription = serde_json::from_str(raw_json)?;
|
||||||
assert_eq!(s.id, "some-id");
|
assert_eq!(s.id, "some-id");
|
||||||
assert_eq!(s.filters.len(), 1);
|
assert_eq!(s.filters.len(), 1);
|
||||||
assert_eq!(s.filters.get(0).unwrap().authors, None);
|
assert_eq!(s.filters.get(0).unwrap().author, None);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_empty_request_parse() -> Result<()> {
|
||||||
|
let raw_json = r#"["REQ","some-id",{}]"#;
|
||||||
|
let s: Subscription = serde_json::from_str(raw_json)?;
|
||||||
|
assert_eq!(s.id, "some-id");
|
||||||
|
assert_eq!(s.filters.len(), 1);
|
||||||
|
assert_eq!(s.filters.get(0).unwrap().author, None);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +270,23 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn author_single() -> Result<()> {
|
||||||
|
// subscription with a filter for ID
|
||||||
|
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"author":"abc"}]"#)?;
|
||||||
|
let e = Event {
|
||||||
|
id: "123".to_owned(),
|
||||||
|
pubkey: "abc".to_owned(),
|
||||||
|
created_at: 0,
|
||||||
|
kind: 0,
|
||||||
|
tags: Vec::new(),
|
||||||
|
content: "".to_owned(),
|
||||||
|
sig: "".to_owned(),
|
||||||
|
};
|
||||||
|
assert_eq!(s.interested_in_event(&e), true);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn authors_single() -> Result<()> {
|
fn authors_single() -> Result<()> {
|
||||||
// subscription with a filter for ID
|
// subscription with a filter for ID
|
||||||
@@ -275,7 +304,6 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
|
|
||||||
fn authors_multi_pubkey() -> Result<()> {
|
fn authors_multi_pubkey() -> Result<()> {
|
||||||
// check for any of a set of authors, against the pubkey
|
// check for any of a set of authors, against the pubkey
|
||||||
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors":["abc", "bcd"]}]"#)?;
|
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors":["abc", "bcd"]}]"#)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user