diff --git a/src/bussy/mod.rs b/src/bussy/mod.rs index e39ce40..358670e 100644 --- a/src/bussy/mod.rs +++ b/src/bussy/mod.rs @@ -1,6 +1,6 @@ use crate::{ noose::sled::BanInfo, - noose::user::{User, UserRow}, + noose::user::{User, UserRow, Nip05Profile}, utils::{error::Error, structs::Subscription}, }; use nostr::secp256k1::XOnlyPublicKey; @@ -25,10 +25,14 @@ pub enum Command { // Old messages DbReqInsertUser(UserRow), - DbReqGetUser(User), DbReqCreateAccount(XOnlyPublicKey, String, String), DbReqGetAccount(String), DbReqClear, + // NIP-05 related messages + DbReqGetUser(String), + DbResUser(Nip05Profile), + + // DbResponse DbResRelayMessages( /* client_id*/ uuid::Uuid, @@ -38,7 +42,6 @@ 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), // Event Pipeline PipelineReqEvent(/* client_id */ uuid::Uuid, Box), diff --git a/src/noose/db.rs b/src/noose/db.rs index cb56a8a..29ee2dd 100644 --- a/src/noose/db.rs +++ b/src/noose/db.rs @@ -6,6 +6,8 @@ use crate::{ use nostr::{Event, RelayMessage}; use std::sync::Arc; +use super::user::Nip05Profile; + pub trait Noose: Send + Sync { async fn start(&mut self, pubsub: Arc) -> Result<(), Error>; @@ -14,4 +16,6 @@ pub trait Noose: Send + Sync { async fn find_event(&self, subscription: Subscription) -> Result, Error>; async fn counts(&self, subscription: Subscription) -> Result; + + async fn get_nip05(&self, username: String) -> Result; } diff --git a/src/noose/migrations/mod.rs b/src/noose/migrations/mod.rs index b9c7fdd..8247991 100644 --- a/src/noose/migrations/mod.rs +++ b/src/noose/migrations/mod.rs @@ -11,7 +11,7 @@ impl MigrationRunner { let m_events_fts = include_str!("./1697410223576_events_fts.sql"); let m_users = include_str!("./1697410294265_users.sql"); let m_unattached_media = include_str!("./1697410480767_unattached_media.sql"); - let m_pragma = include_str!("./1697410424624_pragma.sql"); + let m_nip05 = include_str!("./1706575155557_nip05.sql"); let migrations = Migrations::new(vec![ M::up(m_create_events), @@ -20,7 +20,7 @@ impl MigrationRunner { M::up(m_events_fts), M::up(m_users), M::up(m_unattached_media), - M::up(m_pragma), + M::up(m_nip05), ]); match migrations.to_latest(connection) { diff --git a/src/noose/sqlite.rs b/src/noose/sqlite.rs index f352349..ca114c0 100644 --- a/src/noose/sqlite.rs +++ b/src/noose/sqlite.rs @@ -1,4 +1,8 @@ -use super::{db::Noose, migrations::MigrationRunner}; +use super::{ + db::Noose, + migrations::MigrationRunner, + user::{Nip05Profile, Nip05Table, UserRow}, +}; use crate::{ bussy::{channels, Command, Message, PubSub}, utils::{config::Config as ServiceConfig, error::Error, structs::Subscription}, @@ -149,27 +153,6 @@ impl sea_query::Iden for TagsTable { } } -// 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, @@ -212,7 +195,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 { @@ -1225,6 +1223,48 @@ 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 + } } impl From for Error { @@ -1351,6 +1391,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), @@ -1375,6 +1416,11 @@ impl Noose for NostrSqlite { Err(e) => Command::ServiceError(e), } } + // NIP-05 + Command::DbReqGetUser(username) => match self.get_nip05(username).await { + Ok(user) => Command::DbResUser(user), + Err(e) => Command::ServiceError(e), + }, _ => Command::Noop, }; if command != Command::Noop { @@ -1439,6 +1485,10 @@ 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 + } } #[cfg(test)] @@ -1708,4 +1758,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/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/routes.rs b/src/relay/routes.rs index c1159ca..1607b2a 100644 --- a/src/relay/routes.rs +++ b/src/relay/routes.rs @@ -6,19 +6,24 @@ 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 cors = warp::cors().allow_any_origin(); - let relay_information_document_path = warp::path::end().and(warp::header::header("Accept").and(with_context(context.clone())).and_then(handler::relay_config)).with(&cors); - let nostr_relay_path = warp::path::end().and(warp::ws().and(with_context(context.clone())) - .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) } diff --git a/src/usernames/dto/mod.rs b/src/usernames/dto/mod.rs index eb8bc55..166b658 100644 --- a/src/usernames/dto/mod.rs +++ b/src/usernames/dto/mod.rs @@ -36,7 +36,7 @@ pub struct Nip05 { #[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/handler.rs b/src/usernames/handler.rs index b281d2e..193d8c3 100644 --- a/src/usernames/handler.rs +++ b/src/usernames/handler.rs @@ -45,14 +45,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::DbReqGetUser(user_query.name); context .pubsub .publish( @@ -65,18 +60,12 @@ pub async fn get_user(user_query: UserQuery, context: Context) -> Result { - response = json!({ - "names": { - user.username: user.pubkey - }, - "relays": {} - }); + Command::DbResUser(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", ))), 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..0861a2e 100644 --- a/src/usernames/routes.rs +++ b/src/usernames/routes.rs @@ -1,6 +1,6 @@ use crate::noose::user::User; -use super::accounts::create_account; +// use super::accounts::create_account; use super::dto::{Account, UserQuery}; use super::filter::{validate_body_filter, validate_query_filter}; use super::handler::{get_account, get_user}; @@ -9,41 +9,49 @@ 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(account_create(context.clone())) + .with(&cors) +} + +fn well_known() -> impl Filter + Clone { + warp::get().and(warp::path(".well-known")) +} + +fn nostr_well_known() -> impl Filter + Clone { + well_known().and(warp::path("nostr.json")) } pub fn nip05_get(context: Context) -> impl Filter + Clone { - warp::get() - .and(warp::path(".well-known")) - .and(warp::path("nostr.json")) + 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) -} +// 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_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/utils/config.rs b/src/utils/config.rs index 9644b99..889ea3c 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -43,7 +43,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 ], + "supported_nips": [ 1, 2, 9, 11, 12, 15, 16, 20, 22, 28, 33, 40, 45, 50 ], "software": "git+https://git.zhitno.st/Klink/sneedstr.git", "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\" }" + ) } }