diff --git a/Cargo.lock b/Cargo.lock index 22f571c..2c8df2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,18 +497,6 @@ 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" @@ -761,15 +749,6 @@ 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" @@ -957,12 +936,6 @@ 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" @@ -1591,44 +1564,6 @@ 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" @@ -1741,15 +1676,6 @@ 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" @@ -1833,7 +1759,6 @@ dependencies = [ "rusqlite", "rusqlite_migration", "rustls 0.21.6", - "sailfish", "sea-query", "sea-query-rusqlite", "serde", @@ -2062,26 +1987,11 @@ 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" @@ -2090,8 +2000,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.0.0", - "serde", - "serde_spanned", "toml_datetime", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 159f4d4..42ed8ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,6 @@ 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", diff --git a/flake.nix b/flake.nix index ded426f..80ce26b 100644 --- a/flake.nix +++ b/flake.nix @@ -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/ ''; }; @@ -52,6 +50,8 @@ 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"; }; diff --git a/modules/sneedstr.nix b/modules/sneedstr.nix index 4d901f5..1768b09 100644 --- a/modules/sneedstr.nix +++ b/modules/sneedstr.nix @@ -21,6 +21,11 @@ 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; @@ -36,6 +41,10 @@ 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 { @@ -64,6 +73,8 @@ 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; @@ -111,6 +122,10 @@ in { proxyWebsockets = true; # needed if you need to use WebSocket recommendedProxySettings = true; }; + locations."/register" = { + proxyPass = "http://${cfg.localAddress}:8085"; + recommendedProxySettings = true; + }; }; }; }; diff --git a/src/bussy/mod.rs b/src/bussy/mod.rs index e39ce40..738293d 100644 --- a/src/bussy/mod.rs +++ b/src/bussy/mod.rs @@ -1,15 +1,16 @@ use crate::{ noose::sled::BanInfo, - noose::user::{User, UserRow}, - utils::{error::Error, structs::Subscription}, + noose::user::{User, UserRow, Nip05Profile}, + 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 { 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"; @@ -18,18 +19,21 @@ pub mod channels { #[derive(Debug, Clone, PartialEq)] pub enum Command { // DbRequest + // --- Req DbReqWriteEvent(/* client_id */ uuid::Uuid, Box), DbReqFindEvent(/* client_id*/ uuid::Uuid, Subscription), DbReqDeleteEvents(/* client_id*/ uuid::Uuid, Box), DbReqEventCounts(/* client_id*/ uuid::Uuid, Subscription), - // Old messages DbReqInsertUser(UserRow), - DbReqGetUser(User), DbReqCreateAccount(XOnlyPublicKey, String, String), DbReqGetAccount(String), DbReqClear, - // DbResponse + // NIP-05 related messages + DbReqGetNIP05(String), + DbReqCreateNIP05(UserBody), + // --- Res + DbResNIP05(Nip05Profile), DbResRelayMessages( /* client_id*/ uuid::Uuid, /* Vec */ Vec, @@ -38,26 +42,48 @@ 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), + DbResGetContacts(BTreeSet), + // Event Pipeline + // --- Req PipelineReqEvent(/* client_id */ uuid::Uuid, Box), + // --- Res PipelineResRelayMessageOk(/* client_id */ uuid::Uuid, nostr::RelayMessage), PipelineResStreamOutEvent(Box), PipelineResOk, - // Subscription Errors - ClientSubscriptionError(/* error message */ String), + // Sled + // --- Req SledReqBanUser(Box), SledReqBanInfo(/* pubkey */ String), SledReqUnbanUser(/* pubkey */ String), SledReqGetBans, + // --- Res SledResBan(Option), SledResBans(Vec), - SledResSuccess(bool), + SledResSuccess(bool), + // Other + ServiceRegistrationRequired(/* client_id */ uuid::Uuid, nostr::RelayMessage), Str(String), ServiceError(Error), + + // Subscription Errors + ClientSubscriptionError(/* error message */ String), + + // --- Noop Noop, } @@ -86,7 +112,7 @@ impl PubSub { } pub async fn subscribe(&self, topic: &str) -> broadcast::Receiver { - let (tx, _rx) = broadcast::channel(32); // 32 is the channel capacity + let (tx, _rx) = broadcast::channel(20_000); // 20000 is the channel capacity let mut subscribers = self.subscribers.lock().await; subscribers .entry(topic.to_string()) diff --git a/src/noose/db.rs b/src/noose/db.rs index cb56a8a..fe48413 100644 --- a/src/noose/db.rs +++ b/src/noose/db.rs @@ -1,17 +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) -> Result<(), Error>; + /// Save event in the Database async fn write_event(&self, event: Box) -> Result; + /// Find events by subscription async fn find_event(&self, subscription: Subscription) -> Result, Error>; + /// Get event counts by subscription async fn counts(&self, subscription: Subscription) -> Result; + + /// Get NIP-05 of the registered User by 'username' + async fn get_nip05(&self, username: String) -> Result; + + /// 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; + + /// 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, Error>; + + /// Get Profie contacts list + async fn contacts(&self, public_key: XOnlyPublicKey) -> Result, Error>; } diff --git a/src/noose/migrations/1706575155557_nip05.sql b/src/noose/migrations/1706575155557_nip05.sql new file mode 100644 index 0000000..cb0314e --- /dev/null +++ b/src/noose/migrations/1706575155557_nip05.sql @@ -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); diff --git a/src/noose/migrations/1707327016995_nip42_profile.sql b/src/noose/migrations/1707327016995_nip42_profile.sql new file mode 100644 index 0000000..8a857d2 --- /dev/null +++ b/src/noose/migrations/1707327016995_nip42_profile.sql @@ -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); + diff --git a/src/noose/migrations/mod.rs b/src/noose/migrations/mod.rs index b9c7fdd..de317ca 100644 --- a/src/noose/migrations/mod.rs +++ b/src/noose/migrations/mod.rs @@ -11,7 +11,8 @@ 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_pragma = include_str!("./1697410424624_pragma.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), @@ -20,7 +21,8 @@ impl MigrationRunner { M::up(m_events_fts), M::up(m_users), M::up(m_unattached_media), - M::up(m_pragma), + M::up(m_nip05), + M::up(m_nip42), ]); match migrations.to_latest(connection) { diff --git a/src/noose/mod.rs b/src/noose/mod.rs index 8c260c0..cf386db 100644 --- a/src/noose/mod.rs +++ b/src/noose/mod.rs @@ -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) { diff --git a/src/noose/pipeline.rs b/src/noose/pipeline.rs index 0b0e189..b3c24c6 100644 --- a/src/noose/pipeline.rs +++ b/src/noose/pipeline.rs @@ -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.clone()).await { + match self.handle_event(client_id, event).await { Ok(_) => { log::info!("[Pipeline] handle_event completed"); channel = message.source; diff --git a/src/noose/sqlite.rs b/src/noose/sqlite.rs index 189dff5..821faed 100644 --- a/src/noose/sqlite.rs +++ b/src/noose/sqlite.rs @@ -1,197 +1,27 @@ -use super::{db::Noose, migrations::MigrationRunner}; +use super::{ + db::Noose, + migrations::MigrationRunner, + user::{Nip05Profile, Nip05Table, UserRow}, +}; 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> = 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() - } -} +use crate::noose::sqlite_tables::*; #[derive(Debug)] pub struct NostrSqlite { @@ -212,7 +42,22 @@ impl NostrSqlite { async fn run_migrations(pool: &Pool) -> bool { let connection = pool.get().await.unwrap(); - connection.interact(MigrationRunner::up).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 } async fn get_connection(&self) -> Result { @@ -496,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(), - ]) - .build_rusqlite(SqliteQueryBuilder); - if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) { - log::error!("Error inserting event into 'events' table: {}", err); + 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); - }; - - // 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()]) + } + + 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 'eventsFTS' table: {}", err); + if let Err(err) = tx.execute(sql.as_str(), &*value.as_params()) { tx.rollback().unwrap(); - + + log::debug!("Failed to store Profile"); 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 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'", + )); + }; - if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) - { - log::error!( - "Error inserting event into 'tags' table: {}", - err - ); - tx.rollback().unwrap(); + let metadata_custom_fields = serde_json::to_string(&metadata.custom); - return Ok(false); - } + 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); } } } @@ -844,6 +762,12 @@ 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 @@ -1059,7 +983,9 @@ impl NostrSqlite { let mut event_vec: Vec = vec![]; while let Ok(Some(row)) = rows.next() { let event = EventRow::from(row).to_event(); - event_vec.push(event); + if !event.is_expired() { + event_vec.push(event); + } } Ok(event_vec) @@ -1134,8 +1060,6 @@ 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); @@ -1144,9 +1068,10 @@ impl NostrSqlite { let mut event_vec: Vec = vec![]; while let Ok(Some(row)) = rows.next() { - let event_id_string: String = row.get(0).unwrap(); - let event_id = EventId::from_str(&event_id_string).unwrap(); - event_vec.push(event_id); + let event = EventRow::from(row).to_event(); + if !event.is_expired() { + event_vec.push(event.id); + } } Ok(event_vec) @@ -1176,35 +1101,39 @@ impl NostrSqlite { coordinate.kind, coordinate.pubkey, coordinate.identifier ) }; - + let Ok(query_result) = connection - .interact(move |conn: &mut rusqlite::Connection| -> Result { - 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 { + 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( @@ -1214,6 +1143,147 @@ impl NostrSqlite { query_result } + + async fn get_nip05_profile(&self, username: String) -> Result { + 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 { + 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 { + 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 { + 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 for Error { @@ -1272,7 +1342,8 @@ impl NostrDatabase for NostrSqlite { coordinate: &Coordinate, timestamp: Timestamp, ) -> Result { - self.has_coordinate_been_deleted(coordinate, timestamp).await + self.has_coordinate_been_deleted(coordinate, timestamp) + .await } /// Set [`EventId`] as seen by relay @@ -1339,6 +1410,7 @@ 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), @@ -1363,6 +1435,44 @@ 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 { @@ -1377,7 +1487,7 @@ impl Noose for NostrSqlite { message, channel ); - + pubsub.publish(channel, message).await; } } @@ -1386,7 +1496,6 @@ impl Noose for NostrSqlite { } async fn write_event(&self, event: Box) -> Result { - // 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, ""); @@ -1427,11 +1536,57 @@ impl Noose for NostrSqlite { Err(err) => Err(Error::internal_with_message(err.to_string())), } } + + async fn get_nip05(&self, username: String) -> Result { + 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 { + 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, Error> { + todo!() + } + + async fn contacts( + &self, + public_key: nostr::prelude::XOnlyPublicKey, + ) -> Result, Error> { + todo!() + } } #[cfg(test)] mod tests { use crate::noose::sqlite::*; + use nostr::key::FromSkStr; use nostr::EventBuilder; #[tokio::test] @@ -1656,6 +1811,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()); @@ -1696,4 +1874,14 @@ 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()); + // } } diff --git a/src/noose/sqlite_tables.rs b/src/noose/sqlite_tables.rs new file mode 100644 index 0000000..94b46f5 --- /dev/null +++ b/src/noose/sqlite_tables.rs @@ -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> = 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, + display_name: Option, + about: Option, + website: Option, + picture: Option, + banner: Option, + nip05: Option, + lud06: Option, + lud16: Option, + custom: Option, +} + +impl From<&Row<'_>> for ProfileRow { + fn from(row: &Row) -> Self { + let pubkey: String = row.get("pubkey").unwrap(); + let name: Option = row.get("name").unwrap_or_default(); + let display_name: Option = row.get("display_name").unwrap_or_default(); + let about = row.get("about").unwrap_or_default(); + let website: Option = row.get("website").unwrap_or_default(); + let picture: Option = row.get("picture").unwrap_or_default(); + let banner: Option = row.get("banner").unwrap_or_default(); + let nip05: Option = row.get("nip05").unwrap_or_default(); + let lud06: Option = row.get("lud06").unwrap_or_default(); + let lud16: Option = row.get("lud16").unwrap_or_default(); + let custom: Option = 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 = match row.custom { + Some(fields_str) => { + let fields: HashMap = + serde_json::from_str(&fields_str).unwrap_or(HashMap::new()); + fields + } + None => { + let f: HashMap = 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) + } +} diff --git a/src/noose/user.rs b/src/noose/user.rs index fe31020..d778b22 100644 --- a/src/noose/user.rs +++ b/src/noose/user.rs @@ -1,27 +1,103 @@ -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(); } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Validate)] +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)] pub struct UserRow { pub pubkey: String, pub username: String, - inserted_at: i64, - admin: bool, + relays: Vec, + joined_at: i64, } impl UserRow { - pub fn new(pubkey: String, username: String, admin: bool) -> Self { + pub fn new(pubkey: String, username: String, relays: Vec) -> Self { Self { pubkey, username, - inserted_at: Utc::now().timestamp(), - admin, + 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 = 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, + #[serde(skip_serializing_if = "Option::is_none")] + relays: Option>>, +} + +impl From for Nip05Profile { + fn from(value: UserRow) -> Self { + let mut name: HashMap = HashMap::new(); + name.insert(value.username, value.pubkey.clone()); + + let relay = match value.relays.is_empty() { + true => None, + false => { + let mut relay: HashMap> = HashMap::new(); + relay.insert(value.pubkey.clone(), value.relays); + + Some(relay) + } + }; + + Self { + names: name, + relays: relay, } } } @@ -41,3 +117,33 @@ 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 = match serde_json::from_str(relays_raw) { + Ok(val) => val, + Err(_) => vec![], + }; + + dbg!(relays); + } +} diff --git a/src/relay/handler.rs b/src/relay/handler.rs index 4295254..ea59ea2 100644 --- a/src/relay/handler.rs +++ b/src/relay/handler.rs @@ -11,18 +11,12 @@ 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) -> Result { +pub async fn relay_config(header: String, context: Context) -> Result { if header != "application/nostr+json" { Err(warp::reject::not_found()) } else { - 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)) + let config = context.config.get_relay_config_json(); + + Ok(warp::reply::json(&config)) } } diff --git a/src/relay/routes.rs b/src/relay/routes.rs index d30cfae..1607b2a 100644 --- a/src/relay/routes.rs +++ b/src/relay/routes.rs @@ -6,25 +6,26 @@ use warp::{Filter, Rejection, Reply}; pub fn routes(context: Context) -> impl Filter + 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 + Clone { // let real_client_ip = warp::header::optional::("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(); - 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)), - ) + 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) } fn static_files() -> impl Filter + Clone { diff --git a/src/relay/ws.rs b/src/relay/ws.rs index 30cd88a..3c103cc 100644 --- a/src/relay/ws.rs +++ b/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, @@ -211,23 +215,89 @@ 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) => handle_event(context, client, event).await, + ClientMessage::Event(event) => { + if needs_auth(context, client, Some(&event)) { + 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 needs_auth(context, client, None) { + 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 needs_auth(context, client, None) { + 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) { log::debug!("handle_event is processing new event"); @@ -328,14 +398,77 @@ 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) { - let message = Message::text("AUTH not implemented"); - send(client, message); +async fn handle_auth(context: &Context, client: &mut Client, event: Box) { + 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 } diff --git a/src/usernames/dto/mod.rs b/src/usernames/dto/mod.rs index eb8bc55..9b7d1a2 100644 --- a/src/usernames/dto/mod.rs +++ b/src/usernames/dto/mod.rs @@ -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,31 +10,26 @@ 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 } 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, -} - #[derive(Serialize, Deserialize, Debug, Validate, Clone)] pub struct UserQuery { #[validate(length(min = 1))] - pub user: String, + pub name: String, } #[derive(Serialize, Deserialize, Debug, Clone, Validate)] diff --git a/src/usernames/filter.rs b/src/usernames/filter.rs index b74c5a8..c4e6864 100644 --- a/src/usernames/filter.rs +++ b/src/usernames/filter.rs @@ -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( ) -> impl Filter + Copy { warp::body::json::().and_then(|query: T| async move { diff --git a/src/usernames/handler.rs b/src/usernames/handler.rs index b281d2e..a229aca 100644 --- a/src/usernames/handler.rs +++ b/src/usernames/handler.rs @@ -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, account: Account, @@ -45,14 +47,9 @@ pub async fn get_account( } pub async fn get_user(user_query: UserQuery, context: Context) -> Result { - let name = user_query.user; let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await; - let user = User { - name: Some(name), - pubkey: None, - }; - let command = Command::DbReqGetUser(user); + let command = Command::DbReqGetNIP05(user_query.name); context .pubsub .publish( @@ -65,18 +62,44 @@ pub async fn get_user(user_query: UserQuery, context: Context) -> Result { - response = json!({ - "names": { - user.username: user.pubkey - }, - "relays": {} - }); + Command::DbResNIP05(profile) => { + let response = serde_json::to_value(profile).unwrap(); Ok(warp::reply::json(&response)) } - Command::ServiceError(e) => 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 { + 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", ))), diff --git a/src/usernames/mod.rs b/src/usernames/mod.rs index 379fd9b..d692e03 100644 --- a/src/usernames/mod.rs +++ b/src/usernames/mod.rs @@ -1,4 +1,4 @@ -mod accounts; +// mod accounts; pub mod dto; mod filter; mod handler; diff --git a/src/usernames/routes.rs b/src/usernames/routes.rs index 96d8ad3..ecc435e 100644 --- a/src/usernames/routes.rs +++ b/src/usernames/routes.rs @@ -1,49 +1,69 @@ use crate::noose::user::User; -use super::accounts::create_account; -use super::dto::{Account, UserQuery}; +// use super::accounts::create_account; +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}; pub fn routes(context: Context) -> impl Filter + Clone { + let cors = warp::cors().allow_any_origin(); let index = warp::path::end().map(|| warp::reply::html("

SNEED!

")); index .or(nip05_get(context.clone())) - .or(account_create(context.clone())) + .or(nip05_create(context.clone())) + .with(&cors) } -pub fn nip05_get(context: Context) -> impl Filter + Clone { - warp::get() - .and(warp::path(".well-known")) - .and(warp::path("nostr.json")) +fn well_known(warp_method: M) -> impl Filter + Clone +where + M: (Filter) + Copy, +{ + warp_method.and(warp::path(".well-known")) +} + +fn nostr_well_known() -> impl Filter + Clone { + well_known(warp::get()).and(warp::path("nostr.json")) +} + +fn nip05_get(context: Context) -> impl Filter + Clone { + nostr_well_known() .and(validate_query_filter::()) - .and(with_context(context)) + .and(with_context(context.clone())) .and_then(get_user) } -pub fn account_create( - context: Context, -) -> impl Filter + Clone { - warp::path("account") - .and(warp::post()) - .and(validate_body_filter::()) - .and(with_context(context)) - .and_then(create_account) +fn nip05_create(context: Context) -> impl Filter + Clone { + well_known(warp::post()) + .and(warp::path("nostr.json")) + .and(warp::body::content_length_limit(1024)) + .and(validate_body_filter::()) + .and(with_context(context.clone())) + .and_then(create_user) } -pub fn account_get( - context: Context, -) -> impl Filter + Clone { - warp::path("account") - .and(warp::get()) - .and(validate_body_filter::()) - .and(with_context(context)) - .and_then(get_account) -} +// pub fn account_create( +// context: Context, +// ) -> impl Filter + Clone { +// warp::path("account") +// .and(warp::post()) +// .and(validate_body_filter::()) +// .and(with_context(context)) +// .and_then(create_account) +// } + +// pub fn account_get( +// context: Context, +// ) -> impl Filter + Clone { +// warp::path("account") +// .and(warp::get()) +// .and(validate_body_filter::()) +// .and(with_context(context)) +// .and_then(get_account) +// } // pub fn account_update( // context: Context, diff --git a/src/usernames/validators.rs b/src/usernames/validators.rs index 48f6cb9..b6dbcb7 100644 --- a/src/usernames/validators.rs +++ b/src/usernames/validators.rs @@ -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) -> Result<(), ValidationError> { + if relays.is_empty() { + return Ok(()) + } + + if relays.iter().all(validate_url) { + return Ok(()) + } + + Err(ValidationError::new("Relays have wrong url format")) +} diff --git a/src/utils/config.rs b/src/utils/config.rs index 3349957..ea25463 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr}; use nostr::{key::FromPkStr, secp256k1::XOnlyPublicKey}; @@ -6,6 +6,8 @@ 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 { @@ -23,13 +25,24 @@ 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 } @@ -38,14 +51,18 @@ 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, 9, 11, 12, 15, 16, 20, 22, 28, 33, 45 ], + "supported_nips": [ 1, 2, 4, 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.0" + "version": "0.1.1" }) } } diff --git a/src/utils/error.rs b/src/utils/error.rs index c801287..5812651 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -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, - /// Sneedstr version. - #[serde(skip_serializing_if = "Option::is_none")] - pub sneedstr_version: Option, + pub sneedstr_version: String, } impl StdError for Error { @@ -32,7 +32,7 @@ impl Error { Self { code: code.as_u16(), message, - sneedstr_version: None, + sneedstr_version: VERSION.to_string(), } } @@ -54,12 +54,11 @@ impl Error { Self::new(StatusCode::BAD_REQUEST, message) } - pub fn not_found(resource: &str, identifier: S, service_version: u16) -> Self { + pub fn not_found(resource: &str, identifier: S) -> Self { Self::new( StatusCode::NOT_FOUND, format!("{} not found by {}", resource, identifier), ) - .sneedstr_version(service_version) } pub fn invalid_param(name: &str, value: S) -> Self { @@ -77,11 +76,6 @@ 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 { @@ -116,28 +110,36 @@ mod tests { #[test] fn test_to_string() { let err = Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned()); - assert_eq!(err.to_string(), "400 Bad Request: invalid address") + assert_eq!( + err.to_string(), + "Error { code: 400, message: 'invalid address', sneedstr_version: \"0.1.1\" }" + ) } #[test] fn test_from_anyhow_error_as_internal_error() { let err = Error::from(anyhow::format_err!("hello")); - assert_eq!(err.to_string(), "500 Internal Server Error: hello") + assert_eq!( + err.to_string(), + "Error { code: 500, message: 'hello', sneedstr_version: \"0.1.1\" }" + ) } #[test] fn test_to_string_with_sneedstr_version() { - let err = - Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned()).sneedstr_version(123); + let err = Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned()); assert_eq!( err.to_string(), - "400 Bad Request: invalid address\ndiem ledger version: 123" + "Error { code: 400, message: 'invalid address', sneedstr_version: \"0.1.1\" }" ) } #[test] fn test_internal_error() { let err = Error::internal(anyhow::format_err!("hello")); - assert_eq!(err.to_string(), "500 Internal Server Error: hello") + assert_eq!( + err.to_string(), + "Error { code: 500, message: 'hello', sneedstr_version: \"0.1.1\" }" + ) } } diff --git a/src/utils/structs.rs b/src/utils/structs.rs index e2b91a2..48863d3 100644 --- a/src/utils/structs.rs +++ b/src/utils/structs.rs @@ -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>>, pub subscriptions: HashMap, + pub authenticated: bool, // NIP-42 + challlenge: Option, // 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, 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(); diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index f7ada7e..0000000 --- a/templates/index.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - Registration - - -
-

Create new user

-
-

Sign Up

-
- - - -
-
- - - -
- -
-
- - - diff --git a/templates/index.stpl b/templates/index.stpl deleted file mode 100644 index d044eaa..0000000 --- a/templates/index.stpl +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - Registration - - -
-

Create new user

-
-

Sign Up

-
- - - -
-
- - - -
- -
-
- - - diff --git a/templates/static/android-chrome-192x192.png b/templates/static/android-chrome-192x192.png deleted file mode 100644 index b87bdf7..0000000 Binary files a/templates/static/android-chrome-192x192.png and /dev/null differ diff --git a/templates/static/android-chrome-512x512.png b/templates/static/android-chrome-512x512.png deleted file mode 100644 index 407a549..0000000 Binary files a/templates/static/android-chrome-512x512.png and /dev/null differ diff --git a/templates/static/apple-touch-icon.png b/templates/static/apple-touch-icon.png deleted file mode 100644 index 075ba41..0000000 Binary files a/templates/static/apple-touch-icon.png and /dev/null differ diff --git a/templates/static/favicon-16x16.png b/templates/static/favicon-16x16.png deleted file mode 100644 index 2dd3e01..0000000 Binary files a/templates/static/favicon-16x16.png and /dev/null differ diff --git a/templates/static/favicon-32x32.png b/templates/static/favicon-32x32.png deleted file mode 100644 index 1027bdd..0000000 Binary files a/templates/static/favicon-32x32.png and /dev/null differ diff --git a/templates/static/favicon.ico b/templates/static/favicon.ico deleted file mode 100644 index 896232f..0000000 Binary files a/templates/static/favicon.ico and /dev/null differ diff --git a/templates/static/site.webmanifest b/templates/static/site.webmanifest deleted file mode 100644 index 45dc8a2..0000000 --- a/templates/static/site.webmanifest +++ /dev/null @@ -1 +0,0 @@ -{"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"} \ No newline at end of file