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:
parent
377da44eed
commit
f7b74bd22c
|
@ -13,7 +13,6 @@
|
||||||
naersk' = pkgs.callPackage naersk { };
|
naersk' = pkgs.callPackage naersk { };
|
||||||
|
|
||||||
wwwPath = "www";
|
wwwPath = "www";
|
||||||
templatesPath = "templates";
|
|
||||||
|
|
||||||
in rec {
|
in rec {
|
||||||
# For `nix build` & `nix run`:
|
# For `nix build` & `nix run`:
|
||||||
|
@ -25,7 +24,6 @@
|
||||||
mkdir -p $out/templates
|
mkdir -p $out/templates
|
||||||
mkdir -p $out/www
|
mkdir -p $out/www
|
||||||
cp -r ${wwwPath} $out/
|
cp -r ${wwwPath} $out/
|
||||||
cp -r ${templatesPath} $out/
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,11 @@ in {
|
||||||
'npub' of the administrator account. Must be defined!
|
'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";
|
sslEnable = mkEnableOption "Whether to enable ACME SSL for nginx proxy";
|
||||||
hostAddress = mkOption {
|
hostAddress = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
|
@ -64,6 +69,7 @@ in {
|
||||||
environment = {
|
environment = {
|
||||||
DATABASE_URL = "${DB_PATH}/sneedstr.db";
|
DATABASE_URL = "${DB_PATH}/sneedstr.db";
|
||||||
ADMIN_PUBKEY = cfg.adminPubkey;
|
ADMIN_PUBKEY = cfg.adminPubkey;
|
||||||
|
CONFIG_ENABLE_AUTH = cfg.enableAuth;
|
||||||
};
|
};
|
||||||
startLimitBurst = 1;
|
startLimitBurst = 1;
|
||||||
startLimitIntervalSec = 10;
|
startLimitIntervalSec = 10;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
noose::sled::BanInfo,
|
noose::sled::BanInfo,
|
||||||
noose::user::{User, UserRow, Nip05Profile},
|
noose::user::{User, UserRow, Nip05Profile},
|
||||||
utils::{error::Error, structs::Subscription},
|
utils::{error::Error, structs::Subscription}, usernames::dto::UserBody,
|
||||||
};
|
};
|
||||||
use nostr::secp256k1::XOnlyPublicKey;
|
use nostr::secp256k1::XOnlyPublicKey;
|
||||||
use std::{collections::HashMap, fmt::Debug};
|
use std::{collections::{HashMap, BTreeSet}, fmt::Debug};
|
||||||
use tokio::sync::{broadcast, Mutex};
|
use tokio::sync::{broadcast, Mutex};
|
||||||
|
|
||||||
pub mod channels {
|
pub mod channels {
|
||||||
|
@ -18,22 +18,21 @@ pub mod channels {
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
// DbRequest
|
// DbRequest
|
||||||
|
// --- Req
|
||||||
DbReqWriteEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),
|
DbReqWriteEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),
|
||||||
DbReqFindEvent(/* client_id*/ uuid::Uuid, Subscription),
|
DbReqFindEvent(/* client_id*/ uuid::Uuid, Subscription),
|
||||||
DbReqDeleteEvents(/* client_id*/ uuid::Uuid, Box<nostr::Event>),
|
DbReqDeleteEvents(/* client_id*/ uuid::Uuid, Box<nostr::Event>),
|
||||||
DbReqEventCounts(/* client_id*/ uuid::Uuid, Subscription),
|
DbReqEventCounts(/* client_id*/ uuid::Uuid, Subscription),
|
||||||
|
|
||||||
// Old messages
|
// Old messages
|
||||||
DbReqInsertUser(UserRow),
|
DbReqInsertUser(UserRow),
|
||||||
DbReqCreateAccount(XOnlyPublicKey, String, String),
|
DbReqCreateAccount(XOnlyPublicKey, String, String),
|
||||||
DbReqGetAccount(String),
|
DbReqGetAccount(String),
|
||||||
DbReqClear,
|
DbReqClear,
|
||||||
// NIP-05 related messages
|
// NIP-05 related messages
|
||||||
DbReqGetUser(String),
|
DbReqGetNIP05(String),
|
||||||
DbResUser(Nip05Profile),
|
DbReqCreateNIP05(UserBody),
|
||||||
|
// --- Res
|
||||||
|
DbResNIP05(Nip05Profile),
|
||||||
// DbResponse
|
|
||||||
DbResRelayMessages(
|
DbResRelayMessages(
|
||||||
/* client_id*/ uuid::Uuid,
|
/* client_id*/ uuid::Uuid,
|
||||||
/* Vec<RelayMessage::Event> */ Vec<nostr::RelayMessage>,
|
/* Vec<RelayMessage::Event> */ Vec<nostr::RelayMessage>,
|
||||||
|
@ -43,24 +42,47 @@ pub enum Command {
|
||||||
DbResOkWithStatus(/* client_id */ uuid::Uuid, nostr::RelayMessage),
|
DbResOkWithStatus(/* client_id */ uuid::Uuid, nostr::RelayMessage),
|
||||||
DbResAccount, // TODO: Add Account DTO as a param
|
DbResAccount, // TODO: Add Account DTO as a param
|
||||||
DbResEventCounts(/* client_id */ uuid::Uuid, nostr::RelayMessage),
|
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
|
// Event Pipeline
|
||||||
|
// --- Req
|
||||||
PipelineReqEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),
|
PipelineReqEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),
|
||||||
|
// --- Res
|
||||||
PipelineResRelayMessageOk(/* client_id */ uuid::Uuid, nostr::RelayMessage),
|
PipelineResRelayMessageOk(/* client_id */ uuid::Uuid, nostr::RelayMessage),
|
||||||
PipelineResStreamOutEvent(Box<nostr::Event>),
|
PipelineResStreamOutEvent(Box<nostr::Event>),
|
||||||
PipelineResOk,
|
PipelineResOk,
|
||||||
// Subscription Errors
|
|
||||||
ClientSubscriptionError(/* error message */ String),
|
|
||||||
// Sled
|
// Sled
|
||||||
|
// --- Req
|
||||||
SledReqBanUser(Box<BanInfo>),
|
SledReqBanUser(Box<BanInfo>),
|
||||||
SledReqBanInfo(/* pubkey */ String),
|
SledReqBanInfo(/* pubkey */ String),
|
||||||
SledReqUnbanUser(/* pubkey */ String),
|
SledReqUnbanUser(/* pubkey */ String),
|
||||||
SledReqGetBans,
|
SledReqGetBans,
|
||||||
|
// --- Res
|
||||||
SledResBan(Option<BanInfo>),
|
SledResBan(Option<BanInfo>),
|
||||||
SledResBans(Vec<BanInfo>),
|
SledResBans(Vec<BanInfo>),
|
||||||
SledResSuccess(bool),
|
SledResSuccess(bool),
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
|
ServiceRegistrationRequired(/* client_id */ uuid::Uuid, nostr::RelayMessage),
|
||||||
Str(String),
|
Str(String),
|
||||||
ServiceError(Error),
|
ServiceError(Error),
|
||||||
|
|
||||||
|
// Subscription Errors
|
||||||
|
ClientSubscriptionError(/* error message */ String),
|
||||||
|
|
||||||
|
// --- Noop
|
||||||
Noop,
|
Noop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,58 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
bussy::PubSub,
|
bussy::PubSub,
|
||||||
|
usernames::dto::UserBody,
|
||||||
utils::{error::Error, structs::Subscription},
|
utils::{error::Error, structs::Subscription},
|
||||||
};
|
};
|
||||||
|
|
||||||
use nostr::{Event, RelayMessage};
|
use nostr::{secp256k1::XOnlyPublicKey, Event, RelayMessage};
|
||||||
use std::sync::Arc;
|
use nostr_database::Profile;
|
||||||
|
use std::{collections::BTreeSet, sync::Arc};
|
||||||
|
|
||||||
use super::user::Nip05Profile;
|
use super::user::Nip05Profile;
|
||||||
|
|
||||||
|
/// Handle core nostr events
|
||||||
pub trait Noose: Send + Sync {
|
pub trait Noose: Send + Sync {
|
||||||
|
/// Start event listener
|
||||||
async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error>;
|
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>;
|
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>;
|
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>;
|
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>;
|
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>;
|
||||||
}
|
}
|
||||||
|
|
9
src/noose/migrations/1706575155557_nip05.sql
Normal file
9
src/noose/migrations/1706575155557_nip05.sql
Normal 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);
|
27
src/noose/migrations/1707327016995_nip42_profile.sql
Normal file
27
src/noose/migrations/1707327016995_nip42_profile.sql
Normal 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);
|
||||||
|
|
|
@ -12,6 +12,7 @@ impl MigrationRunner {
|
||||||
let m_users = include_str!("./1697410294265_users.sql");
|
let m_users = include_str!("./1697410294265_users.sql");
|
||||||
let m_unattached_media = include_str!("./1697410480767_unattached_media.sql");
|
let m_unattached_media = include_str!("./1697410480767_unattached_media.sql");
|
||||||
let m_nip05 = include_str!("./1706575155557_nip05.sql");
|
let m_nip05 = include_str!("./1706575155557_nip05.sql");
|
||||||
|
let m_nip42 = include_str!("./1707327016995_nip42_profile.sql");
|
||||||
|
|
||||||
let migrations = Migrations::new(vec![
|
let migrations = Migrations::new(vec![
|
||||||
M::up(m_create_events),
|
M::up(m_create_events),
|
||||||
|
@ -21,6 +22,7 @@ impl MigrationRunner {
|
||||||
M::up(m_users),
|
M::up(m_users),
|
||||||
M::up(m_unattached_media),
|
M::up(m_unattached_media),
|
||||||
M::up(m_nip05),
|
M::up(m_nip05),
|
||||||
|
M::up(m_nip42),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
match migrations.to_latest(connection) {
|
match migrations.to_latest(connection) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ mod migrations;
|
||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
pub mod sled;
|
pub mod sled;
|
||||||
mod sqlite;
|
mod sqlite;
|
||||||
|
mod sqlite_tables;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
pub fn start(context: Context) {
|
pub fn start(context: Context) {
|
||||||
|
|
|
@ -5,176 +5,23 @@ use super::{
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
bussy::{channels, Command, Message, PubSub},
|
bussy::{channels, Command, Message, PubSub},
|
||||||
|
usernames::dto::UserBody,
|
||||||
utils::{config::Config as ServiceConfig, error::Error, structs::Subscription},
|
utils::{config::Config as ServiceConfig, error::Error, structs::Subscription},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use deadpool_sqlite::{Config, Object, Pool, Runtime};
|
use deadpool_sqlite::{Config, Object, Pool, Runtime};
|
||||||
use nostr::{
|
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 nostr_database::{Backend, DatabaseOptions, NostrDatabase, Order};
|
||||||
use rusqlite::Row;
|
|
||||||
use sea_query::{extension::sqlite::SqliteExpr, Order as SqOrder, Query, SqliteQueryBuilder};
|
use sea_query::{extension::sqlite::SqliteExpr, Order as SqOrder, Query, SqliteQueryBuilder};
|
||||||
use sea_query_rusqlite::RusqliteBinder;
|
use sea_query_rusqlite::RusqliteBinder;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{collections::HashSet, str::FromStr};
|
use std::{collections::HashSet, str::FromStr};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
use crate::noose::sqlite_tables::*;
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NostrSqlite {
|
pub struct NostrSqlite {
|
||||||
|
@ -494,86 +341,159 @@ impl NostrSqlite {
|
||||||
|
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log::debug!("inserting new event in events");
|
match tx.commit() {
|
||||||
// Insert into Events table
|
Ok(_) => return Ok(true),
|
||||||
let (sql, values) = Query::insert()
|
Err(err) => {
|
||||||
.into_table(EventsTable::Table)
|
log::error!("Error during transaction commit: {}", err);
|
||||||
.columns([
|
return Ok(false);
|
||||||
EventsTable::EventId,
|
}
|
||||||
EventsTable::Content,
|
}
|
||||||
EventsTable::Kind,
|
}
|
||||||
EventsTable::Pubkey,
|
|
||||||
EventsTable::CreatedAt,
|
if event.kind() == nostr::Kind::Metadata {
|
||||||
EventsTable::Tags,
|
// Try to delete old profile first
|
||||||
EventsTable::Sig,
|
let (sql, value) = Query::delete()
|
||||||
])
|
.from_table(ProfilesTable::Table)
|
||||||
.values_panic([
|
.and_where(sea_query::Expr::col(ProfilesTable::Pubkey).eq(pubkey.clone()))
|
||||||
id.clone().into(),
|
|
||||||
content.clone().into(),
|
|
||||||
kind.into(),
|
|
||||||
pubkey.into(),
|
|
||||||
created_at.into(),
|
|
||||||
tags.into(),
|
|
||||||
sig.into(),
|
|
||||||
])
|
|
||||||
.build_rusqlite(SqliteQueryBuilder);
|
.build_rusqlite(SqliteQueryBuilder);
|
||||||
|
|
||||||
if let Err(err) = tx.execute(sql.as_str(), &*values.as_params()) {
|
if let Err(err) = tx.execute(sql.as_str(), &*value.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();
|
tx.rollback().unwrap();
|
||||||
|
|
||||||
|
log::debug!("Failed to delete old Profile record: {}", err);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert into Tags table
|
let (sql, value) = Query::insert()
|
||||||
log::debug!("inserting new event into tags");
|
.into_table(ProfilesTable::Table)
|
||||||
for tag in event.tags.clone() {
|
.columns([ProfilesTable::Pubkey])
|
||||||
if Self::tag_is_indexable(&tag) {
|
.values_panic([pubkey.clone().into()])
|
||||||
let tag = tag.to_vec();
|
.build_rusqlite(SqliteQueryBuilder);
|
||||||
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())
|
if let Err(err) = tx.execute(sql.as_str(), &*value.as_params()) {
|
||||||
{
|
tx.rollback().unwrap();
|
||||||
log::error!(
|
|
||||||
"Error inserting event into 'tags' table: {}",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
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
|
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 {
|
impl From<nostr_database::DatabaseError> for Error {
|
||||||
|
@ -1417,10 +1436,43 @@ impl Noose for NostrSqlite {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// NIP-05
|
// NIP-05
|
||||||
Command::DbReqGetUser(username) => match self.get_nip05(username).await {
|
Command::DbReqGetNIP05(username) => match self.get_nip05(username).await {
|
||||||
Ok(user) => Command::DbResUser(user),
|
Ok(user) => Command::DbResNIP05(user),
|
||||||
Err(e) => Command::ServiceError(e),
|
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,
|
_ => Command::Noop,
|
||||||
};
|
};
|
||||||
if command != Command::Noop {
|
if command != Command::Noop {
|
||||||
|
@ -1489,11 +1541,53 @@ impl Noose for NostrSqlite {
|
||||||
async fn get_nip05(&self, username: String) -> Result<Nip05Profile, Error> {
|
async fn get_nip05(&self, username: String) -> Result<Nip05Profile, Error> {
|
||||||
self.get_nip05_profile(username).await
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::noose::sqlite::*;
|
use crate::noose::sqlite::*;
|
||||||
|
use nostr::key::FromSkStr;
|
||||||
use nostr::EventBuilder;
|
use nostr::EventBuilder;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -1718,6 +1812,29 @@ mod tests {
|
||||||
assert_eq!(result.len(), 1)
|
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]
|
#[tokio::test]
|
||||||
async fn save_event_with_a_tag() {
|
async fn save_event_with_a_tag() {
|
||||||
let config = Arc::new(ServiceConfig::new());
|
let config = Arc::new(ServiceConfig::new());
|
||||||
|
|
322
src/noose/sqlite_tables.rs
Normal file
322
src/noose/sqlite_tables.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
107
src/relay/ws.rs
107
src/relay/ws.rs
|
@ -94,6 +94,17 @@ pub async fn client_connection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
crate::bussy::Command::ServiceRegistrationRequired(client_id, relay_message) => {
|
||||||
|
if client.client_id == client_id {
|
||||||
|
if let Some(sender) = &client.client_connection {
|
||||||
|
if !sender.is_closed() {
|
||||||
|
log::info!("[Relay] client needs to be authenticated to make request: {}", relay_message.as_json());
|
||||||
|
sender.send(Ok(Message::text(relay_message.as_json()))).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
_ => ()
|
_ => ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,13 +112,6 @@ pub async fn client_connection(
|
||||||
Some(message) = client_receiver.next() => {
|
Some(message) = client_receiver.next() => {
|
||||||
match message {
|
match message {
|
||||||
Ok(message) => {
|
Ok(message) => {
|
||||||
// ws_sender
|
|
||||||
// .send(message)
|
|
||||||
// .unwrap_or_else(|e| {
|
|
||||||
// log::error!("websocket send error: {}", e);
|
|
||||||
// })
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
match ws_sender.send(message).await {
|
match ws_sender.send(message).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {
|
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 {} - {}] message: {}",
|
||||||
client.ip(),
|
client.ip(),
|
||||||
client.client_id,
|
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) {
|
async fn handle_msg(context: &Context, client: &mut Client, client_message: ClientMessage) {
|
||||||
match client_message {
|
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 {
|
ClientMessage::Req {
|
||||||
subscription_id,
|
subscription_id,
|
||||||
filters,
|
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 {
|
ClientMessage::Count {
|
||||||
subscription_id,
|
subscription_id,
|
||||||
filters,
|
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::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>) {
|
async fn handle_event(context: &Context, client: &Client, event: Box<Event>) {
|
||||||
log::debug!("handle_event is processing new 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) {
|
async fn handle_close(client: &mut Client, subscription_id: SubscriptionId) {
|
||||||
// context.pubsub.send(new nostr event) then handle possible errors
|
|
||||||
client.unsubscribe(subscription_id);
|
client.unsubscribe(subscription_id);
|
||||||
|
|
||||||
// let message = Message::text("CLOSE not implemented");
|
|
||||||
// send(client, message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_auth(client: &Client, event: Box<Event>) {
|
async fn handle_auth(context: &Context, client: &mut Client, event: Box<Event>) {
|
||||||
let message = Message::text("AUTH not implemented");
|
client.authenticate(&event);
|
||||||
send(client, message);
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
use std::collections::HashMap;
|
use crate::usernames::validators::{validate_pubkey, validate_relays};
|
||||||
|
|
||||||
use crate::usernames::validators::validate_pubkey;
|
|
||||||
use crate::utils::error::Error;
|
use crate::utils::error::Error;
|
||||||
|
use nostr::key::XOnlyPublicKey;
|
||||||
use nostr::prelude::*;
|
use nostr::prelude::*;
|
||||||
use nostr::{key::XOnlyPublicKey, Keys};
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
@ -12,27 +10,22 @@ lazy_static! {
|
||||||
static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap();
|
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 {
|
pub struct UserBody {
|
||||||
#[validate(length(min = 1), regex = "VALID_CHARACTERS")]
|
#[validate(length(min = 1), regex = "VALID_CHARACTERS")]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[validate(custom(function = "validate_pubkey"))]
|
#[validate(custom(function = "validate_pubkey"))]
|
||||||
pub pubkey: String,
|
pubkey: String,
|
||||||
|
#[validate(custom(function = "validate_relays"))]
|
||||||
|
pub relays: Vec<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserBody {
|
impl UserBody {
|
||||||
pub fn get_pubkey(&self) -> XOnlyPublicKey {
|
pub fn get_pubkey(&self) -> String {
|
||||||
let keys = Keys::from_pk_str(&self.pubkey).unwrap();
|
nostr::Keys::from_pk_str(&self.pubkey).unwrap().public_key().to_string()
|
||||||
|
|
||||||
keys.public_key()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub struct Nip05 {
|
|
||||||
names: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
|
||||||
pub struct UserQuery {
|
pub struct UserQuery {
|
||||||
#[validate(length(min = 1))]
|
#[validate(length(min = 1))]
|
||||||
|
|
|
@ -2,10 +2,6 @@ use crate::utils::error::Error;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
use warp::{Filter, Rejection};
|
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>(
|
pub fn validate_body_filter<T: serde::de::DeserializeOwned + Send + Validate + 'static>(
|
||||||
) -> impl Filter<Extract = (T,), Error = Rejection> + Copy {
|
) -> impl Filter<Extract = (T,), Error = Rejection> + Copy {
|
||||||
warp::body::json::<T>().and_then(|query: T| async move {
|
warp::body::json::<T>().and_then(|query: T| async move {
|
||||||
|
|
|
@ -6,6 +6,8 @@ use crate::utils::structs::Context;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use warp::{Rejection, Reply};
|
use warp::{Rejection, Reply};
|
||||||
|
|
||||||
|
use super::dto::UserBody;
|
||||||
|
|
||||||
pub async fn get_account(
|
pub async fn get_account(
|
||||||
// account: Result<AccountPubkey, Error>,
|
// account: Result<AccountPubkey, Error>,
|
||||||
account: Account,
|
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> {
|
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 mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
|
||||||
|
|
||||||
let command = Command::DbReqGetUser(user_query.name);
|
let command = Command::DbReqGetNIP05(user_query.name);
|
||||||
context
|
context
|
||||||
.pubsub
|
.pubsub
|
||||||
.publish(
|
.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 {
|
if let Ok(message) = subscriber.recv().await {
|
||||||
match message.content {
|
match message.content {
|
||||||
Command::DbResUser(profile) => {
|
Command::DbResNIP05(profile) => {
|
||||||
let response = serde_json::to_value(profile).unwrap();
|
let response = serde_json::to_value(profile).unwrap();
|
||||||
Ok(warp::reply::json(&response))
|
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",
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::noose::user::User;
|
use crate::noose::user::User;
|
||||||
|
|
||||||
// use super::accounts::create_account;
|
// 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::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::filter::with_context;
|
||||||
use crate::utils::structs::Context;
|
use crate::utils::structs::Context;
|
||||||
use warp::{Filter, Rejection, Reply};
|
use warp::{Filter, Rejection, Reply};
|
||||||
|
@ -14,25 +14,37 @@ pub fn routes(context: Context) -> impl Filter<Extract = impl Reply, Error = Rej
|
||||||
|
|
||||||
index
|
index
|
||||||
.or(nip05_get(context.clone()))
|
.or(nip05_get(context.clone()))
|
||||||
// .or(account_create(context.clone()))
|
.or(nip05_create(context.clone()))
|
||||||
.with(&cors)
|
.with(&cors)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn well_known() -> impl Filter<Extract = (), Error = Rejection> + Clone {
|
fn well_known<M>(warp_method: M) -> impl Filter<Extract = (), Error = Rejection> + Clone
|
||||||
warp::get().and(warp::path(".well-known"))
|
where
|
||||||
|
M: (Filter<Extract = (), Error = Rejection>) + Copy,
|
||||||
|
{
|
||||||
|
warp_method.and(warp::path(".well-known"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nostr_well_known() -> impl Filter<Extract = (), Error = Rejection> + Clone {
|
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()
|
nostr_well_known()
|
||||||
.and(validate_query_filter::<UserQuery>())
|
.and(validate_query_filter::<UserQuery>())
|
||||||
.and(with_context(context.clone()))
|
.and(with_context(context.clone()))
|
||||||
.and_then(get_user)
|
.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(
|
// pub fn account_create(
|
||||||
// context: Context,
|
// context: Context,
|
||||||
// ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
// ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use super::dto::AccountPubkey;
|
use super::dto::AccountPubkey;
|
||||||
use crate::utils::error::Error;
|
use crate::utils::error::Error;
|
||||||
use nostr::prelude::FromPkStr;
|
use nostr::prelude::FromPkStr;
|
||||||
use validator::{Validate, ValidationError};
|
use validator::{Validate, ValidationError, validate_url};
|
||||||
|
|
||||||
pub async fn validate_account_pubkey_query(
|
pub async fn validate_account_pubkey_query(
|
||||||
account_pubkey: AccountPubkey,
|
account_pubkey: AccountPubkey,
|
||||||
|
@ -25,6 +25,18 @@ pub fn validate_pubkey(value: &str) -> Result<(), ValidationError> {
|
||||||
|
|
||||||
match nostr::Keys::from_pk_str(value) {
|
match nostr::Keys::from_pk_str(value) {
|
||||||
Ok(_) => Ok(()),
|
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"))
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use nostr::{key::FromPkStr, secp256k1::XOnlyPublicKey};
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
admin_pubkey: XOnlyPublicKey,
|
admin_pubkey: XOnlyPublicKey,
|
||||||
db_path: PathBuf,
|
db_path: PathBuf,
|
||||||
|
auth_required: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
@ -23,13 +24,22 @@ impl Config {
|
||||||
.public_key();
|
.public_key();
|
||||||
|
|
||||||
let db_path = std::env::var("DATABASE_URL").map(PathBuf::from).unwrap();
|
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 {
|
Self {
|
||||||
admin_pubkey,
|
admin_pubkey,
|
||||||
db_path,
|
db_path,
|
||||||
|
auth_required,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn auth_required(&self) -> bool {
|
||||||
|
self.auth_required
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_admin_pubkey(&self) -> &XOnlyPublicKey {
|
pub fn get_admin_pubkey(&self) -> &XOnlyPublicKey {
|
||||||
&self.admin_pubkey
|
&self.admin_pubkey
|
||||||
}
|
}
|
||||||
|
@ -43,7 +53,7 @@ impl Config {
|
||||||
"contact": "klink@zhitno.st",
|
"contact": "klink@zhitno.st",
|
||||||
"name": "zhitno.st",
|
"name": "zhitno.st",
|
||||||
"description": "Very *special* nostr relay",
|
"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",
|
"software": "git+https://git.zhitno.st/Klink/sneedstr.git",
|
||||||
"version": "0.1.1"
|
"version": "0.1.1"
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::PubSub;
|
||||||
|
|
||||||
use nostr::{Event, Filter, SubscriptionId};
|
use nostr::{Event, Filter, SubscriptionId};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -50,6 +51,8 @@ pub struct Client {
|
||||||
pub client_id: Uuid,
|
pub client_id: Uuid,
|
||||||
pub client_connection: Option<mpsc::UnboundedSender<Result<Message, Error>>>,
|
pub client_connection: Option<mpsc::UnboundedSender<Result<Message, Error>>>,
|
||||||
pub subscriptions: HashMap<String, Subscription>,
|
pub subscriptions: HashMap<String, Subscription>,
|
||||||
|
pub authenticated: bool, // NIP-42
|
||||||
|
challlenge: Option<String>, // NIP-42
|
||||||
max_subs: usize,
|
max_subs: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +63,8 @@ impl Client {
|
||||||
client_id: Uuid::new_v4(),
|
client_id: Uuid::new_v4(),
|
||||||
client_connection: None,
|
client_connection: None,
|
||||||
subscriptions: HashMap::new(),
|
subscriptions: HashMap::new(),
|
||||||
|
authenticated: false,
|
||||||
|
challlenge: None,
|
||||||
max_subs: MAX_SUBSCRIPTIONS,
|
max_subs: MAX_SUBSCRIPTIONS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,6 +73,43 @@ impl Client {
|
||||||
&self.client_ip_addr
|
&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> {
|
pub fn subscribe(&mut self, subscription: Subscription) -> Result<(), Error> {
|
||||||
let k = subscription.get_id();
|
let k = subscription.get_id();
|
||||||
let sub_id_len = k.len();
|
let sub_id_len = k.len();
|
||||||
|
|
Loading…
Reference in a new issue