mirror of
https://github.com/scsibug/nostr-rs-relay.git
synced 2025-09-01 03:40:46 -04:00
Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f679fa0893 | ||
|
4cc313fa2d | ||
|
6502f7dcd7 | ||
|
6ca3e3ffea | ||
|
49c668a07c | ||
|
98c6fa6f39 | ||
|
452bbbb0e5 | ||
|
ee0de6f875 | ||
|
699489ebaf | ||
|
af9da65f71 | ||
|
a72eaec3b8 | ||
|
f1206e76f2 | ||
|
af453548ee | ||
|
df251c821c | ||
|
2d28a95ff7 | ||
|
8c93ef5bc2 | ||
|
1c0fc1326d | ||
|
179928378e | ||
|
c605d75bb4 | ||
|
81e4e2b892 | ||
|
6f166433b5 | ||
|
030b64de62 | ||
|
c7eadb1154 | ||
|
62dc77369d | ||
|
24587435ca | ||
|
a3124ccea4 | ||
|
4e51e61d16 | ||
|
5c8390bbe0 | ||
|
da7968efef | ||
|
7037555516 | ||
|
19ed990c57 |
234
Cargo.lock
generated
234
Cargo.lock
generated
@@ -63,7 +63,7 @@ version = "0.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1"
|
||||
dependencies = [
|
||||
"serde 1.0.131",
|
||||
"serde 1.0.136",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -89,9 +89,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.8.0"
|
||||
version = "3.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
|
||||
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@@ -135,7 +135,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"rust-ini",
|
||||
"serde 1.0.131",
|
||||
"serde 1.0.136",
|
||||
"serde-hjson",
|
||||
"serde_json",
|
||||
"toml",
|
||||
@@ -153,9 +153,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
|
||||
checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"lazy_static",
|
||||
@@ -163,13 +163,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.0.0"
|
||||
version = "4.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b799062aaf67eb976af3bdca031ee6f846d2f0a5710ddbb0d2efee33f3cc4760"
|
||||
checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -230,9 +229,9 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e"
|
||||
checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -245,9 +244,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27"
|
||||
checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -255,15 +254,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
|
||||
checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
|
||||
checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -272,15 +271,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11"
|
||||
checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd"
|
||||
checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -289,15 +288,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af"
|
||||
checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
|
||||
checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
@@ -307,9 +306,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
|
||||
checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -325,9 +324,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
||||
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
@@ -335,9 +334,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
||||
checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
@@ -346,9 +345,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3de427a64787873c3b196285e6684cddbf0ae7d1d8d56eaafbb4120c4cb641"
|
||||
checksum = "7df0ee4b237afb71e99f7e2fbd840ffec2d6c4bb569f69b2af18aa1f63077d38"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"futures",
|
||||
@@ -363,9 +362,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.9"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd"
|
||||
checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -415,13 +414,13 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
|
||||
checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
"itoa 1.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -468,7 +467,7 @@ dependencies = [
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"itoa 0.4.8",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
@@ -490,9 +489,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
|
||||
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1",
|
||||
"hashbrown",
|
||||
@@ -514,10 +513,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.55"
|
||||
name = "itoa"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
|
||||
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -543,9 +548,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.111"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e167738f1866a7ec625567bae89ca0d44477232a4f7c52b1c7f2adc2c98804f"
|
||||
checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
@@ -649,7 +654,7 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||
|
||||
[[package]]
|
||||
name = "nostr-rs-relay"
|
||||
version = "0.3.3"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"bitcoin_hashes 0.9.7",
|
||||
"config",
|
||||
@@ -662,9 +667,11 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"nonzero_ext",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rusqlite",
|
||||
"secp256k1",
|
||||
"serde 1.0.131",
|
||||
"serde 1.0.136",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
@@ -702,9 +709,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
|
||||
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
@@ -712,9 +719,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
||||
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@@ -755,9 +762,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.7"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
|
||||
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
@@ -773,15 +780,15 @@ checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.15"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
|
||||
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.33"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a"
|
||||
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
@@ -804,13 +811,34 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.10"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r2d2"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"scheduled-thread-pool",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r2d2_sqlite"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54ca3c9468a76fc2ad724c486a59682fc362efeac7b18d1c012958bc19f34800"
|
||||
dependencies = [
|
||||
"r2d2",
|
||||
"rusqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.6.5"
|
||||
@@ -1024,9 +1052,18 @@ checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.7"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "254df5081ce98661a883445175e52efe99d1cb2a5552891d965d2f5d0cad1c16"
|
||||
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
|
||||
|
||||
[[package]]
|
||||
name = "scheduled-thread-pool"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
@@ -1042,7 +1079,7 @@ dependencies = [
|
||||
"bitcoin_hashes 0.10.0",
|
||||
"rand 0.6.5",
|
||||
"secp256k1-sys",
|
||||
"serde 1.0.131",
|
||||
"serde 1.0.136",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1061,9 +1098,9 @@ checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.131"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1"
|
||||
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -1082,9 +1119,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.131"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2"
|
||||
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1093,14 +1130,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.72"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
|
||||
checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"itoa 1.0.1",
|
||||
"ryu",
|
||||
"serde 1.0.131",
|
||||
"serde 1.0.136",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1133,15 +1170,15 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
||||
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
|
||||
checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
@@ -1155,9 +1192,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.82"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
|
||||
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1210,11 +1247,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
|
||||
checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1",
|
||||
"bytes",
|
||||
"libc",
|
||||
"memchr",
|
||||
@@ -1230,9 +1266,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
|
||||
checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1271,7 +1307,7 @@ version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||
dependencies = [
|
||||
"serde 1.0.131",
|
||||
"serde 1.0.136",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1327,9 +1363,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
|
||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
@@ -1387,9 +1423,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
@@ -1409,9 +1445,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.78"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
|
||||
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
@@ -1419,9 +1455,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.78"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
|
||||
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"lazy_static",
|
||||
@@ -1434,9 +1470,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.78"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
|
||||
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -1444,9 +1480,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.78"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
|
||||
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1457,15 +1493,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.78"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
|
||||
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.55"
|
||||
version = "0.3.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
|
||||
checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nostr-rs-relay"
|
||||
version = "0.3.3"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -19,7 +19,9 @@ secp256k1 = {git = "https://github.com/rust-bitcoin/rust-secp256k1.git", rev = "
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = {version = "^1.0", features = ["preserve_order"]}
|
||||
hex = "^0.4"
|
||||
rusqlite = "^0.26"
|
||||
rusqlite = { version = "^0.26", features = ["limits"]}
|
||||
r2d2 = "^0.8"
|
||||
r2d2_sqlite = "^0.19"
|
||||
lazy_static = "^1.4"
|
||||
governor = "^0.4"
|
||||
nonzero_ext = "^0.3"
|
||||
|
28
README.md
28
README.md
@@ -8,6 +8,20 @@ The project master repository is available on
|
||||
[sourcehut](https://sr.ht/~gheartsfield/nostr-rs-relay/), and is
|
||||
mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
|
||||
|
||||
## Features
|
||||
|
||||
NIPs with a relay-specific implementation are listed here.
|
||||
|
||||
- [x] NIP-01: Core event model
|
||||
- [x] NIP-01: Hide old metadata events
|
||||
- [x] NIP-01: Id/Author prefix search (_experimental_)
|
||||
- [x] NIP-02: Hide old contact list events
|
||||
- [ ] NIP-03: OpenTimestamps
|
||||
- [ ] NIP-05: Mapping Nostr keys to DNS identifiers
|
||||
- [ ] NIP-09: Event deletion
|
||||
- [x] NIP-11: Relay information document
|
||||
- [x] NIP-12: Generic tag search (_experimental_)
|
||||
|
||||
## Quick Start
|
||||
|
||||
The provided `Dockerfile` will compile and build the server
|
||||
@@ -40,11 +54,11 @@ Text Note [81cf...2652] from 296a...9b92 5 seconds ago
|
||||
```
|
||||
|
||||
A pre-built container is also available on DockerHub:
|
||||
https://hub.docker.com/repository/docker/scsibug/nostr-rs-relay
|
||||
https://hub.docker.com/r/scsibug/nostr-rs-relay
|
||||
|
||||
## Configuration
|
||||
|
||||
The sample `[config.toml](config.toml)` file demonstrates the
|
||||
The sample [`config.toml`](config.toml) file demonstrates the
|
||||
configuration available to the relay. This file is optional, but may
|
||||
be mounted into a docker container like so:
|
||||
|
||||
@@ -58,6 +72,16 @@ $ docker run -it -p 7000:8080 \
|
||||
Options include rate-limiting, event size limits, and network address
|
||||
settings.
|
||||
|
||||
## Reverse Proxy Configuration
|
||||
|
||||
For examples of putting the relay behind a reverse proxy (for TLS
|
||||
termination, load balancing, and other features), see [Reverse
|
||||
Proxy](reverse-proxy.md).
|
||||
|
||||
## Dev Channel
|
||||
The current dev discussions for this project is happening at https://discord.gg/ufG6fH52Vk.
|
||||
Drop in to query any development related questions.
|
||||
|
||||
License
|
||||
---
|
||||
This project is MIT licensed.
|
||||
|
33
config.toml
33
config.toml
@@ -13,8 +13,8 @@ description = "A newly created nostr-rs-relay.\n\nCustomize this with your own i
|
||||
# Administrative contact pubkey
|
||||
#pubkey = "0c2d168a4ae8ca58c9f1ab237b5df682599c6c7ab74307ea8b05684b60405d41"
|
||||
|
||||
# Administrative contact email
|
||||
#email = "contact@example.com"
|
||||
# Administrative contact URI
|
||||
#contact = "mailto:contact@example.com"
|
||||
|
||||
[database]
|
||||
# Directory for SQLite files. Defaults to the current directory. Can
|
||||
@@ -22,9 +22,18 @@ description = "A newly created nostr-rs-relay.\n\nCustomize this with your own i
|
||||
# line option.
|
||||
data_directory = "."
|
||||
|
||||
# Database connection pool settings:
|
||||
|
||||
# Minimum number of SQLite reader connections
|
||||
#min_conn = 4
|
||||
|
||||
# Maximum number of SQLite reader connections
|
||||
#max_conn = 128
|
||||
|
||||
[network]
|
||||
# Bind to this network address
|
||||
address = "0.0.0.0"
|
||||
|
||||
# Listen on this port
|
||||
port = 8080
|
||||
|
||||
@@ -37,22 +46,30 @@ 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
|
||||
#messages_per_sec = 0
|
||||
|
||||
# Limit the maximum size of an EVENT message. Defaults to 128 KB.
|
||||
# Set to 0 for unlimited.
|
||||
max_event_bytes = 131072
|
||||
#max_event_bytes = 131072
|
||||
|
||||
# Maximum WebSocket message in bytes. Defaults to 128 KB.
|
||||
max_ws_message_bytes = 131072
|
||||
#max_ws_message_bytes = 131072
|
||||
|
||||
# Maximum WebSocket frame size in bytes. Defaults to 128 KB.
|
||||
max_ws_frame_bytes = 131072
|
||||
#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
|
||||
#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
|
||||
#event_persist_buffer = 16
|
||||
|
||||
[authorization]
|
||||
# Pubkey addresses in this array are whitelisted for event publishing.
|
||||
# Only valid events by these authors will be accepted.
|
||||
#pubkey_whitelist = [
|
||||
# "35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f",
|
||||
# "887645fef0ce0c3c1218d2f5d8e6132a19304cdc57cd20281d082f38cfea0072",
|
||||
#]
|
||||
|
53
reverse-proxy.md
Normal file
53
reverse-proxy.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Reverse Proxy Setup Guide
|
||||
|
||||
It is recommended to run `nostr-rs-relay` behind a reverse proxy such
|
||||
as `haproxy` or `nginx` to provide TLS termination. A simple example
|
||||
of an `haproxy` configuration is documented here.
|
||||
|
||||
## Minimal HAProxy Configuration
|
||||
|
||||
Assumptions:
|
||||
|
||||
* HAProxy version is `2.4.10` or greater (older versions not tested).
|
||||
* Hostname for the relay is `relay.example.com`.
|
||||
* Your relay should be available over wss://relay.example.com
|
||||
* Your (NIP-11) relay info page should be available on https://relay.example.com
|
||||
* SSL certificate is located in `/etc/certs/example.com.pem`.
|
||||
* Relay is running on port 8080.
|
||||
* Limit connections to 400 concurrent.
|
||||
* HSTS (HTTP Strict Transport Security) is desired.
|
||||
* Only TLS 1.2 or greater is allowed.
|
||||
|
||||
```
|
||||
global
|
||||
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
|
||||
|
||||
frontend fe_prod
|
||||
mode http
|
||||
bind :443 ssl crt /etc/certs/example.com.pem alpn h2,http/1.1
|
||||
bind :80
|
||||
http-request set-header X-Forwarded-Proto https if { ssl_fc }
|
||||
redirect scheme https code 301 if !{ ssl_fc }
|
||||
acl host_relay hdr(host) -i relay.example.com
|
||||
use_backend relay if host_relay
|
||||
# HSTS (1 year)
|
||||
http-response set-header Strict-Transport-Security max-age=31536000
|
||||
|
||||
backend relay
|
||||
mode http
|
||||
timeout connect 5s
|
||||
timeout client 50s
|
||||
timeout server 50s
|
||||
timeout tunnel 1h
|
||||
timeout client-fin 30s
|
||||
option tcp-check
|
||||
default-server maxconn 400 check inter 20s fastinter 1s
|
||||
server relay 127.0.0.1:8080
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
You may experience WebSocket connection problems with Firefox if
|
||||
HTTP/2 is enabled, for older versions of HAProxy (2.3.x). Either
|
||||
disable HTTP/2 (`h2`), or upgrade HAProxy.
|
@@ -8,20 +8,22 @@ lazy_static! {
|
||||
pub static ref SETTINGS: RwLock<Settings> = RwLock::new(Settings::default());
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[allow(unused)]
|
||||
pub struct Info {
|
||||
pub relay_url: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub pubkey: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub contact: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[allow(unused)]
|
||||
pub struct Database {
|
||||
pub data_directory: String,
|
||||
pub min_conn: u32,
|
||||
pub max_conn: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -59,6 +61,12 @@ pub struct Limits {
|
||||
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 Authorization {
|
||||
pub pubkey_whitelist: Option<Vec<String>>, // If present, only allow these pubkeys to publish events
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[allow(unused)]
|
||||
pub struct Settings {
|
||||
@@ -66,6 +74,7 @@ pub struct Settings {
|
||||
pub database: Database,
|
||||
pub network: Network,
|
||||
pub limits: Limits,
|
||||
pub authorization: Authorization,
|
||||
pub retention: Retention,
|
||||
pub options: Options,
|
||||
}
|
||||
@@ -93,6 +102,13 @@ impl Settings {
|
||||
// override with file contents
|
||||
.with_merged(config::File::with_name("config"))?
|
||||
.try_into()?;
|
||||
// ensure connection pool size is logical
|
||||
if settings.database.min_conn > settings.database.max_conn {
|
||||
panic!(
|
||||
"Database min_conn setting ({}) cannot exceed max_conn ({})",
|
||||
settings.database.min_conn, settings.database.max_conn
|
||||
);
|
||||
}
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
@@ -105,10 +121,12 @@ impl Default for Settings {
|
||||
name: Some("Unnamed nostr-rs-relay".to_owned()),
|
||||
description: None,
|
||||
pubkey: None,
|
||||
email: None,
|
||||
contact: None,
|
||||
},
|
||||
database: Database {
|
||||
data_directory: ".".to_owned(),
|
||||
min_conn: 4,
|
||||
max_conn: 128,
|
||||
},
|
||||
network: Network {
|
||||
port: 8080,
|
||||
@@ -122,6 +140,9 @@ impl Default for Settings {
|
||||
broadcast_buffer: 4096,
|
||||
event_persist_buffer: 16,
|
||||
},
|
||||
authorization: Authorization {
|
||||
pubkey_whitelist: None, // Allow any address to publish
|
||||
},
|
||||
retention: Retention {
|
||||
max_events: None, // max events
|
||||
max_bytes: None, // max size
|
||||
|
546
src/db.rs
546
src/db.rs
@@ -1,4 +1,5 @@
|
||||
//! Event persistence and querying
|
||||
use crate::config;
|
||||
use crate::error::Result;
|
||||
use crate::event::Event;
|
||||
use crate::subscription::Subscription;
|
||||
@@ -11,11 +12,18 @@ use rusqlite::Connection;
|
||||
use rusqlite::OpenFlags;
|
||||
//use std::num::NonZeroU32;
|
||||
use crate::config::SETTINGS;
|
||||
use r2d2;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::limits::Limit;
|
||||
use rusqlite::types::ToSql;
|
||||
use std::path::Path;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tokio::task;
|
||||
|
||||
pub type SqlitePool = r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>;
|
||||
|
||||
/// Database file
|
||||
const DB_FILE: &str = "nostr.db";
|
||||
|
||||
@@ -34,7 +42,7 @@ PRAGMA journal_mode=WAL;
|
||||
PRAGMA main.synchronous=NORMAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
PRAGMA application_id = 1654008667;
|
||||
PRAGMA user_version = 2;
|
||||
PRAGMA user_version = 3;
|
||||
|
||||
-- Event Table
|
||||
CREATE TABLE IF NOT EXISTS event (
|
||||
@@ -54,6 +62,21 @@ 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);
|
||||
|
||||
-- Tag Table
|
||||
-- Tag values are stored as either a BLOB (if they come in as a
|
||||
-- hex-string), or TEXT otherwise.
|
||||
-- This means that searches need to select the appropriate column.
|
||||
CREATE TABLE IF NOT EXISTS tag (
|
||||
id INTEGER PRIMARY KEY,
|
||||
event_id INTEGER NOT NULL, -- an event ID that contains a tag.
|
||||
name TEXT, -- the tag name ("p", "e", whatever)
|
||||
value TEXT, -- the tag value, if not hex.
|
||||
value_hex BLOB, -- the tag value, if it can be interpreted as a hex string.
|
||||
FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS tag_val_index ON tag(value);
|
||||
CREATE INDEX IF NOT EXISTS tag_val_hex_index ON tag(value_hex);
|
||||
|
||||
-- Event References Table
|
||||
CREATE TABLE IF NOT EXISTS event_ref (
|
||||
id INTEGER PRIMARY KEY,
|
||||
@@ -77,22 +100,66 @@ FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE RESTRICT ON DELETE CASCADE
|
||||
CREATE INDEX IF NOT EXISTS pubkey_ref_index ON pubkey_ref(referenced_pubkey);
|
||||
"##;
|
||||
|
||||
pub fn build_read_pool() -> SqlitePool {
|
||||
let config = config::SETTINGS.read().unwrap();
|
||||
let db_dir = &config.database.data_directory;
|
||||
let full_path = Path::new(db_dir).join(DB_FILE);
|
||||
// small hack; if the database doesn't exist yet, that means the
|
||||
// writer thread hasn't finished. Give it a chance to work. This
|
||||
// is only an issue with the first time we run.
|
||||
while !full_path.exists() {
|
||||
debug!("Database reader pool is waiting on the database to be created...");
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
let manager = SqliteConnectionManager::file(&full_path)
|
||||
.with_flags(OpenFlags::SQLITE_OPEN_READ_ONLY)
|
||||
.with_init(|c| c.execute_batch(STARTUP_SQL));
|
||||
let pool: SqlitePool = r2d2::Pool::builder()
|
||||
.test_on_check_out(true) // no noticeable performance hit
|
||||
.min_idle(Some(config.database.min_conn))
|
||||
.max_size(config.database.max_conn)
|
||||
.build(manager)
|
||||
.unwrap();
|
||||
info!(
|
||||
"Built a connection pool (min={}, max={})",
|
||||
config.database.min_conn, config.database.max_conn
|
||||
);
|
||||
return pool;
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
let mut curr_version = db_version(conn)?;
|
||||
info!("DB version = {:?}", curr_version);
|
||||
|
||||
debug!(
|
||||
"SQLite max query parameters: {}",
|
||||
conn.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER)
|
||||
);
|
||||
debug!(
|
||||
"SQLite max table/blob/text length: {} MB",
|
||||
(conn.limit(Limit::SQLITE_LIMIT_LENGTH) as f64 / (1024 * 1024) as f64).floor()
|
||||
);
|
||||
debug!(
|
||||
"SQLite max SQL length: {} MB",
|
||||
(conn.limit(Limit::SQLITE_LIMIT_SQL_LENGTH) as f64 / (1024 * 1024) as f64).floor()
|
||||
);
|
||||
|
||||
// initialize from scratch
|
||||
if curr_version == 0 {
|
||||
match conn.execute_batch(INIT_SQL) {
|
||||
Ok(()) => info!("database pragma/schema initialized to v2, and ready"),
|
||||
Ok(()) => {
|
||||
info!("database pragma/schema initialized to v3, and ready");
|
||||
//curr_version = 3;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
panic!("database could not be initialized");
|
||||
}
|
||||
}
|
||||
} else if curr_version == 1 {
|
||||
}
|
||||
if curr_version == 1 {
|
||||
// only change is adding a hidden column to events.
|
||||
let upgrade_sql = r##"
|
||||
ALTER TABLE event ADD hidden INTEGER;
|
||||
@@ -100,19 +167,73 @@ UPDATE event SET hidden=FALSE;
|
||||
PRAGMA user_version = 2;
|
||||
"##;
|
||||
match conn.execute_batch(upgrade_sql) {
|
||||
Ok(()) => info!("database schema upgraded v1 -> v2"),
|
||||
Ok(()) => {
|
||||
info!("database schema upgraded v1 -> v2");
|
||||
curr_version = 2;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
} else if curr_version == 2 {
|
||||
}
|
||||
if curr_version == 2 {
|
||||
// this version lacks the tag column
|
||||
debug!("database schema needs update from 2->3");
|
||||
let upgrade_sql = r##"
|
||||
CREATE TABLE IF NOT EXISTS tag (
|
||||
id INTEGER PRIMARY KEY,
|
||||
event_id INTEGER NOT NULL, -- an event ID that contains a tag.
|
||||
name TEXT, -- the tag name ("p", "e", whatever)
|
||||
value TEXT, -- the tag value, if not hex.
|
||||
value_hex BLOB, -- the tag value, if it can be interpreted as a hex string.
|
||||
FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS tag_val_index ON tag(value);
|
||||
CREATE INDEX IF NOT EXISTS tag_val_hex_index ON tag(value_hex);
|
||||
PRAGMA user_version = 3;
|
||||
"##;
|
||||
// TODO: load existing refs into tag table
|
||||
match conn.execute_batch(upgrade_sql) {
|
||||
Ok(()) => {
|
||||
info!("database schema upgraded v2 -> v3");
|
||||
//curr_version = 3;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
info!("Starting transaction");
|
||||
// iterate over every event/pubkey tag
|
||||
let tx = conn.transaction()?;
|
||||
{
|
||||
let mut stmt = tx.prepare("select event_id, \"e\", lower(hex(referenced_event)) from event_ref union select event_id, \"p\", lower(hex(referenced_pubkey)) from pubkey_ref;")?;
|
||||
let mut tag_rows = stmt.query([])?;
|
||||
while let Some(row) = tag_rows.next()? {
|
||||
// we want to capture the event_id that had the tag, the tag name, and the tag hex value.
|
||||
let event_id: u64 = row.get(0)?;
|
||||
let tag_name: String = row.get(1)?;
|
||||
let tag_value: String = row.get(2)?;
|
||||
// this will leave behind p/e tags that were non-hex, but they are invalid anyways.
|
||||
if is_hex(&tag_value) {
|
||||
tx.execute(
|
||||
"INSERT INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3);",
|
||||
params![event_id, tag_name, hex::decode(&tag_value).ok()],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
info!("Upgrade complete");
|
||||
} else if curr_version == 3 {
|
||||
debug!("Database version was already current");
|
||||
} else if curr_version > 2 {
|
||||
} else if curr_version > 3 {
|
||||
panic!("Database version is newer than supported by this executable");
|
||||
}
|
||||
// Setup PRAGMA
|
||||
conn.execute_batch(STARTUP_SQL)?;
|
||||
info!("Finished pragma");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -134,8 +255,13 @@ pub async fn db_writer(
|
||||
)?;
|
||||
info!("opened database {:?} for writing", full_path);
|
||||
upgrade_db(&mut conn)?;
|
||||
|
||||
// Make a copy of the whitelist
|
||||
let whitelist = &config.authorization.pubkey_whitelist.clone();
|
||||
|
||||
// get rate limit settings
|
||||
let rps_setting = config.limits.messages_per_sec;
|
||||
let mut most_recent_rate_limit = Instant::now();
|
||||
let mut lim_opt = None;
|
||||
let clock = governor::clock::QuantaClock::default();
|
||||
if let Some(rps) = rps_setting {
|
||||
@@ -158,6 +284,21 @@ pub async fn db_writer(
|
||||
}
|
||||
let mut event_write = false;
|
||||
let event = next_event.unwrap();
|
||||
|
||||
// check if this event is authorized.
|
||||
if let Some(allowed_addrs) = whitelist {
|
||||
debug!("Checking against whitelist");
|
||||
// if the event address is not in allowed_addrs.
|
||||
if !allowed_addrs.contains(&event.pubkey) {
|
||||
info!(
|
||||
"Rejecting event {}, unauthorized author",
|
||||
event.get_event_id_prefix()
|
||||
);
|
||||
// TODO: define a channel that can send NOTICEs back to the client.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
match write_event(&mut conn, &event) {
|
||||
Ok(updated) => {
|
||||
@@ -182,8 +323,20 @@ pub async fn db_writer(
|
||||
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()));
|
||||
let wait_for = n.wait_time_from(clock.now());
|
||||
// check if we have recently logged rate
|
||||
// limits, but print out a message only once
|
||||
// per second.
|
||||
if most_recent_rate_limit.elapsed().as_secs() > 1 {
|
||||
warn!(
|
||||
"rate limit reached for event creation (sleep for {:?})",
|
||||
wait_for
|
||||
);
|
||||
// reset last rate limit message
|
||||
most_recent_rate_limit = Instant::now();
|
||||
}
|
||||
// block event writes, allowing them to queue up
|
||||
thread::sleep(wait_for);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -221,24 +374,24 @@ pub fn write_event(conn: &mut Connection, e: &Event) -> Result<usize> {
|
||||
}
|
||||
// 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()],
|
||||
)?;
|
||||
// add all tags to the tag table
|
||||
for tag in e.tags.iter() {
|
||||
// ensure we have 2 values.
|
||||
if tag.len() >= 2 {
|
||||
let tagname = &tag[0];
|
||||
let tagval = &tag[1];
|
||||
// if tagvalue is hex;
|
||||
if is_hex(tagval) {
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3)",
|
||||
params![ev_id, &tagname, hex::decode(&tagval).ok()],
|
||||
)?;
|
||||
} else {
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO tag (event_id, name, value) VALUES (?1, ?2, ?3)",
|
||||
params![ev_id, &tagname, &tagval],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// if this event is for a metadata update, hide every other kind=0
|
||||
@@ -281,30 +434,132 @@ 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 {
|
||||
/// Check if a string contains only f chars
|
||||
fn is_all_fs(s: &str) -> bool {
|
||||
s.chars().all(|x| x == 'f' || x == 'F')
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum HexSearch {
|
||||
// when no range is needed, exact 32-byte
|
||||
Exact(Vec<u8>),
|
||||
// lower (inclusive) and upper range (exclusive)
|
||||
Range(Vec<u8>, Vec<u8>),
|
||||
// lower bound only, upper bound is MAX inclusive
|
||||
LowerOnly(Vec<u8>),
|
||||
}
|
||||
|
||||
/// Find the next hex sequence greater than the argument.
|
||||
fn hex_range(s: &str) -> Option<HexSearch> {
|
||||
// handle special cases
|
||||
if !is_hex(s) || s.len() > 64 {
|
||||
return None;
|
||||
}
|
||||
if s.len() == 64 {
|
||||
return Some(HexSearch::Exact(hex::decode(s).ok()?));
|
||||
}
|
||||
// if s is odd, add a zero
|
||||
let mut hash_base = s.to_owned();
|
||||
let mut odd = hash_base.len() % 2 != 0;
|
||||
if odd {
|
||||
// extend the string to make it even
|
||||
hash_base.push('0');
|
||||
}
|
||||
let base = hex::decode(hash_base).ok()?;
|
||||
// check for all ff's
|
||||
if is_all_fs(s) {
|
||||
// there is no higher bound, we only want to search for blobs greater than this.
|
||||
return Some(HexSearch::LowerOnly(base));
|
||||
}
|
||||
|
||||
// return a range
|
||||
let mut upper = base.clone();
|
||||
let mut byte_len = upper.len();
|
||||
|
||||
// for odd strings, we made them longer, but we want to increment the upper char (+16).
|
||||
// we know we can do this without overflowing because we explicitly set the bottom half to 0's.
|
||||
while byte_len > 0 {
|
||||
byte_len -= 1;
|
||||
// check if byte can be incremented, or if we need to carry.
|
||||
let b = upper[byte_len];
|
||||
if b == u8::MAX {
|
||||
// reset and carry
|
||||
upper[byte_len] = 0;
|
||||
} else if odd {
|
||||
// check if first char in this byte is NOT 'f'
|
||||
if b < 240 {
|
||||
upper[byte_len] = b + 16; // bump up the first character in this byte
|
||||
// increment done, stop iterating through the vec
|
||||
break;
|
||||
} else {
|
||||
// if it is 'f', reset the byte to 0 and do a carry
|
||||
// reset and carry
|
||||
upper[byte_len] = 0;
|
||||
}
|
||||
// done with odd logic, so don't repeat this
|
||||
odd = false;
|
||||
} else {
|
||||
// bump up the first character in this byte
|
||||
upper[byte_len] = b + 1;
|
||||
// increment done, stop iterating
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(HexSearch::Range(base, upper))
|
||||
}
|
||||
|
||||
fn repeat_vars(count: usize) -> String {
|
||||
if count == 0 {
|
||||
return "".to_owned();
|
||||
}
|
||||
let mut s = "?,".repeat(count);
|
||||
// Remove trailing comma
|
||||
s.pop();
|
||||
s
|
||||
}
|
||||
|
||||
/// Create a dynamic SQL query string and params from a subscription.
|
||||
fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) {
|
||||
// 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.
|
||||
// hexadecimal characters. Strings that require escaping (tag
|
||||
// names/values) use parameters.
|
||||
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();
|
||||
"SELECT DISTINCT(e.content) FROM event e LEFT JOIN tag t ON e.id=t.event_id ".to_owned();
|
||||
// parameters
|
||||
let mut params: Vec<Box<dyn ToSql>> = vec![];
|
||||
|
||||
// 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(", "));
|
||||
// Query for "authors", allowing prefix matches
|
||||
if let Some(authvec) = &f.authors {
|
||||
// take each author and convert to a hexsearch
|
||||
let mut auth_searches: Vec<String> = vec![];
|
||||
for auth in authvec {
|
||||
match hex_range(auth) {
|
||||
Some(HexSearch::Exact(ex)) => {
|
||||
auth_searches.push("author=?".to_owned());
|
||||
params.push(Box::new(ex));
|
||||
}
|
||||
Some(HexSearch::Range(lower, upper)) => {
|
||||
auth_searches.push("(author>? AND author<?)".to_owned());
|
||||
params.push(Box::new(lower));
|
||||
params.push(Box::new(upper));
|
||||
}
|
||||
Some(HexSearch::LowerOnly(lower)) => {
|
||||
// info!("{:?} => lower; {:?} ", auth, hex::encode(lower));
|
||||
auth_searches.push("author>?".to_owned());
|
||||
params.push(Box::new(lower));
|
||||
}
|
||||
None => {
|
||||
info!("Could not parse hex range from author {:?}", auth);
|
||||
}
|
||||
}
|
||||
}
|
||||
let authors_clause = format!("({})", auth_searches.join(" OR "));
|
||||
filter_components.push(authors_clause);
|
||||
}
|
||||
// Query for Kind
|
||||
@@ -314,46 +569,60 @@ fn query_from_sub(sub: &Subscription) -> String {
|
||||
let kind_clause = format!("kind IN ({})", str_kinds.join(", "));
|
||||
filter_components.push(kind_clause);
|
||||
}
|
||||
// Query for event
|
||||
if f.ids.is_some() {
|
||||
let ids_escaped: Vec<String> = f
|
||||
.ids
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|&x| is_hex(x))
|
||||
.map(|x| format!("x'{}'", x))
|
||||
.collect();
|
||||
let id_clause = format!("event_hash IN ({})", ids_escaped.join(", "));
|
||||
// Query for event, allowing prefix matches
|
||||
if let Some(idvec) = &f.ids {
|
||||
// take each author and convert to a hexsearch
|
||||
let mut id_searches: Vec<String> = vec![];
|
||||
for id in idvec {
|
||||
match hex_range(id) {
|
||||
Some(HexSearch::Exact(ex)) => {
|
||||
id_searches.push("event_hash=?".to_owned());
|
||||
params.push(Box::new(ex));
|
||||
}
|
||||
Some(HexSearch::Range(lower, upper)) => {
|
||||
id_searches.push("(event_hash>? AND event_hash<?)".to_owned());
|
||||
params.push(Box::new(lower));
|
||||
params.push(Box::new(upper));
|
||||
}
|
||||
Some(HexSearch::LowerOnly(lower)) => {
|
||||
id_searches.push("event_hash>?".to_owned());
|
||||
params.push(Box::new(lower));
|
||||
}
|
||||
None => {
|
||||
info!("Could not parse hex range from id {:?}", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
let id_clause = format!("({})", id_searches.join(" OR "));
|
||||
filter_components.push(id_clause);
|
||||
}
|
||||
// Query for referenced event
|
||||
if f.events.is_some() {
|
||||
let events_escaped: Vec<String> = f
|
||||
.events
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|&x| is_hex(x))
|
||||
.map(|x| format!("x'{}'", x))
|
||||
.collect();
|
||||
let events_clause = format!("referenced_event IN ({})", events_escaped.join(", "));
|
||||
filter_components.push(events_clause);
|
||||
// Query for tags
|
||||
if let Some(map) = &f.tags {
|
||||
for (key, val) in map.iter() {
|
||||
let mut str_vals: Vec<Box<dyn ToSql>> = vec![];
|
||||
let mut blob_vals: Vec<Box<dyn ToSql>> = vec![];
|
||||
for v in val {
|
||||
if is_hex(v) {
|
||||
if let Ok(h) = hex::decode(&v) {
|
||||
blob_vals.push(Box::new(h));
|
||||
}
|
||||
} else {
|
||||
str_vals.push(Box::new(v.to_owned()));
|
||||
}
|
||||
}
|
||||
// create clauses with "?" params for each tag value being searched
|
||||
let str_clause = format!("value IN ({})", repeat_vars(str_vals.len()));
|
||||
let blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len()));
|
||||
let tag_clause = format!("(name=? AND ({} OR {}))", str_clause, blob_clause);
|
||||
// add the tag name as the first parameter
|
||||
params.push(Box::new(key.to_owned()));
|
||||
// add all tag values that are plain strings as params
|
||||
params.append(&mut str_vals);
|
||||
// add all tag values that are blobs as params
|
||||
params.append(&mut blob_vals);
|
||||
filter_components.push(tag_clause);
|
||||
}
|
||||
}
|
||||
// Query for referenced pubkey
|
||||
if f.pubkeys.is_some() {
|
||||
let pubkeys_escaped: Vec<String> = f
|
||||
.pubkeys
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|&x| is_hex(x))
|
||||
.map(|x| format!("x'{}'", x))
|
||||
.collect();
|
||||
let pubkeys_clause = format!("referenced_pubkey IN ({})", pubkeys_escaped.join(", "));
|
||||
filter_components.push(pubkeys_clause);
|
||||
}
|
||||
|
||||
// Query for timestamp
|
||||
if f.since.is_some() {
|
||||
let created_clause = format!("created_at > {}", f.since.unwrap());
|
||||
@@ -371,21 +640,22 @@ fn query_from_sub(sub: &Subscription) -> String {
|
||||
fc.push_str(&filter_components.join(" AND "));
|
||||
fc.push_str(" )");
|
||||
filter_clauses.push(fc);
|
||||
} else {
|
||||
// never display hidden events
|
||||
filter_clauses.push("hidden!=TRUE".to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// never display hidden events
|
||||
query.push_str(" WHERE hidden!=TRUE ");
|
||||
|
||||
// combine all filters with OR clauses, if any exist
|
||||
if !filter_clauses.is_empty() {
|
||||
query.push_str(" WHERE ");
|
||||
query.push_str(" AND (");
|
||||
query.push_str(&filter_clauses.join(" OR "));
|
||||
query.push_str(") ");
|
||||
}
|
||||
// add order clause
|
||||
query.push_str(" ORDER BY created_at ASC");
|
||||
debug!("query string: {}", query);
|
||||
query
|
||||
(query, params)
|
||||
}
|
||||
|
||||
/// Perform a database query using a subscription.
|
||||
@@ -396,34 +666,27 @@ fn query_from_sub(sub: &Subscription) -> String {
|
||||
/// query is immediately aborted.
|
||||
pub async fn db_query(
|
||||
sub: Subscription,
|
||||
conn: r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>,
|
||||
query_tx: tokio::sync::mpsc::Sender<QueryResult>,
|
||||
mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>,
|
||||
) {
|
||||
task::spawn_blocking(move || {
|
||||
let config = SETTINGS.read().unwrap();
|
||||
let db_dir = &config.database.data_directory;
|
||||
let full_path = Path::new(db_dir).join(DB_FILE);
|
||||
|
||||
let conn =
|
||||
Connection::open_with_flags(&full_path, OpenFlags::SQLITE_OPEN_READ_ONLY).unwrap();
|
||||
debug!("opened database for reading");
|
||||
debug!("going to query for: {:?}", sub);
|
||||
let mut row_count: usize = 0;
|
||||
let start = Instant::now();
|
||||
// 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() {
|
||||
let (q, p) = query_from_sub(&sub);
|
||||
// execute the query. Don't cache, since queries vary so much.
|
||||
let mut stmt = conn.prepare(&q)?;
|
||||
let mut event_rows = stmt.query(rusqlite::params_from_iter(p))?;
|
||||
while let Some(row) = event_rows.next()? {
|
||||
// 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;
|
||||
return Ok(());
|
||||
}
|
||||
row_count += 1;
|
||||
// TODO: check before unwrapping
|
||||
let event_json = row.get(0).unwrap();
|
||||
let event_json = row.get(0)?;
|
||||
query_tx
|
||||
.blocking_send(QueryResult {
|
||||
sub_id: sub.get_id(),
|
||||
@@ -436,5 +699,88 @@ pub async fn db_query(
|
||||
row_count,
|
||||
start.elapsed()
|
||||
);
|
||||
let ok: Result<()> = Ok(());
|
||||
ok
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hex_range_exact() -> Result<()> {
|
||||
let hex = "abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00";
|
||||
let r = hex_range(hex);
|
||||
assert_eq!(
|
||||
r,
|
||||
Some(HexSearch::Exact(hex::decode(hex).expect("invalid hex")))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn hex_full_range() -> Result<()> {
|
||||
//let hex = "abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00";
|
||||
let hex = "aaaa";
|
||||
let hex_upper = "aaab";
|
||||
let r = hex_range(hex);
|
||||
assert_eq!(
|
||||
r,
|
||||
Some(HexSearch::Range(
|
||||
hex::decode(hex).expect("invalid hex"),
|
||||
hex::decode(hex_upper).expect("invalid hex")
|
||||
))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_full_range_odd() -> Result<()> {
|
||||
let r = hex_range("abc");
|
||||
assert_eq!(
|
||||
r,
|
||||
Some(HexSearch::Range(
|
||||
hex::decode("abc0").expect("invalid hex"),
|
||||
hex::decode("abd0").expect("invalid hex")
|
||||
))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_full_range_odd_end_f() -> Result<()> {
|
||||
let r = hex_range("abf");
|
||||
assert_eq!(
|
||||
r,
|
||||
Some(HexSearch::Range(
|
||||
hex::decode("abf0").expect("invalid hex"),
|
||||
hex::decode("ac00").expect("invalid hex")
|
||||
))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_no_upper() -> Result<()> {
|
||||
let r = hex_range("ffff");
|
||||
assert_eq!(
|
||||
r,
|
||||
Some(HexSearch::LowerOnly(
|
||||
hex::decode("ffff").expect("invalid hex")
|
||||
))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_no_upper_odd() -> Result<()> {
|
||||
let r = hex_range("fff");
|
||||
assert_eq!(
|
||||
r,
|
||||
Some(HexSearch::LowerOnly(
|
||||
hex::decode("fff0").expect("invalid hex")
|
||||
))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@@ -40,6 +40,8 @@ pub enum Error {
|
||||
ConfigError(config::ConfigError),
|
||||
#[error("Data directory does not exist")]
|
||||
DatabaseDirError,
|
||||
#[error("Custom Error : {0}")]
|
||||
CustomError(String),
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for Error {
|
||||
|
99
src/event.rs
99
src/event.rs
@@ -9,6 +9,8 @@ use secp256k1::{schnorr, Secp256k1, VerifyOnly, XOnlyPublicKey};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::value::Value;
|
||||
use serde_json::Number;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
use std::time::SystemTime;
|
||||
|
||||
@@ -35,6 +37,9 @@ pub struct Event {
|
||||
pub(crate) tags: Vec<Vec<String>>,
|
||||
pub(crate) content: String,
|
||||
pub(crate) sig: String,
|
||||
// Optimization for tag search, built on demand
|
||||
#[serde(skip)]
|
||||
pub(crate) tagidx: Option<HashMap<String, HashSet<String>>>,
|
||||
}
|
||||
|
||||
/// Simple tag type for array of array of strings.
|
||||
@@ -56,7 +61,9 @@ impl From<EventCmd> for Result<Event> {
|
||||
if ec.cmd != "EVENT" {
|
||||
Err(CommandUnknownError)
|
||||
} else if ec.event.is_valid() {
|
||||
Ok(ec.event)
|
||||
let mut e = ec.event;
|
||||
e.build_index();
|
||||
Ok(e)
|
||||
} else {
|
||||
Err(EventInvalid)
|
||||
}
|
||||
@@ -72,6 +79,30 @@ fn unix_time() -> u64 {
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// Build an event tag index
|
||||
fn build_index(&mut self) {
|
||||
// if there are no tags; just leave the index as None
|
||||
if self.tags.is_empty() {
|
||||
return;
|
||||
}
|
||||
// otherwise, build an index
|
||||
let mut idx: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
// iterate over tags that have at least 2 elements
|
||||
for t in self.tags.iter().filter(|x| x.len() > 1) {
|
||||
let tagname = t.get(0).unwrap();
|
||||
let tagval = t.get(1).unwrap();
|
||||
// ensure a vector exists for this tag
|
||||
if !idx.contains_key(tagname) {
|
||||
idx.insert(tagname.clone(), HashSet::new());
|
||||
}
|
||||
// get the tag vec and insert entry
|
||||
let tidx = idx.get_mut(tagname).expect("could not get tag vector");
|
||||
tidx.insert(tagval.clone());
|
||||
}
|
||||
// save the tag structure
|
||||
self.tagidx = Some(idx);
|
||||
}
|
||||
|
||||
/// Create a short event identifier, suitable for logging.
|
||||
pub fn get_event_id_prefix(&self) -> String {
|
||||
self.id.chars().take(8).collect()
|
||||
@@ -114,11 +145,16 @@ impl Event {
|
||||
return false;
|
||||
}
|
||||
// * validate the message digest (sig) using the pubkey & computed sha256 message hash.
|
||||
|
||||
let sig = schnorr::Signature::from_str(&self.sig).unwrap();
|
||||
if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) {
|
||||
let pubkey = XOnlyPublicKey::from_str(&self.pubkey).unwrap();
|
||||
let verify = SECP.verify_schnorr(&sig, &msg, &pubkey);
|
||||
matches!(verify, Ok(()))
|
||||
if let Ok(pubkey) = XOnlyPublicKey::from_str(&self.pubkey) {
|
||||
let verify = SECP.verify_schnorr(&sig, &msg, &pubkey);
|
||||
matches!(verify, Ok(()))
|
||||
} else {
|
||||
info!("Client sent malformed pubkey");
|
||||
false
|
||||
}
|
||||
} else {
|
||||
warn!("Error converting digest to secp256k1 message");
|
||||
false
|
||||
@@ -162,36 +198,18 @@ impl Event {
|
||||
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()[..]);
|
||||
}
|
||||
/// Determine if the given tag and value set intersect with tags in this event.
|
||||
pub fn generic_tag_val_intersect(&self, tagname: &str, check: &HashSet<String>) -> bool {
|
||||
match &self.tagidx {
|
||||
Some(idx) => match idx.get(tagname) {
|
||||
Some(valset) => {
|
||||
let common = valset.intersection(check);
|
||||
common.count() > 0
|
||||
}
|
||||
None => false,
|
||||
},
|
||||
None => false,
|
||||
}
|
||||
etags
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +225,7 @@ mod tests {
|
||||
tags: vec![],
|
||||
content: "".to_owned(),
|
||||
sig: "0".to_owned(),
|
||||
tagidx: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +248,8 @@ mod tests {
|
||||
#[test]
|
||||
fn empty_event_tag_match() -> Result<()> {
|
||||
let event = simple_event();
|
||||
assert!(!event.event_tag_match("foo"));
|
||||
assert!(!event
|
||||
.generic_tag_val_intersect("e", &HashSet::from(["foo".to_owned(), "bar".to_owned()])));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -237,7 +257,14 @@ mod tests {
|
||||
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"));
|
||||
event.build_index();
|
||||
assert_eq!(
|
||||
event.generic_tag_val_intersect(
|
||||
"e",
|
||||
&HashSet::from(["foo".to_owned(), "bar".to_owned()])
|
||||
),
|
||||
true
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -281,6 +308,7 @@ mod tests {
|
||||
tags: vec![],
|
||||
content: "this is a test".to_owned(),
|
||||
sig: "abcde".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
let c = e.to_canonical();
|
||||
let expected = Some(r#"[0,"012345",501234,1,[],"this is a test"]"#.to_owned());
|
||||
@@ -304,6 +332,7 @@ mod tests {
|
||||
],
|
||||
content: "this is a test".to_owned(),
|
||||
sig: "abcde".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
let c = e.to_canonical();
|
||||
let expected_json = r###"[0,"012345",501234,1,[["#e","aoeu"],["#p","aaaa","ws://example.com"]],"this is a test"]"###;
|
||||
|
38
src/info.rs
38
src/info.rs
@@ -16,7 +16,7 @@ pub struct RelayInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pubkey: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<String>,
|
||||
pub contact: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_nips: Option<Vec<i64>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -25,36 +25,18 @@ pub struct RelayInfo {
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for RelayInfo {
|
||||
fn default() -> Self {
|
||||
/// Convert an Info configuration into public Relay Info
|
||||
impl From<config::Info> for RelayInfo {
|
||||
fn from(i: config::Info) -> Self {
|
||||
RelayInfo {
|
||||
id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
pubkey: None,
|
||||
email: None,
|
||||
supported_nips: Some(vec![1]),
|
||||
id: i.relay_url,
|
||||
name: i.name,
|
||||
description: i.description,
|
||||
pubkey: i.pubkey,
|
||||
contact: i.contact,
|
||||
supported_nips: Some(vec![1, 2, 11]),
|
||||
software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()),
|
||||
version: CARGO_PKG_VERSION.map(|x| x.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an Info struct into Relay Info json string
|
||||
pub fn relay_info_json(info: &config::Info) -> String {
|
||||
// get a default RelayInfo
|
||||
let mut r = RelayInfo::default();
|
||||
// update fields from Info, if present
|
||||
r.id = info.relay_url.clone();
|
||||
r.name = info.name.clone();
|
||||
r.description = info.description.clone();
|
||||
r.pubkey = info.pubkey.clone();
|
||||
r.email = info.email.clone();
|
||||
r.to_json()
|
||||
}
|
||||
|
||||
impl RelayInfo {
|
||||
pub fn to_json(self) -> String {
|
||||
serde_json::to_string_pretty(&self).unwrap()
|
||||
}
|
||||
}
|
||||
|
@@ -7,3 +7,4 @@ pub mod event;
|
||||
pub mod info;
|
||||
pub mod protostream;
|
||||
pub mod subscription;
|
||||
pub mod tags;
|
||||
|
64
src/main.rs
64
src/main.rs
@@ -14,7 +14,7 @@ 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::info::relay_info_json;
|
||||
use nostr_rs_relay::info::RelayInfo;
|
||||
use nostr_rs_relay::protostream;
|
||||
use nostr_rs_relay::protostream::NostrMessage::*;
|
||||
use nostr_rs_relay::protostream::NostrResponse::*;
|
||||
@@ -32,14 +32,18 @@ use tokio_tungstenite::WebSocketStream;
|
||||
use tungstenite::handshake;
|
||||
use tungstenite::protocol::WebSocketConfig;
|
||||
|
||||
/// Return a requested DB name from command line arguments.
|
||||
fn db_from_args(args: Vec<String>) -> Option<String> {
|
||||
if args.len() == 3 && args.get(1) == Some(&"--db".to_owned()) {
|
||||
return args.get(2).map(|x| x.to_owned());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Handle arbitrary HTTP requests, including for WebSocket upgrades.
|
||||
async fn handle_web_request(
|
||||
mut request: Request<Body>,
|
||||
pool: db::SqlitePool,
|
||||
remote_addr: SocketAddr,
|
||||
broadcast: Sender<Event>,
|
||||
event_tx: tokio::sync::mpsc::Sender<Event>,
|
||||
@@ -64,17 +68,25 @@ async fn handle_web_request(
|
||||
match upgrade::on(&mut request).await {
|
||||
//if successfully upgraded
|
||||
Ok(upgraded) => {
|
||||
// set WebSocket configuration options
|
||||
let mut config = WebSocketConfig::default();
|
||||
{
|
||||
let settings = config::SETTINGS.read().unwrap();
|
||||
config.max_message_size = settings.limits.max_ws_message_bytes;
|
||||
config.max_frame_size = settings.limits.max_ws_frame_bytes;
|
||||
}
|
||||
//create a websocket stream from the upgraded object
|
||||
let ws_stream = WebSocketStream::from_raw_socket(
|
||||
//pass the upgraded object
|
||||
//as the base layer stream of the Websocket
|
||||
upgraded,
|
||||
tokio_tungstenite::tungstenite::protocol::Role::Server,
|
||||
None,
|
||||
Some(config),
|
||||
)
|
||||
.await;
|
||||
|
||||
tokio::spawn(nostr_server(
|
||||
ws_stream, broadcast, event_tx, shutdown,
|
||||
pool, ws_stream, broadcast, event_tx, shutdown,
|
||||
));
|
||||
}
|
||||
Err(e) => println!(
|
||||
@@ -110,7 +122,8 @@ async fn handle_web_request(
|
||||
let config = config::SETTINGS.read().unwrap();
|
||||
// build a relay info response
|
||||
debug!("Responding to server info request");
|
||||
let b = Body::from(relay_info_json(&config.info));
|
||||
let rinfo = RelayInfo::from(config.info.clone());
|
||||
let b = Body::from(serde_json::to_string_pretty(&rinfo).unwrap());
|
||||
return Ok(Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/nostr+json")
|
||||
@@ -164,9 +177,15 @@ fn main() -> Result<(), Error> {
|
||||
error!("Database directory does not exist");
|
||||
return Err(Error::DatabaseDirError);
|
||||
}
|
||||
debug!("config: {:?}", config);
|
||||
trace!("config: {:?}", config);
|
||||
let addr = format!("{}:{}", config.network.address.trim(), config.network.port);
|
||||
let socket_addr = addr.parse().expect("listening address not valid");
|
||||
if let Some(addr_whitelist) = &config.authorization.pubkey_whitelist {
|
||||
info!(
|
||||
"Event publishing restricted to {} pubkey(s)",
|
||||
addr_whitelist.len()
|
||||
);
|
||||
}
|
||||
// configure tokio runtime
|
||||
let rt = Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
@@ -188,21 +207,24 @@ fn main() -> Result<(), Error> {
|
||||
// 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;
|
||||
info!("db writer created");
|
||||
// // listen for ctrl-c interruupts
|
||||
let ctrl_c_shutdown = invoke_shutdown.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::signal::ctrl_c().await.unwrap();
|
||||
info!("shutting down due to SIGINT");
|
||||
ctrl_c_shutdown.send(()).ok();
|
||||
});
|
||||
// build a connection pool for sqlite connections
|
||||
let pool = db::build_read_pool();
|
||||
// A `Service` is needed for every connection, so this
|
||||
// creates one from our `handle_request` function.
|
||||
let make_svc = make_service_fn(|conn: &AddrStream| {
|
||||
let svc_pool = pool.clone();
|
||||
let remote_addr = conn.remote_addr();
|
||||
let bcast = bcast_tx.clone();
|
||||
let event = event_tx.clone();
|
||||
@@ -212,6 +234,7 @@ fn main() -> Result<(), Error> {
|
||||
Ok::<_, Infallible>(service_fn(move |request: Request<Body>| {
|
||||
handle_web_request(
|
||||
request,
|
||||
svc_pool.clone(),
|
||||
remote_addr,
|
||||
bcast.clone(),
|
||||
event.clone(),
|
||||
@@ -235,6 +258,7 @@ fn main() -> Result<(), Error> {
|
||||
/// Handle new client connections. This runs through an event loop
|
||||
/// for all client communication.
|
||||
async fn nostr_server(
|
||||
pool: db::SqlitePool,
|
||||
ws_stream: WebSocketStream<Upgraded>,
|
||||
broadcast: Sender<Event>,
|
||||
event_tx: tokio::sync::mpsc::Sender<Event>,
|
||||
@@ -242,12 +266,6 @@ async fn nostr_server(
|
||||
) {
|
||||
// get a broadcast channel for clients to communicate on
|
||||
let mut bcast_rx = broadcast.subscribe();
|
||||
let mut config = WebSocketConfig::default();
|
||||
{
|
||||
let settings = config::SETTINGS.read().unwrap();
|
||||
config.max_message_size = settings.limits.max_ws_message_bytes;
|
||||
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 ws_stream = conn.expect("websocket handshake error");
|
||||
@@ -262,7 +280,6 @@ async fn nostr_server(
|
||||
// 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.
|
||||
@@ -330,9 +347,14 @@ async fn nostr_server(
|
||||
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);
|
||||
// when we insert, if there was a previous query running with the same name, cancel it.
|
||||
if let Some(previous_query) = running_queries.insert(s.id.to_owned(), abandon_query_tx) {
|
||||
previous_query.send(()).ok();
|
||||
}
|
||||
// start a database query
|
||||
db::db_query(s, query_tx.clone(), abandon_query_rx).await;
|
||||
// show pool stats
|
||||
debug!("DB pool stats: {:?}", pool.state());
|
||||
db::db_query(s, pool.get().expect("could not get connection"), query_tx.clone(), abandon_query_rx).await;
|
||||
},
|
||||
Err(e) => {
|
||||
info!("Subscription error: {}", e);
|
||||
|
@@ -81,8 +81,14 @@ impl Stream for NostrStream {
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Ready(Some(v)) => match v {
|
||||
Ok(Message::Text(vs)) => Poll::Ready(Some(convert(vs))),
|
||||
Ok(Message::Ping(_x)) => {
|
||||
debug!("client ping");
|
||||
//Pin::new(&mut self.ws_stream).start_send(Message::Pong(x));
|
||||
//info!("sent pong");
|
||||
Poll::Pending
|
||||
}
|
||||
Ok(Message::Binary(_)) => Poll::Ready(Some(Err(Error::ProtoParseError))),
|
||||
Ok(Message::Pong(_)) | Ok(Message::Ping(_)) => Poll::Pending,
|
||||
Ok(Message::Pong(_)) => Poll::Pending,
|
||||
Ok(Message::Close(_)) => Poll::Ready(None),
|
||||
Err(WsError::AlreadyClosed) | Err(WsError::ConnectionClosed) => Poll::Ready(None),
|
||||
Err(_) => Poll::Ready(Some(Err(Error::ConnError))),
|
||||
|
@@ -1,7 +1,10 @@
|
||||
//! Subscription and filter parsing
|
||||
use crate::error::Result;
|
||||
use crate::event::Event;
|
||||
use serde::de::Unexpected;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Subscription identifier and set of request filters
|
||||
@@ -16,24 +19,76 @@ pub struct Subscription {
|
||||
/// 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, PartialEq, Debug, Clone)]
|
||||
pub struct ReqFilter {
|
||||
/// Event hashes
|
||||
pub ids: Option<Vec<String>>,
|
||||
/// Event kinds
|
||||
pub kinds: Option<Vec<u64>>,
|
||||
/// Referenced event hash
|
||||
#[serde(rename = "#e")]
|
||||
pub events: Option<Vec<String>>,
|
||||
/// Referenced public key for a petname
|
||||
#[serde(rename = "#p")]
|
||||
pub pubkeys: Option<Vec<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>>,
|
||||
/// Set of tags
|
||||
#[serde(skip)]
|
||||
pub tags: Option<HashMap<String, HashSet<String>>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ReqFilter {
|
||||
fn deserialize<D>(deserializer: D) -> Result<ReqFilter, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let received: Value = Deserialize::deserialize(deserializer)?;
|
||||
let filter = received.as_object().ok_or_else(|| {
|
||||
serde::de::Error::invalid_type(
|
||||
Unexpected::Other("reqfilter is not an object"),
|
||||
&"a json object",
|
||||
)
|
||||
})?;
|
||||
let mut rf = ReqFilter {
|
||||
ids: None,
|
||||
kinds: None,
|
||||
since: None,
|
||||
until: None,
|
||||
authors: None,
|
||||
tags: None,
|
||||
};
|
||||
let mut ts = None;
|
||||
// iterate through each key, and assign values that exist
|
||||
for (key, val) in filter.into_iter() {
|
||||
// ids
|
||||
if key == "ids" {
|
||||
rf.ids = Deserialize::deserialize(val).ok();
|
||||
} else if key == "kinds" {
|
||||
rf.kinds = Deserialize::deserialize(val).ok();
|
||||
} else if key == "since" {
|
||||
rf.since = Deserialize::deserialize(val).ok();
|
||||
} else if key == "until" {
|
||||
rf.until = Deserialize::deserialize(val).ok();
|
||||
} else if key == "authors" {
|
||||
rf.authors = Deserialize::deserialize(val).ok();
|
||||
} else if key.starts_with('#') && key.len() > 1 && val.is_array() {
|
||||
// remove the prefix
|
||||
let tagname = &key[1..];
|
||||
if ts.is_none() {
|
||||
// Initialize the tag if necessary
|
||||
ts = Some(HashMap::new());
|
||||
}
|
||||
if let Some(m) = ts.as_mut() {
|
||||
let tag_vals: Option<Vec<String>> = Deserialize::deserialize(val).ok();
|
||||
if let Some(v) = tag_vals {
|
||||
let hs = HashSet::from_iter(v.into_iter());
|
||||
m.insert(tagname.to_owned(), hs);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
rf.tags = ts;
|
||||
Ok(rf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Subscription {
|
||||
@@ -43,7 +98,7 @@ impl<'de> Deserialize<'de> for Subscription {
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let mut v: serde_json::Value = Deserialize::deserialize(deserializer)?;
|
||||
let mut v: Value = Deserialize::deserialize(deserializer)?;
|
||||
// this shoud be a 3-or-more element array.
|
||||
// verify the first element is a String, REQ
|
||||
// get the subscription from the second element.
|
||||
@@ -78,6 +133,7 @@ impl<'de> Deserialize<'de> for Subscription {
|
||||
for fv in i {
|
||||
let f: ReqFilter = serde_json::from_value(fv.take())
|
||||
.map_err(|_| serde::de::Error::custom("could not parse filter"))?;
|
||||
// create indexes
|
||||
filters.push(f);
|
||||
}
|
||||
Ok(Subscription {
|
||||
@@ -104,50 +160,45 @@ impl Subscription {
|
||||
}
|
||||
}
|
||||
|
||||
fn prefix_match(prefixes: &[String], target: &str) -> bool {
|
||||
for prefix in prefixes {
|
||||
if target.starts_with(prefix) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// none matched
|
||||
false
|
||||
}
|
||||
|
||||
impl ReqFilter {
|
||||
/// Check for a match within the authors list.
|
||||
fn ids_match(&self, event: &Event) -> bool {
|
||||
self.ids
|
||||
.as_ref()
|
||||
.map(|vs| vs.contains(&event.id.to_owned()))
|
||||
.map(|vs| prefix_match(vs, &event.id))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn authors_match(&self, event: &Event) -> bool {
|
||||
self.authors
|
||||
.as_ref()
|
||||
.map(|vs| vs.contains(&event.pubkey.to_owned()))
|
||||
.map(|vs| prefix_match(vs, &event.pubkey))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
/// Check if this filter either matches, or does not care about the event tags.
|
||||
fn event_match(&self, event: &Event) -> bool {
|
||||
// This needs to be analyzed for performance; building these
|
||||
// hash sets for each active subscription isn't great.
|
||||
if let Some(es) = &self.events {
|
||||
let event_refs =
|
||||
HashSet::<_>::from_iter(event.get_event_tags().iter().map(|x| x.to_owned()));
|
||||
let filter_refs = HashSet::<_>::from_iter(es.iter().map(|x| &x[..]));
|
||||
let cardinality = event_refs.intersection(&filter_refs).count();
|
||||
cardinality > 0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this filter either matches, or does not care about
|
||||
/// the pubkey/petname tags.
|
||||
fn pubkey_match(&self, event: &Event) -> bool {
|
||||
// This needs to be analyzed for performance; building these
|
||||
// hash sets for each active subscription isn't great.
|
||||
if let Some(ps) = &self.pubkeys {
|
||||
let pubkey_refs =
|
||||
HashSet::<_>::from_iter(event.get_pubkey_tags().iter().map(|x| x.to_owned()));
|
||||
let filter_refs = HashSet::<_>::from_iter(ps.iter().map(|x| &x[..]));
|
||||
let cardinality = pubkey_refs.intersection(&filter_refs).count();
|
||||
cardinality > 0
|
||||
} else {
|
||||
true
|
||||
fn tag_match(&self, event: &Event) -> bool {
|
||||
// get the hashset from the filter.
|
||||
if let Some(map) = &self.tags {
|
||||
for (key, val) in map.iter() {
|
||||
let tag_match = event.generic_tag_val_intersect(key, val);
|
||||
// if there is no match for this tag, the match fails.
|
||||
if !tag_match {
|
||||
return false;
|
||||
}
|
||||
// if there was a match, we move on to the next one.
|
||||
}
|
||||
}
|
||||
// if the tag map is empty, the match succeeds (there was no filter)
|
||||
true
|
||||
}
|
||||
|
||||
/// Check if this filter either matches, or does not care about the kind.
|
||||
@@ -165,8 +216,7 @@ impl ReqFilter {
|
||||
&& self.since.map(|t| event.created_at > t).unwrap_or(true)
|
||||
&& self.kind_match(event.kind)
|
||||
&& self.authors_match(event)
|
||||
&& self.pubkey_match(event)
|
||||
&& self.event_match(event)
|
||||
&& self.tag_match(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,27 +247,66 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_filter() {
|
||||
// unrecognized field in filter
|
||||
let raw_json = "[\"REQ\",\"some-id\",{\"foo\": 3}]";
|
||||
assert!(serde_json::from_str::<Subscription>(raw_json).is_err());
|
||||
fn legacy_filter() {
|
||||
// legacy field in filter
|
||||
let raw_json = "[\"REQ\",\"some-id\",{\"kind\": 3}]";
|
||||
assert!(serde_json::from_str::<Subscription>(raw_json).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn author_filter() -> Result<()> {
|
||||
let raw_json = "[\"REQ\",\"some-id\",{\"author\": \"test-author-id\"}]";
|
||||
let raw_json = r#"["REQ","some-id",{"authors": ["test-author-id"]}]"#;
|
||||
let s: Subscription = serde_json::from_str(raw_json)?;
|
||||
assert_eq!(s.id, "some-id");
|
||||
assert_eq!(s.filters.len(), 1);
|
||||
let first_filter = s.filters.get(0).unwrap();
|
||||
assert_eq!(first_filter.author, Some("test-author-id".to_owned()));
|
||||
assert_eq!(
|
||||
first_filter.authors,
|
||||
Some(vec!("test-author-id".to_owned()))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interest_author_prefix_match() -> Result<()> {
|
||||
// subscription with a filter for ID
|
||||
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors": ["abc"]}]"#)?;
|
||||
let e = Event {
|
||||
id: "foo".to_owned(),
|
||||
pubkey: "abcd".to_owned(),
|
||||
created_at: 0,
|
||||
kind: 0,
|
||||
tags: Vec::new(),
|
||||
content: "".to_owned(),
|
||||
sig: "".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
assert!(s.interested_in_event(&e));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interest_id_prefix_match() -> Result<()> {
|
||||
// subscription with a filter for ID
|
||||
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"]}]"#)?;
|
||||
let e = Event {
|
||||
id: "abcd".to_owned(),
|
||||
pubkey: "".to_owned(),
|
||||
created_at: 0,
|
||||
kind: 0,
|
||||
tags: Vec::new(),
|
||||
content: "".to_owned(),
|
||||
sig: "".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
assert!(s.interested_in_event(&e));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interest_id_nomatch() -> Result<()> {
|
||||
// subscription with a filter for ID
|
||||
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"id":"abc"}]"#)?;
|
||||
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"ids": ["xyz"]}]"#)?;
|
||||
let e = Event {
|
||||
id: "abcde".to_owned(),
|
||||
pubkey: "".to_owned(),
|
||||
@@ -226,15 +315,17 @@ mod tests {
|
||||
tags: Vec::new(),
|
||||
content: "".to_owned(),
|
||||
sig: "".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
assert_eq!(s.interested_in_event(&e), false);
|
||||
assert!(!s.interested_in_event(&e));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interest_time_and_id() -> Result<()> {
|
||||
// subscription with a filter for ID and time
|
||||
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"id":"abc", "since": 1000}]"#)?;
|
||||
let s: Subscription =
|
||||
serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "since": 1000}]"#)?;
|
||||
let e = Event {
|
||||
id: "abc".to_owned(),
|
||||
pubkey: "".to_owned(),
|
||||
@@ -243,8 +334,9 @@ mod tests {
|
||||
tags: Vec::new(),
|
||||
content: "".to_owned(),
|
||||
sig: "".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
assert_eq!(s.interested_in_event(&e), false);
|
||||
assert!(!s.interested_in_event(&e));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -260,8 +352,9 @@ mod tests {
|
||||
tags: Vec::new(),
|
||||
content: "".to_owned(),
|
||||
sig: "".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
assert_eq!(s.interested_in_event(&e), true);
|
||||
assert!(s.interested_in_event(&e));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -277,8 +370,9 @@ mod tests {
|
||||
tags: Vec::new(),
|
||||
content: "".to_owned(),
|
||||
sig: "".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
assert_eq!(s.interested_in_event(&e), true);
|
||||
assert!(s.interested_in_event(&e));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -294,8 +388,9 @@ mod tests {
|
||||
tags: Vec::new(),
|
||||
content: "".to_owned(),
|
||||
sig: "".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
assert_eq!(s.interested_in_event(&e), true);
|
||||
assert!(s.interested_in_event(&e));
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
@@ -311,8 +406,9 @@ mod tests {
|
||||
tags: Vec::new(),
|
||||
content: "".to_owned(),
|
||||
sig: "".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
assert_eq!(s.interested_in_event(&e), true);
|
||||
assert!(s.interested_in_event(&e));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -328,8 +424,9 @@ mod tests {
|
||||
tags: Vec::new(),
|
||||
content: "".to_owned(),
|
||||
sig: "".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
assert_eq!(s.interested_in_event(&e), false);
|
||||
assert!(!s.interested_in_event(&e));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
315
src/tags.rs
Normal file
315
src/tags.rs
Normal file
@@ -0,0 +1,315 @@
|
||||
//! Tags used in events to link to another event or a pubkey
|
||||
//!
|
||||
//! Reference specification NIP01: https://github.com/fiatjaf/nostr/blob/master/nips/01.md#events-and-signatures
|
||||
//!
|
||||
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
use bitcoin_hashes::sha256;
|
||||
use secp256k1::XOnlyPublicKey;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::de::Unexpected;
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
type EventId = sha256::Hash;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct EventTag {
|
||||
event_id: EventId,
|
||||
recommended_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct PubkeyTag {
|
||||
pubkey: XOnlyPublicKey,
|
||||
recommended_url: Option<String>,
|
||||
}
|
||||
|
||||
// Tag structure representing two possible types of tags
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Tag {
|
||||
Event(EventTag),
|
||||
Pubkey(PubkeyTag),
|
||||
}
|
||||
|
||||
// Custom json serialization into protocol network format
|
||||
// Event tag : ["e", "<32 byte event-id>", "optional<url>"]
|
||||
// Pubkey tag : ["p", "<32 byte Xonly Pubkey>", "optional<url>"]
|
||||
impl serde::Serialize for Tag {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Event(event_tag) => {
|
||||
let mut seq = serializer.serialize_seq(None)?;
|
||||
seq.serialize_element("e")?;
|
||||
seq.serialize_element(&event_tag.event_id.to_hex())?;
|
||||
if let Some(url) = &event_tag.recommended_url {
|
||||
seq.serialize_element(url)?;
|
||||
} else {
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
Self::Pubkey(pubkey_tag) => {
|
||||
let mut seq = serializer.serialize_seq(None)?;
|
||||
seq.serialize_element("p")?;
|
||||
seq.serialize_element(&pubkey_tag.pubkey.to_hex())?;
|
||||
if let Some(url) = &pubkey_tag.recommended_url {
|
||||
seq.serialize_element(url)?;
|
||||
} else {
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom json deserialization from protocol network format
|
||||
impl<'de> serde::Deserialize<'de> for Tag {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// Receive incoming data in a json object
|
||||
let received: Value = Deserialize::deserialize(deserializer)?;
|
||||
|
||||
// Check received data is a json array
|
||||
let values = received.as_array().ok_or_else(|| {
|
||||
serde::de::Error::invalid_type(Unexpected::Other("tag json object"), &"json array")
|
||||
})?;
|
||||
|
||||
// Check json array contains only string
|
||||
let values = values
|
||||
.iter()
|
||||
.map(|value| {
|
||||
value.as_str().ok_or_else(|| {
|
||||
serde::de::Error::invalid_type(
|
||||
Unexpected::Other("tag json data"),
|
||||
&"json string",
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Check length is not more tha 3
|
||||
if values.len() > 3 {
|
||||
Err(serde::de::Error::invalid_length(
|
||||
values.len(),
|
||||
&"tag length is 2 or 3",
|
||||
))
|
||||
} else {
|
||||
// Parse the json array into appropriate types
|
||||
match values[0] {
|
||||
// This denotes an event type tag
|
||||
"e" => {
|
||||
let event_id = EventId::from_str(values[1])
|
||||
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
|
||||
let recomended_url = if values.len() == 3 {
|
||||
Some(values[2].into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Tag::Event(EventTag {
|
||||
event_id,
|
||||
recommended_url: recomended_url,
|
||||
}))
|
||||
}
|
||||
// This denotes a pubkey type tag
|
||||
"p" => {
|
||||
let pubkey = XOnlyPublicKey::from_str(values[1])
|
||||
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
|
||||
let recomended_url = if values.len() == 3 {
|
||||
Some(values[2].into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Tag::Pubkey(PubkeyTag {
|
||||
pubkey,
|
||||
recommended_url: recomended_url,
|
||||
}))
|
||||
}
|
||||
// Any other tag type is currently not supported
|
||||
_ => Err(serde::de::Error::invalid_value(
|
||||
Unexpected::Other("tag type flag"),
|
||||
&"'e' or 'p'",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
// Some api (not public currently) to use Tags
|
||||
impl Tag {
|
||||
fn new_event_tag(event_id: EventId, recomended_url: Option<String>) -> Self {
|
||||
Self::Event(EventTag {
|
||||
event_id,
|
||||
recommended_url: recomended_url,
|
||||
})
|
||||
}
|
||||
|
||||
fn new_pubkey_tag(pubkey: XOnlyPublicKey, recomended_url: Option<String>) -> Self {
|
||||
Self::Pubkey(PubkeyTag {
|
||||
pubkey,
|
||||
recommended_url: recomended_url,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_recomended_url(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Event(EventTag {
|
||||
event_id: _,
|
||||
recommended_url,
|
||||
}) => recommended_url.clone(),
|
||||
Self::Pubkey(PubkeyTag {
|
||||
pubkey: _,
|
||||
recommended_url,
|
||||
}) => recommended_url.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_event_id(&self) -> Result<EventId, Error> {
|
||||
match self {
|
||||
Self::Event(EventTag {
|
||||
event_id,
|
||||
recommended_url: _,
|
||||
}) => Ok(*event_id),
|
||||
_ => Err(Error::CustomError("Expected event tag".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_pubkey(&self) -> Result<XOnlyPublicKey, Error> {
|
||||
match self {
|
||||
Self::Pubkey(PubkeyTag {
|
||||
pubkey,
|
||||
recommended_url: _,
|
||||
}) => Ok(*pubkey),
|
||||
_ => Err(Error::CustomError("Expected pubkey tag".to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use bitcoin_hashes::Hash;
|
||||
|
||||
#[test]
|
||||
fn serde_roundtrip() {
|
||||
let pubkey = XOnlyPublicKey::from_str(
|
||||
"18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
|
||||
)
|
||||
.unwrap();
|
||||
let url = "wss://rsslay.fiatjaf.com";
|
||||
static HASH_BYTES: [u8; 32] = [
|
||||
0xef, 0x53, 0x7f, 0x25, 0xc8, 0x95, 0xbf, 0xa7, 0x82, 0x52, 0x65, 0x29, 0xa9, 0xb6,
|
||||
0x3d, 0x97, 0xaa, 0x63, 0x15, 0x64, 0xd5, 0xd7, 0x89, 0xc2, 0xb7, 0x65, 0x44, 0x8c,
|
||||
0x86, 0x35, 0xfb, 0x6c,
|
||||
];
|
||||
let event_id = EventId::from_slice(&HASH_BYTES).expect("right number of bytes");
|
||||
|
||||
let event_tag = Tag::new_event_tag(event_id, Some(url.to_string()));
|
||||
let pubkey_tag = Tag::new_pubkey_tag(pubkey, Some(url.to_string()));
|
||||
|
||||
let ser_event_tag = serde_json::to_string(&event_tag).unwrap();
|
||||
let ser_pubkey_tag = serde_json::to_string(&pubkey_tag).unwrap();
|
||||
|
||||
let deser_event_tag: Tag = serde_json::from_str(&ser_event_tag).unwrap();
|
||||
let deser_pubkey_tag: Tag = serde_json::from_str(&ser_pubkey_tag).unwrap();
|
||||
|
||||
assert_eq!(deser_event_tag, event_tag);
|
||||
assert_eq!(deser_pubkey_tag, pubkey_tag);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_pubkey() {
|
||||
let test_string = r#"["p","845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166","wss://rsslay.fiatjaf.com"]"#;
|
||||
let tag: Result<Tag, _> = serde_json::from_str(test_string);
|
||||
assert_eq!(
|
||||
tag.err().expect("expect error").to_string(),
|
||||
"secp: malformed public key".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_datatype() {
|
||||
let test_string = r#"["p", 188457, "wss://rsslay.fiatjaf.com"]"#;
|
||||
let tag: Result<Tag, _> = serde_json::from_str(test_string);
|
||||
assert_eq!(
|
||||
tag.err().expect("expect error").to_string(),
|
||||
"invalid type: tag json data, expected json string".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_tagbyte() {
|
||||
let test_string = r#"["q", "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", "wss://rsslay.fiatjaf.com"]"#;
|
||||
let tag: Result<Tag, _> = serde_json::from_str(test_string);
|
||||
assert_eq!(
|
||||
tag.err().expect("expect error").to_string(),
|
||||
"invalid value: tag type flag, expected 'e' or 'p'".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_event_id() {
|
||||
let test_string = r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d78c8635fb6c","wss://rsslay.fiatjaf.com"]"#;
|
||||
let tag: Result<Tag, _> = serde_json::from_str(test_string);
|
||||
assert_eq!(
|
||||
tag.err().expect("expect error").to_string(),
|
||||
"bad hex string length 54 (expected 64)".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_url_type() {
|
||||
let test_string = r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c", 123456788]"#;
|
||||
let tag: Result<Tag, _> = serde_json::from_str(test_string);
|
||||
assert_eq!(
|
||||
tag.err().expect("expect error").to_string(),
|
||||
"invalid type: tag json data, expected json string".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_length() {
|
||||
let test_string = r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c","wss://rsslay.fiatjaf.com", "Random extra data"]"#;
|
||||
let tag: Result<Tag, _> = serde_json::from_str(test_string);
|
||||
assert_eq!(
|
||||
tag.err().expect("expect error").to_string(),
|
||||
"invalid length 4, expected tag length is 2 or 3".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_json_type() {
|
||||
let test_string = r#"{"type": "e","event": "ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c"}"#;
|
||||
let tag: Result<Tag, _> = serde_json::from_str(test_string);
|
||||
assert_eq!(
|
||||
tag.err().expect("expect error").to_string(),
|
||||
"invalid type: tag json object, expected json array".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evenrt_tag_missing_url() {
|
||||
let test_string =
|
||||
r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c"]"#;
|
||||
let tag: Tag = serde_json::from_str(test_string).unwrap();
|
||||
assert!(tag.get_recomended_url().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pubkey_tag_missing_url() {
|
||||
let test_string =
|
||||
r#"["p","18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166"]"#;
|
||||
let tag: Tag = serde_json::from_str(test_string).unwrap();
|
||||
assert!(tag.get_recomended_url().is_none());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user