Compare commits
	
		
			No commits in common. "e40a4645a373223cd9550f5937310cc681b49df0" and "255905021c6e488bc8867ef0664d04646e63f847" have entirely different histories.
		
	
	
		
			e40a4645a3
			...
			255905021c
		
	
		
					 35 changed files with 664 additions and 1404 deletions
				
			
		
							
								
								
									
										92
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										92
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -497,6 +497,18 @@ version = "0.1.9"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "filetime"
 | 
			
		||||
version = "0.2.22"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "redox_syscall 0.3.5",
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "flate2"
 | 
			
		||||
version = "1.0.28"
 | 
			
		||||
| 
						 | 
				
			
			@ -749,6 +761,15 @@ version = "0.1.1"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "home"
 | 
			
		||||
version = "0.5.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "http"
 | 
			
		||||
version = "0.2.9"
 | 
			
		||||
| 
						 | 
				
			
			@ -936,6 +957,12 @@ version = "1.0.9"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "itoap"
 | 
			
		||||
version = "1.0.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "js-sys"
 | 
			
		||||
version = "0.3.64"
 | 
			
		||||
| 
						 | 
				
			
			@ -1564,6 +1591,44 @@ version = "1.0.15"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "sailfish"
 | 
			
		||||
version = "0.7.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "82a03fcade08eb837d7ba0d8f775f1fe6cddb00d413de0c655ab2b93e821a0eb"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "itoap",
 | 
			
		||||
 "ryu",
 | 
			
		||||
 "sailfish-macros",
 | 
			
		||||
 "version_check",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "sailfish-compiler"
 | 
			
		||||
version = "0.7.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0cf1deecb07747a1f7ab55745edcc406875db007602b037af5fcf59f40dd2005"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "filetime",
 | 
			
		||||
 "home",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "syn 2.0.28",
 | 
			
		||||
 "toml",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "sailfish-macros"
 | 
			
		||||
version = "0.7.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c3e5946289d6daa26cde6ffb06678d7a8ba276ec6a7a55d36f64b35e7f644d22"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "sailfish-compiler",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "scoped-tls"
 | 
			
		||||
version = "1.0.1"
 | 
			
		||||
| 
						 | 
				
			
			@ -1676,6 +1741,15 @@ dependencies = [
 | 
			
		|||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_spanned"
 | 
			
		||||
version = "0.6.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_urlencoded"
 | 
			
		||||
version = "0.7.1"
 | 
			
		||||
| 
						 | 
				
			
			@ -1759,6 +1833,7 @@ dependencies = [
 | 
			
		|||
 "rusqlite",
 | 
			
		||||
 "rusqlite_migration",
 | 
			
		||||
 "rustls 0.21.6",
 | 
			
		||||
 "sailfish",
 | 
			
		||||
 "sea-query",
 | 
			
		||||
 "sea-query-rusqlite",
 | 
			
		||||
 "serde",
 | 
			
		||||
| 
						 | 
				
			
			@ -1987,11 +2062,26 @@ dependencies = [
 | 
			
		|||
 "tracing",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toml"
 | 
			
		||||
version = "0.7.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_spanned",
 | 
			
		||||
 "toml_datetime",
 | 
			
		||||
 "toml_edit",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toml_datetime"
 | 
			
		||||
version = "0.6.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toml_edit"
 | 
			
		||||
| 
						 | 
				
			
			@ -2000,6 +2090,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		|||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "indexmap 2.0.0",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_spanned",
 | 
			
		||||
 "toml_datetime",
 | 
			
		||||
 "winnow",
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ rusqlite_migration = "1.0.2"
 | 
			
		|||
nostr = "0.27.0"
 | 
			
		||||
nostr-database = "0.27.0"
 | 
			
		||||
regex = "1.9.5"
 | 
			
		||||
sailfish = "0.7.0"
 | 
			
		||||
sea-query = { version = "0.30.4", features = ["backend-sqlite", "thread-safe"] }
 | 
			
		||||
sea-query-rusqlite = { version="0", features = [
 | 
			
		||||
    "with-chrono",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@
 | 
			
		|||
        naersk' = pkgs.callPackage naersk { };
 | 
			
		||||
 | 
			
		||||
        wwwPath = "www";
 | 
			
		||||
        templatesPath = "templates";
 | 
			
		||||
 | 
			
		||||
      in rec {
 | 
			
		||||
        # For `nix build` & `nix run`:
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +25,7 @@
 | 
			
		|||
            mkdir -p $out/templates
 | 
			
		||||
            mkdir -p $out/www
 | 
			
		||||
            cp -r ${wwwPath} $out/
 | 
			
		||||
            cp -r ${templatesPath} $out/
 | 
			
		||||
          '';
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,8 +52,6 @@
 | 
			
		|||
          env = {
 | 
			
		||||
            DATABASE_URL = "/tmp/sqlite.db";
 | 
			
		||||
            ADMIN_PUBKEY = "npub1m2w0ckmkgj4wtvl8muwjynh56j3qd4nddca4exdg4mdrkepvfnhsmusy54";
 | 
			
		||||
            CONFIG_ENABLE_AUTH = "false";
 | 
			
		||||
            CONFIG_RELAY_URL = "ws://0.0.0.0:8080";
 | 
			
		||||
            RUST_BACKTRACE = 1;
 | 
			
		||||
            RUST_LOG = "debug";
 | 
			
		||||
          };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,11 +21,6 @@ in {
 | 
			
		|||
      'npub' of the administrator account. Must be defined!
 | 
			
		||||
      '';
 | 
			
		||||
    };
 | 
			
		||||
    enableAuth = mkOption {
 | 
			
		||||
      type = types.bool;
 | 
			
		||||
      default = false;
 | 
			
		||||
      description = "Require NIP-42 Authentication for REQ and EVENT";
 | 
			
		||||
    };
 | 
			
		||||
    sslEnable = mkEnableOption "Whether to enable ACME SSL for nginx proxy";
 | 
			
		||||
    hostAddress = mkOption {
 | 
			
		||||
      type = types.nullOr types.str;
 | 
			
		||||
| 
						 | 
				
			
			@ -41,10 +36,6 @@ in {
 | 
			
		|||
        Local nixos-container ip address
 | 
			
		||||
      '';
 | 
			
		||||
    };
 | 
			
		||||
    relayUrl = mkOption {
 | 
			
		||||
      type = types.str;
 | 
			
		||||
      description = "Relay URL that will be used for NIP-42 AUTH validation";
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  config = mkIf cfg.enable {
 | 
			
		||||
| 
						 | 
				
			
			@ -73,8 +64,6 @@ in {
 | 
			
		|||
          environment = {
 | 
			
		||||
            DATABASE_URL = "${DB_PATH}/sneedstr.db";
 | 
			
		||||
            ADMIN_PUBKEY = cfg.adminPubkey;
 | 
			
		||||
            CONFIG_ENABLE_AUTH = boolToString cfg.enableAuth;
 | 
			
		||||
            CONFIG_RELAY_URL = cfg.relayUrl;
 | 
			
		||||
          };
 | 
			
		||||
          startLimitBurst = 1;
 | 
			
		||||
          startLimitIntervalSec = 10;
 | 
			
		||||
| 
						 | 
				
			
			@ -122,10 +111,6 @@ in {
 | 
			
		|||
          proxyWebsockets = true; # needed if you need to use WebSocket
 | 
			
		||||
          recommendedProxySettings = true;
 | 
			
		||||
        };
 | 
			
		||||
        locations."/register" = {
 | 
			
		||||
          proxyPass = "http://${cfg.localAddress}:8085";
 | 
			
		||||
          recommendedProxySettings = true;
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +1,15 @@
 | 
			
		|||
use crate::{
 | 
			
		||||
    noose::sled::BanInfo,
 | 
			
		||||
    noose::user::{User, UserRow, Nip05Profile},
 | 
			
		||||
    utils::{error::Error, structs::Subscription}, usernames::dto::UserBody,
 | 
			
		||||
    noose::user::{User, UserRow},
 | 
			
		||||
    utils::{error::Error, structs::Subscription},
 | 
			
		||||
};
 | 
			
		||||
use nostr::secp256k1::XOnlyPublicKey;
 | 
			
		||||
use std::{collections::{HashMap, BTreeSet}, fmt::Debug};
 | 
			
		||||
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_AUTH: &str = "MSG_AUTH";
 | 
			
		||||
    pub static MSG_RELAY: &str = "MSG_RELAY";
 | 
			
		||||
    pub static MSG_PIPELINE: &str = "MSG_PIPELINE";
 | 
			
		||||
    pub static MSG_SLED: &str = "MSG_SLED";
 | 
			
		||||
| 
						 | 
				
			
			@ -19,21 +18,18 @@ pub mod channels {
 | 
			
		|||
#[derive(Debug, Clone, PartialEq)]
 | 
			
		||||
pub enum Command {
 | 
			
		||||
    // DbRequest
 | 
			
		||||
    // --- Req
 | 
			
		||||
    DbReqWriteEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),
 | 
			
		||||
    DbReqFindEvent(/* client_id*/ uuid::Uuid, Subscription),
 | 
			
		||||
    DbReqDeleteEvents(/* client_id*/ uuid::Uuid, Box<nostr::Event>),
 | 
			
		||||
    DbReqEventCounts(/* client_id*/ uuid::Uuid, Subscription),
 | 
			
		||||
 | 
			
		||||
    // Old messages
 | 
			
		||||
    DbReqInsertUser(UserRow),
 | 
			
		||||
    DbReqGetUser(User),
 | 
			
		||||
    DbReqCreateAccount(XOnlyPublicKey, String, String),
 | 
			
		||||
    DbReqGetAccount(String),
 | 
			
		||||
    DbReqClear,
 | 
			
		||||
    // NIP-05 related messages
 | 
			
		||||
    DbReqGetNIP05(String),
 | 
			
		||||
    DbReqCreateNIP05(UserBody),
 | 
			
		||||
    // --- Res
 | 
			
		||||
    DbResNIP05(Nip05Profile),
 | 
			
		||||
    // DbResponse
 | 
			
		||||
    DbResRelayMessages(
 | 
			
		||||
        /* client_id*/ uuid::Uuid,
 | 
			
		||||
        /* Vec<RelayMessage::Event> */ Vec<nostr::RelayMessage>,
 | 
			
		||||
| 
						 | 
				
			
			@ -42,48 +38,26 @@ pub enum Command {
 | 
			
		|||
    DbResOk,
 | 
			
		||||
    DbResOkWithStatus(/* client_id */ uuid::Uuid, nostr::RelayMessage),
 | 
			
		||||
    DbResAccount, // TODO: Add Account DTO as a param
 | 
			
		||||
    DbResUser(UserRow),
 | 
			
		||||
    DbResEventCounts(/* client_id */ uuid::Uuid, nostr::RelayMessage),
 | 
			
		||||
 | 
			
		||||
    // Noose Profile
 | 
			
		||||
    // --- Req
 | 
			
		||||
    DbReqGetProfile(XOnlyPublicKey),
 | 
			
		||||
    DbReqAddContact(/* profile_pub_key */ XOnlyPublicKey, /* contact_pub_key*/ XOnlyPublicKey),
 | 
			
		||||
    DbReqRemoveContact(/* profile_pub_key */ XOnlyPublicKey, /* contact_pub_key*/ XOnlyPublicKey),
 | 
			
		||||
    DbReqGetContactPubkeys(XOnlyPublicKey),
 | 
			
		||||
    DbReqGetContacts(XOnlyPublicKey),
 | 
			
		||||
    // --- Res
 | 
			
		||||
    DbResGetProfile(nostr_database::Profile),
 | 
			
		||||
    DbResGetContactPubkeys(Vec<XOnlyPublicKey>),
 | 
			
		||||
    DbResGetContacts(BTreeSet<nostr_database::Profile>),
 | 
			
		||||
 | 
			
		||||
    // Event Pipeline
 | 
			
		||||
    // --- Req
 | 
			
		||||
    PipelineReqEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),
 | 
			
		||||
    // --- Res
 | 
			
		||||
    PipelineResRelayMessageOk(/* client_id */ uuid::Uuid, nostr::RelayMessage),
 | 
			
		||||
    PipelineResStreamOutEvent(Box<nostr::Event>),
 | 
			
		||||
    PipelineResOk,
 | 
			
		||||
    
 | 
			
		||||
    // Subscription Errors
 | 
			
		||||
    ClientSubscriptionError(/* error message */ String),
 | 
			
		||||
    // Sled
 | 
			
		||||
    // --- Req
 | 
			
		||||
    SledReqBanUser(Box<BanInfo>),
 | 
			
		||||
    SledReqBanInfo(/* pubkey */ String),
 | 
			
		||||
    SledReqUnbanUser(/* pubkey */ String),
 | 
			
		||||
    SledReqGetBans,
 | 
			
		||||
    // --- Res
 | 
			
		||||
    SledResBan(Option<BanInfo>),
 | 
			
		||||
    SledResBans(Vec<BanInfo>),
 | 
			
		||||
    SledResSuccess(bool),
 | 
			
		||||
 | 
			
		||||
    // Other
 | 
			
		||||
    ServiceRegistrationRequired(/* client_id */ uuid::Uuid, nostr::RelayMessage),
 | 
			
		||||
    Str(String),
 | 
			
		||||
    ServiceError(Error),
 | 
			
		||||
 | 
			
		||||
    // Subscription Errors
 | 
			
		||||
    ClientSubscriptionError(/* error message */ String),
 | 
			
		||||
 | 
			
		||||
    // --- Noop
 | 
			
		||||
    Noop,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +86,7 @@ impl PubSub {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn subscribe(&self, topic: &str) -> broadcast::Receiver<Message> {
 | 
			
		||||
        let (tx, _rx) = broadcast::channel(20_000); // 20000 is the channel capacity
 | 
			
		||||
        let (tx, _rx) = broadcast::channel(32); // 32 is the channel capacity
 | 
			
		||||
        let mut subscribers = self.subscribers.lock().await;
 | 
			
		||||
        subscribers
 | 
			
		||||
            .entry(topic.to_string())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,58 +1,17 @@
 | 
			
		|||
use crate::{
 | 
			
		||||
    bussy::PubSub,
 | 
			
		||||
    usernames::dto::UserBody,
 | 
			
		||||
    utils::{error::Error, structs::Subscription},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use nostr::{secp256k1::XOnlyPublicKey, Event, RelayMessage};
 | 
			
		||||
use nostr_database::Profile;
 | 
			
		||||
use std::{collections::BTreeSet, sync::Arc};
 | 
			
		||||
use nostr::{Event, RelayMessage};
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use super::user::Nip05Profile;
 | 
			
		||||
 | 
			
		||||
/// Handle core nostr events
 | 
			
		||||
pub trait Noose: Send + Sync {
 | 
			
		||||
    /// Start event listener
 | 
			
		||||
    async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error>;
 | 
			
		||||
 | 
			
		||||
    /// Save event in the Database
 | 
			
		||||
    async fn write_event(&self, event: Box<Event>) -> Result<RelayMessage, Error>;
 | 
			
		||||
 | 
			
		||||
    /// Find events by subscription
 | 
			
		||||
    async fn find_event(&self, subscription: Subscription) -> Result<Vec<RelayMessage>, Error>;
 | 
			
		||||
 | 
			
		||||
    /// Get event counts by subscription
 | 
			
		||||
    async fn counts(&self, subscription: Subscription) -> Result<RelayMessage, Error>;
 | 
			
		||||
 | 
			
		||||
    /// Get NIP-05 of the registered User by 'username'
 | 
			
		||||
    async fn get_nip05(&self, username: String) -> Result<Nip05Profile, Error>;
 | 
			
		||||
 | 
			
		||||
    /// Create new NIP-05 for the User
 | 
			
		||||
    async fn create_nip05(&self, user: UserBody) -> Result<(), Error>;
 | 
			
		||||
 | 
			
		||||
    /// Get Profile by public key
 | 
			
		||||
    async fn profile(&self, public_key: XOnlyPublicKey) -> Result<Profile, Error>;
 | 
			
		||||
 | 
			
		||||
    /// Add new contact to Profile
 | 
			
		||||
    async fn add_contact(
 | 
			
		||||
        &self,
 | 
			
		||||
        profile_public_key: XOnlyPublicKey,
 | 
			
		||||
        contact_public_key: XOnlyPublicKey,
 | 
			
		||||
    ) -> Result<(), Error>;
 | 
			
		||||
 | 
			
		||||
    /// Remove contact from the Profile
 | 
			
		||||
    async fn remove_contact(
 | 
			
		||||
        &self,
 | 
			
		||||
        profile_public_key: XOnlyPublicKey,
 | 
			
		||||
        contact_public_key: XOnlyPublicKey,
 | 
			
		||||
    ) -> Result<(), Error>;
 | 
			
		||||
 | 
			
		||||
    /// Get Profile contats (pubkeys)
 | 
			
		||||
    async fn contacts_public_keys(
 | 
			
		||||
        &self,
 | 
			
		||||
        public_key: XOnlyPublicKey,
 | 
			
		||||
    ) -> Result<Vec<XOnlyPublicKey>, Error>;
 | 
			
		||||
 | 
			
		||||
    /// Get Profie contacts list
 | 
			
		||||
    async fn contacts(&self, public_key: XOnlyPublicKey) -> Result<BTreeSet<Profile>, Error>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
CREATE TABLE nip05 (
 | 
			
		||||
    pubkey TEXT PRIMARY KEY,
 | 
			
		||||
    username TEXT NOT NULL UNIQUE,
 | 
			
		||||
    relays TEXT,
 | 
			
		||||
    joined_at INTEGER NOT NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX idx_nip05_pubkey ON nip05 (pubkey);
 | 
			
		||||
CREATE INDEX idx_nip05_username ON nip05 (username);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,27 +0,0 @@
 | 
			
		|||
CREATE TABLE profiles (
 | 
			
		||||
    pubkey TEXT PRIMARY KEY
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX idx_profiles_pubkey ON profiles (pubkey);
 | 
			
		||||
 | 
			
		||||
create TABLE contacts (
 | 
			
		||||
    profile TEXT REFERENCES profiles(pubkey) ON DELETE CASCADE,
 | 
			
		||||
    contact TEXT REFERENCES profiles(pubkey) ON DELETE CASCADE
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE metadata (
 | 
			
		||||
    pubkey TEXT REFERENCES profiles(pubkey) ON DELETE CASCADE,
 | 
			
		||||
    name TEXT,
 | 
			
		||||
    display_name TEXT,
 | 
			
		||||
    about TEXT,
 | 
			
		||||
    website TEXT,
 | 
			
		||||
    picture TEXT,
 | 
			
		||||
    banner TEXT,
 | 
			
		||||
    nip05 TEXT,
 | 
			
		||||
    lud06 TEXT,
 | 
			
		||||
    lud16 TEXT,
 | 
			
		||||
    custom TEXT
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX idx_metadata_profiles_pubkey ON metadata (pubkey);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -11,8 +11,7 @@ impl MigrationRunner {
 | 
			
		|||
        let m_events_fts = include_str!("./1697410223576_events_fts.sql");
 | 
			
		||||
        let m_users = include_str!("./1697410294265_users.sql");
 | 
			
		||||
        let m_unattached_media = include_str!("./1697410480767_unattached_media.sql");
 | 
			
		||||
        let m_nip05 = include_str!("./1706575155557_nip05.sql");
 | 
			
		||||
        let m_nip42 = include_str!("./1707327016995_nip42_profile.sql");
 | 
			
		||||
        let m_pragma = include_str!("./1697410424624_pragma.sql");
 | 
			
		||||
 | 
			
		||||
        let migrations = Migrations::new(vec![
 | 
			
		||||
            M::up(m_create_events),
 | 
			
		||||
| 
						 | 
				
			
			@ -21,8 +20,7 @@ impl MigrationRunner {
 | 
			
		|||
            M::up(m_events_fts),
 | 
			
		||||
            M::up(m_users),
 | 
			
		||||
            M::up(m_unattached_media),
 | 
			
		||||
            M::up(m_nip05),
 | 
			
		||||
            M::up(m_nip42),
 | 
			
		||||
            M::up(m_pragma),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        match migrations.to_latest(connection) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,6 @@ mod migrations;
 | 
			
		|||
pub mod pipeline;
 | 
			
		||||
pub mod sled;
 | 
			
		||||
mod sqlite;
 | 
			
		||||
mod sqlite_tables;
 | 
			
		||||
pub mod user;
 | 
			
		||||
 | 
			
		||||
pub fn start(context: Context) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,7 @@ impl Pipeline {
 | 
			
		|||
            let channel;
 | 
			
		||||
            let command = match message.content {
 | 
			
		||||
                Command::PipelineReqEvent(client_id, event) => {
 | 
			
		||||
                    match self.handle_event(client_id, event).await {
 | 
			
		||||
                    match self.handle_event(client_id, event.clone()).await {
 | 
			
		||||
                        Ok(_) => {
 | 
			
		||||
                            log::info!("[Pipeline] handle_event completed");
 | 
			
		||||
                            channel = message.source;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,27 +1,197 @@
 | 
			
		|||
use super::{
 | 
			
		||||
    db::Noose,
 | 
			
		||||
    migrations::MigrationRunner,
 | 
			
		||||
    user::{Nip05Profile, Nip05Table, UserRow},
 | 
			
		||||
};
 | 
			
		||||
use super::{db::Noose, migrations::MigrationRunner};
 | 
			
		||||
use crate::{
 | 
			
		||||
    bussy::{channels, Command, Message, PubSub},
 | 
			
		||||
    usernames::dto::UserBody,
 | 
			
		||||
    utils::{config::Config as ServiceConfig, error::Error, structs::Subscription},
 | 
			
		||||
};
 | 
			
		||||
use async_trait::async_trait;
 | 
			
		||||
use deadpool_sqlite::{Config, Object, Pool, Runtime};
 | 
			
		||||
use nostr::{
 | 
			
		||||
    nips::nip01::Coordinate, secp256k1::XOnlyPublicKey, Event, EventId, Filter, JsonUtil,
 | 
			
		||||
    RelayMessage, TagKind, Timestamp, Url,
 | 
			
		||||
    nips::nip01::Coordinate, Event, EventId, Filter, RelayMessage, TagKind, Timestamp, Url,
 | 
			
		||||
};
 | 
			
		||||
use nostr_database::{Backend, DatabaseOptions, NostrDatabase, Order};
 | 
			
		||||
 | 
			
		||||
use rusqlite::Row;
 | 
			
		||||
use sea_query::{extension::sqlite::SqliteExpr, Order as SqOrder, Query, SqliteQueryBuilder};
 | 
			
		||||
use sea_query_rusqlite::RusqliteBinder;
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use std::{collections::HashSet, str::FromStr};
 | 
			
		||||
 | 
			
		||||
use crate::noose::sqlite_tables::*;
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
struct EventRow {
 | 
			
		||||
    id: String,
 | 
			
		||||
    pubkey: String,
 | 
			
		||||
    created_at: i64,
 | 
			
		||||
    kind: i64,
 | 
			
		||||
    tags: String,
 | 
			
		||||
    sig: String,
 | 
			
		||||
    content: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&Row<'_>> for EventRow {
 | 
			
		||||
    fn from(row: &Row) -> Self {
 | 
			
		||||
        let id: String = row.get("id").unwrap();
 | 
			
		||||
        let pubkey: String = row.get("pubkey").unwrap();
 | 
			
		||||
        let created_at: i64 = row.get("created_at").unwrap();
 | 
			
		||||
        let kind: i64 = row.get("kind").unwrap();
 | 
			
		||||
        let tags: String = row.get("tags").unwrap();
 | 
			
		||||
        let sig: String = row.get("sig").unwrap();
 | 
			
		||||
        let content: String = row.get("content").unwrap();
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            id,
 | 
			
		||||
            pubkey,
 | 
			
		||||
            created_at,
 | 
			
		||||
            kind,
 | 
			
		||||
            tags,
 | 
			
		||||
            sig,
 | 
			
		||||
            content,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&EventRow> for Event {
 | 
			
		||||
    fn from(row: &EventRow) -> Self {
 | 
			
		||||
        row.to_event()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EventRow {
 | 
			
		||||
    pub fn to_event(&self) -> Event {
 | 
			
		||||
        let tags: Vec<Vec<String>> = serde_json::from_str(&self.tags).unwrap();
 | 
			
		||||
 | 
			
		||||
        let event_json = serde_json::json!(
 | 
			
		||||
            {
 | 
			
		||||
                "id": self.id,
 | 
			
		||||
                "content": self.content,
 | 
			
		||||
                "created_at": self.created_at,
 | 
			
		||||
                "kind": self.kind,
 | 
			
		||||
                "pubkey": self.pubkey,
 | 
			
		||||
                "sig": self.sig,
 | 
			
		||||
                "tags": tags
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Event::from_value(event_json).unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// enum DeletedCoordinatesTable {
 | 
			
		||||
//     Table,
 | 
			
		||||
//     Coordinate,
 | 
			
		||||
//     CreatedAt,
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// impl sea_query::Iden for DeletedCoordinatesTable {
 | 
			
		||||
//     fn unquoted(&self, s: &mut dyn std::fmt::Write) {
 | 
			
		||||
//         write!(
 | 
			
		||||
//             s,
 | 
			
		||||
//             "{}",
 | 
			
		||||
//             match self {
 | 
			
		||||
//                 Self::Table => "deleted_coordinates",
 | 
			
		||||
//                 Self::Coordinate => "coordinate",
 | 
			
		||||
//                 Self::CreatedAt => "created_at",
 | 
			
		||||
//             }
 | 
			
		||||
//         )
 | 
			
		||||
//         .unwrap()
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
enum EventSeenByRelaysTable {
 | 
			
		||||
    Table,
 | 
			
		||||
    Id,
 | 
			
		||||
    EventId,
 | 
			
		||||
    RelayURL,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl sea_query::Iden for EventSeenByRelaysTable {
 | 
			
		||||
    fn unquoted(&self, s: &mut dyn std::fmt::Write) {
 | 
			
		||||
        write!(
 | 
			
		||||
            s,
 | 
			
		||||
            "{}",
 | 
			
		||||
            match self {
 | 
			
		||||
                Self::Table => "event_seen_by_relays",
 | 
			
		||||
                Self::Id => "id",
 | 
			
		||||
                Self::EventId => "event_id",
 | 
			
		||||
                Self::RelayURL => "relay_url",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct NostrSqlite {
 | 
			
		||||
| 
						 | 
				
			
			@ -42,22 +212,7 @@ impl NostrSqlite {
 | 
			
		|||
 | 
			
		||||
    async fn run_migrations(pool: &Pool) -> bool {
 | 
			
		||||
        let connection = pool.get().await.unwrap();
 | 
			
		||||
        connection.interact(MigrationRunner::up).await.unwrap();
 | 
			
		||||
        connection
 | 
			
		||||
            .interact(|conn| {
 | 
			
		||||
                conn.pragma_update(None, "encoding", "UTF-8").unwrap();
 | 
			
		||||
                conn.pragma_update(None, "journal_mode", "WAL").unwrap();
 | 
			
		||||
                conn.pragma_update(None, "foreign_keys", "ON").unwrap();
 | 
			
		||||
                conn.pragma_update(None, "auto_vacuum", "FULL").unwrap();
 | 
			
		||||
                conn.pragma_update(None, "journal_size_limit", "32768")
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                conn.pragma_update(None, "mmap_size", "17179869184")
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
            })
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        true
 | 
			
		||||
        connection.interact(MigrationRunner::up).await.unwrap()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn get_connection(&self) -> Result<Object, Error> {
 | 
			
		||||
| 
						 | 
				
			
			@ -341,159 +496,86 @@ impl NostrSqlite {
 | 
			
		|||
 | 
			
		||||
                        return Ok(false);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    match tx.commit() {
 | 
			
		||||
                        Ok(_) => return Ok(true),
 | 
			
		||||
                        Err(err) => {
 | 
			
		||||
                            log::error!("Error during transaction commit: {}", err);
 | 
			
		||||
                            return Ok(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if event.kind() == nostr::Kind::Metadata {
 | 
			
		||||
                    // Try to delete old profile first
 | 
			
		||||
                    let (sql, value) = Query::delete()
 | 
			
		||||
                        .from_table(ProfilesTable::Table)
 | 
			
		||||
                        .and_where(sea_query::Expr::col(ProfilesTable::Pubkey).eq(pubkey.clone()))
 | 
			
		||||
                        .build_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
                    
 | 
			
		||||
                    if let Err(err) = tx.execute(sql.as_str(), &*value.as_params()) {
 | 
			
		||||
                        tx.rollback().unwrap();
 | 
			
		||||
 | 
			
		||||
                        log::debug!("Failed to delete old Profile record: {}", err);
 | 
			
		||||
                        return Ok(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    let (sql, value) = Query::insert()
 | 
			
		||||
                        .into_table(ProfilesTable::Table)
 | 
			
		||||
                        .columns([ProfilesTable::Pubkey])
 | 
			
		||||
                        .values_panic([pubkey.clone().into()])
 | 
			
		||||
                        .build_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                    if let Err(err) = tx.execute(sql.as_str(), &*value.as_params()) {
 | 
			
		||||
                        tx.rollback().unwrap();
 | 
			
		||||
                        
 | 
			
		||||
                        log::debug!("Failed to store Profile");
 | 
			
		||||
                        return Ok(false);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    let Ok(metadata) = nostr::Metadata::from_json(content.clone()) else {
 | 
			
		||||
                        log::debug!("Failed to parse metadata");
 | 
			
		||||
                        return Err(Error::bad_request(
 | 
			
		||||
                            "Unable to parse metadata from 'content'",
 | 
			
		||||
                        ));
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    let metadata_custom_fields = serde_json::to_string(&metadata.custom);
 | 
			
		||||
 | 
			
		||||
                    let (sql, value) = Query::insert()
 | 
			
		||||
                        .into_table(MetadataTable::Table)
 | 
			
		||||
                } else {
 | 
			
		||||
                    log::debug!("inserting new event in events");
 | 
			
		||||
                    // Insert into Events table
 | 
			
		||||
                    let (sql, values) = Query::insert()
 | 
			
		||||
                        .into_table(EventsTable::Table)
 | 
			
		||||
                        .columns([
 | 
			
		||||
                            MetadataTable::Pubkey,
 | 
			
		||||
                            MetadataTable::Name,
 | 
			
		||||
                            MetadataTable::DisplayName,
 | 
			
		||||
                            MetadataTable::About,
 | 
			
		||||
                            MetadataTable::Website,
 | 
			
		||||
                            MetadataTable::Picture,
 | 
			
		||||
                            MetadataTable::Banner,
 | 
			
		||||
                            MetadataTable::Nip05,
 | 
			
		||||
                            MetadataTable::Lud06,
 | 
			
		||||
                            MetadataTable::Lud16,
 | 
			
		||||
                            MetadataTable::Custom,
 | 
			
		||||
                            EventsTable::EventId,
 | 
			
		||||
                            EventsTable::Content,
 | 
			
		||||
                            EventsTable::Kind,
 | 
			
		||||
                            EventsTable::Pubkey,
 | 
			
		||||
                            EventsTable::CreatedAt,
 | 
			
		||||
                            EventsTable::Tags,
 | 
			
		||||
                            EventsTable::Sig,
 | 
			
		||||
                        ])
 | 
			
		||||
                        .values_panic([
 | 
			
		||||
                            pubkey.clone().into(),
 | 
			
		||||
                            metadata.name.unwrap_or_default().into(),
 | 
			
		||||
                            metadata.display_name.unwrap_or_default().into(),
 | 
			
		||||
                            metadata.about.unwrap_or_default().into(),
 | 
			
		||||
                            metadata.website.unwrap_or_default().into(),
 | 
			
		||||
                            metadata.picture.unwrap_or_default().into(),
 | 
			
		||||
                            metadata.banner.unwrap_or_default().into(),
 | 
			
		||||
                            metadata.nip05.unwrap_or_default().into(),
 | 
			
		||||
                            metadata.lud06.unwrap_or_default().into(),
 | 
			
		||||
                            metadata.lud16.unwrap_or_default().into(),
 | 
			
		||||
                            metadata_custom_fields.unwrap_or_default().into(),
 | 
			
		||||
                            id.clone().into(),
 | 
			
		||||
                            content.clone().into(),
 | 
			
		||||
                            kind.into(),
 | 
			
		||||
                            pubkey.into(),
 | 
			
		||||
                            created_at.into(),
 | 
			
		||||
                            tags.into(),
 | 
			
		||||
                            sig.into(),
 | 
			
		||||
                        ])
 | 
			
		||||
                        .build_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                    if let Err(err) = tx.execute(sql.as_str(), &*value.as_params()) {
 | 
			
		||||
                    if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
 | 
			
		||||
                        log::error!("Error inserting event into 'events' table: {}", err);
 | 
			
		||||
                        tx.rollback().unwrap();
 | 
			
		||||
 | 
			
		||||
                        return Ok(false);
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    // Insert into EventsFTS table
 | 
			
		||||
                    log::debug!("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_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                    if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
 | 
			
		||||
                        log::error!("Error inserting event into 'eventsFTS' table: {}", err);
 | 
			
		||||
                        tx.rollback().unwrap();
 | 
			
		||||
 | 
			
		||||
                        log::debug!("Failed to store Metadata: {}", err);
 | 
			
		||||
                        return Ok(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                log::debug!("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_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
 | 
			
		||||
                    log::error!("Error inserting event into 'events' table: {}", err);
 | 
			
		||||
                    tx.rollback().unwrap();
 | 
			
		||||
                    // Insert into Tags table
 | 
			
		||||
                    log::debug!("inserting new event into tags");
 | 
			
		||||
                    for tag in event.tags.clone() {
 | 
			
		||||
                        if Self::tag_is_indexable(&tag) {
 | 
			
		||||
                            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_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                    return Ok(false);
 | 
			
		||||
                };
 | 
			
		||||
                                    if let Err(err) = tx.execute(sql.as_str(), &*values.as_params())
 | 
			
		||||
                                    {
 | 
			
		||||
                                        log::error!(
 | 
			
		||||
                                            "Error inserting event into 'tags' table: {}",
 | 
			
		||||
                                            err
 | 
			
		||||
                                        );
 | 
			
		||||
                                        tx.rollback().unwrap();
 | 
			
		||||
 | 
			
		||||
                // Insert into EventsFTS table
 | 
			
		||||
                log::debug!("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_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
 | 
			
		||||
                    log::error!("Error inserting event into 'eventsFTS' table: {}", err);
 | 
			
		||||
                    tx.rollback().unwrap();
 | 
			
		||||
 | 
			
		||||
                    return Ok(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Insert into Tags table
 | 
			
		||||
                log::debug!("inserting new event into tags");
 | 
			
		||||
                for tag in event.tags.clone() {
 | 
			
		||||
                    if Self::tag_is_indexable(&tag) {
 | 
			
		||||
                        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_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                                if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
 | 
			
		||||
                                    log::error!("Error inserting event into 'tags' table: {}", err);
 | 
			
		||||
                                    tx.rollback().unwrap();
 | 
			
		||||
 | 
			
		||||
                                    return Ok(false);
 | 
			
		||||
                                        return Ok(false);
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
| 
						 | 
				
			
			@ -762,12 +844,6 @@ impl NostrSqlite {
 | 
			
		|||
 | 
			
		||||
                let event = row.clone().to_event();
 | 
			
		||||
 | 
			
		||||
                if event.is_expired() {
 | 
			
		||||
                    return Err(Error::internal_with_message(
 | 
			
		||||
                        "Event has expired. Ignoring...",
 | 
			
		||||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Ok(event)
 | 
			
		||||
            })
 | 
			
		||||
            .await
 | 
			
		||||
| 
						 | 
				
			
			@ -983,9 +1059,7 @@ impl NostrSqlite {
 | 
			
		|||
                let mut event_vec: Vec<Event> = vec![];
 | 
			
		||||
                while let Ok(Some(row)) = rows.next() {
 | 
			
		||||
                    let event = EventRow::from(row).to_event();
 | 
			
		||||
                    if !event.is_expired() {
 | 
			
		||||
                        event_vec.push(event);
 | 
			
		||||
                    }
 | 
			
		||||
                    event_vec.push(event);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Ok(event_vec)
 | 
			
		||||
| 
						 | 
				
			
			@ -1060,6 +1134,8 @@ impl NostrSqlite {
 | 
			
		|||
        let Ok(query_result) = connection
 | 
			
		||||
            .interact(move |conn| {
 | 
			
		||||
                let (sql, values) = sql_statement
 | 
			
		||||
                    .clear_selects()
 | 
			
		||||
                    .column(EventsTable::EventId)
 | 
			
		||||
                    .order_by(EventsTable::CreatedAt, sq_order.to_owned())
 | 
			
		||||
                    .build_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1068,10 +1144,9 @@ impl NostrSqlite {
 | 
			
		|||
 | 
			
		||||
                let mut event_vec: Vec<EventId> = vec![];
 | 
			
		||||
                while let Ok(Some(row)) = rows.next() {
 | 
			
		||||
                    let event = EventRow::from(row).to_event();
 | 
			
		||||
                    if !event.is_expired() {
 | 
			
		||||
                        event_vec.push(event.id);
 | 
			
		||||
                    }
 | 
			
		||||
                    let event_id_string: String = row.get(0).unwrap();
 | 
			
		||||
                    let event_id = EventId::from_str(&event_id_string).unwrap();
 | 
			
		||||
                    event_vec.push(event_id);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Ok(event_vec)
 | 
			
		||||
| 
						 | 
				
			
			@ -1103,37 +1178,33 @@ impl NostrSqlite {
 | 
			
		|||
        };
 | 
			
		||||
        
 | 
			
		||||
        let Ok(query_result) = connection
 | 
			
		||||
            .interact(
 | 
			
		||||
                move |conn: &mut rusqlite::Connection| -> Result<bool, Error> {
 | 
			
		||||
                    let (sql, value) = Query::select()
 | 
			
		||||
                        .from(EventsTable::Table)
 | 
			
		||||
                        .columns([EventsTable::EventId, EventsTable::CreatedAt])
 | 
			
		||||
                        .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("a"))
 | 
			
		||||
                        .and_where(
 | 
			
		||||
                            sea_query::Expr::col((TagsTable::Table, TagsTable::Value)).eq(ident),
 | 
			
		||||
                        )
 | 
			
		||||
                        .and_where(
 | 
			
		||||
                            sea_query::Expr::col((EventsTable::Table, EventsTable::CreatedAt))
 | 
			
		||||
                                .gte(timestamp.as_i64()),
 | 
			
		||||
                        )
 | 
			
		||||
                        .limit(1)
 | 
			
		||||
                        .build_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
            .interact(move |conn: &mut rusqlite::Connection| -> Result<bool, Error> {
 | 
			
		||||
                let (sql, value) = Query::select()
 | 
			
		||||
                    .from(EventsTable::Table)
 | 
			
		||||
                    .columns([EventsTable::EventId, EventsTable::CreatedAt])
 | 
			
		||||
                    .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("a"))
 | 
			
		||||
                    .and_where(sea_query::Expr::col((TagsTable::Table, TagsTable::Value)).eq(ident))
 | 
			
		||||
                    .and_where(
 | 
			
		||||
                        sea_query::Expr::col((EventsTable::Table, EventsTable::CreatedAt))
 | 
			
		||||
                            .gte(timestamp.as_i64()),
 | 
			
		||||
                    )
 | 
			
		||||
                    .limit(1)
 | 
			
		||||
                    .build_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                    let mut stmt = conn.prepare(sql.as_str()).unwrap();
 | 
			
		||||
                    let mut rows = stmt.query(&*value.as_params()).unwrap();
 | 
			
		||||
                let mut stmt = conn.prepare(sql.as_str()).unwrap();
 | 
			
		||||
                let mut rows = stmt.query(&*value.as_params()).unwrap();
 | 
			
		||||
 | 
			
		||||
                    if let Ok(Some(record)) = rows.next() {
 | 
			
		||||
                        return Ok(false);
 | 
			
		||||
                    }
 | 
			
		||||
                if let Ok(Some(record)) = rows.next() {
 | 
			
		||||
                    return Ok(false)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                    Ok(true)
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            })
 | 
			
		||||
            .await
 | 
			
		||||
        else {
 | 
			
		||||
            return Err(Error::internal_with_message(
 | 
			
		||||
| 
						 | 
				
			
			@ -1143,147 +1214,6 @@ impl NostrSqlite {
 | 
			
		|||
 | 
			
		||||
        query_result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn get_nip05_profile(&self, username: String) -> Result<Nip05Profile, Error> {
 | 
			
		||||
        let Ok(connection) = self.get_connection().await else {
 | 
			
		||||
            return Err(Error::internal_with_message("Unable to get DB connection"));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let Ok(query_result) = connection
 | 
			
		||||
            .interact(
 | 
			
		||||
                move |conn: &mut rusqlite::Connection| -> Result<Nip05Profile, Error> {
 | 
			
		||||
                    let (sql, value) = Query::select()
 | 
			
		||||
                        .from(Nip05Table::Table)
 | 
			
		||||
                        .columns([
 | 
			
		||||
                            Nip05Table::Pubkey,
 | 
			
		||||
                            Nip05Table::Username,
 | 
			
		||||
                            Nip05Table::Relays,
 | 
			
		||||
                            Nip05Table::JoinedAt,
 | 
			
		||||
                        ])
 | 
			
		||||
                        .and_where(sea_query::Expr::col(Nip05Table::Username).eq(&username))
 | 
			
		||||
                        .limit(1)
 | 
			
		||||
                        .build_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                    let Ok(res) = conn.query_row(sql.as_str(), &*value.as_params(), |row| {
 | 
			
		||||
                        let nip05_row: UserRow = row.into();
 | 
			
		||||
                        let nip05 = Nip05Profile::from(nip05_row);
 | 
			
		||||
 | 
			
		||||
                        Ok(nip05)
 | 
			
		||||
                    }) else {
 | 
			
		||||
                        return Err(Error::not_found("user", username));
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    Ok(res)
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            .await
 | 
			
		||||
        else {
 | 
			
		||||
            return Err(Error::internal_with_message(
 | 
			
		||||
                "Failed to execute query 'get_nip05_profile'",
 | 
			
		||||
            ));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        query_result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn get_profile(
 | 
			
		||||
        &self,
 | 
			
		||||
        public_key: XOnlyPublicKey,
 | 
			
		||||
    ) -> Result<nostr_database::Profile, Error> {
 | 
			
		||||
        let pk = public_key.to_string();
 | 
			
		||||
 | 
			
		||||
        let Ok(connection) = self.get_connection().await else {
 | 
			
		||||
            return Err(Error::internal_with_message("Unable to get DB connection"));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let Ok(query_result) = connection
 | 
			
		||||
            .interact(
 | 
			
		||||
                move |conn: &mut rusqlite::Connection| -> Result<nostr_database::Profile, Error> {
 | 
			
		||||
                    let (sql, value) = Query::select()
 | 
			
		||||
                        .from(MetadataTable::Table)
 | 
			
		||||
                        .columns([
 | 
			
		||||
                            MetadataTable::Pubkey,
 | 
			
		||||
                            MetadataTable::Name,
 | 
			
		||||
                            MetadataTable::DisplayName,
 | 
			
		||||
                            MetadataTable::About,
 | 
			
		||||
                            MetadataTable::Website,
 | 
			
		||||
                            MetadataTable::Picture,
 | 
			
		||||
                            MetadataTable::Banner,
 | 
			
		||||
                            MetadataTable::Nip05,
 | 
			
		||||
                            MetadataTable::Lud06,
 | 
			
		||||
                            MetadataTable::Lud16,
 | 
			
		||||
                            MetadataTable::Custom,
 | 
			
		||||
                        ])
 | 
			
		||||
                        .and_where(sea_query::Expr::col(MetadataTable::Pubkey).eq(&pk))
 | 
			
		||||
                        .limit(1)
 | 
			
		||||
                        .build_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                    let Ok(res) = conn.query_row(sql.as_str(), &*value.as_params(), |row| {
 | 
			
		||||
                        let profile_row: ProfileRow = row.into();
 | 
			
		||||
                        let profile = nostr_database::Profile::from(&profile_row);
 | 
			
		||||
 | 
			
		||||
                        Ok(profile)
 | 
			
		||||
                    }) else {
 | 
			
		||||
                        return Err(Error::not_found("user", pk));
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    Ok(res)
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            .await
 | 
			
		||||
        else {
 | 
			
		||||
            return Err(Error::internal_with_message(
 | 
			
		||||
                "Failed to execute query 'get_profile'",
 | 
			
		||||
            ));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        query_result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn create_nip05_profile(&self, user: UserBody) -> Result<(), Error> {
 | 
			
		||||
        let Ok(connection) = self.get_connection().await else {
 | 
			
		||||
            return Err(Error::internal_with_message("Unable to get DB connection"));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let Ok(query_result) = connection
 | 
			
		||||
            .interact(
 | 
			
		||||
                move |conn: &mut rusqlite::Connection| -> Result<(), Error> {
 | 
			
		||||
                    let (sql, values) = Query::insert()
 | 
			
		||||
                        .into_table(Nip05Table::Table)
 | 
			
		||||
                        .columns([
 | 
			
		||||
                            Nip05Table::Username,
 | 
			
		||||
                            Nip05Table::Pubkey,
 | 
			
		||||
                            Nip05Table::Relays,
 | 
			
		||||
                            Nip05Table::JoinedAt,
 | 
			
		||||
                        ])
 | 
			
		||||
                        .values_panic([
 | 
			
		||||
                            user.name.clone().into(),
 | 
			
		||||
                            user.get_pubkey().into(),
 | 
			
		||||
                            serde_json::to_string(&user.relays).unwrap().into(),
 | 
			
		||||
                            nostr::Timestamp::now().as_i64().into(),
 | 
			
		||||
                        ])
 | 
			
		||||
                        .build_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                    match conn.execute(sql.as_str(), &*values.as_params()) {
 | 
			
		||||
                        Ok(_) => Ok(()),
 | 
			
		||||
                        Err(e) => {
 | 
			
		||||
                            if e.to_string().contains("nip05.pubkey") {
 | 
			
		||||
                                return Err(Error::invalid_request_body("pubkey already taken"));
 | 
			
		||||
                            }
 | 
			
		||||
                            Err(Error::invalid_request_body("name already taken"))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            .await
 | 
			
		||||
        else {
 | 
			
		||||
            return Err(Error::internal_with_message(
 | 
			
		||||
                "Failed to execute query 'get_nip05_profile'",
 | 
			
		||||
            ));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        query_result
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<nostr_database::DatabaseError> for Error {
 | 
			
		||||
| 
						 | 
				
			
			@ -1342,8 +1272,7 @@ impl NostrDatabase for NostrSqlite {
 | 
			
		|||
        coordinate: &Coordinate,
 | 
			
		||||
        timestamp: Timestamp,
 | 
			
		||||
    ) -> Result<bool, Self::Err> {
 | 
			
		||||
        self.has_coordinate_been_deleted(coordinate, timestamp)
 | 
			
		||||
            .await
 | 
			
		||||
        self.has_coordinate_been_deleted(coordinate, timestamp).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Set [`EventId`] as seen by relay
 | 
			
		||||
| 
						 | 
				
			
			@ -1410,7 +1339,6 @@ impl Noose for NostrSqlite {
 | 
			
		|||
        while let Ok(message) = subscriber.recv().await {
 | 
			
		||||
            log::info!("[Noose] received message: {:?}", message);
 | 
			
		||||
            let command = match message.content {
 | 
			
		||||
                // Relay Events
 | 
			
		||||
                Command::DbReqWriteEvent(client_id, event) => match self.write_event(event).await {
 | 
			
		||||
                    Ok(status) => Command::DbResOkWithStatus(client_id, status),
 | 
			
		||||
                    Err(e) => Command::ServiceError(e),
 | 
			
		||||
| 
						 | 
				
			
			@ -1435,44 +1363,6 @@ impl Noose for NostrSqlite {
 | 
			
		|||
                        Err(e) => Command::ServiceError(e),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                // NIP-05
 | 
			
		||||
                Command::DbReqGetNIP05(username) => match self.get_nip05(username).await {
 | 
			
		||||
                    Ok(user) => Command::DbResNIP05(user),
 | 
			
		||||
                    Err(e) => Command::ServiceError(e),
 | 
			
		||||
                },
 | 
			
		||||
                Command::DbReqCreateNIP05(user) => match self.create_nip05(user).await {
 | 
			
		||||
                    Ok(_) => Command::DbResOk,
 | 
			
		||||
                    Err(e) => Command::ServiceError(e),
 | 
			
		||||
                },
 | 
			
		||||
                // NIP-42
 | 
			
		||||
                Command::DbReqGetProfile(pubkey) => match self.profile(pubkey).await {
 | 
			
		||||
                    Ok(profile) => Command::DbResGetProfile(profile),
 | 
			
		||||
                    Err(e) => Command::ServiceError(e),
 | 
			
		||||
                },
 | 
			
		||||
                Command::DbReqAddContact(profile_pub_key, contact_pub_key) => {
 | 
			
		||||
                    match self.add_contact(profile_pub_key, contact_pub_key).await {
 | 
			
		||||
                        Ok(_) => Command::DbResOk,
 | 
			
		||||
                        Err(e) => Command::ServiceError(e),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Command::DbReqRemoveContact(profile_pub_key, contact_pub_key) => {
 | 
			
		||||
                    match self.remove_contact(profile_pub_key, contact_pub_key).await {
 | 
			
		||||
                        Ok(_) => Command::DbResOk,
 | 
			
		||||
                        Err(e) => Command::ServiceError(e),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Command::DbReqGetContactPubkeys(profile_pub_key) => {
 | 
			
		||||
                    match self.contacts_public_keys(profile_pub_key).await {
 | 
			
		||||
                        Ok(contact_pubkeys) => Command::DbResGetContactPubkeys(contact_pubkeys),
 | 
			
		||||
                        Err(e) => Command::ServiceError(e),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Command::DbReqGetContacts(profile_pub_key) => {
 | 
			
		||||
                    match self.contacts(profile_pub_key).await {
 | 
			
		||||
                        Ok(contacts) => Command::DbResGetContacts(contacts),
 | 
			
		||||
                        Err(e) => Command::ServiceError(e),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                _ => Command::Noop,
 | 
			
		||||
            };
 | 
			
		||||
            if command != Command::Noop {
 | 
			
		||||
| 
						 | 
				
			
			@ -1496,6 +1386,7 @@ impl Noose for NostrSqlite {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    async fn write_event(&self, event: Box<nostr::Event>) -> Result<nostr::RelayMessage, Error> {
 | 
			
		||||
        // TODO: Maybe do event validation and admin deletions here
 | 
			
		||||
        match self.save_event(&event).await {
 | 
			
		||||
            Ok(status) => {
 | 
			
		||||
                let relay_message = nostr::RelayMessage::ok(event.id, status, "");
 | 
			
		||||
| 
						 | 
				
			
			@ -1536,57 +1427,11 @@ impl Noose for NostrSqlite {
 | 
			
		|||
            Err(err) => Err(Error::internal_with_message(err.to_string())),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn get_nip05(&self, username: String) -> Result<Nip05Profile, Error> {
 | 
			
		||||
        self.get_nip05_profile(username).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn create_nip05(&self, user: UserBody) -> Result<(), Error> {
 | 
			
		||||
        self.create_nip05_profile(user).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn profile(
 | 
			
		||||
        &self,
 | 
			
		||||
        public_key: nostr::prelude::XOnlyPublicKey,
 | 
			
		||||
    ) -> Result<nostr_database::Profile, Error> {
 | 
			
		||||
        self.get_profile(public_key).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn add_contact(
 | 
			
		||||
        &self,
 | 
			
		||||
        profile_public_key: nostr::prelude::XOnlyPublicKey,
 | 
			
		||||
        contact_public_key: nostr::prelude::XOnlyPublicKey,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        todo!()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn remove_contact(
 | 
			
		||||
        &self,
 | 
			
		||||
        profile_public_key: nostr::prelude::XOnlyPublicKey,
 | 
			
		||||
        contact_public_key: nostr::prelude::XOnlyPublicKey,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        todo!()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn contacts_public_keys(
 | 
			
		||||
        &self,
 | 
			
		||||
        public_key: nostr::prelude::XOnlyPublicKey,
 | 
			
		||||
    ) -> Result<Vec<nostr::prelude::XOnlyPublicKey>, Error> {
 | 
			
		||||
        todo!()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn contacts(
 | 
			
		||||
        &self,
 | 
			
		||||
        public_key: nostr::prelude::XOnlyPublicKey,
 | 
			
		||||
    ) -> Result<std::collections::BTreeSet<nostr_database::Profile>, Error> {
 | 
			
		||||
        todo!()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use crate::noose::sqlite::*;
 | 
			
		||||
    use nostr::key::FromSkStr;
 | 
			
		||||
    use nostr::EventBuilder;
 | 
			
		||||
 | 
			
		||||
    #[tokio::test]
 | 
			
		||||
| 
						 | 
				
			
			@ -1811,29 +1656,6 @@ mod tests {
 | 
			
		|||
        assert_eq!(result.len(), 1)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test]
 | 
			
		||||
    async fn save_metadata_event() {
 | 
			
		||||
        let config = Arc::new(ServiceConfig::new());
 | 
			
		||||
        let db = NostrSqlite::new(config).await;
 | 
			
		||||
        let secret_key = "nsec1g24e83hwj5gxl0hqxx9wllwcg9rrxthssv0mrxf4dv3lt8dc29yqrxf09p";
 | 
			
		||||
        let keys = nostr::Keys::from_sk_str(secret_key).unwrap();
 | 
			
		||||
        // Insert
 | 
			
		||||
        let metadata = nostr::Metadata::new()
 | 
			
		||||
            .name("Chuck")
 | 
			
		||||
            .custom_field("feed", "seed");
 | 
			
		||||
        let event = nostr::EventBuilder::metadata(&metadata)
 | 
			
		||||
            .to_event(&keys)
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        let result = db.save_event(&event).await.unwrap();
 | 
			
		||||
 | 
			
		||||
        // Find profile by pk
 | 
			
		||||
        let pubkey = keys.public_key();
 | 
			
		||||
        let res = db.get_profile(pubkey).await.unwrap();
 | 
			
		||||
 | 
			
		||||
        assert_eq!(res.name(), "Sneed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test]
 | 
			
		||||
    async fn save_event_with_a_tag() {
 | 
			
		||||
        let config = Arc::new(ServiceConfig::new());
 | 
			
		||||
| 
						 | 
				
			
			@ -1874,14 +1696,4 @@ mod tests {
 | 
			
		|||
 | 
			
		||||
        dbg!(res);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // #[tokio::test]
 | 
			
		||||
    // async fn get_nip05() {
 | 
			
		||||
    //     let config = Arc::new(ServiceConfig::new());
 | 
			
		||||
    //     let db = NostrSqlite::new(config).await;
 | 
			
		||||
 | 
			
		||||
    //     let res = db.get_nip05("test".to_string()).await.unwrap();
 | 
			
		||||
 | 
			
		||||
    //     dbg!(serde_json::to_value(res).unwrap().to_string());
 | 
			
		||||
    // }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,322 +0,0 @@
 | 
			
		|||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
use nostr::{key::FromPkStr, Event, Metadata};
 | 
			
		||||
use nostr_database::Profile;
 | 
			
		||||
use rusqlite::Row;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct EventRow {
 | 
			
		||||
    pub id: String,
 | 
			
		||||
    pub pubkey: String,
 | 
			
		||||
    pub created_at: i64,
 | 
			
		||||
    pub kind: i64,
 | 
			
		||||
    pub tags: String,
 | 
			
		||||
    pub sig: String,
 | 
			
		||||
    pub content: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&Row<'_>> for EventRow {
 | 
			
		||||
    fn from(row: &Row) -> Self {
 | 
			
		||||
        let id: String = row.get("id").unwrap();
 | 
			
		||||
        let pubkey: String = row.get("pubkey").unwrap();
 | 
			
		||||
        let created_at: i64 = row.get("created_at").unwrap();
 | 
			
		||||
        let kind: i64 = row.get("kind").unwrap();
 | 
			
		||||
        let tags: String = row.get("tags").unwrap();
 | 
			
		||||
        let sig: String = row.get("sig").unwrap();
 | 
			
		||||
        let content: String = row.get("content").unwrap();
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            id,
 | 
			
		||||
            pubkey,
 | 
			
		||||
            created_at,
 | 
			
		||||
            kind,
 | 
			
		||||
            tags,
 | 
			
		||||
            sig,
 | 
			
		||||
            content,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&EventRow> for Event {
 | 
			
		||||
    fn from(row: &EventRow) -> Self {
 | 
			
		||||
        row.to_event()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EventRow {
 | 
			
		||||
    pub fn to_event(&self) -> Event {
 | 
			
		||||
        let tags: Vec<Vec<String>> = serde_json::from_str(&self.tags).unwrap();
 | 
			
		||||
 | 
			
		||||
        let event_json = serde_json::json!(
 | 
			
		||||
            {
 | 
			
		||||
                "id": self.id,
 | 
			
		||||
                "content": self.content,
 | 
			
		||||
                "created_at": self.created_at,
 | 
			
		||||
                "kind": self.kind,
 | 
			
		||||
                "pubkey": self.pubkey,
 | 
			
		||||
                "sig": self.sig,
 | 
			
		||||
                "tags": tags
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Event::from_value(event_json).unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub 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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub 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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub 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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum EventSeenByRelaysTable {
 | 
			
		||||
    Table,
 | 
			
		||||
    Id,
 | 
			
		||||
    EventId,
 | 
			
		||||
    RelayURL,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl sea_query::Iden for EventSeenByRelaysTable {
 | 
			
		||||
    fn unquoted(&self, s: &mut dyn std::fmt::Write) {
 | 
			
		||||
        write!(
 | 
			
		||||
            s,
 | 
			
		||||
            "{}",
 | 
			
		||||
            match self {
 | 
			
		||||
                Self::Table => "event_seen_by_relays",
 | 
			
		||||
                Self::Id => "id",
 | 
			
		||||
                Self::EventId => "event_id",
 | 
			
		||||
                Self::RelayURL => "relay_url",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum ProfilesTable {
 | 
			
		||||
    Table,
 | 
			
		||||
    Pubkey,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl sea_query::Iden for ProfilesTable {
 | 
			
		||||
    fn unquoted(&self, s: &mut dyn std::fmt::Write) {
 | 
			
		||||
        write!(
 | 
			
		||||
            s,
 | 
			
		||||
            "{}",
 | 
			
		||||
            match self {
 | 
			
		||||
                Self::Table => "profiles",
 | 
			
		||||
                Self::Pubkey => "pubkey",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum ContactsTable {
 | 
			
		||||
    Table,
 | 
			
		||||
    Profile,
 | 
			
		||||
    Contact,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl sea_query::Iden for ContactsTable {
 | 
			
		||||
    fn unquoted(&self, s: &mut dyn std::fmt::Write) {
 | 
			
		||||
        write!(
 | 
			
		||||
            s,
 | 
			
		||||
            "{}",
 | 
			
		||||
            match self {
 | 
			
		||||
                Self::Table => "contacts",
 | 
			
		||||
                Self::Profile => "profile",
 | 
			
		||||
                Self::Contact => "contact",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum MetadataTable {
 | 
			
		||||
    Table,
 | 
			
		||||
    Pubkey,
 | 
			
		||||
    Name,
 | 
			
		||||
    DisplayName,
 | 
			
		||||
    About,
 | 
			
		||||
    Website,
 | 
			
		||||
    Picture,
 | 
			
		||||
    Banner,
 | 
			
		||||
    Nip05,
 | 
			
		||||
    Lud06,
 | 
			
		||||
    Lud16,
 | 
			
		||||
    Custom,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl sea_query::Iden for MetadataTable {
 | 
			
		||||
    fn unquoted(&self, s: &mut dyn std::fmt::Write) {
 | 
			
		||||
        write!(
 | 
			
		||||
            s,
 | 
			
		||||
            "{}",
 | 
			
		||||
            match self {
 | 
			
		||||
                Self::Table => "metadata",
 | 
			
		||||
                Self::Pubkey => "pubkey",
 | 
			
		||||
                Self::Name => "name",
 | 
			
		||||
                Self::DisplayName => "display_name",
 | 
			
		||||
                Self::About => "about",
 | 
			
		||||
                Self::Website => "website",
 | 
			
		||||
                Self::Picture => "picture",
 | 
			
		||||
                Self::Banner => "banner",
 | 
			
		||||
                Self::Nip05 => "nip05",
 | 
			
		||||
                Self::Lud06 => "lud06",
 | 
			
		||||
                Self::Lud16 => "lud16",
 | 
			
		||||
                Self::Custom => "custom",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct ProfileRow {
 | 
			
		||||
    pubkey: String,
 | 
			
		||||
    // metadata
 | 
			
		||||
    name: Option<String>,
 | 
			
		||||
    display_name: Option<String>,
 | 
			
		||||
    about: Option<String>,
 | 
			
		||||
    website: Option<String>,
 | 
			
		||||
    picture: Option<String>,
 | 
			
		||||
    banner: Option<String>,
 | 
			
		||||
    nip05: Option<String>,
 | 
			
		||||
    lud06: Option<String>,
 | 
			
		||||
    lud16: Option<String>,
 | 
			
		||||
    custom: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&Row<'_>> for ProfileRow {
 | 
			
		||||
    fn from(row: &Row) -> Self {
 | 
			
		||||
        let pubkey: String = row.get("pubkey").unwrap();
 | 
			
		||||
        let name: Option<String> = row.get("name").unwrap_or_default();
 | 
			
		||||
        let display_name: Option<String> = row.get("display_name").unwrap_or_default();
 | 
			
		||||
        let about = row.get("about").unwrap_or_default();
 | 
			
		||||
        let website: Option<String> = row.get("website").unwrap_or_default();
 | 
			
		||||
        let picture: Option<String> = row.get("picture").unwrap_or_default();
 | 
			
		||||
        let banner: Option<String> = row.get("banner").unwrap_or_default();
 | 
			
		||||
        let nip05: Option<String> = row.get("nip05").unwrap_or_default();
 | 
			
		||||
        let lud06: Option<String> = row.get("lud06").unwrap_or_default();
 | 
			
		||||
        let lud16: Option<String> = row.get("lud16").unwrap_or_default();
 | 
			
		||||
        let custom: Option<String> = row.get("custom").unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            pubkey,
 | 
			
		||||
            name,
 | 
			
		||||
            display_name,
 | 
			
		||||
            about,
 | 
			
		||||
            website,
 | 
			
		||||
            picture,
 | 
			
		||||
            banner,
 | 
			
		||||
            nip05,
 | 
			
		||||
            lud06,
 | 
			
		||||
            lud16,
 | 
			
		||||
            custom,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&ProfileRow> for Profile {
 | 
			
		||||
    fn from(row: &ProfileRow) -> Self {
 | 
			
		||||
        let row = row.to_owned();
 | 
			
		||||
        // let f = nostr::EventBuilder::metadata( // Why am I creating this methods in Noose? Just store Kind 0 on a relay and think that the user is registered on the relay ffs
 | 
			
		||||
        let keys = nostr::Keys::from_pk_str(&row.pubkey).unwrap();
 | 
			
		||||
 | 
			
		||||
        let custom_fields: HashMap<String, serde_json::Value> = match row.custom {
 | 
			
		||||
            Some(fields_str) => {
 | 
			
		||||
                let fields: HashMap<String, serde_json::Value> =
 | 
			
		||||
                    serde_json::from_str(&fields_str).unwrap_or(HashMap::new());
 | 
			
		||||
                fields
 | 
			
		||||
            }
 | 
			
		||||
            None => {
 | 
			
		||||
                let f: HashMap<String, serde_json::Value> = HashMap::new();
 | 
			
		||||
                f
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let metadata = Metadata {
 | 
			
		||||
            name: row.name,
 | 
			
		||||
            display_name: row.display_name,
 | 
			
		||||
            about: row.about,
 | 
			
		||||
            website: row.website,
 | 
			
		||||
            picture: row.picture,
 | 
			
		||||
            banner: row.banner,
 | 
			
		||||
            nip05: row.nip05,
 | 
			
		||||
            lud06: row.lud06,
 | 
			
		||||
            lud16: row.lud16,
 | 
			
		||||
            custom: custom_fields,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        nostr_database::Profile::new(keys.public_key(), metadata)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,103 +1,27 @@
 | 
			
		|||
use chrono::Utc;
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use rusqlite::Row;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use validator::{Validate, ValidationError};
 | 
			
		||||
 | 
			
		||||
lazy_static! {
 | 
			
		||||
    static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum Nip05Table {
 | 
			
		||||
    Table,
 | 
			
		||||
    Pubkey,
 | 
			
		||||
    Username,
 | 
			
		||||
    Relays,
 | 
			
		||||
    JoinedAt,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl sea_query::Iden for Nip05Table {
 | 
			
		||||
    fn unquoted(&self, s: &mut dyn std::fmt::Write) {
 | 
			
		||||
        write!(
 | 
			
		||||
            s,
 | 
			
		||||
            "{}",
 | 
			
		||||
            match self {
 | 
			
		||||
                Self::Table => "nip05",
 | 
			
		||||
                Self::Pubkey => "pubkey",
 | 
			
		||||
                Self::Username => "username",
 | 
			
		||||
                Self::Relays => "relays",
 | 
			
		||||
                Self::JoinedAt => "joined_at",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Validate)]
 | 
			
		||||
pub struct UserRow {
 | 
			
		||||
    pub pubkey: String,
 | 
			
		||||
    pub username: String,
 | 
			
		||||
    relays: Vec<String>,
 | 
			
		||||
    joined_at: i64,
 | 
			
		||||
    inserted_at: i64,
 | 
			
		||||
    admin: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UserRow {
 | 
			
		||||
    pub fn new(pubkey: String, username: String, relays: Vec<String>) -> Self {
 | 
			
		||||
    pub fn new(pubkey: String, username: String, admin: bool) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            pubkey,
 | 
			
		||||
            username,
 | 
			
		||||
            relays,
 | 
			
		||||
            joined_at: nostr::Timestamp::now().as_i64(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&Row<'_>> for UserRow {
 | 
			
		||||
    fn from(row: &Row) -> Self {
 | 
			
		||||
        let pubkey: String = row.get("pubkey").unwrap();
 | 
			
		||||
        let username: String = row.get("username").unwrap();
 | 
			
		||||
        let relays_raw: String = row.get("relays").unwrap_or_default();
 | 
			
		||||
        let joined_at: i64 = row.get("joined_at").unwrap();
 | 
			
		||||
 | 
			
		||||
        let relays: Vec<String> = match serde_json::from_str(&relays_raw) {
 | 
			
		||||
            Ok(val) => val,
 | 
			
		||||
            Err(_) => vec![],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            pubkey,
 | 
			
		||||
            username,
 | 
			
		||||
            relays,
 | 
			
		||||
            joined_at,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
 | 
			
		||||
pub struct Nip05Profile {
 | 
			
		||||
    names: HashMap<String, String>,
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    relays: Option<HashMap<String, Vec<String>>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<UserRow> for Nip05Profile {
 | 
			
		||||
    fn from(value: UserRow) -> Self {
 | 
			
		||||
        let mut name: HashMap<String, String> = HashMap::new();
 | 
			
		||||
        name.insert(value.username, value.pubkey.clone());
 | 
			
		||||
 | 
			
		||||
        let relay = match value.relays.is_empty() {
 | 
			
		||||
            true => None,
 | 
			
		||||
            false => {
 | 
			
		||||
                let mut relay: HashMap<String, Vec<String>> = HashMap::new();
 | 
			
		||||
                relay.insert(value.pubkey.clone(), value.relays);
 | 
			
		||||
 | 
			
		||||
                Some(relay)
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            names: name,
 | 
			
		||||
            relays: relay,
 | 
			
		||||
            inserted_at: Utc::now().timestamp(),
 | 
			
		||||
            admin,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -117,33 +41,3 @@ pub fn validate_pubkey(value: &str) -> Result<(), ValidationError> {
 | 
			
		|||
        Err(_) => Err(ValidationError::new("Unable to parse pubkey")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::{Nip05Profile, UserRow};
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn make_nip05() {
 | 
			
		||||
        let user_row = UserRow::new(
 | 
			
		||||
            "npub1m2w0ckmkgj4wtvl8muwjynh56j3qd4nddca4exdg4mdrkepvfnhsmusy54".to_string(),
 | 
			
		||||
            "test_user".to_string(),
 | 
			
		||||
            vec![],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let nip05 = Nip05Profile::from(user_row);
 | 
			
		||||
 | 
			
		||||
        dbg!(&nip05);
 | 
			
		||||
        dbg!(serde_json::to_string(&nip05).unwrap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn parse_relay_vec() {
 | 
			
		||||
        let relays_raw = "";
 | 
			
		||||
        let relays: Vec<String> = match serde_json::from_str(relays_raw) {
 | 
			
		||||
            Ok(val) => val,
 | 
			
		||||
            Err(_) => vec![],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        dbg!(relays);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,12 +11,18 @@ pub async fn ws_handler(
 | 
			
		|||
    Ok(ws.on_upgrade(move |socket| ws::client_connection(socket, context, real_client_ip)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn relay_config(header: String, context: Context) -> Result<impl Reply, Rejection> {
 | 
			
		||||
pub async fn relay_config(header: String) -> Result<impl Reply, Rejection> {
 | 
			
		||||
    if header != "application/nostr+json" {
 | 
			
		||||
        Err(warp::reject::not_found())
 | 
			
		||||
    } else {
 | 
			
		||||
        let config = context.config.get_relay_config_json();
 | 
			
		||||
        
 | 
			
		||||
        Ok(warp::reply::json(&config))
 | 
			
		||||
        let res = serde_json::json!({
 | 
			
		||||
            "contact": "klink@zhitno.st",
 | 
			
		||||
            "name": "zhitno.st",
 | 
			
		||||
            "description": "Very *special* nostr relay",
 | 
			
		||||
            "supported_nips": [ 1, 9, 11, 12, 15, 16, 20, 22, 28, 33 ],
 | 
			
		||||
            "software": "git+https://git.zhitno.st/Klink/sneedstr.git",
 | 
			
		||||
            "version": "0.1.0"
 | 
			
		||||
        });
 | 
			
		||||
        Ok(warp::reply::json(&res))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,26 +6,25 @@ 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)
 | 
			
		||||
    static_files().or(index(context)).with(cors)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn index(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
			
		||||
    // let real_client_ip = warp::header::optional::<std::net::SocketAddr>("X-Real-IP");
 | 
			
		||||
    let real_client_ip = warp::addr::remote();
 | 
			
		||||
    let accept_application_json_header = warp::header::header("Accept");
 | 
			
		||||
    let cors = warp::cors().allow_any_origin();
 | 
			
		||||
 | 
			
		||||
    let relay_information_document_path = warp::path::end().and(
 | 
			
		||||
        warp::header::header("Accept")
 | 
			
		||||
            .and(with_context(context.clone()))
 | 
			
		||||
            .and_then(handler::relay_config),
 | 
			
		||||
    );
 | 
			
		||||
    let nostr_relay_path = warp::path::end().and(
 | 
			
		||||
        warp::ws()
 | 
			
		||||
            .and(with_context(context.clone()))
 | 
			
		||||
            .and(real_client_ip)
 | 
			
		||||
            .and_then(handler::ws_handler),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    relay_information_document_path.or(nostr_relay_path)
 | 
			
		||||
    warp::path::end().and(
 | 
			
		||||
        accept_application_json_header
 | 
			
		||||
            .and_then(handler::relay_config)
 | 
			
		||||
            .with(&cors)
 | 
			
		||||
            .or(warp::ws()
 | 
			
		||||
                .and(with_context(context))
 | 
			
		||||
                .and(real_client_ip)
 | 
			
		||||
                .and_then(handler::ws_handler)
 | 
			
		||||
                .with(&cors)),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn static_files() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										173
									
								
								src/relay/ws.rs
									
										
									
									
									
								
							
							
						
						
									
										173
									
								
								src/relay/ws.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -94,17 +94,6 @@ pub async fn client_connection(
 | 
			
		|||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    crate::bussy::Command::ServiceRegistrationRequired(client_id, relay_message) => {
 | 
			
		||||
                        if client.client_id == client_id {
 | 
			
		||||
                            if let Some(sender) = &client.client_connection {
 | 
			
		||||
                                if !sender.is_closed() {
 | 
			
		||||
                                    log::info!("[Relay] client needs to be authenticated to make request: {}", relay_message.as_json());
 | 
			
		||||
                                    sender.send(Ok(Message::text(relay_message.as_json()))).unwrap();
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    _ => ()
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -112,6 +101,13 @@ pub async fn client_connection(
 | 
			
		|||
            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) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -198,7 +194,7 @@ async fn socket_on_message(context: &Context, client: &mut Client, msg: Message)
 | 
			
		|||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    log::debug!(
 | 
			
		||||
    log::info!(
 | 
			
		||||
        "[client {} - {}] message: {}",
 | 
			
		||||
        client.ip(),
 | 
			
		||||
        client.client_id,
 | 
			
		||||
| 
						 | 
				
			
			@ -215,89 +211,23 @@ fn send(client: &Client, message: Message) {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn needs_auth(context: &Context, client: &Client, event: Option<&Event>) -> bool {
 | 
			
		||||
    if !context.config.auth_required() {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if event.is_some() {
 | 
			
		||||
        let event = event.unwrap();
 | 
			
		||||
        let admin_pk = context.config.get_admin_pubkey();
 | 
			
		||||
        if event.pubkey == *admin_pk {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if event.kind() == nostr::Kind::Metadata {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if !client.authenticated {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn handle_msg(context: &Context, client: &mut Client, client_message: ClientMessage) {
 | 
			
		||||
    match client_message {
 | 
			
		||||
        ClientMessage::Event(event) => {
 | 
			
		||||
            if needs_auth(context, client, Some(&event)) {
 | 
			
		||||
                request_auth(context, client).await;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            handle_event(context, client, event).await
 | 
			
		||||
        }
 | 
			
		||||
        ClientMessage::Event(event) => handle_event(context, client, event).await,
 | 
			
		||||
        ClientMessage::Req {
 | 
			
		||||
            subscription_id,
 | 
			
		||||
            filters,
 | 
			
		||||
        } => {
 | 
			
		||||
            if needs_auth(context, client, None) {
 | 
			
		||||
                request_auth(context, client).await;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            handle_req(context, client, subscription_id, filters).await
 | 
			
		||||
        }
 | 
			
		||||
        } => handle_req(context, client, subscription_id, filters).await,
 | 
			
		||||
        ClientMessage::Count {
 | 
			
		||||
            subscription_id,
 | 
			
		||||
            filters,
 | 
			
		||||
        } => {
 | 
			
		||||
            if needs_auth(context, client, None) {
 | 
			
		||||
                request_auth(context, client).await;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            handle_count(context, client, subscription_id, filters).await
 | 
			
		||||
        }
 | 
			
		||||
        } => handle_count(context, client, subscription_id, filters).await,
 | 
			
		||||
        ClientMessage::Close(subscription_id) => handle_close(client, subscription_id).await,
 | 
			
		||||
        ClientMessage::Auth(event) => handle_auth(context, client, event).await,
 | 
			
		||||
        // Unhandled messages
 | 
			
		||||
        _ => unhandled_message(context, client).await,
 | 
			
		||||
        ClientMessage::Auth(event) => handle_auth(client, event).await,
 | 
			
		||||
        _ => (),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn request_auth(context: &Context, client: &mut Client) {
 | 
			
		||||
    let challenge = uuid::Uuid::new_v4().to_string();
 | 
			
		||||
    client.set_challenge(challenge.clone());
 | 
			
		||||
 | 
			
		||||
    let auth_message = nostr::RelayMessage::auth(challenge);
 | 
			
		||||
    context
 | 
			
		||||
        .pubsub
 | 
			
		||||
        .publish(
 | 
			
		||||
            channels::MSG_RELAY,
 | 
			
		||||
            crate::bussy::Message {
 | 
			
		||||
                source: channels::MSG_RELAY,
 | 
			
		||||
                content: crate::bussy::Command::ServiceRegistrationRequired(
 | 
			
		||||
                    client.client_id,
 | 
			
		||||
                    auth_message,
 | 
			
		||||
                ),
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        .await;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn handle_event(context: &Context, client: &Client, event: Box<Event>) {
 | 
			
		||||
    log::debug!("handle_event is processing new event");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -398,77 +328,14 @@ async fn handle_count(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
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(context: &Context, client: &mut Client, event: Box<Event>) {
 | 
			
		||||
    let mut subscriber = context.pubsub.subscribe(channels::MSG_AUTH).await;
 | 
			
		||||
    context
 | 
			
		||||
        .pubsub
 | 
			
		||||
        .publish(
 | 
			
		||||
            channels::MSG_NOOSE,
 | 
			
		||||
            crate::bussy::Message {
 | 
			
		||||
                source: channels::MSG_AUTH,
 | 
			
		||||
                content: crate::bussy::Command::DbReqGetProfile(event.pubkey),
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        .await;
 | 
			
		||||
 | 
			
		||||
    let mut message = nostr::RelayMessage::ok(
 | 
			
		||||
        event.id,
 | 
			
		||||
        client.authenticated,
 | 
			
		||||
        "auth-required: User not registered",
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let Ok(result) = subscriber.recv().await else {
 | 
			
		||||
        context
 | 
			
		||||
            .pubsub
 | 
			
		||||
            .publish(
 | 
			
		||||
                channels::MSG_RELAY,
 | 
			
		||||
                crate::bussy::Message {
 | 
			
		||||
                    source: channels::MSG_RELAY,
 | 
			
		||||
                    content: crate::bussy::Command::DbResOkWithStatus(client.client_id, message),
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            .await;
 | 
			
		||||
        return;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if let crate::bussy::Command::DbResGetProfile(profile) = result.content {
 | 
			
		||||
        client.authenticate(context.config.get_relay_url(), &event);
 | 
			
		||||
 | 
			
		||||
        let client_status = format!("Client authenticated: {}", client.authenticated);
 | 
			
		||||
        let status_message = if client.authenticated {
 | 
			
		||||
            ""
 | 
			
		||||
        } else {
 | 
			
		||||
            "auth-required: we only accept events from registered users"
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        message = nostr::RelayMessage::ok(event.id, client.authenticated, status_message);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    context
 | 
			
		||||
        .pubsub
 | 
			
		||||
        .publish(
 | 
			
		||||
            channels::MSG_RELAY,
 | 
			
		||||
            crate::bussy::Message {
 | 
			
		||||
                source: channels::MSG_RELAY,
 | 
			
		||||
                content: crate::bussy::Command::DbResOkWithStatus(client.client_id, message),
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        .await;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn unhandled_message(context: &Context, client: &Client) {
 | 
			
		||||
    let message = nostr::RelayMessage::notice("Unsupported Message");
 | 
			
		||||
    context
 | 
			
		||||
        .pubsub
 | 
			
		||||
        .publish(
 | 
			
		||||
            channels::MSG_RELAY,
 | 
			
		||||
            crate::bussy::Message {
 | 
			
		||||
                source: channels::MSG_RELAY,
 | 
			
		||||
                content: crate::bussy::Command::DbResOkWithStatus(client.client_id, message),
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
async fn handle_auth(client: &Client, event: Box<Event>) {
 | 
			
		||||
    let message = Message::text("AUTH not implemented");
 | 
			
		||||
    send(client, message);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,9 @@
 | 
			
		|||
use crate::usernames::validators::{validate_pubkey, validate_relays};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
use crate::usernames::validators::validate_pubkey;
 | 
			
		||||
use crate::utils::error::Error;
 | 
			
		||||
use nostr::key::XOnlyPublicKey;
 | 
			
		||||
use nostr::prelude::*;
 | 
			
		||||
use nostr::{key::XOnlyPublicKey, Keys};
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use validator::Validate;
 | 
			
		||||
| 
						 | 
				
			
			@ -10,26 +12,31 @@ lazy_static! {
 | 
			
		|||
    static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Debug, Validate, PartialEq, Clone)]
 | 
			
		||||
#[derive(Serialize, Deserialize, Debug, Validate)]
 | 
			
		||||
pub struct UserBody {
 | 
			
		||||
    #[validate(length(min = 1), regex = "VALID_CHARACTERS")]
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    #[validate(custom(function = "validate_pubkey"))]
 | 
			
		||||
    pubkey: String,
 | 
			
		||||
    #[validate(custom(function = "validate_relays"))]
 | 
			
		||||
    pub relays: Vec<String>
 | 
			
		||||
    pub pubkey: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UserBody {
 | 
			
		||||
    pub fn get_pubkey(&self) -> String {
 | 
			
		||||
        nostr::Keys::from_pk_str(&self.pubkey).unwrap().public_key().to_string()
 | 
			
		||||
    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 name: String,
 | 
			
		||||
    pub user: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Debug, Clone, Validate)]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,10 @@ 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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,6 @@ use crate::utils::structs::Context;
 | 
			
		|||
use serde_json::json;
 | 
			
		||||
use warp::{Rejection, Reply};
 | 
			
		||||
 | 
			
		||||
use super::dto::UserBody;
 | 
			
		||||
 | 
			
		||||
pub async fn get_account(
 | 
			
		||||
    // account: Result<AccountPubkey, Error>,
 | 
			
		||||
    account: Account,
 | 
			
		||||
| 
						 | 
				
			
			@ -47,9 +45,14 @@ pub async fn get_account(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
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 command = Command::DbReqGetNIP05(user_query.name);
 | 
			
		||||
    let user = User {
 | 
			
		||||
        name: Some(name),
 | 
			
		||||
        pubkey: None,
 | 
			
		||||
    };
 | 
			
		||||
    let command = Command::DbReqGetUser(user);
 | 
			
		||||
    context
 | 
			
		||||
        .pubsub
 | 
			
		||||
        .publish(
 | 
			
		||||
| 
						 | 
				
			
			@ -62,44 +65,18 @@ pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Re
 | 
			
		|||
        .await;
 | 
			
		||||
 | 
			
		||||
    if let Ok(message) = subscriber.recv().await {
 | 
			
		||||
        let mut response = json!({"names": {}, "relays": {}});
 | 
			
		||||
        match message.content {
 | 
			
		||||
            Command::DbResNIP05(profile) => {
 | 
			
		||||
                let response = serde_json::to_value(profile).unwrap();
 | 
			
		||||
            Command::DbResUser(user) => {
 | 
			
		||||
                response = json!({
 | 
			
		||||
                    "names": {
 | 
			
		||||
                        user.username: user.pubkey
 | 
			
		||||
                    },
 | 
			
		||||
                    "relays": {}
 | 
			
		||||
                });
 | 
			
		||||
                Ok(warp::reply::json(&response))
 | 
			
		||||
            }
 | 
			
		||||
            Command::ServiceError(e) => Err(warp::reject::custom(e)),
 | 
			
		||||
            _ => Err(warp::reject::custom(Error::internal_with_message(
 | 
			
		||||
                "Unhandeled message type",
 | 
			
		||||
            ))),
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        Err(warp::reject::custom(Error::internal_with_message(
 | 
			
		||||
            "Unhandeled message type",
 | 
			
		||||
        )))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn create_user(user_body: UserBody, context: Context) -> Result<impl Reply, Rejection> {
 | 
			
		||||
    let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
 | 
			
		||||
 | 
			
		||||
    let command = Command::DbReqCreateNIP05(user_body);
 | 
			
		||||
    context
 | 
			
		||||
        .pubsub
 | 
			
		||||
        .publish(
 | 
			
		||||
            channels::MSG_NOOSE,
 | 
			
		||||
            Message {
 | 
			
		||||
                source: channels::MSG_NIP05,
 | 
			
		||||
                content: command,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        .await;
 | 
			
		||||
 | 
			
		||||
    if let Ok(message) = subscriber.recv().await {
 | 
			
		||||
        match message.content {
 | 
			
		||||
            Command::DbResOk => {
 | 
			
		||||
                Ok(warp::http::StatusCode::CREATED)
 | 
			
		||||
            }
 | 
			
		||||
            Command::ServiceError(e) => Err(warp::reject::custom(e)),
 | 
			
		||||
            Command::ServiceError(e) => Ok(warp::reply::json(&response)),
 | 
			
		||||
            _ => Err(warp::reject::custom(Error::internal_with_message(
 | 
			
		||||
                "Unhandeled message type",
 | 
			
		||||
            ))),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
// mod accounts;
 | 
			
		||||
mod accounts;
 | 
			
		||||
pub mod dto;
 | 
			
		||||
mod filter;
 | 
			
		||||
mod handler;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,69 +1,49 @@
 | 
			
		|||
use crate::noose::user::User;
 | 
			
		||||
 | 
			
		||||
// use super::accounts::create_account;
 | 
			
		||||
use super::dto::{Account, UserBody, UserQuery};
 | 
			
		||||
use super::accounts::create_account;
 | 
			
		||||
use super::dto::{Account, UserQuery};
 | 
			
		||||
use super::filter::{validate_body_filter, validate_query_filter};
 | 
			
		||||
use super::handler::{create_user, get_account, get_user};
 | 
			
		||||
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 cors = warp::cors().allow_any_origin();
 | 
			
		||||
    let index = warp::path::end().map(|| warp::reply::html("<h1>SNEED!</h1>"));
 | 
			
		||||
 | 
			
		||||
    index
 | 
			
		||||
        .or(nip05_get(context.clone()))
 | 
			
		||||
        .or(nip05_create(context.clone()))
 | 
			
		||||
        .with(&cors)
 | 
			
		||||
        .or(account_create(context.clone()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn well_known<M>(warp_method: M) -> impl Filter<Extract = (), Error = Rejection> + Clone
 | 
			
		||||
where
 | 
			
		||||
    M: (Filter<Extract = (), Error = Rejection>) + Copy,
 | 
			
		||||
{
 | 
			
		||||
    warp_method.and(warp::path(".well-known"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn nostr_well_known() -> impl Filter<Extract = (), Error = Rejection> + Clone {
 | 
			
		||||
    well_known(warp::get()).and(warp::path("nostr.json"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn nip05_get(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
			
		||||
    nostr_well_known()
 | 
			
		||||
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.clone()))
 | 
			
		||||
        .and(with_context(context))
 | 
			
		||||
        .and_then(get_user)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn nip05_create(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
			
		||||
    well_known(warp::post())
 | 
			
		||||
        .and(warp::path("nostr.json"))
 | 
			
		||||
        .and(warp::body::content_length_limit(1024))
 | 
			
		||||
        .and(validate_body_filter::<UserBody>())
 | 
			
		||||
        .and(with_context(context.clone()))
 | 
			
		||||
        .and_then(create_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_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_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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
use super::dto::AccountPubkey;
 | 
			
		||||
use crate::utils::error::Error;
 | 
			
		||||
use nostr::prelude::FromPkStr;
 | 
			
		||||
use validator::{Validate, ValidationError, validate_url};
 | 
			
		||||
use validator::{Validate, ValidationError};
 | 
			
		||||
 | 
			
		||||
pub async fn validate_account_pubkey_query(
 | 
			
		||||
    account_pubkey: AccountPubkey,
 | 
			
		||||
| 
						 | 
				
			
			@ -25,18 +25,6 @@ pub fn validate_pubkey(value: &str) -> Result<(), ValidationError> {
 | 
			
		|||
 | 
			
		||||
    match nostr::Keys::from_pk_str(value) {
 | 
			
		||||
        Ok(_) => Ok(()),
 | 
			
		||||
        Err(_) => Err(ValidationError::new("Failed to parse pubkey")),
 | 
			
		||||
        Err(_) => Err(ValidationError::new("Unable to parse pk_str")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn validate_relays(relays: &Vec<String>) -> Result<(), ValidationError> {
 | 
			
		||||
    if relays.is_empty() {
 | 
			
		||||
        return Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if relays.iter().all(validate_url) {
 | 
			
		||||
        return Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Err(ValidationError::new("Relays have wrong url format"))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
use std::{path::PathBuf, str::FromStr};
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use nostr::{key::FromPkStr, secp256k1::XOnlyPublicKey};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -6,8 +6,6 @@ use nostr::{key::FromPkStr, secp256k1::XOnlyPublicKey};
 | 
			
		|||
pub struct Config {
 | 
			
		||||
    admin_pubkey: XOnlyPublicKey,
 | 
			
		||||
    db_path: PathBuf,
 | 
			
		||||
    auth_required: bool,
 | 
			
		||||
    relay_url: nostr::UncheckedUrl,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for Config {
 | 
			
		||||
| 
						 | 
				
			
			@ -25,24 +23,13 @@ impl Config {
 | 
			
		|||
            .public_key();
 | 
			
		||||
 | 
			
		||||
        let db_path = std::env::var("DATABASE_URL").map(PathBuf::from).unwrap();
 | 
			
		||||
        let auth_required: bool = std::env::var("CONFIG_ENABLE_AUTH")
 | 
			
		||||
            .unwrap_or("false".to_string())
 | 
			
		||||
            .parse()
 | 
			
		||||
            .unwrap();
 | 
			
		||||
        let relay_url = nostr::UncheckedUrl::from_str(&std::env::var("CONFIG_RELAY_URL").unwrap()).unwrap();
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            admin_pubkey,
 | 
			
		||||
            db_path,
 | 
			
		||||
            auth_required,
 | 
			
		||||
            relay_url,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn auth_required(&self) -> bool {
 | 
			
		||||
        self.auth_required
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_admin_pubkey(&self) -> &XOnlyPublicKey {
 | 
			
		||||
        &self.admin_pubkey
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -51,18 +38,14 @@ impl Config {
 | 
			
		|||
        self.db_path.clone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_relay_url(&self) -> nostr::UncheckedUrl {
 | 
			
		||||
        self.relay_url.clone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_relay_config_json(&self) -> serde_json::Value {
 | 
			
		||||
        serde_json::json!({
 | 
			
		||||
            "contact": "klink@zhitno.st",
 | 
			
		||||
            "name": "zhitno.st",
 | 
			
		||||
            "description": "Very *special* nostr relay",
 | 
			
		||||
            "supported_nips": [ 1, 2, 4, 9, 11, 12, 15, 16, 20, 22, 28, 33, 40, 42, 45, 50 ],
 | 
			
		||||
            "supported_nips": [ 1, 9, 11, 12, 15, 16, 20, 22, 28, 33, 45 ],
 | 
			
		||||
            "software": "git+https://git.zhitno.st/Klink/sneedstr.git",
 | 
			
		||||
            "version": "0.1.1"
 | 
			
		||||
            "version": "0.1.0"
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,13 +8,13 @@ use std::{
 | 
			
		|||
use validator::ValidationErrors;
 | 
			
		||||
use warp::{http::StatusCode, reject::Reject};
 | 
			
		||||
 | 
			
		||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
 | 
			
		||||
pub struct Error {
 | 
			
		||||
    pub code: u16,
 | 
			
		||||
    pub message: String,
 | 
			
		||||
    pub sneedstr_version: String,
 | 
			
		||||
    /// Sneedstr version.
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    pub sneedstr_version: Option<u16>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl StdError for Error {
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ impl Error {
 | 
			
		|||
        Self {
 | 
			
		||||
            code: code.as_u16(),
 | 
			
		||||
            message,
 | 
			
		||||
            sneedstr_version: VERSION.to_string(),
 | 
			
		||||
            sneedstr_version: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -54,11 +54,12 @@ impl Error {
 | 
			
		|||
        Self::new(StatusCode::BAD_REQUEST, message)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn not_found<S: Display>(resource: &str, identifier: S) -> Self {
 | 
			
		||||
    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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -76,6 +77,11 @@ impl Error {
 | 
			
		|||
    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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -110,36 +116,28 @@ mod tests {
 | 
			
		|||
    #[test]
 | 
			
		||||
    fn test_to_string() {
 | 
			
		||||
        let err = Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned());
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            err.to_string(),
 | 
			
		||||
            "Error { code: 400, message: 'invalid address', sneedstr_version: \"0.1.1\" }"
 | 
			
		||||
        )
 | 
			
		||||
        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(),
 | 
			
		||||
            "Error { code: 500, message: 'hello', sneedstr_version: \"0.1.1\" }"
 | 
			
		||||
        )
 | 
			
		||||
        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());
 | 
			
		||||
        let err =
 | 
			
		||||
            Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned()).sneedstr_version(123);
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            err.to_string(),
 | 
			
		||||
            "Error { code: 400, message: 'invalid address', sneedstr_version: \"0.1.1\" }"
 | 
			
		||||
            "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(),
 | 
			
		||||
            "Error { code: 500, message: 'hello', sneedstr_version: \"0.1.1\" }"
 | 
			
		||||
        )
 | 
			
		||||
        assert_eq!(err.to_string(), "500 Internal Server Error: hello")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,6 @@ use crate::PubSub;
 | 
			
		|||
 | 
			
		||||
use nostr::{Event, Filter, SubscriptionId};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::str::FromStr;
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use tokio::sync::mpsc;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
| 
						 | 
				
			
			@ -51,8 +50,6 @@ pub struct Client {
 | 
			
		|||
    pub client_id: Uuid,
 | 
			
		||||
    pub client_connection: Option<mpsc::UnboundedSender<Result<Message, Error>>>,
 | 
			
		||||
    pub subscriptions: HashMap<String, Subscription>,
 | 
			
		||||
    pub authenticated: bool, // NIP-42
 | 
			
		||||
    challlenge: Option<String>, // NIP-42
 | 
			
		||||
    max_subs: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -63,8 +60,6 @@ impl Client {
 | 
			
		|||
            client_id: Uuid::new_v4(),
 | 
			
		||||
            client_connection: None,
 | 
			
		||||
            subscriptions: HashMap::new(),
 | 
			
		||||
            authenticated: false,
 | 
			
		||||
            challlenge: None,
 | 
			
		||||
            max_subs: MAX_SUBSCRIPTIONS,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -73,43 +68,6 @@ impl Client {
 | 
			
		|||
        &self.client_ip_addr
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_challenge(&mut self, challenge: String) {
 | 
			
		||||
        self.challlenge = Some(challenge);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn authenticate(&mut self, relay_url: nostr::UncheckedUrl, event: &nostr::Event) {
 | 
			
		||||
        if self.challlenge.is_none() {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let challenge_tag = nostr::Tag::Challenge(self.challlenge.clone().unwrap());
 | 
			
		||||
        let relay_tag = nostr::Tag::Relay(relay_url);
 | 
			
		||||
        if let Ok(()) = event.verify() {
 | 
			
		||||
            log::debug!("event is valid");
 | 
			
		||||
            if event.kind.as_u32() == 22242 && event.created_at() > nostr::Timestamp::from(nostr::Timestamp::now().as_u64() - 600) {
 | 
			
		||||
                log::debug!("kind is correct and timestamp is good");
 | 
			
		||||
                let mut challenge_matched = false;
 | 
			
		||||
                let mut relay_matched = false;
 | 
			
		||||
                event.tags().iter().for_each(|tag| {
 | 
			
		||||
                    if tag == &challenge_tag {
 | 
			
		||||
                        challenge_matched = true;
 | 
			
		||||
                        log::debug!("challenge matched");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if tag == &relay_tag {
 | 
			
		||||
                        relay_matched = true;
 | 
			
		||||
                        log::debug!("relay matched");
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                if challenge_matched && relay_matched {
 | 
			
		||||
                    log::debug!("client now authenticated");
 | 
			
		||||
                    self.authenticated = true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn subscribe(&mut self, subscription: Subscription) -> Result<(), Error> {
 | 
			
		||||
        let k = subscription.get_id();
 | 
			
		||||
        let sub_id_len = k.len();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										66
									
								
								templates/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								templates/static/android-chrome-192x192.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 43 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								templates/static/android-chrome-512x512.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								templates/static/android-chrome-512x512.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 235 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								templates/static/apple-touch-icon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								templates/static/apple-touch-icon.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 39 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								templates/static/favicon-16x16.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								templates/static/favicon-16x16.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 777 B  | 
							
								
								
									
										
											BIN
										
									
								
								templates/static/favicon-32x32.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								templates/static/favicon-32x32.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								templates/static/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								templates/static/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										1
									
								
								templates/static/site.webmanifest
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										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"}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue