Initial commit
							
								
								
									
										2
									
								
								.env
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					DATABASE_URL="sqlite://sqlite.db"
 | 
				
			||||||
 | 
					RUST_BACKTRACE=1
 | 
				
			||||||
							
								
								
									
										1
									
								
								.envrc
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					use flake
 | 
				
			||||||
							
								
								
									
										7
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					.direnv
 | 
				
			||||||
 | 
					result
 | 
				
			||||||
 | 
					target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Added by cargo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/target
 | 
				
			||||||
							
								
								
									
										2996
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										40
									
								
								Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "sneedstr"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					argon2 = "0.5.2"
 | 
				
			||||||
 | 
					async-trait = "0.1.73"
 | 
				
			||||||
 | 
					chrono = "0.4.31"       
 | 
				
			||||||
 | 
					tokio = { version = "1", features = ["full"] }
 | 
				
			||||||
 | 
					warp = { varsion = "0.3.3", features = ["tls"] }
 | 
				
			||||||
 | 
					validator = { version = "0.16", features = ["derive"] }
 | 
				
			||||||
 | 
					tokio-stream = "0.1.14"
 | 
				
			||||||
 | 
					futures-util = "0.3.28"
 | 
				
			||||||
 | 
					rustls = "0.21"
 | 
				
			||||||
 | 
					anyhow = "1.0"
 | 
				
			||||||
 | 
					sled = "0.34.7"
 | 
				
			||||||
 | 
					sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-rustls", "sqlite", "migrate", "macros"] }
 | 
				
			||||||
 | 
					flexi_logger = { version = "0.27.3", features = [ "async", "compress" ] }
 | 
				
			||||||
 | 
					lazy_static = "1.4.0"
 | 
				
			||||||
 | 
					log = "0.4"
 | 
				
			||||||
 | 
					nostr = "0.26.0"
 | 
				
			||||||
 | 
					regex = "1.9.5"
 | 
				
			||||||
 | 
					sailfish = "0.7.0"
 | 
				
			||||||
 | 
					sea-query = { version = "0.30.4", features = ["backend-sqlite", "thread-safe"] }
 | 
				
			||||||
 | 
					sea-query-binder = { version = "0.5.0", features = ["sqlx-sqlite"] }
 | 
				
			||||||
 | 
					serde = "1.0"
 | 
				
			||||||
 | 
					serde_json = "1.0"
 | 
				
			||||||
 | 
					thiserror = "1.0.48"
 | 
				
			||||||
 | 
					flexbuffers = "2.0.0"
 | 
				
			||||||
 | 
					uuid = { version = "1.3.1", features = ["v4", "fast-rng"] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[profile.release]
 | 
				
			||||||
 | 
					opt-level = 'z'     # Optimize for size
 | 
				
			||||||
 | 
					lto = true          # Enable link-time optimization
 | 
				
			||||||
 | 
					codegen-units = 1   # Reduce number of codegen units to increase optimizations
 | 
				
			||||||
 | 
					panic = 'abort'     # Abort on panic
 | 
				
			||||||
 | 
					strip = true        # Strip symbols from binary*
 | 
				
			||||||
							
								
								
									
										8
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					Copyright (C) 2024 Klink
 | 
				
			||||||
 | 
					This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice, this permission notice and the word "NIGGER" shall be included in all copies or substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>
 | 
				
			||||||
							
								
								
									
										93
									
								
								flake.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,93 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "nodes": {
 | 
				
			||||||
 | 
					    "flake-utils": {
 | 
				
			||||||
 | 
					      "inputs": {
 | 
				
			||||||
 | 
					        "systems": "systems"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "locked": {
 | 
				
			||||||
 | 
					        "lastModified": 1701680307,
 | 
				
			||||||
 | 
					        "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
 | 
				
			||||||
 | 
					        "owner": "numtide",
 | 
				
			||||||
 | 
					        "repo": "flake-utils",
 | 
				
			||||||
 | 
					        "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
 | 
				
			||||||
 | 
					        "type": "github"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "original": {
 | 
				
			||||||
 | 
					        "owner": "numtide",
 | 
				
			||||||
 | 
					        "repo": "flake-utils",
 | 
				
			||||||
 | 
					        "type": "github"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "naersk": {
 | 
				
			||||||
 | 
					      "inputs": {
 | 
				
			||||||
 | 
					        "nixpkgs": "nixpkgs"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "locked": {
 | 
				
			||||||
 | 
					        "lastModified": 1698420672,
 | 
				
			||||||
 | 
					        "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
 | 
				
			||||||
 | 
					        "owner": "nix-community",
 | 
				
			||||||
 | 
					        "repo": "naersk",
 | 
				
			||||||
 | 
					        "rev": "aeb58d5e8faead8980a807c840232697982d47b9",
 | 
				
			||||||
 | 
					        "type": "github"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "original": {
 | 
				
			||||||
 | 
					        "owner": "nix-community",
 | 
				
			||||||
 | 
					        "repo": "naersk",
 | 
				
			||||||
 | 
					        "type": "github"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nixpkgs": {
 | 
				
			||||||
 | 
					      "locked": {
 | 
				
			||||||
 | 
					        "lastModified": 1703438236,
 | 
				
			||||||
 | 
					        "narHash": "sha256-aqVBq1u09yFhL7bj1/xyUeJjzr92fXVvQSSEx6AdB1M=",
 | 
				
			||||||
 | 
					        "path": "/nix/store/5acdh8xyry0kdvp6xla2hw7wf3zkphkl-source",
 | 
				
			||||||
 | 
					        "rev": "5f64a12a728902226210bf01d25ec6cbb9d9265b",
 | 
				
			||||||
 | 
					        "type": "path"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "original": {
 | 
				
			||||||
 | 
					        "id": "nixpkgs",
 | 
				
			||||||
 | 
					        "type": "indirect"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nixpkgs_2": {
 | 
				
			||||||
 | 
					      "locked": {
 | 
				
			||||||
 | 
					        "lastModified": 1703499205,
 | 
				
			||||||
 | 
					        "narHash": "sha256-lF9rK5mSUfIZJgZxC3ge40tp1gmyyOXZ+lRY3P8bfbg=",
 | 
				
			||||||
 | 
					        "owner": "NixOS",
 | 
				
			||||||
 | 
					        "repo": "nixpkgs",
 | 
				
			||||||
 | 
					        "rev": "e1fa12d4f6c6fe19ccb59cac54b5b3f25e160870",
 | 
				
			||||||
 | 
					        "type": "github"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "original": {
 | 
				
			||||||
 | 
					        "owner": "NixOS",
 | 
				
			||||||
 | 
					        "ref": "nixpkgs-unstable",
 | 
				
			||||||
 | 
					        "repo": "nixpkgs",
 | 
				
			||||||
 | 
					        "type": "github"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "root": {
 | 
				
			||||||
 | 
					      "inputs": {
 | 
				
			||||||
 | 
					        "flake-utils": "flake-utils",
 | 
				
			||||||
 | 
					        "naersk": "naersk",
 | 
				
			||||||
 | 
					        "nixpkgs": "nixpkgs_2"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										52
									
								
								flake.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,52 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  inputs = {
 | 
				
			||||||
 | 
					    flake-utils.url = "github:numtide/flake-utils";
 | 
				
			||||||
 | 
					    naersk.url = "github:nix-community/naersk";
 | 
				
			||||||
 | 
					    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  outputs = { self, flake-utils, naersk, nixpkgs }:
 | 
				
			||||||
 | 
					    flake-utils.lib.eachDefaultSystem (system:
 | 
				
			||||||
 | 
					      let
 | 
				
			||||||
 | 
					        pkgs = (import nixpkgs) { inherit system; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        naersk' = pkgs.callPackage naersk { };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        wwwPath = "www";
 | 
				
			||||||
 | 
					        templatesPath = "templates";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      in rec {
 | 
				
			||||||
 | 
					        # For `nix build` & `nix run`:
 | 
				
			||||||
 | 
					        defaultPackage = naersk'.buildPackage {
 | 
				
			||||||
 | 
					          src = ./.;
 | 
				
			||||||
 | 
					          nativeBuildInputs = with pkgs; [ pkg-config openssl sqlite ];
 | 
				
			||||||
 | 
					          GIT_HASH = "000000000000000000000000000000";
 | 
				
			||||||
 | 
					          postInstall = ''
 | 
				
			||||||
 | 
					            mkdir -p $out/templates
 | 
				
			||||||
 | 
					            mkdir -p $out/www
 | 
				
			||||||
 | 
					            cp -r ${wwwPath} $out/
 | 
				
			||||||
 | 
					            cp -r ${templatesPath} $out/
 | 
				
			||||||
 | 
					          '';
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # For `nix develop`:
 | 
				
			||||||
 | 
					        devShell = pkgs.mkShell {
 | 
				
			||||||
 | 
					          nativeBuildInputs = with pkgs; [
 | 
				
			||||||
 | 
					            rustc
 | 
				
			||||||
 | 
					            cargo
 | 
				
			||||||
 | 
					            cargo-watch
 | 
				
			||||||
 | 
					            clippy
 | 
				
			||||||
 | 
					            rustfmt
 | 
				
			||||||
 | 
					            rust-analyzer
 | 
				
			||||||
 | 
					            pkg-config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            openssl
 | 
				
			||||||
 | 
					            sqlite
 | 
				
			||||||
 | 
					          ];
 | 
				
			||||||
 | 
					          env = {
 | 
				
			||||||
 | 
					            DATABASE_URL = "sqlite://sqlite.db";
 | 
				
			||||||
 | 
					            RUST_BACKTRACE = 1;
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										143
									
								
								src/bussy/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,143 @@
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    noose::user::{User, UserRow},
 | 
				
			||||||
 | 
					    utils::{error::Error, structs::Subscription},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use nostr::secp256k1::XOnlyPublicKey;
 | 
				
			||||||
 | 
					use std::{collections::HashMap, fmt::Debug};
 | 
				
			||||||
 | 
					use tokio::sync::{broadcast, Mutex};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod channels {
 | 
				
			||||||
 | 
					    pub static MSG_NOOSE: &str = "MSG_NOOSE";
 | 
				
			||||||
 | 
					    pub static MSG_NIP05: &str = "MSG_NIP05";
 | 
				
			||||||
 | 
					    pub static MSG_RELAY: &str = "MSG_RELAY";
 | 
				
			||||||
 | 
					    pub static MSG_PIPELINE: &str = "MSG_PIPELINE";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, PartialEq)]
 | 
				
			||||||
 | 
					pub enum Command {
 | 
				
			||||||
 | 
					    // DbRequest
 | 
				
			||||||
 | 
					    DbReqWriteEvent(Box<nostr::Event>),
 | 
				
			||||||
 | 
					    DbReqFindEvent(/* client_id*/ uuid::Uuid, Subscription),
 | 
				
			||||||
 | 
					    DbReqDeleteEvents(/* event ids */ Vec<String>),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Old messages
 | 
				
			||||||
 | 
					    DbReqInsertUser(UserRow),
 | 
				
			||||||
 | 
					    DbReqGetUser(User),
 | 
				
			||||||
 | 
					    DbReqCreateAccount(XOnlyPublicKey, String, String),
 | 
				
			||||||
 | 
					    DbReqGetAccount(String),
 | 
				
			||||||
 | 
					    DbReqClear,
 | 
				
			||||||
 | 
					    // DbResponse
 | 
				
			||||||
 | 
					    DbResRelayMessage(
 | 
				
			||||||
 | 
					        /* client_id*/ uuid::Uuid,
 | 
				
			||||||
 | 
					        /* Vec<RelayMessage::Event> */ Vec<String>,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    DbResInfo,
 | 
				
			||||||
 | 
					    DbResOk,
 | 
				
			||||||
 | 
					    DbResOkWithStatus(String),
 | 
				
			||||||
 | 
					    DbResAccount, // TODO: Add Account DTO as a param
 | 
				
			||||||
 | 
					    DbResUser(UserRow),
 | 
				
			||||||
 | 
					    // Event Pipeline
 | 
				
			||||||
 | 
					    PipelineReqEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),
 | 
				
			||||||
 | 
					    PipelineResRelayMessageOk(/* client_id */ uuid::Uuid, nostr::RelayMessage),
 | 
				
			||||||
 | 
					    PipelineResStreamOutEvent(Box<nostr::Event>),
 | 
				
			||||||
 | 
					    PipelineResOk,
 | 
				
			||||||
 | 
					    // Other
 | 
				
			||||||
 | 
					    Str(String),
 | 
				
			||||||
 | 
					    ServiceError(Error),
 | 
				
			||||||
 | 
					    Noop,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct Message {
 | 
				
			||||||
 | 
					    pub source: &'static str,
 | 
				
			||||||
 | 
					    pub content: Command,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct PubSub {
 | 
				
			||||||
 | 
					    subscribers: Mutex<HashMap<String, Vec<broadcast::Sender<Message>>>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Default for PubSub {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        panic!("Use PubSub::new() to initialize PubSub");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl PubSub {
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        PubSub {
 | 
				
			||||||
 | 
					            subscribers: Mutex::new(HashMap::new()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn subscribe(&self, topic: &str) -> broadcast::Receiver<Message> {
 | 
				
			||||||
 | 
					        let (tx, _rx) = broadcast::channel(32); // 32 is the channel capacity
 | 
				
			||||||
 | 
					        let mut subscribers = self.subscribers.lock().await;
 | 
				
			||||||
 | 
					        subscribers
 | 
				
			||||||
 | 
					            .entry(topic.to_string())
 | 
				
			||||||
 | 
					            .or_insert_with(Vec::new)
 | 
				
			||||||
 | 
					            .push(tx.clone());
 | 
				
			||||||
 | 
					        tx.subscribe()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn publish(&self, topic: &str, message: Message) {
 | 
				
			||||||
 | 
					        let mut subscribers = self.subscribers.lock().await;
 | 
				
			||||||
 | 
					        if let Some(queue) = subscribers.get_mut(topic) {
 | 
				
			||||||
 | 
					            for sender in queue.iter() {
 | 
				
			||||||
 | 
					                sender.send(message.clone()).ok();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// #[cfg(test)]
 | 
				
			||||||
 | 
					// mod tests {
 | 
				
			||||||
 | 
					//     use super::channels;
 | 
				
			||||||
 | 
					//     use crate::bussy::{Command, Message, PubSub};
 | 
				
			||||||
 | 
					//     use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//     #[tokio::test]
 | 
				
			||||||
 | 
					//     async fn create_bus() {
 | 
				
			||||||
 | 
					//         let pubsub = Arc::new(PubSub::new());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//         let mut subscriber1 = pubsub.subscribe(channels::MSG_NIP05).await;
 | 
				
			||||||
 | 
					//         let mut subscriber2 = pubsub.subscribe(channels::MSG_NOOSE).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//         tokio::spawn(async move {
 | 
				
			||||||
 | 
					//             while let Ok(message) = subscriber1.recv().await {
 | 
				
			||||||
 | 
					//                 println!("Subscriber1 received: {:?}", message);
 | 
				
			||||||
 | 
					//             }
 | 
				
			||||||
 | 
					//         });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//         tokio::spawn(async move {
 | 
				
			||||||
 | 
					//             while let Ok(message) = subscriber2.recv().await {
 | 
				
			||||||
 | 
					//                 println!("Subscriber2 received: {:?}", message);
 | 
				
			||||||
 | 
					//             }
 | 
				
			||||||
 | 
					//         });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//         pubsub
 | 
				
			||||||
 | 
					//             .publish(
 | 
				
			||||||
 | 
					//                 channels::MSG_NIP05,
 | 
				
			||||||
 | 
					//                 Message {
 | 
				
			||||||
 | 
					//                     source: "test",
 | 
				
			||||||
 | 
					//                     content: Command::Str("Hello S1".to_string()),
 | 
				
			||||||
 | 
					//                 },
 | 
				
			||||||
 | 
					//             )
 | 
				
			||||||
 | 
					//             .await;
 | 
				
			||||||
 | 
					//         pubsub
 | 
				
			||||||
 | 
					//             .publish(
 | 
				
			||||||
 | 
					//                 channels::MSG_NOOSE,
 | 
				
			||||||
 | 
					//                 Message {
 | 
				
			||||||
 | 
					//                     source: "test",
 | 
				
			||||||
 | 
					//                     content: Command::Str("Hello S2".to_string()),
 | 
				
			||||||
 | 
					//                 },
 | 
				
			||||||
 | 
					//             )
 | 
				
			||||||
 | 
					//             .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//         dbg!(pubsub);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//         // Sleep to keep the main thread alive while messages are processed in the background.
 | 
				
			||||||
 | 
					//         tokio::time::sleep(std::time::Duration::from_secs(1)).await;
 | 
				
			||||||
 | 
					//     }
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
							
								
								
									
										51
									
								
								src/main.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,51 @@
 | 
				
			||||||
 | 
					#![allow(dead_code)]
 | 
				
			||||||
 | 
					#![allow(unused_variables)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::thread;
 | 
				
			||||||
 | 
					pub mod bussy;
 | 
				
			||||||
 | 
					mod noose;
 | 
				
			||||||
 | 
					mod relay;
 | 
				
			||||||
 | 
					mod usernames;
 | 
				
			||||||
 | 
					pub mod utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use bussy::PubSub;
 | 
				
			||||||
 | 
					use flexi_logger::Logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::utils::structs::Context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[macro_use]
 | 
				
			||||||
 | 
					extern crate lazy_static;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn main() {
 | 
				
			||||||
 | 
					    // Logger init
 | 
				
			||||||
 | 
					    let _logger = Logger::try_with_env_or_str("info")
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					        .log_to_stdout()
 | 
				
			||||||
 | 
					        .duplicate_to_stderr(flexi_logger::Duplicate::Warn)
 | 
				
			||||||
 | 
					        .write_mode(flexi_logger::WriteMode::Async)
 | 
				
			||||||
 | 
					        .start()
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Services
 | 
				
			||||||
 | 
					    let ctx_db = Context::new();
 | 
				
			||||||
 | 
					    let ctx_relay = ctx_db.clone();
 | 
				
			||||||
 | 
					    let ctx_usernames = ctx_relay.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let db_handle = thread::spawn(move || {
 | 
				
			||||||
 | 
					        noose::start(ctx_db);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let relay_handle = thread::spawn(move || {
 | 
				
			||||||
 | 
					        relay::start(ctx_relay);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let nip05_handle = thread::spawn(move || {
 | 
				
			||||||
 | 
					        usernames::start(ctx_usernames);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db_handle.join().unwrap();
 | 
				
			||||||
 | 
					    relay_handle.join().unwrap();
 | 
				
			||||||
 | 
					    nip05_handle.join().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    println!("Done");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/noose/db.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    bussy::PubSub,
 | 
				
			||||||
 | 
					    utils::{error::Error, structs::Subscription},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use async_trait::async_trait;
 | 
				
			||||||
 | 
					use nostr::Event;
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					pub trait Noose: Send + Sync {
 | 
				
			||||||
 | 
					    async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn migration_up(&self);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn write_event(&self, event: Box<Event>) -> Result<String, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn find_event(&self, subscription: Subscription) -> Result<Vec<String>, Error>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/noose/migrations/1697409647688_create_events.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					CREATE TABLE events (
 | 
				
			||||||
 | 
					    id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					    kind INTEGER NOT NULL,
 | 
				
			||||||
 | 
					    pubkey TEXT NOT NULL,
 | 
				
			||||||
 | 
					    content TEXT NOT NULL,
 | 
				
			||||||
 | 
					    created_at INTEGER NOT NULL,
 | 
				
			||||||
 | 
					    tags TEXT NOT NULL,
 | 
				
			||||||
 | 
					    sig TEXT NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE INDEX idx_events_kind ON events (kind);
 | 
				
			||||||
 | 
					CREATE INDEX idx_events_pubkey ON events (pubkey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE tags (
 | 
				
			||||||
 | 
					    tag TEXT NOT NULL,
 | 
				
			||||||
 | 
					    value TEXT NOT NULL,
 | 
				
			||||||
 | 
					    event_id TEXT REFERENCES events(id) ON DELETE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE INDEX idx_tags_tag ON tags (tag);
 | 
				
			||||||
 | 
					CREATE INDEX idx_tags_value ON tags (value);
 | 
				
			||||||
 | 
					CREATE INDEX idx_tags_event_id ON tags (event_id);
 | 
				
			||||||
							
								
								
									
										5
									
								
								src/noose/migrations/1697410161900_add_relays.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					CREATE TABLE relays (
 | 
				
			||||||
 | 
					    url TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					    domain TEXT NOT NULL,
 | 
				
			||||||
 | 
					    active BOOLEAN NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/noose/migrations/1697410223576_events_fts.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					CREATE VIRTUAL TABLE events_fts USING fts5(id, content);
 | 
				
			||||||
							
								
								
									
										10
									
								
								src/noose/migrations/1697410294265_users.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					CREATE TABLE users (
 | 
				
			||||||
 | 
					    pubkey TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					    username TEXT NOT NULL UNIQUE,
 | 
				
			||||||
 | 
					    inserted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
					    admin BOOLEAN DEFAULT false
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE INDEX idx_users_pubkey ON users (pubkey);
 | 
				
			||||||
 | 
					CREATE INDEX idx_users_username ON users (username);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								src/noose/migrations/1697410424624_pragma.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					PRAGMA foreign_keys = ON;
 | 
				
			||||||
 | 
					PRAGMA auto_vacuum = FULL;
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/noose/migrations/1697410480767_unattached_media.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					CREATE TABLE unattached_media (
 | 
				
			||||||
 | 
					    id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					    pubkey TEXT NOT NULL,
 | 
				
			||||||
 | 
					    url TEXT NOT NULL,
 | 
				
			||||||
 | 
					    data TEXT NOT NULL,
 | 
				
			||||||
 | 
					    uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE INDEX unattached_media_id ON unattached_media (id);
 | 
				
			||||||
 | 
					CREATE INDEX unattached_media_pubkey ON unattached_media (pubkey);
 | 
				
			||||||
 | 
					CREATE INDEX unattached_media_url ON unattached_media (url);
 | 
				
			||||||
							
								
								
									
										33
									
								
								src/noose/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					use crate::utils::structs::Context;
 | 
				
			||||||
 | 
					use tokio::runtime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use db::Noose;
 | 
				
			||||||
 | 
					use pipeline::Pipeline;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod db;
 | 
				
			||||||
 | 
					pub mod pipeline;
 | 
				
			||||||
 | 
					// mod sled;
 | 
				
			||||||
 | 
					mod sqlite;
 | 
				
			||||||
 | 
					pub mod user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn start(context: Context) {
 | 
				
			||||||
 | 
					    let rt = runtime::Runtime::new().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rt.block_on(async move {
 | 
				
			||||||
 | 
					        let pipeline_pubsub = context.pubsub.clone();
 | 
				
			||||||
 | 
					        let db_pubsub = context.pubsub.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let pipeline_handle = tokio::task::spawn(async move {
 | 
				
			||||||
 | 
					            let mut pipeline = Pipeline::new(pipeline_pubsub);
 | 
				
			||||||
 | 
					            pipeline.start().await.unwrap();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let sqlite_writer_handle = tokio::task::spawn(async move {
 | 
				
			||||||
 | 
					            let mut db_writer = sqlite::SqliteDb::new().await;
 | 
				
			||||||
 | 
					            db_writer.start(db_pubsub).await.unwrap();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sqlite_writer_handle.await.unwrap();
 | 
				
			||||||
 | 
					        pipeline_handle.await.unwrap();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										166
									
								
								src/noose/pipeline.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,166 @@
 | 
				
			||||||
 | 
					use crate::bussy::{channels, Command, Message, PubSub};
 | 
				
			||||||
 | 
					use crate::utils::error::Error;
 | 
				
			||||||
 | 
					use nostr::Event;
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Pipeline {
 | 
				
			||||||
 | 
					    pubsub: Arc<PubSub>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Pipeline {
 | 
				
			||||||
 | 
					    pub fn new(pubsub: Arc<PubSub>) -> Self {
 | 
				
			||||||
 | 
					        Self { pubsub }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn start(&mut self) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let mut subscriber = self.pubsub.subscribe(channels::MSG_PIPELINE).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while let Ok(message) = subscriber.recv().await {
 | 
				
			||||||
 | 
					            log::debug!("[Pipeline] received message: {:?}", message);
 | 
				
			||||||
 | 
					            let command = match message.content {
 | 
				
			||||||
 | 
					                Command::PipelineReqEvent(client_id, event) => {
 | 
				
			||||||
 | 
					                    match self.handle_event(client_id, event.clone()).await {
 | 
				
			||||||
 | 
					                        Ok(_) => {
 | 
				
			||||||
 | 
					                            let message =
 | 
				
			||||||
 | 
					                                nostr::RelayMessage::new_ok(event.id, true, "".to_string());
 | 
				
			||||||
 | 
					                            Command::PipelineResRelayMessageOk(client_id, message)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        Err(e) => Command::ServiceError(e),
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                _ => Command::Noop,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            if command != Command::Noop {
 | 
				
			||||||
 | 
					                let channel = message.source;
 | 
				
			||||||
 | 
					                let message = Message {
 | 
				
			||||||
 | 
					                    source: channels::MSG_PIPELINE,
 | 
				
			||||||
 | 
					                    content: command,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                log::info!(
 | 
				
			||||||
 | 
					                    "[Pipeline] channel: {} - publishing new message: {:?}",
 | 
				
			||||||
 | 
					                    channel,
 | 
				
			||||||
 | 
					                    message
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                self.pubsub.publish(channel, message).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn handle_event(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        client_id: uuid::Uuid,
 | 
				
			||||||
 | 
					        event: Box<Event>,
 | 
				
			||||||
 | 
					    ) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let store_event_task = self.store_event(event.clone());
 | 
				
			||||||
 | 
					        let process_deletions_task = self.process_deletions(event.clone());
 | 
				
			||||||
 | 
					        let track_hashtags_task = self.track_hashtags(event.clone());
 | 
				
			||||||
 | 
					        let process_media_task = self.process_media(event.clone());
 | 
				
			||||||
 | 
					        let stream_out_task = self.stream_out(event.clone());
 | 
				
			||||||
 | 
					        let broadcast_task = self.broadcast(event.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let (
 | 
				
			||||||
 | 
					            store_event_result,
 | 
				
			||||||
 | 
					            process_deletions_result,
 | 
				
			||||||
 | 
					            track_hashtags_result,
 | 
				
			||||||
 | 
					            process_media_result,
 | 
				
			||||||
 | 
					            stream_out_result,
 | 
				
			||||||
 | 
					            broadcast_result,
 | 
				
			||||||
 | 
					        ) = tokio::join!(
 | 
				
			||||||
 | 
					            store_event_task,
 | 
				
			||||||
 | 
					            process_deletions_task,
 | 
				
			||||||
 | 
					            track_hashtags_task,
 | 
				
			||||||
 | 
					            process_media_task,
 | 
				
			||||||
 | 
					            stream_out_task,
 | 
				
			||||||
 | 
					            broadcast_task
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match (
 | 
				
			||||||
 | 
					            store_event_result,
 | 
				
			||||||
 | 
					            process_deletions_result,
 | 
				
			||||||
 | 
					            track_hashtags_result,
 | 
				
			||||||
 | 
					            process_media_result,
 | 
				
			||||||
 | 
					            stream_out_result,
 | 
				
			||||||
 | 
					            broadcast_result,
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            (Ok(_), Ok(_), Ok(_), Ok(_), Ok(_), Ok(_)) => {
 | 
				
			||||||
 | 
					                log::info!("[Pipeline] Tasks finished successfully");
 | 
				
			||||||
 | 
					                Ok(())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => {
 | 
				
			||||||
 | 
					                log::error!("[Pipeline] One or more futures returned an error.");
 | 
				
			||||||
 | 
					                Err(Error::internal_with_message(
 | 
				
			||||||
 | 
					                    "[Pipeline] One or more futures returned an error.",
 | 
				
			||||||
 | 
					                ))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn store_event(&self, event: Box<Event>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        if event.kind.is_ephemeral() {
 | 
				
			||||||
 | 
					            return Ok(());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.pubsub
 | 
				
			||||||
 | 
					            .publish(
 | 
				
			||||||
 | 
					                channels::MSG_NOOSE,
 | 
				
			||||||
 | 
					                Message {
 | 
				
			||||||
 | 
					                    source: channels::MSG_PIPELINE,
 | 
				
			||||||
 | 
					                    content: Command::DbReqWriteEvent(event),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn process_deletions(&self, event: Box<Event>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        // if event.kind.as_u32() == 5 {
 | 
				
			||||||
 | 
					        //     let events_for_deletion: Vec<String> = event
 | 
				
			||||||
 | 
					        //         .tags
 | 
				
			||||||
 | 
					        //         .iter()
 | 
				
			||||||
 | 
					        //         .filter_map(|tag| match tag {
 | 
				
			||||||
 | 
					        //             nostr::Tag::Event(event_id, _, _) => Some(event_id.to_string()),
 | 
				
			||||||
 | 
					        //             _ => None,
 | 
				
			||||||
 | 
					        //         })
 | 
				
			||||||
 | 
					        //         .collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        //     self.pubsub
 | 
				
			||||||
 | 
					        //         .publish(
 | 
				
			||||||
 | 
					        //             channels::MSG_NOOSE,
 | 
				
			||||||
 | 
					        //             Message {
 | 
				
			||||||
 | 
					        //                 source: channels::MSG_PIPELINE,
 | 
				
			||||||
 | 
					        //                 content: Command::DbReqDeleteEvents(events_for_deletion),
 | 
				
			||||||
 | 
					        //             },
 | 
				
			||||||
 | 
					        //         )
 | 
				
			||||||
 | 
					        //         .await;
 | 
				
			||||||
 | 
					        // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn track_hashtags(&self, event: Box<Event>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn process_media(&self, event: Box<Event>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn stream_out(&self, event: Box<Event>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let message = Message {
 | 
				
			||||||
 | 
					            source: channels::MSG_PIPELINE,
 | 
				
			||||||
 | 
					            content: Command::PipelineResStreamOutEvent(event),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        self.pubsub.publish(channels::MSG_RELAY, message).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn broadcast(&self, event: Box<Event>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										234
									
								
								src/noose/sled.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,234 @@
 | 
				
			||||||
 | 
					use super::db::Noose;
 | 
				
			||||||
 | 
					use crate::bussy::{channels, Command, Message, PubSub};
 | 
				
			||||||
 | 
					use crate::utils::error::Error;
 | 
				
			||||||
 | 
					use crate::utils::structs::Subscription;
 | 
				
			||||||
 | 
					use async_trait::async_trait;
 | 
				
			||||||
 | 
					use nostr::Event;
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::user::{User, UserRow};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Db Interface
 | 
				
			||||||
 | 
					pub struct SledDb {
 | 
				
			||||||
 | 
					    db: sled::Db,
 | 
				
			||||||
 | 
					    events: sled::Tree,
 | 
				
			||||||
 | 
					    nip05s: sled::Tree,
 | 
				
			||||||
 | 
					    pub users: sled::Tree,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    index: sled::Db,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl SledDb {
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        let db = sled::open("/tmp/sled_db").unwrap();
 | 
				
			||||||
 | 
					        let events = db.open_tree("events").unwrap();
 | 
				
			||||||
 | 
					        let nip05s = db.open_tree("identifiers").unwrap();
 | 
				
			||||||
 | 
					        let accounts = db.open_tree("accounts").unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let index = sled::open("/tmp/sled_index").unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            db,
 | 
				
			||||||
 | 
					            events,
 | 
				
			||||||
 | 
					            nip05s,
 | 
				
			||||||
 | 
					            users: accounts,
 | 
				
			||||||
 | 
					            index,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn clear_db(&self) -> Result<(), sled::Error> {
 | 
				
			||||||
 | 
					        self.db.clear()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn clear_index(&self) -> Result<(), sled::Error> {
 | 
				
			||||||
 | 
					        self.index.clear()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn insert_user(&self, user: UserRow) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let pubkey = user.pubkey.clone();
 | 
				
			||||||
 | 
					        let username = user.username.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Ok(Some(_)) = self.nip05s.get(&username) {
 | 
				
			||||||
 | 
					            return Err(Error::internal_with_message("User already exists"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut user_buff = flexbuffers::FlexbufferSerializer::new();
 | 
				
			||||||
 | 
					        user.serialize(&mut user_buff).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.nip05s.insert(&username, user_buff.view()).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let prefix = "nip05:";
 | 
				
			||||||
 | 
					        let key = format!("{}{}", prefix, pubkey);
 | 
				
			||||||
 | 
					        self.index.insert(key, username.as_bytes()).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn get_user(&self, user: User) -> Result<UserRow, Error> {
 | 
				
			||||||
 | 
					        let mut user_row = None;
 | 
				
			||||||
 | 
					        if let Some(username) = user.name {
 | 
				
			||||||
 | 
					            if let Ok(Some(buff)) = self.nip05s.get(username) {
 | 
				
			||||||
 | 
					                let b = flexbuffers::from_slice::<UserRow>(&buff).unwrap();
 | 
				
			||||||
 | 
					                user_row = Some(b);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if let Some(pubkey) = user.pubkey {
 | 
				
			||||||
 | 
					            let prefix = "nip05:";
 | 
				
			||||||
 | 
					            let reference = format!("{}{}", prefix, pubkey);
 | 
				
			||||||
 | 
					            if let Ok(Some(row)) = self.index.get(reference) {
 | 
				
			||||||
 | 
					                let key = String::from_utf8(row.to_vec()).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Ok(Some(buff)) = self.nip05s.get(key) {
 | 
				
			||||||
 | 
					                    let b = flexbuffers::from_slice::<UserRow>(&buff).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    user_row = Some(b);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        match user_row {
 | 
				
			||||||
 | 
					            Some(user) => Ok(user),
 | 
				
			||||||
 | 
					            None => Err(Error::internal_with_message("User not found")),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl Noose for SledDb {
 | 
				
			||||||
 | 
					    async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let mut subscriber = pubsub.subscribe(channels::MSG_NOOSE).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while let Ok(message) = subscriber.recv().await {
 | 
				
			||||||
 | 
					            log::info!("noose subscriber received: {:?}", message);
 | 
				
			||||||
 | 
					            let command = match message.content {
 | 
				
			||||||
 | 
					                Command::DbReqInsertUser(user) => match self.insert_user(user).await {
 | 
				
			||||||
 | 
					                    Ok(_) => Command::DbResOk,
 | 
				
			||||||
 | 
					                    Err(e) => Command::ServiceError(e),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                Command::DbReqGetUser(user) => match self.get_user(user).await {
 | 
				
			||||||
 | 
					                    Ok(user) => Command::DbResUser(user),
 | 
				
			||||||
 | 
					                    Err(e) => Command::ServiceError(e),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                Command::DbReqWriteEvent(event) => match self.write_event(event).await {
 | 
				
			||||||
 | 
					                    Ok(_) => Command::DbResOk,
 | 
				
			||||||
 | 
					                    Err(e) => Command::ServiceError(e),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                _ => Command::Noop,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            if command != Command::Noop {
 | 
				
			||||||
 | 
					                log::info!("Publishing new message");
 | 
				
			||||||
 | 
					                let channel = message.source;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                pubsub
 | 
				
			||||||
 | 
					                    .publish(
 | 
				
			||||||
 | 
					                        channel,
 | 
				
			||||||
 | 
					                        Message {
 | 
				
			||||||
 | 
					                            source: channels::MSG_NOOSE,
 | 
				
			||||||
 | 
					                            content: command,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn migration_up(&self) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn write_event(&self, event: Box<Event>) -> Result<String, Error> {
 | 
				
			||||||
 | 
					        let mut event_buff = flexbuffers::FlexbufferSerializer::new();
 | 
				
			||||||
 | 
					        event.serialize(&mut event_buff).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.events.insert(event.id, event_buff.view()).unwrap();
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Timestamp
 | 
				
			||||||
 | 
					            let key = format!("created_at:{}|#e:{}", event.created_at, event.id);
 | 
				
			||||||
 | 
					            self.index.insert(key, event.id.as_bytes()).unwrap();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Author, pubkeys #p
 | 
				
			||||||
 | 
					            let key = format!("#author:{}|#e:{}", event.pubkey, event.id);
 | 
				
			||||||
 | 
					            self.index.insert(key, event.id.as_bytes()).unwrap();
 | 
				
			||||||
 | 
					            // self.index.scan_prefix(
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Kinds
 | 
				
			||||||
 | 
					            let key = format!("#k:{}|#e:{}", event.kind, event.id);
 | 
				
			||||||
 | 
					            self.index.insert(key, event.id.as_bytes()).unwrap();
 | 
				
			||||||
 | 
					            // self.index.scan_prefix(
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Tags
 | 
				
			||||||
 | 
					            event.tags.iter().for_each(|tag| {
 | 
				
			||||||
 | 
					                if let Some(key) = match tag {
 | 
				
			||||||
 | 
					                    // #e tag
 | 
				
			||||||
 | 
					                    nostr::Tag::Event(event_id, _, _) => Some(format!("#e:{}", event_id)),
 | 
				
			||||||
 | 
					                    // #p tag
 | 
				
			||||||
 | 
					                    nostr::Tag::PubKey(pubkey, _) => Some(format!("#p:{}|#e:{}", pubkey, event.id)),
 | 
				
			||||||
 | 
					                    // #t tag
 | 
				
			||||||
 | 
					                    nostr::Tag::Hashtag(hashtag) => Some(format!("#t:{}|#e:{}", hashtag, event.id)),
 | 
				
			||||||
 | 
					                    // #a tag
 | 
				
			||||||
 | 
					                    nostr::Tag::A {
 | 
				
			||||||
 | 
					                        kind,
 | 
				
			||||||
 | 
					                        public_key,
 | 
				
			||||||
 | 
					                        identifier,
 | 
				
			||||||
 | 
					                        relay_url,
 | 
				
			||||||
 | 
					                    } => Some(format!(
 | 
				
			||||||
 | 
					                        "#a:kind:{}|#a:pubkey:{}#a:identifier:{}|#e:{}",
 | 
				
			||||||
 | 
					                        kind, public_key, identifier, event.id
 | 
				
			||||||
 | 
					                    )),
 | 
				
			||||||
 | 
					                    _ => None,
 | 
				
			||||||
 | 
					                } {
 | 
				
			||||||
 | 
					                    self.index.insert(key, event.id.as_bytes()).unwrap();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // let key = format!("#t:{}|#e:{}", event.kind, event.id);
 | 
				
			||||||
 | 
					            // self.index.insert(key, event.id.as_bytes()).unwrap();
 | 
				
			||||||
 | 
					            // self.index.scan_prefix(
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let message = format!("[\"OK\", \"{}\", true, \"\"]", event.id.to_string());
 | 
				
			||||||
 | 
					        Ok(message)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn find_event(&self, subscription: Subscription) -> Result<Vec<String>, Error> {
 | 
				
			||||||
 | 
					        todo!()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					    use super::SledDb;
 | 
				
			||||||
 | 
					    use crate::{
 | 
				
			||||||
 | 
					        bussy::PubSub,
 | 
				
			||||||
 | 
					        noose::user::{User, UserRow},
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[tokio::test]
 | 
				
			||||||
 | 
					    async fn get_db_names() {
 | 
				
			||||||
 | 
					        let pubsub = Arc::new(PubSub::new());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let db = SledDb::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let pk = "npub1p3ya99jfdafnqlk87p6wfd36d2nme5mkld769rhd9pkht6hmqlaq6mzxdu".to_string();
 | 
				
			||||||
 | 
					        let username = "klink".to_string();
 | 
				
			||||||
 | 
					        let user = UserRow::new(pk, username, false);
 | 
				
			||||||
 | 
					        let result = db.insert_user(user).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let pubkey = "npub1p3ya99jfdafnqlk87p6wfd36d2nme5mkld769rhd9pkht6hmqlaq6mzxdu".to_string();
 | 
				
			||||||
 | 
					        let username = "klink".to_string();
 | 
				
			||||||
 | 
					        let user = User {
 | 
				
			||||||
 | 
					            name: None,
 | 
				
			||||||
 | 
					            pubkey: Some(pubkey),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let user = db.get_user(user).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        db.clear_db().unwrap();
 | 
				
			||||||
 | 
					        db.clear_index().unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										889
									
								
								src/noose/sqlite.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,889 @@
 | 
				
			||||||
 | 
					use async_trait::async_trait;
 | 
				
			||||||
 | 
					use nostr::{Event, JsonUtil};
 | 
				
			||||||
 | 
					use sea_query::{extension::sqlite::SqliteExpr, Query};
 | 
				
			||||||
 | 
					use sea_query_binder::SqlxBinder;
 | 
				
			||||||
 | 
					use sqlx::sqlite::{Sqlite, SqlitePoolOptions};
 | 
				
			||||||
 | 
					use sqlx::FromRow;
 | 
				
			||||||
 | 
					use sqlx::{migrate::MigrateDatabase, Pool};
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::db::Noose;
 | 
				
			||||||
 | 
					use crate::bussy::{channels, Command, Message, PubSub};
 | 
				
			||||||
 | 
					use crate::utils::{error::Error, structs::Subscription};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum EventsTable {
 | 
				
			||||||
 | 
					    Table,
 | 
				
			||||||
 | 
					    EventId,
 | 
				
			||||||
 | 
					    Kind,
 | 
				
			||||||
 | 
					    Pubkey,
 | 
				
			||||||
 | 
					    Content,
 | 
				
			||||||
 | 
					    CreatedAt,
 | 
				
			||||||
 | 
					    Tags,
 | 
				
			||||||
 | 
					    Sig,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl sea_query::Iden for EventsTable {
 | 
				
			||||||
 | 
					    fn unquoted(&self, s: &mut dyn std::fmt::Write) {
 | 
				
			||||||
 | 
					        write!(
 | 
				
			||||||
 | 
					            s,
 | 
				
			||||||
 | 
					            "{}",
 | 
				
			||||||
 | 
					            match self {
 | 
				
			||||||
 | 
					                Self::Table => "events",
 | 
				
			||||||
 | 
					                Self::EventId => "id",
 | 
				
			||||||
 | 
					                Self::Kind => "kind",
 | 
				
			||||||
 | 
					                Self::Pubkey => "pubkey",
 | 
				
			||||||
 | 
					                Self::Content => "content",
 | 
				
			||||||
 | 
					                Self::CreatedAt => "created_at",
 | 
				
			||||||
 | 
					                Self::Tags => "tags",
 | 
				
			||||||
 | 
					                Self::Sig => "sig",
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum EventsFTSTable {
 | 
				
			||||||
 | 
					    Table,
 | 
				
			||||||
 | 
					    EventId,
 | 
				
			||||||
 | 
					    Content,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl sea_query::Iden for EventsFTSTable {
 | 
				
			||||||
 | 
					    fn unquoted(&self, s: &mut dyn std::fmt::Write) {
 | 
				
			||||||
 | 
					        write!(
 | 
				
			||||||
 | 
					            s,
 | 
				
			||||||
 | 
					            "{}",
 | 
				
			||||||
 | 
					            match self {
 | 
				
			||||||
 | 
					                Self::Table => "events_fts",
 | 
				
			||||||
 | 
					                Self::EventId => "id",
 | 
				
			||||||
 | 
					                Self::Content => "content",
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum TagsTable {
 | 
				
			||||||
 | 
					    Table,
 | 
				
			||||||
 | 
					    Tag,
 | 
				
			||||||
 | 
					    Value,
 | 
				
			||||||
 | 
					    EventId,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl sea_query::Iden for TagsTable {
 | 
				
			||||||
 | 
					    fn unquoted(&self, s: &mut dyn std::fmt::Write) {
 | 
				
			||||||
 | 
					        write!(
 | 
				
			||||||
 | 
					            s,
 | 
				
			||||||
 | 
					            "{}",
 | 
				
			||||||
 | 
					            match self {
 | 
				
			||||||
 | 
					                Self::Table => "tags",
 | 
				
			||||||
 | 
					                Self::Tag => "tag",
 | 
				
			||||||
 | 
					                Self::Value => "value",
 | 
				
			||||||
 | 
					                Self::EventId => "event_id",
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(FromRow, Debug)]
 | 
				
			||||||
 | 
					struct EventsCountRow(i32);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(FromRow, Debug)]
 | 
				
			||||||
 | 
					struct EventRow {
 | 
				
			||||||
 | 
					    id: String,
 | 
				
			||||||
 | 
					    pubkey: String,
 | 
				
			||||||
 | 
					    created_at: i64,
 | 
				
			||||||
 | 
					    kind: i64,
 | 
				
			||||||
 | 
					    tags: String,
 | 
				
			||||||
 | 
					    sig: String,
 | 
				
			||||||
 | 
					    content: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl EventRow {
 | 
				
			||||||
 | 
					    pub fn to_string(&self, subscription_id: nostr::SubscriptionId) -> String {
 | 
				
			||||||
 | 
					        let tags: Vec<Vec<String>> = serde_json::from_str(&self.tags).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let message = serde_json::json!([
 | 
				
			||||||
 | 
					            "EVENT",
 | 
				
			||||||
 | 
					            subscription_id,
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "id": self.id,
 | 
				
			||||||
 | 
					                "content": self.content,
 | 
				
			||||||
 | 
					                "created_at": self.created_at,
 | 
				
			||||||
 | 
					                "kind": self.kind,
 | 
				
			||||||
 | 
					                "pubkey": self.pubkey,
 | 
				
			||||||
 | 
					                "sig": self.sig,
 | 
				
			||||||
 | 
					                "tags": tags
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        message.to_string()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct SqliteDb {
 | 
				
			||||||
 | 
					    pool: Pool<Sqlite>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl SqliteDb {
 | 
				
			||||||
 | 
					    pub async fn new() -> Self {
 | 
				
			||||||
 | 
					        let pool = SqliteDb::build_pool("noose_pool", 42).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Self { pool }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn info(&self) {
 | 
				
			||||||
 | 
					        dbg!(self.pool.options());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn migrate(pool: &Pool<Sqlite>) {
 | 
				
			||||||
 | 
					        sqlx::migrate!("src/noose/migrations")
 | 
				
			||||||
 | 
					            .run(pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn build_pool(name: &str, max_size: u32) -> Pool<Sqlite> {
 | 
				
			||||||
 | 
					        let pool_options = SqlitePoolOptions::new()
 | 
				
			||||||
 | 
					            .test_before_acquire(true)
 | 
				
			||||||
 | 
					            // .idle_timeout(Some(Duration::from_secs(10)))
 | 
				
			||||||
 | 
					            // .max_lifetime(Some(Duration::from_secs(30)))
 | 
				
			||||||
 | 
					            .max_lifetime(None)
 | 
				
			||||||
 | 
					            .idle_timeout(None)
 | 
				
			||||||
 | 
					            .max_connections(max_size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let db_url = "sqlite://sqlite.db";
 | 
				
			||||||
 | 
					        if !Sqlite::database_exists(db_url).await.unwrap_or(false) {
 | 
				
			||||||
 | 
					            log::info!("Creating database {}", db_url);
 | 
				
			||||||
 | 
					            match Sqlite::create_database(db_url).await {
 | 
				
			||||||
 | 
					                Ok(_) => log::info!("Db {} created", db_url),
 | 
				
			||||||
 | 
					                Err(_) => panic!("Failed to create database {}", db_url),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Ok(pool) = pool_options.connect(db_url).await {
 | 
				
			||||||
 | 
					            log::info!("Connected to sqlite pool {}", name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            pool
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            panic!("Connection to sqlite pool {} failed", name);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn add_event(&self, event: Box<Event>) -> Result<String, Error> {
 | 
				
			||||||
 | 
					        let id = event.id.to_string();
 | 
				
			||||||
 | 
					        let kind = event.kind.to_string();
 | 
				
			||||||
 | 
					        let pubkey = event.pubkey.to_string();
 | 
				
			||||||
 | 
					        let content = event.content.to_string();
 | 
				
			||||||
 | 
					        let created_at = event.created_at.as_i64();
 | 
				
			||||||
 | 
					        let tags = serde_json::to_string(&event.tags).unwrap();
 | 
				
			||||||
 | 
					        let sig = event.sig.to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let message = format!("[\"OK\", \"{}\", true, \"\"]", id.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if event.is_ephemeral() {
 | 
				
			||||||
 | 
					            return Ok(message);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let tx = self.pool.begin().await.unwrap();
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if event.is_replaceable() {
 | 
				
			||||||
 | 
					                dbg!("new event is replaceable - searching for previously stored event");
 | 
				
			||||||
 | 
					                let (sql, values) = Query::select()
 | 
				
			||||||
 | 
					                    .from(EventsTable::Table)
 | 
				
			||||||
 | 
					                    .columns([EventsTable::EventId])
 | 
				
			||||||
 | 
					                    .and_where(
 | 
				
			||||||
 | 
					                        sea_query::Expr::col(EventsTable::Pubkey).eq(event.pubkey.to_string()),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .and_where(sea_query::Expr::col(EventsTable::Kind).eq(event.kind.as_u32()))
 | 
				
			||||||
 | 
					                    .and_where(
 | 
				
			||||||
 | 
					                        sea_query::Expr::col(EventsTable::CreatedAt).gte(event.created_at.as_i64()),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .limit(1)
 | 
				
			||||||
 | 
					                    .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let repl_count = sqlx::query_with(&sql, values).fetch_one(&self.pool).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if repl_count.ok().is_some() {
 | 
				
			||||||
 | 
					                    return Ok(message);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if event.is_parameterized_replaceable() {
 | 
				
			||||||
 | 
					                dbg!(
 | 
				
			||||||
 | 
					                    "new event is parametrized replaceable - searching for previously stored event"
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                let d_tags: Vec<String> = event
 | 
				
			||||||
 | 
					                    .tags
 | 
				
			||||||
 | 
					                    .iter()
 | 
				
			||||||
 | 
					                    .filter(|tag| tag.kind() == nostr::TagKind::D)
 | 
				
			||||||
 | 
					                    .map(|tag| tag.clone().to_vec()[1].clone())
 | 
				
			||||||
 | 
					                    .collect();
 | 
				
			||||||
 | 
					                let (sql, values) = Query::select()
 | 
				
			||||||
 | 
					                    .from(EventsTable::Table)
 | 
				
			||||||
 | 
					                    .column((EventsTable::Table, EventsTable::EventId))
 | 
				
			||||||
 | 
					                    .left_join(
 | 
				
			||||||
 | 
					                        TagsTable::Table,
 | 
				
			||||||
 | 
					                        sea_query::Expr::col((TagsTable::Table, TagsTable::EventId))
 | 
				
			||||||
 | 
					                            .equals((EventsTable::Table, EventsTable::EventId)),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .and_where(
 | 
				
			||||||
 | 
					                        sea_query::Expr::col((EventsTable::Table, EventsTable::Pubkey))
 | 
				
			||||||
 | 
					                            .eq(event.pubkey.to_string()),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .and_where(
 | 
				
			||||||
 | 
					                        sea_query::Expr::col((EventsTable::Table, EventsTable::Kind))
 | 
				
			||||||
 | 
					                            .eq(event.kind.as_u32()),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .and_where(sea_query::Expr::col((TagsTable::Table, TagsTable::Tag)).eq("d"))
 | 
				
			||||||
 | 
					                    .and_where(
 | 
				
			||||||
 | 
					                        sea_query::Expr::col((TagsTable::Table, TagsTable::Value))
 | 
				
			||||||
 | 
					                            .eq(d_tags[0].to_string()),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .and_where(
 | 
				
			||||||
 | 
					                        sea_query::Expr::col((EventsTable::Table, EventsTable::CreatedAt))
 | 
				
			||||||
 | 
					                            .gte(event.created_at.as_i64()),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .limit(1)
 | 
				
			||||||
 | 
					                    .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let repl_count = sqlx::query_with(&sql, values).fetch_one(&self.pool).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if repl_count.ok().is_some() {
 | 
				
			||||||
 | 
					                    return Ok(message);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Insert replaceble event
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if event.is_replaceable() {
 | 
				
			||||||
 | 
					                dbg!("deleting older replaceable event from events table");
 | 
				
			||||||
 | 
					                let (sql, values) = Query::delete()
 | 
				
			||||||
 | 
					                    .from_table(EventsTable::Table)
 | 
				
			||||||
 | 
					                    .and_where(
 | 
				
			||||||
 | 
					                        sea_query::Expr::col((EventsTable::Table, EventsTable::Kind))
 | 
				
			||||||
 | 
					                            .eq(event.kind.as_u32()),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .and_where(
 | 
				
			||||||
 | 
					                        sea_query::Expr::col((EventsTable::Table, EventsTable::Pubkey))
 | 
				
			||||||
 | 
					                            .eq(event.pubkey.to_string()),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .and_where(
 | 
				
			||||||
 | 
					                        sea_query::Expr::col((EventsTable::Table, EventsTable::EventId))
 | 
				
			||||||
 | 
					                            .not_in_subquery(
 | 
				
			||||||
 | 
					                                Query::select()
 | 
				
			||||||
 | 
					                                    .from(EventsTable::Table)
 | 
				
			||||||
 | 
					                                    .column(EventsTable::EventId)
 | 
				
			||||||
 | 
					                                    .and_where(
 | 
				
			||||||
 | 
					                                        sea_query::Expr::col(EventsTable::Kind)
 | 
				
			||||||
 | 
					                                            .eq(event.kind.as_u32()),
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    .and_where(
 | 
				
			||||||
 | 
					                                        sea_query::Expr::col(EventsTable::Pubkey)
 | 
				
			||||||
 | 
					                                            .eq(event.pubkey.to_string()),
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    .order_by(EventsTable::CreatedAt, sea_query::Order::Desc)
 | 
				
			||||||
 | 
					                                    .limit(1)
 | 
				
			||||||
 | 
					                                    .to_owned(),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let results = sqlx::query_with(&sql, values)
 | 
				
			||||||
 | 
					                    .execute(&self.pool)
 | 
				
			||||||
 | 
					                    .await
 | 
				
			||||||
 | 
					                    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if results.rows_affected() > 0 {
 | 
				
			||||||
 | 
					                    log::info!(
 | 
				
			||||||
 | 
					                        "removed {} older replaceable kind {} events for author: {:?}",
 | 
				
			||||||
 | 
					                        results.rows_affected(),
 | 
				
			||||||
 | 
					                        event.kind.as_u32(),
 | 
				
			||||||
 | 
					                        event.pubkey.to_string()
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Insert parametrized replaceble event
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if event.is_parameterized_replaceable() {
 | 
				
			||||||
 | 
					                dbg!("deleting older parametrized replaceable event from events table");
 | 
				
			||||||
 | 
					                let d_tag = event.identifier();
 | 
				
			||||||
 | 
					                let (sql, values) = Query::delete()
 | 
				
			||||||
 | 
					                    .from_table(EventsTable::Table)
 | 
				
			||||||
 | 
					                    .and_where(
 | 
				
			||||||
 | 
					                        sea_query::Expr::col((EventsTable::Table, EventsTable::EventId))
 | 
				
			||||||
 | 
					                            .in_subquery(
 | 
				
			||||||
 | 
					                                Query::select()
 | 
				
			||||||
 | 
					                                    .from(EventsTable::Table)
 | 
				
			||||||
 | 
					                                    .column((EventsTable::Table, EventsTable::EventId))
 | 
				
			||||||
 | 
					                                    .left_join(
 | 
				
			||||||
 | 
					                                        TagsTable::Table,
 | 
				
			||||||
 | 
					                                        sea_query::Expr::col((
 | 
				
			||||||
 | 
					                                            TagsTable::Table,
 | 
				
			||||||
 | 
					                                            TagsTable::EventId,
 | 
				
			||||||
 | 
					                                        ))
 | 
				
			||||||
 | 
					                                        .equals((EventsTable::Table, EventsTable::EventId)),
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    .and_where(
 | 
				
			||||||
 | 
					                                        sea_query::Expr::col((
 | 
				
			||||||
 | 
					                                            EventsTable::Table,
 | 
				
			||||||
 | 
					                                            EventsTable::Kind,
 | 
				
			||||||
 | 
					                                        ))
 | 
				
			||||||
 | 
					                                        .eq(event.kind.as_u32()),
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    .and_where(
 | 
				
			||||||
 | 
					                                        sea_query::Expr::col((
 | 
				
			||||||
 | 
					                                            EventsTable::Table,
 | 
				
			||||||
 | 
					                                            EventsTable::Pubkey,
 | 
				
			||||||
 | 
					                                        ))
 | 
				
			||||||
 | 
					                                        .eq(event.pubkey.to_string()),
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    .and_where(
 | 
				
			||||||
 | 
					                                        sea_query::Expr::col((TagsTable::Table, TagsTable::Tag))
 | 
				
			||||||
 | 
					                                            .eq("d"),
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    .and_where(
 | 
				
			||||||
 | 
					                                        sea_query::Expr::col((TagsTable::Table, TagsTable::Value))
 | 
				
			||||||
 | 
					                                            .eq(d_tag),
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    .to_owned(),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let results = sqlx::query_with(&sql, values)
 | 
				
			||||||
 | 
					                    .execute(&self.pool)
 | 
				
			||||||
 | 
					                    .await
 | 
				
			||||||
 | 
					                    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if results.rows_affected() > 0 {
 | 
				
			||||||
 | 
					                    log::info!("removed {} older parameterized replaceable kind {} events for author: {:?}", results.rows_affected(), event.kind, event.pubkey);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Process deletion events
 | 
				
			||||||
 | 
					        dbg!(event.as_json());
 | 
				
			||||||
 | 
					        if event.kind.as_u32() == 5 {
 | 
				
			||||||
 | 
					            dbg!("deleting event");
 | 
				
			||||||
 | 
					            let ids: Vec<String> = event.event_ids().map(|eid| eid.to_string()).collect();
 | 
				
			||||||
 | 
					            let (sql, values) = Query::delete()
 | 
				
			||||||
 | 
					                .from_table(EventsTable::Table)
 | 
				
			||||||
 | 
					                .and_where(sea_query::Expr::col(EventsTable::Kind).ne(5))
 | 
				
			||||||
 | 
					                .and_where(sea_query::Expr::col(EventsTable::Pubkey).eq(event.pubkey.to_string()))
 | 
				
			||||||
 | 
					                .and_where(sea_query::Expr::col(EventsTable::EventId).is_in(&ids))
 | 
				
			||||||
 | 
					                .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let results = sqlx::query_with(&sql, values)
 | 
				
			||||||
 | 
					                .execute(&self.pool)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if results.rows_affected() > 0 {
 | 
				
			||||||
 | 
					                log::info!(
 | 
				
			||||||
 | 
					                    "removed {} events for author {:?}",
 | 
				
			||||||
 | 
					                    results.rows_affected(),
 | 
				
			||||||
 | 
					                    event.pubkey
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Delete from EventsFTS
 | 
				
			||||||
 | 
					            let (sql, values) = Query::delete()
 | 
				
			||||||
 | 
					                .from_table(EventsFTSTable::Table)
 | 
				
			||||||
 | 
					                .and_where(sea_query::Expr::col(EventsFTSTable::EventId).is_in(&ids))
 | 
				
			||||||
 | 
					                .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					            let _ = sqlx::query_with(&sql, values)
 | 
				
			||||||
 | 
					                .execute(&self.pool)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            dbg!("inserting new event in events");
 | 
				
			||||||
 | 
					            // Insert into Events table
 | 
				
			||||||
 | 
					            let (sql, values) = Query::insert()
 | 
				
			||||||
 | 
					                .into_table(EventsTable::Table)
 | 
				
			||||||
 | 
					                .columns([
 | 
				
			||||||
 | 
					                    EventsTable::EventId,
 | 
				
			||||||
 | 
					                    EventsTable::Content,
 | 
				
			||||||
 | 
					                    EventsTable::Kind,
 | 
				
			||||||
 | 
					                    EventsTable::Pubkey,
 | 
				
			||||||
 | 
					                    EventsTable::CreatedAt,
 | 
				
			||||||
 | 
					                    EventsTable::Tags,
 | 
				
			||||||
 | 
					                    EventsTable::Sig,
 | 
				
			||||||
 | 
					                ])
 | 
				
			||||||
 | 
					                .values_panic([
 | 
				
			||||||
 | 
					                    id.clone().into(),
 | 
				
			||||||
 | 
					                    content.clone().into(),
 | 
				
			||||||
 | 
					                    kind.into(),
 | 
				
			||||||
 | 
					                    pubkey.into(),
 | 
				
			||||||
 | 
					                    created_at.into(),
 | 
				
			||||||
 | 
					                    tags.into(),
 | 
				
			||||||
 | 
					                    sig.into(),
 | 
				
			||||||
 | 
					                ])
 | 
				
			||||||
 | 
					                .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let results = sqlx::query_with(&sql, values)
 | 
				
			||||||
 | 
					                .execute(&self.pool)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Insert into EventsFTS table
 | 
				
			||||||
 | 
					            dbg!("inserting new event into eventsFTS");
 | 
				
			||||||
 | 
					            let (sql, values) = Query::insert()
 | 
				
			||||||
 | 
					                .into_table(EventsFTSTable::Table)
 | 
				
			||||||
 | 
					                .columns([EventsFTSTable::EventId, EventsFTSTable::Content])
 | 
				
			||||||
 | 
					                .values_panic([id.clone().into(), content.into()])
 | 
				
			||||||
 | 
					                .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let results = sqlx::query_with(&sql, values)
 | 
				
			||||||
 | 
					                .execute(&self.pool)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Insert into Tags table
 | 
				
			||||||
 | 
					            dbg!("inserting new event into tags");
 | 
				
			||||||
 | 
					            for tag in event.tags.clone() {
 | 
				
			||||||
 | 
					                let tag = tag.to_vec();
 | 
				
			||||||
 | 
					                if tag.len() >= 2 {
 | 
				
			||||||
 | 
					                    let tag_name = &tag[0];
 | 
				
			||||||
 | 
					                    let tag_value = &tag[1];
 | 
				
			||||||
 | 
					                    if tag_name.len() == 1 {
 | 
				
			||||||
 | 
					                        let (sql, values) = Query::insert()
 | 
				
			||||||
 | 
					                            .into_table(TagsTable::Table)
 | 
				
			||||||
 | 
					                            .columns([TagsTable::Tag, TagsTable::Value, TagsTable::EventId])
 | 
				
			||||||
 | 
					                            .values_panic([tag_name.into(), tag_value.into(), id.clone().into()])
 | 
				
			||||||
 | 
					                            .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        let results = sqlx::query_with(&sql, values)
 | 
				
			||||||
 | 
					                            .execute(&self.pool)
 | 
				
			||||||
 | 
					                            .await
 | 
				
			||||||
 | 
					                            .unwrap();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        tx.commit().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(message)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn index_search(&self, event: Box<Event>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let id = event.id.to_string();
 | 
				
			||||||
 | 
					        let content = event.content.to_string();
 | 
				
			||||||
 | 
					        let (sql, values) = Query::insert()
 | 
				
			||||||
 | 
					            .into_table(EventsFTSTable::Table)
 | 
				
			||||||
 | 
					            .columns([EventsFTSTable::EventId, EventsFTSTable::Content])
 | 
				
			||||||
 | 
					            .values_panic([id.into(), content.into()])
 | 
				
			||||||
 | 
					            .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let results = sqlx::query_with(&sql, values).execute(&self.pool).await;
 | 
				
			||||||
 | 
					        if results.is_ok() {
 | 
				
			||||||
 | 
					            Ok(())
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Err(Error::internal_with_message(
 | 
				
			||||||
 | 
					                "Unable to write event to events_fts index",
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn index_tags(&self, event: Box<Event>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        // let t: Vec<String> = Vec::new();
 | 
				
			||||||
 | 
					        // for tag in event.tags {
 | 
				
			||||||
 | 
					        //     tag.kind()
 | 
				
			||||||
 | 
					        // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_filter_query(&self, filter: &nostr::Filter) -> sea_query::SelectStatement {
 | 
				
			||||||
 | 
					        let mut query = Query::select()
 | 
				
			||||||
 | 
					            .column((EventsTable::Table, EventsTable::EventId))
 | 
				
			||||||
 | 
					            .column((EventsTable::Table, EventsTable::Content))
 | 
				
			||||||
 | 
					            .columns([
 | 
				
			||||||
 | 
					                EventsTable::Kind,
 | 
				
			||||||
 | 
					                EventsTable::Pubkey,
 | 
				
			||||||
 | 
					                EventsTable::CreatedAt,
 | 
				
			||||||
 | 
					                EventsTable::Tags,
 | 
				
			||||||
 | 
					                EventsTable::Sig,
 | 
				
			||||||
 | 
					            ])
 | 
				
			||||||
 | 
					            .from(EventsTable::Table)
 | 
				
			||||||
 | 
					            .order_by(EventsTable::CreatedAt, sea_query::Order::Desc)
 | 
				
			||||||
 | 
					            .to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if !filter.ids.is_empty() {
 | 
				
			||||||
 | 
					            let ids = filter.ids.iter().map(|id| id.to_string());
 | 
				
			||||||
 | 
					            query = query
 | 
				
			||||||
 | 
					                .and_where(
 | 
				
			||||||
 | 
					                    sea_query::Expr::col((EventsTable::Table, EventsTable::EventId)).is_in(ids),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .to_owned();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if !filter.kinds.is_empty() {
 | 
				
			||||||
 | 
					            let kinds: Vec<u32> = filter.kinds.iter().map(|kind| kind.as_u32()).collect();
 | 
				
			||||||
 | 
					            query = query
 | 
				
			||||||
 | 
					                .and_where(
 | 
				
			||||||
 | 
					                    sea_query::Expr::col((EventsTable::Table, EventsTable::Kind)).is_in(kinds),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .to_owned();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if !filter.authors.is_empty() {
 | 
				
			||||||
 | 
					            let authors = filter.authors.iter().map(|author| author.to_string());
 | 
				
			||||||
 | 
					            query = query
 | 
				
			||||||
 | 
					                .and_where(
 | 
				
			||||||
 | 
					                    sea_query::Expr::col((EventsTable::Table, EventsTable::Pubkey)).is_in(authors),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .to_owned();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(since) = filter.since {
 | 
				
			||||||
 | 
					            query = query
 | 
				
			||||||
 | 
					                .and_where(
 | 
				
			||||||
 | 
					                    sea_query::Expr::col((EventsTable::Table, EventsTable::CreatedAt))
 | 
				
			||||||
 | 
					                        .gte(since.as_u64()),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .to_owned();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(until) = filter.until {
 | 
				
			||||||
 | 
					            query = query
 | 
				
			||||||
 | 
					                .and_where(
 | 
				
			||||||
 | 
					                    sea_query::Expr::col((EventsTable::Table, EventsTable::CreatedAt))
 | 
				
			||||||
 | 
					                        .lte(until.as_u64()),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .to_owned();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(limit) = filter.limit {
 | 
				
			||||||
 | 
					            query = query.limit(limit as u64).to_owned();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        filter.generic_tags.iter().for_each(|(tag, values)| {
 | 
				
			||||||
 | 
					            let values = values.iter().map(|val| val.to_string());
 | 
				
			||||||
 | 
					            query = query
 | 
				
			||||||
 | 
					                .left_join(
 | 
				
			||||||
 | 
					                    TagsTable::Table,
 | 
				
			||||||
 | 
					                    sea_query::Expr::col((TagsTable::Table, TagsTable::EventId))
 | 
				
			||||||
 | 
					                        .equals((EventsTable::Table, EventsTable::EventId)),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .and_where(
 | 
				
			||||||
 | 
					                    sea_query::Expr::col((TagsTable::Table, TagsTable::Tag)).eq(tag.to_string()),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .and_where(sea_query::Expr::col((TagsTable::Table, TagsTable::Value)).is_in(values))
 | 
				
			||||||
 | 
					                .to_owned();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(search) = &filter.search {
 | 
				
			||||||
 | 
					            query = query
 | 
				
			||||||
 | 
					                .inner_join(
 | 
				
			||||||
 | 
					                    EventsFTSTable::Table,
 | 
				
			||||||
 | 
					                    sea_query::Expr::col((EventsTable::Table, EventsTable::EventId))
 | 
				
			||||||
 | 
					                        .equals((EventsFTSTable::Table, EventsFTSTable::EventId)),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .and_where(
 | 
				
			||||||
 | 
					                    sea_query::Expr::col((EventsFTSTable::Table, EventsFTSTable::Content))
 | 
				
			||||||
 | 
					                        .matches(search),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .to_owned();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        query
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_filters_query(&self, subscription: Subscription) -> Option<sea_query::SelectStatement> {
 | 
				
			||||||
 | 
					        subscription
 | 
				
			||||||
 | 
					            .filters
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .map(|filter| {
 | 
				
			||||||
 | 
					                Query::select()
 | 
				
			||||||
 | 
					                    .column((EventsTable::Table, EventsTable::EventId))
 | 
				
			||||||
 | 
					                    .column((EventsTable::Table, EventsTable::Content))
 | 
				
			||||||
 | 
					                    .columns([
 | 
				
			||||||
 | 
					                        EventsTable::Kind,
 | 
				
			||||||
 | 
					                        EventsTable::Pubkey,
 | 
				
			||||||
 | 
					                        EventsTable::CreatedAt,
 | 
				
			||||||
 | 
					                        EventsTable::Tags,
 | 
				
			||||||
 | 
					                        EventsTable::Sig,
 | 
				
			||||||
 | 
					                    ])
 | 
				
			||||||
 | 
					                    .from_subquery(
 | 
				
			||||||
 | 
					                        self.get_filter_query(filter),
 | 
				
			||||||
 | 
					                        sea_query::Alias::new("events"),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .to_owned()
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .reduce(|mut result, query| result.union(sea_query::UnionType::All, query).to_owned())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn delete_filters(&self, subscription: Subscription) -> Vec<EventRow> {
 | 
				
			||||||
 | 
					        todo!()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn count_events_by_filters(&self, subscription: Subscription) -> i32 {
 | 
				
			||||||
 | 
					        if subscription.filters.is_empty() {
 | 
				
			||||||
 | 
					            return 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let (sql, values) = self
 | 
				
			||||||
 | 
					            .get_filters_query(subscription)
 | 
				
			||||||
 | 
					            .unwrap()
 | 
				
			||||||
 | 
					            .clear_selects()
 | 
				
			||||||
 | 
					            .expr_as(
 | 
				
			||||||
 | 
					                sea_query::Func::count(sea_query::Expr::col((
 | 
				
			||||||
 | 
					                    EventsTable::Table,
 | 
				
			||||||
 | 
					                    EventsTable::EventId,
 | 
				
			||||||
 | 
					                ))),
 | 
				
			||||||
 | 
					                sea_query::Alias::new("count"),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        println!("count_filters SEA_QUERY built SQL: {}", sql.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let counts = sqlx::query_as_with::<_, EventsCountRow, _>(&sql, values)
 | 
				
			||||||
 | 
					            .fetch_one(&self.pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dbg!(counts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        1
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl Noose for SqliteDb {
 | 
				
			||||||
 | 
					    async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let mut subscriber = pubsub.subscribe(channels::MSG_NOOSE).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while let Ok(message) = subscriber.recv().await {
 | 
				
			||||||
 | 
					            log::info!("[Noose] received message: {:?}", message);
 | 
				
			||||||
 | 
					            let command = match message.content {
 | 
				
			||||||
 | 
					                Command::DbReqWriteEvent(event) => match self.write_event(event).await {
 | 
				
			||||||
 | 
					                    Ok(status) => Command::DbResOkWithStatus(status),
 | 
				
			||||||
 | 
					                    Err(e) => Command::ServiceError(e),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                Command::DbReqFindEvent(client_id, subscriptioin) => {
 | 
				
			||||||
 | 
					                    match self.find_event(subscriptioin).await {
 | 
				
			||||||
 | 
					                        Ok(events) => Command::DbResRelayMessage(client_id, events),
 | 
				
			||||||
 | 
					                        Err(e) => Command::ServiceError(e),
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                _ => Command::Noop,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            if command != Command::Noop {
 | 
				
			||||||
 | 
					                let channel = message.source;
 | 
				
			||||||
 | 
					                let message = Message {
 | 
				
			||||||
 | 
					                    source: channels::MSG_NOOSE,
 | 
				
			||||||
 | 
					                    content: command,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                log::debug!("[Noose] publishing new message: {:?}", message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                pubsub.publish(channel, message).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn migration_up(&self) {
 | 
				
			||||||
 | 
					        SqliteDb::migrate(&self.pool).await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn write_event(&self, event: Box<Event>) -> Result<String, Error> {
 | 
				
			||||||
 | 
					        log::debug!("[Noose] write_event triggered");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let status = self.add_event(event).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Ok(status);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn find_event(&self, subscription: Subscription) -> Result<Vec<String>, Error> {
 | 
				
			||||||
 | 
					        log::debug!("making query from filters...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let eose_message =
 | 
				
			||||||
 | 
					            vec![nostr::RelayMessage::EndOfStoredEvents(subscription.id.clone()).as_json()];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(sql_statement) = self.get_filters_query(subscription.clone()) {
 | 
				
			||||||
 | 
					            let (sql, values) = sql_statement.build_sqlx(sea_query::SqliteQueryBuilder);
 | 
				
			||||||
 | 
					            log::info!("SEA_QUERY built SQL: {}", sql.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            match sqlx::query_as_with::<_, EventRow, _>(&sql, values)
 | 
				
			||||||
 | 
					                .fetch_all(&self.pool)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Ok(rows) => {
 | 
				
			||||||
 | 
					                    if rows.is_empty() {
 | 
				
			||||||
 | 
					                        return Ok(eose_message);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        let relay_messages: Vec<String> = rows
 | 
				
			||||||
 | 
					                            .iter()
 | 
				
			||||||
 | 
					                            .map(|row| row.to_string(subscription.id.clone()))
 | 
				
			||||||
 | 
					                            .collect();
 | 
				
			||||||
 | 
					                        return Ok(relay_messages);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    log::error!("{}", e);
 | 
				
			||||||
 | 
					                    return Err(Error::internal(e.into()));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Ok(eose_message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					    use super::Noose;
 | 
				
			||||||
 | 
					    use super::SqliteDb;
 | 
				
			||||||
 | 
					    use crate::utils::structs::Subscription;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use nostr::util::JsonUtil;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[tokio::test]
 | 
				
			||||||
 | 
					    async fn find_event() {
 | 
				
			||||||
 | 
					        let db = SqliteDb::new().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let t = std::time::Instant::now();
 | 
				
			||||||
 | 
					        let client_id = "test_id".to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let cm = nostr::ClientMessage::from_json(
 | 
				
			||||||
 | 
					            r#"["REQ","7b9bc4b6-701c-40b6-898f-4e7c6b5b1510",{"authors":["04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"],"kinds":[0]}]"#,
 | 
				
			||||||
 | 
					        ).unwrap();
 | 
				
			||||||
 | 
					        let (sub_id, filters) = match cm {
 | 
				
			||||||
 | 
					            nostr::ClientMessage::Req {
 | 
				
			||||||
 | 
					                subscription_id,
 | 
				
			||||||
 | 
					                filters,
 | 
				
			||||||
 | 
					            } => (subscription_id, filters),
 | 
				
			||||||
 | 
					            _ => panic!("sneed :("),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let sub = Subscription::new(sub_id, filters);
 | 
				
			||||||
 | 
					        db.find_event(sub).await.unwrap();
 | 
				
			||||||
 | 
					        println!(
 | 
				
			||||||
 | 
					            "Time passed: {}",
 | 
				
			||||||
 | 
					            (std::time::Instant::now() - t).as_millis()
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[tokio::test]
 | 
				
			||||||
 | 
					    async fn delete_events() {
 | 
				
			||||||
 | 
					        let db = SqliteDb::new().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let t = std::time::Instant::now();
 | 
				
			||||||
 | 
					        let client_id = "test_id".to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let my_keys = nostr::Keys::generate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let eid = nostr::EventId::all_zeros();
 | 
				
			||||||
 | 
					        let tag_event = nostr::Tag::Event {
 | 
				
			||||||
 | 
					            event_id: eid,
 | 
				
			||||||
 | 
					            relay_url: None,
 | 
				
			||||||
 | 
					            marker: None,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let tag_url = nostr::Tag::AbsoluteURL(nostr::types::UncheckedUrl::new(
 | 
				
			||||||
 | 
					            "http://foo.net".to_string(),
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					        let tag_hashtag = nostr::Tag::Hashtag("farm".to_string());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let event = nostr::EventBuilder::new_text_note(
 | 
				
			||||||
 | 
					            "sneed feed and seed",
 | 
				
			||||||
 | 
					            vec![tag_event, tag_url, tag_hashtag],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .to_event(&my_keys)
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dbg!(&event.as_json());
 | 
				
			||||||
 | 
					        let resp = db.add_event(Box::new(event.clone())).await.unwrap();
 | 
				
			||||||
 | 
					        dbg!(resp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let delete_event = nostr::EventBuilder::delete(vec![event.id])
 | 
				
			||||||
 | 
					            .to_event(&my_keys)
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dbg!(&delete_event);
 | 
				
			||||||
 | 
					        let resp = db.add_event(Box::new(delete_event.clone())).await.unwrap();
 | 
				
			||||||
 | 
					        dbg!(resp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // let sub_id = nostr::SubscriptionId::new("test".to_string());
 | 
				
			||||||
 | 
					        // let mut subscription = Subscription::new(sub_id, vec![]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // if delete_event.kind == nostr::Kind::EventDeletion {
 | 
				
			||||||
 | 
					        //     delete_event
 | 
				
			||||||
 | 
					        //         .tags
 | 
				
			||||||
 | 
					        //         .iter()
 | 
				
			||||||
 | 
					        //         .filter(|tag| {
 | 
				
			||||||
 | 
					        //             matches!(
 | 
				
			||||||
 | 
					        //                 tag,
 | 
				
			||||||
 | 
					        //                 nostr::Tag::Event {
 | 
				
			||||||
 | 
					        //                     event_id,
 | 
				
			||||||
 | 
					        //                     relay_url,
 | 
				
			||||||
 | 
					        //                     marker,
 | 
				
			||||||
 | 
					        //                 }
 | 
				
			||||||
 | 
					        //             )
 | 
				
			||||||
 | 
					        //         })
 | 
				
			||||||
 | 
					        //         .for_each(|tag| {
 | 
				
			||||||
 | 
					        //             if let nostr::Tag::Event {
 | 
				
			||||||
 | 
					        //                 event_id,
 | 
				
			||||||
 | 
					        //                 relay_url,
 | 
				
			||||||
 | 
					        //                 marker,
 | 
				
			||||||
 | 
					        //             } = tag
 | 
				
			||||||
 | 
					        //             {
 | 
				
			||||||
 | 
					        //                 let filter = nostr::Filter::new();
 | 
				
			||||||
 | 
					        //                 let filter = &filter.event(*event_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        //                 subscription.filters.push(filter.clone());
 | 
				
			||||||
 | 
					        //             }
 | 
				
			||||||
 | 
					        //         });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        //     dbg!(&subscription);
 | 
				
			||||||
 | 
					        // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // let res = db.delete_filters(subscription).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // dbg!(res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // let sub = Subscription::new(sub_id, filters);
 | 
				
			||||||
 | 
					        // let num = db.delete_filters(sub).await.len();
 | 
				
			||||||
 | 
					        // println!(
 | 
				
			||||||
 | 
					        //     "Time passed: {}",
 | 
				
			||||||
 | 
					        //     (std::time::Instant::now() - t).as_millis()
 | 
				
			||||||
 | 
					        // );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // assert_eq!(num, 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[tokio::test]
 | 
				
			||||||
 | 
					    async fn count_events() {
 | 
				
			||||||
 | 
					        let db = SqliteDb::new().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let t = std::time::Instant::now();
 | 
				
			||||||
 | 
					        let client_id = "test_id".to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let cm = nostr::ClientMessage::from_json(
 | 
				
			||||||
 | 
					            r#"["COUNT","7b9bc4b6-701c-40b6-898f-4e7c6b5b1510",{"authors":["6be3c1446231fe6d117d72e29b60094bbb3eec029100c34f627dc4ebe8369a64"],"kinds":[1]}]"#,
 | 
				
			||||||
 | 
					        ).unwrap();
 | 
				
			||||||
 | 
					        let (sub_id, filters) = match cm {
 | 
				
			||||||
 | 
					            nostr::ClientMessage::Count {
 | 
				
			||||||
 | 
					                subscription_id,
 | 
				
			||||||
 | 
					                filters,
 | 
				
			||||||
 | 
					            } => (subscription_id, filters),
 | 
				
			||||||
 | 
					            _ => panic!("sneed :("),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let sub = Subscription::new(sub_id, filters);
 | 
				
			||||||
 | 
					        let num = db.count_events_by_filters(sub).await;
 | 
				
			||||||
 | 
					        println!(
 | 
				
			||||||
 | 
					            "Time passed: {}",
 | 
				
			||||||
 | 
					            (std::time::Instant::now() - t).as_millis()
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(num, 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										43
									
								
								src/noose/user.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					use chrono::Utc;
 | 
				
			||||||
 | 
					use regex::Regex;
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use validator::{Validate, ValidationError};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Validate)]
 | 
				
			||||||
 | 
					pub struct UserRow {
 | 
				
			||||||
 | 
					    pub pubkey: String,
 | 
				
			||||||
 | 
					    pub username: String,
 | 
				
			||||||
 | 
					    inserted_at: i64,
 | 
				
			||||||
 | 
					    admin: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl UserRow {
 | 
				
			||||||
 | 
					    pub fn new(pubkey: String, username: String, admin: bool) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            pubkey,
 | 
				
			||||||
 | 
					            username,
 | 
				
			||||||
 | 
					            inserted_at: Utc::now().timestamp(),
 | 
				
			||||||
 | 
					            admin,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Validate)]
 | 
				
			||||||
 | 
					pub struct User {
 | 
				
			||||||
 | 
					    #[validate(custom = "validate_pubkey")]
 | 
				
			||||||
 | 
					    pub pubkey: Option<String>,
 | 
				
			||||||
 | 
					    #[validate(length(min = 1), regex = "VALID_CHARACTERS")]
 | 
				
			||||||
 | 
					    pub name: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn validate_pubkey(value: &str) -> Result<(), ValidationError> {
 | 
				
			||||||
 | 
					    use nostr::prelude::FromPkStr;
 | 
				
			||||||
 | 
					    match nostr::Keys::from_pk_str(value) {
 | 
				
			||||||
 | 
					        Ok(_) => Ok(()),
 | 
				
			||||||
 | 
					        Err(_) => Err(ValidationError::new("Unable to parse pubkey")),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/relay/handler.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					use crate::relay::ws;
 | 
				
			||||||
 | 
					use crate::utils::structs::Context;
 | 
				
			||||||
 | 
					use std::net::SocketAddr;
 | 
				
			||||||
 | 
					use warp::{Rejection, Reply};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn ws_handler(
 | 
				
			||||||
 | 
					    ws: warp::ws::Ws,
 | 
				
			||||||
 | 
					    context: Context,
 | 
				
			||||||
 | 
					    client_addr: Option<SocketAddr>,
 | 
				
			||||||
 | 
					) -> Result<impl Reply, Rejection> {
 | 
				
			||||||
 | 
					    Ok(ws.on_upgrade(move |socket| ws::client_connection(socket, context, client_addr)))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								src/relay/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					mod handler;
 | 
				
			||||||
 | 
					mod routes;
 | 
				
			||||||
 | 
					mod ws;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::utils::rejection_handler::handle_rejection;
 | 
				
			||||||
 | 
					use crate::utils::structs::Context;
 | 
				
			||||||
 | 
					use tokio::runtime;
 | 
				
			||||||
 | 
					use warp::Filter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn start(context: Context) {
 | 
				
			||||||
 | 
					    let rt = runtime::Runtime::new().unwrap();
 | 
				
			||||||
 | 
					    rt.block_on(async {
 | 
				
			||||||
 | 
					        log::info!("Starting Relay on wss://127.0.0.1:8080");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let routes = routes::routes(context).recover(handle_rejection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        warp::serve(routes)
 | 
				
			||||||
 | 
					            // .tls()
 | 
				
			||||||
 | 
					            // .cert(CERT)
 | 
				
			||||||
 | 
					            // .key(KEY)
 | 
				
			||||||
 | 
					            .run(([127, 0, 0, 1], 8080))
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										31
									
								
								src/relay/routes.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					use super::handler;
 | 
				
			||||||
 | 
					use crate::utils::filter::with_context;
 | 
				
			||||||
 | 
					use crate::utils::structs::Context;
 | 
				
			||||||
 | 
					use warp::{Filter, Rejection, Reply};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn routes(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
				
			||||||
 | 
					    let cors = warp::cors().allow_any_origin();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static_files().or(index(context)).with(cors)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn index(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
				
			||||||
 | 
					    let client_addr = warp::addr::remote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    warp::path::end()
 | 
				
			||||||
 | 
					        .and(warp::ws())
 | 
				
			||||||
 | 
					        .and(with_context(context))
 | 
				
			||||||
 | 
					        .and(client_addr)
 | 
				
			||||||
 | 
					        .and_then(handler::ws_handler)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn static_files() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
				
			||||||
 | 
					    let mut foo = std::env::current_exe().unwrap();
 | 
				
			||||||
 | 
					    foo.pop();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut www = foo.clone();
 | 
				
			||||||
 | 
					    www.pop();
 | 
				
			||||||
 | 
					    www.push(std::path::Path::new("www/static"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    warp::get().and(warp::fs::dir(www))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										264
									
								
								src/relay/ws.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,264 @@
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    bussy::channels,
 | 
				
			||||||
 | 
					    utils::structs::{Client, Context, Subscription},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_util::StreamExt;
 | 
				
			||||||
 | 
					use nostr::{ClientMessage, Event, Filter, JsonUtil, SubscriptionId};
 | 
				
			||||||
 | 
					use serde_json::from_str;
 | 
				
			||||||
 | 
					use std::net::SocketAddr;
 | 
				
			||||||
 | 
					use tokio::sync::mpsc;
 | 
				
			||||||
 | 
					use tokio_stream::wrappers::UnboundedReceiverStream;
 | 
				
			||||||
 | 
					use warp::ws::{Message, WebSocket};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_util::SinkExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn client_connection(ws: WebSocket, context: Context, client_addr: Option<SocketAddr>) {
 | 
				
			||||||
 | 
					    let (mut ws_sender, mut ws_receiver) = ws.split();
 | 
				
			||||||
 | 
					    let (client_sender, client_receiver) = mpsc::unbounded_channel();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut client_receiver = UnboundedReceiverStream::new(client_receiver);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Create and Add to the Context new Client and set its sender
 | 
				
			||||||
 | 
					    let ip = client_addr.unwrap().ip().to_string();
 | 
				
			||||||
 | 
					    let mut client = Client::new(ip);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    client.client_connection = Some(client_sender);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut subscriber = context.pubsub.subscribe(channels::MSG_RELAY).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    loop {
 | 
				
			||||||
 | 
					        tokio::select! {
 | 
				
			||||||
 | 
					            Ok(message) = subscriber.recv() => {
 | 
				
			||||||
 | 
					                match message.content {
 | 
				
			||||||
 | 
					                    crate::bussy::Command::PipelineResRelayMessageOk(client_id, relay_message) => {
 | 
				
			||||||
 | 
					                        if client.client_id == client_id {
 | 
				
			||||||
 | 
					                            if let Some(sender) = &client.client_connection {
 | 
				
			||||||
 | 
					                                if !sender.is_closed() {
 | 
				
			||||||
 | 
					                                    log::info!("[Relay] sending back the status of the processed event: {}", relay_message.as_json());
 | 
				
			||||||
 | 
					                                    sender.send(Ok(Message::text(relay_message.as_json()))).unwrap();
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    crate::bussy::Command::PipelineResStreamOutEvent(event) => {
 | 
				
			||||||
 | 
					                        // if client.client_id == client_id {
 | 
				
			||||||
 | 
					                            if let Some(sender) = &client.client_connection {
 | 
				
			||||||
 | 
					                                if let Some(relay_message) = get_relay_message(&client, event) {
 | 
				
			||||||
 | 
					                                    log::info!("[Relay] sending processed event to subscribed client: {}", relay_message.as_json());
 | 
				
			||||||
 | 
					                                    if !sender.is_closed() {sender.send(Ok(Message::text(relay_message.as_json()))).unwrap()};
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        // }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    crate::bussy::Command::DbResRelayMessage(client_id, events) => {
 | 
				
			||||||
 | 
					                        if client.client_id == client_id {
 | 
				
			||||||
 | 
					                        if let Some(sender) = &client.client_connection {
 | 
				
			||||||
 | 
					                            if !sender.is_closed() {
 | 
				
			||||||
 | 
					                                for event in events {
 | 
				
			||||||
 | 
					                                    sender.send(Ok(Message::text(event))).unwrap();
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    crate::bussy::Command::DbResOkWithStatus(status) => {
 | 
				
			||||||
 | 
					                        if let Some(sender) = &client.client_connection {
 | 
				
			||||||
 | 
					                            sender.send(Ok(Message::text(status))).unwrap();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    _ => ()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            Some(message) = client_receiver.next() => {
 | 
				
			||||||
 | 
					            match message {
 | 
				
			||||||
 | 
					                Ok(message) => {
 | 
				
			||||||
 | 
					                    // ws_sender
 | 
				
			||||||
 | 
					                    //     .send(message)
 | 
				
			||||||
 | 
					                    //     .unwrap_or_else(|e| {
 | 
				
			||||||
 | 
					                    //         log::error!("websocket send error: {}", e);
 | 
				
			||||||
 | 
					                    //     })
 | 
				
			||||||
 | 
					                    //     .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    match ws_sender.send(message).await {
 | 
				
			||||||
 | 
					                        Ok(_) => (),
 | 
				
			||||||
 | 
					                        Err(e) => {
 | 
				
			||||||
 | 
					                            log::error!("websocket send error: {}", e);
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    log::error!("websocket send error: {}", e);
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					            Some(result) = ws_receiver.next() => {
 | 
				
			||||||
 | 
					                let msg = match result {
 | 
				
			||||||
 | 
					                Ok(msg) => msg,
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    log::error!(
 | 
				
			||||||
 | 
					                        "error receiving ws message for id: {}: {}",
 | 
				
			||||||
 | 
					                        client.client_id.clone(),
 | 
				
			||||||
 | 
					                        e
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            socket_on_message(&context, &mut client, msg).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Handle proper disconnects
 | 
				
			||||||
 | 
					    socket_on_close(&client).await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Checking if client with id needs the event
 | 
				
			||||||
 | 
					fn get_relay_message(client: &Client, event: Box<Event>) -> Option<nostr::RelayMessage> {
 | 
				
			||||||
 | 
					    let mut id = &"".to_string();
 | 
				
			||||||
 | 
					    log::info!(
 | 
				
			||||||
 | 
					        "Checking if client with id {} needs the event",
 | 
				
			||||||
 | 
					        client.client_id
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if client.subscriptions.iter().any(|(sub_id, sub)| {
 | 
				
			||||||
 | 
					        if sub.interested_in_event(&event) {
 | 
				
			||||||
 | 
					            id = sub_id;
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        false
 | 
				
			||||||
 | 
					    }) {
 | 
				
			||||||
 | 
					        return Some(nostr::RelayMessage::Event {
 | 
				
			||||||
 | 
					            subscription_id: nostr::SubscriptionId::new(id),
 | 
				
			||||||
 | 
					            event,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    None
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn socket_on_close(client: &Client) {
 | 
				
			||||||
 | 
					    // clients.write().await.remove(id);
 | 
				
			||||||
 | 
					    log::info!("{} disconnected", client.client_id);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn socket_on_message(context: &Context, client: &mut Client, msg: Message) {
 | 
				
			||||||
 | 
					    let message = match msg.to_str() {
 | 
				
			||||||
 | 
					        Ok(raw_message) => raw_message,
 | 
				
			||||||
 | 
					        Err(_) => return,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if message == "ping" || message == "ping\n" {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let client_message: ClientMessage = match from_str(message) {
 | 
				
			||||||
 | 
					        Ok(parsed_message) => parsed_message,
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            log::error!("error while parsing client message request: {}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let response = nostr::RelayMessage::new_notice("Invalid message");
 | 
				
			||||||
 | 
					            let message = Message::text(response.as_json());
 | 
				
			||||||
 | 
					            send(client, message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log::info!(
 | 
				
			||||||
 | 
					        "[client {} - {}] message: {}",
 | 
				
			||||||
 | 
					        client.ip(),
 | 
				
			||||||
 | 
					        client.client_id,
 | 
				
			||||||
 | 
					        client_message.as_json()
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    handle_msg(context, client, client_message).await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn send(client: &Client, message: Message) {
 | 
				
			||||||
 | 
					    if let Some(sender) = &client.client_connection {
 | 
				
			||||||
 | 
					        if !sender.is_closed() {
 | 
				
			||||||
 | 
					            sender.send(Ok(message)).unwrap();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn handle_msg(context: &Context, client: &mut Client, client_message: ClientMessage) {
 | 
				
			||||||
 | 
					    match client_message {
 | 
				
			||||||
 | 
					        ClientMessage::Event(event) => handle_event(context, client, event).await,
 | 
				
			||||||
 | 
					        ClientMessage::Req {
 | 
				
			||||||
 | 
					            subscription_id,
 | 
				
			||||||
 | 
					            filters,
 | 
				
			||||||
 | 
					        } => handle_req(context, client, subscription_id, filters).await,
 | 
				
			||||||
 | 
					        ClientMessage::Count {
 | 
				
			||||||
 | 
					            subscription_id,
 | 
				
			||||||
 | 
					            filters,
 | 
				
			||||||
 | 
					        } => handle_count(client, subscription_id, filters).await,
 | 
				
			||||||
 | 
					        ClientMessage::Close(subscription_id) => handle_close(client, subscription_id).await,
 | 
				
			||||||
 | 
					        ClientMessage::Auth(event) => handle_auth(client, event).await,
 | 
				
			||||||
 | 
					        _ => (),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn handle_event(context: &Context, client: &Client, event: Box<Event>) {
 | 
				
			||||||
 | 
					    log::debug!("handle_event is processing new event");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context
 | 
				
			||||||
 | 
					        .pubsub
 | 
				
			||||||
 | 
					        .publish(
 | 
				
			||||||
 | 
					            channels::MSG_PIPELINE,
 | 
				
			||||||
 | 
					            crate::bussy::Message {
 | 
				
			||||||
 | 
					                source: channels::MSG_RELAY,
 | 
				
			||||||
 | 
					                content: crate::bussy::Command::PipelineReqEvent(client.client_id, event),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn handle_req(
 | 
				
			||||||
 | 
					    context: &Context,
 | 
				
			||||||
 | 
					    client: &mut Client,
 | 
				
			||||||
 | 
					    subscription_id: SubscriptionId,
 | 
				
			||||||
 | 
					    filters: Vec<Filter>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    let subscription = Subscription::new(subscription_id.clone(), filters);
 | 
				
			||||||
 | 
					    let needs_historical_events = subscription.needs_historical_events();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    client.subscribe(subscription.clone()).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if needs_historical_events {
 | 
				
			||||||
 | 
					        context
 | 
				
			||||||
 | 
					            .pubsub
 | 
				
			||||||
 | 
					            .publish(
 | 
				
			||||||
 | 
					                channels::MSG_NOOSE,
 | 
				
			||||||
 | 
					                crate::bussy::Message {
 | 
				
			||||||
 | 
					                    source: channels::MSG_RELAY,
 | 
				
			||||||
 | 
					                    content: crate::bussy::Command::DbReqFindEvent(client.client_id, subscription),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn handle_count(client: &Client, subscription_id: SubscriptionId, filters: Vec<Filter>) {
 | 
				
			||||||
 | 
					    // context.pubsub.send(new nostr event) then handle possible errors
 | 
				
			||||||
 | 
					    let subscription = Subscription::new(subscription_id, filters);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let message = Message::text("COUNT not implemented");
 | 
				
			||||||
 | 
					    send(client, message);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn handle_close(client: &mut Client, subscription_id: SubscriptionId) {
 | 
				
			||||||
 | 
					    // context.pubsub.send(new nostr event) then handle possible errors
 | 
				
			||||||
 | 
					    client.unsubscribe(subscription_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // let message = Message::text("CLOSE not implemented");
 | 
				
			||||||
 | 
					    // send(client, message);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn handle_auth(client: &Client, event: Box<Event>) {
 | 
				
			||||||
 | 
					    let message = Message::text("AUTH not implemented");
 | 
				
			||||||
 | 
					    send(client, message);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										62
									
								
								src/usernames/accounts.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,62 @@
 | 
				
			||||||
 | 
					use crate::noose::user::{User, UserRow};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::bussy::{channels, Command, Message};
 | 
				
			||||||
 | 
					use crate::usernames::util::InvalidParameter;
 | 
				
			||||||
 | 
					use crate::utils::error::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use warp::{Rejection, Reply};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::Context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn create_account(user: User, context: Context) -> Result<impl Reply, Rejection> {
 | 
				
			||||||
 | 
					    let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let pubkey = match user.pubkey {
 | 
				
			||||||
 | 
					        Some(pk) => {
 | 
				
			||||||
 | 
					            use nostr::prelude::FromPkStr;
 | 
				
			||||||
 | 
					            let keys = nostr::Keys::from_pk_str(&pk).unwrap();
 | 
				
			||||||
 | 
					            keys.public_key().to_string()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        None => {
 | 
				
			||||||
 | 
					            return Err(warp::reject::custom(Error::bad_request(
 | 
				
			||||||
 | 
					                "Pubkey is required",
 | 
				
			||||||
 | 
					            )))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let name = match user.name {
 | 
				
			||||||
 | 
					        Some(n) => n,
 | 
				
			||||||
 | 
					        None => return Err(warp::reject::custom(Error::bad_request("Name is required"))),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let user_row = UserRow::new(pubkey, name, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let command = Command::DbReqInsertUser(user_row);
 | 
				
			||||||
 | 
					    context
 | 
				
			||||||
 | 
					        .pubsub
 | 
				
			||||||
 | 
					        .publish(
 | 
				
			||||||
 | 
					            channels::MSG_NOOSE,
 | 
				
			||||||
 | 
					            Message {
 | 
				
			||||||
 | 
					                source: channels::MSG_NIP05,
 | 
				
			||||||
 | 
					                content: command,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Ok(result) = subscriber.recv().await {
 | 
				
			||||||
 | 
					        match result.content {
 | 
				
			||||||
 | 
					            Command::DbResOk => Ok(warp::reply::with_status(
 | 
				
			||||||
 | 
					                "ACCOUNT CREATED",
 | 
				
			||||||
 | 
					                warp::http::StatusCode::CREATED,
 | 
				
			||||||
 | 
					            )),
 | 
				
			||||||
 | 
					            Command::ServiceError(e) => Err(warp::reject::custom(e)),
 | 
				
			||||||
 | 
					            // _ => Err(warp::reject::custom(InvalidParameter)),
 | 
				
			||||||
 | 
					            _ => Ok(warp::reply::with_status(
 | 
				
			||||||
 | 
					                "something else",
 | 
				
			||||||
 | 
					                warp::http::StatusCode::INTERNAL_SERVER_ERROR,
 | 
				
			||||||
 | 
					            )),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Err(warp::reject::custom(InvalidParameter))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										65
									
								
								src/usernames/dto/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,65 @@
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::usernames::validators::validate_pubkey;
 | 
				
			||||||
 | 
					use crate::utils::error::Error;
 | 
				
			||||||
 | 
					use nostr::prelude::*;
 | 
				
			||||||
 | 
					use nostr::{key::XOnlyPublicKey, Keys};
 | 
				
			||||||
 | 
					use regex::Regex;
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use validator::Validate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Debug, Validate)]
 | 
				
			||||||
 | 
					pub struct UserBody {
 | 
				
			||||||
 | 
					    #[validate(length(min = 1), regex = "VALID_CHARACTERS")]
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    #[validate(custom(function = "validate_pubkey"))]
 | 
				
			||||||
 | 
					    pub pubkey: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl UserBody {
 | 
				
			||||||
 | 
					    pub fn get_pubkey(&self) -> XOnlyPublicKey {
 | 
				
			||||||
 | 
					        let keys = Keys::from_pk_str(&self.pubkey).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        keys.public_key()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct Nip05 {
 | 
				
			||||||
 | 
					    names: HashMap<String, String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
 | 
				
			||||||
 | 
					pub struct UserQuery {
 | 
				
			||||||
 | 
					    #[validate(length(min = 1))]
 | 
				
			||||||
 | 
					    pub user: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Debug, Clone, Validate)]
 | 
				
			||||||
 | 
					pub struct Account {
 | 
				
			||||||
 | 
					    #[validate(custom(function = "validate_pubkey"))]
 | 
				
			||||||
 | 
					    pub pubkey: String,
 | 
				
			||||||
 | 
					    #[validate(length(min = 1), regex = "VALID_CHARACTERS")]
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    #[validate(length(min = 1))]
 | 
				
			||||||
 | 
					    pub password: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Account {
 | 
				
			||||||
 | 
					    pub fn get_pubkey(&self) -> Result<XOnlyPublicKey, Error> {
 | 
				
			||||||
 | 
					        match nostr::Keys::from_pk_str(&self.pubkey) {
 | 
				
			||||||
 | 
					            Ok(pk) => Ok(pk.public_key()),
 | 
				
			||||||
 | 
					            Err(e) => Err(Error::invalid_param("pubkey", self.pubkey.clone())),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Debug, Clone, Validate)]
 | 
				
			||||||
 | 
					pub struct AccountPubkey {
 | 
				
			||||||
 | 
					    #[validate(custom(function = "validate_pubkey"))]
 | 
				
			||||||
 | 
					    pub pubkey: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/usernames/filter.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,27 @@
 | 
				
			||||||
 | 
					use crate::utils::error::Error;
 | 
				
			||||||
 | 
					use validator::Validate;
 | 
				
			||||||
 | 
					use warp::{Filter, Rejection};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn with_client_ip() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn with_user_body() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn validate_body_filter<T: serde::de::DeserializeOwned + Send + Validate + 'static>(
 | 
				
			||||||
 | 
					) -> impl Filter<Extract = (T,), Error = Rejection> + Copy {
 | 
				
			||||||
 | 
					    warp::body::json::<T>().and_then(|query: T| async move {
 | 
				
			||||||
 | 
					        match query.validate() {
 | 
				
			||||||
 | 
					            Ok(_) => Ok(query),
 | 
				
			||||||
 | 
					            Err(e) => Err(warp::reject::custom(Error::validation_error(e))),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn validate_query_filter<T: serde::de::DeserializeOwned + Send + Validate + 'static>(
 | 
				
			||||||
 | 
					) -> impl Filter<Extract = (T,), Error = Rejection> + Copy {
 | 
				
			||||||
 | 
					    warp::query::query::<T>().and_then(|query: T| async move {
 | 
				
			||||||
 | 
					        match query.validate() {
 | 
				
			||||||
 | 
					            Ok(_) => Ok(query),
 | 
				
			||||||
 | 
					            Err(e) => Err(warp::reject::custom(Error::validation_error(e))),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										89
									
								
								src/usernames/handler.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,89 @@
 | 
				
			||||||
 | 
					use crate::bussy::{channels, Command, Message};
 | 
				
			||||||
 | 
					use crate::noose::user::User;
 | 
				
			||||||
 | 
					use crate::usernames::dto::{Account, UserQuery};
 | 
				
			||||||
 | 
					use crate::utils::error::Error;
 | 
				
			||||||
 | 
					use crate::utils::structs::Context;
 | 
				
			||||||
 | 
					use serde_json::json;
 | 
				
			||||||
 | 
					use warp::{Rejection, Reply};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn get_account(
 | 
				
			||||||
 | 
					    // account: Result<AccountPubkey, Error>,
 | 
				
			||||||
 | 
					    account: Account,
 | 
				
			||||||
 | 
					    context: Context,
 | 
				
			||||||
 | 
					) -> Result<impl Reply, Rejection> {
 | 
				
			||||||
 | 
					    let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let command = Command::DbReqGetAccount(account.pubkey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context
 | 
				
			||||||
 | 
					        .pubsub
 | 
				
			||||||
 | 
					        .publish(
 | 
				
			||||||
 | 
					            channels::MSG_NOOSE,
 | 
				
			||||||
 | 
					            Message {
 | 
				
			||||||
 | 
					                source: channels::MSG_NIP05,
 | 
				
			||||||
 | 
					                content: command,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Ok(result) = subscriber.recv().await {
 | 
				
			||||||
 | 
					        match result.content {
 | 
				
			||||||
 | 
					            Command::DbResAccount => Ok(warp::reply::with_status(
 | 
				
			||||||
 | 
					                "ACCOUNT CREATED",
 | 
				
			||||||
 | 
					                warp::http::StatusCode::CREATED,
 | 
				
			||||||
 | 
					            )),
 | 
				
			||||||
 | 
					            Command::ServiceError(e) => Err(warp::reject::custom(e)),
 | 
				
			||||||
 | 
					            _ => Err(warp::reject::custom(Error::internal_with_message(
 | 
				
			||||||
 | 
					                "Unhandled message type",
 | 
				
			||||||
 | 
					            ))),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Err(warp::reject::custom(Error::internal_with_message(
 | 
				
			||||||
 | 
					            "Unhandeled message type",
 | 
				
			||||||
 | 
					        )))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Reply, Rejection> {
 | 
				
			||||||
 | 
					    let name = user_query.user;
 | 
				
			||||||
 | 
					    let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let user = User {
 | 
				
			||||||
 | 
					        name: Some(name),
 | 
				
			||||||
 | 
					        pubkey: None,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    let command = Command::DbReqGetUser(user);
 | 
				
			||||||
 | 
					    context
 | 
				
			||||||
 | 
					        .pubsub
 | 
				
			||||||
 | 
					        .publish(
 | 
				
			||||||
 | 
					            channels::MSG_NOOSE,
 | 
				
			||||||
 | 
					            Message {
 | 
				
			||||||
 | 
					                source: channels::MSG_NIP05,
 | 
				
			||||||
 | 
					                content: command,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Ok(message) = subscriber.recv().await {
 | 
				
			||||||
 | 
					        let mut response = json!({"names": {}, "relays": {}});
 | 
				
			||||||
 | 
					        match message.content {
 | 
				
			||||||
 | 
					            Command::DbResUser(user) => {
 | 
				
			||||||
 | 
					                response = json!({
 | 
				
			||||||
 | 
					                    "names": {
 | 
				
			||||||
 | 
					                        user.username: user.pubkey
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    "relays": {}
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                Ok(warp::reply::json(&response))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Command::ServiceError(e) => Ok(warp::reply::json(&response)),
 | 
				
			||||||
 | 
					            _ => Err(warp::reject::custom(Error::internal_with_message(
 | 
				
			||||||
 | 
					                "Unhandeled message type",
 | 
				
			||||||
 | 
					            ))),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Err(warp::reject::custom(Error::internal_with_message(
 | 
				
			||||||
 | 
					            "Unhandeled message type",
 | 
				
			||||||
 | 
					        )))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/usernames/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					mod accounts;
 | 
				
			||||||
 | 
					pub mod dto;
 | 
				
			||||||
 | 
					mod filter;
 | 
				
			||||||
 | 
					mod handler;
 | 
				
			||||||
 | 
					mod routes;
 | 
				
			||||||
 | 
					mod util;
 | 
				
			||||||
 | 
					mod validators;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::utils::structs::Context;
 | 
				
			||||||
 | 
					use crate::utils::rejection_handler::handle_rejection;
 | 
				
			||||||
 | 
					use tokio::runtime;
 | 
				
			||||||
 | 
					use warp::Filter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn start(context: Context) {
 | 
				
			||||||
 | 
					    let rt = runtime::Runtime::new().unwrap();
 | 
				
			||||||
 | 
					    rt.block_on(async {
 | 
				
			||||||
 | 
					        log::info!("Starting NIP-05 on http://127.0.0.1:8085");
 | 
				
			||||||
 | 
					        let routes = routes::routes(context).recover(handle_rejection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        warp::serve(routes).run(([127, 0, 0, 1], 8085)).await;
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										57
									
								
								src/usernames/routes.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,57 @@
 | 
				
			||||||
 | 
					use crate::noose::user::User;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::accounts::create_account;
 | 
				
			||||||
 | 
					use super::dto::{Account, UserQuery};
 | 
				
			||||||
 | 
					use super::filter::{validate_body_filter, validate_query_filter};
 | 
				
			||||||
 | 
					use super::handler::{get_account, get_user};
 | 
				
			||||||
 | 
					use crate::utils::filter::with_context;
 | 
				
			||||||
 | 
					use crate::utils::structs::Context;
 | 
				
			||||||
 | 
					use warp::{Filter, Rejection, Reply};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn routes(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
				
			||||||
 | 
					    let index = warp::path::end().map(|| warp::reply::html("<h1>SNEED!</h1>"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    index
 | 
				
			||||||
 | 
					        .or(nip05_get(context.clone()))
 | 
				
			||||||
 | 
					        .or(account_create(context.clone()))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn nip05_get(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
				
			||||||
 | 
					    warp::get()
 | 
				
			||||||
 | 
					        .and(warp::path(".well-known"))
 | 
				
			||||||
 | 
					        .and(warp::path("nostr.json"))
 | 
				
			||||||
 | 
					        .and(validate_query_filter::<UserQuery>())
 | 
				
			||||||
 | 
					        .and(with_context(context))
 | 
				
			||||||
 | 
					        .and_then(get_user)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn account_create(
 | 
				
			||||||
 | 
					    context: Context,
 | 
				
			||||||
 | 
					) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
				
			||||||
 | 
					    warp::path("account")
 | 
				
			||||||
 | 
					        .and(warp::post())
 | 
				
			||||||
 | 
					        .and(validate_body_filter::<User>())
 | 
				
			||||||
 | 
					        .and(with_context(context))
 | 
				
			||||||
 | 
					        .and_then(create_account)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn account_get(
 | 
				
			||||||
 | 
					    context: Context,
 | 
				
			||||||
 | 
					) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
				
			||||||
 | 
					    warp::path("account")
 | 
				
			||||||
 | 
					        .and(warp::get())
 | 
				
			||||||
 | 
					        .and(validate_body_filter::<Account>())
 | 
				
			||||||
 | 
					        .and(with_context(context))
 | 
				
			||||||
 | 
					        .and_then(get_account)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// pub fn account_update(
 | 
				
			||||||
 | 
					//     context: Context,
 | 
				
			||||||
 | 
					// ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
				
			||||||
 | 
					//     warp::path("account")
 | 
				
			||||||
 | 
					//         .and(warp::put())
 | 
				
			||||||
 | 
					//         .and(warp::body::json::<Account>())
 | 
				
			||||||
 | 
					//         .then(validate_body)
 | 
				
			||||||
 | 
					//         .and(with_context(context))
 | 
				
			||||||
 | 
					//         .and_then(update_account)
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
							
								
								
									
										3
									
								
								src/usernames/util.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct InvalidParameter;
 | 
				
			||||||
 | 
					impl warp::reject::Reject for InvalidParameter {}
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/usernames/validators.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					use super::dto::AccountPubkey;
 | 
				
			||||||
 | 
					use crate::utils::error::Error;
 | 
				
			||||||
 | 
					use nostr::prelude::FromPkStr;
 | 
				
			||||||
 | 
					use validator::{Validate, ValidationError};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn validate_account_pubkey_query(
 | 
				
			||||||
 | 
					    account_pubkey: AccountPubkey,
 | 
				
			||||||
 | 
					) -> Result<AccountPubkey, Error>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    AccountPubkey: Validate,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    match account_pubkey.validate() {
 | 
				
			||||||
 | 
					        Ok(_) => Ok(account_pubkey),
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            log::error!("AccountPubkey validation errors: {}", e);
 | 
				
			||||||
 | 
					            Err(Error::validation_error(e))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn validate_pubkey(value: &str) -> Result<(), ValidationError> {
 | 
				
			||||||
 | 
					    if value.is_empty() {
 | 
				
			||||||
 | 
					        return Err(ValidationError::new("Value is empty"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match nostr::Keys::from_pk_str(value) {
 | 
				
			||||||
 | 
					        Ok(_) => Ok(()),
 | 
				
			||||||
 | 
					        Err(_) => Err(ValidationError::new("Unable to parse pk_str")),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										52
									
								
								src/utils/crypto.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,52 @@
 | 
				
			||||||
 | 
					use argon2::{
 | 
				
			||||||
 | 
					    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
 | 
				
			||||||
 | 
					    Argon2,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn password_hash(password: String) -> String {
 | 
				
			||||||
 | 
					    let password = password.as_bytes();
 | 
				
			||||||
 | 
					    let salt = SaltString::generate(&mut OsRng);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Argon2 with default params (Argon2id v19)
 | 
				
			||||||
 | 
					    let argon2 = Argon2::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Hash password to PHC string ($argon2id$v=19$...)
 | 
				
			||||||
 | 
					    let password_hash = argon2.hash_password(password, &salt).unwrap().to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    password_hash
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn password_verify(password: String, password_hash: String) -> bool {
 | 
				
			||||||
 | 
					    // Verify password against PHC string.
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // NOTE: hash params from `parsed_hash` are used instead of what is configured in the
 | 
				
			||||||
 | 
					    // `Argon2` instance.
 | 
				
			||||||
 | 
					    let password = password.as_bytes();
 | 
				
			||||||
 | 
					    let parsed_hash = PasswordHash::new(&password_hash).unwrap();
 | 
				
			||||||
 | 
					    Argon2::default()
 | 
				
			||||||
 | 
					        .verify_password(password, &parsed_hash)
 | 
				
			||||||
 | 
					        .is_ok()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					    use super::{password_hash, password_verify};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn hash() {
 | 
				
			||||||
 | 
					        let pass = "mysecretpassword".to_string();
 | 
				
			||||||
 | 
					        let hash = password_hash(pass);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_ne!(hash, "".to_string());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn verify() {
 | 
				
			||||||
 | 
					        let pass = "mysecretpassword".to_string();
 | 
				
			||||||
 | 
					        let hash = password_hash(pass.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let verification = password_verify(pass, hash);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert!(verification);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										132
									
								
								src/utils/error.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,132 @@
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use serde_json;
 | 
				
			||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    convert::From,
 | 
				
			||||||
 | 
					    fmt::{self, Display},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use validator::ValidationErrors;
 | 
				
			||||||
 | 
					use warp::{http::StatusCode, reject::Reject};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
 | 
				
			||||||
 | 
					pub struct Error {
 | 
				
			||||||
 | 
					    pub code: u16,
 | 
				
			||||||
 | 
					    pub message: String,
 | 
				
			||||||
 | 
					    /// Sneedstr version.
 | 
				
			||||||
 | 
					    #[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			||||||
 | 
					    pub sneedstr_version: Option<u16>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Error {
 | 
				
			||||||
 | 
					    pub fn new(code: StatusCode, message: String) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            code: code.as_u16(),
 | 
				
			||||||
 | 
					            message,
 | 
				
			||||||
 | 
					            sneedstr_version: None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn from_anyhow_error(code: StatusCode, err: anyhow::Error) -> Self {
 | 
				
			||||||
 | 
					        Self::new(code, err.to_string())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn bad_request<S: Display>(msg: S) -> Self {
 | 
				
			||||||
 | 
					        Self::new(StatusCode::BAD_REQUEST, msg.to_string())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn internal_with_message<S: Display>(msg: S) -> Self {
 | 
				
			||||||
 | 
					        Self::new(StatusCode::INTERNAL_SERVER_ERROR, msg.to_string())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn validation_error(msg: ValidationErrors) -> Self {
 | 
				
			||||||
 | 
					        let message =
 | 
				
			||||||
 | 
					            serde_json::to_string(&msg.field_errors()).unwrap_or("Validation Error".to_string());
 | 
				
			||||||
 | 
					        Self::new(StatusCode::BAD_REQUEST, message)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn not_found<S: Display>(resource: &str, identifier: S, service_version: u16) -> Self {
 | 
				
			||||||
 | 
					        Self::new(
 | 
				
			||||||
 | 
					            StatusCode::NOT_FOUND,
 | 
				
			||||||
 | 
					            format!("{} not found by {}", resource, identifier),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .sneedstr_version(service_version)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn invalid_param<S: Display>(name: &str, value: S) -> Self {
 | 
				
			||||||
 | 
					        Self::bad_request(format!("invalid parameter {}: {}", name, value))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn invalid_request_body<S: Display>(msg: S) -> Self {
 | 
				
			||||||
 | 
					        Self::bad_request(format!("invalid request body: {}", msg))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn internal(err: anyhow::Error) -> Self {
 | 
				
			||||||
 | 
					        Self::from_anyhow_error(StatusCode::INTERNAL_SERVER_ERROR, err)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn status_code(&self) -> StatusCode {
 | 
				
			||||||
 | 
					        StatusCode::from_u16(self.code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn sneedstr_version(mut self, service_version: u16) -> Self {
 | 
				
			||||||
 | 
					        self.sneedstr_version = Some(service_version);
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl fmt::Display for Error {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			||||||
 | 
					        write!(f, "{}: {}", self.status_code(), &self.message)?;
 | 
				
			||||||
 | 
					        if let Some(val) = &self.sneedstr_version {
 | 
				
			||||||
 | 
					            write!(f, "\ndiem ledger version: {}", val)?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Reject for Error {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<anyhow::Error> for Error {
 | 
				
			||||||
 | 
					    fn from(e: anyhow::Error) -> Self {
 | 
				
			||||||
 | 
					        Self::internal(e)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<serde_json::error::Error> for Error {
 | 
				
			||||||
 | 
					    fn from(err: serde_json::error::Error) -> Self {
 | 
				
			||||||
 | 
					        Self::internal(err.into())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					    use super::Error;
 | 
				
			||||||
 | 
					    use warp::http::StatusCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn test_to_string() {
 | 
				
			||||||
 | 
					        let err = Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned());
 | 
				
			||||||
 | 
					        assert_eq!(err.to_string(), "400 Bad Request: invalid address")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn test_from_anyhow_error_as_internal_error() {
 | 
				
			||||||
 | 
					        let err = Error::from(anyhow::format_err!("hello"));
 | 
				
			||||||
 | 
					        assert_eq!(err.to_string(), "500 Internal Server Error: hello")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn test_to_string_with_sneedstr_version() {
 | 
				
			||||||
 | 
					        let err =
 | 
				
			||||||
 | 
					            Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned()).sneedstr_version(123);
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            err.to_string(),
 | 
				
			||||||
 | 
					            "400 Bad Request: invalid address\ndiem ledger version: 123"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn test_internal_error() {
 | 
				
			||||||
 | 
					        let err = Error::internal(anyhow::format_err!("hello"));
 | 
				
			||||||
 | 
					        assert_eq!(err.to_string(), "500 Internal Server Error: hello")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/utils/filter.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					use super::structs::Context;
 | 
				
			||||||
 | 
					use warp::Filter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn with_context(
 | 
				
			||||||
 | 
					    context: Context,
 | 
				
			||||||
 | 
					) -> impl Filter<Extract = (Context,), Error = std::convert::Infallible> + Clone {
 | 
				
			||||||
 | 
					    warp::any().map(move || context.clone())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								src/utils/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					pub mod crypto;
 | 
				
			||||||
 | 
					pub mod error;
 | 
				
			||||||
 | 
					pub mod filter;
 | 
				
			||||||
 | 
					// mod nostr_filter_helpers;
 | 
				
			||||||
 | 
					pub mod rejection_handler;
 | 
				
			||||||
 | 
					pub mod response;
 | 
				
			||||||
 | 
					pub mod structs;
 | 
				
			||||||
							
								
								
									
										156
									
								
								src/utils/nostr_filter_helpers.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,156 @@
 | 
				
			||||||
 | 
					use nostr::{Event, Filter, Kind, Tag};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn ids_match(filter: &Filter, event: &Event) -> bool {
 | 
				
			||||||
 | 
					    if filter.ids.is_empty() {
 | 
				
			||||||
 | 
					        println!("[FILTER][IDS] skipped");
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    println!(
 | 
				
			||||||
 | 
					        "[FILTER][IDS] matched: {:?}",
 | 
				
			||||||
 | 
					        filter.ids.iter().any(|id| id == &event.id.to_string())
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filter.ids.iter().any(|id| id == &event.id.to_string())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn kind_match(filter: &Filter, kind: Kind) -> bool {
 | 
				
			||||||
 | 
					    if filter.kinds.is_empty() {
 | 
				
			||||||
 | 
					        println!("[FILTER][KINDS] skipped");
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    println!(
 | 
				
			||||||
 | 
					        "[FILTER][KIND] matched: {:?}",
 | 
				
			||||||
 | 
					        filter.kinds.iter().any(|k| k == &kind)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filter.kinds.iter().any(|k| k == &kind)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn pubkeys_match(filter: &Filter, event: &Event) -> bool {
 | 
				
			||||||
 | 
					    if filter.pubkeys.is_empty() {
 | 
				
			||||||
 | 
					        println!("[FILTER][PUBKEYS] skipped");
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    println!(
 | 
				
			||||||
 | 
					        "[FILTER][PUBKEYS] matched: {:?}",
 | 
				
			||||||
 | 
					        filter.pubkeys.iter().any(|pk| pk == &event.pubkey)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    filter.pubkeys.iter().any(|pk| pk == &event.pubkey)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn authors_match(filter: &Filter, event: &Event) -> bool {
 | 
				
			||||||
 | 
					    dbg!(filter);
 | 
				
			||||||
 | 
					    if filter.authors.is_empty() {
 | 
				
			||||||
 | 
					        println!("[FILTER][AUTHORS] skipped");
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    println!(
 | 
				
			||||||
 | 
					        "[FILTER][AUTHORS] matched: {:?}",
 | 
				
			||||||
 | 
					        filter
 | 
				
			||||||
 | 
					            .authors
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .any(|author| author == &event.pubkey.to_string())
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    filter
 | 
				
			||||||
 | 
					        .authors
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .any(|author| author == &event.pubkey.to_string())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn delegated_authors_match(filter: &Filter, event: &Event) -> bool {
 | 
				
			||||||
 | 
					    // Optional implementation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // let delegated_authors_match = filter.authors.iter().any(|author| {
 | 
				
			||||||
 | 
					    //     event.tags.iter().any(|tag| match tag {
 | 
				
			||||||
 | 
					    //         Tag::Delegation {
 | 
				
			||||||
 | 
					    //             delegator_pk,
 | 
				
			||||||
 | 
					    //             conditions,
 | 
				
			||||||
 | 
					    //             sig,
 | 
				
			||||||
 | 
					    //         } => filter
 | 
				
			||||||
 | 
					    //             .authors
 | 
				
			||||||
 | 
					    //             .iter()
 | 
				
			||||||
 | 
					    //             .any(|author| author == &delegator_pk.to_string()),
 | 
				
			||||||
 | 
					    //         _ => false,
 | 
				
			||||||
 | 
					    //     })
 | 
				
			||||||
 | 
					    // });
 | 
				
			||||||
 | 
					    println!(
 | 
				
			||||||
 | 
					        "[FILTER][DELEGATED_AUTHORS] matched: {:?}",
 | 
				
			||||||
 | 
					        event.tags.iter().any(|tag| match tag {
 | 
				
			||||||
 | 
					            Tag::Delegation {
 | 
				
			||||||
 | 
					                delegator_pk,
 | 
				
			||||||
 | 
					                conditions,
 | 
				
			||||||
 | 
					                sig,
 | 
				
			||||||
 | 
					            } => filter
 | 
				
			||||||
 | 
					                .authors
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .any(|author| author == &delegator_pk.to_string()),
 | 
				
			||||||
 | 
					            _ => false,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event.tags.iter().any(|tag| match tag {
 | 
				
			||||||
 | 
					        Tag::Delegation {
 | 
				
			||||||
 | 
					            delegator_pk,
 | 
				
			||||||
 | 
					            conditions,
 | 
				
			||||||
 | 
					            sig,
 | 
				
			||||||
 | 
					        } => filter
 | 
				
			||||||
 | 
					            .authors
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .any(|author| author == &delegator_pk.to_string()),
 | 
				
			||||||
 | 
					        _ => true,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn tag_match(filter: &Filter, event: &Event) -> bool {
 | 
				
			||||||
 | 
					    println!(
 | 
				
			||||||
 | 
					        "[FILTER][TAG] matched: {:?}",
 | 
				
			||||||
 | 
					        filter.generic_tags.iter().any(|(key, value)| {
 | 
				
			||||||
 | 
					            event.tags.iter().any(|tag| {
 | 
				
			||||||
 | 
					                let kv = tag.as_vec();
 | 
				
			||||||
 | 
					                key.to_string() == kv[0] && value.iter().any(|vv| vv == &kv[1])
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filter.generic_tags.iter().any(|(key, value)| {
 | 
				
			||||||
 | 
					        event.tags.iter().any(|tag| {
 | 
				
			||||||
 | 
					            let kv = tag.as_vec();
 | 
				
			||||||
 | 
					            key.to_string() == kv[0] && value.iter().any(|vv| vv == &kv[1])
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    true // TODO: Fix delegated authors check
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn interested_in_event(filter: &Filter, event: &Event) -> bool {
 | 
				
			||||||
 | 
					    ids_match(filter, event)
 | 
				
			||||||
 | 
					        && filter.since.map_or(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                println!("[FILTER][SINCE][default] matched: {:?}", true);
 | 
				
			||||||
 | 
					                true
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            |t| {
 | 
				
			||||||
 | 
					                println!("[FILTER][SINCE] matched: {:?}", event.created_at >= t);
 | 
				
			||||||
 | 
					                event.created_at >= t
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        && filter.until.map_or(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                println!("[FILTER][UNTIL][default] matched: {:?}", true);
 | 
				
			||||||
 | 
					                true
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            |t| {
 | 
				
			||||||
 | 
					                println!("[FILTER][UNTIL] matched: {:?}", event.created_at <= t);
 | 
				
			||||||
 | 
					                event.created_at <= t
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        && kind_match(filter, event.kind)
 | 
				
			||||||
 | 
					        && (pubkeys_match(filter, event)
 | 
				
			||||||
 | 
					            || authors_match(filter, event)
 | 
				
			||||||
 | 
					            || delegated_authors_match(filter, event))
 | 
				
			||||||
 | 
					        && tag_match(filter, event)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/utils/rejection_handler.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					use super::error::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::convert::Infallible;
 | 
				
			||||||
 | 
					use warp::{
 | 
				
			||||||
 | 
					    body::BodyDeserializeError,
 | 
				
			||||||
 | 
					    cors::CorsForbidden,
 | 
				
			||||||
 | 
					    http::StatusCode,
 | 
				
			||||||
 | 
					    reject::{LengthRequired, MethodNotAllowed, PayloadTooLarge, UnsupportedMediaType},
 | 
				
			||||||
 | 
					    reply, Rejection, Reply,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
 | 
				
			||||||
 | 
					    let code;
 | 
				
			||||||
 | 
					    let body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if err.is_not_found() {
 | 
				
			||||||
 | 
					        code = StatusCode::NOT_FOUND;
 | 
				
			||||||
 | 
					        body = reply::json(&Error::new(code, "Not Found".to_owned()));
 | 
				
			||||||
 | 
					    } else if let Some(error) = err.find::<Error>() {
 | 
				
			||||||
 | 
					        code = error.status_code();
 | 
				
			||||||
 | 
					        body = reply::json(error);
 | 
				
			||||||
 | 
					    } else if let Some(cause) = err.find::<CorsForbidden>() {
 | 
				
			||||||
 | 
					        code = StatusCode::FORBIDDEN;
 | 
				
			||||||
 | 
					        body = reply::json(&Error::new(code, cause.to_string()));
 | 
				
			||||||
 | 
					    } else if let Some(cause) = err.find::<BodyDeserializeError>() {
 | 
				
			||||||
 | 
					        code = StatusCode::BAD_REQUEST;
 | 
				
			||||||
 | 
					        body = reply::json(&Error::new(code, cause.to_string()));
 | 
				
			||||||
 | 
					    } else if let Some(cause) = err.find::<LengthRequired>() {
 | 
				
			||||||
 | 
					        code = StatusCode::LENGTH_REQUIRED;
 | 
				
			||||||
 | 
					        body = reply::json(&Error::new(code, cause.to_string()));
 | 
				
			||||||
 | 
					    } else if let Some(cause) = err.find::<PayloadTooLarge>() {
 | 
				
			||||||
 | 
					        code = StatusCode::PAYLOAD_TOO_LARGE;
 | 
				
			||||||
 | 
					        body = reply::json(&Error::new(code, cause.to_string()));
 | 
				
			||||||
 | 
					    } else if let Some(cause) = err.find::<UnsupportedMediaType>() {
 | 
				
			||||||
 | 
					        code = StatusCode::UNSUPPORTED_MEDIA_TYPE;
 | 
				
			||||||
 | 
					        body = reply::json(&Error::new(code, cause.to_string()));
 | 
				
			||||||
 | 
					    } else if let Some(cause) = err.find::<MethodNotAllowed>() {
 | 
				
			||||||
 | 
					        code = StatusCode::METHOD_NOT_ALLOWED;
 | 
				
			||||||
 | 
					        body = reply::json(&Error::new(code, cause.to_string()));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        code = StatusCode::INTERNAL_SERVER_ERROR;
 | 
				
			||||||
 | 
					        body = reply::json(&Error::new(code, format!("unexpected error: {:?}", err)));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Ok(reply::with_status(body, code))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								src/utils/response.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					use super::error::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use anyhow::Result;
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use warp::http::header::{HeaderValue, CONTENT_TYPE};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Response {
 | 
				
			||||||
 | 
					    pub body: Vec<u8>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Response {
 | 
				
			||||||
 | 
					    pub fn new<T: Serialize>(body: &T) -> Result<Self, Error> {
 | 
				
			||||||
 | 
					        Ok(Self {
 | 
				
			||||||
 | 
					            body: serde_json::to_vec(body)?,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl warp::Reply for Response {
 | 
				
			||||||
 | 
					    fn into_response(self) -> warp::reply::Response {
 | 
				
			||||||
 | 
					        let mut res = warp::reply::Response::new(self.body.into());
 | 
				
			||||||
 | 
					        let headers = res.headers_mut();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										135
									
								
								src/utils/structs.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,135 @@
 | 
				
			||||||
 | 
					use super::error::Error;
 | 
				
			||||||
 | 
					// use super::nostr_filter_helpers;
 | 
				
			||||||
 | 
					use crate::PubSub;
 | 
				
			||||||
 | 
					use nostr::{Event, Filter, SubscriptionId};
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					use tokio::sync::mpsc;
 | 
				
			||||||
 | 
					use uuid::Uuid;
 | 
				
			||||||
 | 
					use warp::ws::Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MAX_SUBSCRIPTIONS: usize = 256;
 | 
				
			||||||
 | 
					const MAX_SUBSCRIPTION_ID_LEN: usize = 256;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(serde::Serialize, PartialEq, Eq, Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct Subscription {
 | 
				
			||||||
 | 
					    pub id: SubscriptionId,
 | 
				
			||||||
 | 
					    pub filters: Vec<Filter>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Subscription {
 | 
				
			||||||
 | 
					    pub fn new(id: SubscriptionId, filters: Vec<Filter>) -> Self {
 | 
				
			||||||
 | 
					        Self { id, filters }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_id(&self) -> String {
 | 
				
			||||||
 | 
					        self.id.to_string()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn needs_historical_events(&self) -> bool {
 | 
				
			||||||
 | 
					        self.filters.iter().any(|f| f.limit != Some(0))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn interested_in_event(&self, event: &Event) -> bool {
 | 
				
			||||||
 | 
					        log::info!("[Subscription] Checking if client is interested in the new event");
 | 
				
			||||||
 | 
					        for filter in &self.filters {
 | 
				
			||||||
 | 
					            if filter.match_event(event) {
 | 
				
			||||||
 | 
					                log::info!("[Subscription] found filter that matches the event");
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct Client {
 | 
				
			||||||
 | 
					    client_ip_addr: String,
 | 
				
			||||||
 | 
					    pub client_id: Uuid,
 | 
				
			||||||
 | 
					    pub client_connection: Option<mpsc::UnboundedSender<Result<Message, Error>>>,
 | 
				
			||||||
 | 
					    pub subscriptions: HashMap<String, Subscription>,
 | 
				
			||||||
 | 
					    max_subs: usize,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Client {
 | 
				
			||||||
 | 
					    pub fn new(client_ip_addr: String) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            client_ip_addr,
 | 
				
			||||||
 | 
					            client_id: Uuid::new_v4(),
 | 
				
			||||||
 | 
					            client_connection: None,
 | 
				
			||||||
 | 
					            subscriptions: HashMap::new(),
 | 
				
			||||||
 | 
					            max_subs: MAX_SUBSCRIPTIONS,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn ip(&self) -> &str {
 | 
				
			||||||
 | 
					        &self.client_ip_addr
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn subscribe(&mut self, subscription: Subscription) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let k = subscription.get_id();
 | 
				
			||||||
 | 
					        let sub_id_len = k.len();
 | 
				
			||||||
 | 
					        if sub_id_len > MAX_SUBSCRIPTION_ID_LEN {
 | 
				
			||||||
 | 
					            log::debug!(
 | 
				
			||||||
 | 
					                "Ignoring sub request with excessive length: ({})",
 | 
				
			||||||
 | 
					                sub_id_len
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return Err(Error::bad_request("sub request is too long"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.subscriptions.contains_key(&k) {
 | 
				
			||||||
 | 
					            self.subscriptions.remove(&k);
 | 
				
			||||||
 | 
					            self.subscriptions.insert(k, subscription.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            log::debug!(
 | 
				
			||||||
 | 
					                "replaced existing subscription (cid: {}, sub: {:?})",
 | 
				
			||||||
 | 
					                self.client_id,
 | 
				
			||||||
 | 
					                subscription.get_id()
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return Ok(());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.subscriptions.len() >= self.max_subs {
 | 
				
			||||||
 | 
					            return Err(Error::bad_request("max subs exceeded"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Insert subscription
 | 
				
			||||||
 | 
					        self.subscriptions.insert(k, subscription);
 | 
				
			||||||
 | 
					        log::debug!(
 | 
				
			||||||
 | 
					            "registered new subscription, currently have {} active subs (cid: {})",
 | 
				
			||||||
 | 
					            self.subscriptions.len(),
 | 
				
			||||||
 | 
					            self.client_id
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn unsubscribe(&mut self, subscription_id: SubscriptionId) {
 | 
				
			||||||
 | 
					        self.subscriptions.remove(&subscription_id.to_string());
 | 
				
			||||||
 | 
					        log::debug!(
 | 
				
			||||||
 | 
					            "removed subscription, currently have {} active subs (cid: {})",
 | 
				
			||||||
 | 
					            self.subscriptions.len(),
 | 
				
			||||||
 | 
					            self.client_id
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct Context {
 | 
				
			||||||
 | 
					    pub pubsub: Arc<PubSub>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Default for Context {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        panic!("Use Context::new() to initialize Contexr");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Context {
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        let pubsub = Arc::new(PubSub::new());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Self { pubsub }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										66
									
								
								templates/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,66 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="utf-8">
 | 
				
			||||||
 | 
					    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
 | 
					    <title>Registration</title>
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div id="app">
 | 
				
			||||||
 | 
					      <h1>Create new user</h1>
 | 
				
			||||||
 | 
					        <form action="/register" method="post" id="signup">
 | 
				
			||||||
 | 
					          <h1>Sign Up</h1>
 | 
				
			||||||
 | 
						  <div class="field">
 | 
				
			||||||
 | 
						    <label for="name">Name:</label>
 | 
				
			||||||
 | 
						    <input type="text" id="name" name="name" placeholder="Enter your profile name" />
 | 
				
			||||||
 | 
						    <small></small>
 | 
				
			||||||
 | 
						  </div>
 | 
				
			||||||
 | 
						  <div class="field">
 | 
				
			||||||
 | 
						    <label for="email">PubKey:</label>
 | 
				
			||||||
 | 
						    <input type="text" id="pubkey" name="pubkey" placeholder="Enter your pubkey" />
 | 
				
			||||||
 | 
						    <small></small>
 | 
				
			||||||
 | 
						  </div>
 | 
				
			||||||
 | 
						  <button type="submit">Register</button>
 | 
				
			||||||
 | 
					        </form> 
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					  <script>
 | 
				
			||||||
 | 
					    const form = document.getElementById('signup');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    form.addEventListener('submit', async (event) => {
 | 
				
			||||||
 | 
					      // stop form submission
 | 
				
			||||||
 | 
						event.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const name = form.elements['name'];
 | 
				
			||||||
 | 
						const pubkey = form.elements['pubkey'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// getting the element's value
 | 
				
			||||||
 | 
						let userName = name.value;
 | 
				
			||||||
 | 
						let pubKey = pubkey.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						console.log(`${userName}:${pubKey}`);
 | 
				
			||||||
 | 
						const data = { name: userName, pubkey: pubKey};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Send user data
 | 
				
			||||||
 | 
						await postJSON(data);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async function postJSON(data) {
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
						    const response = await fetch("http://localhost:8085/register", {
 | 
				
			||||||
 | 
							method: "POST",
 | 
				
			||||||
 | 
							headers: {
 | 
				
			||||||
 | 
							    "Content-Type": "application/json",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							body: JSON.stringify(data),
 | 
				
			||||||
 | 
						    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						    const result = await response.json();
 | 
				
			||||||
 | 
						    console.log("Success:", result);
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
						    console.error("Error:", error);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  </script>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										70
									
								
								templates/index.stpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,70 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="utf-8">
 | 
				
			||||||
 | 
					    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
 | 
					    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
 | 
				
			||||||
 | 
					    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
 | 
				
			||||||
 | 
					    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
 | 
				
			||||||
 | 
					    <link rel="manifest" href="/site.webmanifest">
 | 
				
			||||||
 | 
					    <title>Registration</title>
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div id="app">
 | 
				
			||||||
 | 
					      <h1>Create new user</h1>
 | 
				
			||||||
 | 
					        <form action="/register" method="post" id="signup">
 | 
				
			||||||
 | 
					          <h1>Sign Up</h1>
 | 
				
			||||||
 | 
						  <div class="field">
 | 
				
			||||||
 | 
						    <label for="name">Name:</label>
 | 
				
			||||||
 | 
						    <input type="text" id="name" name="name" placeholder="Enter your profile name" />
 | 
				
			||||||
 | 
						    <small></small>
 | 
				
			||||||
 | 
						  </div>
 | 
				
			||||||
 | 
						  <div class="field">
 | 
				
			||||||
 | 
						    <label for="email">PubKey:</label>
 | 
				
			||||||
 | 
						    <input type="text" id="pubkey" name="pubkey" placeholder="Enter your pubkey" />
 | 
				
			||||||
 | 
						    <small></small>
 | 
				
			||||||
 | 
						  </div>
 | 
				
			||||||
 | 
						  <button type="submit">Register</button>
 | 
				
			||||||
 | 
					        </form> 
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					  <script>
 | 
				
			||||||
 | 
					    const form = document.getElementById('signup');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    form.addEventListener('submit', async (event) => {
 | 
				
			||||||
 | 
					      // stop form submission
 | 
				
			||||||
 | 
						event.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const name = form.elements['name'];
 | 
				
			||||||
 | 
						const pubkey = form.elements['pubkey'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// getting the element's value
 | 
				
			||||||
 | 
						let userName = name.value;
 | 
				
			||||||
 | 
						let pubKey = pubkey.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						console.log(`${userName}:${pubKey}`);
 | 
				
			||||||
 | 
						const data = { name: userName, pubkey: pubKey};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Send user data
 | 
				
			||||||
 | 
						await postJSON(data);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async function postJSON(data) {
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
						    const response = await fetch("http://localhost:8085/register", {
 | 
				
			||||||
 | 
							method: "POST",
 | 
				
			||||||
 | 
							headers: {
 | 
				
			||||||
 | 
							    "Content-Type": "application/json",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							body: JSON.stringify(data),
 | 
				
			||||||
 | 
						    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						    const result = await response.json();
 | 
				
			||||||
 | 
						    console.log("Success:", result);
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
						    console.error("Error:", error);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  </script>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								templates/static/android-chrome-192x192.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 43 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								templates/static/android-chrome-512x512.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 235 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								templates/static/apple-touch-icon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 39 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								templates/static/favicon-16x16.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 777 B  | 
							
								
								
									
										
											BIN
										
									
								
								templates/static/favicon-32x32.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								templates/static/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										1
									
								
								templates/static/site.webmanifest
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								www/static/android-chrome-192x192.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								www/static/android-chrome-512x512.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 14 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								www/static/apple-touch-icon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								www/static/favicon-16x16.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 715 B  | 
							
								
								
									
										
											BIN
										
									
								
								www/static/favicon-32x32.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								www/static/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										1
									
								
								www/static/site.webmanifest
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
 | 
				
			||||||