Implemented feature:

- NIP-42 Can be enabled in nix module or as environment variable 'CONFIG_ENABLE_AUTH'
 - NIP-05 Still WIP, but building up slowly
This commit is contained in:
Tony Klink 2024-02-08 19:19:03 -06:00
parent 377da44eed
commit f7b74bd22c
Signed by: klink
GPG key ID: 85175567C4D19231
18 changed files with 1001 additions and 294 deletions

View file

@ -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/
'';
};

View file

@ -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;

View file

@ -1,10 +1,10 @@
use crate::{
noose::sled::BanInfo,
noose::user::{User, UserRow, Nip05Profile},
utils::{error::Error, structs::Subscription},
utils::{error::Error, structs::Subscription}, usernames::dto::UserBody,
};
use nostr::secp256k1::XOnlyPublicKey;
use std::{collections::HashMap, fmt::Debug};
use std::{collections::{HashMap, BTreeSet}, fmt::Debug};
use tokio::sync::{broadcast, Mutex};
pub mod channels {
@ -18,22 +18,21 @@ pub mod channels {
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
// DbRequest
// --- Req
DbReqWriteEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),
DbReqFindEvent(/* client_id*/ uuid::Uuid, Subscription),
DbReqDeleteEvents(/* client_id*/ uuid::Uuid, Box<nostr::Event>),
DbReqEventCounts(/* client_id*/ uuid::Uuid, Subscription),
// Old messages
DbReqInsertUser(UserRow),
DbReqCreateAccount(XOnlyPublicKey, String, String),
DbReqGetAccount(String),
DbReqClear,
// NIP-05 related messages
DbReqGetUser(String),
DbResUser(Nip05Profile),
// DbResponse
DbReqGetNIP05(String),
DbReqCreateNIP05(UserBody),
// --- Res
DbResNIP05(Nip05Profile),
DbResRelayMessages(
/* client_id*/ uuid::Uuid,
/* Vec<RelayMessage::Event> */ Vec<nostr::RelayMessage>,
@ -43,24 +42,47 @@ pub enum Command {
DbResOkWithStatus(/* client_id */ uuid::Uuid, nostr::RelayMessage),
DbResAccount, // TODO: Add Account DTO as a param
DbResEventCounts(/* client_id */ uuid::Uuid, nostr::RelayMessage),
// Noose Profile
// --- Req
DbReqGetProfile(XOnlyPublicKey),
DbReqAddContact(/* profile_pub_key */ XOnlyPublicKey, /* contact_pub_key*/ XOnlyPublicKey),
DbReqRemoveContact(/* profile_pub_key */ XOnlyPublicKey, /* contact_pub_key*/ XOnlyPublicKey),
DbReqGetContactPubkeys(XOnlyPublicKey),
DbReqGetContacts(XOnlyPublicKey),
// --- Res
DbResGetProfile(nostr_database::Profile),
DbResGetContactPubkeys(Vec<XOnlyPublicKey>),
DbResGetContacts(BTreeSet<nostr_database::Profile>),
// Event Pipeline
// --- Req
PipelineReqEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),
// --- Res
PipelineResRelayMessageOk(/* client_id */ uuid::Uuid, nostr::RelayMessage),
PipelineResStreamOutEvent(Box<nostr::Event>),
PipelineResOk,
// Subscription Errors
ClientSubscriptionError(/* error message */ String),
// Sled
// --- Req
SledReqBanUser(Box<BanInfo>),
SledReqBanInfo(/* pubkey */ String),
SledReqUnbanUser(/* pubkey */ String),
SledReqGetBans,
// --- Res
SledResBan(Option<BanInfo>),
SledResBans(Vec<BanInfo>),
SledResSuccess(bool),
// Other
ServiceRegistrationRequired(/* client_id */ uuid::Uuid, nostr::RelayMessage),
Str(String),
ServiceError(Error),
// Subscription Errors
ClientSubscriptionError(/* error message */ String),
// --- Noop
Noop,
}

View file

@ -1,21 +1,58 @@
use crate::{
bussy::PubSub,
usernames::dto::UserBody,
utils::{error::Error, structs::Subscription},
};
use nostr::{Event, RelayMessage};
use std::sync::Arc;
use nostr::{secp256k1::XOnlyPublicKey, Event, RelayMessage};
use nostr_database::Profile;
use std::{collections::BTreeSet, sync::Arc};
use super::user::Nip05Profile;
/// Handle core nostr events
pub trait Noose: Send + Sync {
/// Start event listener
async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error>;
/// Save event in the Database
async fn write_event(&self, event: Box<Event>) -> Result<RelayMessage, Error>;
/// Find events by subscription
async fn find_event(&self, subscription: Subscription) -> Result<Vec<RelayMessage>, Error>;
/// Get event counts by subscription
async fn counts(&self, subscription: Subscription) -> Result<RelayMessage, Error>;
/// Get NIP-05 of the registered User by 'username'
async fn get_nip05(&self, username: String) -> Result<Nip05Profile, Error>;
/// Create new NIP-05 for the User
async fn create_nip05(&self, user: UserBody) -> Result<(), Error>;
/// Get Profile by public key
async fn profile(&self, public_key: XOnlyPublicKey) -> Result<Profile, Error>;
/// Add new contact to Profile
async fn add_contact(
&self,
profile_public_key: XOnlyPublicKey,
contact_public_key: XOnlyPublicKey,
) -> Result<(), Error>;
/// Remove contact from the Profile
async fn remove_contact(
&self,
profile_public_key: XOnlyPublicKey,
contact_public_key: XOnlyPublicKey,
) -> Result<(), Error>;
/// Get Profile contats (pubkeys)
async fn contacts_public_keys(
&self,
public_key: XOnlyPublicKey,
) -> Result<Vec<XOnlyPublicKey>, Error>;
/// Get Profie contacts list
async fn contacts(&self, public_key: XOnlyPublicKey) -> Result<BTreeSet<Profile>, Error>;
}

View file

@ -0,0 +1,9 @@
CREATE TABLE nip05 (
pubkey TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
relays TEXT,
joined_at INTEGER NOT NULL
);
CREATE INDEX idx_nip05_pubkey ON nip05 (pubkey);
CREATE INDEX idx_nip05_username ON nip05 (username);

View file

@ -0,0 +1,27 @@
CREATE TABLE profiles (
pubkey TEXT PRIMARY KEY
);
CREATE INDEX idx_profiles_pubkey ON profiles (pubkey);
create TABLE contacts (
profile TEXT REFERENCES profiles(pubkey) ON DELETE CASCADE,
contact TEXT REFERENCES profiles(pubkey) ON DELETE CASCADE
);
CREATE TABLE metadata (
pubkey TEXT REFERENCES profiles(pubkey) ON DELETE CASCADE,
name TEXT,
display_name TEXT,
about TEXT,
website TEXT,
picture TEXT,
banner TEXT,
nip05 TEXT,
lud06 TEXT,
lud16 TEXT,
custom TEXT
);
CREATE INDEX idx_metadata_profiles_pubkey ON metadata (pubkey);

View file

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

View file

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

View file

@ -5,176 +5,23 @@ use super::{
};
use crate::{
bussy::{channels, Command, Message, PubSub},
usernames::dto::UserBody,
utils::{config::Config as ServiceConfig, error::Error, structs::Subscription},
};
use async_trait::async_trait;
use deadpool_sqlite::{Config, Object, Pool, Runtime};
use nostr::{
nips::nip01::Coordinate, Event, EventId, Filter, RelayMessage, TagKind, Timestamp, Url,
nips::nip01::Coordinate, secp256k1::XOnlyPublicKey, Event, EventId, Filter, JsonUtil,
RelayMessage, TagKind, Timestamp, Url,
};
use nostr_database::{Backend, DatabaseOptions, NostrDatabase, Order};
use rusqlite::Row;
use sea_query::{extension::sqlite::SqliteExpr, Order as SqOrder, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder;
use std::sync::Arc;
use std::{collections::HashSet, str::FromStr};
#[derive(Debug, Clone)]
struct EventRow {
id: String,
pubkey: String,
created_at: i64,
kind: i64,
tags: String,
sig: String,
content: String,
}
impl From<&Row<'_>> for EventRow {
fn from(row: &Row) -> Self {
let id: String = row.get("id").unwrap();
let pubkey: String = row.get("pubkey").unwrap();
let created_at: i64 = row.get("created_at").unwrap();
let kind: i64 = row.get("kind").unwrap();
let tags: String = row.get("tags").unwrap();
let sig: String = row.get("sig").unwrap();
let content: String = row.get("content").unwrap();
Self {
id,
pubkey,
created_at,
kind,
tags,
sig,
content,
}
}
}
impl From<&EventRow> for Event {
fn from(row: &EventRow) -> Self {
row.to_event()
}
}
impl EventRow {
pub fn to_event(&self) -> Event {
let tags: Vec<Vec<String>> = serde_json::from_str(&self.tags).unwrap();
let event_json = serde_json::json!(
{
"id": self.id,
"content": self.content,
"created_at": self.created_at,
"kind": self.kind,
"pubkey": self.pubkey,
"sig": self.sig,
"tags": tags
}
);
Event::from_value(event_json).unwrap()
}
}
enum EventsTable {
Table,
EventId,
Kind,
Pubkey,
Content,
CreatedAt,
Tags,
Sig,
}
impl sea_query::Iden for EventsTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "events",
Self::EventId => "id",
Self::Kind => "kind",
Self::Pubkey => "pubkey",
Self::Content => "content",
Self::CreatedAt => "created_at",
Self::Tags => "tags",
Self::Sig => "sig",
}
)
.unwrap()
}
}
enum EventsFTSTable {
Table,
EventId,
Content,
}
impl sea_query::Iden for EventsFTSTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "events_fts",
Self::EventId => "id",
Self::Content => "content",
}
)
.unwrap()
}
}
enum TagsTable {
Table,
Tag,
Value,
EventId,
}
impl sea_query::Iden for TagsTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "tags",
Self::Tag => "tag",
Self::Value => "value",
Self::EventId => "event_id",
}
)
.unwrap()
}
}
enum EventSeenByRelaysTable {
Table,
Id,
EventId,
RelayURL,
}
impl sea_query::Iden for EventSeenByRelaysTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "event_seen_by_relays",
Self::Id => "id",
Self::EventId => "event_id",
Self::RelayURL => "relay_url",
}
)
.unwrap()
}
}
use crate::noose::sqlite_tables::*;
#[derive(Debug)]
pub struct NostrSqlite {
@ -494,86 +341,159 @@ impl NostrSqlite {
return Ok(false);
}
} else {
log::debug!("inserting new event in events");
// Insert into Events table
let (sql, values) = Query::insert()
.into_table(EventsTable::Table)
.columns([
EventsTable::EventId,
EventsTable::Content,
EventsTable::Kind,
EventsTable::Pubkey,
EventsTable::CreatedAt,
EventsTable::Tags,
EventsTable::Sig,
])
.values_panic([
id.clone().into(),
content.clone().into(),
kind.into(),
pubkey.into(),
created_at.into(),
tags.into(),
sig.into(),
])
match tx.commit() {
Ok(_) => return Ok(true),
Err(err) => {
log::error!("Error during transaction commit: {}", err);
return Ok(false);
}
}
}
if event.kind() == nostr::Kind::Metadata {
// Try to delete old profile first
let (sql, value) = Query::delete()
.from_table(ProfilesTable::Table)
.and_where(sea_query::Expr::col(ProfilesTable::Pubkey).eq(pubkey.clone()))
.build_rusqlite(SqliteQueryBuilder);
if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
log::error!("Error inserting event into 'events' table: {}", err);
tx.rollback().unwrap();
return Ok(false);
};
// Insert into EventsFTS table
log::debug!("inserting new event into eventsFTS");
let (sql, values) = Query::insert()
.into_table(EventsFTSTable::Table)
.columns([EventsFTSTable::EventId, EventsFTSTable::Content])
.values_panic([id.clone().into(), content.into()])
.build_rusqlite(SqliteQueryBuilder);
if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
log::error!("Error inserting event into 'eventsFTS' table: {}", err);
if let Err(err) = tx.execute(sql.as_str(), &*value.as_params()) {
tx.rollback().unwrap();
log::debug!("Failed to delete old Profile record: {}", err);
return Ok(false);
}
// Insert into Tags table
log::debug!("inserting new event into tags");
for tag in event.tags.clone() {
if Self::tag_is_indexable(&tag) {
let tag = tag.to_vec();
if tag.len() >= 2 {
let tag_name = &tag[0];
let tag_value = &tag[1];
if tag_name.len() == 1 {
let (sql, values) = Query::insert()
.into_table(TagsTable::Table)
.columns([
TagsTable::Tag,
TagsTable::Value,
TagsTable::EventId,
])
.values_panic([
tag_name.into(),
tag_value.into(),
id.clone().into(),
])
.build_rusqlite(SqliteQueryBuilder);
let (sql, value) = Query::insert()
.into_table(ProfilesTable::Table)
.columns([ProfilesTable::Pubkey])
.values_panic([pubkey.clone().into()])
.build_rusqlite(SqliteQueryBuilder);
if let Err(err) = tx.execute(sql.as_str(), &*values.as_params())
{
log::error!(
"Error inserting event into 'tags' table: {}",
err
);
tx.rollback().unwrap();
if let Err(err) = tx.execute(sql.as_str(), &*value.as_params()) {
tx.rollback().unwrap();
return Ok(false);
}
log::debug!("Failed to store Profile");
return Ok(false);
}
let Ok(metadata) = nostr::Metadata::from_json(content.clone()) else {
log::debug!("Failed to parse metadata");
return Err(Error::bad_request(
"Unable to parse metadata from 'content'",
));
};
let metadata_custom_fields = serde_json::to_string(&metadata.custom);
let (sql, value) = Query::insert()
.into_table(MetadataTable::Table)
.columns([
MetadataTable::Pubkey,
MetadataTable::Name,
MetadataTable::DisplayName,
MetadataTable::About,
MetadataTable::Website,
MetadataTable::Picture,
MetadataTable::Banner,
MetadataTable::Nip05,
MetadataTable::Lud06,
MetadataTable::Lud16,
MetadataTable::Custom,
])
.values_panic([
pubkey.clone().into(),
metadata.name.unwrap_or_default().into(),
metadata.display_name.unwrap_or_default().into(),
metadata.about.unwrap_or_default().into(),
metadata.website.unwrap_or_default().into(),
metadata.picture.unwrap_or_default().into(),
metadata.banner.unwrap_or_default().into(),
metadata.nip05.unwrap_or_default().into(),
metadata.lud06.unwrap_or_default().into(),
metadata.lud16.unwrap_or_default().into(),
metadata_custom_fields.unwrap_or_default().into(),
])
.build_rusqlite(SqliteQueryBuilder);
if let Err(err) = tx.execute(sql.as_str(), &*value.as_params()) {
tx.rollback().unwrap();
log::debug!("Failed to store Metadata: {}", err);
return Ok(false);
}
}
log::debug!("inserting new event in events");
// Insert into Events table
let (sql, values) = Query::insert()
.into_table(EventsTable::Table)
.columns([
EventsTable::EventId,
EventsTable::Content,
EventsTable::Kind,
EventsTable::Pubkey,
EventsTable::CreatedAt,
EventsTable::Tags,
EventsTable::Sig,
])
.values_panic([
id.clone().into(),
content.clone().into(),
kind.into(),
pubkey.into(),
created_at.into(),
tags.into(),
sig.into(),
])
.build_rusqlite(SqliteQueryBuilder);
if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
log::error!("Error inserting event into 'events' table: {}", err);
tx.rollback().unwrap();
return Ok(false);
};
// Insert into EventsFTS table
log::debug!("inserting new event into eventsFTS");
let (sql, values) = Query::insert()
.into_table(EventsFTSTable::Table)
.columns([EventsFTSTable::EventId, EventsFTSTable::Content])
.values_panic([id.clone().into(), content.into()])
.build_rusqlite(SqliteQueryBuilder);
if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
log::error!("Error inserting event into 'eventsFTS' table: {}", err);
tx.rollback().unwrap();
return Ok(false);
}
// Insert into Tags table
log::debug!("inserting new event into tags");
for tag in event.tags.clone() {
if Self::tag_is_indexable(&tag) {
let tag = tag.to_vec();
if tag.len() >= 2 {
let tag_name = &tag[0];
let tag_value = &tag[1];
if tag_name.len() == 1 {
let (sql, values) = Query::insert()
.into_table(TagsTable::Table)
.columns([TagsTable::Tag, TagsTable::Value, TagsTable::EventId])
.values_panic([
tag_name.into(),
tag_value.into(),
id.clone().into(),
])
.build_rusqlite(SqliteQueryBuilder);
if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
log::error!("Error inserting event into 'tags' table: {}", err);
tx.rollback().unwrap();
return Ok(false);
}
}
}
@ -1265,6 +1185,105 @@ impl NostrSqlite {
query_result
}
async fn get_profile(
&self,
public_key: XOnlyPublicKey,
) -> Result<nostr_database::Profile, Error> {
let pk = public_key.to_string();
let Ok(connection) = self.get_connection().await else {
return Err(Error::internal_with_message("Unable to get DB connection"));
};
let Ok(query_result) = connection
.interact(
move |conn: &mut rusqlite::Connection| -> Result<nostr_database::Profile, Error> {
let (sql, value) = Query::select()
.from(MetadataTable::Table)
.columns([
MetadataTable::Pubkey,
MetadataTable::Name,
MetadataTable::DisplayName,
MetadataTable::About,
MetadataTable::Website,
MetadataTable::Picture,
MetadataTable::Banner,
MetadataTable::Nip05,
MetadataTable::Lud06,
MetadataTable::Lud16,
MetadataTable::Custom,
])
.and_where(sea_query::Expr::col(MetadataTable::Pubkey).eq(&pk))
.limit(1)
.build_rusqlite(SqliteQueryBuilder);
let Ok(res) = conn.query_row(sql.as_str(), &*value.as_params(), |row| {
let profile_row: ProfileRow = row.into();
let profile = nostr_database::Profile::from(&profile_row);
Ok(profile)
}) else {
return Err(Error::not_found("user", pk));
};
Ok(res)
},
)
.await
else {
return Err(Error::internal_with_message(
"Failed to execute query 'get_profile'",
));
};
query_result
}
async fn create_nip05_profile(&self, user: UserBody) -> Result<(), Error> {
let Ok(connection) = self.get_connection().await else {
return Err(Error::internal_with_message("Unable to get DB connection"));
};
let Ok(query_result) = connection
.interact(
move |conn: &mut rusqlite::Connection| -> Result<(), Error> {
let (sql, values) = Query::insert()
.into_table(Nip05Table::Table)
.columns([
Nip05Table::Username,
Nip05Table::Pubkey,
Nip05Table::Relays,
Nip05Table::JoinedAt,
])
.values_panic([
user.name.clone().into(),
user.get_pubkey().into(),
serde_json::to_string(&user.relays).unwrap().into(),
nostr::Timestamp::now().as_i64().into(),
])
.build_rusqlite(SqliteQueryBuilder);
match conn.execute(sql.as_str(), &*values.as_params()) {
Ok(_) => Ok(()),
Err(e) => {
if e.to_string().contains("nip05.pubkey") {
return Err(Error::invalid_request_body("pubkey already taken"));
}
Err(Error::invalid_request_body("name already taken"))
}
}
},
)
.await
else {
return Err(Error::internal_with_message(
"Failed to execute query 'get_nip05_profile'",
));
};
query_result
}
}
impl From<nostr_database::DatabaseError> for Error {
@ -1417,10 +1436,43 @@ impl Noose for NostrSqlite {
}
}
// NIP-05
Command::DbReqGetUser(username) => match self.get_nip05(username).await {
Ok(user) => Command::DbResUser(user),
Command::DbReqGetNIP05(username) => match self.get_nip05(username).await {
Ok(user) => Command::DbResNIP05(user),
Err(e) => Command::ServiceError(e),
},
Command::DbReqCreateNIP05(user) => match self.create_nip05(user).await {
Ok(_) => Command::DbResOk,
Err(e) => Command::ServiceError(e),
},
// NIP-42
Command::DbReqGetProfile(pubkey) => match self.profile(pubkey).await {
Ok(profile) => Command::DbResGetProfile(profile),
Err(e) => Command::ServiceError(e),
},
Command::DbReqAddContact(profile_pub_key, contact_pub_key) => {
match self.add_contact(profile_pub_key, contact_pub_key).await {
Ok(_) => Command::DbResOk,
Err(e) => Command::ServiceError(e),
}
}
Command::DbReqRemoveContact(profile_pub_key, contact_pub_key) => {
match self.remove_contact(profile_pub_key, contact_pub_key).await {
Ok(_) => Command::DbResOk,
Err(e) => Command::ServiceError(e),
}
}
Command::DbReqGetContactPubkeys(profile_pub_key) => {
match self.contacts_public_keys(profile_pub_key).await {
Ok(contact_pubkeys) => Command::DbResGetContactPubkeys(contact_pubkeys),
Err(e) => Command::ServiceError(e),
}
}
Command::DbReqGetContacts(profile_pub_key) => {
match self.contacts(profile_pub_key).await {
Ok(contacts) => Command::DbResGetContacts(contacts),
Err(e) => Command::ServiceError(e),
}
}
_ => Command::Noop,
};
if command != Command::Noop {
@ -1489,11 +1541,53 @@ impl Noose for NostrSqlite {
async fn get_nip05(&self, username: String) -> Result<Nip05Profile, Error> {
self.get_nip05_profile(username).await
}
async fn create_nip05(&self, user: UserBody) -> Result<(), Error> {
self.create_nip05_profile(user).await
}
async fn profile(
&self,
public_key: nostr::prelude::XOnlyPublicKey,
) -> Result<nostr_database::Profile, Error> {
self.get_profile(public_key).await
}
async fn add_contact(
&self,
profile_public_key: nostr::prelude::XOnlyPublicKey,
contact_public_key: nostr::prelude::XOnlyPublicKey,
) -> Result<(), Error> {
todo!()
}
async fn remove_contact(
&self,
profile_public_key: nostr::prelude::XOnlyPublicKey,
contact_public_key: nostr::prelude::XOnlyPublicKey,
) -> Result<(), Error> {
todo!()
}
async fn contacts_public_keys(
&self,
public_key: nostr::prelude::XOnlyPublicKey,
) -> Result<Vec<nostr::prelude::XOnlyPublicKey>, Error> {
todo!()
}
async fn contacts(
&self,
public_key: nostr::prelude::XOnlyPublicKey,
) -> Result<std::collections::BTreeSet<nostr_database::Profile>, Error> {
todo!()
}
}
#[cfg(test)]
mod tests {
use crate::noose::sqlite::*;
use nostr::key::FromSkStr;
use nostr::EventBuilder;
#[tokio::test]
@ -1718,6 +1812,29 @@ mod tests {
assert_eq!(result.len(), 1)
}
#[tokio::test]
async fn save_metadata_event() {
let config = Arc::new(ServiceConfig::new());
let db = NostrSqlite::new(config).await;
let secret_key = "nsec1g24e83hwj5gxl0hqxx9wllwcg9rrxthssv0mrxf4dv3lt8dc29yqrxf09p";
let keys = nostr::Keys::from_sk_str(secret_key).unwrap();
// Insert
let metadata = nostr::Metadata::new()
.name("Chuck")
.custom_field("feed", "seed");
let event = nostr::EventBuilder::metadata(&metadata)
.to_event(&keys)
.unwrap();
let result = db.save_event(&event).await.unwrap();
// Find profile by pk
let pubkey = keys.public_key();
let res = db.get_profile(pubkey).await.unwrap();
assert_eq!(res.name(), "Sneed");
}
#[tokio::test]
async fn save_event_with_a_tag() {
let config = Arc::new(ServiceConfig::new());

322
src/noose/sqlite_tables.rs Normal file
View file

@ -0,0 +1,322 @@
use std::collections::HashMap;
use nostr::{key::FromPkStr, Event, Metadata};
use nostr_database::Profile;
use rusqlite::Row;
#[derive(Debug, Clone)]
pub struct EventRow {
pub id: String,
pub pubkey: String,
pub created_at: i64,
pub kind: i64,
pub tags: String,
pub sig: String,
pub content: String,
}
impl From<&Row<'_>> for EventRow {
fn from(row: &Row) -> Self {
let id: String = row.get("id").unwrap();
let pubkey: String = row.get("pubkey").unwrap();
let created_at: i64 = row.get("created_at").unwrap();
let kind: i64 = row.get("kind").unwrap();
let tags: String = row.get("tags").unwrap();
let sig: String = row.get("sig").unwrap();
let content: String = row.get("content").unwrap();
Self {
id,
pubkey,
created_at,
kind,
tags,
sig,
content,
}
}
}
impl From<&EventRow> for Event {
fn from(row: &EventRow) -> Self {
row.to_event()
}
}
impl EventRow {
pub fn to_event(&self) -> Event {
let tags: Vec<Vec<String>> = serde_json::from_str(&self.tags).unwrap();
let event_json = serde_json::json!(
{
"id": self.id,
"content": self.content,
"created_at": self.created_at,
"kind": self.kind,
"pubkey": self.pubkey,
"sig": self.sig,
"tags": tags
}
);
Event::from_value(event_json).unwrap()
}
}
pub enum EventsTable {
Table,
EventId,
Kind,
Pubkey,
Content,
CreatedAt,
Tags,
Sig,
}
impl sea_query::Iden for EventsTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "events",
Self::EventId => "id",
Self::Kind => "kind",
Self::Pubkey => "pubkey",
Self::Content => "content",
Self::CreatedAt => "created_at",
Self::Tags => "tags",
Self::Sig => "sig",
}
)
.unwrap()
}
}
pub enum EventsFTSTable {
Table,
EventId,
Content,
}
impl sea_query::Iden for EventsFTSTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "events_fts",
Self::EventId => "id",
Self::Content => "content",
}
)
.unwrap()
}
}
pub enum TagsTable {
Table,
Tag,
Value,
EventId,
}
impl sea_query::Iden for TagsTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "tags",
Self::Tag => "tag",
Self::Value => "value",
Self::EventId => "event_id",
}
)
.unwrap()
}
}
pub enum EventSeenByRelaysTable {
Table,
Id,
EventId,
RelayURL,
}
impl sea_query::Iden for EventSeenByRelaysTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "event_seen_by_relays",
Self::Id => "id",
Self::EventId => "event_id",
Self::RelayURL => "relay_url",
}
)
.unwrap()
}
}
pub enum ProfilesTable {
Table,
Pubkey,
}
impl sea_query::Iden for ProfilesTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "profiles",
Self::Pubkey => "pubkey",
}
)
.unwrap()
}
}
pub enum ContactsTable {
Table,
Profile,
Contact,
}
impl sea_query::Iden for ContactsTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "contacts",
Self::Profile => "profile",
Self::Contact => "contact",
}
)
.unwrap()
}
}
pub enum MetadataTable {
Table,
Pubkey,
Name,
DisplayName,
About,
Website,
Picture,
Banner,
Nip05,
Lud06,
Lud16,
Custom,
}
impl sea_query::Iden for MetadataTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "metadata",
Self::Pubkey => "pubkey",
Self::Name => "name",
Self::DisplayName => "display_name",
Self::About => "about",
Self::Website => "website",
Self::Picture => "picture",
Self::Banner => "banner",
Self::Nip05 => "nip05",
Self::Lud06 => "lud06",
Self::Lud16 => "lud16",
Self::Custom => "custom",
}
)
.unwrap()
}
}
#[derive(Debug, Clone)]
pub struct ProfileRow {
pubkey: String,
// metadata
name: Option<String>,
display_name: Option<String>,
about: Option<String>,
website: Option<String>,
picture: Option<String>,
banner: Option<String>,
nip05: Option<String>,
lud06: Option<String>,
lud16: Option<String>,
custom: Option<String>,
}
impl From<&Row<'_>> for ProfileRow {
fn from(row: &Row) -> Self {
let pubkey: String = row.get("pubkey").unwrap();
let name: Option<String> = row.get("name").unwrap_or_default();
let display_name: Option<String> = row.get("display_name").unwrap_or_default();
let about = row.get("about").unwrap_or_default();
let website: Option<String> = row.get("website").unwrap_or_default();
let picture: Option<String> = row.get("picture").unwrap_or_default();
let banner: Option<String> = row.get("banner").unwrap_or_default();
let nip05: Option<String> = row.get("nip05").unwrap_or_default();
let lud06: Option<String> = row.get("lud06").unwrap_or_default();
let lud16: Option<String> = row.get("lud16").unwrap_or_default();
let custom: Option<String> = row.get("custom").unwrap_or_default();
Self {
pubkey,
name,
display_name,
about,
website,
picture,
banner,
nip05,
lud06,
lud16,
custom,
}
}
}
impl From<&ProfileRow> for Profile {
fn from(row: &ProfileRow) -> Self {
let row = row.to_owned();
// let f = nostr::EventBuilder::metadata( // Why am I creating this methods in Noose? Just store Kind 0 on a relay and think that the user is registered on the relay ffs
let keys = nostr::Keys::from_pk_str(&row.pubkey).unwrap();
let custom_fields: HashMap<String, serde_json::Value> = match row.custom {
Some(fields_str) => {
let fields: HashMap<String, serde_json::Value> =
serde_json::from_str(&fields_str).unwrap_or(HashMap::new());
fields
}
None => {
let f: HashMap<String, serde_json::Value> = HashMap::new();
f
}
};
let metadata = Metadata {
name: row.name,
display_name: row.display_name,
about: row.about,
website: row.website,
picture: row.picture,
banner: row.banner,
nip05: row.nip05,
lud06: row.lud06,
lud16: row.lud16,
custom: custom_fields,
};
nostr_database::Profile::new(keys.public_key(), metadata)
}
}

View file

@ -94,6 +94,17 @@ pub async fn client_connection(
}
}
},
crate::bussy::Command::ServiceRegistrationRequired(client_id, relay_message) => {
if client.client_id == client_id {
if let Some(sender) = &client.client_connection {
if !sender.is_closed() {
log::info!("[Relay] client needs to be authenticated to make request: {}", relay_message.as_json());
sender.send(Ok(Message::text(relay_message.as_json()))).unwrap();
}
}
}
},
_ => ()
}
@ -101,13 +112,6 @@ pub async fn client_connection(
Some(message) = client_receiver.next() => {
match message {
Ok(message) => {
// ws_sender
// .send(message)
// .unwrap_or_else(|e| {
// log::error!("websocket send error: {}", e);
// })
// .await;
match ws_sender.send(message).await {
Ok(_) => (),
Err(e) => {
@ -194,7 +198,7 @@ async fn socket_on_message(context: &Context, client: &mut Client, msg: Message)
}
};
log::info!(
log::debug!(
"[client {} - {}] message: {}",
client.ip(),
client.client_id,
@ -213,21 +217,63 @@ fn send(client: &Client, message: Message) {
async fn handle_msg(context: &Context, client: &mut Client, client_message: ClientMessage) {
match client_message {
ClientMessage::Event(event) => handle_event(context, client, event).await,
ClientMessage::Event(event) => {
if context.config.auth_required()
&& event.kind() != nostr::Kind::Metadata
&& !client.authenticated
{
request_auth(context, client).await;
return;
}
handle_event(context, client, event).await
}
ClientMessage::Req {
subscription_id,
filters,
} => handle_req(context, client, subscription_id, filters).await,
} => {
if context.config.auth_required() && !client.authenticated {
request_auth(context, client).await;
return;
}
handle_req(context, client, subscription_id, filters).await
}
ClientMessage::Count {
subscription_id,
filters,
} => handle_count(context, client, subscription_id, filters).await,
} => {
if context.config.auth_required() && !client.authenticated {
request_auth(context, client).await;
return;
}
handle_count(context, client, subscription_id, filters).await
}
ClientMessage::Close(subscription_id) => handle_close(client, subscription_id).await,
ClientMessage::Auth(event) => handle_auth(client, event).await,
_ => (),
ClientMessage::Auth(event) => handle_auth(context, client, event).await,
// Unhandled messages
_ => unhandled_message(context, client).await,
}
}
async fn request_auth(context: &Context, client: &mut Client) {
let challenge = uuid::Uuid::new_v4().to_string();
client.set_challenge(challenge.clone());
let auth_message = nostr::RelayMessage::auth(challenge);
context
.pubsub
.publish(
channels::MSG_RELAY,
crate::bussy::Message {
source: channels::MSG_RELAY,
content: crate::bussy::Command::ServiceRegistrationRequired(
client.client_id,
auth_message,
),
},
)
.await;
}
async fn handle_event(context: &Context, client: &Client, event: Box<Event>) {
log::debug!("handle_event is processing new event");
@ -328,14 +374,35 @@ async fn handle_count(
}
async fn handle_close(client: &mut Client, subscription_id: SubscriptionId) {
// context.pubsub.send(new nostr event) then handle possible errors
client.unsubscribe(subscription_id);
// let message = Message::text("CLOSE not implemented");
// send(client, message);
}
async fn handle_auth(client: &Client, event: Box<Event>) {
let message = Message::text("AUTH not implemented");
send(client, message);
async fn handle_auth(context: &Context, client: &mut Client, event: Box<Event>) {
client.authenticate(&event);
let client_status = format!("Client authenticated: {}", client.authenticated);
let message = nostr::RelayMessage::notice(client_status);
context
.pubsub
.publish(
channels::MSG_RELAY,
crate::bussy::Message {
source: channels::MSG_RELAY,
content: crate::bussy::Command::DbResOkWithStatus(client.client_id, message),
},
)
.await;
}
async fn unhandled_message(context: &Context, client: &Client) {
let message = nostr::RelayMessage::notice("Unsupported Message");
context
.pubsub
.publish(
channels::MSG_RELAY,
crate::bussy::Message {
source: channels::MSG_RELAY,
content: crate::bussy::Command::DbResOkWithStatus(client.client_id, message),
},
)
.await
}

View file

@ -1,9 +1,7 @@
use std::collections::HashMap;
use crate::usernames::validators::validate_pubkey;
use crate::usernames::validators::{validate_pubkey, validate_relays};
use crate::utils::error::Error;
use nostr::key::XOnlyPublicKey;
use nostr::prelude::*;
use nostr::{key::XOnlyPublicKey, Keys};
use regex::Regex;
use serde::{Deserialize, Serialize};
use validator::Validate;
@ -12,27 +10,22 @@ lazy_static! {
static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap();
}
#[derive(Serialize, Deserialize, Debug, Validate)]
#[derive(Serialize, Deserialize, Debug, Validate, PartialEq, Clone)]
pub struct UserBody {
#[validate(length(min = 1), regex = "VALID_CHARACTERS")]
pub name: String,
#[validate(custom(function = "validate_pubkey"))]
pub pubkey: String,
pubkey: String,
#[validate(custom(function = "validate_relays"))]
pub relays: Vec<String>
}
impl UserBody {
pub fn get_pubkey(&self) -> XOnlyPublicKey {
let keys = Keys::from_pk_str(&self.pubkey).unwrap();
keys.public_key()
pub fn get_pubkey(&self) -> String {
nostr::Keys::from_pk_str(&self.pubkey).unwrap().public_key().to_string()
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Nip05 {
names: HashMap<String, String>,
}
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct UserQuery {
#[validate(length(min = 1))]

View file

@ -2,10 +2,6 @@ use crate::utils::error::Error;
use validator::Validate;
use warp::{Filter, Rejection};
pub fn with_client_ip() {}
pub fn with_user_body() {}
pub fn validate_body_filter<T: serde::de::DeserializeOwned + Send + Validate + 'static>(
) -> impl Filter<Extract = (T,), Error = Rejection> + Copy {
warp::body::json::<T>().and_then(|query: T| async move {

View file

@ -6,6 +6,8 @@ use crate::utils::structs::Context;
use serde_json::json;
use warp::{Rejection, Reply};
use super::dto::UserBody;
pub async fn get_account(
// account: Result<AccountPubkey, Error>,
account: Account,
@ -47,7 +49,7 @@ pub async fn get_account(
pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Reply, Rejection> {
let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
let command = Command::DbReqGetUser(user_query.name);
let command = Command::DbReqGetNIP05(user_query.name);
context
.pubsub
.publish(
@ -61,7 +63,7 @@ pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Re
if let Ok(message) = subscriber.recv().await {
match message.content {
Command::DbResUser(profile) => {
Command::DbResNIP05(profile) => {
let response = serde_json::to_value(profile).unwrap();
Ok(warp::reply::json(&response))
}
@ -76,3 +78,35 @@ pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Re
)))
}
}
pub async fn create_user(user_body: UserBody, context: Context) -> Result<impl Reply, Rejection> {
let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
let command = Command::DbReqCreateNIP05(user_body);
context
.pubsub
.publish(
channels::MSG_NOOSE,
Message {
source: channels::MSG_NIP05,
content: command,
},
)
.await;
if let Ok(message) = subscriber.recv().await {
match message.content {
Command::DbResOk => {
Ok(warp::http::StatusCode::CREATED)
}
Command::ServiceError(e) => Err(warp::reject::custom(e)),
_ => Err(warp::reject::custom(Error::internal_with_message(
"Unhandeled message type",
))),
}
} else {
Err(warp::reject::custom(Error::internal_with_message(
"Unhandeled message type",
)))
}
}

View file

@ -1,9 +1,9 @@
use crate::noose::user::User;
// use super::accounts::create_account;
use super::dto::{Account, UserQuery};
use super::dto::{Account, UserBody, UserQuery};
use super::filter::{validate_body_filter, validate_query_filter};
use super::handler::{get_account, get_user};
use super::handler::{create_user, get_account, get_user};
use crate::utils::filter::with_context;
use crate::utils::structs::Context;
use warp::{Filter, Rejection, Reply};
@ -14,25 +14,37 @@ pub fn routes(context: Context) -> impl Filter<Extract = impl Reply, Error = Rej
index
.or(nip05_get(context.clone()))
// .or(account_create(context.clone()))
.or(nip05_create(context.clone()))
.with(&cors)
}
fn well_known() -> impl Filter<Extract = (), Error = Rejection> + Clone {
warp::get().and(warp::path(".well-known"))
fn well_known<M>(warp_method: M) -> impl Filter<Extract = (), Error = Rejection> + Clone
where
M: (Filter<Extract = (), Error = Rejection>) + Copy,
{
warp_method.and(warp::path(".well-known"))
}
fn nostr_well_known() -> impl Filter<Extract = (), Error = Rejection> + Clone {
well_known().and(warp::path("nostr.json"))
well_known(warp::get()).and(warp::path("nostr.json"))
}
pub fn nip05_get(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
fn nip05_get(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
nostr_well_known()
.and(validate_query_filter::<UserQuery>())
.and(with_context(context.clone()))
.and_then(get_user)
}
fn nip05_create(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
well_known(warp::post())
.and(warp::path("nostr.json"))
.and(warp::body::content_length_limit(1024))
.and(validate_body_filter::<UserBody>())
.and(with_context(context.clone()))
.and_then(create_user)
}
// pub fn account_create(
// context: Context,
// ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {

View file

@ -1,7 +1,7 @@
use super::dto::AccountPubkey;
use crate::utils::error::Error;
use nostr::prelude::FromPkStr;
use validator::{Validate, ValidationError};
use validator::{Validate, ValidationError, validate_url};
pub async fn validate_account_pubkey_query(
account_pubkey: AccountPubkey,
@ -25,6 +25,18 @@ pub fn validate_pubkey(value: &str) -> Result<(), ValidationError> {
match nostr::Keys::from_pk_str(value) {
Ok(_) => Ok(()),
Err(_) => Err(ValidationError::new("Unable to parse pk_str")),
Err(_) => Err(ValidationError::new("Failed to parse pubkey")),
}
}
pub fn validate_relays(relays: &Vec<String>) -> Result<(), ValidationError> {
if relays.is_empty() {
return Ok(())
}
if relays.iter().all(validate_url) {
return Ok(())
}
Err(ValidationError::new("Relays have wrong url format"))
}

View file

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

View file

@ -4,6 +4,7 @@ use crate::PubSub;
use nostr::{Event, Filter, SubscriptionId};
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::mpsc;
use uuid::Uuid;
@ -50,6 +51,8 @@ pub struct Client {
pub client_id: Uuid,
pub client_connection: Option<mpsc::UnboundedSender<Result<Message, Error>>>,
pub subscriptions: HashMap<String, Subscription>,
pub authenticated: bool, // NIP-42
challlenge: Option<String>, // NIP-42
max_subs: usize,
}
@ -60,6 +63,8 @@ impl Client {
client_id: Uuid::new_v4(),
client_connection: None,
subscriptions: HashMap::new(),
authenticated: false,
challlenge: None,
max_subs: MAX_SUBSCRIPTIONS,
}
}
@ -68,6 +73,43 @@ impl Client {
&self.client_ip_addr
}
pub fn set_challenge(&mut self, challenge: String) {
self.challlenge = Some(challenge);
}
pub fn authenticate(&mut self, event: &nostr::Event) {
if self.challlenge.is_none() {
return;
}
let challenge_tag = nostr::Tag::Challenge(self.challlenge.clone().unwrap());
let relay_tag = nostr::Tag::Relay(nostr::UncheckedUrl::from_str("ws://0.0.0.0:8080").unwrap()); // TODO: Use relay address from env variable
if let Ok(()) = event.verify() {
log::debug!("event is valid");
if event.kind.as_u32() == 22242 && event.created_at() > nostr::Timestamp::from(nostr::Timestamp::now().as_u64() - 600) {
log::debug!("kind is correct and timestamp is good");
let mut challenge_matched = false;
let mut relay_matched = false;
event.tags().iter().for_each(|tag| {
if tag == &challenge_tag {
challenge_matched = true;
log::debug!("challenge matched");
}
if tag == &relay_tag {
relay_matched = true;
log::debug!("relay matched");
}
});
if challenge_matched && relay_matched {
log::debug!("client now authenticated");
self.authenticated = true;
}
}
}
}
pub fn subscribe(&mut self, subscription: Subscription) -> Result<(), Error> {
let k = subscription.get_id();
let sub_id_len = k.len();