Implemented feature:
- NIP-42 Can be enabled in nix module or as environment variable 'CONFIG_ENABLE_AUTH' - NIP-05 Still WIP, but building up slowly
This commit is contained in:
		
							parent
							
								
									377da44eed
								
							
						
					
					
						commit
						f7b74bd22c
					
				
					 18 changed files with 1001 additions and 294 deletions
				
			
		| 
						 | 
				
			
			@ -13,7 +13,6 @@
 | 
			
		|||
        naersk' = pkgs.callPackage naersk { };
 | 
			
		||||
 | 
			
		||||
        wwwPath = "www";
 | 
			
		||||
        templatesPath = "templates";
 | 
			
		||||
 | 
			
		||||
      in rec {
 | 
			
		||||
        # For `nix build` & `nix run`:
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +24,6 @@
 | 
			
		|||
            mkdir -p $out/templates
 | 
			
		||||
            mkdir -p $out/www
 | 
			
		||||
            cp -r ${wwwPath} $out/
 | 
			
		||||
            cp -r ${templatesPath} $out/
 | 
			
		||||
          '';
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,11 @@ in {
 | 
			
		|||
      'npub' of the administrator account. Must be defined!
 | 
			
		||||
      '';
 | 
			
		||||
    };
 | 
			
		||||
    enableAuth = mkOption {
 | 
			
		||||
      type = type.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;
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +69,7 @@ in {
 | 
			
		|||
          environment = {
 | 
			
		||||
            DATABASE_URL = "${DB_PATH}/sneedstr.db";
 | 
			
		||||
            ADMIN_PUBKEY = cfg.adminPubkey;
 | 
			
		||||
            CONFIG_ENABLE_AUTH = cfg.enableAuth;
 | 
			
		||||
          };
 | 
			
		||||
          startLimitBurst = 1;
 | 
			
		||||
          startLimitIntervalSec = 10;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
use crate::{
 | 
			
		||||
    noose::sled::BanInfo,
 | 
			
		||||
    noose::user::{User, UserRow, Nip05Profile},
 | 
			
		||||
    utils::{error::Error, structs::Subscription},
 | 
			
		||||
    utils::{error::Error, structs::Subscription}, usernames::dto::UserBody,
 | 
			
		||||
};
 | 
			
		||||
use nostr::secp256k1::XOnlyPublicKey;
 | 
			
		||||
use std::{collections::HashMap, fmt::Debug};
 | 
			
		||||
use std::{collections::{HashMap, BTreeSet}, fmt::Debug};
 | 
			
		||||
use tokio::sync::{broadcast, Mutex};
 | 
			
		||||
 | 
			
		||||
pub mod channels {
 | 
			
		||||
| 
						 | 
				
			
			@ -18,22 +18,21 @@ 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),
 | 
			
		||||
    DbReqCreateAccount(XOnlyPublicKey, String, String),
 | 
			
		||||
    DbReqGetAccount(String),
 | 
			
		||||
    DbReqClear,
 | 
			
		||||
    // NIP-05 related messages
 | 
			
		||||
    DbReqGetUser(String),
 | 
			
		||||
    DbResUser(Nip05Profile),
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // DbResponse
 | 
			
		||||
    DbReqGetNIP05(String),
 | 
			
		||||
    DbReqCreateNIP05(UserBody),
 | 
			
		||||
    // --- Res
 | 
			
		||||
    DbResNIP05(Nip05Profile),
 | 
			
		||||
    DbResRelayMessages(
 | 
			
		||||
        /* client_id*/ uuid::Uuid,
 | 
			
		||||
        /* Vec<RelayMessage::Event> */ Vec<nostr::RelayMessage>,
 | 
			
		||||
| 
						 | 
				
			
			@ -43,24 +42,47 @@ pub enum Command {
 | 
			
		|||
    DbResOkWithStatus(/* client_id */ uuid::Uuid, nostr::RelayMessage),
 | 
			
		||||
    DbResAccount, // TODO: Add Account DTO as a param
 | 
			
		||||
    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,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,21 +1,58 @@
 | 
			
		|||
use crate::{
 | 
			
		||||
    bussy::PubSub,
 | 
			
		||||
    usernames::dto::UserBody,
 | 
			
		||||
    utils::{error::Error, structs::Subscription},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use nostr::{Event, RelayMessage};
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use nostr::{secp256k1::XOnlyPublicKey, Event, RelayMessage};
 | 
			
		||||
use nostr_database::Profile;
 | 
			
		||||
use std::{collections::BTreeSet, 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>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										9
									
								
								src/noose/migrations/1706575155557_nip05.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/noose/migrations/1706575155557_nip05.sql
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
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);
 | 
			
		||||
							
								
								
									
										27
									
								
								src/noose/migrations/1707327016995_nip42_profile.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/noose/migrations/1707327016995_nip42_profile.sql
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
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);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ impl MigrationRunner {
 | 
			
		|||
        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 migrations = Migrations::new(vec![
 | 
			
		||||
            M::up(m_create_events),
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +22,7 @@ impl MigrationRunner {
 | 
			
		|||
            M::up(m_users),
 | 
			
		||||
            M::up(m_unattached_media),
 | 
			
		||||
            M::up(m_nip05),
 | 
			
		||||
            M::up(m_nip42),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        match migrations.to_latest(connection) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ mod migrations;
 | 
			
		|||
pub mod pipeline;
 | 
			
		||||
pub mod sled;
 | 
			
		||||
mod sqlite;
 | 
			
		||||
mod sqlite_tables;
 | 
			
		||||
pub mod user;
 | 
			
		||||
 | 
			
		||||
pub fn start(context: Context) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,176 +5,23 @@ use super::{
 | 
			
		|||
};
 | 
			
		||||
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, Event, EventId, Filter, RelayMessage, TagKind, Timestamp, Url,
 | 
			
		||||
    nips::nip01::Coordinate, secp256k1::XOnlyPublicKey, Event, EventId, Filter, JsonUtil,
 | 
			
		||||
    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};
 | 
			
		||||
 | 
			
		||||
#[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 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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
use crate::noose::sqlite_tables::*;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct NostrSqlite {
 | 
			
		||||
| 
						 | 
				
			
			@ -494,86 +341,159 @@ impl NostrSqlite {
 | 
			
		|||
 | 
			
		||||
                        return Ok(false);
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    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(),
 | 
			
		||||
                        ])
 | 
			
		||||
 | 
			
		||||
                    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(), &*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);
 | 
			
		||||
                    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);
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    // 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);
 | 
			
		||||
                    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(), &*values.as_params())
 | 
			
		||||
                                    {
 | 
			
		||||
                                        log::error!(
 | 
			
		||||
                                            "Error inserting event into 'tags' table: {}",
 | 
			
		||||
                                            err
 | 
			
		||||
                                        );
 | 
			
		||||
                                        tx.rollback().unwrap();
 | 
			
		||||
                    if let Err(err) = tx.execute(sql.as_str(), &*value.as_params()) {
 | 
			
		||||
                        tx.rollback().unwrap();
 | 
			
		||||
                        
 | 
			
		||||
                                        return Ok(false);
 | 
			
		||||
                                    }
 | 
			
		||||
                        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)
 | 
			
		||||
                        .columns([
 | 
			
		||||
                            MetadataTable::Pubkey,
 | 
			
		||||
                            MetadataTable::Name,
 | 
			
		||||
                            MetadataTable::DisplayName,
 | 
			
		||||
                            MetadataTable::About,
 | 
			
		||||
                            MetadataTable::Website,
 | 
			
		||||
                            MetadataTable::Picture,
 | 
			
		||||
                            MetadataTable::Banner,
 | 
			
		||||
                            MetadataTable::Nip05,
 | 
			
		||||
                            MetadataTable::Lud06,
 | 
			
		||||
                            MetadataTable::Lud16,
 | 
			
		||||
                            MetadataTable::Custom,
 | 
			
		||||
                        ])
 | 
			
		||||
                        .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(),
 | 
			
		||||
                        ])
 | 
			
		||||
                        .build_rusqlite(SqliteQueryBuilder);
 | 
			
		||||
 | 
			
		||||
                    if let Err(err) = tx.execute(sql.as_str(), &*value.as_params()) {
 | 
			
		||||
                        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();
 | 
			
		||||
 | 
			
		||||
                    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();
 | 
			
		||||
 | 
			
		||||
                    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);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
| 
						 | 
				
			
			@ -1265,6 +1185,105 @@ impl NostrSqlite {
 | 
			
		|||
 | 
			
		||||
        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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -1417,10 +1436,43 @@ impl Noose for NostrSqlite {
 | 
			
		|||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                // NIP-05
 | 
			
		||||
                Command::DbReqGetUser(username) => match self.get_nip05(username).await {
 | 
			
		||||
                    Ok(user) => Command::DbResUser(user),
 | 
			
		||||
                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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -1489,11 +1541,53 @@ impl Noose for NostrSqlite {
 | 
			
		|||
    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]
 | 
			
		||||
| 
						 | 
				
			
			@ -1718,6 +1812,29 @@ 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());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										322
									
								
								src/noose/sqlite_tables.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										322
									
								
								src/noose/sqlite_tables.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,322 @@
 | 
			
		|||
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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										107
									
								
								src/relay/ws.rs
									
										
									
									
									
								
							
							
						
						
									
										107
									
								
								src/relay/ws.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -94,6 +94,17 @@ 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();
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    _ => ()
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -101,13 +112,6 @@ 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) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -194,7 +198,7 @@ async fn socket_on_message(context: &Context, client: &mut Client, msg: Message)
 | 
			
		|||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    log::info!(
 | 
			
		||||
    log::debug!(
 | 
			
		||||
        "[client {} - {}] message: {}",
 | 
			
		||||
        client.ip(),
 | 
			
		||||
        client.client_id,
 | 
			
		||||
| 
						 | 
				
			
			@ -213,21 +217,63 @@ fn send(client: &Client, message: Message) {
 | 
			
		|||
 | 
			
		||||
async fn handle_msg(context: &Context, client: &mut Client, client_message: ClientMessage) {
 | 
			
		||||
    match client_message {
 | 
			
		||||
        ClientMessage::Event(event) => handle_event(context, client, event).await,
 | 
			
		||||
        ClientMessage::Event(event) => {
 | 
			
		||||
            if context.config.auth_required()
 | 
			
		||||
                && event.kind() != nostr::Kind::Metadata
 | 
			
		||||
                && !client.authenticated
 | 
			
		||||
            {
 | 
			
		||||
                request_auth(context, client).await;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            handle_event(context, client, event).await
 | 
			
		||||
        }
 | 
			
		||||
        ClientMessage::Req {
 | 
			
		||||
            subscription_id,
 | 
			
		||||
            filters,
 | 
			
		||||
        } => handle_req(context, client, subscription_id, filters).await,
 | 
			
		||||
        } => {
 | 
			
		||||
            if context.config.auth_required() && !client.authenticated {
 | 
			
		||||
                request_auth(context, client).await;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            handle_req(context, client, subscription_id, filters).await
 | 
			
		||||
        }
 | 
			
		||||
        ClientMessage::Count {
 | 
			
		||||
            subscription_id,
 | 
			
		||||
            filters,
 | 
			
		||||
        } => handle_count(context, client, subscription_id, filters).await,
 | 
			
		||||
        } => {
 | 
			
		||||
            if context.config.auth_required() && !client.authenticated {
 | 
			
		||||
                request_auth(context, client).await;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            handle_count(context, client, subscription_id, filters).await
 | 
			
		||||
        }
 | 
			
		||||
        ClientMessage::Close(subscription_id) => handle_close(client, subscription_id).await,
 | 
			
		||||
        ClientMessage::Auth(event) => handle_auth(client, event).await,
 | 
			
		||||
        _ => (),
 | 
			
		||||
        ClientMessage::Auth(event) => handle_auth(context, client, event).await,
 | 
			
		||||
        // Unhandled messages
 | 
			
		||||
        _ => unhandled_message(context, client).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");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -328,14 +374,35 @@ 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(client: &Client, event: Box<Event>) {
 | 
			
		||||
    let message = Message::text("AUTH not implemented");
 | 
			
		||||
    send(client, message);
 | 
			
		||||
async fn handle_auth(context: &Context, client: &mut Client, event: Box<Event>) {
 | 
			
		||||
    client.authenticate(&event);
 | 
			
		||||
    let client_status = format!("Client authenticated: {}", client.authenticated);
 | 
			
		||||
    let message = nostr::RelayMessage::notice(client_status);
 | 
			
		||||
    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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,7 @@
 | 
			
		|||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
use crate::usernames::validators::validate_pubkey;
 | 
			
		||||
use crate::usernames::validators::{validate_pubkey, validate_relays};
 | 
			
		||||
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;
 | 
			
		||||
| 
						 | 
				
			
			@ -12,27 +10,22 @@ lazy_static! {
 | 
			
		|||
    static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Debug, Validate)]
 | 
			
		||||
#[derive(Serialize, Deserialize, Debug, Validate, PartialEq, Clone)]
 | 
			
		||||
pub struct UserBody {
 | 
			
		||||
    #[validate(length(min = 1), regex = "VALID_CHARACTERS")]
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    #[validate(custom(function = "validate_pubkey"))]
 | 
			
		||||
    pub pubkey: String,
 | 
			
		||||
    pubkey: String,
 | 
			
		||||
    #[validate(custom(function = "validate_relays"))]
 | 
			
		||||
    pub relays: Vec<String>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UserBody {
 | 
			
		||||
    pub fn get_pubkey(&self) -> XOnlyPublicKey {
 | 
			
		||||
        let keys = Keys::from_pk_str(&self.pubkey).unwrap();
 | 
			
		||||
 | 
			
		||||
        keys.public_key()
 | 
			
		||||
    pub fn get_pubkey(&self) -> String {
 | 
			
		||||
        nostr::Keys::from_pk_str(&self.pubkey).unwrap().public_key().to_string()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[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))]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,10 +2,6 @@ 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,6 +6,8 @@ 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,7 +49,7 @@ pub async fn get_account(
 | 
			
		|||
pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Reply, Rejection> {
 | 
			
		||||
    let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
 | 
			
		||||
 | 
			
		||||
    let command = Command::DbReqGetUser(user_query.name);
 | 
			
		||||
    let command = Command::DbReqGetNIP05(user_query.name);
 | 
			
		||||
    context
 | 
			
		||||
        .pubsub
 | 
			
		||||
        .publish(
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +63,7 @@ pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Re
 | 
			
		|||
 | 
			
		||||
    if let Ok(message) = subscriber.recv().await {
 | 
			
		||||
        match message.content {
 | 
			
		||||
            Command::DbResUser(profile) => {
 | 
			
		||||
            Command::DbResNIP05(profile) => {
 | 
			
		||||
                let response = serde_json::to_value(profile).unwrap();
 | 
			
		||||
                Ok(warp::reply::json(&response))
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -76,3 +78,35 @@ pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Re
 | 
			
		|||
        )))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)),
 | 
			
		||||
            _ => Err(warp::reject::custom(Error::internal_with_message(
 | 
			
		||||
                "Unhandeled message type",
 | 
			
		||||
            ))),
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        Err(warp::reject::custom(Error::internal_with_message(
 | 
			
		||||
            "Unhandeled message type",
 | 
			
		||||
        )))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
use crate::noose::user::User;
 | 
			
		||||
 | 
			
		||||
// use super::accounts::create_account;
 | 
			
		||||
use super::dto::{Account, UserQuery};
 | 
			
		||||
use super::dto::{Account, UserBody, UserQuery};
 | 
			
		||||
use super::filter::{validate_body_filter, validate_query_filter};
 | 
			
		||||
use super::handler::{get_account, get_user};
 | 
			
		||||
use super::handler::{create_user, get_account, get_user};
 | 
			
		||||
use crate::utils::filter::with_context;
 | 
			
		||||
use crate::utils::structs::Context;
 | 
			
		||||
use warp::{Filter, Rejection, Reply};
 | 
			
		||||
| 
						 | 
				
			
			@ -14,25 +14,37 @@ pub fn routes(context: Context) -> impl Filter<Extract = impl Reply, Error = Rej
 | 
			
		|||
 | 
			
		||||
    index
 | 
			
		||||
        .or(nip05_get(context.clone()))
 | 
			
		||||
        // .or(account_create(context.clone()))
 | 
			
		||||
        .or(nip05_create(context.clone()))
 | 
			
		||||
        .with(&cors)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn well_known() -> impl Filter<Extract = (), Error = Rejection> + Clone {
 | 
			
		||||
    warp::get().and(warp::path(".well-known"))
 | 
			
		||||
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().and(warp::path("nostr.json"))
 | 
			
		||||
    well_known(warp::get()).and(warp::path("nostr.json"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn nip05_get(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
			
		||||
fn nip05_get(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
 | 
			
		||||
    nostr_well_known()
 | 
			
		||||
        .and(validate_query_filter::<UserQuery>())
 | 
			
		||||
        .and(with_context(context.clone()))
 | 
			
		||||
        .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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
use super::dto::AccountPubkey;
 | 
			
		||||
use crate::utils::error::Error;
 | 
			
		||||
use nostr::prelude::FromPkStr;
 | 
			
		||||
use validator::{Validate, ValidationError};
 | 
			
		||||
use validator::{Validate, ValidationError, validate_url};
 | 
			
		||||
 | 
			
		||||
pub async fn validate_account_pubkey_query(
 | 
			
		||||
    account_pubkey: AccountPubkey,
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +25,18 @@ pub fn validate_pubkey(value: &str) -> Result<(), ValidationError> {
 | 
			
		|||
 | 
			
		||||
    match nostr::Keys::from_pk_str(value) {
 | 
			
		||||
        Ok(_) => Ok(()),
 | 
			
		||||
        Err(_) => Err(ValidationError::new("Unable to parse pk_str")),
 | 
			
		||||
        Err(_) => Err(ValidationError::new("Failed to parse pubkey")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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"))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ use nostr::{key::FromPkStr, secp256k1::XOnlyPublicKey};
 | 
			
		|||
pub struct Config {
 | 
			
		||||
    admin_pubkey: XOnlyPublicKey,
 | 
			
		||||
    db_path: PathBuf,
 | 
			
		||||
    auth_required: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for Config {
 | 
			
		||||
| 
						 | 
				
			
			@ -23,13 +24,22 @@ 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();
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            admin_pubkey,
 | 
			
		||||
            db_path,
 | 
			
		||||
            auth_required,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn auth_required(&self) -> bool {
 | 
			
		||||
        self.auth_required
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_admin_pubkey(&self) -> &XOnlyPublicKey {
 | 
			
		||||
        &self.admin_pubkey
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +53,7 @@ impl Config {
 | 
			
		|||
            "contact": "klink@zhitno.st",
 | 
			
		||||
            "name": "zhitno.st",
 | 
			
		||||
            "description": "Very *special* nostr relay",
 | 
			
		||||
            "supported_nips": [ 1, 2, 9, 11, 12, 15, 16, 20, 22, 28, 33, 40, 45, 50 ],
 | 
			
		||||
            "supported_nips": [ 1, 2, 9, 11, 12, 15, 16, 20, 22, 28, 33, 40, 42, 45, 50 ],
 | 
			
		||||
            "software": "git+https://git.zhitno.st/Klink/sneedstr.git",
 | 
			
		||||
            "version": "0.1.1"
 | 
			
		||||
        })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ 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;
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +51,8 @@ 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,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -60,6 +63,8 @@ impl Client {
 | 
			
		|||
            client_id: Uuid::new_v4(),
 | 
			
		||||
            client_connection: None,
 | 
			
		||||
            subscriptions: HashMap::new(),
 | 
			
		||||
            authenticated: false,
 | 
			
		||||
            challlenge: None,
 | 
			
		||||
            max_subs: MAX_SUBSCRIPTIONS,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -68,6 +73,43 @@ impl Client {
 | 
			
		|||
        &self.client_ip_addr
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_challenge(&mut self, challenge: String) {
 | 
			
		||||
        self.challlenge = Some(challenge);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn authenticate(&mut self, 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(nostr::UncheckedUrl::from_str("ws://0.0.0.0:8080").unwrap()); // TODO: Use relay address from env variable
 | 
			
		||||
        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();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue