Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf3e67500f | ||
|
|
1d19442cfd | ||
|
|
13cc24b5cd | ||
|
|
f543957b34 | ||
|
|
7021f102e8 | ||
|
|
fddbf321bc | ||
|
|
3e7f2e21df | ||
|
|
9d9c6c78d1 | ||
|
|
703b2efe6e | ||
|
|
0db6487ce3 | ||
|
|
ba987d3212 | ||
|
|
73f4f60cc7 | ||
|
|
d06d227ebe | ||
|
|
3519488c4e | ||
|
|
fbd3315110 | ||
|
|
3d3d1bde53 | ||
|
|
ed336111bb | ||
|
|
8aed572989 | ||
|
|
62e8da689d | ||
|
|
807d1aa384 | ||
|
|
66a55b55b9 | ||
|
|
76c77c3e56 | ||
|
|
50daab8a6f | ||
|
|
ffd4e6f997 | ||
|
|
bbd716963e | ||
|
|
ca95e8cf22 | ||
|
|
e9d2a2cbd0 | ||
|
|
39a945b493 | ||
|
|
9a84dc19e9 | ||
|
|
20c4bb42eb | ||
|
|
0e519f6b77 | ||
|
|
3dd0f2c9c6 | ||
|
|
b7c8737166 | ||
|
|
c0b112c094 | ||
|
|
cb283ac316 | ||
|
|
2c6ac69bfd | ||
|
|
d929ae2752 | ||
|
|
14fe9f9ee1 | ||
|
|
7774db8c47 | ||
|
|
104ef2b9e1 | ||
|
|
c06139ec99 | ||
|
|
19ec89593d | ||
|
|
27902bc5f4 | ||
|
|
d2adddaee4 | ||
|
|
b23b3ce8ec | ||
|
|
5f9fe1ce59 | ||
|
|
6a8c4ed1b5 | ||
|
|
966c853700 | ||
|
|
65fd0ed08b | ||
|
|
0b51675b38 | ||
|
|
2e22334631 | ||
|
|
cb2ac4bf0f | ||
|
|
38dc7789dc | ||
|
|
ce0e00ffb3 | ||
|
|
3e4ae4aeec | ||
|
|
c6a8807485 | ||
|
|
8137b6211c | ||
|
|
29effaae23 | ||
|
|
e5074f2e46 | ||
|
|
4fd7643907 | ||
|
|
1e1ec69175 | ||
|
|
e08647867c | ||
|
|
ae0f7171ed | ||
|
|
4f1a912f36 | ||
|
|
95748647f0 | ||
|
|
25480e837f | ||
|
|
b80b54cd9d | ||
|
|
8ea732cbe5 | ||
|
|
0f68c4e5c2 | ||
|
|
dab2cd5792 | ||
|
|
f411aa6fc2 | ||
|
|
d31bbda087 | ||
|
|
5917bc53b2 | ||
|
|
91177c61a1 | ||
|
|
53c2a8051c | ||
|
|
168cf513ac | ||
|
|
ea204761c9 | ||
|
|
c270ae1434 | ||
|
|
64bd983cb6 | ||
|
|
1c153bc784 | ||
|
|
dc11d9a619 | ||
|
|
cd1557787b | ||
|
|
86bb7aeb9a | ||
|
|
ce37fc1a2d | ||
|
|
2cfd384339 | ||
|
|
8c013107f9 | ||
|
|
64a4466d30 | ||
|
|
1596c23eb4 | ||
|
|
129badd4e1 | ||
|
|
6f7c080180 | ||
|
|
af92561ef6 | ||
|
|
d833a3e40d | ||
|
|
462eb46642 | ||
|
|
cf144d503d | ||
|
|
fb8375aef2 | ||
|
|
88ac31b549 | ||
|
|
677b7d39e9 | ||
|
|
b24d2f9aaa | ||
|
|
7a3899d852 | ||
|
|
818108b793 | ||
|
|
d10348f7e1 | ||
|
|
8598e443d8 | ||
|
|
43222d44e5 | ||
|
|
7c1516c4fb | ||
|
|
0c72053a49 | ||
|
|
3f32ff67ab | ||
|
|
0b9778d6ca | ||
|
|
9be04120c7 | ||
|
|
cc06167e06 | ||
|
|
b6e33f044f | ||
|
|
1b2c6f9fca | ||
|
|
0d8d39ad22 | ||
|
|
0e851d4f71 | ||
|
|
3c880b2f49 | ||
|
|
7a4c9266ec |
@@ -11,6 +11,6 @@ repos:
|
|||||||
- repo: https://github.com/doublify/pre-commit-rust
|
- repo: https://github.com/doublify/pre-commit-rust
|
||||||
rev: v1.0
|
rev: v1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: fmt
|
# - id: fmt
|
||||||
- id: cargo-check
|
- id: cargo-check
|
||||||
- id: clippy
|
- id: clippy
|
||||||
|
|||||||
Generated
+259
-160
@@ -54,9 +54,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.66"
|
version = "1.0.68"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
|
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-stream"
|
name = "async-stream"
|
||||||
@@ -81,9 +81,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.59"
|
version = "0.1.61"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364"
|
checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -107,9 +107,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.6.1"
|
version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08b108ad2665fa3f6e6a517c3d80ec3e77d224c47d605167aefaa5d7ef97fa48"
|
checksum = "1304eab461cf02bd70b083ed8273388f9724c549b316ba3d1e213ce0e9e7fb7e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
@@ -136,9 +136,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79b8558f5a0581152dc94dcd289132a1d377494bdeafcd41869b3258e3e2ad92"
|
checksum = "f487e40dc9daee24d8a1779df88522f159a54a980f99cfbe43db0be0bd3444a8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -223,6 +223,43 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ec7a4128863c188deefe750ac1d1dfe66c236909f845af04beed823638dc1b2"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"clap_derive",
|
||||||
|
"clap_lex",
|
||||||
|
"is-terminal",
|
||||||
|
"once_cell",
|
||||||
|
"strsim",
|
||||||
|
"termcolor",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade"
|
||||||
|
dependencies = [
|
||||||
|
"os_str_bytes",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cloudabi"
|
name = "cloudabi"
|
||||||
version = "0.0.3"
|
version = "0.0.3"
|
||||||
@@ -382,9 +419,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxx"
|
name = "cxx"
|
||||||
version = "1.0.83"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf"
|
checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cxxbridge-flags",
|
"cxxbridge-flags",
|
||||||
@@ -394,9 +431,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxx-build"
|
name = "cxx-build"
|
||||||
version = "1.0.83"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39"
|
checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
@@ -409,15 +446,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxxbridge-flags"
|
name = "cxxbridge-flags"
|
||||||
version = "1.0.83"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12"
|
checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxxbridge-macro"
|
name = "cxxbridge-macro"
|
||||||
version = "1.0.83"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
|
checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -462,6 +499,27 @@ version = "1.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.2.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||||
|
dependencies = [
|
||||||
|
"errno-dragonfly",
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno-dragonfly"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fallible-iterator"
|
name = "fallible-iterator"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -728,10 +786,16 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "heck"
|
||||||
version = "0.1.19"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@@ -890,6 +954,28 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "io-lifetimes"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-terminal"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"io-lifetimes",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
@@ -901,9 +987,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
|
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
@@ -933,9 +1019,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.138"
|
version = "0.2.139"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
|
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
@@ -950,9 +1036,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "link-cplusplus"
|
name = "link-cplusplus"
|
||||||
version = "1.0.7"
|
version = "1.0.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
|
checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
@@ -963,6 +1049,12 @@ version = "0.5.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@@ -1051,7 +1143,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.42.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1080,9 +1172,9 @@ checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.1"
|
version = "7.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
@@ -1096,10 +1188,11 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-rs-relay"
|
name = "nostr-rs-relay"
|
||||||
version = "0.7.5"
|
version = "0.7.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitcoin_hashes",
|
"bitcoin_hashes",
|
||||||
|
"clap",
|
||||||
"config",
|
"config",
|
||||||
"console-subscriber",
|
"console-subscriber",
|
||||||
"const_format",
|
"const_format",
|
||||||
@@ -1209,9 +1302,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.14.0"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
|
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi",
|
"hermit-abi",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -1219,15 +1312,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.16.0"
|
version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
|
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.44"
|
version = "0.10.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29d971fd5722fec23977260f6e81aa67d2f22cadbdc2aa049f1022d9a3be1566"
|
checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -1257,9 +1350,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.79"
|
version = "0.9.80"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5454462c0eced1e97f2ec09036abc8da362e66802f66fd20f86854d9d8cbcbc4"
|
checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg 1.1.0",
|
"autocfg 1.1.0",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -1278,6 +1371,12 @@ dependencies = [
|
|||||||
"hashbrown 0.9.1",
|
"hashbrown 0.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_str_bytes"
|
||||||
|
version = "6.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -1290,15 +1389,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot_core"
|
name = "parking_lot_core"
|
||||||
version = "0.9.5"
|
version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
|
checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-sys 0.42.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1326,9 +1425,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.5.1"
|
version = "2.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0"
|
checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"ucd-trie",
|
"ucd-trie",
|
||||||
@@ -1336,9 +1435,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_derive"
|
name = "pest_derive"
|
||||||
version = "2.5.1"
|
version = "2.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344"
|
checksum = "241cda393b0cdd65e62e07e12454f1f25d57017dcc514b1514cd3c4645e3a0a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_generator",
|
"pest_generator",
|
||||||
@@ -1346,9 +1445,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_generator"
|
name = "pest_generator"
|
||||||
version = "2.5.1"
|
version = "2.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c"
|
checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_meta",
|
"pest_meta",
|
||||||
@@ -1359,13 +1458,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_meta"
|
name = "pest_meta"
|
||||||
version = "2.5.1"
|
version = "2.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20"
|
checksum = "0ef4f1332a8d4678b41966bb4cc1d0676880e84183a1ecc3f4b69f03e99c7a51"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pest",
|
"pest",
|
||||||
"sha1",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1413,19 +1512,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro-error"
|
||||||
version = "1.0.47"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
|
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.49"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost"
|
name = "prost"
|
||||||
version = "0.11.3"
|
version = "0.11.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0b18e655c21ff5ac2084a5ad0611e827b3f92badf79f4910b5a5c58f4d87ff0"
|
checksum = "21dc42e00223fc37204bd4aa177e69420c604ca4a183209a8f9de30c6d934698"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"prost-derive",
|
"prost-derive",
|
||||||
@@ -1433,9 +1556,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-derive"
|
name = "prost-derive"
|
||||||
version = "0.11.2"
|
version = "0.11.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "164ae68b6587001ca506d3bf7f1000bfa248d0e1217b618108fba4ec1d0cc306"
|
checksum = "8bda8c0881ea9f722eb9629376db3d0b903b462477c1aafcb0566610ac28ac5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools",
|
"itertools",
|
||||||
@@ -1446,9 +1569,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-types"
|
name = "prost-types"
|
||||||
version = "0.11.2"
|
version = "0.11.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "747761bc3dc48f9a34553bf65605cf6cb6288ba219f3450b4275dbd81539551a"
|
checksum = "a5e0526209433e96d83d750dd81a99118edbc55739e7e61a46764fd2ad537788"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"prost",
|
"prost",
|
||||||
@@ -1472,9 +1595,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.21"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
|
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -1665,9 +1788,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.7.0"
|
version = "1.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
|
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1735,25 +1858,38 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustix"
|
||||||
version = "1.0.9"
|
version = "0.36.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
|
checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"io-lifetimes",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.11"
|
version = "1.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.20"
|
version = "0.1.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
|
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"windows-sys",
|
||||||
"windows-sys 0.36.1",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1773,9 +1909,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scratch"
|
name = "scratch"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
|
checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "secp256k1"
|
name = "secp256k1"
|
||||||
@@ -1823,18 +1959,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.150"
|
version = "1.0.152"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e326c9ec8042f1b5da33252c8a37e9ffbd2c9bef0155215b6e6c80c790e05f91"
|
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.150"
|
version = "1.0.152"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42a3df25b0713732468deadad63ab9da1f1fd75a48a15024b50363f128db627e"
|
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1843,9 +1979,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.89"
|
version = "1.0.91"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
|
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
@@ -1865,10 +2001,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha2"
|
||||||
version = "0.10.5"
|
version = "0.10.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
|
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
@@ -1919,10 +2055,16 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "strsim"
|
||||||
version = "1.0.105"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1951,27 +2093,27 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.1.3"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.37"
|
version = "1.0.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
|
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.37"
|
version = "1.0.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
|
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2004,9 +2146,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.23.0"
|
version = "1.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
|
checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg 1.1.0",
|
"autocfg 1.1.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2020,7 +2162,7 @@ dependencies = [
|
|||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.42.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2287,9 +2429,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.3"
|
version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
@@ -2330,9 +2472,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
|
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
@@ -2516,19 +2658,6 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.36.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_msvc 0.36.1",
|
|
||||||
"windows_i686_gnu 0.36.1",
|
|
||||||
"windows_i686_msvc 0.36.1",
|
|
||||||
"windows_x86_64_gnu 0.36.1",
|
|
||||||
"windows_x86_64_msvc 0.36.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.42.0"
|
version = "0.42.0"
|
||||||
@@ -2536,85 +2665,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows_aarch64_gnullvm",
|
"windows_aarch64_gnullvm",
|
||||||
"windows_aarch64_msvc 0.42.0",
|
"windows_aarch64_msvc",
|
||||||
"windows_i686_gnu 0.42.0",
|
"windows_i686_gnu",
|
||||||
"windows_i686_msvc 0.42.0",
|
"windows_i686_msvc",
|
||||||
"windows_x86_64_gnu 0.42.0",
|
"windows_x86_64_gnu",
|
||||||
"windows_x86_64_gnullvm",
|
"windows_x86_64_gnullvm",
|
||||||
"windows_x86_64_msvc 0.42.0",
|
"windows_x86_64_msvc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.42.0"
|
version = "0.42.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
|
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.36.1"
|
version = "0.42.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.36.1"
|
version = "0.42.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.36.1"
|
version = "0.42.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.36.1"
|
version = "0.42.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.42.0"
|
version = "0.42.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
|
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.36.1"
|
version = "0.42.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaml-rust"
|
name = "yaml-rust"
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nostr-rs-relay"
|
name = "nostr-rs-relay"
|
||||||
version = "0.7.5"
|
version = "0.7.17"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Greg Heartsfield <scsibug@imap.cc>"]
|
authors = ["Greg Heartsfield <scsibug@imap.cc>"]
|
||||||
description = "A relay implementation for the Nostr protocol"
|
description = "A relay implementation for the Nostr protocol"
|
||||||
@@ -12,6 +12,7 @@ keywords = ["nostr", "server"]
|
|||||||
categories = ["network-programming", "web-programming"]
|
categories = ["network-programming", "web-programming"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = { version = "4.0.32", features = ["env", "default", "derive"]}
|
||||||
tracing = "0.1.36"
|
tracing = "0.1.36"
|
||||||
tracing-subscriber = "0.2.0"
|
tracing-subscriber = "0.2.0"
|
||||||
tokio = { version = "1", features = ["full", "tracing", "signal"] }
|
tokio = { version = "1", features = ["full", "tracing", "signal"] }
|
||||||
@@ -28,7 +29,7 @@ secp256k1 = {version = "0.21", features = ["rand", "rand-std", "serde", "bitcoin
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = {version = "1.0", features = ["preserve_order"]}
|
serde_json = {version = "1.0", features = ["preserve_order"]}
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
rusqlite = { version = "0.26", features = ["limits","bundled"]}
|
rusqlite = { version = "0.26", features = ["limits","bundled","modern_sqlite", "trace"]}
|
||||||
r2d2 = "0.8"
|
r2d2 = "0.8"
|
||||||
r2d2_sqlite = "0.19"
|
r2d2_sqlite = "0.19"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM docker.io/library/rust:1.66.0@sha256:359949280cebefe93ccb33089fe25111a3aadfe99eac4b6cbe8ec3e1b571dacb as builder
|
FROM docker.io/library/rust:1.66.0 as builder
|
||||||
|
|
||||||
RUN USER=root cargo install cargo-auditable
|
RUN USER=root cargo install cargo-auditable
|
||||||
RUN USER=root cargo new --bin nostr-rs-relay
|
RUN USER=root cargo new --bin nostr-rs-relay
|
||||||
@@ -17,7 +17,7 @@ COPY ./src ./src
|
|||||||
RUN rm ./target/release/deps/nostr*relay*
|
RUN rm ./target/release/deps/nostr*relay*
|
||||||
RUN cargo auditable build --release --locked
|
RUN cargo auditable build --release --locked
|
||||||
|
|
||||||
FROM docker.io/library/debian:bullseye-20221205-slim@sha256:25f10b4f1ded5341a3ca0a30290ff3cd5639415f0c5a2222d5e7d5dd72952aa1
|
FROM docker.io/library/debian:bullseye-slim
|
||||||
|
|
||||||
ARG APP=/usr/src/app
|
ARG APP=/usr/src/app
|
||||||
ARG APP_DATA=/usr/src/app/db
|
ARG APP_DATA=/usr/src/app/db
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
|
|||||||
- [x] NIP-16: [Event Treatment](https://github.com/nostr-protocol/nips/blob/master/16.md)
|
- [x] NIP-16: [Event Treatment](https://github.com/nostr-protocol/nips/blob/master/16.md)
|
||||||
- [x] NIP-20: [Command Results](https://github.com/nostr-protocol/nips/blob/master/20.md)
|
- [x] NIP-20: [Command Results](https://github.com/nostr-protocol/nips/blob/master/20.md)
|
||||||
- [x] NIP-22: [Event `created_at` limits](https://github.com/nostr-protocol/nips/blob/master/22.md) (_future-dated events only_)
|
- [x] NIP-22: [Event `created_at` limits](https://github.com/nostr-protocol/nips/blob/master/22.md) (_future-dated events only_)
|
||||||
- [x] NIP-26: [Event Delegation](https://github.com/nostr-protocol/nips/blob/master/26.md)
|
- [ ] NIP-26: [Event Delegation](https://github.com/nostr-protocol/nips/blob/master/26.md) (_implemented, but currently disabled_)
|
||||||
|
- [x] NIP-28: [Public Chat](https://github.com/nostr-protocol/nips/blob/master/28.md)
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -81,6 +82,38 @@ Text Note [81cf...2652] from 296a...9b92 5 seconds ago
|
|||||||
A pre-built container is also available on DockerHub:
|
A pre-built container is also available on DockerHub:
|
||||||
https://hub.docker.com/r/scsibug/nostr-rs-relay
|
https://hub.docker.com/r/scsibug/nostr-rs-relay
|
||||||
|
|
||||||
|
## Build and Run (without Docker)
|
||||||
|
|
||||||
|
Building `nostr-rs-relay` requires an installation of Cargo & Rust: https://www.rust-lang.org/tools/install
|
||||||
|
|
||||||
|
Clone this repository, and then build a release version of the relay:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ git clone -q https://git.sr.ht/\~gheartsfield/nostr-rs-relay
|
||||||
|
$ cd nostr-rs-relay
|
||||||
|
$ cargo build -q -r
|
||||||
|
```
|
||||||
|
|
||||||
|
The relay executable is now located in
|
||||||
|
`target/release/nostr-rs-relay`. In order to run it with logging
|
||||||
|
enabled, execute it with the `RUST_LOG` variable set:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ RUST_LOG=warn,nostr_rs_relay=info ./target/release/nostr-rs-relay
|
||||||
|
Dec 26 10:31:56.455 INFO nostr_rs_relay: Starting up from main
|
||||||
|
Dec 26 10:31:56.464 INFO nostr_rs_relay::server: listening on: 0.0.0.0:8080
|
||||||
|
Dec 26 10:31:56.466 INFO nostr_rs_relay::server: db writer created
|
||||||
|
Dec 26 10:31:56.466 INFO nostr_rs_relay::db: Built a connection pool "event writer" (min=1, max=2)
|
||||||
|
Dec 26 10:31:56.466 INFO nostr_rs_relay::db: opened database "./nostr.db" for writing
|
||||||
|
Dec 26 10:31:56.466 INFO nostr_rs_relay::schema: DB version = 11
|
||||||
|
Dec 26 10:31:56.467 INFO nostr_rs_relay::db: Built a connection pool "maintenance writer" (min=1, max=2)
|
||||||
|
Dec 26 10:31:56.467 INFO nostr_rs_relay::server: control message listener started
|
||||||
|
Dec 26 10:31:56.468 INFO nostr_rs_relay::db: Built a connection pool "client query" (min=4, max=8)
|
||||||
|
```
|
||||||
|
|
||||||
|
You now have a running relay, on port `8080`. Use a `nostr` client or
|
||||||
|
`websocat` to connect and send/query for events.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The sample [`config.toml`](config.toml) file demonstrates the
|
The sample [`config.toml`](config.toml) file demonstrates the
|
||||||
@@ -115,3 +148,8 @@ To chat about `nostr-rs-relay` on `nostr` itself; visit our channel on [anigma](
|
|||||||
License
|
License
|
||||||
---
|
---
|
||||||
This project is MIT licensed.
|
This project is MIT licensed.
|
||||||
|
|
||||||
|
External Documentation and Links
|
||||||
|
---
|
||||||
|
|
||||||
|
* [BlockChainCaffe's Nostr Relay Setup Guide](https://github.com/BlockChainCaffe/Nostr-Relay-Setup-Guide)
|
||||||
|
|||||||
+27
-5
@@ -18,7 +18,7 @@ description = "A newly created nostr-rs-relay.\n\nCustomize this with your own i
|
|||||||
|
|
||||||
[diagnostics]
|
[diagnostics]
|
||||||
# Enable tokio tracing (for use with tokio-console)
|
# Enable tokio tracing (for use with tokio-console)
|
||||||
#tracing = true
|
#tracing = false
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
# Directory for SQLite files. Defaults to the current directory. Can
|
# Directory for SQLite files. Defaults to the current directory. Can
|
||||||
@@ -36,8 +36,9 @@ data_directory = "."
|
|||||||
# Minimum number of SQLite reader connections
|
# Minimum number of SQLite reader connections
|
||||||
#min_conn = 4
|
#min_conn = 4
|
||||||
|
|
||||||
# Maximum number of SQLite reader connections
|
# Maximum number of SQLite reader connections. Recommend setting this
|
||||||
#max_conn = 128
|
# to approx the number of cores.
|
||||||
|
#max_conn = 8
|
||||||
|
|
||||||
[network]
|
[network]
|
||||||
# Bind to this network address
|
# Bind to this network address
|
||||||
@@ -62,8 +63,24 @@ reject_future_seconds = 1800
|
|||||||
|
|
||||||
[limits]
|
[limits]
|
||||||
# Limit events created per second, averaged over one minute. Must be
|
# Limit events created per second, averaged over one minute. Must be
|
||||||
# an integer. If not set (or set to 0), defaults to unlimited.
|
# an integer. If not set (or set to 0), defaults to unlimited. Note:
|
||||||
#messages_per_sec = 0
|
# this is for the server as a whole, not per-connection.
|
||||||
|
# messages_per_sec = 0
|
||||||
|
|
||||||
|
# Limit client subscriptions created per second, averaged over one
|
||||||
|
# minute. Must be an integer. If not set (or set to 0), defaults to
|
||||||
|
# unlimited.
|
||||||
|
#subscriptions_per_min = 0
|
||||||
|
|
||||||
|
# UNIMPLEMENTED...
|
||||||
|
# Limit how many concurrent database connections a client can have.
|
||||||
|
# This prevents a single client from starting too many expensive
|
||||||
|
# database queries. Must be an integer. If not set (or set to 0),
|
||||||
|
# defaults to unlimited (subject to subscription limits).
|
||||||
|
#db_conns_per_client = 0
|
||||||
|
|
||||||
|
# Limit blocking threads used for database connections. Defaults to 16.
|
||||||
|
#max_blocking_threads = 16
|
||||||
|
|
||||||
# Limit the maximum size of an EVENT message. Defaults to 128 KB.
|
# Limit the maximum size of an EVENT message. Defaults to 128 KB.
|
||||||
# Set to 0 for unlimited.
|
# Set to 0 for unlimited.
|
||||||
@@ -83,6 +100,11 @@ reject_future_seconds = 1800
|
|||||||
# backpressure to senders if writes are slow.
|
# backpressure to senders if writes are slow.
|
||||||
#event_persist_buffer = 4096
|
#event_persist_buffer = 4096
|
||||||
|
|
||||||
|
# Event kind blacklist. Events with these kinds will be discarded.
|
||||||
|
#event_kind_blacklist = [
|
||||||
|
# 70202,
|
||||||
|
#]
|
||||||
|
|
||||||
[authorization]
|
[authorization]
|
||||||
# Pubkey addresses in this array are whitelisted for event publishing.
|
# Pubkey addresses in this array are whitelisted for event publishing.
|
||||||
# Only valid events by these authors will be accepted, if the variable
|
# Only valid events by these authors will be accepted, if the variable
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Database Maintenance
|
||||||
|
|
||||||
|
`nostr-rs-relay` uses the SQLite embedded database to minimize
|
||||||
|
dependencies and overall footprint of running a relay. If traffic is
|
||||||
|
light, the relay should just run with very little need for
|
||||||
|
intervention. For heavily trafficked relays, there are a number of
|
||||||
|
steps that the operator may need to take to maintain performance and
|
||||||
|
limit disk usage.
|
||||||
|
|
||||||
|
This maintenance guide is current as of version `0.7.14`. Future
|
||||||
|
versions may incorporate and automate some of these steps.
|
||||||
|
|
||||||
|
## Backing Up the Database
|
||||||
|
|
||||||
|
To prevent data loss, the database should be backed up regularly. The
|
||||||
|
recommended method is to use the `sqlite3` command to perform an
|
||||||
|
"Online Backup". This can be done while the relay is running, queries
|
||||||
|
can still run and events will be persisted during the backup.
|
||||||
|
|
||||||
|
The following commands will perform a backup of the database to a
|
||||||
|
dated file, and then compress to minimize size:
|
||||||
|
|
||||||
|
```console
|
||||||
|
BACKUP_FILE=/var/backups/nostr/`date +%Y%m%d_%H%M`.db
|
||||||
|
sqlite3 -readonly /apps/nostr-relay/nostr.db ".backup $BACKUP_FILE
|
||||||
|
sqlite3 $BACKUP_FILE "vacuum;"
|
||||||
|
bzip2 -9 $BACKUP_FILE
|
||||||
|
```
|
||||||
|
|
||||||
|
Nostr events are very compressible. Expect a compression ratio on the
|
||||||
|
order of 4:1, resulting in a 75% space saving.
|
||||||
|
|
||||||
|
## Vacuuming the Database
|
||||||
|
|
||||||
|
As the database is updated, it can become fragmented. Performing a
|
||||||
|
full `vacuum` will rebuild the entire database file, and can reduce
|
||||||
|
space. Running this may reduce the size of the database file,
|
||||||
|
especially if a large amount of data was updated or deleted.
|
||||||
|
|
||||||
|
```console
|
||||||
|
vacuum;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clearing Hidden Events
|
||||||
|
|
||||||
|
When events are deleted, either through deletion events, metadata or
|
||||||
|
follower updates, or a replaceable event kind, the event is not
|
||||||
|
actually removed from the database. Instead, a flag `HIDDEN` is set
|
||||||
|
to true for the event, which excludes it from search results. The
|
||||||
|
original intent was to ensure that subsequent rebroadcasts of the
|
||||||
|
event would be easily detected as having been deleted, and would not
|
||||||
|
need to be stored again. In practice, this decision causes excessive
|
||||||
|
growth of the `tags` table, since all the previous followers are
|
||||||
|
retained for those `HIDDEN` events.
|
||||||
|
|
||||||
|
The `event` and especially the `tag` table can be significantly
|
||||||
|
reduced in size by running these commands:
|
||||||
|
|
||||||
|
```console
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
delete from event where HIDDEN=true;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manually Removing Events
|
||||||
|
|
||||||
|
For a variety of reasons, an operator may wish to remove some events
|
||||||
|
from the database. The only way of achieving this today is with
|
||||||
|
manually run SQL commands.
|
||||||
|
|
||||||
|
It is recommended to have a good backup prior to manually running SQL
|
||||||
|
commands!
|
||||||
|
|
||||||
|
In all cases, it is mandatory to enable foreign keys, and this must be
|
||||||
|
done for every connection. Otherwise, you will likely orphan rows in
|
||||||
|
the `tag` table.
|
||||||
|
|
||||||
|
### Deleting Specific Event
|
||||||
|
|
||||||
|
```console
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
delete from event where event_hash=x'00000000000c1271675dc86e3e1dd1336827bccabb90dc4c9d3b4465efefe00e';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deleting All Events for Pubkey
|
||||||
|
|
||||||
|
```console
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
delete from event where author=x'000000000002c7831d9c5a99f183afc2813a6f69a16edda7f6fc0ed8110566e6';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deleting All Events of a Kind
|
||||||
|
|
||||||
|
|
||||||
|
```console
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
delete from event where kind=70202;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deleting Old Events
|
||||||
|
|
||||||
|
In this scenario, we wish to delete any event that has been stored by
|
||||||
|
our relay for more than 1 month. Crucially, this is based on when the
|
||||||
|
event was stored, not when the event says it was created. If an event
|
||||||
|
has a `created` field of 2 years ago, but was first sent to our relay
|
||||||
|
yesterday, it would not be deleted in this scenario. Keep in mind, we
|
||||||
|
do not track anything for re-broadcast events that we already have, so
|
||||||
|
this is not a very effective way of implementing a "least recently
|
||||||
|
seen" policy.
|
||||||
|
|
||||||
|
```console
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
TODO!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Profile Events with No Recent Events
|
||||||
|
|
||||||
|
Many users create profiles, post a "hello world" event, and then never
|
||||||
|
appear again (likely using an ephemeral keypair that was lost in the
|
||||||
|
browser cache). We can find these accounts and remove them after some
|
||||||
|
time.
|
||||||
|
|
||||||
|
```console
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
TODO!
|
||||||
|
```
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
sed -E 's/@sha256:[[:alnum:]]+//g' Dockerfile > Dockerfile.any-platform
|
|
||||||
echo "Created platform-agnostic Dockerfile in 'Dockerfile.any-platform'"
|
|
||||||
+19
-2
@@ -68,8 +68,25 @@ http {
|
|||||||
server_name relay.example.com;
|
server_name relay.example.com;
|
||||||
ssl_certificate /etc/letsencrypt/live/relay.example.com/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/relay.example.com/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/relay.example.com/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/relay.example.com/privkey.pem;
|
||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
ssl_protocols TLSv1.3 TLSv1.2;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ecdh_curve secp521r1:secp384r1;
|
||||||
|
ssl_ciphers EECDH+AESGCM:EECDH+AES256;
|
||||||
|
|
||||||
|
# Optional Diffie-Helmann parameters
|
||||||
|
# Generate with openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
|
||||||
|
#ssl_dhparam /etc/ssl/certs/dhparam.pem;
|
||||||
|
|
||||||
|
ssl_session_cache shared:TLS:2m;
|
||||||
|
ssl_buffer_size 4k;
|
||||||
|
|
||||||
|
# OCSP stapling
|
||||||
|
ssl_stapling on;
|
||||||
|
ssl_stapling_verify on;
|
||||||
|
resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001]; # Cloudflare
|
||||||
|
|
||||||
|
# Set HSTS to 365 days
|
||||||
|
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload' always;
|
||||||
keepalive_timeout 70;
|
keepalive_timeout 70;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
#max_width = 140
|
||||||
|
#chain_width = 100
|
||||||
|
#fn_call_width = 100
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
use nostr_rs_relay::utils::is_lower_hex;
|
||||||
|
use tracing::*;
|
||||||
|
use nostr_rs_relay::config;
|
||||||
|
use nostr_rs_relay::event::{Event,single_char_tagname};
|
||||||
|
use nostr_rs_relay::error::{Error, Result};
|
||||||
|
use nostr_rs_relay::db::build_pool;
|
||||||
|
use nostr_rs_relay::schema::{curr_db_version, DB_VERSION};
|
||||||
|
use rusqlite::{OpenFlags, Transaction};
|
||||||
|
use nostr_rs_relay::db::PooledConnection;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
use rusqlite::params;
|
||||||
|
|
||||||
|
/// Bulk load JSONL data from STDIN to the database specified in config.toml (or ./nostr.db as a default).
|
||||||
|
/// The database must already exist, this will not create a new one.
|
||||||
|
/// Tested against schema v13.
|
||||||
|
|
||||||
|
pub fn main() -> Result<()> {
|
||||||
|
let _trace_sub = tracing_subscriber::fmt::try_init();
|
||||||
|
println!("Nostr-rs-relay Bulk Loader");
|
||||||
|
// check for a database file, or create one.
|
||||||
|
let settings = config::Settings::new();
|
||||||
|
if !Path::new(&settings.database.data_directory).is_dir() {
|
||||||
|
info!("Database directory does not exist");
|
||||||
|
return Err(Error::DatabaseDirError);
|
||||||
|
}
|
||||||
|
// Get a database pool
|
||||||
|
let pool = build_pool("bulk-loader", &settings, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE, 1,4,false);
|
||||||
|
{
|
||||||
|
// check for database schema version
|
||||||
|
let mut conn: PooledConnection = pool.get()?;
|
||||||
|
let version = curr_db_version(&mut conn)?;
|
||||||
|
info!("current version is: {:?}", version);
|
||||||
|
// ensure the schema version is current.
|
||||||
|
if version != DB_VERSION {
|
||||||
|
info!("version is not current, exiting");
|
||||||
|
panic!("cannot write to schema other than v{}", DB_VERSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// this channel will contain parsed events ready to be inserted
|
||||||
|
let (event_tx, event_rx) = mpsc::sync_channel(100_000);
|
||||||
|
// Thread for reading events
|
||||||
|
let _stdin_reader_handler = thread::spawn(move || {
|
||||||
|
let stdin = io::stdin();
|
||||||
|
for readline in stdin.lines() {
|
||||||
|
if let Ok(line) = readline {
|
||||||
|
// try to parse a nostr event
|
||||||
|
let eres: Result<Event, serde_json::Error> = serde_json::from_str(&line);
|
||||||
|
if let Ok(mut e) = eres {
|
||||||
|
if let Ok(()) = e.validate() {
|
||||||
|
e.build_index();
|
||||||
|
//debug!("Event: {:?}", e);
|
||||||
|
event_tx.send(Some(e)).ok();
|
||||||
|
} else {
|
||||||
|
info!("could not validate event");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("error reading event: {:?}", eres);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// error reading
|
||||||
|
info!("error reading: {:?}", readline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("finished parsing events");
|
||||||
|
event_tx.send(None).ok();
|
||||||
|
let ok: Result<()> = Ok(());
|
||||||
|
return ok;
|
||||||
|
});
|
||||||
|
let mut conn: PooledConnection = pool.get()?;
|
||||||
|
let mut events_read = 0;
|
||||||
|
let event_batch_size =50_000;
|
||||||
|
let mut new_events = 0;
|
||||||
|
let mut has_more_events = true;
|
||||||
|
while has_more_events {
|
||||||
|
// begin a transaction
|
||||||
|
let tx = conn.transaction()?;
|
||||||
|
// read in batch_size events and commit
|
||||||
|
for _ in 0..event_batch_size {
|
||||||
|
match event_rx.recv() {
|
||||||
|
Ok(Some(e)) => {
|
||||||
|
events_read += 1;
|
||||||
|
// ignore ephemeral events
|
||||||
|
if !(e.kind >= 20000 && e.kind < 30000) {
|
||||||
|
match write_event(&tx, e) {
|
||||||
|
Ok(c) => {
|
||||||
|
new_events += c;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
info!("error inserting event: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(None) => {
|
||||||
|
// signal that the sender will never produce more
|
||||||
|
// events
|
||||||
|
has_more_events=false;
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
info!("sender is closed");
|
||||||
|
// sender is done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("committed {} events...", new_events);
|
||||||
|
tx.commit()?;
|
||||||
|
conn.execute_batch("pragma wal_checkpoint(truncate)")?;
|
||||||
|
|
||||||
|
}
|
||||||
|
info!("processed {} events", events_read);
|
||||||
|
info!("stored {} new events", new_events);
|
||||||
|
// get a connection for writing events
|
||||||
|
// read standard in.
|
||||||
|
info!("finished reading input");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write an event and update the tag table.
|
||||||
|
/// Assumes the event has its index built.
|
||||||
|
fn write_event(tx: &Transaction, e: Event) -> Result<usize> {
|
||||||
|
let id_blob = hex::decode(&e.id).ok();
|
||||||
|
let pubkey_blob: Option<Vec<u8>> = hex::decode(&e.pubkey).ok();
|
||||||
|
let delegator_blob: Option<Vec<u8>> = e.delegated_by.as_ref().and_then(|d| hex::decode(d).ok());
|
||||||
|
let event_str = serde_json::to_string(&e).ok();
|
||||||
|
// ignore if the event hash is a duplicate.
|
||||||
|
let ins_count = tx.execute(
|
||||||
|
"INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, delegated_by, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, ?6, strftime('%s','now'), FALSE);",
|
||||||
|
params![id_blob, e.created_at, e.kind, pubkey_blob, delegator_blob, event_str]
|
||||||
|
)?;
|
||||||
|
if ins_count == 0 {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
// we want to capture the event_id that had the tag, the tag name, and the tag hex value.
|
||||||
|
let event_id = tx.last_insert_rowid();
|
||||||
|
// look at each event, and each tag, creating new tag entries if appropriate.
|
||||||
|
for t in e.tags.iter().filter(|x| x.len() > 1) {
|
||||||
|
let tagname = t.get(0).unwrap();
|
||||||
|
let tagnamechar_opt = single_char_tagname(tagname);
|
||||||
|
if tagnamechar_opt.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// safe because len was > 1
|
||||||
|
let tagval = t.get(1).unwrap();
|
||||||
|
// insert as BLOB if we can restore it losslessly.
|
||||||
|
// this means it needs to be even length and lowercase.
|
||||||
|
if (tagval.len() % 2 == 0) && is_lower_hex(tagval) {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3);",
|
||||||
|
params![event_id, tagname, hex::decode(tagval).ok()],
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
// otherwise, insert as text
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO tag (event_id, name, value) VALUES (?1, ?2, ?3);",
|
||||||
|
params![event_id, tagname, &tagval],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.is_replaceable() {
|
||||||
|
//let query = "SELECT id FROM event WHERE kind=? AND author=? ORDER BY created_at DESC LIMIT 1;";
|
||||||
|
//let count: usize = tx.query_row(query, params![e.kind, pubkey_blob], |row| row.get(0))?;
|
||||||
|
//info!("found {} rows that /would/ be preserved", count);
|
||||||
|
match tx.execute(
|
||||||
|
"DELETE FROM event WHERE kind=? and author=? and id NOT IN (SELECT id FROM event WHERE kind=? AND author=? ORDER BY created_at DESC LIMIT 1);",
|
||||||
|
params![e.kind, pubkey_blob, e.kind, pubkey_blob],
|
||||||
|
) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(x) => {info!("error deleting replaceable event: {:?}",x);}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ins_count)
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(about = "A nostr relay written in Rust", author = env!("CARGO_PKG_AUTHORS"), version = env!("CARGO_PKG_VERSION"))]
|
||||||
|
pub struct CLIArgs {
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "Use the <directory> as the location of the database",
|
||||||
|
default_value = ".",
|
||||||
|
required = false
|
||||||
|
)]
|
||||||
|
pub db: String,
|
||||||
|
}
|
||||||
+9
-1
@@ -52,11 +52,15 @@ pub struct Retention {
|
|||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct Limits {
|
pub struct Limits {
|
||||||
pub messages_per_sec: Option<u32>, // Artificially slow down event writing to limit disk consumption (averaged over 1 minute)
|
pub messages_per_sec: Option<u32>, // Artificially slow down event writing to limit disk consumption (averaged over 1 minute)
|
||||||
|
pub subscriptions_per_min: Option<u32>, // Artificially slow down request (db query) creation to prevent abuse (averaged over 1 minute)
|
||||||
|
pub db_conns_per_client: Option<u32>, // How many concurrent database queries (not subscriptions) may a client have?
|
||||||
|
pub max_blocking_threads: usize,
|
||||||
pub max_event_bytes: Option<usize>, // Maximum size of an EVENT message
|
pub max_event_bytes: Option<usize>, // Maximum size of an EVENT message
|
||||||
pub max_ws_message_bytes: Option<usize>,
|
pub max_ws_message_bytes: Option<usize>,
|
||||||
pub max_ws_frame_bytes: Option<usize>,
|
pub max_ws_frame_bytes: Option<usize>,
|
||||||
pub broadcast_buffer: usize, // events to buffer for subscribers (prevents slow readers from consuming memory)
|
pub broadcast_buffer: usize, // events to buffer for subscribers (prevents slow readers from consuming memory)
|
||||||
pub event_persist_buffer: usize, // events to buffer for database commits (block senders if database writes are too slow)
|
pub event_persist_buffer: usize, // events to buffer for database commits (block senders if database writes are too slow)
|
||||||
|
pub event_kind_blacklist: Option<Vec<u64>>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -204,7 +208,7 @@ impl Default for Settings {
|
|||||||
data_directory: ".".to_owned(),
|
data_directory: ".".to_owned(),
|
||||||
in_memory: false,
|
in_memory: false,
|
||||||
min_conn: 4,
|
min_conn: 4,
|
||||||
max_conn: 128,
|
max_conn: 8,
|
||||||
},
|
},
|
||||||
network: Network {
|
network: Network {
|
||||||
port: 8080,
|
port: 8080,
|
||||||
@@ -214,11 +218,15 @@ impl Default for Settings {
|
|||||||
},
|
},
|
||||||
limits: Limits {
|
limits: Limits {
|
||||||
messages_per_sec: None,
|
messages_per_sec: None,
|
||||||
|
subscriptions_per_min: None,
|
||||||
|
db_conns_per_client: None,
|
||||||
|
max_blocking_threads: 16,
|
||||||
max_event_bytes: Some(2 << 17), // 128K
|
max_event_bytes: Some(2 << 17), // 128K
|
||||||
max_ws_message_bytes: Some(2 << 17), // 128K
|
max_ws_message_bytes: Some(2 << 17), // 128K
|
||||||
max_ws_frame_bytes: Some(2 << 17), // 128K
|
max_ws_frame_bytes: Some(2 << 17), // 128K
|
||||||
broadcast_buffer: 16384,
|
broadcast_buffer: 16384,
|
||||||
event_persist_buffer: 4096,
|
event_persist_buffer: 4096,
|
||||||
|
event_kind_blacklist: None,
|
||||||
},
|
},
|
||||||
authorization: Authorization {
|
authorization: Authorization {
|
||||||
pubkey_whitelist: None, // Allow any address to publish
|
pubkey_whitelist: None, // Allow any address to publish
|
||||||
|
|||||||
+10
-5
@@ -5,7 +5,7 @@ use crate::error::Result;
|
|||||||
|
|
||||||
use crate::subscription::Subscription;
|
use crate::subscription::Subscription;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, trace};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// A subscription identifier has a maximum length
|
/// A subscription identifier has a maximum length
|
||||||
@@ -46,6 +46,11 @@ impl ClientConn {
|
|||||||
&self.subscriptions
|
&self.subscriptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the given subscription already exists
|
||||||
|
pub fn has_subscription(&self, sub: &Subscription) -> bool {
|
||||||
|
self.subscriptions.values().any(|x| x == sub)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a short prefix of the client's unique identifier, suitable
|
/// Get a short prefix of the client's unique identifier, suitable
|
||||||
/// for logging.
|
/// for logging.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -69,7 +74,7 @@ impl ClientConn {
|
|||||||
// prevent arbitrarily long subscription identifiers from
|
// prevent arbitrarily long subscription identifiers from
|
||||||
// being used.
|
// being used.
|
||||||
if sub_id_len > MAX_SUBSCRIPTION_ID_LEN {
|
if sub_id_len > MAX_SUBSCRIPTION_ID_LEN {
|
||||||
info!(
|
debug!(
|
||||||
"ignoring sub request with excessive length: ({})",
|
"ignoring sub request with excessive length: ({})",
|
||||||
sub_id_len
|
sub_id_len
|
||||||
);
|
);
|
||||||
@@ -79,7 +84,7 @@ impl ClientConn {
|
|||||||
if self.subscriptions.contains_key(&k) {
|
if self.subscriptions.contains_key(&k) {
|
||||||
self.subscriptions.remove(&k);
|
self.subscriptions.remove(&k);
|
||||||
self.subscriptions.insert(k, s.clone());
|
self.subscriptions.insert(k, s.clone());
|
||||||
debug!(
|
trace!(
|
||||||
"replaced existing subscription (cid: {}, sub: {:?})",
|
"replaced existing subscription (cid: {}, sub: {:?})",
|
||||||
self.get_client_prefix(),
|
self.get_client_prefix(),
|
||||||
s.get_id()
|
s.get_id()
|
||||||
@@ -93,7 +98,7 @@ impl ClientConn {
|
|||||||
}
|
}
|
||||||
// add subscription
|
// add subscription
|
||||||
self.subscriptions.insert(k, s);
|
self.subscriptions.insert(k, s);
|
||||||
debug!(
|
trace!(
|
||||||
"registered new subscription, currently have {} active subs (cid: {})",
|
"registered new subscription, currently have {} active subs (cid: {})",
|
||||||
self.subscriptions.len(),
|
self.subscriptions.len(),
|
||||||
self.get_client_prefix(),
|
self.get_client_prefix(),
|
||||||
@@ -105,7 +110,7 @@ impl ClientConn {
|
|||||||
pub fn unsubscribe(&mut self, c: &Close) {
|
pub fn unsubscribe(&mut self, c: &Close) {
|
||||||
// TODO: return notice if subscription did not exist.
|
// TODO: return notice if subscription did not exist.
|
||||||
self.subscriptions.remove(&c.id);
|
self.subscriptions.remove(&c.id);
|
||||||
debug!(
|
trace!(
|
||||||
"removed subscription, currently have {} active subs (cid: {})",
|
"removed subscription, currently have {} active subs (cid: {})",
|
||||||
self.subscriptions.len(),
|
self.subscriptions.len(),
|
||||||
self.get_client_prefix(),
|
self.get_client_prefix(),
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ use r2d2_sqlite::SqliteConnectionManager;
|
|||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use rusqlite::types::ToSql;
|
use rusqlite::types::ToSql;
|
||||||
use rusqlite::OpenFlags;
|
use rusqlite::OpenFlags;
|
||||||
|
use tokio::sync::{Mutex, MutexGuard};
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -39,6 +41,9 @@ pub struct SubmittedEvent {
|
|||||||
/// Database file
|
/// Database file
|
||||||
pub const DB_FILE: &str = "nostr.db";
|
pub const DB_FILE: &str = "nostr.db";
|
||||||
|
|
||||||
|
/// How frequently to attempt checkpointing
|
||||||
|
pub const CHECKPOINT_FREQ_SEC: u64 = 60;
|
||||||
|
|
||||||
/// Build a database connection pool.
|
/// Build a database connection pool.
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
@@ -76,6 +81,7 @@ pub fn build_pool(
|
|||||||
.test_on_check_out(true) // no noticeable performance hit
|
.test_on_check_out(true) // no noticeable performance hit
|
||||||
.min_idle(Some(min_size))
|
.min_idle(Some(min_size))
|
||||||
.max_size(max_size)
|
.max_size(max_size)
|
||||||
|
.max_lifetime(Some(Duration::from_secs(30)))
|
||||||
.build(manager)
|
.build(manager)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
info!(
|
info!(
|
||||||
@@ -85,6 +91,56 @@ pub fn build_pool(
|
|||||||
pool
|
pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display database pool stats every 1 minute
|
||||||
|
pub async fn monitor_pool(name: &str, pool: SqlitePool) {
|
||||||
|
let sleep_dur = Duration::from_secs(60);
|
||||||
|
loop {
|
||||||
|
log_pool_stats(name, &pool);
|
||||||
|
tokio::time::sleep(sleep_dur).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Perform normal maintenance
|
||||||
|
pub fn optimize_db(conn: &mut PooledConnection) -> Result<()> {
|
||||||
|
let start = Instant::now();
|
||||||
|
conn.execute_batch("PRAGMA optimize;")?;
|
||||||
|
info!("optimize ran in {:?}", start.elapsed());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum SqliteStatus {
|
||||||
|
Ok,
|
||||||
|
Busy,
|
||||||
|
Error,
|
||||||
|
Other(u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checkpoint/Truncate WAL. Returns the number of WAL pages remaining.
|
||||||
|
pub fn checkpoint_db(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
|
let query = "PRAGMA wal_checkpoint(TRUNCATE);";
|
||||||
|
let start = Instant::now();
|
||||||
|
let (cp_result, wal_size, _frames_checkpointed) = conn.query_row(query, [], |row| {
|
||||||
|
let checkpoint_result: u64 = row.get(0)?;
|
||||||
|
let wal_size: u64 = row.get(1)?;
|
||||||
|
let frames_checkpointed: u64 = row.get(2)?;
|
||||||
|
Ok((checkpoint_result, wal_size, frames_checkpointed))
|
||||||
|
})?;
|
||||||
|
let result = match cp_result {
|
||||||
|
0 => SqliteStatus::Ok,
|
||||||
|
1 => SqliteStatus::Busy,
|
||||||
|
2 => SqliteStatus::Error,
|
||||||
|
x => SqliteStatus::Other(x),
|
||||||
|
};
|
||||||
|
info!(
|
||||||
|
"checkpoint ran in {:?} (result: {:?}, WAL size: {})",
|
||||||
|
start.elapsed(),
|
||||||
|
result,
|
||||||
|
wal_size
|
||||||
|
);
|
||||||
|
Ok(wal_size as usize)
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn a database writer that persists events to the SQLite store.
|
/// Spawn a database writer that persists events to the SQLite store.
|
||||||
pub async fn db_writer(
|
pub async fn db_writer(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
@@ -107,7 +163,7 @@ pub async fn db_writer(
|
|||||||
&settings,
|
&settings,
|
||||||
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
|
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
|
||||||
1,
|
1,
|
||||||
4,
|
2,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
if settings.database.in_memory {
|
if settings.database.in_memory {
|
||||||
@@ -154,8 +210,8 @@ pub async fn db_writer(
|
|||||||
// TODO: incorporate delegated pubkeys
|
// TODO: incorporate delegated pubkeys
|
||||||
// if the event address is not in allowed_addrs.
|
// if the event address is not in allowed_addrs.
|
||||||
if !allowed_addrs.contains(&event.pubkey) {
|
if !allowed_addrs.contains(&event.pubkey) {
|
||||||
info!(
|
debug!(
|
||||||
"Rejecting event {}, unauthorized author",
|
"rejecting event: {}, unauthorized author",
|
||||||
event.get_event_id_prefix()
|
event.get_event_id_prefix()
|
||||||
);
|
);
|
||||||
notice_tx
|
notice_tx
|
||||||
@@ -168,6 +224,25 @@ pub async fn db_writer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that event kind isn't blacklisted
|
||||||
|
let kinds_blacklist = &settings.limits.event_kind_blacklist.clone();
|
||||||
|
if let Some(event_kind_blacklist) = kinds_blacklist {
|
||||||
|
if event_kind_blacklist.contains(&event.kind) {
|
||||||
|
debug!(
|
||||||
|
"rejecting event: {}, blacklisted kind: {}",
|
||||||
|
&event.get_event_id_prefix(),
|
||||||
|
&event.kind
|
||||||
|
);
|
||||||
|
notice_tx
|
||||||
|
.try_send(Notice::blocked(
|
||||||
|
event.id,
|
||||||
|
"event kind is blocked by relay"
|
||||||
|
))
|
||||||
|
.ok();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// send any metadata events to the NIP-05 verifier
|
// send any metadata events to the NIP-05 verifier
|
||||||
if nip05_active && event.is_kind_metadata() {
|
if nip05_active && event.is_kind_metadata() {
|
||||||
// we are sending this prior to even deciding if we
|
// we are sending this prior to even deciding if we
|
||||||
@@ -188,7 +263,8 @@ pub async fn db_writer(
|
|||||||
event.get_author_prefix()
|
event.get_author_prefix()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
info!("rejecting event, author ({:?} / {:?}) verification invalid (expired/wrong domain)",
|
info!(
|
||||||
|
"rejecting event, author ({:?} / {:?}) verification invalid (expired/wrong domain)",
|
||||||
uv.name.to_string(),
|
uv.name.to_string(),
|
||||||
event.get_author_prefix()
|
event.get_author_prefix()
|
||||||
);
|
);
|
||||||
@@ -239,8 +315,9 @@ pub async fn db_writer(
|
|||||||
notice_tx.try_send(Notice::duplicate(event.id)).ok();
|
notice_tx.try_send(Notice::duplicate(event.id)).ok();
|
||||||
} else {
|
} else {
|
||||||
info!(
|
info!(
|
||||||
"persisted event: {:?} from: {:?} in: {:?}",
|
"persisted event: {:?} (kind: {}) from: {:?} in: {:?}",
|
||||||
event.get_event_id_prefix(),
|
event.get_event_id_prefix(),
|
||||||
|
event.kind,
|
||||||
event.get_author_prefix(),
|
event.get_author_prefix(),
|
||||||
start.elapsed()
|
start.elapsed()
|
||||||
);
|
);
|
||||||
@@ -288,6 +365,9 @@ pub async fn db_writer(
|
|||||||
|
|
||||||
/// Persist an event to the database, returning rows added.
|
/// Persist an event to the database, returning rows added.
|
||||||
pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> {
|
pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> {
|
||||||
|
// enable auto vacuum
|
||||||
|
conn.execute_batch("pragma auto_vacuum = FULL")?;
|
||||||
|
|
||||||
// start transaction
|
// start transaction
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
// get relevant fields from event and convert to blobs.
|
// get relevant fields from event and convert to blobs.
|
||||||
@@ -295,6 +375,15 @@ pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> {
|
|||||||
let pubkey_blob: Option<Vec<u8>> = hex::decode(&e.pubkey).ok();
|
let pubkey_blob: Option<Vec<u8>> = hex::decode(&e.pubkey).ok();
|
||||||
let delegator_blob: Option<Vec<u8>> = e.delegated_by.as_ref().and_then(|d| hex::decode(d).ok());
|
let delegator_blob: Option<Vec<u8>> = e.delegated_by.as_ref().and_then(|d| hex::decode(d).ok());
|
||||||
let event_str = serde_json::to_string(&e).ok();
|
let event_str = serde_json::to_string(&e).ok();
|
||||||
|
// check for replaceable events that would hide this one; we won't even attempt to insert these.
|
||||||
|
if e.is_replaceable() {
|
||||||
|
let repl_count = tx.query_row(
|
||||||
|
"SELECT e.id FROM event e INDEXED BY author_index WHERE e.author=? AND e.kind=? AND e.created_at > ? LIMIT 1;",
|
||||||
|
params![pubkey_blob, e.kind, e.created_at], |row| row.get::<usize, usize>(0));
|
||||||
|
if repl_count.ok().is_some() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
// ignore if the event hash is a duplicate.
|
// ignore if the event hash is a duplicate.
|
||||||
let mut ins_count = tx.execute(
|
let mut ins_count = tx.execute(
|
||||||
"INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, delegated_by, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, ?6, strftime('%s','now'), FALSE);",
|
"INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, delegated_by, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, ?6, strftime('%s','now'), FALSE);",
|
||||||
@@ -302,7 +391,8 @@ pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> {
|
|||||||
)?;
|
)?;
|
||||||
if ins_count == 0 {
|
if ins_count == 0 {
|
||||||
// if the event was a duplicate, no need to insert event or
|
// if the event was a duplicate, no need to insert event or
|
||||||
// pubkey references. This will abort the txn.
|
// pubkey references.
|
||||||
|
tx.rollback().ok();
|
||||||
return Ok(ins_count);
|
return Ok(ins_count);
|
||||||
}
|
}
|
||||||
// remember primary key of the event most recently inserted.
|
// remember primary key of the event most recently inserted.
|
||||||
@@ -334,17 +424,19 @@ pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if this event is replaceable update, hide every other replaceable
|
// if this event is replaceable update, remove other replaceable
|
||||||
// event with the same kind from the same author that was issued
|
// event with the same kind from the same author that was issued
|
||||||
// earlier than this.
|
// earlier than this.
|
||||||
if e.kind == 0 || e.kind == 3 || (e.kind >= 10000 && e.kind < 20000) {
|
if e.is_replaceable() {
|
||||||
|
let author = hex::decode(&e.pubkey).ok();
|
||||||
|
// this is a backwards check - hide any events that were older.
|
||||||
let update_count = tx.execute(
|
let update_count = tx.execute(
|
||||||
"UPDATE event SET hidden=TRUE WHERE id!=? AND kind=? AND author=? AND created_at <= ? and hidden!=TRUE",
|
"DELETE FROM event WHERE kind=? and author=? and id NOT IN (SELECT id FROM event INDEXED BY author_kind_index WHERE kind=? AND author=? ORDER BY created_at DESC LIMIT 1)",
|
||||||
params![ev_id, e.kind, hex::decode(&e.pubkey).ok(), e.created_at],
|
params![e.kind, author, e.kind, author],
|
||||||
)?;
|
)?;
|
||||||
if update_count > 0 {
|
if update_count > 0 {
|
||||||
info!(
|
info!(
|
||||||
"hid {} older replaceable kind {} events for author: {:?}",
|
"removed {} older replaceable kind {} events for author: {:?}",
|
||||||
update_count,
|
update_count,
|
||||||
e.kind,
|
e.kind,
|
||||||
e.get_author_prefix()
|
e.get_author_prefix()
|
||||||
@@ -419,8 +511,40 @@ fn repeat_vars(count: usize) -> String {
|
|||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a dynamic SQL subquery and params from a subscription filter.
|
/// Decide if there is an index that should be used explicitly
|
||||||
fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
|
fn override_index(f: &ReqFilter) -> Option<String> {
|
||||||
|
// queries for multiple kinds default to kind_index, which is
|
||||||
|
// significantly slower than kind_created_at_index.
|
||||||
|
if let Some(ks) = &f.kinds {
|
||||||
|
if f.ids.is_none() &&
|
||||||
|
ks.len() > 1 &&
|
||||||
|
f.since.is_none() &&
|
||||||
|
f.until.is_none() &&
|
||||||
|
f.tags.is_none() &&
|
||||||
|
f.authors.is_none() {
|
||||||
|
return Some("kind_created_at_index".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if there is an author, it is much better to force the authors index.
|
||||||
|
if let Some(_) = &f.authors {
|
||||||
|
if f.since.is_none() && f.until.is_none() {
|
||||||
|
if f.kinds.is_none() {
|
||||||
|
// with no use of kinds/created_at, just author
|
||||||
|
return Some("author_index".into());
|
||||||
|
} else {
|
||||||
|
// prefer author_kind if there are kinds
|
||||||
|
return Some("author_kind_index".into());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// finally, prefer author_created_at if time is provided
|
||||||
|
return Some("author_created_at_index".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a dynamic SQL subquery and params from a subscription filter (and optional explicit index used)
|
||||||
|
fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>, Option<String>) {
|
||||||
// build a dynamic SQL query. all user-input is either an integer
|
// build a dynamic SQL query. all user-input is either an integer
|
||||||
// (sqli-safe), or a string that is filtered to only contain
|
// (sqli-safe), or a string that is filtered to only contain
|
||||||
// hexadecimal characters. Strings that require escaping (tag
|
// hexadecimal characters. Strings that require escaping (tag
|
||||||
@@ -428,14 +552,16 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
|
|||||||
|
|
||||||
// if the filter is malformed, don't return anything.
|
// if the filter is malformed, don't return anything.
|
||||||
if f.force_no_match {
|
if f.force_no_match {
|
||||||
let empty_query =
|
let empty_query = "SELECT e.content, e.created_at FROM event e WHERE 1=0".to_owned();
|
||||||
"SELECT DISTINCT(e.content), e.created_at FROM event e WHERE 1=0".to_owned();
|
|
||||||
// query parameters for SQLite
|
// query parameters for SQLite
|
||||||
let empty_params: Vec<Box<dyn ToSql>> = vec![];
|
let empty_params: Vec<Box<dyn ToSql>> = vec![];
|
||||||
return (empty_query, empty_params);
|
return (empty_query, empty_params, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut query = "SELECT DISTINCT(e.content), e.created_at FROM event e".to_owned();
|
// check if the index needs to be overriden
|
||||||
|
let idx_name = override_index(f);
|
||||||
|
let idx_stmt = idx_name.as_ref().map_or_else(|| "".to_owned(), |i| format!("INDEXED BY {}",i));
|
||||||
|
let mut query = format!("SELECT e.content, e.created_at FROM event e {}", idx_stmt);
|
||||||
// query parameters for SQLite
|
// query parameters for SQLite
|
||||||
let mut params: Vec<Box<dyn ToSql>> = vec![];
|
let mut params: Vec<Box<dyn ToSql>> = vec![];
|
||||||
|
|
||||||
@@ -448,22 +574,18 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
|
|||||||
for auth in authvec {
|
for auth in authvec {
|
||||||
match hex_range(auth) {
|
match hex_range(auth) {
|
||||||
Some(HexSearch::Exact(ex)) => {
|
Some(HexSearch::Exact(ex)) => {
|
||||||
auth_searches.push("author=? OR delegated_by=?".to_owned());
|
auth_searches.push("author=?".to_owned());
|
||||||
params.push(Box::new(ex.clone()));
|
|
||||||
params.push(Box::new(ex));
|
params.push(Box::new(ex));
|
||||||
}
|
}
|
||||||
Some(HexSearch::Range(lower, upper)) => {
|
Some(HexSearch::Range(lower, upper)) => {
|
||||||
auth_searches.push(
|
auth_searches.push(
|
||||||
"(author>? AND author<?) OR (delegated_by>? AND delegated_by<?)".to_owned(),
|
"(author>? AND author<?)".to_owned(),
|
||||||
);
|
);
|
||||||
params.push(Box::new(lower.clone()));
|
|
||||||
params.push(Box::new(upper.clone()));
|
|
||||||
params.push(Box::new(lower));
|
params.push(Box::new(lower));
|
||||||
params.push(Box::new(upper));
|
params.push(Box::new(upper));
|
||||||
}
|
}
|
||||||
Some(HexSearch::LowerOnly(lower)) => {
|
Some(HexSearch::LowerOnly(lower)) => {
|
||||||
auth_searches.push("author>? OR delegated_by>?".to_owned());
|
auth_searches.push("author>?".to_owned());
|
||||||
params.push(Box::new(lower.clone()));
|
|
||||||
params.push(Box::new(lower));
|
params.push(Box::new(lower));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -472,11 +594,9 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !authvec.is_empty() {
|
if !authvec.is_empty() {
|
||||||
let authors_clause = format!("({})", auth_searches.join(" OR "));
|
let auth_clause = format!("({})", auth_searches.join(" OR "));
|
||||||
filter_components.push(authors_clause);
|
filter_components.push(auth_clause);
|
||||||
} else {
|
} else {
|
||||||
// if the authors list was empty, we should never return
|
|
||||||
// any results.
|
|
||||||
filter_components.push("false".to_owned());
|
filter_components.push("false".to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,7 +658,10 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
|
|||||||
let str_clause = format!("value IN ({})", repeat_vars(str_vals.len()));
|
let str_clause = format!("value IN ({})", repeat_vars(str_vals.len()));
|
||||||
let blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len()));
|
let blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len()));
|
||||||
// find evidence of the target tag name/value existing for this event.
|
// find evidence of the target tag name/value existing for this event.
|
||||||
let tag_clause = format!("e.id IN (SELECT e.id FROM event e LEFT JOIN tag t on e.id=t.event_id WHERE hidden!=TRUE and (name=? AND ({} OR {})))", str_clause, blob_clause);
|
let tag_clause = format!(
|
||||||
|
"e.id IN (SELECT e.id FROM event e LEFT JOIN tag t on e.id=t.event_id WHERE hidden!=TRUE and (name=? AND ({} OR {})))",
|
||||||
|
str_clause, blob_clause
|
||||||
|
);
|
||||||
// add the tag name as the first parameter
|
// add the tag name as the first parameter
|
||||||
params.push(Box::new(key.to_string()));
|
params.push(Box::new(key.to_string()));
|
||||||
// add all tag values that are plain strings as params
|
// add all tag values that are plain strings as params
|
||||||
@@ -572,40 +695,113 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
|
|||||||
} else {
|
} else {
|
||||||
query.push_str(" ORDER BY e.created_at ASC")
|
query.push_str(" ORDER BY e.created_at ASC")
|
||||||
}
|
}
|
||||||
(query, params)
|
(query, params, idx_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a dynamic SQL query string and params from a subscription.
|
/// Create a dynamic SQL query string and params from a subscription.
|
||||||
fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) {
|
fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>, Vec<String>) {
|
||||||
// build a dynamic SQL query for an entire subscription, based on
|
// build a dynamic SQL query for an entire subscription, based on
|
||||||
// SQL subqueries for filters.
|
// SQL subqueries for filters.
|
||||||
let mut subqueries: Vec<String> = Vec::new();
|
let mut subqueries: Vec<String> = Vec::new();
|
||||||
|
let mut indexes = vec![];
|
||||||
// subquery params
|
// subquery params
|
||||||
let mut params: Vec<Box<dyn ToSql>> = vec![];
|
let mut params: Vec<Box<dyn ToSql>> = vec![];
|
||||||
// for every filter in the subscription, generate a subquery
|
// for every filter in the subscription, generate a subquery
|
||||||
for f in sub.filters.iter() {
|
for f in sub.filters.iter() {
|
||||||
let (f_subquery, mut f_params) = query_from_filter(f);
|
let (f_subquery, mut f_params, index) = query_from_filter(f);
|
||||||
|
if let Some(i) = index {
|
||||||
|
indexes.push(i);
|
||||||
|
}
|
||||||
subqueries.push(f_subquery);
|
subqueries.push(f_subquery);
|
||||||
params.append(&mut f_params);
|
params.append(&mut f_params);
|
||||||
}
|
}
|
||||||
// encapsulate subqueries into select statements
|
// encapsulate subqueries into select statements
|
||||||
let subqueries_selects: Vec<String> = subqueries
|
let subqueries_selects: Vec<String> = subqueries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| format!("SELECT content, created_at FROM ({})", s))
|
.map(|s| format!("SELECT distinct content, created_at FROM ({})", s))
|
||||||
.collect();
|
.collect();
|
||||||
let query: String = subqueries_selects.join(" UNION ");
|
let query: String = subqueries_selects.join(" UNION ");
|
||||||
(query, params)
|
(query, params,indexes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_pool_stats(pool: &SqlitePool) {
|
/// Check if the pool is fully utilized
|
||||||
|
fn _pool_at_capacity(pool: &SqlitePool) -> bool {
|
||||||
|
let state: r2d2::State = pool.state();
|
||||||
|
state.idle_connections == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log pool stats
|
||||||
|
fn log_pool_stats(name: &str, pool: &SqlitePool) {
|
||||||
let state: r2d2::State = pool.state();
|
let state: r2d2::State = pool.state();
|
||||||
let in_use_cxns = state.connections - state.idle_connections;
|
let in_use_cxns = state.connections - state.idle_connections;
|
||||||
debug!(
|
debug!(
|
||||||
"DB pool usage (in_use: {}, available: {})",
|
"DB pool {:?} usage (in_use: {}, available: {}, max: {})",
|
||||||
in_use_cxns, state.connections
|
name,
|
||||||
|
in_use_cxns,
|
||||||
|
state.connections,
|
||||||
|
pool.max_size()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Perform database maintenance on a regular basis
|
||||||
|
pub async fn db_optimize_task(pool: SqlitePool) {
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::time::sleep(Duration::from_secs(60*60)) => {
|
||||||
|
if let Ok(mut conn) = pool.get() {
|
||||||
|
// the busy timer will block writers, so don't set
|
||||||
|
// this any higher than you want max latency for event
|
||||||
|
// writes.
|
||||||
|
info!("running database optimizer");
|
||||||
|
optimize_db(&mut conn).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform database WAL checkpoint on a regular basis
|
||||||
|
pub async fn db_checkpoint_task(pool: SqlitePool, safe_to_read: Arc<Mutex<u64>>) {
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
// WAL size in pages.
|
||||||
|
let mut current_wal_size = 0;
|
||||||
|
// WAL threshold for more aggressive checkpointing (10,000 pages, or about 40MB)
|
||||||
|
let wal_threshold = 1000*10;
|
||||||
|
// default threshold for the busy timer
|
||||||
|
let busy_wait_default = Duration::from_secs(1);
|
||||||
|
// if the WAL file is getting too big, switch to this
|
||||||
|
let busy_wait_default_long = Duration::from_secs(10);
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::time::sleep(Duration::from_secs(CHECKPOINT_FREQ_SEC)) => {
|
||||||
|
if let Ok(mut conn) = pool.get() {
|
||||||
|
let mut _guard:Option<MutexGuard<u64>> = None;
|
||||||
|
// the busy timer will block writers, so don't set
|
||||||
|
// this any higher than you want max latency for event
|
||||||
|
// writes.
|
||||||
|
if current_wal_size <= wal_threshold {
|
||||||
|
conn.busy_timeout(busy_wait_default).ok();
|
||||||
|
} else {
|
||||||
|
// if the wal size has exceeded a threshold, increase the busy timeout.
|
||||||
|
conn.busy_timeout(busy_wait_default_long).ok();
|
||||||
|
// take a lock that will prevent new readers.
|
||||||
|
info!("blocking new readers to perform wal_checkpoint");
|
||||||
|
_guard = Some(safe_to_read.lock().await);
|
||||||
|
}
|
||||||
|
debug!("running wal_checkpoint(TRUNCATE)");
|
||||||
|
if let Ok(new_size) = checkpoint_db(&mut conn) {
|
||||||
|
current_wal_size = new_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform a database query using a subscription.
|
/// Perform a database query using a subscription.
|
||||||
///
|
///
|
||||||
/// The [`Subscription`] is converted into a SQL query. Each result
|
/// The [`Subscription`] is converted into a SQL query. Each result
|
||||||
@@ -618,65 +814,125 @@ pub async fn db_query(
|
|||||||
pool: SqlitePool,
|
pool: SqlitePool,
|
||||||
query_tx: tokio::sync::mpsc::Sender<QueryResult>,
|
query_tx: tokio::sync::mpsc::Sender<QueryResult>,
|
||||||
mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>,
|
mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>,
|
||||||
|
safe_to_read: Arc<Mutex<u64>>,
|
||||||
) {
|
) {
|
||||||
|
let pre_spawn_start = Instant::now();
|
||||||
task::spawn_blocking(move || {
|
task::spawn_blocking(move || {
|
||||||
|
{
|
||||||
|
// if we are waiting on a checkpoint, stop until it is complete
|
||||||
|
let _ = safe_to_read.blocking_lock();
|
||||||
|
}
|
||||||
|
let db_queue_time = pre_spawn_start.elapsed();
|
||||||
|
// if the queue time was very long (>5 seconds), spare the DB and abort.
|
||||||
|
if db_queue_time > Duration::from_secs(5) {
|
||||||
|
info!(
|
||||||
|
"shedding DB query load queued for {:?} (cid: {}, sub: {:?})",
|
||||||
|
db_queue_time, client_id, sub.id
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// otherwise, report queuing time if it is slow
|
||||||
|
else if db_queue_time > Duration::from_secs(1) {
|
||||||
|
debug!(
|
||||||
|
"(slow) DB query queued for {:?} (cid: {}, sub: {:?})",
|
||||||
|
db_queue_time, client_id, sub.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let start = Instant::now();
|
||||||
let mut row_count: usize = 0;
|
let mut row_count: usize = 0;
|
||||||
let start = Instant::now();
|
|
||||||
// generate SQL query
|
// generate SQL query
|
||||||
let (q, p) = query_from_sub(&sub);
|
let (q, p, idxs) = query_from_sub(&sub);
|
||||||
trace!("SQL generated in {:?}", start.elapsed());
|
let sql_gen_elapsed = start.elapsed();
|
||||||
// show pool stats
|
|
||||||
log_pool_stats(&pool);
|
if sql_gen_elapsed > Duration::from_millis(10) {
|
||||||
|
debug!("SQL (slow) generated in {:?}", start.elapsed());
|
||||||
|
}
|
||||||
// cutoff for displaying slow queries
|
// cutoff for displaying slow queries
|
||||||
let slow_cutoff = Duration::from_millis(1000);
|
let slow_cutoff = Duration::from_millis(2000);
|
||||||
|
// any client that doesn't cause us to generate new rows in 5
|
||||||
|
// seconds gets dropped.
|
||||||
|
let abort_cutoff = Duration::from_secs(5);
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
if let Ok(conn) = pool.get() {
|
let mut slow_first_event;
|
||||||
// execute the query. Don't cache, since queries vary so much.
|
let mut last_successful_send = Instant::now();
|
||||||
let mut stmt = conn.prepare(&q)?;
|
if let Ok(mut conn) = pool.get() {
|
||||||
|
// execute the query.
|
||||||
|
// make the actual SQL query (with parameters inserted) available
|
||||||
|
conn.trace(Some(|x| {trace!("SQL trace: {:?}", x)}));
|
||||||
|
let mut stmt = conn.prepare_cached(&q)?;
|
||||||
let mut event_rows = stmt.query(rusqlite::params_from_iter(p))?;
|
let mut event_rows = stmt.query(rusqlite::params_from_iter(p))?;
|
||||||
|
|
||||||
let mut first_result = true;
|
let mut first_result = true;
|
||||||
while let Some(row) = event_rows.next()? {
|
while let Some(row) = event_rows.next()? {
|
||||||
|
let first_event_elapsed = start.elapsed();
|
||||||
|
slow_first_event = first_event_elapsed >= slow_cutoff;
|
||||||
if first_result {
|
if first_result {
|
||||||
let first_result_elapsed = start.elapsed();
|
|
||||||
// logging for slow queries; show sub and SQL
|
|
||||||
if first_result_elapsed >= slow_cutoff {
|
|
||||||
info!(
|
|
||||||
"going to query for: {:?} (cid: {}, sub: {:?})",
|
|
||||||
sub, client_id, sub.id
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"final query string (slow): {} (cid: {}, sub: {:?})",
|
|
||||||
q, client_id, sub.id
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
trace!(
|
|
||||||
"going to query for: {:?} (cid: {}, sub: {:?})",
|
|
||||||
sub,
|
|
||||||
client_id,
|
|
||||||
sub.id
|
|
||||||
);
|
|
||||||
trace!("final query string: {}", q);
|
|
||||||
}
|
|
||||||
debug!(
|
debug!(
|
||||||
"first result in {:?} (cid: {}, sub: {:?})",
|
"first result in {:?} (cid: {}, sub: {:?}) [used indexes: {:?}]",
|
||||||
first_result_elapsed, client_id, sub.id
|
first_event_elapsed, client_id, sub.id, idxs
|
||||||
);
|
);
|
||||||
first_result = false;
|
first_result = false;
|
||||||
}
|
}
|
||||||
// check if this is still active
|
// logging for slow queries; show sub and SQL.
|
||||||
// TODO: check every N rows
|
// to reduce logging; only show 1/16th of clients (leading 0)
|
||||||
if abandon_query_rx.try_recv().is_ok() {
|
if row_count == 0 && slow_first_event && client_id.starts_with('0') {
|
||||||
|
debug!(
|
||||||
|
"query req (slow): {:?} (cid: {}, sub: {:?})",
|
||||||
|
sub, client_id, sub.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// check if a checkpoint is trying to run, and abort
|
||||||
|
if row_count % 100 == 0 {
|
||||||
|
{
|
||||||
|
if safe_to_read.try_lock().is_err() {
|
||||||
|
// lock was held, abort this query
|
||||||
|
debug!("query aborted due to checkpoint (cid: {}, sub: {:?})", client_id, sub.id);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if this is still active; every 100 rows
|
||||||
|
if row_count % 100 == 0 && abandon_query_rx.try_recv().is_ok() {
|
||||||
debug!("query aborted (cid: {}, sub: {:?})", client_id, sub.id);
|
debug!("query aborted (cid: {}, sub: {:?})", client_id, sub.id);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
row_count += 1;
|
row_count += 1;
|
||||||
let event_json = row.get(0)?;
|
let event_json = row.get(0)?;
|
||||||
|
loop {
|
||||||
|
if query_tx.capacity() != 0 {
|
||||||
|
// we have capacity to add another item
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// the queue is full
|
||||||
|
trace!("db reader thread is stalled");
|
||||||
|
if last_successful_send + abort_cutoff < Instant::now() {
|
||||||
|
// the queue has been full for too long, abort
|
||||||
|
info!("aborting database query due to slow client (cid: {}, sub: {:?})",
|
||||||
|
client_id, sub.id);
|
||||||
|
let ok: Result<()> = Ok(());
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
// check if a checkpoint is trying to run, and abort
|
||||||
|
if safe_to_read.try_lock().is_err() {
|
||||||
|
// lock was held, abort this query
|
||||||
|
debug!("query aborted due to checkpoint (cid: {}, sub: {:?})", client_id, sub.id);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// give the queue a chance to clear before trying again
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: we could use try_send, but we'd have to juggle
|
||||||
|
// getting the query result back as part of the error
|
||||||
|
// result.
|
||||||
query_tx
|
query_tx
|
||||||
.blocking_send(QueryResult {
|
.blocking_send(QueryResult {
|
||||||
sub_id: sub.get_id(),
|
sub_id: sub.get_id(),
|
||||||
event: event_json,
|
event: event_json,
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
last_successful_send = Instant::now();
|
||||||
}
|
}
|
||||||
query_tx
|
query_tx
|
||||||
.blocking_send(QueryResult {
|
.blocking_send(QueryResult {
|
||||||
@@ -685,10 +941,11 @@ pub async fn db_query(
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
debug!(
|
debug!(
|
||||||
"query completed in {:?} (cid: {}, sub: {:?}, rows: {})",
|
"query completed in {:?} (cid: {}, sub: {:?}, db_time: {:?}, rows: {})",
|
||||||
start.elapsed(),
|
pre_spawn_start.elapsed(),
|
||||||
client_id,
|
client_id,
|
||||||
sub.id,
|
sub.id,
|
||||||
|
start.elapsed(),
|
||||||
row_count
|
row_count
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+7
-20
@@ -80,7 +80,7 @@ impl FromStr for Operator {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
||||||
pub struct ConditionQuery {
|
pub struct ConditionQuery {
|
||||||
pub(crate) conditions: Vec<Condition>,
|
pub conditions: Vec<Condition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConditionQuery {
|
impl ConditionQuery {
|
||||||
@@ -137,9 +137,9 @@ pub fn validate_delegation(
|
|||||||
/// An example complex condition would be: kind=1,2,3&created_at<1665265999
|
/// An example complex condition would be: kind=1,2,3&created_at<1665265999
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
||||||
pub struct Condition {
|
pub struct Condition {
|
||||||
pub(crate) field: Field,
|
pub field: Field,
|
||||||
pub(crate) operator: Operator,
|
pub operator: Operator,
|
||||||
pub(crate) values: Vec<u64>,
|
pub values: Vec<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Condition {
|
impl Condition {
|
||||||
@@ -332,19 +332,6 @@ mod tests {
|
|||||||
assert_eq!(parsed, cq);
|
assert_eq!(parsed, cq);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn simple_event() -> Event {
|
|
||||||
Event {
|
|
||||||
id: "0".to_owned(),
|
|
||||||
pubkey: "0".to_owned(),
|
|
||||||
delegated_by: None,
|
|
||||||
created_at: 0,
|
|
||||||
kind: 0,
|
|
||||||
tags: vec![],
|
|
||||||
content: "".to_owned(),
|
|
||||||
sig: "0".to_owned(),
|
|
||||||
tagidx: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check for condition logic on event w/ empty values
|
// Check for condition logic on event w/ empty values
|
||||||
#[test]
|
#[test]
|
||||||
fn condition_with_empty_values() {
|
fn condition_with_empty_values() {
|
||||||
@@ -353,7 +340,7 @@ mod tests {
|
|||||||
operator: Operator::GreaterThan,
|
operator: Operator::GreaterThan,
|
||||||
values: vec![],
|
values: vec![],
|
||||||
};
|
};
|
||||||
let e = simple_event();
|
let e = Event::simple_event();
|
||||||
assert!(!c.allows_event(&e));
|
assert!(!c.allows_event(&e));
|
||||||
c.operator = Operator::LessThan;
|
c.operator = Operator::LessThan;
|
||||||
assert!(!c.allows_event(&e));
|
assert!(!c.allows_event(&e));
|
||||||
@@ -373,7 +360,7 @@ mod tests {
|
|||||||
operator: Operator::GreaterThan,
|
operator: Operator::GreaterThan,
|
||||||
values: vec![10],
|
values: vec![10],
|
||||||
};
|
};
|
||||||
let mut e = simple_event();
|
let mut e = Event::simple_event();
|
||||||
// kind is not greater than 10, not allowed
|
// kind is not greater than 10, not allowed
|
||||||
e.kind = 1;
|
e.kind = 1;
|
||||||
assert!(!c.allows_event(&e));
|
assert!(!c.allows_event(&e));
|
||||||
@@ -392,7 +379,7 @@ mod tests {
|
|||||||
operator: Operator::Equals,
|
operator: Operator::Equals,
|
||||||
values: vec![0, 10, 20],
|
values: vec![0, 10, 20],
|
||||||
};
|
};
|
||||||
let mut e = simple_event();
|
let mut e = Event::simple_event();
|
||||||
// Allow if event kind is in list for Equals
|
// Allow if event kind is in list for Equals
|
||||||
e.kind = 10;
|
e.kind = 10;
|
||||||
assert!(c.allows_event(&e));
|
assert!(c.allows_event(&e));
|
||||||
|
|||||||
+49
-29
@@ -37,19 +37,19 @@ impl EventCmd {
|
|||||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub(crate) pubkey: String,
|
pub pubkey: String,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub(crate) delegated_by: Option<String>,
|
pub delegated_by: Option<String>,
|
||||||
pub(crate) created_at: u64,
|
pub created_at: u64,
|
||||||
pub(crate) kind: u64,
|
pub kind: u64,
|
||||||
#[serde(deserialize_with = "tag_from_string")]
|
#[serde(deserialize_with = "tag_from_string")]
|
||||||
// NOTE: array-of-arrays may need to be more general than a string container
|
// NOTE: array-of-arrays may need to be more general than a string container
|
||||||
pub(crate) tags: Vec<Vec<String>>,
|
pub tags: Vec<Vec<String>>,
|
||||||
pub(crate) content: String,
|
pub content: String,
|
||||||
pub(crate) sig: String,
|
pub sig: String,
|
||||||
// Optimization for tag search, built on demand.
|
// Optimization for tag search, built on demand.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub(crate) tagidx: Option<HashMap<char, HashSet<String>>>,
|
pub tagidx: Option<HashMap<char, HashSet<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple tag type for array of array of strings.
|
/// Simple tag type for array of array of strings.
|
||||||
@@ -101,10 +101,30 @@ impl From<EventCmd> for Result<Event> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn simple_event() -> Event {
|
||||||
|
Event {
|
||||||
|
id: "0".to_owned(),
|
||||||
|
pubkey: "0".to_owned(),
|
||||||
|
delegated_by: None,
|
||||||
|
created_at: 0,
|
||||||
|
kind: 0,
|
||||||
|
tags: vec![],
|
||||||
|
content: "".to_owned(),
|
||||||
|
sig: "0".to_owned(),
|
||||||
|
tagidx: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_kind_metadata(&self) -> bool {
|
pub fn is_kind_metadata(&self) -> bool {
|
||||||
self.kind == 0
|
self.kind == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Should this event be replaced with newer timestamps from same author?
|
||||||
|
pub fn is_replaceable(&self) -> bool {
|
||||||
|
self.kind == 0 || self.kind == 3 || self.kind == 41 || (self.kind >= 10000 && self.kind < 20000)
|
||||||
|
}
|
||||||
|
|
||||||
/// Pull a NIP-05 Name out of the event, if one exists
|
/// Pull a NIP-05 Name out of the event, if one exists
|
||||||
pub fn get_nip05_addr(&self) -> Option<nip05::Nip05Name> {
|
pub fn get_nip05_addr(&self) -> Option<nip05::Nip05Name> {
|
||||||
if self.is_kind_metadata() {
|
if self.is_kind_metadata() {
|
||||||
@@ -161,11 +181,11 @@ impl Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update delegation status
|
/// Update delegation status
|
||||||
fn update_delegation(&mut self) {
|
pub fn update_delegation(&mut self) {
|
||||||
self.delegated_by = self.delegated_author();
|
self.delegated_by = self.delegated_author();
|
||||||
}
|
}
|
||||||
/// Build an event tag index
|
/// Build an event tag index
|
||||||
fn build_index(&mut self) {
|
pub fn build_index(&mut self) {
|
||||||
// if there are no tags; just leave the index as None
|
// if there are no tags; just leave the index as None
|
||||||
if self.tags.is_empty() {
|
if self.tags.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -226,7 +246,7 @@ impl Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this event has a valid signature.
|
/// Check if this event has a valid signature.
|
||||||
fn validate(&self) -> Result<()> {
|
pub fn validate(&self) -> Result<()> {
|
||||||
// TODO: return a Result with a reason for invalid events
|
// TODO: return a Result with a reason for invalid events
|
||||||
// validation is performed by:
|
// validation is performed by:
|
||||||
// * parsing JSON string into event fields
|
// * parsing JSON string into event fields
|
||||||
@@ -319,31 +339,18 @@ impl Event {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
fn simple_event() -> Event {
|
|
||||||
Event {
|
|
||||||
id: "0".to_owned(),
|
|
||||||
pubkey: "0".to_owned(),
|
|
||||||
delegated_by: None,
|
|
||||||
created_at: 0,
|
|
||||||
kind: 0,
|
|
||||||
tags: vec![],
|
|
||||||
content: "".to_owned(),
|
|
||||||
sig: "0".to_owned(),
|
|
||||||
tagidx: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn event_creation() {
|
fn event_creation() {
|
||||||
// create an event
|
// create an event
|
||||||
let event = simple_event();
|
let event = Event::simple_event();
|
||||||
assert_eq!(event.id, "0");
|
assert_eq!(event.id, "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn event_serialize() -> Result<()> {
|
fn event_serialize() -> Result<()> {
|
||||||
// serialize an event to JSON string
|
// serialize an event to JSON string
|
||||||
let event = simple_event();
|
let event = Event::simple_event();
|
||||||
let j = serde_json::to_string(&event)?;
|
let j = serde_json::to_string(&event)?;
|
||||||
assert_eq!(j, "{\"id\":\"0\",\"pubkey\":\"0\",\"created_at\":0,\"kind\":0,\"tags\":[],\"content\":\"\",\"sig\":\"0\"}");
|
assert_eq!(j, "{\"id\":\"0\",\"pubkey\":\"0\",\"created_at\":0,\"kind\":0,\"tags\":[],\"content\":\"\",\"sig\":\"0\"}");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -351,14 +358,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_event_tag_match() {
|
fn empty_event_tag_match() {
|
||||||
let event = simple_event();
|
let event = Event::simple_event();
|
||||||
assert!(!event
|
assert!(!event
|
||||||
.generic_tag_val_intersect('e', &HashSet::from(["foo".to_owned(), "bar".to_owned()])));
|
.generic_tag_val_intersect('e', &HashSet::from(["foo".to_owned(), "bar".to_owned()])));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn single_event_tag_match() {
|
fn single_event_tag_match() {
|
||||||
let mut event = simple_event();
|
let mut event = Event::simple_event();
|
||||||
event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]];
|
event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]];
|
||||||
event.build_index();
|
event.build_index();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -373,7 +380,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn event_tags_serialize() -> Result<()> {
|
fn event_tags_serialize() -> Result<()> {
|
||||||
// serialize an event with tags to JSON string
|
// serialize an event with tags to JSON string
|
||||||
let mut event = simple_event();
|
let mut event = Event::simple_event();
|
||||||
event.tags = vec![
|
event.tags = vec![
|
||||||
vec![
|
vec![
|
||||||
"e".to_owned(),
|
"e".to_owned(),
|
||||||
@@ -497,4 +504,17 @@ mod tests {
|
|||||||
let expected = Some(expected_json.to_owned());
|
let expected = Some(expected_json.to_owned());
|
||||||
assert_eq!(c, expected);
|
assert_eq!(c, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replaceable_event() {
|
||||||
|
let mut event = Event::simple_event();
|
||||||
|
event.kind=0;
|
||||||
|
assert!(event.is_replaceable());
|
||||||
|
event.kind=3;
|
||||||
|
assert!(event.is_replaceable());
|
||||||
|
event.kind=12000;
|
||||||
|
assert!(event.is_replaceable());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ use crate::utils::is_hex;
|
|||||||
use hex;
|
use hex;
|
||||||
|
|
||||||
/// Types of hexadecimal queries.
|
/// Types of hexadecimal queries.
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)]
|
||||||
pub enum HexSearch {
|
pub enum HexSearch {
|
||||||
// when no range is needed, exact 32-byte
|
// when no range is needed, exact 32-byte
|
||||||
Exact(Vec<u8>),
|
Exact(Vec<u8>),
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ impl From<config::Info> for RelayInfo {
|
|||||||
description: i.description,
|
description: i.description,
|
||||||
pubkey: i.pubkey,
|
pubkey: i.pubkey,
|
||||||
contact: i.contact,
|
contact: i.contact,
|
||||||
supported_nips: Some(vec![1, 2, 9, 11, 12, 15, 16, 20, 22, 26]),
|
supported_nips: Some(vec![1, 2, 9, 11, 12, 15, 16, 20, 22]),
|
||||||
software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()),
|
software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()),
|
||||||
version: CARGO_PKG_VERSION.map(|x| x.to_owned()),
|
version: CARGO_PKG_VERSION.map(|x| x.to_owned()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod cli;
|
||||||
pub mod close;
|
pub mod close;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod conn;
|
pub mod conn;
|
||||||
|
|||||||
+16
-17
@@ -1,42 +1,41 @@
|
|||||||
//! Server process
|
//! Server process
|
||||||
|
use clap::Parser;
|
||||||
|
use nostr_rs_relay::cli::*;
|
||||||
use nostr_rs_relay::config;
|
use nostr_rs_relay::config;
|
||||||
use nostr_rs_relay::server::start_server;
|
use nostr_rs_relay::server::start_server;
|
||||||
use std::env;
|
|
||||||
use std::sync::mpsc as syncmpsc;
|
use std::sync::mpsc as syncmpsc;
|
||||||
use std::sync::mpsc::{Receiver as MpscReceiver, Sender as MpscSender};
|
use std::sync::mpsc::{Receiver as MpscReceiver, Sender as MpscSender};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
|
||||||
use console_subscriber::ConsoleLayer;
|
use console_subscriber::ConsoleLayer;
|
||||||
|
|
||||||
/// Return a requested DB name from command line arguments.
|
|
||||||
fn db_from_args(args: &[String]) -> Option<String> {
|
|
||||||
if args.len() == 3 && args.get(1) == Some(&"--db".to_owned()) {
|
|
||||||
return args.get(2).map(std::clone::Clone::clone);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start running a Nostr relay server.
|
/// Start running a Nostr relay server.
|
||||||
fn main() {
|
fn main() {
|
||||||
// setup tracing
|
|
||||||
let _trace_sub = tracing_subscriber::fmt::try_init();
|
|
||||||
info!("Starting up from main");
|
|
||||||
// get database directory from args
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
let db_dir: Option<String> = db_from_args(&args);
|
|
||||||
// configure settings from config.toml
|
// configure settings from config.toml
|
||||||
// replace default settings with those read from config.toml
|
// replace default settings with those read from config.toml
|
||||||
let mut settings = config::Settings::new();
|
let mut settings = config::Settings::new();
|
||||||
|
|
||||||
|
// setup tracing
|
||||||
if settings.diagnostics.tracing {
|
if settings.diagnostics.tracing {
|
||||||
// enable tracing with tokio-console
|
// enable tracing with tokio-console
|
||||||
ConsoleLayer::builder().with_default_env().init();
|
ConsoleLayer::builder().with_default_env().init();
|
||||||
|
} else {
|
||||||
|
// standard logging
|
||||||
|
tracing_subscriber::fmt::try_init().unwrap();
|
||||||
}
|
}
|
||||||
|
info!("Starting up from main");
|
||||||
|
|
||||||
|
let args = CLIArgs::parse();
|
||||||
|
|
||||||
|
// get database directory from args
|
||||||
|
let db_dir = args.db;
|
||||||
|
|
||||||
// update with database location
|
// update with database location
|
||||||
if let Some(db) = db_dir {
|
if db_dir.len() > 0 {
|
||||||
settings.database.data_directory = db;
|
settings.database.data_directory = db_dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (_, ctrl_rx): (MpscSender<()>, MpscReceiver<()>) = syncmpsc::channel();
|
let (_, ctrl_rx): (MpscSender<()>, MpscReceiver<()>) = syncmpsc::channel();
|
||||||
|
|||||||
+2
-2
@@ -517,7 +517,7 @@ impl Verifier {
|
|||||||
Ok(updated) => {
|
Ok(updated) => {
|
||||||
if updated != 0 {
|
if updated != 0 {
|
||||||
info!(
|
info!(
|
||||||
"persisted event: {:?} in {:?}",
|
"persisted event (new verified pubkey): {:?} in {:?}",
|
||||||
event.get_event_id_prefix(),
|
event.get_event_id_prefix(),
|
||||||
start.elapsed()
|
start.elapsed()
|
||||||
);
|
);
|
||||||
@@ -721,7 +721,7 @@ pub fn query_oldest_user_verification(
|
|||||||
earliest: u64,
|
earliest: u64,
|
||||||
) -> Result<VerificationRecord> {
|
) -> Result<VerificationRecord> {
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
let query = "SELECT v.id, v.name, e.event_hash, e.author, e.created_at, v.verified_at, v.failed_at, v.failure_count FROM user_verification v LEFT JOIN event e ON e.id=v.metadata_event WHERE (v.verified_at < ? OR v.verified_at IS NULL) AND (v.failed_at < ? OR v.failed_at IS NULL) ORDER BY v.verified_at ASC, v.failed_at ASC LIMIT 1;";
|
let query = "SELECT v.id, v.name, e.event_hash, e.author, e.created_at, v.verified_at, v.failed_at, v.failure_count FROM user_verification v INNER JOIN event e ON e.id=v.metadata_event WHERE (v.verified_at < ? OR v.verified_at IS NULL) AND (v.failed_at < ? OR v.failed_at IS NULL) ORDER BY v.verified_at ASC, v.failed_at ASC LIMIT 1;";
|
||||||
let mut stmt = tx.prepare_cached(query)?;
|
let mut stmt = tx.prepare_cached(query)?;
|
||||||
let fields = stmt.query_row(params![earliest, earliest], |r| {
|
let fields = stmt.query_row(params![earliest, earliest], |r| {
|
||||||
let rowid: u64 = r.get(0)?;
|
let rowid: u64 = r.get(0)?;
|
||||||
|
|||||||
+242
-12
@@ -13,21 +13,22 @@ use tracing::{debug, error, info};
|
|||||||
|
|
||||||
/// Startup DB Pragmas
|
/// Startup DB Pragmas
|
||||||
pub const STARTUP_SQL: &str = r##"
|
pub const STARTUP_SQL: &str = r##"
|
||||||
PRAGMA main.synchronous=NORMAL;
|
PRAGMA main.synchronous = NORMAL;
|
||||||
PRAGMA foreign_keys = ON;
|
PRAGMA foreign_keys = ON;
|
||||||
PRAGMA journal_size_limit=32768;
|
PRAGMA journal_size_limit = 32768;
|
||||||
pragma mmap_size = 536870912; -- 512MB of mmap
|
pragma mmap_size = 17179869184; -- cap mmap at 16GB
|
||||||
"##;
|
"##;
|
||||||
|
|
||||||
/// Latest database version
|
/// Latest database version
|
||||||
pub const DB_VERSION: usize = 9;
|
pub const DB_VERSION: usize = 15;
|
||||||
|
|
||||||
/// Schema definition
|
/// Schema definition
|
||||||
const INIT_SQL: &str = formatcp!(
|
const INIT_SQL: &str = formatcp!(
|
||||||
r##"
|
r##"
|
||||||
-- Database settings
|
-- Database settings
|
||||||
PRAGMA encoding = "UTF-8";
|
PRAGMA encoding = "UTF-8";
|
||||||
PRAGMA journal_mode=WAL;
|
PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA auto_vacuum = FULL;
|
||||||
PRAGMA main.synchronous=NORMAL;
|
PRAGMA main.synchronous=NORMAL;
|
||||||
PRAGMA foreign_keys = ON;
|
PRAGMA foreign_keys = ON;
|
||||||
PRAGMA application_id = 1654008667;
|
PRAGMA application_id = 1654008667;
|
||||||
@@ -49,9 +50,14 @@ content TEXT NOT NULL -- serialized json of event object
|
|||||||
-- Event Indexes
|
-- Event Indexes
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash);
|
CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash);
|
||||||
CREATE INDEX IF NOT EXISTS author_index ON event(author);
|
CREATE INDEX IF NOT EXISTS author_index ON event(author);
|
||||||
|
CREATE INDEX IF NOT EXISTS kind_index ON event(kind);
|
||||||
CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at);
|
CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at);
|
||||||
CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by);
|
CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by);
|
||||||
CREATE INDEX IF NOT EXISTS event_composite_index ON event(kind,created_at);
|
CREATE INDEX IF NOT EXISTS event_composite_index ON event(kind,created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS kind_author_index ON event(kind,author);
|
||||||
|
CREATE INDEX IF NOT EXISTS kind_created_at_index ON event(kind,created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS author_created_at_index ON event(author,created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS author_kind_index ON event(author,kind);
|
||||||
|
|
||||||
-- Tag Table
|
-- Tag Table
|
||||||
-- Tag values are stored as either a BLOB (if they come in as a
|
-- Tag values are stored as either a BLOB (if they come in as a
|
||||||
@@ -67,6 +73,8 @@ 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_index ON tag(value);
|
||||||
CREATE INDEX IF NOT EXISTS tag_val_hex_index ON tag(value_hex);
|
CREATE INDEX IF NOT EXISTS tag_val_hex_index ON tag(value_hex);
|
||||||
|
CREATE INDEX IF NOT EXISTS tag_composite_index ON tag(event_id,name,value_hex,value);
|
||||||
|
CREATE INDEX IF NOT EXISTS tag_name_eid_index ON tag(name,event_id,value_hex);
|
||||||
|
|
||||||
-- NIP-05 User Validation
|
-- NIP-05 User Validation
|
||||||
CREATE TABLE IF NOT EXISTS user_verification (
|
CREATE TABLE IF NOT EXISTS user_verification (
|
||||||
@@ -91,6 +99,20 @@ pub fn curr_db_version(conn: &mut Connection) -> Result<usize> {
|
|||||||
Ok(curr_version)
|
Ok(curr_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine event count
|
||||||
|
pub fn db_event_count(conn: &mut Connection) -> Result<usize> {
|
||||||
|
let query = "SELECT count(*) FROM event;";
|
||||||
|
let count = conn.query_row(query, [], |row| row.get(0))?;
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine tag count
|
||||||
|
pub fn db_tag_count(conn: &mut Connection) -> Result<usize> {
|
||||||
|
let query = "SELECT count(*) FROM tag;";
|
||||||
|
let count = conn.query_row(query, [], |row| row.get(0))?;
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
fn mig_init(conn: &mut PooledConnection) -> Result<usize> {
|
fn mig_init(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
match conn.execute_batch(INIT_SQL) {
|
match conn.execute_batch(INIT_SQL) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
@@ -138,19 +160,15 @@ pub fn upgrade_db(conn: &mut PooledConnection) -> Result<()> {
|
|||||||
if curr_version == 1 {
|
if curr_version == 1 {
|
||||||
curr_version = mig_1_to_2(conn)?;
|
curr_version = mig_1_to_2(conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if curr_version == 2 {
|
if curr_version == 2 {
|
||||||
curr_version = mig_2_to_3(conn)?;
|
curr_version = mig_2_to_3(conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if curr_version == 3 {
|
if curr_version == 3 {
|
||||||
curr_version = mig_3_to_4(conn)?;
|
curr_version = mig_3_to_4(conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if curr_version == 4 {
|
if curr_version == 4 {
|
||||||
curr_version = mig_4_to_5(conn)?;
|
curr_version = mig_4_to_5(conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if curr_version == 5 {
|
if curr_version == 5 {
|
||||||
curr_version = mig_5_to_6(conn)?;
|
curr_version = mig_5_to_6(conn)?;
|
||||||
}
|
}
|
||||||
@@ -163,6 +181,24 @@ pub fn upgrade_db(conn: &mut PooledConnection) -> Result<()> {
|
|||||||
if curr_version == 8 {
|
if curr_version == 8 {
|
||||||
curr_version = mig_8_to_9(conn)?;
|
curr_version = mig_8_to_9(conn)?;
|
||||||
}
|
}
|
||||||
|
if curr_version == 9 {
|
||||||
|
curr_version = mig_9_to_10(conn)?;
|
||||||
|
}
|
||||||
|
if curr_version == 10 {
|
||||||
|
curr_version = mig_10_to_11(conn)?;
|
||||||
|
}
|
||||||
|
if curr_version == 11 {
|
||||||
|
curr_version = mig_11_to_12(conn)?;
|
||||||
|
}
|
||||||
|
if curr_version == 12 {
|
||||||
|
curr_version = mig_12_to_13(conn)?;
|
||||||
|
}
|
||||||
|
if curr_version == 13 {
|
||||||
|
curr_version = mig_13_to_14(conn)?;
|
||||||
|
}
|
||||||
|
if curr_version == 14 {
|
||||||
|
curr_version = mig_14_to_15(conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
if curr_version == DB_VERSION {
|
if curr_version == DB_VERSION {
|
||||||
info!(
|
info!(
|
||||||
@@ -190,6 +226,62 @@ pub fn upgrade_db(conn: &mut PooledConnection) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn rebuild_tags(conn: &mut PooledConnection) -> Result<()> {
|
||||||
|
// Check how many events we have to process
|
||||||
|
let count = db_event_count(conn)?;
|
||||||
|
let update_each_percent = 0.05;
|
||||||
|
let mut percent_done = 0.0;
|
||||||
|
let mut events_processed = 0;
|
||||||
|
let start = Instant::now();
|
||||||
|
let tx = conn.transaction()?;
|
||||||
|
{
|
||||||
|
// Clear out table
|
||||||
|
tx.execute("DELETE FROM tag;", [])?;
|
||||||
|
let mut stmt = tx.prepare("select id, content from event order by id;")?;
|
||||||
|
let mut tag_rows = stmt.query([])?;
|
||||||
|
while let Some(row) = tag_rows.next()? {
|
||||||
|
if (events_processed as f32)/(count as f32) > percent_done {
|
||||||
|
info!("Tag update {}% complete...", (100.0*percent_done).round());
|
||||||
|
percent_done += update_each_percent;
|
||||||
|
}
|
||||||
|
// 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 event_json: String = row.get(1)?;
|
||||||
|
let event: Event = serde_json::from_str(&event_json)?;
|
||||||
|
// look at each event, and each tag, creating new tag entries if appropriate.
|
||||||
|
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
||||||
|
let tagname = t.get(0).unwrap();
|
||||||
|
let tagnamechar_opt = single_char_tagname(tagname);
|
||||||
|
if tagnamechar_opt.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// safe because len was > 1
|
||||||
|
let tagval = t.get(1).unwrap();
|
||||||
|
// insert as BLOB if we can restore it losslessly.
|
||||||
|
// this means it needs to be even length and lowercase.
|
||||||
|
if (tagval.len() % 2 == 0) && is_lower_hex(tagval) {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3);",
|
||||||
|
params![event_id, tagname, hex::decode(tagval).ok()],
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
// otherwise, insert as text
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO tag (event_id, name, value) VALUES (?1, ?2, ?3);",
|
||||||
|
params![event_id, tagname, &tagval],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events_processed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.commit()?;
|
||||||
|
info!("rebuilt tags in {:?}", start.elapsed());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//// Migration Scripts
|
//// Migration Scripts
|
||||||
|
|
||||||
fn mig_1_to_2(conn: &mut PooledConnection) -> Result<usize> {
|
fn mig_1_to_2(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
@@ -321,7 +413,6 @@ fn mig_5_to_6(conn: &mut PooledConnection) -> Result<usize> {
|
|||||||
let mut stmt = tx.prepare("select id, content from event order by id;")?;
|
let mut stmt = tx.prepare("select id, content from event order by id;")?;
|
||||||
let mut tag_rows = stmt.query([])?;
|
let mut tag_rows = stmt.query([])?;
|
||||||
while let Some(row) = tag_rows.next()? {
|
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 event_id: u64 = row.get(0)?;
|
||||||
let event_json: String = row.get(1)?;
|
let event_json: String = row.get(1)?;
|
||||||
let event: Event = serde_json::from_str(&event_json)?;
|
let event: Event = serde_json::from_str(&event_json)?;
|
||||||
@@ -363,7 +454,6 @@ fn mig_5_to_6(conn: &mut PooledConnection) -> Result<usize> {
|
|||||||
|
|
||||||
fn mig_6_to_7(conn: &mut PooledConnection) -> Result<usize> {
|
fn mig_6_to_7(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
info!("database schema needs update from 6->7");
|
info!("database schema needs update from 6->7");
|
||||||
// only change is adding a hidden column to events.
|
|
||||||
let upgrade_sql = r##"
|
let upgrade_sql = r##"
|
||||||
ALTER TABLE event ADD delegated_by BLOB;
|
ALTER TABLE event ADD delegated_by BLOB;
|
||||||
CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by);
|
CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by);
|
||||||
@@ -419,5 +509,145 @@ PRAGMA user_version = 9;
|
|||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(8)
|
Ok(9)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mig_9_to_10(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
|
info!("database schema needs update from 9->10");
|
||||||
|
// Those old indexes were actually helpful...
|
||||||
|
let upgrade_sql = r##"
|
||||||
|
CREATE INDEX IF NOT EXISTS tag_composite_index ON tag(event_id,name,value_hex,value);
|
||||||
|
PRAGMA user_version = 10;
|
||||||
|
"##;
|
||||||
|
match conn.execute_batch(upgrade_sql) {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("database schema upgraded v9 -> v10");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("update failed: {}", err);
|
||||||
|
panic!("database could not be upgraded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mig_10_to_11(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
|
info!("database schema needs update from 10->11");
|
||||||
|
// Those old indexes were actually helpful...
|
||||||
|
let upgrade_sql = r##"
|
||||||
|
CREATE INDEX IF NOT EXISTS tag_name_eid_index ON tag(name,event_id,value_hex);
|
||||||
|
reindex;
|
||||||
|
pragma optimize;
|
||||||
|
PRAGMA user_version = 11;
|
||||||
|
"##;
|
||||||
|
match conn.execute_batch(upgrade_sql) {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("database schema upgraded v10 -> v11");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("update failed: {}", err);
|
||||||
|
panic!("database could not be upgraded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mig_11_to_12(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
|
info!("database schema needs update from 11->12");
|
||||||
|
let start = Instant::now();
|
||||||
|
let tx = conn.transaction()?;
|
||||||
|
{
|
||||||
|
// Lookup every replaceable event
|
||||||
|
let mut stmt = tx.prepare("select kind,author from event where kind in (0,3,41) or (kind>=10000 and kind<20000) order by id;")?;
|
||||||
|
let mut replaceable_rows = stmt.query([])?;
|
||||||
|
info!("updating replaceable events; this could take awhile...");
|
||||||
|
while let Some(row) = replaceable_rows.next()? {
|
||||||
|
// we want to capture the event_id that had the tag, the tag name, and the tag hex value.
|
||||||
|
let event_kind: u64 = row.get(0)?;
|
||||||
|
let event_author: Vec<u8> = row.get(1)?;
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE event SET hidden=TRUE WHERE hidden!=TRUE and kind=? and author=? and id NOT IN (SELECT id FROM event WHERE kind=? AND author=? ORDER BY created_at DESC LIMIT 1)",
|
||||||
|
params![event_kind, event_author, event_kind, event_author],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
tx.execute("PRAGMA user_version = 12;", [])?;
|
||||||
|
}
|
||||||
|
tx.commit()?;
|
||||||
|
info!("database schema upgraded v11 -> v12 in {:?}", start.elapsed());
|
||||||
|
// vacuum after large table modification
|
||||||
|
let start = Instant::now();
|
||||||
|
conn.execute("VACUUM;", [])?;
|
||||||
|
info!("vacuumed DB after hidden event cleanup in {:?}", start.elapsed());
|
||||||
|
Ok(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mig_12_to_13(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
|
info!("database schema needs update from 12->13");
|
||||||
|
let upgrade_sql = r##"
|
||||||
|
CREATE INDEX IF NOT EXISTS kind_author_index ON event(kind,author);
|
||||||
|
reindex;
|
||||||
|
pragma optimize;
|
||||||
|
PRAGMA user_version = 13;
|
||||||
|
"##;
|
||||||
|
match conn.execute_batch(upgrade_sql) {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("database schema upgraded v12 -> v13");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("update failed: {}", err);
|
||||||
|
panic!("database could not be upgraded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(13)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mig_13_to_14(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
|
info!("database schema needs update from 13->14");
|
||||||
|
let upgrade_sql = r##"
|
||||||
|
CREATE INDEX IF NOT EXISTS kind_index ON event(kind);
|
||||||
|
CREATE INDEX IF NOT EXISTS kind_created_at_index ON event(kind,created_at);
|
||||||
|
pragma optimize;
|
||||||
|
PRAGMA user_version = 14;
|
||||||
|
"##;
|
||||||
|
match conn.execute_batch(upgrade_sql) {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("database schema upgraded v13 -> v14");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("update failed: {}", err);
|
||||||
|
panic!("database could not be upgraded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(14)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mig_14_to_15(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
|
info!("database schema needs update from 14->15");
|
||||||
|
let upgrade_sql = r##"
|
||||||
|
CREATE INDEX IF NOT EXISTS author_created_at_index ON event(author,created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS author_kind_index ON event(author,kind);
|
||||||
|
PRAGMA user_version = 15;
|
||||||
|
"##;
|
||||||
|
match conn.execute_batch(upgrade_sql) {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("database schema upgraded v14 -> v15");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("update failed: {}", err);
|
||||||
|
panic!("database could not be upgraded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// clear out hidden events
|
||||||
|
let clear_hidden_sql = r##"DELETE FROM event WHERE HIDDEN=true;"##;
|
||||||
|
info!("removing hidden events; this may take awhile...");
|
||||||
|
match conn.execute_batch(clear_hidden_sql) {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("all hidden events removed");
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("delete failed: {}", err);
|
||||||
|
panic!("could not remove hidden events");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(15)
|
||||||
}
|
}
|
||||||
|
|||||||
+93
-24
@@ -14,6 +14,7 @@ use crate::notice::Notice;
|
|||||||
use crate::subscription::Subscription;
|
use crate::subscription::Subscription;
|
||||||
use futures::SinkExt;
|
use futures::SinkExt;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use governor::{Jitter, Quota, RateLimiter};
|
||||||
use http::header::HeaderMap;
|
use http::header::HeaderMap;
|
||||||
use hyper::header::ACCEPT;
|
use hyper::header::ACCEPT;
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
@@ -21,12 +22,16 @@ use hyper::upgrade::Upgraded;
|
|||||||
use hyper::{
|
use hyper::{
|
||||||
header, server::conn::AddrStream, upgrade, Body, Request, Response, Server, StatusCode,
|
header, server::conn::AddrStream, upgrade, Body, Request, Response, Server, StatusCode,
|
||||||
};
|
};
|
||||||
|
use rusqlite::OpenFlags;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc::Receiver as MpscReceiver;
|
use std::sync::mpsc::Receiver as MpscReceiver;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -51,6 +56,7 @@ async fn handle_web_request(
|
|||||||
broadcast: Sender<Event>,
|
broadcast: Sender<Event>,
|
||||||
event_tx: tokio::sync::mpsc::Sender<SubmittedEvent>,
|
event_tx: tokio::sync::mpsc::Sender<SubmittedEvent>,
|
||||||
shutdown: Receiver<()>,
|
shutdown: Receiver<()>,
|
||||||
|
safe_to_read: Arc<Mutex<u64>>,
|
||||||
) -> Result<Response<Body>, Infallible> {
|
) -> Result<Response<Body>, Infallible> {
|
||||||
match (
|
match (
|
||||||
request.uri().path(),
|
request.uri().path(),
|
||||||
@@ -73,6 +79,7 @@ async fn handle_web_request(
|
|||||||
Ok(upgraded) => {
|
Ok(upgraded) => {
|
||||||
// set WebSocket configuration options
|
// set WebSocket configuration options
|
||||||
let config = WebSocketConfig {
|
let config = WebSocketConfig {
|
||||||
|
max_send_queue: Some(1024),
|
||||||
max_message_size: settings.limits.max_ws_message_bytes,
|
max_message_size: settings.limits.max_ws_message_bytes,
|
||||||
max_frame_size: settings.limits.max_ws_frame_bytes,
|
max_frame_size: settings.limits.max_ws_frame_bytes,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -86,6 +93,7 @@ async fn handle_web_request(
|
|||||||
Some(config),
|
Some(config),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
let origin = get_header_string("origin", request.headers());
|
||||||
let user_agent = get_header_string("user-agent", request.headers());
|
let user_agent = get_header_string("user-agent", request.headers());
|
||||||
// determine the remote IP from headers if the exist
|
// determine the remote IP from headers if the exist
|
||||||
let header_ip = settings
|
let header_ip = settings
|
||||||
@@ -99,6 +107,7 @@ async fn handle_web_request(
|
|||||||
let client_info = ClientInfo {
|
let client_info = ClientInfo {
|
||||||
remote_ip,
|
remote_ip,
|
||||||
user_agent,
|
user_agent,
|
||||||
|
origin,
|
||||||
};
|
};
|
||||||
// spawn a nostr server with our websocket
|
// spawn a nostr server with our websocket
|
||||||
tokio::spawn(nostr_server(
|
tokio::spawn(nostr_server(
|
||||||
@@ -109,6 +118,7 @@ async fn handle_web_request(
|
|||||||
broadcast,
|
broadcast,
|
||||||
event_tx,
|
event_tx,
|
||||||
shutdown,
|
shutdown,
|
||||||
|
safe_to_read,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// todo: trace, don't print...
|
// todo: trace, don't print...
|
||||||
@@ -243,7 +253,20 @@ pub fn start_server(settings: Settings, shutdown_rx: MpscReceiver<()>) -> Result
|
|||||||
// configure tokio runtime
|
// configure tokio runtime
|
||||||
let rt = Builder::new_multi_thread()
|
let rt = Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.thread_name("tokio-ws")
|
.thread_name_fn(|| {
|
||||||
|
// give each thread a unique numeric name
|
||||||
|
static ATOMIC_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||||
|
let id = ATOMIC_ID.fetch_add(1,Ordering::SeqCst);
|
||||||
|
format!("tokio-ws-{}", id)
|
||||||
|
})
|
||||||
|
// limit concurrent SQLite blocking threads
|
||||||
|
.max_blocking_threads(settings.limits.max_blocking_threads)
|
||||||
|
.on_thread_start(|| {
|
||||||
|
trace!("started new thread: {:?}", std::thread::current().name());
|
||||||
|
})
|
||||||
|
.on_thread_stop(|| {
|
||||||
|
trace!("stopped thread: {:?}", std::thread::current().name());
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// start tokio
|
// start tokio
|
||||||
@@ -301,6 +324,23 @@ pub fn start_server(settings: Settings, shutdown_rx: MpscReceiver<()>) -> Result
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// build a connection pool for DB maintenance
|
||||||
|
let maintenance_pool = db::build_pool(
|
||||||
|
"maintenance writer",
|
||||||
|
&settings,
|
||||||
|
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a mutex that will block readers, so that a
|
||||||
|
// checkpoint can be performed quickly.
|
||||||
|
let safe_to_read = Arc::new(Mutex::new(0));
|
||||||
|
|
||||||
|
db::db_optimize_task(maintenance_pool.clone()).await;
|
||||||
|
db::db_checkpoint_task(maintenance_pool, safe_to_read.clone()).await;
|
||||||
|
|
||||||
// listen for (external to tokio) shutdown request
|
// listen for (external to tokio) shutdown request
|
||||||
let controlled_shutdown = invoke_shutdown.clone();
|
let controlled_shutdown = invoke_shutdown.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -330,12 +370,15 @@ pub fn start_server(settings: Settings, shutdown_rx: MpscReceiver<()>) -> Result
|
|||||||
let pool = db::build_pool(
|
let pool = db::build_pool(
|
||||||
"client query",
|
"client query",
|
||||||
&settings,
|
&settings,
|
||||||
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY
|
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
|
||||||
| rusqlite::OpenFlags::SQLITE_OPEN_SHARED_CACHE,
|
|
||||||
db_min_conn,
|
db_min_conn,
|
||||||
db_max_conn,
|
db_max_conn,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
// spawn a task to check the pool size.
|
||||||
|
let pool_monitor = pool.clone();
|
||||||
|
tokio::spawn(async move {db::monitor_pool("reader", pool_monitor).await;});
|
||||||
|
|
||||||
// A `Service` is needed for every connection, so this
|
// A `Service` is needed for every connection, so this
|
||||||
// creates one from our `handle_request` function.
|
// creates one from our `handle_request` function.
|
||||||
let make_svc = make_service_fn(|conn: &AddrStream| {
|
let make_svc = make_service_fn(|conn: &AddrStream| {
|
||||||
@@ -345,6 +388,7 @@ pub fn start_server(settings: Settings, shutdown_rx: MpscReceiver<()>) -> Result
|
|||||||
let event = event_tx.clone();
|
let event = event_tx.clone();
|
||||||
let stop = invoke_shutdown.clone();
|
let stop = invoke_shutdown.clone();
|
||||||
let settings = settings.clone();
|
let settings = settings.clone();
|
||||||
|
let safe_to_read = safe_to_read.clone();
|
||||||
async move {
|
async move {
|
||||||
// service_fn converts our function into a `Service`
|
// service_fn converts our function into a `Service`
|
||||||
Ok::<_, Infallible>(service_fn(move |request: Request<Body>| {
|
Ok::<_, Infallible>(service_fn(move |request: Request<Body>| {
|
||||||
@@ -356,6 +400,7 @@ pub fn start_server(settings: Settings, shutdown_rx: MpscReceiver<()>) -> Result
|
|||||||
bcast.clone(),
|
bcast.clone(),
|
||||||
event.clone(),
|
event.clone(),
|
||||||
stop.subscribe(),
|
stop.subscribe(),
|
||||||
|
safe_to_read.clone(),
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -388,6 +433,10 @@ fn convert_to_msg(msg: String, max_bytes: Option<usize>) -> Result<NostrMessage>
|
|||||||
let parsed_res: Result<NostrMessage> = serde_json::from_str(&msg).map_err(|e| e.into());
|
let parsed_res: Result<NostrMessage> = serde_json::from_str(&msg).map_err(|e| e.into());
|
||||||
match parsed_res {
|
match parsed_res {
|
||||||
Ok(m) => {
|
Ok(m) => {
|
||||||
|
if let NostrMessage::SubMsg(_) = m {
|
||||||
|
// note; this only prints the first 16k of a REQ and then truncates.
|
||||||
|
trace!("REQ: {:?}",msg);
|
||||||
|
};
|
||||||
if let NostrMessage::EventMsg(_) = m {
|
if let NostrMessage::EventMsg(_) = m {
|
||||||
if let Some(max_size) = max_bytes {
|
if let Some(max_size) = max_bytes {
|
||||||
// check length, ensure that some max size is set.
|
// check length, ensure that some max size is set.
|
||||||
@@ -399,8 +448,8 @@ fn convert_to_msg(msg: String, max_bytes: Option<usize>) -> Result<NostrMessage>
|
|||||||
Ok(m)
|
Ok(m)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("proto parse error: {:?}", e);
|
trace!("proto parse error: {:?}", e);
|
||||||
debug!("parse error on message: {}", msg.trim());
|
trace!("parse error on message: {:?}", msg.trim());
|
||||||
Err(Error::ProtoParseError)
|
Err(Error::ProtoParseError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -419,6 +468,7 @@ fn make_notice_message(notice: Notice) -> Message {
|
|||||||
struct ClientInfo {
|
struct ClientInfo {
|
||||||
remote_ip: String,
|
remote_ip: String,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
|
origin: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle new client connections. This runs through an event loop
|
/// Handle new client connections. This runs through an event loop
|
||||||
@@ -431,6 +481,7 @@ async fn nostr_server(
|
|||||||
broadcast: Sender<Event>,
|
broadcast: Sender<Event>,
|
||||||
event_tx: mpsc::Sender<SubmittedEvent>,
|
event_tx: mpsc::Sender<SubmittedEvent>,
|
||||||
mut shutdown: Receiver<()>,
|
mut shutdown: Receiver<()>,
|
||||||
|
safe_to_read: Arc<Mutex<u64>>,
|
||||||
) {
|
) {
|
||||||
// the time this websocket nostr server started
|
// the time this websocket nostr server started
|
||||||
let orig_start = Instant::now();
|
let orig_start = Instant::now();
|
||||||
@@ -438,13 +489,28 @@ async fn nostr_server(
|
|||||||
let mut bcast_rx = broadcast.subscribe();
|
let mut bcast_rx = broadcast.subscribe();
|
||||||
// Track internal client state
|
// Track internal client state
|
||||||
let mut conn = conn::ClientConn::new(client_info.remote_ip);
|
let mut conn = conn::ClientConn::new(client_info.remote_ip);
|
||||||
|
// subscription creation rate limiting
|
||||||
|
let mut sub_lim_opt = None;
|
||||||
|
// 100ms jitter when the rate limiter returns
|
||||||
|
let jitter = Jitter::up_to(Duration::from_millis(100));
|
||||||
|
let sub_per_min_setting = settings.limits.subscriptions_per_min;
|
||||||
|
if let Some(sub_per_min) = sub_per_min_setting {
|
||||||
|
if sub_per_min > 0 {
|
||||||
|
trace!("Rate limits for sub creation ({}/min)", sub_per_min);
|
||||||
|
let quota_time = core::num::NonZeroU32::new(sub_per_min).unwrap();
|
||||||
|
let quota = Quota::per_minute(quota_time);
|
||||||
|
sub_lim_opt = Some(RateLimiter::direct(quota));
|
||||||
|
}
|
||||||
|
}
|
||||||
// Use the remote IP as the client identifier
|
// Use the remote IP as the client identifier
|
||||||
let cid = conn.get_client_prefix();
|
let cid = conn.get_client_prefix();
|
||||||
// Create a channel for receiving query results from the database.
|
// Create a channel for receiving query results from the database.
|
||||||
// we will send out the tx handle to any query we generate.
|
// we will send out the tx handle to any query we generate.
|
||||||
let (query_tx, mut query_rx) = mpsc::channel::<db::QueryResult>(256);
|
// this has capacity for some of the larger requests we see, which
|
||||||
|
// should allow the DB thread to release the handle earlier.
|
||||||
|
let (query_tx, mut query_rx) = mpsc::channel::<db::QueryResult>(20000);
|
||||||
// Create channel for receiving NOTICEs
|
// Create channel for receiving NOTICEs
|
||||||
let (notice_tx, mut notice_rx) = mpsc::channel::<Notice>(32);
|
let (notice_tx, mut notice_rx) = mpsc::channel::<Notice>(128);
|
||||||
|
|
||||||
// last time this client sent data (message, ping, etc.)
|
// last time this client sent data (message, ping, etc.)
|
||||||
let mut last_message_time = Instant::now();
|
let mut last_message_time = Instant::now();
|
||||||
@@ -462,16 +528,19 @@ async fn nostr_server(
|
|||||||
// when these subscriptions are cancelled, make a message
|
// when these subscriptions are cancelled, make a message
|
||||||
// available to the executing query so it knows to stop.
|
// available to the executing query so it knows to stop.
|
||||||
let mut running_queries: HashMap<String, oneshot::Sender<()>> = HashMap::new();
|
let mut running_queries: HashMap<String, oneshot::Sender<()>> = HashMap::new();
|
||||||
// keep track of the subscriptions we have
|
|
||||||
let mut current_subs: Vec<Subscription> = Vec::new();
|
|
||||||
// for stats, keep track of how many events the client published,
|
// for stats, keep track of how many events the client published,
|
||||||
// and how many it received from queries.
|
// and how many it received from queries.
|
||||||
let mut client_published_event_count: usize = 0;
|
let mut client_published_event_count: usize = 0;
|
||||||
let mut client_received_event_count: usize = 0;
|
let mut client_received_event_count: usize = 0;
|
||||||
debug!("new client connection (cid: {}, ip: {:?})", cid, conn.ip());
|
debug!("new client connection (cid: {}, ip: {:?})", cid, conn.ip());
|
||||||
if let Some(ua) = client_info.user_agent {
|
let origin = client_info.origin.unwrap_or_else(|| "<unspecified>".into());
|
||||||
debug!("cid: {}, user-agent: {:?}", cid, ua);
|
let user_agent = client_info
|
||||||
}
|
.user_agent
|
||||||
|
.unwrap_or_else(|| "<unspecified>".into());
|
||||||
|
debug!(
|
||||||
|
"cid: {}, origin: {:?}, user-agent: {:?}",
|
||||||
|
cid, origin, user_agent
|
||||||
|
);
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = shutdown.recv() => {
|
_ = shutdown.recv() => {
|
||||||
@@ -517,7 +586,7 @@ async fn nostr_server(
|
|||||||
// TODO: serialize at broadcast time, instead of
|
// TODO: serialize at broadcast time, instead of
|
||||||
// once for each consumer.
|
// once for each consumer.
|
||||||
if let Ok(event_str) = serde_json::to_string(&global_event) {
|
if let Ok(event_str) = serde_json::to_string(&global_event) {
|
||||||
debug!("sub match for client: {}, sub: {:?}, event: {:?}",
|
trace!("sub match for client: {}, sub: {:?}, event: {:?}",
|
||||||
cid, s,
|
cid, s,
|
||||||
global_event.get_event_id_prefix());
|
global_event.get_event_id_prefix());
|
||||||
// create an event response and send it
|
// create an event response and send it
|
||||||
@@ -606,12 +675,15 @@ async fn nostr_server(
|
|||||||
Ok(NostrMessage::SubMsg(s)) => {
|
Ok(NostrMessage::SubMsg(s)) => {
|
||||||
debug!("subscription requested (cid: {}, sub: {:?})", cid, s.id);
|
debug!("subscription requested (cid: {}, sub: {:?})", cid, s.id);
|
||||||
// subscription handling consists of:
|
// subscription handling consists of:
|
||||||
|
// * check for rate limits
|
||||||
// * registering the subscription so future events can be matched
|
// * registering the subscription so future events can be matched
|
||||||
// * making a channel to cancel to request later
|
// * making a channel to cancel to request later
|
||||||
// * sending a request for a SQL query
|
// * sending a request for a SQL query
|
||||||
// Do nothing if the sub already exists.
|
// Do nothing if the sub already exists.
|
||||||
if !current_subs.contains(&s) {
|
if !conn.has_subscription(&s) {
|
||||||
current_subs.push(s.clone());
|
if let Some(ref lim) = sub_lim_opt {
|
||||||
|
lim.until_ready_with_jitter(jitter).await;
|
||||||
|
}
|
||||||
let (abandon_query_tx, abandon_query_rx) = oneshot::channel::<()>();
|
let (abandon_query_tx, abandon_query_rx) = oneshot::channel::<()>();
|
||||||
match conn.subscribe(s.clone()) {
|
match conn.subscribe(s.clone()) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
@@ -619,27 +691,24 @@ async fn nostr_server(
|
|||||||
if let Some(previous_query) = running_queries.insert(s.id.to_owned(), abandon_query_tx) {
|
if let Some(previous_query) = running_queries.insert(s.id.to_owned(), abandon_query_tx) {
|
||||||
previous_query.send(()).ok();
|
previous_query.send(()).ok();
|
||||||
}
|
}
|
||||||
// start a database query
|
if s.needs_historical_events() {
|
||||||
db::db_query(s, cid.to_owned(), pool.clone(), query_tx.clone(), abandon_query_rx).await;
|
// start a database query. this spawns a blocking database query on a worker thread.
|
||||||
|
db::db_query(s, cid.to_owned(), pool.clone(), query_tx.clone(), abandon_query_rx,safe_to_read.clone()).await;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("Subscription error: {}", e);
|
info!("Subscription error: {} (cid: {}, sub: {:?})", e, cid, s.id);
|
||||||
ws_stream.send(make_notice_message(Notice::message(format!("Subscription error: {}", e)))).await.ok();
|
ws_stream.send(make_notice_message(Notice::message(format!("Subscription error: {}", e)))).await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
info!("client send duplicate subscription, ignoring (cid: {}, sub: {:?})", cid, s.id);
|
info!("client sent duplicate subscription, ignoring (cid: {}, sub: {:?})", cid, s.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Ok(NostrMessage::CloseMsg(cc)) => {
|
Ok(NostrMessage::CloseMsg(cc)) => {
|
||||||
// closing a request simply removes the subscription.
|
// closing a request simply removes the subscription.
|
||||||
let parsed : Result<Close> = Result::<Close>::from(cc);
|
let parsed : Result<Close> = Result::<Close>::from(cc);
|
||||||
if let Ok(c) = parsed {
|
if let Ok(c) = parsed {
|
||||||
// remove from the list of known subs
|
|
||||||
if let Some(pos) = current_subs.iter().position(|s| *s.id == c.id) {
|
|
||||||
current_subs.remove(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if a query is currently
|
// check if a query is currently
|
||||||
// running, and remove it if so.
|
// running, and remove it if so.
|
||||||
let stop_tx = running_queries.remove(&c.id);
|
let stop_tx = running_queries.remove(&c.id);
|
||||||
|
|||||||
+44
-2
@@ -65,12 +65,21 @@ impl<'de> Deserialize<'de> for ReqFilter {
|
|||||||
tags: None,
|
tags: None,
|
||||||
force_no_match: false,
|
force_no_match: false,
|
||||||
};
|
};
|
||||||
|
let empty_string = "".into();
|
||||||
let mut ts = None;
|
let mut ts = None;
|
||||||
// iterate through each key, and assign values that exist
|
// iterate through each key, and assign values that exist
|
||||||
for (key, val) in filter.into_iter() {
|
for (key, val) in filter.into_iter() {
|
||||||
// ids
|
// ids
|
||||||
if key == "ids" {
|
if key == "ids" {
|
||||||
rf.ids = Deserialize::deserialize(val).ok();
|
let raw_ids: Option<Vec<String>>= Deserialize::deserialize(val).ok();
|
||||||
|
if let Some(a) = raw_ids.as_ref() {
|
||||||
|
if a.contains(&empty_string) {
|
||||||
|
return Err(serde::de::Error::invalid_type(
|
||||||
|
Unexpected::Other("prefix matches must not be empty strings"),
|
||||||
|
&"a json object"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rf.ids =raw_ids;
|
||||||
} else if key == "kinds" {
|
} else if key == "kinds" {
|
||||||
rf.kinds = Deserialize::deserialize(val).ok();
|
rf.kinds = Deserialize::deserialize(val).ok();
|
||||||
} else if key == "since" {
|
} else if key == "since" {
|
||||||
@@ -80,7 +89,15 @@ impl<'de> Deserialize<'de> for ReqFilter {
|
|||||||
} else if key == "limit" {
|
} else if key == "limit" {
|
||||||
rf.limit = Deserialize::deserialize(val).ok();
|
rf.limit = Deserialize::deserialize(val).ok();
|
||||||
} else if key == "authors" {
|
} else if key == "authors" {
|
||||||
rf.authors = Deserialize::deserialize(val).ok();
|
let raw_authors: Option<Vec<String>>= Deserialize::deserialize(val).ok();
|
||||||
|
if let Some(a) = raw_authors.as_ref() {
|
||||||
|
if a.contains(&empty_string) {
|
||||||
|
return Err(serde::de::Error::invalid_type(
|
||||||
|
Unexpected::Other("prefix matches must not be empty strings"),
|
||||||
|
&"a json object"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rf.authors = raw_authors;
|
||||||
} else if key.starts_with('#') && key.len() > 1 && val.is_array() {
|
} else if key.starts_with('#') && key.len() > 1 && val.is_array() {
|
||||||
if let Some(tag_search) = tag_search_char_from_filter(key) {
|
if let Some(tag_search) = tag_search_char_from_filter(key) {
|
||||||
if ts.is_none() {
|
if ts.is_none() {
|
||||||
@@ -183,6 +200,13 @@ impl Subscription {
|
|||||||
pub fn get_id(&self) -> String {
|
pub fn get_id(&self) -> String {
|
||||||
self.id.clone()
|
self.id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine if any filter is requesting historical (database)
|
||||||
|
/// queries. If every filter has limit:0, we do not need to query the DB.
|
||||||
|
pub fn needs_historical_events(&self) -> bool {
|
||||||
|
self.filters.iter().any(|f| f.limit!=Some(0))
|
||||||
|
}
|
||||||
|
|
||||||
/// Determine if this subscription matches a given [`Event`]. Any
|
/// Determine if this subscription matches a given [`Event`]. Any
|
||||||
/// individual filter match is sufficient.
|
/// individual filter match is sufficient.
|
||||||
pub fn interested_in_event(&self, event: &Event) -> bool {
|
pub fn interested_in_event(&self, event: &Event) -> bool {
|
||||||
@@ -294,6 +318,24 @@ mod tests {
|
|||||||
assert!(serde_json::from_str::<Subscription>(raw_json).is_err());
|
assert!(serde_json::from_str::<Subscription>(raw_json).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn req_empty_authors_prefix() {
|
||||||
|
let raw_json = "[\"REQ\",\"some-id\",{\"authors\": [\"\"]}]";
|
||||||
|
assert!(serde_json::from_str::<Subscription>(raw_json).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn req_empty_ids_prefix() {
|
||||||
|
let raw_json = "[\"REQ\",\"some-id\",{\"ids\": [\"\"]}]";
|
||||||
|
assert!(serde_json::from_str::<Subscription>(raw_json).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn req_empty_ids_prefix_mixed() {
|
||||||
|
let raw_json = "[\"REQ\",\"some-id\",{\"ids\": [\"\",\"aaa\"]}]";
|
||||||
|
assert!(serde_json::from_str::<Subscription>(raw_json).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_filter() {
|
fn legacy_filter() {
|
||||||
// legacy field in filter
|
// legacy field in filter
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use nostr_rs_relay::cli::CLIArgs;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_tests() {
|
||||||
|
use clap::CommandFactory;
|
||||||
|
CLIArgs::command().debug_assert();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user