mirror of
https://github.com/scsibug/nostr-rs-relay.git
synced 2025-09-01 11:40:48 -04:00
Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d72af96d5f | ||
|
b4234eae25 | ||
|
d73cde2844 | ||
|
afbd7559e8 | ||
|
a6b48620fd | ||
|
d71f5cb029 | ||
|
1ed8cc08cc | ||
|
ff65ec2acd | ||
|
4461648c64 | ||
|
6329acd82b | ||
|
05411eb9e3 | ||
|
5a21890625 | ||
|
0d04b5eefa | ||
|
07198b2cb9 | ||
|
af6d101c21 | ||
|
5ad318e6e8 | ||
|
914ec77617 | ||
|
4f518fd0e7 | ||
|
b04ab76e73 | ||
|
39a3a258a0 | ||
|
44c6e3d88b | ||
|
767b76b2b3 | ||
|
c5fb16cd98 | ||
|
9c86f03902 | ||
|
971889f9a6 | ||
|
388eadf880 | ||
|
1ce029860c | ||
|
b7e10e26a2 | ||
|
ab736f5f98 | ||
|
b4471a6698 | ||
|
7120de4ff8 | ||
|
4ff77ab537 | ||
|
84f60f0abc | ||
|
8a67770206 | ||
|
7650f5f4a3 | ||
|
a7b169c0d3 | ||
|
24b1705a08 | ||
|
9d0a98f8bf |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
nostr.db
|
nostr.db
|
||||||
nostr.db-*
|
nostr.db-*
|
||||||
justfile
|
justfile
|
||||||
|
result
|
||||||
|
1934
Cargo.lock
generated
1934
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nostr-rs-relay"
|
name = "nostr-rs-relay"
|
||||||
version = "0.8.13"
|
version = "0.9.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Greg Heartsfield <scsibug@imap.cc>"]
|
authors = ["Greg Heartsfield <scsibug@imap.cc>"]
|
||||||
description = "A relay implementation for the Nostr protocol"
|
description = "A relay implementation for the Nostr protocol"
|
||||||
@@ -55,9 +55,11 @@ bech32 = "0.9.1"
|
|||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
qrcode = { version = "0.12.0", default-features = false, features = ["svg"] }
|
qrcode = { version = "0.12.0", default-features = false, features = ["svg"] }
|
||||||
nostr = { version = "0.18.0", default-features = false, features = ["base", "nip04", "nip19"] }
|
nostr = { version = "0.18.0", default-features = false, features = ["base", "nip04", "nip19"] }
|
||||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
|
||||||
tikv-jemallocator = "0.5"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
cln-rpc = "0.1.9"
|
||||||
|
|
||||||
|
[target.'cfg(all(not(target_env = "msvc"), not(target_os = "openbsd")))'.dependencies]
|
||||||
|
tikv-jemallocator = "0.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
14
README.md
14
README.md
@@ -11,7 +11,7 @@ mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
|
|||||||
|
|
||||||
[](https://builds.sr.ht/~gheartsfield/nostr-rs-relay/commits/master?)
|
[](https://builds.sr.ht/~gheartsfield/nostr-rs-relay/commits/master?)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -49,7 +49,7 @@ The examples below start a rootless podman container, mapping a local
|
|||||||
data directory and config file.
|
data directory and config file.
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ podman build -t nostr-rs-relay .
|
$ podman build --pull -t nostr-rs-relay .
|
||||||
|
|
||||||
$ mkdir data
|
$ mkdir data
|
||||||
|
|
||||||
@@ -98,6 +98,11 @@ The following OS packages will be helpful; on Debian/Ubuntu:
|
|||||||
$ sudo apt-get install build-essential cmake protobuf-compiler pkg-config libssl-dev
|
$ sudo apt-get install build-essential cmake protobuf-compiler pkg-config libssl-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On OpenBSD:
|
||||||
|
```console
|
||||||
|
$ doas pkg_add rust protobuf
|
||||||
|
```
|
||||||
|
|
||||||
Clone this repository, and then build a release version of the relay:
|
Clone this repository, and then build a release version of the relay:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
@@ -136,6 +141,7 @@ be mounted into a docker container like so:
|
|||||||
$ docker run -it -p 7000:8080 \
|
$ docker run -it -p 7000:8080 \
|
||||||
--mount src=$(pwd)/config.toml,target=/usr/src/app/config.toml,type=bind \
|
--mount src=$(pwd)/config.toml,target=/usr/src/app/config.toml,type=bind \
|
||||||
--mount src=$(pwd)/data,target=/usr/src/app/db,type=bind \
|
--mount src=$(pwd)/data,target=/usr/src/app/db,type=bind \
|
||||||
|
--mount src=$(pwd)/index.html,target=/usr/src/app/index.html,type=bind \
|
||||||
nostr-rs-relay
|
nostr-rs-relay
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -152,10 +158,6 @@ Proxy](docs/reverse-proxy.md).
|
|||||||
|
|
||||||
For development discussions, please feel free to use the [sourcehut
|
For development discussions, please feel free to use the [sourcehut
|
||||||
mailing list](https://lists.sr.ht/~gheartsfield/nostr-rs-relay-devel).
|
mailing list](https://lists.sr.ht/~gheartsfield/nostr-rs-relay-devel).
|
||||||
Or, drop by the [Nostr Telegram Channel](https://t.me/nostr_protocol).
|
|
||||||
|
|
||||||
To chat about `nostr-rs-relay` on `nostr` itself; visit our channel on [anigma](https://anigma.io/) or another client that supports [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md) chats:
|
|
||||||
* `2ad246a094fee48c6e455dd13d759d5f41b5a233120f5719d81ebc1935075194`
|
|
||||||
|
|
||||||
License
|
License
|
||||||
---
|
---
|
||||||
|
25
config.toml
25
config.toml
@@ -10,7 +10,7 @@ name = "nostr-rs-relay"
|
|||||||
# Description
|
# Description
|
||||||
description = "A newly created nostr-rs-relay.\n\nCustomize this with your own info."
|
description = "A newly created nostr-rs-relay.\n\nCustomize this with your own info."
|
||||||
|
|
||||||
# Administrative contact pubkey
|
# Administrative contact pubkey (32-byte hex, not npub)
|
||||||
#pubkey = "0c2d168a4ae8ca58c9f1ab237b5df682599c6c7ab74307ea8b05684b60405d41"
|
#pubkey = "0c2d168a4ae8ca58c9f1ab237b5df682599c6c7ab74307ea8b05684b60405d41"
|
||||||
|
|
||||||
# Administrative contact URI
|
# Administrative contact URI
|
||||||
@@ -23,6 +23,9 @@ description = "A newly created nostr-rs-relay.\n\nCustomize this with your own i
|
|||||||
# URL of Relay's icon.
|
# URL of Relay's icon.
|
||||||
#relay_icon = "https://example.test/img.png"
|
#relay_icon = "https://example.test/img.png"
|
||||||
|
|
||||||
|
# Path to custom relay html page
|
||||||
|
#relay_page = "index.html"
|
||||||
|
|
||||||
[diagnostics]
|
[diagnostics]
|
||||||
# Enable tokio tracing (for use with tokio-console)
|
# Enable tokio tracing (for use with tokio-console)
|
||||||
#tracing = false
|
#tracing = false
|
||||||
@@ -75,6 +78,11 @@ description = "A newly created nostr-rs-relay.\n\nCustomize this with your own i
|
|||||||
# `proto/nauthz.proto`.
|
# `proto/nauthz.proto`.
|
||||||
# event_admission_server = "http://[::1]:50051"
|
# event_admission_server = "http://[::1]:50051"
|
||||||
|
|
||||||
|
# If the event admission server denies writes
|
||||||
|
# in any case (excluding spam filtering).
|
||||||
|
# This is reflected in the relay information document.
|
||||||
|
# restricts_write = true
|
||||||
|
|
||||||
[network]
|
[network]
|
||||||
# Bind to this network address
|
# Bind to this network address
|
||||||
address = "0.0.0.0"
|
address = "0.0.0.0"
|
||||||
@@ -150,6 +158,11 @@ reject_future_seconds = 1800
|
|||||||
# 0, 1, 2, 3, 7, 40, 41, 42, 43, 44, 30023,
|
# 0, 1, 2, 3, 7, 40, 41, 42, 43, 44, 30023,
|
||||||
#]
|
#]
|
||||||
|
|
||||||
|
# Rejects imprecise requests (kind only and author only etc)
|
||||||
|
# This is a temperary measure to improve the adoption of outbox model
|
||||||
|
# Its recommended to have this enabled
|
||||||
|
limit_scrapers = false
|
||||||
|
|
||||||
[authorization]
|
[authorization]
|
||||||
# Pubkey addresses in this array are whitelisted for event publishing.
|
# Pubkey addresses in this array are whitelisted for event publishing.
|
||||||
# Only valid events by these authors will be accepted, if the variable
|
# Only valid events by these authors will be accepted, if the variable
|
||||||
@@ -193,20 +206,26 @@ reject_future_seconds = 1800
|
|||||||
# Enable pay to relay
|
# Enable pay to relay
|
||||||
#enabled = false
|
#enabled = false
|
||||||
|
|
||||||
|
# Node interface to use
|
||||||
|
#processor = "ClnRest/LNBits"
|
||||||
|
|
||||||
# The cost to be admitted to relay
|
# The cost to be admitted to relay
|
||||||
#admission_cost = 4200
|
#admission_cost = 4200
|
||||||
|
|
||||||
# The cost in sats per post
|
# The cost in sats per post
|
||||||
#cost_per_event = 0
|
#cost_per_event = 0
|
||||||
|
|
||||||
# Url of lnbits api
|
# Url of node api
|
||||||
#node_url = "<node url>"
|
#node_url = "<node url>"
|
||||||
|
|
||||||
# LNBits api secret
|
# LNBits api secret
|
||||||
#api_secret = "<ln bits api>"
|
#api_secret = "<ln bits api>"
|
||||||
|
|
||||||
|
# Path to CLN rune
|
||||||
|
#rune_path = "<rune path>"
|
||||||
|
|
||||||
# Nostr direct message on signup
|
# Nostr direct message on signup
|
||||||
#direct_message=true
|
#direct_message=false
|
||||||
|
|
||||||
# Terms of service
|
# Terms of service
|
||||||
#terms_message = """
|
#terms_message = """
|
||||||
|
@@ -30,7 +30,8 @@ To get the service running, we need to reload the systemd daemon and enable the
|
|||||||
|
|
||||||
1. `sudo systemctl daemon-reload`
|
1. `sudo systemctl daemon-reload`
|
||||||
2. `sudo systemctl start nostr-rs-relay.service`
|
2. `sudo systemctl start nostr-rs-relay.service`
|
||||||
3. `sudo systemctl status nostr-rs-relay.service`
|
3. `sudo systemctl enable nostr-rs-relay.service`
|
||||||
|
4. `sudo systemctl status nostr-rs-relay.service`
|
||||||
|
|
||||||
|
|
||||||
### Tips
|
### Tips
|
||||||
|
82
flake.lock
generated
Normal file
82
flake.lock
generated
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1719249093,
|
||||||
|
"narHash": "sha256-0q1haa3sw6GbmJ+WhogMnducZGjEaCa/iR6hF2vq80I=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "9791c77eb7e98b8d8ac5b0305d47282f994411ca",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710146030,
|
||||||
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1719254875,
|
||||||
|
"narHash": "sha256-ECni+IkwXjusHsm9Sexdtq8weAq/yUyt1TWIemXt3Ko=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "2893f56de08021cffd9b6b6dfc70fd9ccd51eb60",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
44
flake.nix
Normal file
44
flake.nix
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
description = "Nostr Relay written in Rust";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
|
crane = {
|
||||||
|
url = "github:ipetkov/crane";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = inputs@{ self, ... }:
|
||||||
|
inputs.flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = inputs.nixpkgs.legacyPackages.${system};
|
||||||
|
craneLib = inputs.crane.mkLib pkgs;
|
||||||
|
src = pkgs.lib.cleanSourceWith {
|
||||||
|
src = ./.;
|
||||||
|
filter = path: type:
|
||||||
|
(pkgs.lib.hasSuffix "\.proto" path) ||
|
||||||
|
# Default filter from crane (allow .rs files)
|
||||||
|
(craneLib.filterCargoSources path type)
|
||||||
|
;
|
||||||
|
};
|
||||||
|
crate = craneLib.buildPackage {
|
||||||
|
name = "nostr-rs-relay";
|
||||||
|
inherit src;
|
||||||
|
nativeBuildInputs = [ pkgs.pkg-config pkgs.protobuf ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
checks = {
|
||||||
|
inherit crate;
|
||||||
|
};
|
||||||
|
packages.default = crate;
|
||||||
|
formatter = pkgs.nixpkgs-fmt;
|
||||||
|
devShells.default = craneLib.devShell {
|
||||||
|
checks = self.checks.${system};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
@@ -15,7 +15,6 @@ use tracing::info;
|
|||||||
/// Bulk load JSONL data from STDIN to the database specified in config.toml (or ./nostr.db as a default).
|
/// Bulk load JSONL data from STDIN to the database specified in config.toml (or ./nostr.db as a default).
|
||||||
/// The database must already exist, this will not create a new one.
|
/// The database must already exist, this will not create a new one.
|
||||||
/// Tested against schema v13.
|
/// Tested against schema v13.
|
||||||
|
|
||||||
pub fn main() -> Result<()> {
|
pub fn main() -> Result<()> {
|
||||||
let _trace_sub = tracing_subscriber::fmt::try_init();
|
let _trace_sub = tracing_subscriber::fmt::try_init();
|
||||||
println!("Nostr-rs-relay Bulk Loader");
|
println!("Nostr-rs-relay Bulk Loader");
|
||||||
@@ -143,7 +142,7 @@ fn write_event(tx: &Transaction, e: Event) -> Result<usize> {
|
|||||||
let event_id = tx.last_insert_rowid();
|
let event_id = tx.last_insert_rowid();
|
||||||
// look at each event, and each tag, creating new tag entries if appropriate.
|
// look at each event, and each tag, creating new tag entries if appropriate.
|
||||||
for t in e.tags.iter().filter(|x| x.len() > 1) {
|
for t in e.tags.iter().filter(|x| x.len() > 1) {
|
||||||
let tagname = t.get(0).unwrap();
|
let tagname = t.first().unwrap();
|
||||||
let tagnamechar_opt = single_char_tagname(tagname);
|
let tagnamechar_opt = single_char_tagname(tagname);
|
||||||
if tagnamechar_opt.is_none() {
|
if tagnamechar_opt.is_none() {
|
||||||
continue;
|
continue;
|
||||||
|
@@ -14,6 +14,7 @@ pub struct Info {
|
|||||||
pub contact: Option<String>,
|
pub contact: Option<String>,
|
||||||
pub favicon: Option<String>,
|
pub favicon: Option<String>,
|
||||||
pub relay_icon: Option<String>,
|
pub relay_icon: Option<String>,
|
||||||
|
pub relay_page: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -32,6 +33,7 @@ pub struct Database {
|
|||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct Grpc {
|
pub struct Grpc {
|
||||||
pub event_admission_server: Option<String>,
|
pub event_admission_server: Option<String>,
|
||||||
|
pub restricts_write: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -73,6 +75,7 @@ pub struct Limits {
|
|||||||
pub event_persist_buffer: usize, // events to buffer for database commits (block senders if database writes are too slow)
|
pub event_persist_buffer: usize, // events to buffer for database commits (block senders if database writes are too slow)
|
||||||
pub event_kind_blacklist: Option<Vec<u64>>,
|
pub event_kind_blacklist: Option<Vec<u64>>,
|
||||||
pub event_kind_allowlist: Option<Vec<u64>>,
|
pub event_kind_allowlist: Option<Vec<u64>>,
|
||||||
|
pub limit_scrapers: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -96,6 +99,7 @@ pub struct PayToRelay {
|
|||||||
pub direct_message: bool, // Send direct message to user with invoice and terms
|
pub direct_message: bool, // Send direct message to user with invoice and terms
|
||||||
pub secret_key: Option<String>,
|
pub secret_key: Option<String>,
|
||||||
pub processor: Processor,
|
pub processor: Processor,
|
||||||
|
pub rune_path: Option<String>, // To access clightning API
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -245,17 +249,25 @@ impl Settings {
|
|||||||
|
|
||||||
// Validate pay to relay settings
|
// Validate pay to relay settings
|
||||||
if settings.pay_to_relay.enabled {
|
if settings.pay_to_relay.enabled {
|
||||||
assert_ne!(settings.pay_to_relay.api_secret, "");
|
if settings.pay_to_relay.processor == Processor::ClnRest {
|
||||||
|
assert!(settings
|
||||||
|
.pay_to_relay
|
||||||
|
.rune_path
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|path| path != "<rune path>"));
|
||||||
|
} else if settings.pay_to_relay.processor == Processor::LNBits {
|
||||||
|
assert_ne!(settings.pay_to_relay.api_secret, "");
|
||||||
|
}
|
||||||
// Should check that url is valid
|
// Should check that url is valid
|
||||||
assert_ne!(settings.pay_to_relay.node_url, "");
|
assert_ne!(settings.pay_to_relay.node_url, "");
|
||||||
assert_ne!(settings.pay_to_relay.terms_message, "");
|
assert_ne!(settings.pay_to_relay.terms_message, "");
|
||||||
|
|
||||||
if settings.pay_to_relay.direct_message {
|
if settings.pay_to_relay.direct_message {
|
||||||
assert_ne!(
|
assert!(settings
|
||||||
settings.pay_to_relay.secret_key,
|
.pay_to_relay
|
||||||
Some("<nostr nsec>".to_string())
|
.secret_key
|
||||||
);
|
.as_ref()
|
||||||
assert!(settings.pay_to_relay.secret_key.is_some());
|
.is_some_and(|key| key != "<nostr nsec>"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +286,7 @@ impl Default for Settings {
|
|||||||
contact: None,
|
contact: None,
|
||||||
favicon: None,
|
favicon: None,
|
||||||
relay_icon: None,
|
relay_icon: None,
|
||||||
|
relay_page: None,
|
||||||
},
|
},
|
||||||
diagnostics: Diagnostics { tracing: false },
|
diagnostics: Diagnostics { tracing: false },
|
||||||
database: Database {
|
database: Database {
|
||||||
@@ -287,6 +300,7 @@ impl Default for Settings {
|
|||||||
},
|
},
|
||||||
grpc: Grpc {
|
grpc: Grpc {
|
||||||
event_admission_server: None,
|
event_admission_server: None,
|
||||||
|
restricts_write: false,
|
||||||
},
|
},
|
||||||
network: Network {
|
network: Network {
|
||||||
port: 8080,
|
port: 8080,
|
||||||
@@ -306,6 +320,7 @@ impl Default for Settings {
|
|||||||
event_persist_buffer: 4096,
|
event_persist_buffer: 4096,
|
||||||
event_kind_blacklist: None,
|
event_kind_blacklist: None,
|
||||||
event_kind_allowlist: None,
|
event_kind_allowlist: None,
|
||||||
|
limit_scrapers: false,
|
||||||
},
|
},
|
||||||
authorization: Authorization {
|
authorization: Authorization {
|
||||||
pubkey_whitelist: None, // Allow any address to publish
|
pubkey_whitelist: None, // Allow any address to publish
|
||||||
@@ -319,8 +334,9 @@ impl Default for Settings {
|
|||||||
terms_message: "".to_string(),
|
terms_message: "".to_string(),
|
||||||
node_url: "".to_string(),
|
node_url: "".to_string(),
|
||||||
api_secret: "".to_string(),
|
api_secret: "".to_string(),
|
||||||
|
rune_path: None,
|
||||||
sign_ups: false,
|
sign_ups: false,
|
||||||
direct_message: true,
|
direct_message: false,
|
||||||
secret_key: None,
|
secret_key: None,
|
||||||
processor: Processor::LNBits,
|
processor: Processor::LNBits,
|
||||||
},
|
},
|
||||||
|
@@ -185,10 +185,10 @@ impl ClientConn {
|
|||||||
let mut relay: Option<&str> = None;
|
let mut relay: Option<&str> = None;
|
||||||
|
|
||||||
for tag in &event.tags {
|
for tag in &event.tags {
|
||||||
if tag.len() == 2 && tag.get(0) == Some(&"challenge".into()) {
|
if tag.len() == 2 && tag.first() == Some(&"challenge".into()) {
|
||||||
challenge = tag.get(1).map(|x| x.as_str());
|
challenge = tag.get(1).map(|x| x.as_str());
|
||||||
}
|
}
|
||||||
if tag.len() == 2 && tag.get(0) == Some(&"relay".into()) {
|
if tag.len() == 2 && tag.first() == Some(&"relay".into()) {
|
||||||
relay = tag.get(1).map(|x| x.as_str());
|
relay = tag.get(1).map(|x| x.as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
src/db.rs
23
src/db.rs
@@ -261,7 +261,7 @@ pub async fn db_writer(
|
|||||||
) => {
|
) => {
|
||||||
// User does not exist
|
// User does not exist
|
||||||
info!("Unregistered user");
|
info!("Unregistered user");
|
||||||
if settings.pay_to_relay.sign_ups {
|
if settings.pay_to_relay.sign_ups && settings.pay_to_relay.direct_message {
|
||||||
payment_tx
|
payment_tx
|
||||||
.send(PaymentMessage::NewAccount(event.pubkey))
|
.send(PaymentMessage::NewAccount(event.pubkey))
|
||||||
.ok();
|
.ok();
|
||||||
@@ -281,15 +281,6 @@ pub async fn db_writer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// send any metadata events to the NIP-05 verifier
|
|
||||||
if nip05_active && event.is_kind_metadata() {
|
|
||||||
// we are sending this prior to even deciding if we
|
|
||||||
// persist it. this allows the nip05 module to
|
|
||||||
// inspect it, update if necessary, or persist a new
|
|
||||||
// event and broadcast it itself.
|
|
||||||
metadata_tx.send(event.clone()).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
// get a validation result for use in verification and GPRC
|
// get a validation result for use in verification and GPRC
|
||||||
let validation = if nip05_active {
|
let validation = if nip05_active {
|
||||||
Some(repo.get_latest_user_verification(&event.pubkey).await)
|
Some(repo.get_latest_user_verification(&event.pubkey).await)
|
||||||
@@ -390,6 +381,15 @@ pub async fn db_writer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send any metadata events to the NIP-05 verifier
|
||||||
|
if nip05_active && event.is_kind_metadata() {
|
||||||
|
// we are sending this prior to even deciding if we
|
||||||
|
// persist it. this allows the nip05 module to
|
||||||
|
// inspect it, update if necessary, or persist a new
|
||||||
|
// event and broadcast it itself.
|
||||||
|
metadata_tx.send(event.clone()).ok();
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: cache recent list of authors to remove a DB call.
|
// TODO: cache recent list of authors to remove a DB call.
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
if event.is_ephemeral() {
|
if event.is_ephemeral() {
|
||||||
@@ -401,6 +401,9 @@ pub async fn db_writer(
|
|||||||
start.elapsed()
|
start.elapsed()
|
||||||
);
|
);
|
||||||
event_write = true;
|
event_write = true;
|
||||||
|
|
||||||
|
// send OK message
|
||||||
|
notice_tx.try_send(Notice::saved(event.id)).ok();
|
||||||
} else {
|
} else {
|
||||||
match repo.write_event(&event).await {
|
match repo.write_event(&event).await {
|
||||||
Ok(updated) => {
|
Ok(updated) => {
|
||||||
|
@@ -42,7 +42,7 @@ pub enum Error {
|
|||||||
CommandUnknownError,
|
CommandUnknownError,
|
||||||
#[error("SQL error")]
|
#[error("SQL error")]
|
||||||
SqlError(rusqlite::Error),
|
SqlError(rusqlite::Error),
|
||||||
#[error("Config error")]
|
#[error("Config error : {0}")]
|
||||||
ConfigError(config::ConfigError),
|
ConfigError(config::ConfigError),
|
||||||
#[error("Data directory does not exist")]
|
#[error("Data directory does not exist")]
|
||||||
DatabaseDirError,
|
DatabaseDirError,
|
||||||
|
24
src/event.rs
24
src/event.rs
@@ -160,11 +160,11 @@ impl Event {
|
|||||||
.tags
|
.tags
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| !x.is_empty())
|
.filter(|x| !x.is_empty())
|
||||||
.filter(|x| x.get(0).unwrap() == "expiration")
|
.filter(|x| x.first().unwrap() == "expiration")
|
||||||
.map(|x| x.get(1).unwrap_or(&default))
|
.map(|x| x.get(1).unwrap_or(&default))
|
||||||
.take(1)
|
.take(1)
|
||||||
.collect();
|
.collect();
|
||||||
let val_first = dvals.get(0);
|
let val_first = dvals.first();
|
||||||
val_first.and_then(|t| t.parse::<u64>().ok())
|
val_first.and_then(|t| t.parse::<u64>().ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,11 +192,11 @@ impl Event {
|
|||||||
.tags
|
.tags
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| !x.is_empty())
|
.filter(|x| !x.is_empty())
|
||||||
.filter(|x| x.get(0).unwrap() == "d")
|
.filter(|x| x.first().unwrap() == "d")
|
||||||
.map(|x| x.get(1).unwrap_or(&default))
|
.map(|x| x.get(1).unwrap_or(&default))
|
||||||
.take(1)
|
.take(1)
|
||||||
.collect();
|
.collect();
|
||||||
let dval_first = dvals.get(0);
|
let dval_first = dvals.first();
|
||||||
match dval_first {
|
match dval_first {
|
||||||
Some(_) => dval_first.map(|x| x.to_string()),
|
Some(_) => dval_first.map(|x| x.to_string()),
|
||||||
None => Some(default),
|
None => Some(default),
|
||||||
@@ -232,7 +232,7 @@ impl Event {
|
|||||||
.tags
|
.tags
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| x.len() == 4)
|
.filter(|x| x.len() == 4)
|
||||||
.filter(|x| x.get(0).unwrap() == "delegation")
|
.filter(|x| x.first().unwrap() == "delegation")
|
||||||
.take(1)
|
.take(1)
|
||||||
.next()?
|
.next()?
|
||||||
.clone(); // get first tag
|
.clone(); // get first tag
|
||||||
@@ -277,7 +277,7 @@ impl Event {
|
|||||||
let mut idx: HashMap<char, HashSet<String>> = HashMap::new();
|
let mut idx: HashMap<char, HashSet<String>> = HashMap::new();
|
||||||
// iterate over tags that have at least 2 elements
|
// iterate over tags that have at least 2 elements
|
||||||
for t in self.tags.iter().filter(|x| x.len() > 1) {
|
for t in self.tags.iter().filter(|x| x.len() > 1) {
|
||||||
let tagname = t.get(0).unwrap();
|
let tagname = t.first().unwrap();
|
||||||
let tagnamechar_opt = single_char_tagname(tagname);
|
let tagnamechar_opt = single_char_tagname(tagname);
|
||||||
if tagnamechar_opt.is_none() {
|
if tagnamechar_opt.is_none() {
|
||||||
continue;
|
continue;
|
||||||
@@ -285,7 +285,7 @@ impl Event {
|
|||||||
let tagnamechar = tagnamechar_opt.unwrap();
|
let tagnamechar = tagnamechar_opt.unwrap();
|
||||||
let tagval = t.get(1).unwrap();
|
let tagval = t.get(1).unwrap();
|
||||||
// ensure a vector exists for this tag
|
// ensure a vector exists for this tag
|
||||||
idx.entry(tagnamechar).or_insert_with(HashSet::new);
|
idx.entry(tagnamechar).or_default();
|
||||||
// get the tag vec and insert entry
|
// get the tag vec and insert entry
|
||||||
let idx_tag_vec = idx.get_mut(&tagnamechar).expect("could not get tag vector");
|
let idx_tag_vec = idx.get_mut(&tagnamechar).expect("could not get tag vector");
|
||||||
idx_tag_vec.insert(tagval.clone());
|
idx_tag_vec.insert(tagval.clone());
|
||||||
@@ -310,7 +310,7 @@ impl Event {
|
|||||||
self.tags
|
self.tags
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| x.len() > 1)
|
.filter(|x| x.len() > 1)
|
||||||
.filter(|x| x.get(0).unwrap() == tag_name)
|
.filter(|x| x.first().unwrap() == tag_name)
|
||||||
.map(|x| x.get(1).unwrap().clone())
|
.map(|x| x.get(1).unwrap().clone())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -472,12 +472,8 @@ mod tests {
|
|||||||
let mut event = Event::simple_event();
|
let mut event = Event::simple_event();
|
||||||
event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]];
|
event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]];
|
||||||
event.build_index();
|
event.build_index();
|
||||||
assert!(
|
assert!(event
|
||||||
event.generic_tag_val_intersect(
|
.generic_tag_val_intersect('e', &HashSet::from(["foo".to_owned(), "bar".to_owned()])));
|
||||||
'e',
|
|
||||||
&HashSet::from(["foo".to_owned(), "bar".to_owned()])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
159
src/hexrange.rs
159
src/hexrange.rs
@@ -1,159 +0,0 @@
|
|||||||
//! Utilities for searching hexadecimal
|
|
||||||
use crate::utils::is_hex;
|
|
||||||
use hex;
|
|
||||||
|
|
||||||
/// Types of hexadecimal queries.
|
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)]
|
|
||||||
pub enum HexSearch {
|
|
||||||
// when no range is needed, exact 32-byte
|
|
||||||
Exact(Vec<u8>),
|
|
||||||
// lower (inclusive) and upper range (exclusive)
|
|
||||||
Range(Vec<u8>, Vec<u8>),
|
|
||||||
// lower bound only, upper bound is MAX inclusive
|
|
||||||
LowerOnly(Vec<u8>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a string contains only f chars
|
|
||||||
fn is_all_fs(s: &str) -> bool {
|
|
||||||
s.chars().all(|x| x == 'f' || x == 'F')
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the next hex sequence greater than the argument.
|
|
||||||
#[must_use]
|
|
||||||
pub fn hex_range(s: &str) -> Option<HexSearch> {
|
|
||||||
let mut hash_base = s.to_owned();
|
|
||||||
if !is_hex(&hash_base) || hash_base.len() > 64 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if hash_base.len() == 64 {
|
|
||||||
return Some(HexSearch::Exact(hex::decode(&hash_base).ok()?));
|
|
||||||
}
|
|
||||||
// if s is odd, add a zero
|
|
||||||
let mut odd = hash_base.len() % 2 != 0;
|
|
||||||
if odd {
|
|
||||||
// extend the string to make it even
|
|
||||||
hash_base.push('0');
|
|
||||||
}
|
|
||||||
let base = hex::decode(hash_base).ok()?;
|
|
||||||
// check for all ff's
|
|
||||||
if is_all_fs(s) {
|
|
||||||
// there is no higher bound, we only want to search for blobs greater than this.
|
|
||||||
return Some(HexSearch::LowerOnly(base));
|
|
||||||
}
|
|
||||||
|
|
||||||
// return a range
|
|
||||||
let mut upper = base.clone();
|
|
||||||
let mut byte_len = upper.len();
|
|
||||||
|
|
||||||
// for odd strings, we made them longer, but we want to increment the upper char (+16).
|
|
||||||
// we know we can do this without overflowing because we explicitly set the bottom half to 0's.
|
|
||||||
while byte_len > 0 {
|
|
||||||
byte_len -= 1;
|
|
||||||
// check if byte can be incremented, or if we need to carry.
|
|
||||||
let b = upper[byte_len];
|
|
||||||
if b == u8::MAX {
|
|
||||||
// reset and carry
|
|
||||||
upper[byte_len] = 0;
|
|
||||||
} else if odd {
|
|
||||||
// check if first char in this byte is NOT 'f'
|
|
||||||
if b < 240 {
|
|
||||||
// bump up the first character in this byte
|
|
||||||
upper[byte_len] = b + 16;
|
|
||||||
// increment done, stop iterating through the vec
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// if it is 'f', reset the byte to 0 and do a carry
|
|
||||||
// reset and carry
|
|
||||||
upper[byte_len] = 0;
|
|
||||||
// done with odd logic, so don't repeat this
|
|
||||||
odd = false;
|
|
||||||
} else {
|
|
||||||
// bump up the first character in this byte
|
|
||||||
upper[byte_len] = b + 1;
|
|
||||||
// increment done, stop iterating
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(HexSearch::Range(base, upper))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::error::Result;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hex_range_exact() -> Result<()> {
|
|
||||||
let hex = "abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00";
|
|
||||||
let r = hex_range(hex);
|
|
||||||
assert_eq!(
|
|
||||||
r,
|
|
||||||
Some(HexSearch::Exact(hex::decode(hex).expect("invalid hex")))
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn hex_full_range() -> Result<()> {
|
|
||||||
let hex = "aaaa";
|
|
||||||
let hex_upper = "aaab";
|
|
||||||
let r = hex_range(hex);
|
|
||||||
assert_eq!(
|
|
||||||
r,
|
|
||||||
Some(HexSearch::Range(
|
|
||||||
hex::decode(hex).expect("invalid hex"),
|
|
||||||
hex::decode(hex_upper).expect("invalid hex")
|
|
||||||
))
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hex_full_range_odd() -> Result<()> {
|
|
||||||
let r = hex_range("abc");
|
|
||||||
assert_eq!(
|
|
||||||
r,
|
|
||||||
Some(HexSearch::Range(
|
|
||||||
hex::decode("abc0").expect("invalid hex"),
|
|
||||||
hex::decode("abd0").expect("invalid hex")
|
|
||||||
))
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hex_full_range_odd_end_f() -> Result<()> {
|
|
||||||
let r = hex_range("abf");
|
|
||||||
assert_eq!(
|
|
||||||
r,
|
|
||||||
Some(HexSearch::Range(
|
|
||||||
hex::decode("abf0").expect("invalid hex"),
|
|
||||||
hex::decode("ac00").expect("invalid hex")
|
|
||||||
))
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hex_no_upper() -> Result<()> {
|
|
||||||
let r = hex_range("ffff");
|
|
||||||
assert_eq!(
|
|
||||||
r,
|
|
||||||
Some(HexSearch::LowerOnly(
|
|
||||||
hex::decode("ffff").expect("invalid hex")
|
|
||||||
))
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hex_no_upper_odd() -> Result<()> {
|
|
||||||
let r = hex_range("fff");
|
|
||||||
assert_eq!(
|
|
||||||
r,
|
|
||||||
Some(HexSearch::LowerOnly(
|
|
||||||
hex::decode("fff0").expect("invalid hex")
|
|
||||||
))
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
17
src/info.rs
17
src/info.rs
@@ -4,7 +4,7 @@ use crate::config::Settings;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
|
pub const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
|
||||||
pub const UNIT: &str = "sats";
|
pub const UNIT: &str = "msats";
|
||||||
|
|
||||||
/// Limitations of the relay as specified in NIP-111
|
/// Limitations of the relay as specified in NIP-111
|
||||||
/// (This nip isn't finalized so may change)
|
/// (This nip isn't finalized so may change)
|
||||||
@@ -13,6 +13,9 @@ pub const UNIT: &str = "sats";
|
|||||||
pub struct Limitation {
|
pub struct Limitation {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
payment_required: Option<bool>,
|
payment_required: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
restricted_writes: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -45,7 +48,7 @@ pub struct RelayInfo {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub contact: Option<String>,
|
pub contact: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub supported_nips: Option<Vec<i64>>,
|
pub supported_nips: Option<Vec<i64>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -75,12 +78,18 @@ impl From<Settings> for RelayInfo {
|
|||||||
|
|
||||||
let limitations = Limitation {
|
let limitations = Limitation {
|
||||||
payment_required: Some(p.enabled),
|
payment_required: Some(p.enabled),
|
||||||
|
restricted_writes: Some(
|
||||||
|
p.enabled
|
||||||
|
|| c.verified_users.is_enabled()
|
||||||
|
|| c.authorization.pubkey_whitelist.is_some()
|
||||||
|
|| c.grpc.restricts_write,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (payment_url, fees) = if p.enabled {
|
let (payment_url, fees) = if p.enabled {
|
||||||
let admission_fee = if p.admission_cost > 0 {
|
let admission_fee = if p.admission_cost > 0 {
|
||||||
Some(vec![Fee {
|
Some(vec![Fee {
|
||||||
amount: p.admission_cost,
|
amount: p.admission_cost * 1000,
|
||||||
unit: UNIT.to_string(),
|
unit: UNIT.to_string(),
|
||||||
}])
|
}])
|
||||||
} else {
|
} else {
|
||||||
@@ -89,7 +98,7 @@ impl From<Settings> for RelayInfo {
|
|||||||
|
|
||||||
let post_fee = if p.cost_per_event > 0 {
|
let post_fee = if p.cost_per_event > 0 {
|
||||||
Some(vec![Fee {
|
Some(vec![Fee {
|
||||||
amount: p.cost_per_event,
|
amount: p.cost_per_event * 1000,
|
||||||
unit: UNIT.to_string(),
|
unit: UNIT.to_string(),
|
||||||
}])
|
}])
|
||||||
} else {
|
} else {
|
||||||
|
@@ -6,7 +6,6 @@ pub mod db;
|
|||||||
pub mod delegation;
|
pub mod delegation;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod hexrange;
|
|
||||||
pub mod info;
|
pub mod info;
|
||||||
pub mod nauthz;
|
pub mod nauthz;
|
||||||
pub mod nip05;
|
pub mod nip05;
|
||||||
|
@@ -10,13 +10,13 @@ use std::process;
|
|||||||
use std::sync::mpsc as syncmpsc;
|
use std::sync::mpsc as syncmpsc;
|
||||||
use std::sync::mpsc::{Receiver as MpscReceiver, Sender as MpscSender};
|
use std::sync::mpsc::{Receiver as MpscReceiver, Sender as MpscSender};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
#[cfg(not(target_env = "msvc"))]
|
#[cfg(all(not(target_env = "msvc"), not(target_os = "openbsd")))]
|
||||||
use tikv_jemallocator::Jemalloc;
|
use tikv_jemallocator::Jemalloc;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing_appender::non_blocking::WorkerGuard;
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
#[cfg(not(target_env = "msvc"))]
|
#[cfg(all(not(target_env = "msvc"), not(target_os = "openbsd")))]
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static GLOBAL: Jemalloc = Jemalloc;
|
static GLOBAL: Jemalloc = Jemalloc;
|
||||||
|
|
||||||
|
86
src/nip05.rs
86
src/nip05.rs
@@ -121,7 +121,7 @@ fn body_contains_user(username: &str, address: &str, bytes: &hyper::body::Bytes)
|
|||||||
// get the pubkey for the requested user
|
// get the pubkey for the requested user
|
||||||
let check_name = names_map.get(username).and_then(serde_json::Value::as_str);
|
let check_name = names_map.get(username).and_then(serde_json::Value::as_str);
|
||||||
// ensure the address is a match
|
// ensure the address is a match
|
||||||
Ok(check_name.map_or(false, |x| x == address))
|
Ok(check_name == Some(address))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Verifier {
|
impl Verifier {
|
||||||
@@ -208,7 +208,7 @@ impl Verifier {
|
|||||||
.ok_or_else(|| Error::CustomError("invalid NIP-05 URL".to_owned()))?;
|
.ok_or_else(|| Error::CustomError("invalid NIP-05 URL".to_owned()))?;
|
||||||
let req = hyper::Request::builder()
|
let req = hyper::Request::builder()
|
||||||
.method(hyper::Method::GET)
|
.method(hyper::Method::GET)
|
||||||
.uri(url)
|
.uri(url.clone())
|
||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
.header(
|
.header(
|
||||||
"User-Agent",
|
"User-Agent",
|
||||||
@@ -226,38 +226,84 @@ impl Verifier {
|
|||||||
// limit size of verification document to 1MB.
|
// limit size of verification document to 1MB.
|
||||||
const MAX_ALLOWED_RESPONSE_SIZE: u64 = 1024 * 1024;
|
const MAX_ALLOWED_RESPONSE_SIZE: u64 = 1024 * 1024;
|
||||||
let response = response_res?;
|
let response = response_res?;
|
||||||
|
let status = response.status();
|
||||||
|
|
||||||
|
// Log non-2XX status codes
|
||||||
|
if !status.is_success() {
|
||||||
|
info!(
|
||||||
|
"unexpected status code {} received for account {:?} at URL: {}",
|
||||||
|
status,
|
||||||
|
nip.to_string(),
|
||||||
|
url
|
||||||
|
);
|
||||||
|
return Ok(UserWebVerificationStatus::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
// determine content length from response
|
// determine content length from response
|
||||||
let response_content_length = match response.body().size_hint().upper() {
|
let response_content_length = match response.body().size_hint().upper() {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => MAX_ALLOWED_RESPONSE_SIZE + 1, // reject missing content length
|
None => {
|
||||||
|
info!(
|
||||||
|
"missing content length header for account {:?} at URL: {}",
|
||||||
|
nip.to_string(),
|
||||||
|
url
|
||||||
|
);
|
||||||
|
return Ok(UserWebVerificationStatus::Unknown);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// TODO: test how hyper handles the client providing an inaccurate content-length.
|
|
||||||
if response_content_length <= MAX_ALLOWED_RESPONSE_SIZE {
|
if response_content_length > MAX_ALLOWED_RESPONSE_SIZE {
|
||||||
let (parts, body) = response.into_parts();
|
info!(
|
||||||
// TODO: consider redirects
|
"content length {} exceeded limit of {} bytes for account {:?} at URL: {}",
|
||||||
if parts.status == http::StatusCode::OK {
|
response_content_length,
|
||||||
// parse body, determine if the username / key / address is present
|
MAX_ALLOWED_RESPONSE_SIZE,
|
||||||
let body_bytes = hyper::body::to_bytes(body).await?;
|
nip.to_string(),
|
||||||
let body_matches = body_contains_user(&nip.local, pubkey, &body_bytes)?;
|
url
|
||||||
if body_matches {
|
);
|
||||||
return Ok(UserWebVerificationStatus::Verified);
|
return Ok(UserWebVerificationStatus::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (parts, body) = response.into_parts();
|
||||||
|
// TODO: consider redirects
|
||||||
|
if parts.status == http::StatusCode::OK {
|
||||||
|
// parse body, determine if the username / key / address is present
|
||||||
|
let body_bytes = match hyper::body::to_bytes(body).await {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(e) => {
|
||||||
|
info!(
|
||||||
|
"failed to read response body for account {:?} at URL: {}: {:?}",
|
||||||
|
nip.to_string(),
|
||||||
|
url,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return Ok(UserWebVerificationStatus::Unknown);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match body_contains_user(&nip.local, pubkey, &body_bytes) {
|
||||||
|
Ok(true) => Ok(UserWebVerificationStatus::Verified),
|
||||||
|
Ok(false) => Ok(UserWebVerificationStatus::Unverified),
|
||||||
|
Err(e) => {
|
||||||
|
info!(
|
||||||
|
"error parsing response body for account {:?}: {:?}",
|
||||||
|
nip.to_string(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
Ok(UserWebVerificationStatus::Unknown)
|
||||||
}
|
}
|
||||||
// successful response, parsed as a nip-05
|
|
||||||
// document, but this name/pubkey was not
|
|
||||||
// present.
|
|
||||||
return Ok(UserWebVerificationStatus::Unverified);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
info!(
|
info!(
|
||||||
"content length missing or exceeded limits for account: {:?}",
|
"unexpected status code {} for account {:?}",
|
||||||
|
parts.status,
|
||||||
nip.to_string()
|
nip.to_string()
|
||||||
);
|
);
|
||||||
|
Ok(UserWebVerificationStatus::Unknown)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
info!("timeout verifying account {:?}", nip);
|
info!("timeout verifying account {:?}", nip);
|
||||||
return Ok(UserWebVerificationStatus::Unknown);
|
Ok(UserWebVerificationStatus::Unknown)
|
||||||
}
|
}
|
||||||
Ok(UserWebVerificationStatus::Unknown)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform NIP-05 verifier tasks.
|
/// Perform NIP-05 verifier tasks.
|
||||||
|
@@ -25,7 +25,9 @@ impl EventResultStatus {
|
|||||||
pub fn to_bool(&self) -> bool {
|
pub fn to_bool(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Duplicate | Self::Saved => true,
|
Self::Duplicate | Self::Saved => true,
|
||||||
Self::Invalid | Self::Blocked | Self::RateLimited | Self::Error | Self::Restricted => false,
|
Self::Invalid | Self::Blocked | Self::RateLimited | Self::Error | Self::Restricted => {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
137
src/payment/cln_rest.rs
Normal file
137
src/payment/cln_rest.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
use std::{fs, str::FromStr};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use cln_rpc::{
|
||||||
|
model::{
|
||||||
|
requests::InvoiceRequest,
|
||||||
|
responses::{InvoiceResponse, ListinvoicesInvoicesStatus, ListinvoicesResponse},
|
||||||
|
},
|
||||||
|
primitives::{Amount, AmountOrAny},
|
||||||
|
};
|
||||||
|
use config::ConfigError;
|
||||||
|
use http::{header::CONTENT_TYPE, HeaderValue, Uri};
|
||||||
|
use hyper::{client::HttpConnector, Client};
|
||||||
|
use hyper_rustls::HttpsConnector;
|
||||||
|
use nostr::Keys;
|
||||||
|
use rand::random;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Settings,
|
||||||
|
error::{Error, Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{InvoiceInfo, InvoiceStatus, PaymentProcessor};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ClnRestPaymentProcessor {
|
||||||
|
client: hyper::Client<HttpsConnector<HttpConnector>, hyper::Body>,
|
||||||
|
settings: Settings,
|
||||||
|
rune_header: HeaderValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClnRestPaymentProcessor {
|
||||||
|
pub fn new(settings: &Settings) -> Result<Self> {
|
||||||
|
let rune_path = settings
|
||||||
|
.pay_to_relay
|
||||||
|
.rune_path
|
||||||
|
.clone()
|
||||||
|
.ok_or(ConfigError::NotFound("rune_path".to_string()))?;
|
||||||
|
let rune = String::from_utf8(fs::read(rune_path)?)
|
||||||
|
.map_err(|_| ConfigError::Message("Rune should be UTF8".to_string()))?;
|
||||||
|
let mut rune_header = HeaderValue::from_str(rune.trim())
|
||||||
|
.map_err(|_| ConfigError::Message("Invalid Rune header".to_string()))?;
|
||||||
|
rune_header.set_sensitive(true);
|
||||||
|
|
||||||
|
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||||
|
.with_native_roots()
|
||||||
|
.https_only()
|
||||||
|
.enable_http1()
|
||||||
|
.build();
|
||||||
|
let client = Client::builder().build::<_, hyper::Body>(https);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
settings: settings.clone(),
|
||||||
|
rune_header,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PaymentProcessor for ClnRestPaymentProcessor {
|
||||||
|
async fn get_invoice(&self, key: &Keys, amount: u64) -> Result<InvoiceInfo, Error> {
|
||||||
|
let random_number: u16 = random();
|
||||||
|
let memo = format!("{}: {}", random_number, key.public_key());
|
||||||
|
|
||||||
|
let body = InvoiceRequest {
|
||||||
|
cltv: None,
|
||||||
|
deschashonly: None,
|
||||||
|
expiry: None,
|
||||||
|
preimage: None,
|
||||||
|
exposeprivatechannels: None,
|
||||||
|
fallbacks: None,
|
||||||
|
amount_msat: AmountOrAny::Amount(Amount::from_sat(amount)),
|
||||||
|
description: memo.clone(),
|
||||||
|
label: "Nostr".to_string(),
|
||||||
|
};
|
||||||
|
let uri = Uri::from_str(&format!(
|
||||||
|
"{}/v1/invoice",
|
||||||
|
&self.settings.pay_to_relay.node_url
|
||||||
|
))
|
||||||
|
.map_err(|_| ConfigError::Message("Bad node URL".to_string()))?;
|
||||||
|
|
||||||
|
let req = hyper::Request::builder()
|
||||||
|
.method(hyper::Method::POST)
|
||||||
|
.uri(uri)
|
||||||
|
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
|
||||||
|
.header("Rune", self.rune_header.clone())
|
||||||
|
.body(hyper::Body::from(serde_json::to_string(&body)?))
|
||||||
|
.expect("request builder");
|
||||||
|
|
||||||
|
let res = self.client.request(req).await?;
|
||||||
|
|
||||||
|
let body = hyper::body::to_bytes(res.into_body()).await?;
|
||||||
|
let invoice_response: InvoiceResponse = serde_json::from_slice(&body)?;
|
||||||
|
|
||||||
|
Ok(InvoiceInfo {
|
||||||
|
pubkey: key.public_key().to_string(),
|
||||||
|
payment_hash: invoice_response.payment_hash.to_string(),
|
||||||
|
bolt11: invoice_response.bolt11,
|
||||||
|
amount,
|
||||||
|
memo,
|
||||||
|
status: InvoiceStatus::Unpaid,
|
||||||
|
confirmed_at: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_invoice(&self, payment_hash: &str) -> Result<InvoiceStatus, Error> {
|
||||||
|
let uri = Uri::from_str(&format!(
|
||||||
|
"{}/v1/listinvoices?payment_hash={}",
|
||||||
|
&self.settings.pay_to_relay.node_url, payment_hash
|
||||||
|
))
|
||||||
|
.map_err(|_| ConfigError::Message("Bad node URL".to_string()))?;
|
||||||
|
|
||||||
|
let req = hyper::Request::builder()
|
||||||
|
.method(hyper::Method::POST)
|
||||||
|
.uri(uri)
|
||||||
|
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
|
||||||
|
.header("Rune", self.rune_header.clone())
|
||||||
|
.body(hyper::Body::empty())
|
||||||
|
.expect("request builder");
|
||||||
|
|
||||||
|
let res = self.client.request(req).await?;
|
||||||
|
|
||||||
|
let body = hyper::body::to_bytes(res.into_body()).await?;
|
||||||
|
let invoice_response: ListinvoicesResponse = serde_json::from_slice(&body)?;
|
||||||
|
let invoice = invoice_response
|
||||||
|
.invoices
|
||||||
|
.first()
|
||||||
|
.ok_or(Error::CustomError("Invoice not found".to_string()))?;
|
||||||
|
let status = match invoice.status {
|
||||||
|
ListinvoicesInvoicesStatus::PAID => InvoiceStatus::Paid,
|
||||||
|
ListinvoicesInvoicesStatus::UNPAID => InvoiceStatus::Unpaid,
|
||||||
|
ListinvoicesInvoicesStatus::EXPIRED => InvoiceStatus::Expired,
|
||||||
|
};
|
||||||
|
Ok(status)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::event::Event;
|
use crate::event::Event;
|
||||||
|
use crate::payment::cln_rest::ClnRestPaymentProcessor;
|
||||||
use crate::payment::lnbits::LNBitsPaymentProcessor;
|
use crate::payment::lnbits::LNBitsPaymentProcessor;
|
||||||
use crate::repo::NostrRepo;
|
use crate::repo::NostrRepo;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -10,6 +11,7 @@ use async_trait::async_trait;
|
|||||||
use nostr::key::{FromPkStr, FromSkStr};
|
use nostr::key::{FromPkStr, FromSkStr};
|
||||||
use nostr::{key::Keys, Event as NostrEvent, EventBuilder};
|
use nostr::{key::Keys, Event as NostrEvent, EventBuilder};
|
||||||
|
|
||||||
|
pub mod cln_rest;
|
||||||
pub mod lnbits;
|
pub mod lnbits;
|
||||||
|
|
||||||
/// Payment handler
|
/// Payment handler
|
||||||
@@ -41,6 +43,7 @@ pub trait PaymentProcessor: Send + Sync {
|
|||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
pub enum Processor {
|
pub enum Processor {
|
||||||
LNBits,
|
LNBits,
|
||||||
|
ClnRest,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Possible states of an invoice
|
/// Possible states of an invoice
|
||||||
@@ -52,12 +55,12 @@ pub enum InvoiceStatus {
|
|||||||
Expired,
|
Expired,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for InvoiceStatus {
|
impl std::fmt::Display for InvoiceStatus {
|
||||||
fn to_string(&self) -> String {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
InvoiceStatus::Paid => "Paid".to_string(),
|
InvoiceStatus::Paid => write!(f, "Paid"),
|
||||||
InvoiceStatus::Unpaid => "Unpaid".to_string(),
|
InvoiceStatus::Unpaid => write!(f, "Unpaid"),
|
||||||
InvoiceStatus::Expired => "Expired".to_string(),
|
InvoiceStatus::Expired => write!(f, "Expired"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,8 +112,9 @@ impl Payment {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create processor kind defined in settings
|
// Create processor kind defined in settings
|
||||||
let processor = match &settings.pay_to_relay.processor {
|
let processor: Arc<dyn PaymentProcessor> = match &settings.pay_to_relay.processor {
|
||||||
Processor::LNBits => Arc::new(LNBitsPaymentProcessor::new(&settings)),
|
Processor::LNBits => Arc::new(LNBitsPaymentProcessor::new(&settings)),
|
||||||
|
Processor::ClnRest => Arc::new(ClnRestPaymentProcessor::new(&settings)?),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Payment {
|
Ok(Payment {
|
||||||
|
@@ -14,7 +14,6 @@ use sqlx::{Error, Execute, FromRow, Postgres, QueryBuilder, Row};
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::error;
|
use crate::error;
|
||||||
use crate::hexrange::{hex_range, HexSearch};
|
|
||||||
use crate::repo::postgres_migration::run_migrations;
|
use crate::repo::postgres_migration::run_migrations;
|
||||||
use crate::server::NostrMetrics;
|
use crate::server::NostrMetrics;
|
||||||
use crate::utils::{self, is_hex, is_lower_hex};
|
use crate::utils::{self, is_hex, is_lower_hex};
|
||||||
@@ -178,30 +177,25 @@ ON CONFLICT (id) DO NOTHING"#,
|
|||||||
let tag_val = &tag[1];
|
let tag_val = &tag[1];
|
||||||
// only single-char tags are searchable
|
// only single-char tags are searchable
|
||||||
let tag_char_opt = single_char_tagname(tag_name);
|
let tag_char_opt = single_char_tagname(tag_name);
|
||||||
match &tag_char_opt {
|
if tag_char_opt.is_some() {
|
||||||
Some(_) => {
|
// if tag value is lowercase hex;
|
||||||
// if tag value is lowercase hex;
|
if is_lower_hex(tag_val) && (tag_val.len() % 2 == 0) {
|
||||||
if is_lower_hex(tag_val) && (tag_val.len() % 2 == 0) {
|
sqlx::query("INSERT INTO tag (event_id, \"name\", value, value_hex) VALUES($1, $2, NULL, $3) \
|
||||||
sqlx::query("INSERT INTO tag (event_id, \"name\", value, value_hex) VALUES($1, $2, NULL, $3) \
|
ON CONFLICT (event_id, \"name\", value, value_hex) DO NOTHING")
|
||||||
ON CONFLICT (event_id, \"name\", value, value_hex) DO NOTHING")
|
|
||||||
.bind(&id_blob)
|
.bind(&id_blob)
|
||||||
.bind(tag_name)
|
.bind(tag_name)
|
||||||
.bind(hex::decode(tag_val).ok())
|
.bind(hex::decode(tag_val).ok())
|
||||||
.execute(&mut tx)
|
.execute(&mut tx)
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
} else {
|
||||||
} else {
|
sqlx::query("INSERT INTO tag (event_id, \"name\", value, value_hex) VALUES($1, $2, $3, NULL) \
|
||||||
sqlx::query("INSERT INTO tag (event_id, \"name\", value, value_hex) VALUES($1, $2, $3, NULL) \
|
ON CONFLICT (event_id, \"name\", value, value_hex) DO NOTHING")
|
||||||
ON CONFLICT (event_id, \"name\", value, value_hex) DO NOTHING")
|
|
||||||
.bind(&id_blob)
|
.bind(&id_blob)
|
||||||
.bind(tag_name)
|
.bind(tag_name)
|
||||||
.bind(tag_val.as_bytes())
|
.bind(tag_val.as_bytes())
|
||||||
.execute(&mut tx)
|
.execute(&mut tx)
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -637,14 +631,14 @@ ON CONFLICT (id) DO NOTHING"#,
|
|||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO invoice (pubkey, payment_hash, amount, status, description, created_at, invoice) VALUES ($1, $2, $3, $4, $5, now(), $6)",
|
"INSERT INTO invoice (pubkey, payment_hash, amount, status, description, created_at, invoice) VALUES ($1, $2, $3, $4, $5, now(), $6)",
|
||||||
)
|
)
|
||||||
.bind(pub_key)
|
.bind(pub_key)
|
||||||
.bind(invoice_info.payment_hash)
|
.bind(invoice_info.payment_hash)
|
||||||
.bind(invoice_info.amount as i64)
|
.bind(invoice_info.amount as i64)
|
||||||
.bind(invoice_info.status)
|
.bind(invoice_info.status)
|
||||||
.bind(invoice_info.memo)
|
.bind(invoice_info.memo)
|
||||||
.bind(invoice_info.bolt11)
|
.bind(invoice_info.bolt11)
|
||||||
.execute(&mut tx)
|
.execute(&mut tx)
|
||||||
.await.unwrap();
|
.await.unwrap();
|
||||||
|
|
||||||
debug!("Invoice added");
|
debug!("Invoice added");
|
||||||
|
|
||||||
@@ -733,140 +727,62 @@ fn query_from_filter(f: &ReqFilter) -> Option<QueryBuilder<Postgres>> {
|
|||||||
// filter out non-hex values
|
// filter out non-hex values
|
||||||
let auth_vec: Vec<&String> = auth_vec.iter().filter(|a| is_hex(a)).collect();
|
let auth_vec: Vec<&String> = auth_vec.iter().filter(|a| is_hex(a)).collect();
|
||||||
|
|
||||||
if !auth_vec.is_empty() {
|
if auth_vec.is_empty() {
|
||||||
query.push("(");
|
return None;
|
||||||
|
|
||||||
// shortcut authors into "IN" query
|
|
||||||
let any_is_range = auth_vec.iter().any(|pk| pk.len() != 64);
|
|
||||||
if !any_is_range {
|
|
||||||
query.push("e.pub_key in (");
|
|
||||||
let mut pk_sep = query.separated(", ");
|
|
||||||
for pk in auth_vec.iter() {
|
|
||||||
pk_sep.push_bind(hex::decode(pk).ok());
|
|
||||||
}
|
|
||||||
query.push(") OR e.delegated_by in (");
|
|
||||||
let mut pk_delegated_sep = query.separated(", ");
|
|
||||||
for pk in auth_vec.iter() {
|
|
||||||
pk_delegated_sep.push_bind(hex::decode(pk).ok());
|
|
||||||
}
|
|
||||||
query.push(")");
|
|
||||||
push_and = true;
|
|
||||||
} else {
|
|
||||||
let mut range_authors = query.separated(" OR ");
|
|
||||||
for auth in auth_vec {
|
|
||||||
match hex_range(auth) {
|
|
||||||
Some(HexSearch::Exact(ex)) => {
|
|
||||||
range_authors
|
|
||||||
.push("(e.pub_key = ")
|
|
||||||
.push_bind_unseparated(ex.clone())
|
|
||||||
.push_unseparated(" OR e.delegated_by = ")
|
|
||||||
.push_bind_unseparated(ex)
|
|
||||||
.push_unseparated(")");
|
|
||||||
}
|
|
||||||
Some(HexSearch::Range(lower, upper)) => {
|
|
||||||
range_authors
|
|
||||||
.push("((e.pub_key > ")
|
|
||||||
.push_bind_unseparated(lower.clone())
|
|
||||||
.push_unseparated(" AND e.pub_key < ")
|
|
||||||
.push_bind_unseparated(upper.clone())
|
|
||||||
.push_unseparated(") OR (e.delegated_by > ")
|
|
||||||
.push_bind_unseparated(lower)
|
|
||||||
.push_unseparated(" AND e.delegated_by < ")
|
|
||||||
.push_bind_unseparated(upper)
|
|
||||||
.push_unseparated("))");
|
|
||||||
}
|
|
||||||
Some(HexSearch::LowerOnly(lower)) => {
|
|
||||||
range_authors
|
|
||||||
.push("(e.pub_key > ")
|
|
||||||
.push_bind_unseparated(lower.clone())
|
|
||||||
.push_unseparated(" OR e.delegated_by > ")
|
|
||||||
.push_bind_unseparated(lower)
|
|
||||||
.push_unseparated(")");
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!("Could not parse hex range from author {:?}", auth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
push_and = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
query.push(")");
|
|
||||||
}
|
}
|
||||||
|
query.push("(e.pub_key in (");
|
||||||
|
|
||||||
|
let mut pk_sep = query.separated(", ");
|
||||||
|
for pk in auth_vec.iter() {
|
||||||
|
pk_sep.push_bind(hex::decode(pk).ok());
|
||||||
|
}
|
||||||
|
query.push(") OR e.delegated_by in (");
|
||||||
|
let mut pk_delegated_sep = query.separated(", ");
|
||||||
|
for pk in auth_vec.iter() {
|
||||||
|
pk_delegated_sep.push_bind(hex::decode(pk).ok());
|
||||||
|
}
|
||||||
|
push_and = true;
|
||||||
|
query.push("))");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query for Kind
|
// Query for Kind
|
||||||
if let Some(ks) = &f.kinds {
|
if let Some(ks) = &f.kinds {
|
||||||
if !ks.is_empty() {
|
if ks.is_empty() {
|
||||||
if push_and {
|
return None;
|
||||||
query.push(" AND ");
|
|
||||||
}
|
|
||||||
push_and = true;
|
|
||||||
|
|
||||||
query.push("e.kind in (");
|
|
||||||
let mut list_query = query.separated(", ");
|
|
||||||
for k in ks.iter() {
|
|
||||||
list_query.push_bind(*k as i64);
|
|
||||||
}
|
|
||||||
query.push(")");
|
|
||||||
}
|
}
|
||||||
|
if push_and {
|
||||||
|
query.push(" AND ");
|
||||||
|
}
|
||||||
|
push_and = true;
|
||||||
|
|
||||||
|
query.push("e.kind in (");
|
||||||
|
let mut list_query = query.separated(", ");
|
||||||
|
for k in ks.iter() {
|
||||||
|
list_query.push_bind(*k as i64);
|
||||||
|
}
|
||||||
|
query.push(")");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query for event, allowing prefix matches
|
// Query for event,
|
||||||
if let Some(id_vec) = &f.ids {
|
if let Some(id_vec) = &f.ids {
|
||||||
// filter out non-hex values
|
// filter out non-hex values
|
||||||
let id_vec: Vec<&String> = id_vec.iter().filter(|a| is_hex(a)).collect();
|
let id_vec: Vec<&String> = id_vec.iter().filter(|a| is_hex(a)).collect();
|
||||||
|
if id_vec.is_empty() {
|
||||||
if !id_vec.is_empty() {
|
return None;
|
||||||
if push_and {
|
|
||||||
query.push(" AND (");
|
|
||||||
} else {
|
|
||||||
query.push("(");
|
|
||||||
}
|
|
||||||
push_and = true;
|
|
||||||
|
|
||||||
// shortcut ids into "IN" query
|
|
||||||
let any_is_range = id_vec.iter().any(|pk| pk.len() != 64);
|
|
||||||
if !any_is_range {
|
|
||||||
query.push("id in (");
|
|
||||||
let mut sep = query.separated(", ");
|
|
||||||
for id in id_vec.iter() {
|
|
||||||
sep.push_bind(hex::decode(id).ok());
|
|
||||||
}
|
|
||||||
query.push(")");
|
|
||||||
} else {
|
|
||||||
// take each author and convert to a hex search
|
|
||||||
let mut id_query = query.separated(" OR ");
|
|
||||||
for id in id_vec {
|
|
||||||
match hex_range(id) {
|
|
||||||
Some(HexSearch::Exact(ex)) => {
|
|
||||||
id_query
|
|
||||||
.push("(id = ")
|
|
||||||
.push_bind_unseparated(ex)
|
|
||||||
.push_unseparated(")");
|
|
||||||
}
|
|
||||||
Some(HexSearch::Range(lower, upper)) => {
|
|
||||||
id_query
|
|
||||||
.push("(id > ")
|
|
||||||
.push_bind_unseparated(lower)
|
|
||||||
.push_unseparated(" AND id < ")
|
|
||||||
.push_bind_unseparated(upper)
|
|
||||||
.push_unseparated(")");
|
|
||||||
}
|
|
||||||
Some(HexSearch::LowerOnly(lower)) => {
|
|
||||||
id_query
|
|
||||||
.push("(id > ")
|
|
||||||
.push_bind_unseparated(lower)
|
|
||||||
.push_unseparated(")");
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!("Could not parse hex range from id {:?}", id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query.push(")");
|
|
||||||
}
|
}
|
||||||
|
if push_and {
|
||||||
|
query.push(" AND (");
|
||||||
|
} else {
|
||||||
|
query.push("(");
|
||||||
|
}
|
||||||
|
push_and = true;
|
||||||
|
|
||||||
|
query.push("id in (");
|
||||||
|
let mut sep = query.separated(", ");
|
||||||
|
for id in id_vec.iter() {
|
||||||
|
sep.push_bind(hex::decode(id).ok());
|
||||||
|
}
|
||||||
|
query.push("))");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query for tags
|
// Query for tags
|
||||||
@@ -877,22 +793,46 @@ fn query_from_filter(f: &ReqFilter) -> Option<QueryBuilder<Postgres>> {
|
|||||||
}
|
}
|
||||||
push_and = true;
|
push_and = true;
|
||||||
|
|
||||||
|
let mut push_or = false;
|
||||||
|
query.push("e.id IN (SELECT ee.id FROM \"event\" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and ");
|
||||||
for (key, val) in map.iter() {
|
for (key, val) in map.iter() {
|
||||||
query.push("e.id IN (SELECT ee.id FROM \"event\" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and (t.\"name\" = ")
|
if val.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if push_or {
|
||||||
|
query.push(" OR ");
|
||||||
|
}
|
||||||
|
query
|
||||||
|
.push("(t.\"name\" = ")
|
||||||
.push_bind(key.to_string())
|
.push_bind(key.to_string())
|
||||||
.push(" AND (value in (");
|
.push(" AND (");
|
||||||
|
|
||||||
// plain value match first
|
let has_plain_values = val.iter().any(|v| (v.len() % 2 != 0 || !is_lower_hex(v)));
|
||||||
let mut tag_query = query.separated(", ");
|
let has_hex_values = val.iter().any(|v| v.len() % 2 == 0 && is_lower_hex(v));
|
||||||
for v in val.iter() {
|
if has_plain_values {
|
||||||
if (v.len() % 2 != 0) && !is_lower_hex(v) {
|
query.push("value in (");
|
||||||
|
// plain value match first
|
||||||
|
let mut tag_query = query.separated(", ");
|
||||||
|
for v in val.iter().filter(|v| v.len() % 2 != 0 || !is_lower_hex(v)) {
|
||||||
tag_query.push_bind(v.as_bytes());
|
tag_query.push_bind(v.as_bytes());
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
if has_plain_values && has_hex_values {
|
||||||
|
query.push(") OR ");
|
||||||
|
}
|
||||||
|
if has_hex_values {
|
||||||
|
query.push("value_hex in (");
|
||||||
|
// plain value match first
|
||||||
|
let mut tag_query = query.separated(", ");
|
||||||
|
for v in val.iter().filter(|v| v.len() % 2 == 0 && is_lower_hex(v)) {
|
||||||
tag_query.push_bind(hex::decode(v).ok());
|
tag_query.push_bind(hex::decode(v).ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
query.push("))))");
|
|
||||||
|
query.push(")))");
|
||||||
|
push_or = true;
|
||||||
}
|
}
|
||||||
|
query.push(")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -925,10 +865,7 @@ fn query_from_filter(f: &ReqFilter) -> Option<QueryBuilder<Postgres>> {
|
|||||||
query.push("e.hidden != 1::bit(1)");
|
query.push("e.hidden != 1::bit(1)");
|
||||||
}
|
}
|
||||||
// never display expired events
|
// never display expired events
|
||||||
query
|
query.push(" AND (e.expires_at IS NULL OR e.expires_at > now())");
|
||||||
.push(" AND (e.expires_at IS NULL OR e.expires_at > ")
|
|
||||||
.push_bind(Utc.timestamp_opt(utils::unix_time() as i64, 0).unwrap())
|
|
||||||
.push(")");
|
|
||||||
|
|
||||||
// Apply per-filter limit to this query.
|
// Apply per-filter limit to this query.
|
||||||
// The use of a LIMIT implies a DESC order, to capture only the most recent events.
|
// The use of a LIMIT implies a DESC order, to capture only the most recent events.
|
||||||
@@ -963,3 +900,111 @@ impl FromRow<'_, PgRow> for VerificationRecord {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_query_gen_tag_value_hex() {
|
||||||
|
let filter = ReqFilter {
|
||||||
|
ids: None,
|
||||||
|
kinds: Some(vec![1000]),
|
||||||
|
since: None,
|
||||||
|
until: None,
|
||||||
|
authors: Some(vec![
|
||||||
|
"84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(),
|
||||||
|
]),
|
||||||
|
limit: None,
|
||||||
|
tags: Some(HashMap::from([(
|
||||||
|
'p',
|
||||||
|
HashSet::from([
|
||||||
|
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed".to_owned(),
|
||||||
|
]),
|
||||||
|
)])),
|
||||||
|
force_no_match: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let q = query_from_filter(&filter).unwrap();
|
||||||
|
assert_eq!(q.sql(), "SELECT e.\"content\", e.created_at FROM \"event\" e WHERE (e.pub_key in ($1) OR e.delegated_by in ($2)) AND e.kind in ($3) AND e.id IN (SELECT ee.id FROM \"event\" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and (t.\"name\" = $4 AND (value_hex in ($5)))) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_query_gen_tag_value() {
|
||||||
|
let filter = ReqFilter {
|
||||||
|
ids: None,
|
||||||
|
kinds: Some(vec![1000]),
|
||||||
|
since: None,
|
||||||
|
until: None,
|
||||||
|
authors: Some(vec![
|
||||||
|
"84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(),
|
||||||
|
]),
|
||||||
|
limit: None,
|
||||||
|
tags: Some(HashMap::from([('d', HashSet::from(["test".to_owned()]))])),
|
||||||
|
force_no_match: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let q = query_from_filter(&filter).unwrap();
|
||||||
|
assert_eq!(q.sql(), "SELECT e.\"content\", e.created_at FROM \"event\" e WHERE (e.pub_key in ($1) OR e.delegated_by in ($2)) AND e.kind in ($3) AND e.id IN (SELECT ee.id FROM \"event\" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and (t.\"name\" = $4 AND (value in ($5)))) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_query_gen_tag_value_and_value_hex() {
|
||||||
|
let filter = ReqFilter {
|
||||||
|
ids: None,
|
||||||
|
kinds: Some(vec![1000]),
|
||||||
|
since: None,
|
||||||
|
until: None,
|
||||||
|
authors: Some(vec![
|
||||||
|
"84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(),
|
||||||
|
]),
|
||||||
|
limit: None,
|
||||||
|
tags: Some(HashMap::from([(
|
||||||
|
'd',
|
||||||
|
HashSet::from([
|
||||||
|
"test".to_owned(),
|
||||||
|
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed".to_owned(),
|
||||||
|
]),
|
||||||
|
)])),
|
||||||
|
force_no_match: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let q = query_from_filter(&filter).unwrap();
|
||||||
|
assert_eq!(q.sql(), "SELECT e.\"content\", e.created_at FROM \"event\" e WHERE (e.pub_key in ($1) OR e.delegated_by in ($2)) AND e.kind in ($3) AND e.id IN (SELECT ee.id FROM \"event\" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and (t.\"name\" = $4 AND (value in ($5) OR value_hex in ($6)))) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_query_multiple_tags() {
|
||||||
|
let filter = ReqFilter {
|
||||||
|
ids: None,
|
||||||
|
kinds: Some(vec![30_001]),
|
||||||
|
since: None,
|
||||||
|
until: None,
|
||||||
|
authors: None,
|
||||||
|
limit: None,
|
||||||
|
tags: Some(HashMap::from([
|
||||||
|
('d', HashSet::from(["follow".to_owned()])),
|
||||||
|
('t', HashSet::from(["siamstr".to_owned()])),
|
||||||
|
])),
|
||||||
|
force_no_match: false,
|
||||||
|
};
|
||||||
|
let q = query_from_filter(&filter).unwrap();
|
||||||
|
assert_eq!(q.sql(), "SELECT e.\"content\", e.created_at FROM \"event\" e WHERE e.kind in ($1) AND e.id IN (SELECT ee.id FROM \"event\" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and (t.\"name\" = $2 AND (value in ($3))) OR (t.\"name\" = $4 AND (value in ($5)))) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_query_empty_tags() {
|
||||||
|
let filter = ReqFilter {
|
||||||
|
ids: None,
|
||||||
|
kinds: Some(vec![1, 6, 16, 30023, 1063, 6969]),
|
||||||
|
since: Some(1700697846),
|
||||||
|
until: None,
|
||||||
|
authors: None,
|
||||||
|
limit: None,
|
||||||
|
tags: Some(HashMap::from([('a', HashSet::new())])),
|
||||||
|
force_no_match: false,
|
||||||
|
};
|
||||||
|
assert!(query_from_filter(&filter).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -205,7 +205,7 @@ CREATE INDEX tag_value_hex_idx ON tag USING btree (value_hex);
|
|||||||
let event: Event = serde_json::from_str(&String::from_utf8(event_bytes).unwrap())?;
|
let event: Event = serde_json::from_str(&String::from_utf8(event_bytes).unwrap())?;
|
||||||
|
|
||||||
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
||||||
let tagname = t.get(0).unwrap();
|
let tagname = t.first().unwrap();
|
||||||
let tagnamechar_opt = single_char_tagname(tagname);
|
let tagnamechar_opt = single_char_tagname(tagname);
|
||||||
if tagnamechar_opt.is_none() {
|
if tagnamechar_opt.is_none() {
|
||||||
continue;
|
continue;
|
||||||
|
@@ -4,8 +4,6 @@ use crate::config::Settings;
|
|||||||
use crate::db::QueryResult;
|
use crate::db::QueryResult;
|
||||||
use crate::error::{Error::SqlError, Result};
|
use crate::error::{Error::SqlError, Result};
|
||||||
use crate::event::{single_char_tagname, Event};
|
use crate::event::{single_char_tagname, Event};
|
||||||
use crate::hexrange::hex_range;
|
|
||||||
use crate::hexrange::HexSearch;
|
|
||||||
use crate::nip05::{Nip05Name, VerificationRecord};
|
use crate::nip05::{Nip05Name, VerificationRecord};
|
||||||
use crate::payment::{InvoiceInfo, InvoiceStatus};
|
use crate::payment::{InvoiceInfo, InvoiceStatus};
|
||||||
use crate::repo::sqlite_migration::{upgrade_db, STARTUP_SQL};
|
use crate::repo::sqlite_migration::{upgrade_db, STARTUP_SQL};
|
||||||
@@ -158,14 +156,11 @@ impl SqliteRepo {
|
|||||||
let tagval = &tag[1];
|
let tagval = &tag[1];
|
||||||
// only single-char tags are searchable
|
// only single-char tags are searchable
|
||||||
let tagchar_opt = single_char_tagname(tagname);
|
let tagchar_opt = single_char_tagname(tagname);
|
||||||
match &tagchar_opt {
|
if tagchar_opt.is_some() {
|
||||||
Some(_) => {
|
tx.execute(
|
||||||
tx.execute(
|
"INSERT OR IGNORE INTO tag (event_id, name, value, kind, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
"INSERT OR IGNORE INTO tag (event_id, name, value, kind, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
params![ev_id, &tagname, &tagval, e.kind, e.created_at],
|
||||||
params![ev_id, &tagname, &tagval, e.kind, e.created_at],
|
)?;
|
||||||
)?;
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -994,24 +989,9 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>, Option<Stri
|
|||||||
// take each author and convert to a hexsearch
|
// take each author and convert to a hexsearch
|
||||||
let mut auth_searches: Vec<String> = vec![];
|
let mut auth_searches: Vec<String> = vec![];
|
||||||
for auth in authvec {
|
for auth in authvec {
|
||||||
match hex_range(auth) {
|
auth_searches.push("author=?".to_owned());
|
||||||
Some(HexSearch::Exact(ex)) => {
|
let auth_bin = hex::decode(auth).ok();
|
||||||
auth_searches.push("author=?".to_owned());
|
params.push(Box::new(auth_bin));
|
||||||
params.push(Box::new(ex));
|
|
||||||
}
|
|
||||||
Some(HexSearch::Range(lower, upper)) => {
|
|
||||||
auth_searches.push("(author>? AND author<?)".to_owned());
|
|
||||||
params.push(Box::new(lower));
|
|
||||||
params.push(Box::new(upper));
|
|
||||||
}
|
|
||||||
Some(HexSearch::LowerOnly(lower)) => {
|
|
||||||
auth_searches.push("author>?".to_owned());
|
|
||||||
params.push(Box::new(lower));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
trace!("Could not parse hex range from author {:?}", auth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !authvec.is_empty() {
|
if !authvec.is_empty() {
|
||||||
let auth_clause = format!("({})", auth_searches.join(" OR "));
|
let auth_clause = format!("({})", auth_searches.join(" OR "));
|
||||||
@@ -1032,24 +1012,9 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>, Option<Stri
|
|||||||
// take each author and convert to a hexsearch
|
// take each author and convert to a hexsearch
|
||||||
let mut id_searches: Vec<String> = vec![];
|
let mut id_searches: Vec<String> = vec![];
|
||||||
for id in idvec {
|
for id in idvec {
|
||||||
match hex_range(id) {
|
id_searches.push("event_hash=?".to_owned());
|
||||||
Some(HexSearch::Exact(ex)) => {
|
let id_bin = hex::decode(id).ok();
|
||||||
id_searches.push("event_hash=?".to_owned());
|
params.push(Box::new(id_bin));
|
||||||
params.push(Box::new(ex));
|
|
||||||
}
|
|
||||||
Some(HexSearch::Range(lower, upper)) => {
|
|
||||||
id_searches.push("(event_hash>? AND event_hash<?)".to_owned());
|
|
||||||
params.push(Box::new(lower));
|
|
||||||
params.push(Box::new(upper));
|
|
||||||
}
|
|
||||||
Some(HexSearch::LowerOnly(lower)) => {
|
|
||||||
id_searches.push("event_hash>?".to_owned());
|
|
||||||
params.push(Box::new(lower));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!("Could not parse hex range from id {:?}", id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if idvec.is_empty() {
|
if idvec.is_empty() {
|
||||||
// if the ids list was empty, we should never return
|
// if the ids list was empty, we should never return
|
||||||
@@ -1315,6 +1280,7 @@ pub async fn db_checkpoint_task(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
enum SqliteStatus {
|
enum SqliteStatus {
|
||||||
Ok,
|
Ok,
|
||||||
Busy,
|
Busy,
|
||||||
|
@@ -40,7 +40,7 @@ PRAGMA user_version = {};
|
|||||||
-- Event Table
|
-- Event Table
|
||||||
CREATE TABLE IF NOT EXISTS event (
|
CREATE TABLE IF NOT EXISTS event (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
event_hash BLOB NOT NULL, -- 4-byte hash
|
event_hash BLOB NOT NULL, -- 32-byte SHA256 hash
|
||||||
first_seen INTEGER NOT NULL, -- when the event was first seen (not authored!) (seconds since 1970)
|
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
|
created_at INTEGER NOT NULL, -- when the event was authored
|
||||||
expires_at INTEGER, -- when the event expires and may be deleted
|
expires_at INTEGER, -- when the event expires and may be deleted
|
||||||
@@ -159,7 +159,7 @@ fn mig_init(conn: &mut PooledConnection) -> usize {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (init) failed: {}", err);
|
||||||
panic!("database could not be initialized");
|
panic!("database could not be initialized");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,7 +295,7 @@ pub fn rebuild_tags(conn: &mut PooledConnection) -> Result<()> {
|
|||||||
let event: Event = serde_json::from_str(&event_json)?;
|
let event: Event = serde_json::from_str(&event_json)?;
|
||||||
// look at each event, and each tag, creating new tag entries if appropriate.
|
// look at each event, and each tag, creating new tag entries if appropriate.
|
||||||
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
||||||
let tagname = t.get(0).unwrap();
|
let tagname = t.first().unwrap();
|
||||||
let tagnamechar_opt = single_char_tagname(tagname);
|
let tagnamechar_opt = single_char_tagname(tagname);
|
||||||
if tagnamechar_opt.is_none() {
|
if tagnamechar_opt.is_none() {
|
||||||
continue;
|
continue;
|
||||||
@@ -325,7 +325,7 @@ pub fn rebuild_tags(conn: &mut PooledConnection) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
//// Migration Scripts
|
// Migration Scripts
|
||||||
|
|
||||||
fn mig_1_to_2(conn: &mut PooledConnection) -> Result<usize> {
|
fn mig_1_to_2(conn: &mut PooledConnection) -> Result<usize> {
|
||||||
// only change is adding a hidden column to events.
|
// only change is adding a hidden column to events.
|
||||||
@@ -339,7 +339,7 @@ PRAGMA user_version = 2;
|
|||||||
info!("database schema upgraded v1 -> v2");
|
info!("database schema upgraded v1 -> v2");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v1->v2) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,7 +366,7 @@ PRAGMA user_version = 3;
|
|||||||
info!("database schema upgraded v2 -> v3");
|
info!("database schema upgraded v2 -> v3");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v2->v3) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,7 +416,7 @@ PRAGMA user_version = 4;
|
|||||||
info!("database schema upgraded v3 -> v4");
|
info!("database schema upgraded v3 -> v4");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v3->v4) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,7 +435,7 @@ PRAGMA user_version=5;
|
|||||||
info!("database schema upgraded v4 -> v5");
|
info!("database schema upgraded v4 -> v5");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v4->v5) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,7 +461,7 @@ fn mig_5_to_6(conn: &mut PooledConnection) -> Result<usize> {
|
|||||||
let event: Event = serde_json::from_str(&event_json)?;
|
let event: Event = serde_json::from_str(&event_json)?;
|
||||||
// look at each event, and each tag, creating new tag entries if appropriate.
|
// look at each event, and each tag, creating new tag entries if appropriate.
|
||||||
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
||||||
let tagname = t.get(0).unwrap();
|
let tagname = t.first().unwrap();
|
||||||
let tagnamechar_opt = single_char_tagname(tagname);
|
let tagnamechar_opt = single_char_tagname(tagname);
|
||||||
if tagnamechar_opt.is_none() {
|
if tagnamechar_opt.is_none() {
|
||||||
continue;
|
continue;
|
||||||
@@ -507,7 +507,7 @@ PRAGMA user_version = 7;
|
|||||||
info!("database schema upgraded v6 -> v7");
|
info!("database schema upgraded v6 -> v7");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v6->v7) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,7 +528,7 @@ PRAGMA user_version = 8;
|
|||||||
info!("database schema upgraded v7 -> v8");
|
info!("database schema upgraded v7 -> v8");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v7->v8) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -548,7 +548,7 @@ PRAGMA user_version = 9;
|
|||||||
info!("database schema upgraded v8 -> v9");
|
info!("database schema upgraded v8 -> v9");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v8->v9) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,7 +567,7 @@ PRAGMA user_version = 10;
|
|||||||
info!("database schema upgraded v9 -> v10");
|
info!("database schema upgraded v9 -> v10");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v9->v10) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -588,7 +588,7 @@ PRAGMA user_version = 11;
|
|||||||
info!("database schema upgraded v10 -> v11");
|
info!("database schema upgraded v10 -> v11");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v10->v11) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -643,7 +643,7 @@ PRAGMA user_version = 13;
|
|||||||
info!("database schema upgraded v12 -> v13");
|
info!("database schema upgraded v12 -> v13");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v12->v13) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -663,7 +663,7 @@ PRAGMA user_version = 14;
|
|||||||
info!("database schema upgraded v13 -> v14");
|
info!("database schema upgraded v13 -> v14");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v13->v14) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -682,7 +682,7 @@ PRAGMA user_version = 15;
|
|||||||
info!("database schema upgraded v14 -> v15");
|
info!("database schema upgraded v14 -> v15");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v14->v15) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -749,7 +749,7 @@ CREATE INDEX IF NOT EXISTS tag_covering_index ON tag(name,kind,value,created_at,
|
|||||||
let event: Event = serde_json::from_str(&event_json)?;
|
let event: Event = serde_json::from_str(&event_json)?;
|
||||||
// look at each event, and each tag, creating new tag entries if appropriate.
|
// look at each event, and each tag, creating new tag entries if appropriate.
|
||||||
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
||||||
let tagname = t.get(0).unwrap();
|
let tagname = t.first().unwrap();
|
||||||
let tagnamechar_opt = single_char_tagname(tagname);
|
let tagnamechar_opt = single_char_tagname(tagname);
|
||||||
if tagnamechar_opt.is_none() {
|
if tagnamechar_opt.is_none() {
|
||||||
continue;
|
continue;
|
||||||
@@ -786,7 +786,7 @@ PRAGMA user_version = 17;
|
|||||||
info!("database schema upgraded v16 -> v17");
|
info!("database schema upgraded v16 -> v17");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v16->v17) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -833,7 +833,7 @@ PRAGMA user_version = 18;
|
|||||||
info!("database schema upgraded v17 -> v18");
|
info!("database schema upgraded v17 -> v18");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("update failed: {}", err);
|
error!("update (v17->v18) failed: {}", err);
|
||||||
panic!("database could not be upgraded");
|
panic!("database could not be upgraded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -194,6 +194,21 @@ async fn handle_web_request(
|
|||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(relay_file_path) = settings.info.relay_page {
|
||||||
|
match file_bytes(&relay_file_path) {
|
||||||
|
Ok(file_content) => {
|
||||||
|
return Ok(Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.header("Content-Type", "text/html; charset=UTF-8")
|
||||||
|
.body(Body::from(file_content))
|
||||||
|
.expect("request builder"));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to read relay_page file: {}. Will use default", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
.header("Content-Type", "text/plain")
|
.header("Content-Type", "text/plain")
|
||||||
@@ -568,6 +583,11 @@ async fn handle_web_request(
|
|||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Account is checked async so user will have to refresh the page a couple times after
|
||||||
|
// they have paid.
|
||||||
|
if let Err(e) = payment_tx.send(PaymentMessage::CheckAccount(pubkey.clone())) {
|
||||||
|
warn!("Could not check account: {}", e);
|
||||||
|
}
|
||||||
// Checks if user is already admitted
|
// Checks if user is already admitted
|
||||||
let text =
|
let text =
|
||||||
if let Ok((admission_status, _)) = repo.get_account_balance(&key.unwrap()).await {
|
if let Ok((admission_status, _)) = repo.get_account_balance(&key.unwrap()).await {
|
||||||
@@ -653,6 +673,7 @@ fn get_header_string(header: &str, headers: &HeaderMap) -> Option<String> {
|
|||||||
async fn ctrl_c_or_signal(mut shutdown_signal: Receiver<()>) {
|
async fn ctrl_c_or_signal(mut shutdown_signal: Receiver<()>) {
|
||||||
let mut term_signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
let mut term_signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||||
.expect("could not define signal");
|
.expect("could not define signal");
|
||||||
|
#[allow(clippy::never_loop)]
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = shutdown_signal.recv() => {
|
_ = shutdown_signal.recv() => {
|
||||||
@@ -893,11 +914,17 @@ pub fn start_server(settings: &Settings, shutdown_rx: MpscReceiver<()>) -> Resul
|
|||||||
bcast_tx.clone(),
|
bcast_tx.clone(),
|
||||||
settings.clone(),
|
settings.clone(),
|
||||||
);
|
);
|
||||||
if let Ok(mut p) = payment_opt {
|
match payment_opt {
|
||||||
tokio::task::spawn(async move {
|
Ok(mut p) => {
|
||||||
info!("starting payment process ...");
|
tokio::task::spawn(async move {
|
||||||
p.run().await;
|
info!("starting payment process ...");
|
||||||
});
|
p.run().await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to start payment process {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1261,7 +1288,6 @@ async fn nostr_server(
|
|||||||
// handle each type of message
|
// handle each type of message
|
||||||
let evid = ec.event_id().to_owned();
|
let evid = ec.event_id().to_owned();
|
||||||
let parsed : Result<EventWrapper> = Result::<EventWrapper>::from(ec);
|
let parsed : Result<EventWrapper> = Result::<EventWrapper>::from(ec);
|
||||||
metrics.cmd_event.inc();
|
|
||||||
match parsed {
|
match parsed {
|
||||||
Ok(WrappedEvent(e)) => {
|
Ok(WrappedEvent(e)) => {
|
||||||
metrics.cmd_event.inc();
|
metrics.cmd_event.inc();
|
||||||
@@ -1342,10 +1368,15 @@ async fn nostr_server(
|
|||||||
if conn.has_subscription(&s) {
|
if conn.has_subscription(&s) {
|
||||||
info!("client sent duplicate subscription, ignoring (cid: {}, sub: {:?})", cid, s.id);
|
info!("client sent duplicate subscription, ignoring (cid: {}, sub: {:?})", cid, s.id);
|
||||||
} else {
|
} else {
|
||||||
metrics.cmd_req.inc();
|
metrics.cmd_req.inc();
|
||||||
if let Some(ref lim) = sub_lim_opt {
|
if let Some(ref lim) = sub_lim_opt {
|
||||||
lim.until_ready_with_jitter(jitter).await;
|
lim.until_ready_with_jitter(jitter).await;
|
||||||
}
|
}
|
||||||
|
if settings.limits.limit_scrapers && s.is_scraper() {
|
||||||
|
info!("subscription was scraper, ignoring (cid: {}, sub: {:?})", cid, s.id);
|
||||||
|
ws_stream.send(Message::Text(format!("[\"EOSE\",\"{}\"]", s.id))).await.ok();
|
||||||
|
continue
|
||||||
|
}
|
||||||
let (abandon_query_tx, abandon_query_rx) = oneshot::channel::<()>();
|
let (abandon_query_tx, abandon_query_rx) = oneshot::channel::<()>();
|
||||||
match conn.subscribe(s.clone()) {
|
match conn.subscribe(s.clone()) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
@@ -1369,7 +1400,7 @@ async fn nostr_server(
|
|||||||
// closing a request simply removes the subscription.
|
// closing a request simply removes the subscription.
|
||||||
let parsed : Result<Close> = Result::<Close>::from(cc);
|
let parsed : Result<Close> = Result::<Close>::from(cc);
|
||||||
if let Ok(c) = parsed {
|
if let Ok(c) = parsed {
|
||||||
metrics.cmd_close.inc();
|
metrics.cmd_close.inc();
|
||||||
// check if a query is currently
|
// check if a query is currently
|
||||||
// running, and remove it if so.
|
// running, and remove it if so.
|
||||||
let stop_tx = running_queries.remove(&c.id);
|
let stop_tx = running_queries.remove(&c.id);
|
||||||
|
@@ -258,6 +258,29 @@ impl Subscription {
|
|||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is this subscription defined as a scraper query
|
||||||
|
pub fn is_scraper(&self) -> bool {
|
||||||
|
for f in &self.filters {
|
||||||
|
let mut precision = 0;
|
||||||
|
if f.ids.is_some() {
|
||||||
|
precision += 2;
|
||||||
|
}
|
||||||
|
if f.authors.is_some() {
|
||||||
|
precision += 1;
|
||||||
|
}
|
||||||
|
if f.kinds.is_some() {
|
||||||
|
precision += 1;
|
||||||
|
}
|
||||||
|
if f.tags.is_some() {
|
||||||
|
precision += 1;
|
||||||
|
}
|
||||||
|
if precision < 2 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prefix_match(prefixes: &[String], target: &str) -> bool {
|
fn prefix_match(prefixes: &[String], target: &str) -> bool {
|
||||||
@@ -338,7 +361,7 @@ mod tests {
|
|||||||
let s: Subscription = serde_json::from_str(raw_json)?;
|
let s: Subscription = serde_json::from_str(raw_json)?;
|
||||||
assert_eq!(s.id, "some-id");
|
assert_eq!(s.id, "some-id");
|
||||||
assert_eq!(s.filters.len(), 1);
|
assert_eq!(s.filters.len(), 1);
|
||||||
assert_eq!(s.filters.get(0).unwrap().authors, None);
|
assert_eq!(s.filters.first().unwrap().authors, None);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +425,7 @@ mod tests {
|
|||||||
let s: Subscription = serde_json::from_str(raw_json)?;
|
let s: Subscription = serde_json::from_str(raw_json)?;
|
||||||
assert_eq!(s.id, "some-id");
|
assert_eq!(s.id, "some-id");
|
||||||
assert_eq!(s.filters.len(), 1);
|
assert_eq!(s.filters.len(), 1);
|
||||||
let first_filter = s.filters.get(0).unwrap();
|
let first_filter = s.filters.first().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
first_filter.authors,
|
first_filter.authors,
|
||||||
Some(vec!("test-author-id".to_owned()))
|
Some(vec!("test-author-id".to_owned()))
|
||||||
@@ -633,11 +656,11 @@ mod tests {
|
|||||||
let s: Subscription = serde_json::from_str(
|
let s: Subscription = serde_json::from_str(
|
||||||
r##"["REQ","xyz",{"authors":["abc", "bcd"], "since": 10, "until": 20, "limit":100, "#e": ["foo", "bar"], "#d": ["test"]}]"##,
|
r##"["REQ","xyz",{"authors":["abc", "bcd"], "since": 10, "until": 20, "limit":100, "#e": ["foo", "bar"], "#d": ["test"]}]"##,
|
||||||
)?;
|
)?;
|
||||||
let f = s.filters.get(0);
|
let f = s.filters.first();
|
||||||
let serialized = serde_json::to_string(&f)?;
|
let serialized = serde_json::to_string(&f)?;
|
||||||
let serialized_wrapped = format!(r##"["REQ", "xyz",{}]"##, serialized);
|
let serialized_wrapped = format!(r##"["REQ", "xyz",{}]"##, serialized);
|
||||||
let parsed: Subscription = serde_json::from_str(&serialized_wrapped)?;
|
let parsed: Subscription = serde_json::from_str(&serialized_wrapped)?;
|
||||||
let parsed_filter = parsed.filters.get(0);
|
let parsed_filter = parsed.filters.first();
|
||||||
if let Some(pf) = parsed_filter {
|
if let Some(pf) = parsed_filter {
|
||||||
assert_eq!(pf.since, Some(10));
|
assert_eq!(pf.since, Some(10));
|
||||||
assert_eq!(pf.until, Some(20));
|
assert_eq!(pf.until, Some(20));
|
||||||
@@ -647,4 +670,29 @@ mod tests {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_scraper() -> Result<()> {
|
||||||
|
assert!(serde_json::from_str::<Subscription>(
|
||||||
|
r#"["REQ","some-id",{"kinds": [1984],"since": 123,"limit":1}]"#
|
||||||
|
)?
|
||||||
|
.is_scraper());
|
||||||
|
assert!(serde_json::from_str::<Subscription>(
|
||||||
|
r#"["REQ","some-id",{"kinds": [1984]},{"kinds": [1984],"authors":["aaaa"]}]"#
|
||||||
|
)?
|
||||||
|
.is_scraper());
|
||||||
|
assert!(!serde_json::from_str::<Subscription>(
|
||||||
|
r#"["REQ","some-id",{"kinds": [1984],"authors":["aaaa"]}]"#
|
||||||
|
)?
|
||||||
|
.is_scraper());
|
||||||
|
assert!(
|
||||||
|
!serde_json::from_str::<Subscription>(r#"["REQ","some-id",{"ids": ["aaaa"]}]"#)?
|
||||||
|
.is_scraper()
|
||||||
|
);
|
||||||
|
assert!(!serde_json::from_str::<Subscription>(
|
||||||
|
r##"["REQ","some-id",{"#p": ["aaaa"],"kinds":[1,4]}]"##
|
||||||
|
)?
|
||||||
|
.is_scraper());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -103,8 +103,5 @@ fn get_available_port() -> Option<u16> {
|
|||||||
}
|
}
|
||||||
pub fn port_is_available(port: u16) -> bool {
|
pub fn port_is_available(port: u16) -> bool {
|
||||||
info!("checking on port {}", port);
|
info!("checking on port {}", port);
|
||||||
match TcpListener::bind(("127.0.0.1", port)) {
|
TcpListener::bind(("127.0.0.1", port)).is_ok()
|
||||||
Ok(_) => true,
|
|
||||||
Err(_) => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -52,7 +52,7 @@ mod tests {
|
|||||||
let challenge = client_conn.auth_challenge().unwrap();
|
let challenge = client_conn.auth_challenge().unwrap();
|
||||||
let event = auth_event(challenge);
|
let event = auth_event(challenge);
|
||||||
|
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Ok(())));
|
assert!(matches!(result, Ok(())));
|
||||||
assert_eq!(client_conn.auth_challenge(), None);
|
assert_eq!(client_conn.auth_challenge(), None);
|
||||||
@@ -67,7 +67,7 @@ mod tests {
|
|||||||
assert_eq!(client_conn.auth_pubkey(), None);
|
assert_eq!(client_conn.auth_pubkey(), None);
|
||||||
|
|
||||||
let event = auth_event(&"challenge".into());
|
let event = auth_event(&"challenge".into());
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||||
}
|
}
|
||||||
@@ -87,14 +87,14 @@ mod tests {
|
|||||||
let challenge = client_conn.auth_challenge().unwrap().clone();
|
let challenge = client_conn.auth_challenge().unwrap().clone();
|
||||||
|
|
||||||
let event = auth_event(&challenge);
|
let event = auth_event(&challenge);
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Ok(())));
|
assert!(matches!(result, Ok(())));
|
||||||
assert_eq!(client_conn.auth_challenge(), None);
|
assert_eq!(client_conn.auth_challenge(), None);
|
||||||
assert_eq!(client_conn.auth_pubkey(), Some(&event.pubkey));
|
assert_eq!(client_conn.auth_pubkey(), Some(&event.pubkey));
|
||||||
|
|
||||||
let event1 = auth_event(&challenge);
|
let event1 = auth_event(&challenge);
|
||||||
let result1 = client_conn.authenticate(&event1, RELAY.into());
|
let result1 = client_conn.authenticate(&event1, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result1, Ok(())));
|
assert!(matches!(result1, Ok(())));
|
||||||
assert_eq!(client_conn.auth_challenge(), None);
|
assert_eq!(client_conn.auth_challenge(), None);
|
||||||
@@ -118,7 +118,7 @@ mod tests {
|
|||||||
let mut event = auth_event(challenge);
|
let mut event = auth_event(challenge);
|
||||||
event.sig = event.sig.chars().rev().collect::<String>();
|
event.sig = event.sig.chars().rev().collect::<String>();
|
||||||
|
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ mod tests {
|
|||||||
let challenge = client_conn.auth_challenge().unwrap();
|
let challenge = client_conn.auth_challenge().unwrap();
|
||||||
let event = auth_event_with_kind(challenge, 9999999999999999);
|
let event = auth_event_with_kind(challenge, 9999999999999999);
|
||||||
|
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ mod tests {
|
|||||||
let challenge = client_conn.auth_challenge().unwrap();
|
let challenge = client_conn.auth_challenge().unwrap();
|
||||||
let event = auth_event_with_created_at(challenge, unix_time() - 1200); // 20 minutes
|
let event = auth_event_with_created_at(challenge, unix_time() - 1200); // 20 minutes
|
||||||
|
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||||
}
|
}
|
||||||
@@ -178,7 +178,7 @@ mod tests {
|
|||||||
let challenge = client_conn.auth_challenge().unwrap();
|
let challenge = client_conn.auth_challenge().unwrap();
|
||||||
let event = auth_event_with_created_at(challenge, unix_time() + 1200); // 20 minutes
|
let event = auth_event_with_created_at(challenge, unix_time() + 1200); // 20 minutes
|
||||||
|
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ mod tests {
|
|||||||
|
|
||||||
let event = auth_event_without_tags();
|
let event = auth_event_without_tags();
|
||||||
|
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||||
}
|
}
|
||||||
@@ -216,7 +216,7 @@ mod tests {
|
|||||||
|
|
||||||
let event = auth_event_without_challenge();
|
let event = auth_event_without_challenge();
|
||||||
|
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ mod tests {
|
|||||||
let challenge = client_conn.auth_challenge().unwrap();
|
let challenge = client_conn.auth_challenge().unwrap();
|
||||||
let event = auth_event_without_relay(challenge);
|
let event = auth_event_without_relay(challenge);
|
||||||
|
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,7 @@ mod tests {
|
|||||||
|
|
||||||
let event = auth_event(&"invalid challenge".into());
|
let event = auth_event(&"invalid challenge".into());
|
||||||
|
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||||
}
|
}
|
||||||
@@ -275,7 +275,7 @@ mod tests {
|
|||||||
let challenge = client_conn.auth_challenge().unwrap();
|
let challenge = client_conn.auth_challenge().unwrap();
|
||||||
let event = auth_event_with_relay(challenge, &"xyz".into());
|
let event = auth_event_with_relay(challenge, &"xyz".into());
|
||||||
|
|
||||||
let result = client_conn.authenticate(&event, RELAY.into());
|
let result = client_conn.authenticate(&event, RELAY);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user