Compare commits

..

10 commits

Author SHA1 Message Date
Tony Klink e40a4645a3
Convert bool to string 2024-02-19 11:02:19 -06:00
Tony Klink 1f7270dabf
Convert bool to string 2024-02-19 10:56:32 -06:00
Tony Klink 5944414d15
Fix typo 2024-02-19 10:53:45 -06:00
Tony Klink b6dcf61995
Increase channel capacity 2024-02-19 10:47:49 -06:00
Tony Klink d06206bb24
Finished NIP42 implementation 2024-02-13 09:53:41 -06:00
Tony Klink f7b74bd22c
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
2024-02-08 19:19:03 -06:00
Tony Klink 377da44eed
Add support for querying NIP-05 on /.well-known/nostr.json?name=username 2024-01-30 11:43:03 -06:00
Tony Klink e1306608ef
Remove sailfish for now 2024-01-30 08:25:00 -06:00
Tony Klink 645d125077
Use real config for RID NIP-11 2024-01-28 22:53:29 -06:00
Tony Klink bf08ac12e0
Implement NIP-40 (event expiration) 2024-01-28 21:22:45 -06:00
35 changed files with 1405 additions and 665 deletions

92
Cargo.lock generated
View file

@ -497,18 +497,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "filetime"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.3.5",
"windows-sys",
]
[[package]]
name = "flate2"
version = "1.0.28"
@ -761,15 +749,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
[[package]]
name = "home"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
dependencies = [
"windows-sys",
]
[[package]]
name = "http"
version = "0.2.9"
@ -957,12 +936,6 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "itoap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8"
[[package]]
name = "js-sys"
version = "0.3.64"
@ -1591,44 +1564,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "sailfish"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a03fcade08eb837d7ba0d8f775f1fe6cddb00d413de0c655ab2b93e821a0eb"
dependencies = [
"itoap",
"ryu",
"sailfish-macros",
"version_check",
]
[[package]]
name = "sailfish-compiler"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cf1deecb07747a1f7ab55745edcc406875db007602b037af5fcf59f40dd2005"
dependencies = [
"filetime",
"home",
"memchr",
"proc-macro2",
"quote",
"serde",
"syn 2.0.28",
"toml",
]
[[package]]
name = "sailfish-macros"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e5946289d6daa26cde6ffb06678d7a8ba276ec6a7a55d36f64b35e7f644d22"
dependencies = [
"proc-macro2",
"sailfish-compiler",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@ -1741,15 +1676,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1833,7 +1759,6 @@ dependencies = [
"rusqlite",
"rusqlite_migration",
"rustls 0.21.6",
"sailfish",
"sea-query",
"sea-query-rusqlite",
"serde",
@ -2062,26 +1987,11 @@ dependencies = [
"tracing",
]
[[package]]
name = "toml"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
@ -2090,8 +2000,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]

View file

@ -26,7 +26,6 @@ rusqlite_migration = "1.0.2"
nostr = "0.27.0"
nostr-database = "0.27.0"
regex = "1.9.5"
sailfish = "0.7.0"
sea-query = { version = "0.30.4", features = ["backend-sqlite", "thread-safe"] }
sea-query-rusqlite = { version="0", features = [
"with-chrono",

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/
'';
};
@ -52,6 +50,8 @@
env = {
DATABASE_URL = "/tmp/sqlite.db";
ADMIN_PUBKEY = "npub1m2w0ckmkgj4wtvl8muwjynh56j3qd4nddca4exdg4mdrkepvfnhsmusy54";
CONFIG_ENABLE_AUTH = "false";
CONFIG_RELAY_URL = "ws://0.0.0.0:8080";
RUST_BACKTRACE = 1;
RUST_LOG = "debug";
};

View file

@ -21,6 +21,11 @@ in {
'npub' of the administrator account. Must be defined!
'';
};
enableAuth = mkOption {
type = types.bool;
default = false;
description = "Require NIP-42 Authentication for REQ and EVENT";
};
sslEnable = mkEnableOption "Whether to enable ACME SSL for nginx proxy";
hostAddress = mkOption {
type = types.nullOr types.str;
@ -36,6 +41,10 @@ in {
Local nixos-container ip address
'';
};
relayUrl = mkOption {
type = types.str;
description = "Relay URL that will be used for NIP-42 AUTH validation";
};
};
config = mkIf cfg.enable {
@ -64,6 +73,8 @@ in {
environment = {
DATABASE_URL = "${DB_PATH}/sneedstr.db";
ADMIN_PUBKEY = cfg.adminPubkey;
CONFIG_ENABLE_AUTH = boolToString cfg.enableAuth;
CONFIG_RELAY_URL = cfg.relayUrl;
};
startLimitBurst = 1;
startLimitIntervalSec = 10;
@ -111,6 +122,10 @@ in {
proxyWebsockets = true; # needed if you need to use WebSocket
recommendedProxySettings = true;
};
locations."/register" = {
proxyPass = "http://${cfg.localAddress}:8085";
recommendedProxySettings = true;
};
};
};
};

View file

@ -1,15 +1,16 @@
use crate::{
noose::sled::BanInfo,
noose::user::{User, UserRow},
utils::{error::Error, structs::Subscription},
noose::user::{User, UserRow, Nip05Profile},
utils::{error::Error, structs::Subscription}, usernames::dto::UserBody,
};
use nostr::secp256k1::XOnlyPublicKey;
use std::{collections::HashMap, fmt::Debug};
use std::{collections::{HashMap, BTreeSet}, fmt::Debug};
use tokio::sync::{broadcast, Mutex};
pub mod channels {
pub static MSG_NOOSE: &str = "MSG_NOOSE";
pub static MSG_NIP05: &str = "MSG_NIP05";
pub static MSG_AUTH: &str = "MSG_AUTH";
pub static MSG_RELAY: &str = "MSG_RELAY";
pub static MSG_PIPELINE: &str = "MSG_PIPELINE";
pub static MSG_SLED: &str = "MSG_SLED";
@ -18,18 +19,21 @@ pub mod channels {
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
// DbRequest
// --- Req
DbReqWriteEvent(/* client_id */ uuid::Uuid, Box<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),
DbReqGetUser(User),
DbReqCreateAccount(XOnlyPublicKey, String, String),
DbReqGetAccount(String),
DbReqClear,
// DbResponse
// NIP-05 related messages
DbReqGetNIP05(String),
DbReqCreateNIP05(UserBody),
// --- Res
DbResNIP05(Nip05Profile),
DbResRelayMessages(
/* client_id*/ uuid::Uuid,
/* Vec<RelayMessage::Event> */ Vec<nostr::RelayMessage>,
@ -38,26 +42,48 @@ pub enum Command {
DbResOk,
DbResOkWithStatus(/* client_id */ uuid::Uuid, nostr::RelayMessage),
DbResAccount, // TODO: Add Account DTO as a param
DbResUser(UserRow),
DbResEventCounts(/* client_id */ uuid::Uuid, nostr::RelayMessage),
// Noose Profile
// --- Req
DbReqGetProfile(XOnlyPublicKey),
DbReqAddContact(/* profile_pub_key */ XOnlyPublicKey, /* contact_pub_key*/ XOnlyPublicKey),
DbReqRemoveContact(/* profile_pub_key */ XOnlyPublicKey, /* contact_pub_key*/ XOnlyPublicKey),
DbReqGetContactPubkeys(XOnlyPublicKey),
DbReqGetContacts(XOnlyPublicKey),
// --- Res
DbResGetProfile(nostr_database::Profile),
DbResGetContactPubkeys(Vec<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,
}
@ -86,7 +112,7 @@ impl PubSub {
}
pub async fn subscribe(&self, topic: &str) -> broadcast::Receiver<Message> {
let (tx, _rx) = broadcast::channel(32); // 32 is the channel capacity
let (tx, _rx) = broadcast::channel(20_000); // 20000 is the channel capacity
let mut subscribers = self.subscribers.lock().await;
subscribers
.entry(topic.to_string())

View file

@ -1,17 +1,58 @@
use crate::{
bussy::PubSub,
usernames::dto::UserBody,
utils::{error::Error, structs::Subscription},
};
use nostr::{Event, RelayMessage};
use std::sync::Arc;
use nostr::{secp256k1::XOnlyPublicKey, Event, RelayMessage};
use nostr_database::Profile;
use std::{collections::BTreeSet, sync::Arc};
use super::user::Nip05Profile;
/// Handle core nostr events
pub trait Noose: Send + Sync {
/// Start event listener
async fn start(&mut self, pubsub: Arc<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

@ -11,7 +11,8 @@ impl MigrationRunner {
let m_events_fts = include_str!("./1697410223576_events_fts.sql");
let m_users = include_str!("./1697410294265_users.sql");
let m_unattached_media = include_str!("./1697410480767_unattached_media.sql");
let m_pragma = include_str!("./1697410424624_pragma.sql");
let m_nip05 = include_str!("./1706575155557_nip05.sql");
let m_nip42 = include_str!("./1707327016995_nip42_profile.sql");
let migrations = Migrations::new(vec![
M::up(m_create_events),
@ -20,7 +21,8 @@ impl MigrationRunner {
M::up(m_events_fts),
M::up(m_users),
M::up(m_unattached_media),
M::up(m_pragma),
M::up(m_nip05),
M::up(m_nip42),
]);
match migrations.to_latest(connection) {

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

@ -22,7 +22,7 @@ impl Pipeline {
let channel;
let command = match message.content {
Command::PipelineReqEvent(client_id, event) => {
match self.handle_event(client_id, event.clone()).await {
match self.handle_event(client_id, event).await {
Ok(_) => {
log::info!("[Pipeline] handle_event completed");
channel = message.source;

View file

@ -1,197 +1,27 @@
use super::{db::Noose, migrations::MigrationRunner};
use super::{
db::Noose,
migrations::MigrationRunner,
user::{Nip05Profile, Nip05Table, UserRow},
};
use crate::{
bussy::{channels, Command, Message, PubSub},
usernames::dto::UserBody,
utils::{config::Config as ServiceConfig, error::Error, structs::Subscription},
};
use async_trait::async_trait;
use deadpool_sqlite::{Config, Object, Pool, Runtime};
use nostr::{
nips::nip01::Coordinate, Event, EventId, Filter, RelayMessage, TagKind, Timestamp, Url,
nips::nip01::Coordinate, secp256k1::XOnlyPublicKey, Event, EventId, Filter, JsonUtil,
RelayMessage, TagKind, Timestamp, Url,
};
use nostr_database::{Backend, DatabaseOptions, NostrDatabase, Order};
use rusqlite::Row;
use sea_query::{extension::sqlite::SqliteExpr, Order as SqOrder, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder;
use std::sync::Arc;
use std::{collections::HashSet, str::FromStr};
#[derive(Debug, Clone)]
struct EventRow {
id: String,
pubkey: String,
created_at: i64,
kind: i64,
tags: String,
sig: String,
content: String,
}
impl From<&Row<'_>> for EventRow {
fn from(row: &Row) -> Self {
let id: String = row.get("id").unwrap();
let pubkey: String = row.get("pubkey").unwrap();
let created_at: i64 = row.get("created_at").unwrap();
let kind: i64 = row.get("kind").unwrap();
let tags: String = row.get("tags").unwrap();
let sig: String = row.get("sig").unwrap();
let content: String = row.get("content").unwrap();
Self {
id,
pubkey,
created_at,
kind,
tags,
sig,
content,
}
}
}
impl From<&EventRow> for Event {
fn from(row: &EventRow) -> Self {
row.to_event()
}
}
impl EventRow {
pub fn to_event(&self) -> Event {
let tags: Vec<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 DeletedCoordinatesTable {
// Table,
// Coordinate,
// CreatedAt,
// }
// impl sea_query::Iden for DeletedCoordinatesTable {
// fn unquoted(&self, s: &mut dyn std::fmt::Write) {
// write!(
// s,
// "{}",
// match self {
// Self::Table => "deleted_coordinates",
// Self::Coordinate => "coordinate",
// Self::CreatedAt => "created_at",
// }
// )
// .unwrap()
// }
// }
enum EventSeenByRelaysTable {
Table,
Id,
EventId,
RelayURL,
}
impl sea_query::Iden for EventSeenByRelaysTable {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "event_seen_by_relays",
Self::Id => "id",
Self::EventId => "event_id",
Self::RelayURL => "relay_url",
}
)
.unwrap()
}
}
use crate::noose::sqlite_tables::*;
#[derive(Debug)]
pub struct NostrSqlite {
@ -212,7 +42,22 @@ impl NostrSqlite {
async fn run_migrations(pool: &Pool) -> bool {
let connection = pool.get().await.unwrap();
connection.interact(MigrationRunner::up).await.unwrap()
connection.interact(MigrationRunner::up).await.unwrap();
connection
.interact(|conn| {
conn.pragma_update(None, "encoding", "UTF-8").unwrap();
conn.pragma_update(None, "journal_mode", "WAL").unwrap();
conn.pragma_update(None, "foreign_keys", "ON").unwrap();
conn.pragma_update(None, "auto_vacuum", "FULL").unwrap();
conn.pragma_update(None, "journal_size_limit", "32768")
.unwrap();
conn.pragma_update(None, "mmap_size", "17179869184")
.unwrap();
})
.await
.unwrap();
true
}
async fn get_connection(&self) -> Result<Object, Error> {
@ -496,86 +341,159 @@ impl NostrSqlite {
return Ok(false);
}
} else {
log::debug!("inserting new event in events");
// Insert into Events table
let (sql, values) = Query::insert()
.into_table(EventsTable::Table)
.columns([
EventsTable::EventId,
EventsTable::Content,
EventsTable::Kind,
EventsTable::Pubkey,
EventsTable::CreatedAt,
EventsTable::Tags,
EventsTable::Sig,
])
.values_panic([
id.clone().into(),
content.clone().into(),
kind.into(),
pubkey.into(),
created_at.into(),
tags.into(),
sig.into(),
])
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);
}
}
}
@ -844,6 +762,12 @@ impl NostrSqlite {
let event = row.clone().to_event();
if event.is_expired() {
return Err(Error::internal_with_message(
"Event has expired. Ignoring...",
));
}
Ok(event)
})
.await
@ -1059,7 +983,9 @@ impl NostrSqlite {
let mut event_vec: Vec<Event> = vec![];
while let Ok(Some(row)) = rows.next() {
let event = EventRow::from(row).to_event();
event_vec.push(event);
if !event.is_expired() {
event_vec.push(event);
}
}
Ok(event_vec)
@ -1134,8 +1060,6 @@ impl NostrSqlite {
let Ok(query_result) = connection
.interact(move |conn| {
let (sql, values) = sql_statement
.clear_selects()
.column(EventsTable::EventId)
.order_by(EventsTable::CreatedAt, sq_order.to_owned())
.build_rusqlite(SqliteQueryBuilder);
@ -1144,9 +1068,10 @@ impl NostrSqlite {
let mut event_vec: Vec<EventId> = vec![];
while let Ok(Some(row)) = rows.next() {
let event_id_string: String = row.get(0).unwrap();
let event_id = EventId::from_str(&event_id_string).unwrap();
event_vec.push(event_id);
let event = EventRow::from(row).to_event();
if !event.is_expired() {
event_vec.push(event.id);
}
}
Ok(event_vec)
@ -1178,33 +1103,37 @@ impl NostrSqlite {
};
let Ok(query_result) = connection
.interact(move |conn: &mut rusqlite::Connection| -> Result<bool, Error> {
let (sql, value) = Query::select()
.from(EventsTable::Table)
.columns([EventsTable::EventId, EventsTable::CreatedAt])
.left_join(
TagsTable::Table,
sea_query::Expr::col((TagsTable::Table, TagsTable::EventId))
.equals((EventsTable::Table, EventsTable::EventId)),
)
.and_where(sea_query::Expr::col((TagsTable::Table, TagsTable::Tag)).eq("a"))
.and_where(sea_query::Expr::col((TagsTable::Table, TagsTable::Value)).eq(ident))
.and_where(
sea_query::Expr::col((EventsTable::Table, EventsTable::CreatedAt))
.gte(timestamp.as_i64()),
)
.limit(1)
.build_rusqlite(SqliteQueryBuilder);
.interact(
move |conn: &mut rusqlite::Connection| -> Result<bool, Error> {
let (sql, value) = Query::select()
.from(EventsTable::Table)
.columns([EventsTable::EventId, EventsTable::CreatedAt])
.left_join(
TagsTable::Table,
sea_query::Expr::col((TagsTable::Table, TagsTable::EventId))
.equals((EventsTable::Table, EventsTable::EventId)),
)
.and_where(sea_query::Expr::col((TagsTable::Table, TagsTable::Tag)).eq("a"))
.and_where(
sea_query::Expr::col((TagsTable::Table, TagsTable::Value)).eq(ident),
)
.and_where(
sea_query::Expr::col((EventsTable::Table, EventsTable::CreatedAt))
.gte(timestamp.as_i64()),
)
.limit(1)
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = conn.prepare(sql.as_str()).unwrap();
let mut rows = stmt.query(&*value.as_params()).unwrap();
let mut stmt = conn.prepare(sql.as_str()).unwrap();
let mut rows = stmt.query(&*value.as_params()).unwrap();
if let Ok(Some(record)) = rows.next() {
return Ok(false)
}
if let Ok(Some(record)) = rows.next() {
return Ok(false);
}
Ok(true)
})
Ok(true)
},
)
.await
else {
return Err(Error::internal_with_message(
@ -1214,6 +1143,147 @@ impl NostrSqlite {
query_result
}
async fn get_nip05_profile(&self, username: String) -> Result<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
}
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 {
@ -1272,7 +1342,8 @@ impl NostrDatabase for NostrSqlite {
coordinate: &Coordinate,
timestamp: Timestamp,
) -> Result<bool, Self::Err> {
self.has_coordinate_been_deleted(coordinate, timestamp).await
self.has_coordinate_been_deleted(coordinate, timestamp)
.await
}
/// Set [`EventId`] as seen by relay
@ -1339,6 +1410,7 @@ impl Noose for NostrSqlite {
while let Ok(message) = subscriber.recv().await {
log::info!("[Noose] received message: {:?}", message);
let command = match message.content {
// Relay Events
Command::DbReqWriteEvent(client_id, event) => match self.write_event(event).await {
Ok(status) => Command::DbResOkWithStatus(client_id, status),
Err(e) => Command::ServiceError(e),
@ -1363,6 +1435,44 @@ impl Noose for NostrSqlite {
Err(e) => Command::ServiceError(e),
}
}
// NIP-05
Command::DbReqGetNIP05(username) => match self.get_nip05(username).await {
Ok(user) => Command::DbResNIP05(user),
Err(e) => Command::ServiceError(e),
},
Command::DbReqCreateNIP05(user) => match self.create_nip05(user).await {
Ok(_) => Command::DbResOk,
Err(e) => Command::ServiceError(e),
},
// NIP-42
Command::DbReqGetProfile(pubkey) => match self.profile(pubkey).await {
Ok(profile) => Command::DbResGetProfile(profile),
Err(e) => Command::ServiceError(e),
},
Command::DbReqAddContact(profile_pub_key, contact_pub_key) => {
match self.add_contact(profile_pub_key, contact_pub_key).await {
Ok(_) => Command::DbResOk,
Err(e) => Command::ServiceError(e),
}
}
Command::DbReqRemoveContact(profile_pub_key, contact_pub_key) => {
match self.remove_contact(profile_pub_key, contact_pub_key).await {
Ok(_) => Command::DbResOk,
Err(e) => Command::ServiceError(e),
}
}
Command::DbReqGetContactPubkeys(profile_pub_key) => {
match self.contacts_public_keys(profile_pub_key).await {
Ok(contact_pubkeys) => Command::DbResGetContactPubkeys(contact_pubkeys),
Err(e) => Command::ServiceError(e),
}
}
Command::DbReqGetContacts(profile_pub_key) => {
match self.contacts(profile_pub_key).await {
Ok(contacts) => Command::DbResGetContacts(contacts),
Err(e) => Command::ServiceError(e),
}
}
_ => Command::Noop,
};
if command != Command::Noop {
@ -1386,7 +1496,6 @@ impl Noose for NostrSqlite {
}
async fn write_event(&self, event: Box<nostr::Event>) -> Result<nostr::RelayMessage, Error> {
// TODO: Maybe do event validation and admin deletions here
match self.save_event(&event).await {
Ok(status) => {
let relay_message = nostr::RelayMessage::ok(event.id, status, "");
@ -1427,11 +1536,57 @@ impl Noose for NostrSqlite {
Err(err) => Err(Error::internal_with_message(err.to_string())),
}
}
async fn get_nip05(&self, username: String) -> Result<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]
@ -1656,6 +1811,29 @@ mod tests {
assert_eq!(result.len(), 1)
}
#[tokio::test]
async fn save_metadata_event() {
let config = Arc::new(ServiceConfig::new());
let db = NostrSqlite::new(config).await;
let secret_key = "nsec1g24e83hwj5gxl0hqxx9wllwcg9rrxthssv0mrxf4dv3lt8dc29yqrxf09p";
let keys = nostr::Keys::from_sk_str(secret_key).unwrap();
// Insert
let metadata = nostr::Metadata::new()
.name("Chuck")
.custom_field("feed", "seed");
let event = nostr::EventBuilder::metadata(&metadata)
.to_event(&keys)
.unwrap();
let result = db.save_event(&event).await.unwrap();
// Find profile by pk
let pubkey = keys.public_key();
let res = db.get_profile(pubkey).await.unwrap();
assert_eq!(res.name(), "Sneed");
}
#[tokio::test]
async fn save_event_with_a_tag() {
let config = Arc::new(ServiceConfig::new());
@ -1696,4 +1874,14 @@ mod tests {
dbg!(res);
}
// #[tokio::test]
// async fn get_nip05() {
// let config = Arc::new(ServiceConfig::new());
// let db = NostrSqlite::new(config).await;
// let res = db.get_nip05("test".to_string()).await.unwrap();
// dbg!(serde_json::to_value(res).unwrap().to_string());
// }
}

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

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

@ -11,18 +11,12 @@ pub async fn ws_handler(
Ok(ws.on_upgrade(move |socket| ws::client_connection(socket, context, real_client_ip)))
}
pub async fn relay_config(header: String) -> Result<impl Reply, Rejection> {
pub async fn relay_config(header: String, context: Context) -> Result<impl Reply, Rejection> {
if header != "application/nostr+json" {
Err(warp::reject::not_found())
} else {
let res = serde_json::json!({
"contact": "klink@zhitno.st",
"name": "zhitno.st",
"description": "Very *special* nostr relay",
"supported_nips": [ 1, 9, 11, 12, 15, 16, 20, 22, 28, 33 ],
"software": "git+https://git.zhitno.st/Klink/sneedstr.git",
"version": "0.1.0"
});
Ok(warp::reply::json(&res))
let config = context.config.get_relay_config_json();
Ok(warp::reply::json(&config))
}
}

View file

@ -6,25 +6,26 @@ 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 accept_application_json_header = warp::header::header("Accept");
let cors = warp::cors().allow_any_origin();
warp::path::end().and(
accept_application_json_header
.and_then(handler::relay_config)
.with(&cors)
.or(warp::ws()
.and(with_context(context))
.and(real_client_ip)
.and_then(handler::ws_handler)
.with(&cors)),
)
let relay_information_document_path = warp::path::end().and(
warp::header::header("Accept")
.and(with_context(context.clone()))
.and_then(handler::relay_config),
);
let nostr_relay_path = warp::path::end().and(
warp::ws()
.and(with_context(context.clone()))
.and(real_client_ip)
.and_then(handler::ws_handler),
);
relay_information_document_path.or(nostr_relay_path)
}
fn static_files() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {

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,
@ -211,23 +215,89 @@ fn send(client: &Client, message: Message) {
}
}
fn needs_auth(context: &Context, client: &Client, event: Option<&Event>) -> bool {
if !context.config.auth_required() {
return false;
}
if event.is_some() {
let event = event.unwrap();
let admin_pk = context.config.get_admin_pubkey();
if event.pubkey == *admin_pk {
return false;
}
if event.kind() == nostr::Kind::Metadata {
return false;
}
}
if !client.authenticated {
return true;
}
false
}
async fn handle_msg(context: &Context, client: &mut Client, client_message: ClientMessage) {
match client_message {
ClientMessage::Event(event) => handle_event(context, client, event).await,
ClientMessage::Event(event) => {
if needs_auth(context, client, Some(&event)) {
request_auth(context, client).await;
return;
}
handle_event(context, client, event).await
}
ClientMessage::Req {
subscription_id,
filters,
} => handle_req(context, client, subscription_id, filters).await,
} => {
if needs_auth(context, client, None) {
request_auth(context, client).await;
return;
}
handle_req(context, client, subscription_id, filters).await
}
ClientMessage::Count {
subscription_id,
filters,
} => handle_count(context, client, subscription_id, filters).await,
} => {
if needs_auth(context, client, None) {
request_auth(context, client).await;
return;
}
handle_count(context, client, subscription_id, filters).await
}
ClientMessage::Close(subscription_id) => handle_close(client, subscription_id).await,
ClientMessage::Auth(event) => handle_auth(client, event).await,
_ => (),
ClientMessage::Auth(event) => handle_auth(context, client, event).await,
// Unhandled messages
_ => unhandled_message(context, client).await,
}
}
async fn request_auth(context: &Context, client: &mut Client) {
let challenge = uuid::Uuid::new_v4().to_string();
client.set_challenge(challenge.clone());
let auth_message = nostr::RelayMessage::auth(challenge);
context
.pubsub
.publish(
channels::MSG_RELAY,
crate::bussy::Message {
source: channels::MSG_RELAY,
content: crate::bussy::Command::ServiceRegistrationRequired(
client.client_id,
auth_message,
),
},
)
.await;
}
async fn handle_event(context: &Context, client: &Client, event: Box<Event>) {
log::debug!("handle_event is processing new event");
@ -328,14 +398,77 @@ async fn handle_count(
}
async fn handle_close(client: &mut Client, subscription_id: SubscriptionId) {
// context.pubsub.send(new nostr event) then handle possible errors
client.unsubscribe(subscription_id);
// let message = Message::text("CLOSE not implemented");
// send(client, message);
}
async fn handle_auth(client: &Client, event: Box<Event>) {
let message = Message::text("AUTH not implemented");
send(client, message);
async fn handle_auth(context: &Context, client: &mut Client, event: Box<Event>) {
let mut subscriber = context.pubsub.subscribe(channels::MSG_AUTH).await;
context
.pubsub
.publish(
channels::MSG_NOOSE,
crate::bussy::Message {
source: channels::MSG_AUTH,
content: crate::bussy::Command::DbReqGetProfile(event.pubkey),
},
)
.await;
let mut message = nostr::RelayMessage::ok(
event.id,
client.authenticated,
"auth-required: User not registered",
);
let Ok(result) = subscriber.recv().await else {
context
.pubsub
.publish(
channels::MSG_RELAY,
crate::bussy::Message {
source: channels::MSG_RELAY,
content: crate::bussy::Command::DbResOkWithStatus(client.client_id, message),
},
)
.await;
return;
};
if let crate::bussy::Command::DbResGetProfile(profile) = result.content {
client.authenticate(context.config.get_relay_url(), &event);
let client_status = format!("Client authenticated: {}", client.authenticated);
let status_message = if client.authenticated {
""
} else {
"auth-required: we only accept events from registered users"
};
message = nostr::RelayMessage::ok(event.id, client.authenticated, status_message);
};
context
.pubsub
.publish(
channels::MSG_RELAY,
crate::bussy::Message {
source: channels::MSG_RELAY,
content: crate::bussy::Command::DbResOkWithStatus(client.client_id, message),
},
)
.await;
}
async fn unhandled_message(context: &Context, client: &Client) {
let message = nostr::RelayMessage::notice("Unsupported Message");
context
.pubsub
.publish(
channels::MSG_RELAY,
crate::bussy::Message {
source: channels::MSG_RELAY,
content: crate::bussy::Command::DbResOkWithStatus(client.client_id, message),
},
)
.await
}

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,31 +10,26 @@ lazy_static! {
static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap();
}
#[derive(Serialize, Deserialize, Debug, Validate)]
#[derive(Serialize, Deserialize, Debug, Validate, PartialEq, Clone)]
pub struct UserBody {
#[validate(length(min = 1), regex = "VALID_CHARACTERS")]
pub name: String,
#[validate(custom(function = "validate_pubkey"))]
pub pubkey: String,
pubkey: String,
#[validate(custom(function = "validate_relays"))]
pub relays: Vec<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))]
pub user: String,
pub name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, Validate)]

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,
@ -45,14 +47,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::DbReqGetNIP05(user_query.name);
context
.pubsub
.publish(
@ -65,18 +62,44 @@ 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::DbResNIP05(profile) => {
let response = serde_json::to_value(profile).unwrap();
Ok(warp::reply::json(&response))
}
Command::ServiceError(e) => Ok(warp::reply::json(&response)),
Command::ServiceError(e) => Err(warp::reject::custom(e)),
_ => Err(warp::reject::custom(Error::internal_with_message(
"Unhandeled message type",
))),
}
} else {
Err(warp::reject::custom(Error::internal_with_message(
"Unhandeled message type",
)))
}
}
pub async fn create_user(user_body: UserBody, context: Context) -> Result<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",
))),

View file

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

View file

@ -1,49 +1,69 @@
use crate::noose::user::User;
use super::accounts::create_account;
use super::dto::{Account, UserQuery};
// use super::accounts::create_account;
use super::dto::{Account, UserBody, UserQuery};
use super::filter::{validate_body_filter, validate_query_filter};
use super::handler::{get_account, get_user};
use super::handler::{create_user, get_account, get_user};
use crate::utils::filter::with_context;
use crate::utils::structs::Context;
use warp::{Filter, Rejection, Reply};
pub fn routes(context: Context) -> impl Filter<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(nip05_create(context.clone()))
.with(&cors)
}
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"))
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(warp::get()).and(warp::path("nostr.json"))
}
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))
.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)
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_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_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_update(
// context: Context,

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

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{path::PathBuf, str::FromStr};
use nostr::{key::FromPkStr, secp256k1::XOnlyPublicKey};
@ -6,6 +6,8 @@ use nostr::{key::FromPkStr, secp256k1::XOnlyPublicKey};
pub struct Config {
admin_pubkey: XOnlyPublicKey,
db_path: PathBuf,
auth_required: bool,
relay_url: nostr::UncheckedUrl,
}
impl Default for Config {
@ -23,13 +25,24 @@ impl Config {
.public_key();
let db_path = std::env::var("DATABASE_URL").map(PathBuf::from).unwrap();
let auth_required: bool = std::env::var("CONFIG_ENABLE_AUTH")
.unwrap_or("false".to_string())
.parse()
.unwrap();
let relay_url = nostr::UncheckedUrl::from_str(&std::env::var("CONFIG_RELAY_URL").unwrap()).unwrap();
Self {
admin_pubkey,
db_path,
auth_required,
relay_url,
}
}
pub fn auth_required(&self) -> bool {
self.auth_required
}
pub fn get_admin_pubkey(&self) -> &XOnlyPublicKey {
&self.admin_pubkey
}
@ -38,14 +51,18 @@ impl Config {
self.db_path.clone()
}
pub fn get_relay_url(&self) -> nostr::UncheckedUrl {
self.relay_url.clone()
}
pub fn get_relay_config_json(&self) -> serde_json::Value {
serde_json::json!({
"contact": "klink@zhitno.st",
"name": "zhitno.st",
"description": "Very *special* nostr relay",
"supported_nips": [ 1, 9, 11, 12, 15, 16, 20, 22, 28, 33, 45 ],
"supported_nips": [ 1, 2, 4, 9, 11, 12, 15, 16, 20, 22, 28, 33, 40, 42, 45, 50 ],
"software": "git+https://git.zhitno.st/Klink/sneedstr.git",
"version": "0.1.0"
"version": "0.1.1"
})
}
}

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

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, relay_url: nostr::UncheckedUrl, event: &nostr::Event) {
if self.challlenge.is_none() {
return;
}
let challenge_tag = nostr::Tag::Challenge(self.challlenge.clone().unwrap());
let relay_tag = nostr::Tag::Relay(relay_url);
if let Ok(()) = event.verify() {
log::debug!("event is valid");
if event.kind.as_u32() == 22242 && event.created_at() > nostr::Timestamp::from(nostr::Timestamp::now().as_u64() - 600) {
log::debug!("kind is correct and timestamp is good");
let mut challenge_matched = false;
let mut relay_matched = false;
event.tags().iter().for_each(|tag| {
if tag == &challenge_tag {
challenge_matched = true;
log::debug!("challenge matched");
}
if tag == &relay_tag {
relay_matched = true;
log::debug!("relay matched");
}
});
if challenge_matched && relay_matched {
log::debug!("client now authenticated");
self.authenticated = true;
}
}
}
}
pub fn subscribe(&mut self, subscription: Subscription) -> Result<(), Error> {
let k = subscription.get_id();
let sub_id_len = k.len();

View file

@ -1,66 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Registration</title>
</head>
<body>
<div id="app">
<h1>Create new user</h1>
<form action="/register" method="post" id="signup">
<h1>Sign Up</h1>
<div class="field">
<label for="name">Name:</label>
<input type="text" id="name" name="name" placeholder="Enter your profile name" />
<small></small>
</div>
<div class="field">
<label for="email">PubKey:</label>
<input type="text" id="pubkey" name="pubkey" placeholder="Enter your pubkey" />
<small></small>
</div>
<button type="submit">Register</button>
</form>
</div>
</body>
<script>
const form = document.getElementById('signup');
form.addEventListener('submit', async (event) => {
// stop form submission
event.preventDefault();
const name = form.elements['name'];
const pubkey = form.elements['pubkey'];
// getting the element's value
let userName = name.value;
let pubKey = pubkey.value;
console.log(`${userName}:${pubKey}`);
const data = { name: userName, pubkey: pubKey};
// Send user data
await postJSON(data);
});
async function postJSON(data) {
try {
const response = await fetch("http://localhost:8085/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
console.log("Success:", result);
} catch (error) {
console.error("Error:", error);
}
}
</script>
</html>

View file

@ -1,70 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<title>Registration</title>
</head>
<body>
<div id="app">
<h1>Create new user</h1>
<form action="/register" method="post" id="signup">
<h1>Sign Up</h1>
<div class="field">
<label for="name">Name:</label>
<input type="text" id="name" name="name" placeholder="Enter your profile name" />
<small></small>
</div>
<div class="field">
<label for="email">PubKey:</label>
<input type="text" id="pubkey" name="pubkey" placeholder="Enter your pubkey" />
<small></small>
</div>
<button type="submit">Register</button>
</form>
</div>
</body>
<script>
const form = document.getElementById('signup');
form.addEventListener('submit', async (event) => {
// stop form submission
event.preventDefault();
const name = form.elements['name'];
const pubkey = form.elements['pubkey'];
// getting the element's value
let userName = name.value;
let pubKey = pubkey.value;
console.log(`${userName}:${pubKey}`);
const data = { name: userName, pubkey: pubKey};
// Send user data
await postJSON(data);
});
async function postJSON(data) {
try {
const response = await fetch("http://localhost:8085/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
console.log("Success:", result);
} catch (error) {
console.error("Error:", error);
}
}
</script>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1 +0,0 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}