From f7b74bd22c4180eceac887df572c5e4ffeb8a54d Mon Sep 17 00:00:00 2001 From: Tony Klink Date: Thu, 8 Feb 2024 19:19:03 -0600 Subject: [PATCH] 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 --- flake.nix | 2 - modules/sneedstr.nix | 6 + src/bussy/mod.rs | 44 +- src/noose/db.rs | 41 +- src/noose/migrations/1706575155557_nip05.sql | 9 + .../1707327016995_nip42_profile.sql | 27 + src/noose/migrations/mod.rs | 2 + src/noose/mod.rs | 1 + src/noose/sqlite.rs | 573 +++++++++++------- src/noose/sqlite_tables.rs | 322 ++++++++++ src/relay/ws.rs | 107 +++- src/usernames/dto/mod.rs | 23 +- src/usernames/filter.rs | 4 - src/usernames/handler.rs | 38 +- src/usernames/routes.rs | 26 +- src/usernames/validators.rs | 16 +- src/utils/config.rs | 12 +- src/utils/structs.rs | 42 ++ 18 files changed, 1001 insertions(+), 294 deletions(-) create mode 100644 src/noose/migrations/1706575155557_nip05.sql create mode 100644 src/noose/migrations/1707327016995_nip42_profile.sql create mode 100644 src/noose/sqlite_tables.rs diff --git a/flake.nix b/flake.nix index ded426f..a0fed6d 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/ ''; }; diff --git a/modules/sneedstr.nix b/modules/sneedstr.nix index 4d901f5..57e9d27 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 = 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; diff --git a/src/bussy/mod.rs b/src/bussy/mod.rs index 358670e..b5f899e 100644 --- a/src/bussy/mod.rs +++ b/src/bussy/mod.rs @@ -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), DbReqFindEvent(/* client_id*/ uuid::Uuid, Subscription), DbReqDeleteEvents(/* client_id*/ uuid::Uuid, Box), 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 */ Vec, @@ -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), + 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, } diff --git a/src/noose/db.rs b/src/noose/db.rs index 29ee2dd..fe48413 100644 --- a/src/noose/db.rs +++ b/src/noose/db.rs @@ -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) -> 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 8247991..de317ca 100644 --- a/src/noose/migrations/mod.rs +++ b/src/noose/migrations/mod.rs @@ -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) { 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/sqlite.rs b/src/noose/sqlite.rs index ca114c0..77e037c 100644 --- a/src/noose/sqlite.rs +++ b/src/noose/sqlite.rs @@ -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> = 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(), - ]) - .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); } } } @@ -1265,6 +1185,105 @@ impl NostrSqlite { 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 { @@ -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 { 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] @@ -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()); 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/relay/ws.rs b/src/relay/ws.rs index 30cd88a..cdcb0da 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, @@ -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) { 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) { - let message = Message::text("AUTH not implemented"); - send(client, message); +async fn handle_auth(context: &Context, client: &mut Client, event: Box) { + 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 } diff --git a/src/usernames/dto/mod.rs b/src/usernames/dto/mod.rs index 166b658..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,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 } 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))] 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 193d8c3..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, @@ -47,7 +49,7 @@ pub async fn get_account( pub async fn get_user(user_query: UserQuery, context: Context) -> Result { 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 { + 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 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", + ))), + } + } else { + Err(warp::reject::custom(Error::internal_with_message( + "Unhandeled message type", + ))) + } +} diff --git a/src/usernames/routes.rs b/src/usernames/routes.rs index 0861a2e..ecc435e 100644 --- a/src/usernames/routes.rs +++ b/src/usernames/routes.rs @@ -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 impl Filter + Clone { - warp::get().and(warp::path(".well-known")) +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().and(warp::path("nostr.json")) + well_known(warp::get()).and(warp::path("nostr.json")) } -pub fn nip05_get(context: Context) -> impl Filter + Clone { +fn nip05_get(context: Context) -> impl Filter + Clone { nostr_well_known() .and(validate_query_filter::()) .and(with_context(context.clone())) .and_then(get_user) } +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_create( // context: Context, // ) -> impl Filter + Clone { 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 889ea3c..f3d3d22 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -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" }) diff --git a/src/utils/structs.rs b/src/utils/structs.rs index e2b91a2..cd90fdb 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, 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();