Compare commits

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