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-*
|
||||
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]
|
||||
name = "nostr-rs-relay"
|
||||
version = "0.8.13"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
authors = ["Greg Heartsfield <scsibug@imap.cc>"]
|
||||
description = "A relay implementation for the Nostr protocol"
|
||||
@@ -55,9 +55,11 @@ bech32 = "0.9.1"
|
||||
url = "2.3.1"
|
||||
qrcode = { version = "0.12.0", default-features = false, features = ["svg"] }
|
||||
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"
|
||||
cln-rpc = "0.1.9"
|
||||
|
||||
[target.'cfg(all(not(target_env = "msvc"), not(target_os = "openbsd")))'.dependencies]
|
||||
tikv-jemallocator = "0.5"
|
||||
|
||||
[dev-dependencies]
|
||||
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?)
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Features
|
||||
@@ -49,7 +49,7 @@ The examples below start a rootless podman container, mapping a local
|
||||
data directory and config file.
|
||||
|
||||
```console
|
||||
$ podman build -t nostr-rs-relay .
|
||||
$ podman build --pull -t nostr-rs-relay .
|
||||
|
||||
$ 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
|
||||
```
|
||||
|
||||
On OpenBSD:
|
||||
```console
|
||||
$ doas pkg_add rust protobuf
|
||||
```
|
||||
|
||||
Clone this repository, and then build a release version of the relay:
|
||||
|
||||
```console
|
||||
@@ -136,6 +141,7 @@ be mounted into a docker container like so:
|
||||
$ docker run -it -p 7000:8080 \
|
||||
--mount src=$(pwd)/config.toml,target=/usr/src/app/config.toml,type=bind \
|
||||
--mount src=$(pwd)/data,target=/usr/src/app/db,type=bind \
|
||||
--mount src=$(pwd)/index.html,target=/usr/src/app/index.html,type=bind \
|
||||
nostr-rs-relay
|
||||
```
|
||||
|
||||
@@ -152,10 +158,6 @@ Proxy](docs/reverse-proxy.md).
|
||||
|
||||
For development discussions, please feel free to use the [sourcehut
|
||||
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
|
||||
---
|
||||
|
25
config.toml
25
config.toml
@@ -10,7 +10,7 @@ name = "nostr-rs-relay"
|
||||
# Description
|
||||
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"
|
||||
|
||||
# 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.
|
||||
#relay_icon = "https://example.test/img.png"
|
||||
|
||||
# Path to custom relay html page
|
||||
#relay_page = "index.html"
|
||||
|
||||
[diagnostics]
|
||||
# Enable tokio tracing (for use with tokio-console)
|
||||
#tracing = false
|
||||
@@ -75,6 +78,11 @@ description = "A newly created nostr-rs-relay.\n\nCustomize this with your own i
|
||||
# `proto/nauthz.proto`.
|
||||
# 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]
|
||||
# Bind to this network address
|
||||
address = "0.0.0.0"
|
||||
@@ -150,6 +158,11 @@ reject_future_seconds = 1800
|
||||
# 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]
|
||||
# Pubkey addresses in this array are whitelisted for event publishing.
|
||||
# Only valid events by these authors will be accepted, if the variable
|
||||
@@ -193,20 +206,26 @@ reject_future_seconds = 1800
|
||||
# Enable pay to relay
|
||||
#enabled = false
|
||||
|
||||
# Node interface to use
|
||||
#processor = "ClnRest/LNBits"
|
||||
|
||||
# The cost to be admitted to relay
|
||||
#admission_cost = 4200
|
||||
|
||||
# The cost in sats per post
|
||||
#cost_per_event = 0
|
||||
|
||||
# Url of lnbits api
|
||||
# Url of node api
|
||||
#node_url = "<node url>"
|
||||
|
||||
# LNBits api secret
|
||||
#api_secret = "<ln bits api>"
|
||||
|
||||
# Path to CLN rune
|
||||
#rune_path = "<rune path>"
|
||||
|
||||
# Nostr direct message on signup
|
||||
#direct_message=true
|
||||
#direct_message=false
|
||||
|
||||
# Terms of service
|
||||
#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`
|
||||
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
|
||||
|
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).
|
||||
/// The database must already exist, this will not create a new one.
|
||||
/// Tested against schema v13.
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let _trace_sub = tracing_subscriber::fmt::try_init();
|
||||
println!("Nostr-rs-relay Bulk Loader");
|
||||
@@ -143,7 +142,7 @@ fn write_event(tx: &Transaction, e: Event) -> Result<usize> {
|
||||
let event_id = tx.last_insert_rowid();
|
||||
// look at each event, and each tag, creating new tag entries if appropriate.
|
||||
for t in e.tags.iter().filter(|x| x.len() > 1) {
|
||||
let tagname = t.get(0).unwrap();
|
||||
let tagname = t.first().unwrap();
|
||||
let tagnamechar_opt = single_char_tagname(tagname);
|
||||
if tagnamechar_opt.is_none() {
|
||||
continue;
|
||||
|
@@ -14,6 +14,7 @@ pub struct Info {
|
||||
pub contact: Option<String>,
|
||||
pub favicon: Option<String>,
|
||||
pub relay_icon: Option<String>,
|
||||
pub relay_page: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -32,6 +33,7 @@ pub struct Database {
|
||||
#[allow(unused)]
|
||||
pub struct Grpc {
|
||||
pub event_admission_server: Option<String>,
|
||||
pub restricts_write: bool,
|
||||
}
|
||||
|
||||
#[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_kind_blacklist: Option<Vec<u64>>,
|
||||
pub event_kind_allowlist: Option<Vec<u64>>,
|
||||
pub limit_scrapers: bool,
|
||||
}
|
||||
|
||||
#[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 secret_key: Option<String>,
|
||||
pub processor: Processor,
|
||||
pub rune_path: Option<String>, // To access clightning API
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -245,17 +249,25 @@ impl Settings {
|
||||
|
||||
// Validate pay to relay settings
|
||||
if settings.pay_to_relay.enabled {
|
||||
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
|
||||
assert_ne!(settings.pay_to_relay.node_url, "");
|
||||
assert_ne!(settings.pay_to_relay.terms_message, "");
|
||||
|
||||
if settings.pay_to_relay.direct_message {
|
||||
assert_ne!(
|
||||
settings.pay_to_relay.secret_key,
|
||||
Some("<nostr nsec>".to_string())
|
||||
);
|
||||
assert!(settings.pay_to_relay.secret_key.is_some());
|
||||
assert!(settings
|
||||
.pay_to_relay
|
||||
.secret_key
|
||||
.as_ref()
|
||||
.is_some_and(|key| key != "<nostr nsec>"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +286,7 @@ impl Default for Settings {
|
||||
contact: None,
|
||||
favicon: None,
|
||||
relay_icon: None,
|
||||
relay_page: None,
|
||||
},
|
||||
diagnostics: Diagnostics { tracing: false },
|
||||
database: Database {
|
||||
@@ -287,6 +300,7 @@ impl Default for Settings {
|
||||
},
|
||||
grpc: Grpc {
|
||||
event_admission_server: None,
|
||||
restricts_write: false,
|
||||
},
|
||||
network: Network {
|
||||
port: 8080,
|
||||
@@ -306,6 +320,7 @@ impl Default for Settings {
|
||||
event_persist_buffer: 4096,
|
||||
event_kind_blacklist: None,
|
||||
event_kind_allowlist: None,
|
||||
limit_scrapers: false,
|
||||
},
|
||||
authorization: Authorization {
|
||||
pubkey_whitelist: None, // Allow any address to publish
|
||||
@@ -319,8 +334,9 @@ impl Default for Settings {
|
||||
terms_message: "".to_string(),
|
||||
node_url: "".to_string(),
|
||||
api_secret: "".to_string(),
|
||||
rune_path: None,
|
||||
sign_ups: false,
|
||||
direct_message: true,
|
||||
direct_message: false,
|
||||
secret_key: None,
|
||||
processor: Processor::LNBits,
|
||||
},
|
||||
|
@@ -185,10 +185,10 @@ impl ClientConn {
|
||||
let mut relay: Option<&str> = None;
|
||||
|
||||
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());
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
23
src/db.rs
23
src/db.rs
@@ -261,7 +261,7 @@ pub async fn db_writer(
|
||||
) => {
|
||||
// User does not exist
|
||||
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
|
||||
.send(PaymentMessage::NewAccount(event.pubkey))
|
||||
.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
|
||||
let validation = if nip05_active {
|
||||
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.
|
||||
let start = Instant::now();
|
||||
if event.is_ephemeral() {
|
||||
@@ -401,6 +401,9 @@ pub async fn db_writer(
|
||||
start.elapsed()
|
||||
);
|
||||
event_write = true;
|
||||
|
||||
// send OK message
|
||||
notice_tx.try_send(Notice::saved(event.id)).ok();
|
||||
} else {
|
||||
match repo.write_event(&event).await {
|
||||
Ok(updated) => {
|
||||
|
@@ -42,7 +42,7 @@ pub enum Error {
|
||||
CommandUnknownError,
|
||||
#[error("SQL error")]
|
||||
SqlError(rusqlite::Error),
|
||||
#[error("Config error")]
|
||||
#[error("Config error : {0}")]
|
||||
ConfigError(config::ConfigError),
|
||||
#[error("Data directory does not exist")]
|
||||
DatabaseDirError,
|
||||
|
24
src/event.rs
24
src/event.rs
@@ -160,11 +160,11 @@ impl Event {
|
||||
.tags
|
||||
.iter()
|
||||
.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))
|
||||
.take(1)
|
||||
.collect();
|
||||
let val_first = dvals.get(0);
|
||||
let val_first = dvals.first();
|
||||
val_first.and_then(|t| t.parse::<u64>().ok())
|
||||
}
|
||||
|
||||
@@ -192,11 +192,11 @@ impl Event {
|
||||
.tags
|
||||
.iter()
|
||||
.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))
|
||||
.take(1)
|
||||
.collect();
|
||||
let dval_first = dvals.get(0);
|
||||
let dval_first = dvals.first();
|
||||
match dval_first {
|
||||
Some(_) => dval_first.map(|x| x.to_string()),
|
||||
None => Some(default),
|
||||
@@ -232,7 +232,7 @@ impl Event {
|
||||
.tags
|
||||
.iter()
|
||||
.filter(|x| x.len() == 4)
|
||||
.filter(|x| x.get(0).unwrap() == "delegation")
|
||||
.filter(|x| x.first().unwrap() == "delegation")
|
||||
.take(1)
|
||||
.next()?
|
||||
.clone(); // get first tag
|
||||
@@ -277,7 +277,7 @@ impl Event {
|
||||
let mut idx: HashMap<char, HashSet<String>> = HashMap::new();
|
||||
// iterate over tags that have at least 2 elements
|
||||
for t in self.tags.iter().filter(|x| x.len() > 1) {
|
||||
let tagname = t.get(0).unwrap();
|
||||
let tagname = t.first().unwrap();
|
||||
let tagnamechar_opt = single_char_tagname(tagname);
|
||||
if tagnamechar_opt.is_none() {
|
||||
continue;
|
||||
@@ -285,7 +285,7 @@ impl Event {
|
||||
let tagnamechar = tagnamechar_opt.unwrap();
|
||||
let tagval = t.get(1).unwrap();
|
||||
// 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
|
||||
let idx_tag_vec = idx.get_mut(&tagnamechar).expect("could not get tag vector");
|
||||
idx_tag_vec.insert(tagval.clone());
|
||||
@@ -310,7 +310,7 @@ impl Event {
|
||||
self.tags
|
||||
.iter()
|
||||
.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())
|
||||
.collect()
|
||||
}
|
||||
@@ -472,12 +472,8 @@ mod tests {
|
||||
let mut event = Event::simple_event();
|
||||
event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]];
|
||||
event.build_index();
|
||||
assert!(
|
||||
event.generic_tag_val_intersect(
|
||||
'e',
|
||||
&HashSet::from(["foo".to_owned(), "bar".to_owned()])
|
||||
)
|
||||
);
|
||||
assert!(event
|
||||
.generic_tag_val_intersect('e', &HashSet::from(["foo".to_owned(), "bar".to_owned()])));
|
||||
}
|
||||
|
||||
#[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(())
|
||||
}
|
||||
}
|
15
src/info.rs
15
src/info.rs
@@ -4,7 +4,7 @@ use crate::config::Settings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
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
|
||||
/// (This nip isn't finalized so may change)
|
||||
@@ -13,6 +13,9 @@ pub const UNIT: &str = "sats";
|
||||
pub struct Limitation {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
payment_required: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
restricted_writes: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -75,12 +78,18 @@ impl From<Settings> for RelayInfo {
|
||||
|
||||
let limitations = Limitation {
|
||||
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 admission_fee = if p.admission_cost > 0 {
|
||||
Some(vec![Fee {
|
||||
amount: p.admission_cost,
|
||||
amount: p.admission_cost * 1000,
|
||||
unit: UNIT.to_string(),
|
||||
}])
|
||||
} else {
|
||||
@@ -89,7 +98,7 @@ impl From<Settings> for RelayInfo {
|
||||
|
||||
let post_fee = if p.cost_per_event > 0 {
|
||||
Some(vec![Fee {
|
||||
amount: p.cost_per_event,
|
||||
amount: p.cost_per_event * 1000,
|
||||
unit: UNIT.to_string(),
|
||||
}])
|
||||
} else {
|
||||
|
@@ -6,7 +6,6 @@ pub mod db;
|
||||
pub mod delegation;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod hexrange;
|
||||
pub mod info;
|
||||
pub mod nauthz;
|
||||
pub mod nip05;
|
||||
|
@@ -10,13 +10,13 @@ use std::process;
|
||||
use std::sync::mpsc as syncmpsc;
|
||||
use std::sync::mpsc::{Receiver as MpscReceiver, Sender as MpscSender};
|
||||
use std::thread;
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
#[cfg(all(not(target_env = "msvc"), not(target_os = "openbsd")))]
|
||||
use tikv_jemallocator::Jemalloc;
|
||||
use tracing::info;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
#[cfg(all(not(target_env = "msvc"), not(target_os = "openbsd")))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: Jemalloc = Jemalloc;
|
||||
|
||||
|
78
src/nip05.rs
78
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
|
||||
let check_name = names_map.get(username).and_then(serde_json::Value::as_str);
|
||||
// ensure the address is a match
|
||||
Ok(check_name.map_or(false, |x| x == address))
|
||||
Ok(check_name == Some(address))
|
||||
}
|
||||
|
||||
impl Verifier {
|
||||
@@ -208,7 +208,7 @@ impl Verifier {
|
||||
.ok_or_else(|| Error::CustomError("invalid NIP-05 URL".to_owned()))?;
|
||||
let req = hyper::Request::builder()
|
||||
.method(hyper::Method::GET)
|
||||
.uri(url)
|
||||
.uri(url.clone())
|
||||
.header("Accept", "application/json")
|
||||
.header(
|
||||
"User-Agent",
|
||||
@@ -226,39 +226,85 @@ impl Verifier {
|
||||
// limit size of verification document to 1MB.
|
||||
const MAX_ALLOWED_RESPONSE_SIZE: u64 = 1024 * 1024;
|
||||
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
|
||||
let response_content_length = match response.body().size_hint().upper() {
|
||||
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 {
|
||||
info!(
|
||||
"content length {} exceeded limit of {} bytes for account {:?} at URL: {}",
|
||||
response_content_length,
|
||||
MAX_ALLOWED_RESPONSE_SIZE,
|
||||
nip.to_string(),
|
||||
url
|
||||
);
|
||||
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 = hyper::body::to_bytes(body).await?;
|
||||
let body_matches = body_contains_user(&nip.local, pubkey, &body_bytes)?;
|
||||
if body_matches {
|
||||
return Ok(UserWebVerificationStatus::Verified);
|
||||
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 {
|
||||
info!(
|
||||
"content length missing or exceeded limits for account: {:?}",
|
||||
"unexpected status code {} for account {:?}",
|
||||
parts.status,
|
||||
nip.to_string()
|
||||
);
|
||||
Ok(UserWebVerificationStatus::Unknown)
|
||||
}
|
||||
} else {
|
||||
info!("timeout verifying account {:?}", nip);
|
||||
return Ok(UserWebVerificationStatus::Unknown);
|
||||
}
|
||||
Ok(UserWebVerificationStatus::Unknown)
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform NIP-05 verifier tasks.
|
||||
pub async fn run(&mut self) {
|
||||
|
@@ -25,7 +25,9 @@ impl EventResultStatus {
|
||||
pub fn to_bool(&self) -> bool {
|
||||
match self {
|
||||
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::event::Event;
|
||||
use crate::payment::cln_rest::ClnRestPaymentProcessor;
|
||||
use crate::payment::lnbits::LNBitsPaymentProcessor;
|
||||
use crate::repo::NostrRepo;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -10,6 +11,7 @@ use async_trait::async_trait;
|
||||
use nostr::key::{FromPkStr, FromSkStr};
|
||||
use nostr::{key::Keys, Event as NostrEvent, EventBuilder};
|
||||
|
||||
pub mod cln_rest;
|
||||
pub mod lnbits;
|
||||
|
||||
/// Payment handler
|
||||
@@ -41,6 +43,7 @@ pub trait PaymentProcessor: Send + Sync {
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub enum Processor {
|
||||
LNBits,
|
||||
ClnRest,
|
||||
}
|
||||
|
||||
/// Possible states of an invoice
|
||||
@@ -52,12 +55,12 @@ pub enum InvoiceStatus {
|
||||
Expired,
|
||||
}
|
||||
|
||||
impl ToString for InvoiceStatus {
|
||||
fn to_string(&self) -> String {
|
||||
impl std::fmt::Display for InvoiceStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
InvoiceStatus::Paid => "Paid".to_string(),
|
||||
InvoiceStatus::Unpaid => "Unpaid".to_string(),
|
||||
InvoiceStatus::Expired => "Expired".to_string(),
|
||||
InvoiceStatus::Paid => write!(f, "Paid"),
|
||||
InvoiceStatus::Unpaid => write!(f, "Unpaid"),
|
||||
InvoiceStatus::Expired => write!(f, "Expired"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,8 +112,9 @@ impl Payment {
|
||||
};
|
||||
|
||||
// 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::ClnRest => Arc::new(ClnRestPaymentProcessor::new(&settings)?),
|
||||
};
|
||||
|
||||
Ok(Payment {
|
||||
|
@@ -14,7 +14,6 @@ use sqlx::{Error, Execute, FromRow, Postgres, QueryBuilder, Row};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::error;
|
||||
use crate::hexrange::{hex_range, HexSearch};
|
||||
use crate::repo::postgres_migration::run_migrations;
|
||||
use crate::server::NostrMetrics;
|
||||
use crate::utils::{self, is_hex, is_lower_hex};
|
||||
@@ -178,8 +177,7 @@ ON CONFLICT (id) DO NOTHING"#,
|
||||
let tag_val = &tag[1];
|
||||
// only single-char tags are searchable
|
||||
let tag_char_opt = single_char_tagname(tag_name);
|
||||
match &tag_char_opt {
|
||||
Some(_) => {
|
||||
if tag_char_opt.is_some() {
|
||||
// if tag value is lowercase hex;
|
||||
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) \
|
||||
@@ -188,8 +186,7 @@ ON CONFLICT (id) DO NOTHING"#,
|
||||
.bind(tag_name)
|
||||
.bind(hex::decode(tag_val).ok())
|
||||
.execute(&mut tx)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
} else {
|
||||
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")
|
||||
@@ -197,12 +194,9 @@ ON CONFLICT (id) DO NOTHING"#,
|
||||
.bind(tag_name)
|
||||
.bind(tag_val.as_bytes())
|
||||
.execute(&mut tx)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.is_replaceable() {
|
||||
@@ -733,13 +727,11 @@ fn query_from_filter(f: &ReqFilter) -> Option<QueryBuilder<Postgres>> {
|
||||
// filter out non-hex values
|
||||
let auth_vec: Vec<&String> = auth_vec.iter().filter(|a| is_hex(a)).collect();
|
||||
|
||||
if !auth_vec.is_empty() {
|
||||
query.push("(");
|
||||
if auth_vec.is_empty() {
|
||||
return None;
|
||||
}
|
||||
query.push("(e.pub_key in (");
|
||||
|
||||
// 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());
|
||||
@@ -749,54 +741,15 @@ fn query_from_filter(f: &ReqFilter) -> Option<QueryBuilder<Postgres>> {
|
||||
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("))");
|
||||
}
|
||||
|
||||
// Query for Kind
|
||||
if let Some(ks) = &f.kinds {
|
||||
if !ks.is_empty() {
|
||||
if ks.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if push_and {
|
||||
query.push(" AND ");
|
||||
}
|
||||
@@ -809,14 +762,14 @@ fn query_from_filter(f: &ReqFilter) -> Option<QueryBuilder<Postgres>> {
|
||||
}
|
||||
query.push(")");
|
||||
}
|
||||
}
|
||||
|
||||
// Query for event, allowing prefix matches
|
||||
// Query for event,
|
||||
if let Some(id_vec) = &f.ids {
|
||||
// filter out non-hex values
|
||||
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 {
|
||||
@@ -824,49 +777,12 @@ fn query_from_filter(f: &ReqFilter) -> Option<QueryBuilder<Postgres>> {
|
||||
}
|
||||
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(")");
|
||||
}
|
||||
query.push("))");
|
||||
}
|
||||
|
||||
// Query for tags
|
||||
@@ -877,22 +793,46 @@ fn query_from_filter(f: &ReqFilter) -> Option<QueryBuilder<Postgres>> {
|
||||
}
|
||||
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() {
|
||||
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(" AND (value in (");
|
||||
.push(" AND (");
|
||||
|
||||
let has_plain_values = val.iter().any(|v| (v.len() % 2 != 0 || !is_lower_hex(v)));
|
||||
let has_hex_values = val.iter().any(|v| v.len() % 2 == 0 && is_lower_hex(v));
|
||||
if has_plain_values {
|
||||
query.push("value in (");
|
||||
// plain value match first
|
||||
let mut tag_query = query.separated(", ");
|
||||
for v in val.iter() {
|
||||
if (v.len() % 2 != 0) && !is_lower_hex(v) {
|
||||
for v in val.iter().filter(|v| v.len() % 2 != 0 || !is_lower_hex(v)) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
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)");
|
||||
}
|
||||
// never display expired events
|
||||
query
|
||||
.push(" AND (e.expires_at IS NULL OR e.expires_at > ")
|
||||
.push_bind(Utc.timestamp_opt(utils::unix_time() as i64, 0).unwrap())
|
||||
.push(")");
|
||||
query.push(" AND (e.expires_at IS NULL OR e.expires_at > now())");
|
||||
|
||||
// Apply per-filter limit to this query.
|
||||
// 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())?;
|
||||
|
||||
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);
|
||||
if tagnamechar_opt.is_none() {
|
||||
continue;
|
||||
|
@@ -4,8 +4,6 @@ use crate::config::Settings;
|
||||
use crate::db::QueryResult;
|
||||
use crate::error::{Error::SqlError, Result};
|
||||
use crate::event::{single_char_tagname, Event};
|
||||
use crate::hexrange::hex_range;
|
||||
use crate::hexrange::HexSearch;
|
||||
use crate::nip05::{Nip05Name, VerificationRecord};
|
||||
use crate::payment::{InvoiceInfo, InvoiceStatus};
|
||||
use crate::repo::sqlite_migration::{upgrade_db, STARTUP_SQL};
|
||||
@@ -158,15 +156,12 @@ impl SqliteRepo {
|
||||
let tagval = &tag[1];
|
||||
// only single-char tags are searchable
|
||||
let tagchar_opt = single_char_tagname(tagname);
|
||||
match &tagchar_opt {
|
||||
Some(_) => {
|
||||
if tagchar_opt.is_some() {
|
||||
tx.execute(
|
||||
"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],
|
||||
)?;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// if this event is replaceable update, remove other replaceable
|
||||
@@ -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
|
||||
let mut auth_searches: Vec<String> = vec![];
|
||||
for auth in authvec {
|
||||
match hex_range(auth) {
|
||||
Some(HexSearch::Exact(ex)) => {
|
||||
auth_searches.push("author=?".to_owned());
|
||||
params.push(Box::new(ex));
|
||||
}
|
||||
Some(HexSearch::Range(lower, upper)) => {
|
||||
auth_searches.push("(author>? AND author<?)".to_owned());
|
||||
params.push(Box::new(lower));
|
||||
params.push(Box::new(upper));
|
||||
}
|
||||
Some(HexSearch::LowerOnly(lower)) => {
|
||||
auth_searches.push("author>?".to_owned());
|
||||
params.push(Box::new(lower));
|
||||
}
|
||||
None => {
|
||||
trace!("Could not parse hex range from author {:?}", auth);
|
||||
}
|
||||
}
|
||||
let auth_bin = hex::decode(auth).ok();
|
||||
params.push(Box::new(auth_bin));
|
||||
}
|
||||
if !authvec.is_empty() {
|
||||
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
|
||||
let mut id_searches: Vec<String> = vec![];
|
||||
for id in idvec {
|
||||
match hex_range(id) {
|
||||
Some(HexSearch::Exact(ex)) => {
|
||||
id_searches.push("event_hash=?".to_owned());
|
||||
params.push(Box::new(ex));
|
||||
}
|
||||
Some(HexSearch::Range(lower, upper)) => {
|
||||
id_searches.push("(event_hash>? AND event_hash<?)".to_owned());
|
||||
params.push(Box::new(lower));
|
||||
params.push(Box::new(upper));
|
||||
}
|
||||
Some(HexSearch::LowerOnly(lower)) => {
|
||||
id_searches.push("event_hash>?".to_owned());
|
||||
params.push(Box::new(lower));
|
||||
}
|
||||
None => {
|
||||
info!("Could not parse hex range from id {:?}", id);
|
||||
}
|
||||
}
|
||||
let id_bin = hex::decode(id).ok();
|
||||
params.push(Box::new(id_bin));
|
||||
}
|
||||
if idvec.is_empty() {
|
||||
// if the ids list was empty, we should never return
|
||||
@@ -1315,6 +1280,7 @@ pub async fn db_checkpoint_task(
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
enum SqliteStatus {
|
||||
Ok,
|
||||
Busy,
|
||||
|
@@ -40,7 +40,7 @@ PRAGMA user_version = {};
|
||||
-- Event Table
|
||||
CREATE TABLE IF NOT EXISTS event (
|
||||
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)
|
||||
created_at INTEGER NOT NULL, -- when the event was authored
|
||||
expires_at INTEGER, -- when the event expires and may be deleted
|
||||
@@ -159,7 +159,7 @@ fn mig_init(conn: &mut PooledConnection) -> usize {
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (init) failed: {}", err);
|
||||
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)?;
|
||||
// look at each event, and each tag, creating new tag entries if appropriate.
|
||||
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
||||
let tagname = t.get(0).unwrap();
|
||||
let tagname = t.first().unwrap();
|
||||
let tagnamechar_opt = single_char_tagname(tagname);
|
||||
if tagnamechar_opt.is_none() {
|
||||
continue;
|
||||
@@ -325,7 +325,7 @@ pub fn rebuild_tags(conn: &mut PooledConnection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//// Migration Scripts
|
||||
// Migration Scripts
|
||||
|
||||
fn mig_1_to_2(conn: &mut PooledConnection) -> Result<usize> {
|
||||
// only change is adding a hidden column to events.
|
||||
@@ -339,7 +339,7 @@ PRAGMA user_version = 2;
|
||||
info!("database schema upgraded v1 -> v2");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v1->v2) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -366,7 +366,7 @@ PRAGMA user_version = 3;
|
||||
info!("database schema upgraded v2 -> v3");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v2->v3) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -416,7 +416,7 @@ PRAGMA user_version = 4;
|
||||
info!("database schema upgraded v3 -> v4");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v3->v4) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -435,7 +435,7 @@ PRAGMA user_version=5;
|
||||
info!("database schema upgraded v4 -> v5");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v4->v5) failed: {}", err);
|
||||
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)?;
|
||||
// look at each event, and each tag, creating new tag entries if appropriate.
|
||||
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
||||
let tagname = t.get(0).unwrap();
|
||||
let tagname = t.first().unwrap();
|
||||
let tagnamechar_opt = single_char_tagname(tagname);
|
||||
if tagnamechar_opt.is_none() {
|
||||
continue;
|
||||
@@ -507,7 +507,7 @@ PRAGMA user_version = 7;
|
||||
info!("database schema upgraded v6 -> v7");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v6->v7) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -528,7 +528,7 @@ PRAGMA user_version = 8;
|
||||
info!("database schema upgraded v7 -> v8");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v7->v8) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -548,7 +548,7 @@ PRAGMA user_version = 9;
|
||||
info!("database schema upgraded v8 -> v9");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v8->v9) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -567,7 +567,7 @@ PRAGMA user_version = 10;
|
||||
info!("database schema upgraded v9 -> v10");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v9->v10) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -588,7 +588,7 @@ PRAGMA user_version = 11;
|
||||
info!("database schema upgraded v10 -> v11");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v10->v11) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -643,7 +643,7 @@ PRAGMA user_version = 13;
|
||||
info!("database schema upgraded v12 -> v13");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v12->v13) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -663,7 +663,7 @@ PRAGMA user_version = 14;
|
||||
info!("database schema upgraded v13 -> v14");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v13->v14) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -682,7 +682,7 @@ PRAGMA user_version = 15;
|
||||
info!("database schema upgraded v14 -> v15");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v14->v15) failed: {}", err);
|
||||
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)?;
|
||||
// look at each event, and each tag, creating new tag entries if appropriate.
|
||||
for t in event.tags.iter().filter(|x| x.len() > 1) {
|
||||
let tagname = t.get(0).unwrap();
|
||||
let tagname = t.first().unwrap();
|
||||
let tagnamechar_opt = single_char_tagname(tagname);
|
||||
if tagnamechar_opt.is_none() {
|
||||
continue;
|
||||
@@ -786,7 +786,7 @@ PRAGMA user_version = 17;
|
||||
info!("database schema upgraded v16 -> v17");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v16->v17) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
@@ -833,7 +833,7 @@ PRAGMA user_version = 18;
|
||||
info!("database schema upgraded v17 -> v18");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("update failed: {}", err);
|
||||
error!("update (v17->v18) failed: {}", err);
|
||||
panic!("database could not be upgraded");
|
||||
}
|
||||
}
|
||||
|
@@ -194,6 +194,21 @@ async fn handle_web_request(
|
||||
.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()
|
||||
.status(200)
|
||||
.header("Content-Type", "text/plain")
|
||||
@@ -568,6 +583,11 @@ async fn handle_web_request(
|
||||
.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
|
||||
let text =
|
||||
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<()>) {
|
||||
let mut term_signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("could not define signal");
|
||||
#[allow(clippy::never_loop)]
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown_signal.recv() => {
|
||||
@@ -893,12 +914,18 @@ pub fn start_server(settings: &Settings, shutdown_rx: MpscReceiver<()>) -> Resul
|
||||
bcast_tx.clone(),
|
||||
settings.clone(),
|
||||
);
|
||||
if let Ok(mut p) = payment_opt {
|
||||
match payment_opt {
|
||||
Ok(mut p) => {
|
||||
tokio::task::spawn(async move {
|
||||
info!("starting payment process ...");
|
||||
p.run().await;
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to start payment process {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// listen for (external to tokio) shutdown request
|
||||
@@ -1261,7 +1288,6 @@ async fn nostr_server(
|
||||
// handle each type of message
|
||||
let evid = ec.event_id().to_owned();
|
||||
let parsed : Result<EventWrapper> = Result::<EventWrapper>::from(ec);
|
||||
metrics.cmd_event.inc();
|
||||
match parsed {
|
||||
Ok(WrappedEvent(e)) => {
|
||||
metrics.cmd_event.inc();
|
||||
@@ -1346,6 +1372,11 @@ async fn nostr_server(
|
||||
if let Some(ref lim) = sub_lim_opt {
|
||||
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::<()>();
|
||||
match conn.subscribe(s.clone()) {
|
||||
Ok(()) => {
|
||||
|
@@ -258,6 +258,29 @@ impl Subscription {
|
||||
}
|
||||
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 {
|
||||
@@ -338,7 +361,7 @@ mod tests {
|
||||
let s: Subscription = serde_json::from_str(raw_json)?;
|
||||
assert_eq!(s.id, "some-id");
|
||||
assert_eq!(s.filters.len(), 1);
|
||||
assert_eq!(s.filters.get(0).unwrap().authors, None);
|
||||
assert_eq!(s.filters.first().unwrap().authors, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -402,7 +425,7 @@ mod tests {
|
||||
let s: Subscription = serde_json::from_str(raw_json)?;
|
||||
assert_eq!(s.id, "some-id");
|
||||
assert_eq!(s.filters.len(), 1);
|
||||
let first_filter = s.filters.get(0).unwrap();
|
||||
let first_filter = s.filters.first().unwrap();
|
||||
assert_eq!(
|
||||
first_filter.authors,
|
||||
Some(vec!("test-author-id".to_owned()))
|
||||
@@ -633,11 +656,11 @@ mod tests {
|
||||
let s: Subscription = serde_json::from_str(
|
||||
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_wrapped = format!(r##"["REQ", "xyz",{}]"##, serialized);
|
||||
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 {
|
||||
assert_eq!(pf.since, Some(10));
|
||||
assert_eq!(pf.until, Some(20));
|
||||
@@ -647,4 +670,29 @@ mod tests {
|
||||
}
|
||||
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 {
|
||||
info!("checking on port {}", port);
|
||||
match TcpListener::bind(("127.0.0.1", port)) {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
TcpListener::bind(("127.0.0.1", port)).is_ok()
|
||||
}
|
||||
|
@@ -52,7 +52,7 @@ mod tests {
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
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_eq!(client_conn.auth_challenge(), None);
|
||||
@@ -67,7 +67,7 @@ mod tests {
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
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)));
|
||||
}
|
||||
@@ -87,14 +87,14 @@ mod tests {
|
||||
let challenge = client_conn.auth_challenge().unwrap().clone();
|
||||
|
||||
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_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), Some(&event.pubkey));
|
||||
|
||||
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_eq!(client_conn.auth_challenge(), None);
|
||||
@@ -118,7 +118,7 @@ mod tests {
|
||||
let mut event = auth_event(challenge);
|
||||
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)));
|
||||
}
|
||||
@@ -138,7 +138,7 @@ mod tests {
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
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)));
|
||||
}
|
||||
@@ -158,7 +158,7 @@ mod tests {
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
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)));
|
||||
}
|
||||
@@ -178,7 +178,7 @@ mod tests {
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
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)));
|
||||
}
|
||||
@@ -197,7 +197,7 @@ mod tests {
|
||||
|
||||
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)));
|
||||
}
|
||||
@@ -216,7 +216,7 @@ mod tests {
|
||||
|
||||
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)));
|
||||
}
|
||||
@@ -236,7 +236,7 @@ mod tests {
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
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)));
|
||||
}
|
||||
@@ -255,7 +255,7 @@ mod tests {
|
||||
|
||||
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)));
|
||||
}
|
||||
@@ -275,7 +275,7 @@ mod tests {
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
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)));
|
||||
}
|
||||
|
Reference in New Issue
Block a user