Add support for querying NIP-05 on /.well-known/nostr.json?name=username

This commit is contained in:
Tony Klink 2024-01-30 11:43:03 -06:00
parent e1306608ef
commit 377da44eed
Signed by: klink
GPG key ID: 85175567C4D19231
12 changed files with 278 additions and 101 deletions

View file

@ -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<nostr::Event>),

View file

@ -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<PubSub>) -> Result<(), Error>;
@ -14,4 +16,6 @@ pub trait Noose: Send + Sync {
async fn find_event(&self, subscription: Subscription) -> Result<Vec<RelayMessage>, Error>;
async fn counts(&self, subscription: Subscription) -> Result<RelayMessage, Error>;
async fn get_nip05(&self, username: String) -> Result<Nip05Profile, Error>;
}

View file

@ -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) {

View file

@ -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<Object, Error> {
@ -1225,6 +1223,48 @@ impl NostrSqlite {
query_result
}
async fn get_nip05_profile(&self, username: String) -> Result<Nip05Profile, Error> {
let Ok(connection) = self.get_connection().await else {
return Err(Error::internal_with_message("Unable to get DB connection"));
};
let Ok(query_result) = connection
.interact(
move |conn: &mut rusqlite::Connection| -> Result<Nip05Profile, Error> {
let (sql, value) = Query::select()
.from(Nip05Table::Table)
.columns([
Nip05Table::Pubkey,
Nip05Table::Username,
Nip05Table::Relays,
Nip05Table::JoinedAt,
])
.and_where(sea_query::Expr::col(Nip05Table::Username).eq(&username))
.limit(1)
.build_rusqlite(SqliteQueryBuilder);
let Ok(res) = conn.query_row(sql.as_str(), &*value.as_params(), |row| {
let nip05_row: UserRow = row.into();
let nip05 = Nip05Profile::from(nip05_row);
Ok(nip05)
}) else {
return Err(Error::not_found("user", username));
};
Ok(res)
},
)
.await
else {
return Err(Error::internal_with_message(
"Failed to execute query 'get_nip05_profile'",
));
};
query_result
}
}
impl From<nostr_database::DatabaseError> 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<Nip05Profile, Error> {
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());
// }
}

View file

@ -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<String>,
joined_at: i64,
}
impl UserRow {
pub fn new(pubkey: String, username: String, admin: bool) -> Self {
pub fn new(pubkey: String, username: String, relays: Vec<String>) -> 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<String> = match serde_json::from_str(&relays_raw) {
Ok(val) => val,
Err(_) => vec![],
};
Self {
pubkey,
username,
relays,
joined_at,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Nip05Profile {
names: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
relays: Option<HashMap<String, Vec<String>>>,
}
impl From<UserRow> for Nip05Profile {
fn from(value: UserRow) -> Self {
let mut name: HashMap<String, String> = HashMap::new();
name.insert(value.username, value.pubkey.clone());
let relay = match value.relays.is_empty() {
true => None,
false => {
let mut relay: HashMap<String, Vec<String>> = HashMap::new();
relay.insert(value.pubkey.clone(), value.relays);
Some(relay)
}
};
Self {
names: name,
relays: relay,
}
}
}
@ -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<String> = match serde_json::from_str(relays_raw) {
Ok(val) => val,
Err(_) => vec![],
};
dbg!(relays);
}
}

View file

@ -6,19 +6,24 @@ use warp::{Filter, Rejection, Reply};
pub fn routes(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
let cors = warp::cors().allow_any_origin();
static_files().or(index(context)).with(cors)
static_files().or(index(context)).with(&cors)
}
fn index(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
// let real_client_ip = warp::header::optional::<std::net::SocketAddr>("X-Real-IP");
let real_client_ip = warp::addr::remote();
let 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)
}

View file

@ -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)]

View file

@ -45,14 +45,9 @@ pub async fn get_account(
}
pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Reply, Rejection> {
let name = user_query.user;
let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
let 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<impl Re
.await;
if let Ok(message) = subscriber.recv().await {
let mut response = json!({"names": {}, "relays": {}});
match message.content {
Command::DbResUser(user) => {
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",
))),

View file

@ -1,4 +1,4 @@
mod accounts;
// mod accounts;
pub mod dto;
mod filter;
mod handler;

View file

@ -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<Extract = impl Reply, Error = Rejection> + Clone {
let cors = warp::cors().allow_any_origin();
let index = warp::path::end().map(|| warp::reply::html("<h1>SNEED!</h1>"));
index
.or(nip05_get(context.clone()))
.or(account_create(context.clone()))
// .or(account_create(context.clone()))
.with(&cors)
}
fn well_known() -> impl Filter<Extract = (), Error = Rejection> + Clone {
warp::get().and(warp::path(".well-known"))
}
fn nostr_well_known() -> impl Filter<Extract = (), Error = Rejection> + Clone {
well_known().and(warp::path("nostr.json"))
}
pub fn nip05_get(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::get()
.and(warp::path(".well-known"))
.and(warp::path("nostr.json"))
nostr_well_known()
.and(validate_query_filter::<UserQuery>())
.and(with_context(context))
.and(with_context(context.clone()))
.and_then(get_user)
}
pub fn account_create(
context: Context,
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::path("account")
.and(warp::post())
.and(validate_body_filter::<User>())
.and(with_context(context))
.and_then(create_account)
}
// pub fn account_create(
// context: Context,
// ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
// warp::path("account")
// .and(warp::post())
// .and(validate_body_filter::<User>())
// .and(with_context(context))
// .and_then(create_account)
// }
pub fn account_get(
context: Context,
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::path("account")
.and(warp::get())
.and(validate_body_filter::<Account>())
.and(with_context(context))
.and_then(get_account)
}
// pub fn account_get(
// context: Context,
// ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
// warp::path("account")
// .and(warp::get())
// .and(validate_body_filter::<Account>())
// .and(with_context(context))
// .and_then(get_account)
// }
// pub fn account_update(
// context: Context,

View file

@ -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"
})

View file

@ -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<u16>,
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<S: Display>(resource: &str, identifier: S, service_version: u16) -> Self {
pub fn not_found<S: Display>(resource: &str, identifier: S) -> Self {
Self::new(
StatusCode::NOT_FOUND,
format!("{} not found by {}", resource, identifier),
)
.sneedstr_version(service_version)
}
pub fn invalid_param<S: Display>(name: &str, value: S) -> Self {
@ -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\" }"
)
}
}