Initial commit
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
.direnv
|
||||
result
|
||||
target
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
2996
Cargo.lock
generated
Normal file
40
Cargo.toml
Normal file
|
@ -0,0 +1,40 @@
|
|||
[package]
|
||||
name = "sneedstr"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
argon2 = "0.5.2"
|
||||
async-trait = "0.1.73"
|
||||
chrono = "0.4.31"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
warp = { varsion = "0.3.3", features = ["tls"] }
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
tokio-stream = "0.1.14"
|
||||
futures-util = "0.3.28"
|
||||
rustls = "0.21"
|
||||
anyhow = "1.0"
|
||||
sled = "0.34.7"
|
||||
sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-rustls", "sqlite", "migrate", "macros"] }
|
||||
flexi_logger = { version = "0.27.3", features = [ "async", "compress" ] }
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4"
|
||||
nostr = "0.26.0"
|
||||
regex = "1.9.5"
|
||||
sailfish = "0.7.0"
|
||||
sea-query = { version = "0.30.4", features = ["backend-sqlite", "thread-safe"] }
|
||||
sea-query-binder = { version = "0.5.0", features = ["sqlx-sqlite"] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0.48"
|
||||
flexbuffers = "2.0.0"
|
||||
uuid = { version = "1.3.1", features = ["v4", "fast-rng"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 'z' # Optimize for size
|
||||
lto = true # Enable link-time optimization
|
||||
codegen-units = 1 # Reduce number of codegen units to increase optimizations
|
||||
panic = 'abort' # Abort on panic
|
||||
strip = true # Strip symbols from binary*
|
8
LICENSE
Normal file
|
@ -0,0 +1,8 @@
|
|||
Copyright (C) 2024 Klink
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
|
||||
|
||||
The above copyright notice, this permission notice and the word "NIGGER" shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>
|
93
flake.lock
Normal file
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1698420672,
|
||||
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1703438236,
|
||||
"narHash": "sha256-aqVBq1u09yFhL7bj1/xyUeJjzr92fXVvQSSEx6AdB1M=",
|
||||
"path": "/nix/store/5acdh8xyry0kdvp6xla2hw7wf3zkphkl-source",
|
||||
"rev": "5f64a12a728902226210bf01d25ec6cbb9d9265b",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1703499205,
|
||||
"narHash": "sha256-lF9rK5mSUfIZJgZxC3ge40tp1gmyyOXZ+lRY3P8bfbg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e1fa12d4f6c6fe19ccb59cac54b5b3f25e160870",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
52
flake.nix
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
outputs = { self, flake-utils, naersk, nixpkgs }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = (import nixpkgs) { inherit system; };
|
||||
|
||||
naersk' = pkgs.callPackage naersk { };
|
||||
|
||||
wwwPath = "www";
|
||||
templatesPath = "templates";
|
||||
|
||||
in rec {
|
||||
# For `nix build` & `nix run`:
|
||||
defaultPackage = naersk'.buildPackage {
|
||||
src = ./.;
|
||||
nativeBuildInputs = with pkgs; [ pkg-config openssl sqlite ];
|
||||
GIT_HASH = "000000000000000000000000000000";
|
||||
postInstall = ''
|
||||
mkdir -p $out/templates
|
||||
mkdir -p $out/www
|
||||
cp -r ${wwwPath} $out/
|
||||
cp -r ${templatesPath} $out/
|
||||
'';
|
||||
};
|
||||
|
||||
# For `nix develop`:
|
||||
devShell = pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
rustc
|
||||
cargo
|
||||
cargo-watch
|
||||
clippy
|
||||
rustfmt
|
||||
rust-analyzer
|
||||
pkg-config
|
||||
|
||||
openssl
|
||||
sqlite
|
||||
];
|
||||
env = {
|
||||
DATABASE_URL = "sqlite://sqlite.db";
|
||||
RUST_BACKTRACE = 1;
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
143
src/bussy/mod.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
use crate::{
|
||||
noose::user::{User, UserRow},
|
||||
utils::{error::Error, structs::Subscription},
|
||||
};
|
||||
use nostr::secp256k1::XOnlyPublicKey;
|
||||
use std::{collections::HashMap, 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_RELAY: &str = "MSG_RELAY";
|
||||
pub static MSG_PIPELINE: &str = "MSG_PIPELINE";
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Command {
|
||||
// DbRequest
|
||||
DbReqWriteEvent(Box<nostr::Event>),
|
||||
DbReqFindEvent(/* client_id*/ uuid::Uuid, Subscription),
|
||||
DbReqDeleteEvents(/* event ids */ Vec<String>),
|
||||
|
||||
// Old messages
|
||||
DbReqInsertUser(UserRow),
|
||||
DbReqGetUser(User),
|
||||
DbReqCreateAccount(XOnlyPublicKey, String, String),
|
||||
DbReqGetAccount(String),
|
||||
DbReqClear,
|
||||
// DbResponse
|
||||
DbResRelayMessage(
|
||||
/* client_id*/ uuid::Uuid,
|
||||
/* Vec<RelayMessage::Event> */ Vec<String>,
|
||||
),
|
||||
DbResInfo,
|
||||
DbResOk,
|
||||
DbResOkWithStatus(String),
|
||||
DbResAccount, // TODO: Add Account DTO as a param
|
||||
DbResUser(UserRow),
|
||||
// Event Pipeline
|
||||
PipelineReqEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),
|
||||
PipelineResRelayMessageOk(/* client_id */ uuid::Uuid, nostr::RelayMessage),
|
||||
PipelineResStreamOutEvent(Box<nostr::Event>),
|
||||
PipelineResOk,
|
||||
// Other
|
||||
Str(String),
|
||||
ServiceError(Error),
|
||||
Noop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub source: &'static str,
|
||||
pub content: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PubSub {
|
||||
subscribers: Mutex<HashMap<String, Vec<broadcast::Sender<Message>>>>,
|
||||
}
|
||||
|
||||
impl Default for PubSub {
|
||||
fn default() -> Self {
|
||||
panic!("Use PubSub::new() to initialize PubSub");
|
||||
}
|
||||
}
|
||||
|
||||
impl PubSub {
|
||||
pub fn new() -> Self {
|
||||
PubSub {
|
||||
subscribers: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn subscribe(&self, topic: &str) -> broadcast::Receiver<Message> {
|
||||
let (tx, _rx) = broadcast::channel(32); // 32 is the channel capacity
|
||||
let mut subscribers = self.subscribers.lock().await;
|
||||
subscribers
|
||||
.entry(topic.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(tx.clone());
|
||||
tx.subscribe()
|
||||
}
|
||||
|
||||
pub async fn publish(&self, topic: &str, message: Message) {
|
||||
let mut subscribers = self.subscribers.lock().await;
|
||||
if let Some(queue) = subscribers.get_mut(topic) {
|
||||
for sender in queue.iter() {
|
||||
sender.send(message.clone()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::channels;
|
||||
// use crate::bussy::{Command, Message, PubSub};
|
||||
// use std::sync::Arc;
|
||||
|
||||
// #[tokio::test]
|
||||
// async fn create_bus() {
|
||||
// let pubsub = Arc::new(PubSub::new());
|
||||
|
||||
// let mut subscriber1 = pubsub.subscribe(channels::MSG_NIP05).await;
|
||||
// let mut subscriber2 = pubsub.subscribe(channels::MSG_NOOSE).await;
|
||||
|
||||
// tokio::spawn(async move {
|
||||
// while let Ok(message) = subscriber1.recv().await {
|
||||
// println!("Subscriber1 received: {:?}", message);
|
||||
// }
|
||||
// });
|
||||
|
||||
// tokio::spawn(async move {
|
||||
// while let Ok(message) = subscriber2.recv().await {
|
||||
// println!("Subscriber2 received: {:?}", message);
|
||||
// }
|
||||
// });
|
||||
|
||||
// pubsub
|
||||
// .publish(
|
||||
// channels::MSG_NIP05,
|
||||
// Message {
|
||||
// source: "test",
|
||||
// content: Command::Str("Hello S1".to_string()),
|
||||
// },
|
||||
// )
|
||||
// .await;
|
||||
// pubsub
|
||||
// .publish(
|
||||
// channels::MSG_NOOSE,
|
||||
// Message {
|
||||
// source: "test",
|
||||
// content: Command::Str("Hello S2".to_string()),
|
||||
// },
|
||||
// )
|
||||
// .await;
|
||||
|
||||
// dbg!(pubsub);
|
||||
|
||||
// // Sleep to keep the main thread alive while messages are processed in the background.
|
||||
// tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
// }
|
||||
// }
|
51
src/main.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
#![allow(dead_code)]
|
||||
#![allow(unused_variables)]
|
||||
|
||||
use std::thread;
|
||||
pub mod bussy;
|
||||
mod noose;
|
||||
mod relay;
|
||||
mod usernames;
|
||||
pub mod utils;
|
||||
|
||||
use bussy::PubSub;
|
||||
use flexi_logger::Logger;
|
||||
|
||||
use crate::utils::structs::Context;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
fn main() {
|
||||
// Logger init
|
||||
let _logger = Logger::try_with_env_or_str("info")
|
||||
.unwrap()
|
||||
.log_to_stdout()
|
||||
.duplicate_to_stderr(flexi_logger::Duplicate::Warn)
|
||||
.write_mode(flexi_logger::WriteMode::Async)
|
||||
.start()
|
||||
.unwrap();
|
||||
|
||||
// Services
|
||||
let ctx_db = Context::new();
|
||||
let ctx_relay = ctx_db.clone();
|
||||
let ctx_usernames = ctx_relay.clone();
|
||||
|
||||
let db_handle = thread::spawn(move || {
|
||||
noose::start(ctx_db);
|
||||
});
|
||||
|
||||
let relay_handle = thread::spawn(move || {
|
||||
relay::start(ctx_relay);
|
||||
});
|
||||
|
||||
let nip05_handle = thread::spawn(move || {
|
||||
usernames::start(ctx_usernames);
|
||||
});
|
||||
|
||||
db_handle.join().unwrap();
|
||||
relay_handle.join().unwrap();
|
||||
nip05_handle.join().unwrap();
|
||||
|
||||
println!("Done");
|
||||
}
|
18
src/noose/db.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use crate::{
|
||||
bussy::PubSub,
|
||||
utils::{error::Error, structs::Subscription},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use nostr::Event;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Noose: Send + Sync {
|
||||
async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error>;
|
||||
|
||||
async fn migration_up(&self);
|
||||
|
||||
async fn write_event(&self, event: Box<Event>) -> Result<String, Error>;
|
||||
|
||||
async fn find_event(&self, subscription: Subscription) -> Result<Vec<String>, Error>;
|
||||
}
|
23
src/noose/migrations/1697409647688_create_events.sql
Normal file
|
@ -0,0 +1,23 @@
|
|||
CREATE TABLE events (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind INTEGER NOT NULL,
|
||||
pubkey TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
tags TEXT NOT NULL,
|
||||
sig TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_events_kind ON events (kind);
|
||||
CREATE INDEX idx_events_pubkey ON events (pubkey);
|
||||
|
||||
|
||||
CREATE TABLE tags (
|
||||
tag TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
event_id TEXT REFERENCES events(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tags_tag ON tags (tag);
|
||||
CREATE INDEX idx_tags_value ON tags (value);
|
||||
CREATE INDEX idx_tags_event_id ON tags (event_id);
|
5
src/noose/migrations/1697410161900_add_relays.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE relays (
|
||||
url TEXT PRIMARY KEY,
|
||||
domain TEXT NOT NULL,
|
||||
active BOOLEAN NOT NULL
|
||||
);
|
1
src/noose/migrations/1697410223576_events_fts.sql
Normal file
|
@ -0,0 +1 @@
|
|||
CREATE VIRTUAL TABLE events_fts USING fts5(id, content);
|
10
src/noose/migrations/1697410294265_users.sql
Normal file
|
@ -0,0 +1,10 @@
|
|||
CREATE TABLE users (
|
||||
pubkey TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
inserted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
admin BOOLEAN DEFAULT false
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_pubkey ON users (pubkey);
|
||||
CREATE INDEX idx_users_username ON users (username);
|
||||
|
2
src/noose/migrations/1697410424624_pragma.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
PRAGMA foreign_keys = ON;
|
||||
PRAGMA auto_vacuum = FULL;
|
11
src/noose/migrations/1697410480767_unattached_media.sql
Normal file
|
@ -0,0 +1,11 @@
|
|||
CREATE TABLE unattached_media (
|
||||
id TEXT PRIMARY KEY,
|
||||
pubkey TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX unattached_media_id ON unattached_media (id);
|
||||
CREATE INDEX unattached_media_pubkey ON unattached_media (pubkey);
|
||||
CREATE INDEX unattached_media_url ON unattached_media (url);
|
33
src/noose/mod.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use crate::utils::structs::Context;
|
||||
use tokio::runtime;
|
||||
|
||||
use db::Noose;
|
||||
use pipeline::Pipeline;
|
||||
|
||||
pub mod db;
|
||||
pub mod pipeline;
|
||||
// mod sled;
|
||||
mod sqlite;
|
||||
pub mod user;
|
||||
|
||||
pub fn start(context: Context) {
|
||||
let rt = runtime::Runtime::new().unwrap();
|
||||
|
||||
rt.block_on(async move {
|
||||
let pipeline_pubsub = context.pubsub.clone();
|
||||
let db_pubsub = context.pubsub.clone();
|
||||
|
||||
let pipeline_handle = tokio::task::spawn(async move {
|
||||
let mut pipeline = Pipeline::new(pipeline_pubsub);
|
||||
pipeline.start().await.unwrap();
|
||||
});
|
||||
|
||||
let sqlite_writer_handle = tokio::task::spawn(async move {
|
||||
let mut db_writer = sqlite::SqliteDb::new().await;
|
||||
db_writer.start(db_pubsub).await.unwrap();
|
||||
});
|
||||
|
||||
sqlite_writer_handle.await.unwrap();
|
||||
pipeline_handle.await.unwrap();
|
||||
});
|
||||
}
|
166
src/noose/pipeline.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use crate::bussy::{channels, Command, Message, PubSub};
|
||||
use crate::utils::error::Error;
|
||||
use nostr::Event;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Pipeline {
|
||||
pubsub: Arc<PubSub>,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
pub fn new(pubsub: Arc<PubSub>) -> Self {
|
||||
Self { pubsub }
|
||||
}
|
||||
|
||||
pub async fn start(&mut self) -> Result<(), Error> {
|
||||
let mut subscriber = self.pubsub.subscribe(channels::MSG_PIPELINE).await;
|
||||
|
||||
while let Ok(message) = subscriber.recv().await {
|
||||
log::debug!("[Pipeline] received message: {:?}", message);
|
||||
let command = match message.content {
|
||||
Command::PipelineReqEvent(client_id, event) => {
|
||||
match self.handle_event(client_id, event.clone()).await {
|
||||
Ok(_) => {
|
||||
let message =
|
||||
nostr::RelayMessage::new_ok(event.id, true, "".to_string());
|
||||
Command::PipelineResRelayMessageOk(client_id, message)
|
||||
}
|
||||
Err(e) => Command::ServiceError(e),
|
||||
}
|
||||
}
|
||||
_ => Command::Noop,
|
||||
};
|
||||
if command != Command::Noop {
|
||||
let channel = message.source;
|
||||
let message = Message {
|
||||
source: channels::MSG_PIPELINE,
|
||||
content: command,
|
||||
};
|
||||
|
||||
log::info!(
|
||||
"[Pipeline] channel: {} - publishing new message: {:?}",
|
||||
channel,
|
||||
message
|
||||
);
|
||||
|
||||
self.pubsub.publish(channel, message).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_event(
|
||||
&self,
|
||||
client_id: uuid::Uuid,
|
||||
event: Box<Event>,
|
||||
) -> Result<(), Error> {
|
||||
let store_event_task = self.store_event(event.clone());
|
||||
let process_deletions_task = self.process_deletions(event.clone());
|
||||
let track_hashtags_task = self.track_hashtags(event.clone());
|
||||
let process_media_task = self.process_media(event.clone());
|
||||
let stream_out_task = self.stream_out(event.clone());
|
||||
let broadcast_task = self.broadcast(event.clone());
|
||||
|
||||
let (
|
||||
store_event_result,
|
||||
process_deletions_result,
|
||||
track_hashtags_result,
|
||||
process_media_result,
|
||||
stream_out_result,
|
||||
broadcast_result,
|
||||
) = tokio::join!(
|
||||
store_event_task,
|
||||
process_deletions_task,
|
||||
track_hashtags_task,
|
||||
process_media_task,
|
||||
stream_out_task,
|
||||
broadcast_task
|
||||
);
|
||||
|
||||
match (
|
||||
store_event_result,
|
||||
process_deletions_result,
|
||||
track_hashtags_result,
|
||||
process_media_result,
|
||||
stream_out_result,
|
||||
broadcast_result,
|
||||
) {
|
||||
(Ok(_), Ok(_), Ok(_), Ok(_), Ok(_), Ok(_)) => {
|
||||
log::info!("[Pipeline] Tasks finished successfully");
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
log::error!("[Pipeline] One or more futures returned an error.");
|
||||
Err(Error::internal_with_message(
|
||||
"[Pipeline] One or more futures returned an error.",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn store_event(&self, event: Box<Event>) -> Result<(), Error> {
|
||||
if event.kind.is_ephemeral() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.pubsub
|
||||
.publish(
|
||||
channels::MSG_NOOSE,
|
||||
Message {
|
||||
source: channels::MSG_PIPELINE,
|
||||
content: Command::DbReqWriteEvent(event),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_deletions(&self, event: Box<Event>) -> Result<(), Error> {
|
||||
// if event.kind.as_u32() == 5 {
|
||||
// let events_for_deletion: Vec<String> = event
|
||||
// .tags
|
||||
// .iter()
|
||||
// .filter_map(|tag| match tag {
|
||||
// nostr::Tag::Event(event_id, _, _) => Some(event_id.to_string()),
|
||||
// _ => None,
|
||||
// })
|
||||
// .collect();
|
||||
|
||||
// self.pubsub
|
||||
// .publish(
|
||||
// channels::MSG_NOOSE,
|
||||
// Message {
|
||||
// source: channels::MSG_PIPELINE,
|
||||
// content: Command::DbReqDeleteEvents(events_for_deletion),
|
||||
// },
|
||||
// )
|
||||
// .await;
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn track_hashtags(&self, event: Box<Event>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_media(&self, event: Box<Event>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stream_out(&self, event: Box<Event>) -> Result<(), Error> {
|
||||
let message = Message {
|
||||
source: channels::MSG_PIPELINE,
|
||||
content: Command::PipelineResStreamOutEvent(event),
|
||||
};
|
||||
self.pubsub.publish(channels::MSG_RELAY, message).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn broadcast(&self, event: Box<Event>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
234
src/noose/sled.rs
Normal file
|
@ -0,0 +1,234 @@
|
|||
use super::db::Noose;
|
||||
use crate::bussy::{channels, Command, Message, PubSub};
|
||||
use crate::utils::error::Error;
|
||||
use crate::utils::structs::Subscription;
|
||||
use async_trait::async_trait;
|
||||
use nostr::Event;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::user::{User, UserRow};
|
||||
|
||||
// Db Interface
|
||||
pub struct SledDb {
|
||||
db: sled::Db,
|
||||
events: sled::Tree,
|
||||
nip05s: sled::Tree,
|
||||
pub users: sled::Tree,
|
||||
|
||||
index: sled::Db,
|
||||
}
|
||||
|
||||
impl SledDb {
|
||||
pub fn new() -> Self {
|
||||
let db = sled::open("/tmp/sled_db").unwrap();
|
||||
let events = db.open_tree("events").unwrap();
|
||||
let nip05s = db.open_tree("identifiers").unwrap();
|
||||
let accounts = db.open_tree("accounts").unwrap();
|
||||
|
||||
let index = sled::open("/tmp/sled_index").unwrap();
|
||||
|
||||
Self {
|
||||
db,
|
||||
events,
|
||||
nip05s,
|
||||
users: accounts,
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_db(&self) -> Result<(), sled::Error> {
|
||||
self.db.clear()
|
||||
}
|
||||
|
||||
fn clear_index(&self) -> Result<(), sled::Error> {
|
||||
self.index.clear()
|
||||
}
|
||||
|
||||
async fn insert_user(&self, user: UserRow) -> Result<(), Error> {
|
||||
let pubkey = user.pubkey.clone();
|
||||
let username = user.username.clone();
|
||||
|
||||
if let Ok(Some(_)) = self.nip05s.get(&username) {
|
||||
return Err(Error::internal_with_message("User already exists"));
|
||||
}
|
||||
|
||||
let mut user_buff = flexbuffers::FlexbufferSerializer::new();
|
||||
user.serialize(&mut user_buff).unwrap();
|
||||
|
||||
self.nip05s.insert(&username, user_buff.view()).unwrap();
|
||||
|
||||
let prefix = "nip05:";
|
||||
let key = format!("{}{}", prefix, pubkey);
|
||||
self.index.insert(key, username.as_bytes()).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user(&self, user: User) -> Result<UserRow, Error> {
|
||||
let mut user_row = None;
|
||||
if let Some(username) = user.name {
|
||||
if let Ok(Some(buff)) = self.nip05s.get(username) {
|
||||
let b = flexbuffers::from_slice::<UserRow>(&buff).unwrap();
|
||||
user_row = Some(b);
|
||||
}
|
||||
} else if let Some(pubkey) = user.pubkey {
|
||||
let prefix = "nip05:";
|
||||
let reference = format!("{}{}", prefix, pubkey);
|
||||
if let Ok(Some(row)) = self.index.get(reference) {
|
||||
let key = String::from_utf8(row.to_vec()).unwrap();
|
||||
|
||||
if let Ok(Some(buff)) = self.nip05s.get(key) {
|
||||
let b = flexbuffers::from_slice::<UserRow>(&buff).unwrap();
|
||||
|
||||
user_row = Some(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
match user_row {
|
||||
Some(user) => Ok(user),
|
||||
None => Err(Error::internal_with_message("User not found")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Noose for SledDb {
|
||||
async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error> {
|
||||
let mut subscriber = pubsub.subscribe(channels::MSG_NOOSE).await;
|
||||
|
||||
while let Ok(message) = subscriber.recv().await {
|
||||
log::info!("noose subscriber received: {:?}", message);
|
||||
let command = match message.content {
|
||||
Command::DbReqInsertUser(user) => match self.insert_user(user).await {
|
||||
Ok(_) => Command::DbResOk,
|
||||
Err(e) => Command::ServiceError(e),
|
||||
},
|
||||
Command::DbReqGetUser(user) => match self.get_user(user).await {
|
||||
Ok(user) => Command::DbResUser(user),
|
||||
Err(e) => Command::ServiceError(e),
|
||||
},
|
||||
Command::DbReqWriteEvent(event) => match self.write_event(event).await {
|
||||
Ok(_) => Command::DbResOk,
|
||||
Err(e) => Command::ServiceError(e),
|
||||
},
|
||||
_ => Command::Noop,
|
||||
};
|
||||
if command != Command::Noop {
|
||||
log::info!("Publishing new message");
|
||||
let channel = message.source;
|
||||
|
||||
pubsub
|
||||
.publish(
|
||||
channel,
|
||||
Message {
|
||||
source: channels::MSG_NOOSE,
|
||||
content: command,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn migration_up(&self) {}
|
||||
|
||||
async fn write_event(&self, event: Box<Event>) -> Result<String, Error> {
|
||||
let mut event_buff = flexbuffers::FlexbufferSerializer::new();
|
||||
event.serialize(&mut event_buff).unwrap();
|
||||
|
||||
self.events.insert(event.id, event_buff.view()).unwrap();
|
||||
{
|
||||
// Timestamp
|
||||
let key = format!("created_at:{}|#e:{}", event.created_at, event.id);
|
||||
self.index.insert(key, event.id.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
// Author, pubkeys #p
|
||||
let key = format!("#author:{}|#e:{}", event.pubkey, event.id);
|
||||
self.index.insert(key, event.id.as_bytes()).unwrap();
|
||||
// self.index.scan_prefix(
|
||||
}
|
||||
|
||||
{
|
||||
// Kinds
|
||||
let key = format!("#k:{}|#e:{}", event.kind, event.id);
|
||||
self.index.insert(key, event.id.as_bytes()).unwrap();
|
||||
// self.index.scan_prefix(
|
||||
}
|
||||
|
||||
{
|
||||
// Tags
|
||||
event.tags.iter().for_each(|tag| {
|
||||
if let Some(key) = match tag {
|
||||
// #e tag
|
||||
nostr::Tag::Event(event_id, _, _) => Some(format!("#e:{}", event_id)),
|
||||
// #p tag
|
||||
nostr::Tag::PubKey(pubkey, _) => Some(format!("#p:{}|#e:{}", pubkey, event.id)),
|
||||
// #t tag
|
||||
nostr::Tag::Hashtag(hashtag) => Some(format!("#t:{}|#e:{}", hashtag, event.id)),
|
||||
// #a tag
|
||||
nostr::Tag::A {
|
||||
kind,
|
||||
public_key,
|
||||
identifier,
|
||||
relay_url,
|
||||
} => Some(format!(
|
||||
"#a:kind:{}|#a:pubkey:{}#a:identifier:{}|#e:{}",
|
||||
kind, public_key, identifier, event.id
|
||||
)),
|
||||
_ => None,
|
||||
} {
|
||||
self.index.insert(key, event.id.as_bytes()).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// let key = format!("#t:{}|#e:{}", event.kind, event.id);
|
||||
// self.index.insert(key, event.id.as_bytes()).unwrap();
|
||||
// self.index.scan_prefix(
|
||||
}
|
||||
|
||||
let message = format!("[\"OK\", \"{}\", true, \"\"]", event.id.to_string());
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
async fn find_event(&self, subscription: Subscription) -> Result<Vec<String>, Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SledDb;
|
||||
use crate::{
|
||||
bussy::PubSub,
|
||||
noose::user::{User, UserRow},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_db_names() {
|
||||
let pubsub = Arc::new(PubSub::new());
|
||||
|
||||
let db = SledDb::new();
|
||||
|
||||
let pk = "npub1p3ya99jfdafnqlk87p6wfd36d2nme5mkld769rhd9pkht6hmqlaq6mzxdu".to_string();
|
||||
let username = "klink".to_string();
|
||||
let user = UserRow::new(pk, username, false);
|
||||
let result = db.insert_user(user).await;
|
||||
|
||||
let pubkey = "npub1p3ya99jfdafnqlk87p6wfd36d2nme5mkld769rhd9pkht6hmqlaq6mzxdu".to_string();
|
||||
let username = "klink".to_string();
|
||||
let user = User {
|
||||
name: None,
|
||||
pubkey: Some(pubkey),
|
||||
};
|
||||
let user = db.get_user(user).await;
|
||||
|
||||
db.clear_db().unwrap();
|
||||
db.clear_index().unwrap();
|
||||
}
|
||||
}
|
889
src/noose/sqlite.rs
Normal file
|
@ -0,0 +1,889 @@
|
|||
use async_trait::async_trait;
|
||||
use nostr::{Event, JsonUtil};
|
||||
use sea_query::{extension::sqlite::SqliteExpr, Query};
|
||||
use sea_query_binder::SqlxBinder;
|
||||
use sqlx::sqlite::{Sqlite, SqlitePoolOptions};
|
||||
use sqlx::FromRow;
|
||||
use sqlx::{migrate::MigrateDatabase, Pool};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::db::Noose;
|
||||
use crate::bussy::{channels, Command, Message, PubSub};
|
||||
use crate::utils::{error::Error, structs::Subscription};
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow, Debug)]
|
||||
struct EventsCountRow(i32);
|
||||
|
||||
#[derive(FromRow, Debug)]
|
||||
struct EventRow {
|
||||
id: String,
|
||||
pubkey: String,
|
||||
created_at: i64,
|
||||
kind: i64,
|
||||
tags: String,
|
||||
sig: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl EventRow {
|
||||
pub fn to_string(&self, subscription_id: nostr::SubscriptionId) -> String {
|
||||
let tags: Vec<Vec<String>> = serde_json::from_str(&self.tags).unwrap();
|
||||
|
||||
let message = serde_json::json!([
|
||||
"EVENT",
|
||||
subscription_id,
|
||||
{
|
||||
"id": self.id,
|
||||
"content": self.content,
|
||||
"created_at": self.created_at,
|
||||
"kind": self.kind,
|
||||
"pubkey": self.pubkey,
|
||||
"sig": self.sig,
|
||||
"tags": tags
|
||||
}
|
||||
]);
|
||||
|
||||
message.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SqliteDb {
|
||||
pool: Pool<Sqlite>,
|
||||
}
|
||||
|
||||
impl SqliteDb {
|
||||
pub async fn new() -> Self {
|
||||
let pool = SqliteDb::build_pool("noose_pool", 42).await;
|
||||
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub fn info(&self) {
|
||||
dbg!(self.pool.options());
|
||||
}
|
||||
|
||||
async fn migrate(pool: &Pool<Sqlite>) {
|
||||
sqlx::migrate!("src/noose/migrations")
|
||||
.run(pool)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn build_pool(name: &str, max_size: u32) -> Pool<Sqlite> {
|
||||
let pool_options = SqlitePoolOptions::new()
|
||||
.test_before_acquire(true)
|
||||
// .idle_timeout(Some(Duration::from_secs(10)))
|
||||
// .max_lifetime(Some(Duration::from_secs(30)))
|
||||
.max_lifetime(None)
|
||||
.idle_timeout(None)
|
||||
.max_connections(max_size);
|
||||
|
||||
let db_url = "sqlite://sqlite.db";
|
||||
if !Sqlite::database_exists(db_url).await.unwrap_or(false) {
|
||||
log::info!("Creating database {}", db_url);
|
||||
match Sqlite::create_database(db_url).await {
|
||||
Ok(_) => log::info!("Db {} created", db_url),
|
||||
Err(_) => panic!("Failed to create database {}", db_url),
|
||||
}
|
||||
}
|
||||
if let Ok(pool) = pool_options.connect(db_url).await {
|
||||
log::info!("Connected to sqlite pool {}", name);
|
||||
|
||||
pool
|
||||
} else {
|
||||
panic!("Connection to sqlite pool {} failed", name);
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_event(&self, event: Box<Event>) -> Result<String, Error> {
|
||||
let id = event.id.to_string();
|
||||
let kind = event.kind.to_string();
|
||||
let pubkey = event.pubkey.to_string();
|
||||
let content = event.content.to_string();
|
||||
let created_at = event.created_at.as_i64();
|
||||
let tags = serde_json::to_string(&event.tags).unwrap();
|
||||
let sig = event.sig.to_string();
|
||||
|
||||
let message = format!("[\"OK\", \"{}\", true, \"\"]", id.clone());
|
||||
|
||||
if event.is_ephemeral() {
|
||||
return Ok(message);
|
||||
}
|
||||
|
||||
let tx = self.pool.begin().await.unwrap();
|
||||
{
|
||||
if event.is_replaceable() {
|
||||
dbg!("new event is replaceable - searching for previously stored event");
|
||||
let (sql, values) = Query::select()
|
||||
.from(EventsTable::Table)
|
||||
.columns([EventsTable::EventId])
|
||||
.and_where(
|
||||
sea_query::Expr::col(EventsTable::Pubkey).eq(event.pubkey.to_string()),
|
||||
)
|
||||
.and_where(sea_query::Expr::col(EventsTable::Kind).eq(event.kind.as_u32()))
|
||||
.and_where(
|
||||
sea_query::Expr::col(EventsTable::CreatedAt).gte(event.created_at.as_i64()),
|
||||
)
|
||||
.limit(1)
|
||||
.build_sqlx(sea_query::SqliteQueryBuilder);
|
||||
|
||||
let repl_count = sqlx::query_with(&sql, values).fetch_one(&self.pool).await;
|
||||
|
||||
if repl_count.ok().is_some() {
|
||||
return Ok(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if event.is_parameterized_replaceable() {
|
||||
dbg!(
|
||||
"new event is parametrized replaceable - searching for previously stored event"
|
||||
);
|
||||
let d_tags: Vec<String> = event
|
||||
.tags
|
||||
.iter()
|
||||
.filter(|tag| tag.kind() == nostr::TagKind::D)
|
||||
.map(|tag| tag.clone().to_vec()[1].clone())
|
||||
.collect();
|
||||
let (sql, values) = Query::select()
|
||||
.from(EventsTable::Table)
|
||||
.column((EventsTable::Table, EventsTable::EventId))
|
||||
.left_join(
|
||||
TagsTable::Table,
|
||||
sea_query::Expr::col((TagsTable::Table, TagsTable::EventId))
|
||||
.equals((EventsTable::Table, EventsTable::EventId)),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::Pubkey))
|
||||
.eq(event.pubkey.to_string()),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::Kind))
|
||||
.eq(event.kind.as_u32()),
|
||||
)
|
||||
.and_where(sea_query::Expr::col((TagsTable::Table, TagsTable::Tag)).eq("d"))
|
||||
.and_where(
|
||||
sea_query::Expr::col((TagsTable::Table, TagsTable::Value))
|
||||
.eq(d_tags[0].to_string()),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::CreatedAt))
|
||||
.gte(event.created_at.as_i64()),
|
||||
)
|
||||
.limit(1)
|
||||
.build_sqlx(sea_query::SqliteQueryBuilder);
|
||||
|
||||
let repl_count = sqlx::query_with(&sql, values).fetch_one(&self.pool).await;
|
||||
|
||||
if repl_count.ok().is_some() {
|
||||
return Ok(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert replaceble event
|
||||
{
|
||||
if event.is_replaceable() {
|
||||
dbg!("deleting older replaceable event from events table");
|
||||
let (sql, values) = Query::delete()
|
||||
.from_table(EventsTable::Table)
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::Kind))
|
||||
.eq(event.kind.as_u32()),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::Pubkey))
|
||||
.eq(event.pubkey.to_string()),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::EventId))
|
||||
.not_in_subquery(
|
||||
Query::select()
|
||||
.from(EventsTable::Table)
|
||||
.column(EventsTable::EventId)
|
||||
.and_where(
|
||||
sea_query::Expr::col(EventsTable::Kind)
|
||||
.eq(event.kind.as_u32()),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col(EventsTable::Pubkey)
|
||||
.eq(event.pubkey.to_string()),
|
||||
)
|
||||
.order_by(EventsTable::CreatedAt, sea_query::Order::Desc)
|
||||
.limit(1)
|
||||
.to_owned(),
|
||||
),
|
||||
)
|
||||
.build_sqlx(sea_query::SqliteQueryBuilder);
|
||||
|
||||
let results = sqlx::query_with(&sql, values)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if results.rows_affected() > 0 {
|
||||
log::info!(
|
||||
"removed {} older replaceable kind {} events for author: {:?}",
|
||||
results.rows_affected(),
|
||||
event.kind.as_u32(),
|
||||
event.pubkey.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert parametrized replaceble event
|
||||
{
|
||||
if event.is_parameterized_replaceable() {
|
||||
dbg!("deleting older parametrized replaceable event from events table");
|
||||
let d_tag = event.identifier();
|
||||
let (sql, values) = Query::delete()
|
||||
.from_table(EventsTable::Table)
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::EventId))
|
||||
.in_subquery(
|
||||
Query::select()
|
||||
.from(EventsTable::Table)
|
||||
.column((EventsTable::Table, EventsTable::EventId))
|
||||
.left_join(
|
||||
TagsTable::Table,
|
||||
sea_query::Expr::col((
|
||||
TagsTable::Table,
|
||||
TagsTable::EventId,
|
||||
))
|
||||
.equals((EventsTable::Table, EventsTable::EventId)),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col((
|
||||
EventsTable::Table,
|
||||
EventsTable::Kind,
|
||||
))
|
||||
.eq(event.kind.as_u32()),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col((
|
||||
EventsTable::Table,
|
||||
EventsTable::Pubkey,
|
||||
))
|
||||
.eq(event.pubkey.to_string()),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col((TagsTable::Table, TagsTable::Tag))
|
||||
.eq("d"),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col((TagsTable::Table, TagsTable::Value))
|
||||
.eq(d_tag),
|
||||
)
|
||||
.to_owned(),
|
||||
),
|
||||
)
|
||||
.build_sqlx(sea_query::SqliteQueryBuilder);
|
||||
|
||||
let results = sqlx::query_with(&sql, values)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if results.rows_affected() > 0 {
|
||||
log::info!("removed {} older parameterized replaceable kind {} events for author: {:?}", results.rows_affected(), event.kind, event.pubkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process deletion events
|
||||
dbg!(event.as_json());
|
||||
if event.kind.as_u32() == 5 {
|
||||
dbg!("deleting event");
|
||||
let ids: Vec<String> = event.event_ids().map(|eid| eid.to_string()).collect();
|
||||
let (sql, values) = Query::delete()
|
||||
.from_table(EventsTable::Table)
|
||||
.and_where(sea_query::Expr::col(EventsTable::Kind).ne(5))
|
||||
.and_where(sea_query::Expr::col(EventsTable::Pubkey).eq(event.pubkey.to_string()))
|
||||
.and_where(sea_query::Expr::col(EventsTable::EventId).is_in(&ids))
|
||||
.build_sqlx(sea_query::SqliteQueryBuilder);
|
||||
|
||||
let results = sqlx::query_with(&sql, values)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if results.rows_affected() > 0 {
|
||||
log::info!(
|
||||
"removed {} events for author {:?}",
|
||||
results.rows_affected(),
|
||||
event.pubkey
|
||||
);
|
||||
}
|
||||
|
||||
// Delete from EventsFTS
|
||||
let (sql, values) = Query::delete()
|
||||
.from_table(EventsFTSTable::Table)
|
||||
.and_where(sea_query::Expr::col(EventsFTSTable::EventId).is_in(&ids))
|
||||
.build_sqlx(sea_query::SqliteQueryBuilder);
|
||||
let _ = sqlx::query_with(&sql, values)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
dbg!("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_sqlx(sea_query::SqliteQueryBuilder);
|
||||
|
||||
let results = sqlx::query_with(&sql, values)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert into EventsFTS table
|
||||
dbg!("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_sqlx(sea_query::SqliteQueryBuilder);
|
||||
|
||||
let results = sqlx::query_with(&sql, values)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert into Tags table
|
||||
dbg!("inserting new event into tags");
|
||||
for tag in event.tags.clone() {
|
||||
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_sqlx(sea_query::SqliteQueryBuilder);
|
||||
|
||||
let results = sqlx::query_with(&sql, values)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
async fn index_search(&self, event: Box<Event>) -> Result<(), Error> {
|
||||
let id = event.id.to_string();
|
||||
let content = event.content.to_string();
|
||||
let (sql, values) = Query::insert()
|
||||
.into_table(EventsFTSTable::Table)
|
||||
.columns([EventsFTSTable::EventId, EventsFTSTable::Content])
|
||||
.values_panic([id.into(), content.into()])
|
||||
.build_sqlx(sea_query::SqliteQueryBuilder);
|
||||
|
||||
let results = sqlx::query_with(&sql, values).execute(&self.pool).await;
|
||||
if results.is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::internal_with_message(
|
||||
"Unable to write event to events_fts index",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn index_tags(&self, event: Box<Event>) -> Result<(), Error> {
|
||||
// let t: Vec<String> = Vec::new();
|
||||
// for tag in event.tags {
|
||||
// tag.kind()
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_filter_query(&self, filter: &nostr::Filter) -> sea_query::SelectStatement {
|
||||
let mut query = Query::select()
|
||||
.column((EventsTable::Table, EventsTable::EventId))
|
||||
.column((EventsTable::Table, EventsTable::Content))
|
||||
.columns([
|
||||
EventsTable::Kind,
|
||||
EventsTable::Pubkey,
|
||||
EventsTable::CreatedAt,
|
||||
EventsTable::Tags,
|
||||
EventsTable::Sig,
|
||||
])
|
||||
.from(EventsTable::Table)
|
||||
.order_by(EventsTable::CreatedAt, sea_query::Order::Desc)
|
||||
.to_owned();
|
||||
|
||||
if !filter.ids.is_empty() {
|
||||
let ids = filter.ids.iter().map(|id| id.to_string());
|
||||
query = query
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::EventId)).is_in(ids),
|
||||
)
|
||||
.to_owned();
|
||||
}
|
||||
|
||||
if !filter.kinds.is_empty() {
|
||||
let kinds: Vec<u32> = filter.kinds.iter().map(|kind| kind.as_u32()).collect();
|
||||
query = query
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::Kind)).is_in(kinds),
|
||||
)
|
||||
.to_owned();
|
||||
}
|
||||
|
||||
if !filter.authors.is_empty() {
|
||||
let authors = filter.authors.iter().map(|author| author.to_string());
|
||||
query = query
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::Pubkey)).is_in(authors),
|
||||
)
|
||||
.to_owned();
|
||||
}
|
||||
|
||||
if let Some(since) = filter.since {
|
||||
query = query
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::CreatedAt))
|
||||
.gte(since.as_u64()),
|
||||
)
|
||||
.to_owned();
|
||||
}
|
||||
|
||||
if let Some(until) = filter.until {
|
||||
query = query
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::CreatedAt))
|
||||
.lte(until.as_u64()),
|
||||
)
|
||||
.to_owned();
|
||||
}
|
||||
|
||||
if let Some(limit) = filter.limit {
|
||||
query = query.limit(limit as u64).to_owned();
|
||||
}
|
||||
|
||||
filter.generic_tags.iter().for_each(|(tag, values)| {
|
||||
let values = values.iter().map(|val| val.to_string());
|
||||
query = query
|
||||
.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(tag.to_string()),
|
||||
)
|
||||
.and_where(sea_query::Expr::col((TagsTable::Table, TagsTable::Value)).is_in(values))
|
||||
.to_owned();
|
||||
});
|
||||
|
||||
if let Some(search) = &filter.search {
|
||||
query = query
|
||||
.inner_join(
|
||||
EventsFTSTable::Table,
|
||||
sea_query::Expr::col((EventsTable::Table, EventsTable::EventId))
|
||||
.equals((EventsFTSTable::Table, EventsFTSTable::EventId)),
|
||||
)
|
||||
.and_where(
|
||||
sea_query::Expr::col((EventsFTSTable::Table, EventsFTSTable::Content))
|
||||
.matches(search),
|
||||
)
|
||||
.to_owned();
|
||||
}
|
||||
|
||||
query
|
||||
}
|
||||
|
||||
fn get_filters_query(&self, subscription: Subscription) -> Option<sea_query::SelectStatement> {
|
||||
subscription
|
||||
.filters
|
||||
.iter()
|
||||
.map(|filter| {
|
||||
Query::select()
|
||||
.column((EventsTable::Table, EventsTable::EventId))
|
||||
.column((EventsTable::Table, EventsTable::Content))
|
||||
.columns([
|
||||
EventsTable::Kind,
|
||||
EventsTable::Pubkey,
|
||||
EventsTable::CreatedAt,
|
||||
EventsTable::Tags,
|
||||
EventsTable::Sig,
|
||||
])
|
||||
.from_subquery(
|
||||
self.get_filter_query(filter),
|
||||
sea_query::Alias::new("events"),
|
||||
)
|
||||
.to_owned()
|
||||
})
|
||||
.reduce(|mut result, query| result.union(sea_query::UnionType::All, query).to_owned())
|
||||
}
|
||||
|
||||
async fn delete_filters(&self, subscription: Subscription) -> Vec<EventRow> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn count_events_by_filters(&self, subscription: Subscription) -> i32 {
|
||||
if subscription.filters.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let (sql, values) = self
|
||||
.get_filters_query(subscription)
|
||||
.unwrap()
|
||||
.clear_selects()
|
||||
.expr_as(
|
||||
sea_query::Func::count(sea_query::Expr::col((
|
||||
EventsTable::Table,
|
||||
EventsTable::EventId,
|
||||
))),
|
||||
sea_query::Alias::new("count"),
|
||||
)
|
||||
.build_sqlx(sea_query::SqliteQueryBuilder);
|
||||
|
||||
println!("count_filters SEA_QUERY built SQL: {}", sql.clone());
|
||||
|
||||
let counts = sqlx::query_as_with::<_, EventsCountRow, _>(&sql, values)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
dbg!(counts);
|
||||
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Noose for SqliteDb {
|
||||
async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error> {
|
||||
let mut subscriber = pubsub.subscribe(channels::MSG_NOOSE).await;
|
||||
|
||||
while let Ok(message) = subscriber.recv().await {
|
||||
log::info!("[Noose] received message: {:?}", message);
|
||||
let command = match message.content {
|
||||
Command::DbReqWriteEvent(event) => match self.write_event(event).await {
|
||||
Ok(status) => Command::DbResOkWithStatus(status),
|
||||
Err(e) => Command::ServiceError(e),
|
||||
},
|
||||
Command::DbReqFindEvent(client_id, subscriptioin) => {
|
||||
match self.find_event(subscriptioin).await {
|
||||
Ok(events) => Command::DbResRelayMessage(client_id, events),
|
||||
Err(e) => Command::ServiceError(e),
|
||||
}
|
||||
}
|
||||
_ => Command::Noop,
|
||||
};
|
||||
if command != Command::Noop {
|
||||
let channel = message.source;
|
||||
let message = Message {
|
||||
source: channels::MSG_NOOSE,
|
||||
content: command,
|
||||
};
|
||||
|
||||
log::debug!("[Noose] publishing new message: {:?}", message);
|
||||
|
||||
pubsub.publish(channel, message).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn migration_up(&self) {
|
||||
SqliteDb::migrate(&self.pool).await;
|
||||
}
|
||||
|
||||
async fn write_event(&self, event: Box<Event>) -> Result<String, Error> {
|
||||
log::debug!("[Noose] write_event triggered");
|
||||
|
||||
let status = self.add_event(event).await.unwrap();
|
||||
|
||||
return Ok(status);
|
||||
}
|
||||
|
||||
async fn find_event(&self, subscription: Subscription) -> Result<Vec<String>, Error> {
|
||||
log::debug!("making query from filters...");
|
||||
|
||||
let eose_message =
|
||||
vec![nostr::RelayMessage::EndOfStoredEvents(subscription.id.clone()).as_json()];
|
||||
|
||||
if let Some(sql_statement) = self.get_filters_query(subscription.clone()) {
|
||||
let (sql, values) = sql_statement.build_sqlx(sea_query::SqliteQueryBuilder);
|
||||
log::info!("SEA_QUERY built SQL: {}", sql.clone());
|
||||
|
||||
match sqlx::query_as_with::<_, EventRow, _>(&sql, values)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => {
|
||||
if rows.is_empty() {
|
||||
return Ok(eose_message);
|
||||
} else {
|
||||
let relay_messages: Vec<String> = rows
|
||||
.iter()
|
||||
.map(|row| row.to_string(subscription.id.clone()))
|
||||
.collect();
|
||||
return Ok(relay_messages);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
return Err(Error::internal(e.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(eose_message);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Noose;
|
||||
use super::SqliteDb;
|
||||
use crate::utils::structs::Subscription;
|
||||
|
||||
use nostr::util::JsonUtil;
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_event() {
|
||||
let db = SqliteDb::new().await;
|
||||
|
||||
let t = std::time::Instant::now();
|
||||
let client_id = "test_id".to_string();
|
||||
|
||||
let cm = nostr::ClientMessage::from_json(
|
||||
r#"["REQ","7b9bc4b6-701c-40b6-898f-4e7c6b5b1510",{"authors":["04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"],"kinds":[0]}]"#,
|
||||
).unwrap();
|
||||
let (sub_id, filters) = match cm {
|
||||
nostr::ClientMessage::Req {
|
||||
subscription_id,
|
||||
filters,
|
||||
} => (subscription_id, filters),
|
||||
_ => panic!("sneed :("),
|
||||
};
|
||||
let sub = Subscription::new(sub_id, filters);
|
||||
db.find_event(sub).await.unwrap();
|
||||
println!(
|
||||
"Time passed: {}",
|
||||
(std::time::Instant::now() - t).as_millis()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_events() {
|
||||
let db = SqliteDb::new().await;
|
||||
|
||||
let t = std::time::Instant::now();
|
||||
let client_id = "test_id".to_string();
|
||||
|
||||
let my_keys = nostr::Keys::generate();
|
||||
|
||||
let eid = nostr::EventId::all_zeros();
|
||||
let tag_event = nostr::Tag::Event {
|
||||
event_id: eid,
|
||||
relay_url: None,
|
||||
marker: None,
|
||||
};
|
||||
let tag_url = nostr::Tag::AbsoluteURL(nostr::types::UncheckedUrl::new(
|
||||
"http://foo.net".to_string(),
|
||||
));
|
||||
let tag_hashtag = nostr::Tag::Hashtag("farm".to_string());
|
||||
|
||||
let event = nostr::EventBuilder::new_text_note(
|
||||
"sneed feed and seed",
|
||||
vec![tag_event, tag_url, tag_hashtag],
|
||||
)
|
||||
.to_event(&my_keys)
|
||||
.unwrap();
|
||||
|
||||
dbg!(&event.as_json());
|
||||
let resp = db.add_event(Box::new(event.clone())).await.unwrap();
|
||||
dbg!(resp);
|
||||
|
||||
let delete_event = nostr::EventBuilder::delete(vec![event.id])
|
||||
.to_event(&my_keys)
|
||||
.unwrap();
|
||||
|
||||
dbg!(&delete_event);
|
||||
let resp = db.add_event(Box::new(delete_event.clone())).await.unwrap();
|
||||
dbg!(resp);
|
||||
|
||||
// let sub_id = nostr::SubscriptionId::new("test".to_string());
|
||||
// let mut subscription = Subscription::new(sub_id, vec![]);
|
||||
|
||||
// if delete_event.kind == nostr::Kind::EventDeletion {
|
||||
// delete_event
|
||||
// .tags
|
||||
// .iter()
|
||||
// .filter(|tag| {
|
||||
// matches!(
|
||||
// tag,
|
||||
// nostr::Tag::Event {
|
||||
// event_id,
|
||||
// relay_url,
|
||||
// marker,
|
||||
// }
|
||||
// )
|
||||
// })
|
||||
// .for_each(|tag| {
|
||||
// if let nostr::Tag::Event {
|
||||
// event_id,
|
||||
// relay_url,
|
||||
// marker,
|
||||
// } = tag
|
||||
// {
|
||||
// let filter = nostr::Filter::new();
|
||||
// let filter = &filter.event(*event_id);
|
||||
|
||||
// subscription.filters.push(filter.clone());
|
||||
// }
|
||||
// });
|
||||
|
||||
// dbg!(&subscription);
|
||||
// }
|
||||
|
||||
// let res = db.delete_filters(subscription).await;
|
||||
|
||||
// dbg!(res);
|
||||
|
||||
// let sub = Subscription::new(sub_id, filters);
|
||||
// let num = db.delete_filters(sub).await.len();
|
||||
// println!(
|
||||
// "Time passed: {}",
|
||||
// (std::time::Instant::now() - t).as_millis()
|
||||
// );
|
||||
|
||||
// assert_eq!(num, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn count_events() {
|
||||
let db = SqliteDb::new().await;
|
||||
|
||||
let t = std::time::Instant::now();
|
||||
let client_id = "test_id".to_string();
|
||||
|
||||
let cm = nostr::ClientMessage::from_json(
|
||||
r#"["COUNT","7b9bc4b6-701c-40b6-898f-4e7c6b5b1510",{"authors":["6be3c1446231fe6d117d72e29b60094bbb3eec029100c34f627dc4ebe8369a64"],"kinds":[1]}]"#,
|
||||
).unwrap();
|
||||
let (sub_id, filters) = match cm {
|
||||
nostr::ClientMessage::Count {
|
||||
subscription_id,
|
||||
filters,
|
||||
} => (subscription_id, filters),
|
||||
_ => panic!("sneed :("),
|
||||
};
|
||||
|
||||
let sub = Subscription::new(sub_id, filters);
|
||||
let num = db.count_events_by_filters(sub).await;
|
||||
println!(
|
||||
"Time passed: {}",
|
||||
(std::time::Instant::now() - t).as_millis()
|
||||
);
|
||||
|
||||
assert_eq!(num, 1);
|
||||
}
|
||||
}
|
43
src/noose/user.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use chrono::Utc;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
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 struct UserRow {
|
||||
pub pubkey: String,
|
||||
pub username: String,
|
||||
inserted_at: i64,
|
||||
admin: bool,
|
||||
}
|
||||
|
||||
impl UserRow {
|
||||
pub fn new(pubkey: String, username: String, admin: bool) -> Self {
|
||||
Self {
|
||||
pubkey,
|
||||
username,
|
||||
inserted_at: Utc::now().timestamp(),
|
||||
admin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Validate)]
|
||||
pub struct User {
|
||||
#[validate(custom = "validate_pubkey")]
|
||||
pub pubkey: Option<String>,
|
||||
#[validate(length(min = 1), regex = "VALID_CHARACTERS")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
pub fn validate_pubkey(value: &str) -> Result<(), ValidationError> {
|
||||
use nostr::prelude::FromPkStr;
|
||||
match nostr::Keys::from_pk_str(value) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(ValidationError::new("Unable to parse pubkey")),
|
||||
}
|
||||
}
|
12
src/relay/handler.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use crate::relay::ws;
|
||||
use crate::utils::structs::Context;
|
||||
use std::net::SocketAddr;
|
||||
use warp::{Rejection, Reply};
|
||||
|
||||
pub async fn ws_handler(
|
||||
ws: warp::ws::Ws,
|
||||
context: Context,
|
||||
client_addr: Option<SocketAddr>,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
Ok(ws.on_upgrade(move |socket| ws::client_connection(socket, context, client_addr)))
|
||||
}
|
24
src/relay/mod.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
mod handler;
|
||||
mod routes;
|
||||
mod ws;
|
||||
|
||||
use crate::utils::rejection_handler::handle_rejection;
|
||||
use crate::utils::structs::Context;
|
||||
use tokio::runtime;
|
||||
use warp::Filter;
|
||||
|
||||
pub fn start(context: Context) {
|
||||
let rt = runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
log::info!("Starting Relay on wss://127.0.0.1:8080");
|
||||
|
||||
let routes = routes::routes(context).recover(handle_rejection);
|
||||
|
||||
warp::serve(routes)
|
||||
// .tls()
|
||||
// .cert(CERT)
|
||||
// .key(KEY)
|
||||
.run(([127, 0, 0, 1], 8080))
|
||||
.await;
|
||||
});
|
||||
}
|
31
src/relay/routes.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use super::handler;
|
||||
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();
|
||||
|
||||
static_files().or(index(context)).with(cors)
|
||||
}
|
||||
|
||||
fn index(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||
let client_addr = warp::addr::remote();
|
||||
|
||||
warp::path::end()
|
||||
.and(warp::ws())
|
||||
.and(with_context(context))
|
||||
.and(client_addr)
|
||||
.and_then(handler::ws_handler)
|
||||
}
|
||||
|
||||
fn static_files() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||
let mut foo = std::env::current_exe().unwrap();
|
||||
foo.pop();
|
||||
|
||||
let mut www = foo.clone();
|
||||
www.pop();
|
||||
www.push(std::path::Path::new("www/static"));
|
||||
|
||||
warp::get().and(warp::fs::dir(www))
|
||||
}
|
264
src/relay/ws.rs
Normal file
|
@ -0,0 +1,264 @@
|
|||
use crate::{
|
||||
bussy::channels,
|
||||
utils::structs::{Client, Context, Subscription},
|
||||
};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use nostr::{ClientMessage, Event, Filter, JsonUtil, SubscriptionId};
|
||||
use serde_json::from_str;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
use warp::ws::{Message, WebSocket};
|
||||
|
||||
use futures_util::SinkExt;
|
||||
|
||||
pub async fn client_connection(ws: WebSocket, context: Context, client_addr: Option<SocketAddr>) {
|
||||
let (mut ws_sender, mut ws_receiver) = ws.split();
|
||||
let (client_sender, client_receiver) = mpsc::unbounded_channel();
|
||||
|
||||
let mut client_receiver = UnboundedReceiverStream::new(client_receiver);
|
||||
|
||||
// Create and Add to the Context new Client and set its sender
|
||||
let ip = client_addr.unwrap().ip().to_string();
|
||||
let mut client = Client::new(ip);
|
||||
|
||||
client.client_connection = Some(client_sender);
|
||||
|
||||
let mut subscriber = context.pubsub.subscribe(channels::MSG_RELAY).await;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
Ok(message) = subscriber.recv() => {
|
||||
match message.content {
|
||||
crate::bussy::Command::PipelineResRelayMessageOk(client_id, relay_message) => {
|
||||
if client.client_id == client_id {
|
||||
if let Some(sender) = &client.client_connection {
|
||||
if !sender.is_closed() {
|
||||
log::info!("[Relay] sending back the status of the processed event: {}", relay_message.as_json());
|
||||
sender.send(Ok(Message::text(relay_message.as_json()))).unwrap();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
crate::bussy::Command::PipelineResStreamOutEvent(event) => {
|
||||
// if client.client_id == client_id {
|
||||
if let Some(sender) = &client.client_connection {
|
||||
if let Some(relay_message) = get_relay_message(&client, event) {
|
||||
log::info!("[Relay] sending processed event to subscribed client: {}", relay_message.as_json());
|
||||
if !sender.is_closed() {sender.send(Ok(Message::text(relay_message.as_json()))).unwrap()};
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
crate::bussy::Command::DbResRelayMessage(client_id, events) => {
|
||||
if client.client_id == client_id {
|
||||
if let Some(sender) = &client.client_connection {
|
||||
if !sender.is_closed() {
|
||||
for event in events {
|
||||
sender.send(Ok(Message::text(event))).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::bussy::Command::DbResOkWithStatus(status) => {
|
||||
if let Some(sender) = &client.client_connection {
|
||||
sender.send(Ok(Message::text(status))).unwrap();
|
||||
}
|
||||
},
|
||||
_ => ()
|
||||
}
|
||||
|
||||
},
|
||||
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) => {
|
||||
log::error!("websocket send error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("websocket send error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(result) = ws_receiver.next() => {
|
||||
let msg = match result {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"error receiving ws message for id: {}: {}",
|
||||
client.client_id.clone(),
|
||||
e
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
socket_on_message(&context, &mut client, msg).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle proper disconnects
|
||||
socket_on_close(&client).await;
|
||||
}
|
||||
|
||||
/// Checking if client with id needs the event
|
||||
fn get_relay_message(client: &Client, event: Box<Event>) -> Option<nostr::RelayMessage> {
|
||||
let mut id = &"".to_string();
|
||||
log::info!(
|
||||
"Checking if client with id {} needs the event",
|
||||
client.client_id
|
||||
);
|
||||
if client.subscriptions.iter().any(|(sub_id, sub)| {
|
||||
if sub.interested_in_event(&event) {
|
||||
id = sub_id;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}) {
|
||||
return Some(nostr::RelayMessage::Event {
|
||||
subscription_id: nostr::SubscriptionId::new(id),
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn socket_on_close(client: &Client) {
|
||||
// clients.write().await.remove(id);
|
||||
log::info!("{} disconnected", client.client_id);
|
||||
}
|
||||
|
||||
async fn socket_on_message(context: &Context, client: &mut Client, msg: Message) {
|
||||
let message = match msg.to_str() {
|
||||
Ok(raw_message) => raw_message,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if message == "ping" || message == "ping\n" {
|
||||
return;
|
||||
}
|
||||
|
||||
let client_message: ClientMessage = match from_str(message) {
|
||||
Ok(parsed_message) => parsed_message,
|
||||
Err(e) => {
|
||||
log::error!("error while parsing client message request: {}", e);
|
||||
|
||||
let response = nostr::RelayMessage::new_notice("Invalid message");
|
||||
let message = Message::text(response.as_json());
|
||||
send(client, message);
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(
|
||||
"[client {} - {}] message: {}",
|
||||
client.ip(),
|
||||
client.client_id,
|
||||
client_message.as_json()
|
||||
);
|
||||
handle_msg(context, client, client_message).await;
|
||||
}
|
||||
|
||||
fn send(client: &Client, message: Message) {
|
||||
if let Some(sender) = &client.client_connection {
|
||||
if !sender.is_closed() {
|
||||
sender.send(Ok(message)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::Req {
|
||||
subscription_id,
|
||||
filters,
|
||||
} => handle_req(context, client, subscription_id, filters).await,
|
||||
ClientMessage::Count {
|
||||
subscription_id,
|
||||
filters,
|
||||
} => handle_count(client, subscription_id, filters).await,
|
||||
ClientMessage::Close(subscription_id) => handle_close(client, subscription_id).await,
|
||||
ClientMessage::Auth(event) => handle_auth(client, event).await,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_event(context: &Context, client: &Client, event: Box<Event>) {
|
||||
log::debug!("handle_event is processing new event");
|
||||
|
||||
context
|
||||
.pubsub
|
||||
.publish(
|
||||
channels::MSG_PIPELINE,
|
||||
crate::bussy::Message {
|
||||
source: channels::MSG_RELAY,
|
||||
content: crate::bussy::Command::PipelineReqEvent(client.client_id, event),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn handle_req(
|
||||
context: &Context,
|
||||
client: &mut Client,
|
||||
subscription_id: SubscriptionId,
|
||||
filters: Vec<Filter>,
|
||||
) {
|
||||
let subscription = Subscription::new(subscription_id.clone(), filters);
|
||||
let needs_historical_events = subscription.needs_historical_events();
|
||||
|
||||
client.subscribe(subscription.clone()).unwrap();
|
||||
|
||||
if needs_historical_events {
|
||||
context
|
||||
.pubsub
|
||||
.publish(
|
||||
channels::MSG_NOOSE,
|
||||
crate::bussy::Message {
|
||||
source: channels::MSG_RELAY,
|
||||
content: crate::bussy::Command::DbReqFindEvent(client.client_id, subscription),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_count(client: &Client, subscription_id: SubscriptionId, filters: Vec<Filter>) {
|
||||
// context.pubsub.send(new nostr event) then handle possible errors
|
||||
let subscription = Subscription::new(subscription_id, filters);
|
||||
|
||||
let message = Message::text("COUNT not implemented");
|
||||
send(client, message);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
62
src/usernames/accounts.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use crate::noose::user::{User, UserRow};
|
||||
|
||||
use crate::bussy::{channels, Command, Message};
|
||||
use crate::usernames::util::InvalidParameter;
|
||||
use crate::utils::error::Error;
|
||||
|
||||
use warp::{Rejection, Reply};
|
||||
|
||||
use super::Context;
|
||||
|
||||
pub async fn create_account(user: User, context: Context) -> Result<impl Reply, Rejection> {
|
||||
let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
|
||||
|
||||
let pubkey = match user.pubkey {
|
||||
Some(pk) => {
|
||||
use nostr::prelude::FromPkStr;
|
||||
let keys = nostr::Keys::from_pk_str(&pk).unwrap();
|
||||
keys.public_key().to_string()
|
||||
}
|
||||
None => {
|
||||
return Err(warp::reject::custom(Error::bad_request(
|
||||
"Pubkey is required",
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let name = match user.name {
|
||||
Some(n) => n,
|
||||
None => return Err(warp::reject::custom(Error::bad_request("Name is required"))),
|
||||
};
|
||||
|
||||
let user_row = UserRow::new(pubkey, name, false);
|
||||
|
||||
let command = Command::DbReqInsertUser(user_row);
|
||||
context
|
||||
.pubsub
|
||||
.publish(
|
||||
channels::MSG_NOOSE,
|
||||
Message {
|
||||
source: channels::MSG_NIP05,
|
||||
content: command,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(result) = subscriber.recv().await {
|
||||
match result.content {
|
||||
Command::DbResOk => Ok(warp::reply::with_status(
|
||||
"ACCOUNT CREATED",
|
||||
warp::http::StatusCode::CREATED,
|
||||
)),
|
||||
Command::ServiceError(e) => Err(warp::reject::custom(e)),
|
||||
// _ => Err(warp::reject::custom(InvalidParameter)),
|
||||
_ => Ok(warp::reply::with_status(
|
||||
"something else",
|
||||
warp::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(warp::reject::custom(InvalidParameter))
|
||||
}
|
||||
}
|
65
src/usernames/dto/mod.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::usernames::validators::validate_pubkey;
|
||||
use crate::utils::error::Error;
|
||||
use nostr::prelude::*;
|
||||
use nostr::{key::XOnlyPublicKey, Keys};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
lazy_static! {
|
||||
static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Validate)]
|
||||
pub struct UserBody {
|
||||
#[validate(length(min = 1), regex = "VALID_CHARACTERS")]
|
||||
pub name: String,
|
||||
#[validate(custom(function = "validate_pubkey"))]
|
||||
pub pubkey: String,
|
||||
}
|
||||
|
||||
impl UserBody {
|
||||
pub fn get_pubkey(&self) -> XOnlyPublicKey {
|
||||
let keys = Keys::from_pk_str(&self.pubkey).unwrap();
|
||||
|
||||
keys.public_key()
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Validate)]
|
||||
pub struct Account {
|
||||
#[validate(custom(function = "validate_pubkey"))]
|
||||
pub pubkey: String,
|
||||
#[validate(length(min = 1), regex = "VALID_CHARACTERS")]
|
||||
pub name: String,
|
||||
#[validate(length(min = 1))]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn get_pubkey(&self) -> Result<XOnlyPublicKey, Error> {
|
||||
match nostr::Keys::from_pk_str(&self.pubkey) {
|
||||
Ok(pk) => Ok(pk.public_key()),
|
||||
Err(e) => Err(Error::invalid_param("pubkey", self.pubkey.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Validate)]
|
||||
pub struct AccountPubkey {
|
||||
#[validate(custom(function = "validate_pubkey"))]
|
||||
pub pubkey: String,
|
||||
}
|
27
src/usernames/filter.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
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 {
|
||||
match query.validate() {
|
||||
Ok(_) => Ok(query),
|
||||
Err(e) => Err(warp::reject::custom(Error::validation_error(e))),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_query_filter<T: serde::de::DeserializeOwned + Send + Validate + 'static>(
|
||||
) -> impl Filter<Extract = (T,), Error = Rejection> + Copy {
|
||||
warp::query::query::<T>().and_then(|query: T| async move {
|
||||
match query.validate() {
|
||||
Ok(_) => Ok(query),
|
||||
Err(e) => Err(warp::reject::custom(Error::validation_error(e))),
|
||||
}
|
||||
})
|
||||
}
|
89
src/usernames/handler.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use crate::bussy::{channels, Command, Message};
|
||||
use crate::noose::user::User;
|
||||
use crate::usernames::dto::{Account, UserQuery};
|
||||
use crate::utils::error::Error;
|
||||
use crate::utils::structs::Context;
|
||||
use serde_json::json;
|
||||
use warp::{Rejection, Reply};
|
||||
|
||||
pub async fn get_account(
|
||||
// account: Result<AccountPubkey, Error>,
|
||||
account: Account,
|
||||
context: Context,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
|
||||
|
||||
let command = Command::DbReqGetAccount(account.pubkey);
|
||||
|
||||
context
|
||||
.pubsub
|
||||
.publish(
|
||||
channels::MSG_NOOSE,
|
||||
Message {
|
||||
source: channels::MSG_NIP05,
|
||||
content: command,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(result) = subscriber.recv().await {
|
||||
match result.content {
|
||||
Command::DbResAccount => Ok(warp::reply::with_status(
|
||||
"ACCOUNT CREATED",
|
||||
warp::http::StatusCode::CREATED,
|
||||
)),
|
||||
Command::ServiceError(e) => Err(warp::reject::custom(e)),
|
||||
_ => Err(warp::reject::custom(Error::internal_with_message(
|
||||
"Unhandled message type",
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
Err(warp::reject::custom(Error::internal_with_message(
|
||||
"Unhandeled message type",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
context
|
||||
.pubsub
|
||||
.publish(
|
||||
channels::MSG_NOOSE,
|
||||
Message {
|
||||
source: channels::MSG_NIP05,
|
||||
content: command,
|
||||
},
|
||||
)
|
||||
.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": {}
|
||||
});
|
||||
Ok(warp::reply::json(&response))
|
||||
}
|
||||
Command::ServiceError(e) => Ok(warp::reply::json(&response)),
|
||||
_ => Err(warp::reject::custom(Error::internal_with_message(
|
||||
"Unhandeled message type",
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
Err(warp::reject::custom(Error::internal_with_message(
|
||||
"Unhandeled message type",
|
||||
)))
|
||||
}
|
||||
}
|
22
src/usernames/mod.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
mod accounts;
|
||||
pub mod dto;
|
||||
mod filter;
|
||||
mod handler;
|
||||
mod routes;
|
||||
mod util;
|
||||
mod validators;
|
||||
|
||||
use super::utils::structs::Context;
|
||||
use crate::utils::rejection_handler::handle_rejection;
|
||||
use tokio::runtime;
|
||||
use warp::Filter;
|
||||
|
||||
pub fn start(context: Context) {
|
||||
let rt = runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
log::info!("Starting NIP-05 on http://127.0.0.1:8085");
|
||||
let routes = routes::routes(context).recover(handle_rejection);
|
||||
|
||||
warp::serve(routes).run(([127, 0, 0, 1], 8085)).await;
|
||||
})
|
||||
}
|
57
src/usernames/routes.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use crate::noose::user::User;
|
||||
|
||||
use super::accounts::create_account;
|
||||
use super::dto::{Account, UserQuery};
|
||||
use super::filter::{validate_body_filter, validate_query_filter};
|
||||
use super::handler::{get_account, get_user};
|
||||
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 index = warp::path::end().map(|| warp::reply::html("<h1>SNEED!</h1>"));
|
||||
|
||||
index
|
||||
.or(nip05_get(context.clone()))
|
||||
.or(account_create(context.clone()))
|
||||
}
|
||||
|
||||
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"))
|
||||
.and(validate_query_filter::<UserQuery>())
|
||||
.and(with_context(context))
|
||||
.and_then(get_user)
|
||||
}
|
||||
|
||||
pub fn account_create(
|
||||
context: Context,
|
||||
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||
warp::path("account")
|
||||
.and(warp::post())
|
||||
.and(validate_body_filter::<User>())
|
||||
.and(with_context(context))
|
||||
.and_then(create_account)
|
||||
}
|
||||
|
||||
pub fn account_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,
|
||||
// ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||
// warp::path("account")
|
||||
// .and(warp::put())
|
||||
// .and(warp::body::json::<Account>())
|
||||
// .then(validate_body)
|
||||
// .and(with_context(context))
|
||||
// .and_then(update_account)
|
||||
// }
|
3
src/usernames/util.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
#[derive(Debug)]
|
||||
pub struct InvalidParameter;
|
||||
impl warp::reject::Reject for InvalidParameter {}
|
30
src/usernames/validators.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use super::dto::AccountPubkey;
|
||||
use crate::utils::error::Error;
|
||||
use nostr::prelude::FromPkStr;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
pub async fn validate_account_pubkey_query(
|
||||
account_pubkey: AccountPubkey,
|
||||
) -> Result<AccountPubkey, Error>
|
||||
where
|
||||
AccountPubkey: Validate,
|
||||
{
|
||||
match account_pubkey.validate() {
|
||||
Ok(_) => Ok(account_pubkey),
|
||||
Err(e) => {
|
||||
log::error!("AccountPubkey validation errors: {}", e);
|
||||
Err(Error::validation_error(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_pubkey(value: &str) -> Result<(), ValidationError> {
|
||||
if value.is_empty() {
|
||||
return Err(ValidationError::new("Value is empty"));
|
||||
}
|
||||
|
||||
match nostr::Keys::from_pk_str(value) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(ValidationError::new("Unable to parse pk_str")),
|
||||
}
|
||||
}
|
52
src/utils/crypto.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
|
||||
pub fn password_hash(password: String) -> String {
|
||||
let password = password.as_bytes();
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
// Argon2 with default params (Argon2id v19)
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
// Hash password to PHC string ($argon2id$v=19$...)
|
||||
let password_hash = argon2.hash_password(password, &salt).unwrap().to_string();
|
||||
|
||||
password_hash
|
||||
}
|
||||
|
||||
pub fn password_verify(password: String, password_hash: String) -> bool {
|
||||
// Verify password against PHC string.
|
||||
//
|
||||
// NOTE: hash params from `parsed_hash` are used instead of what is configured in the
|
||||
// `Argon2` instance.
|
||||
let password = password.as_bytes();
|
||||
let parsed_hash = PasswordHash::new(&password_hash).unwrap();
|
||||
Argon2::default()
|
||||
.verify_password(password, &parsed_hash)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{password_hash, password_verify};
|
||||
|
||||
#[test]
|
||||
fn hash() {
|
||||
let pass = "mysecretpassword".to_string();
|
||||
let hash = password_hash(pass);
|
||||
|
||||
assert_ne!(hash, "".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify() {
|
||||
let pass = "mysecretpassword".to_string();
|
||||
let hash = password_hash(pass.clone());
|
||||
|
||||
let verification = password_verify(pass, hash);
|
||||
|
||||
assert!(verification);
|
||||
}
|
||||
}
|
132
src/utils/error.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::{
|
||||
convert::From,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
use validator::ValidationErrors;
|
||||
use warp::{http::StatusCode, reject::Reject};
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(code: StatusCode, message: String) -> Self {
|
||||
Self {
|
||||
code: code.as_u16(),
|
||||
message,
|
||||
sneedstr_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_anyhow_error(code: StatusCode, err: anyhow::Error) -> Self {
|
||||
Self::new(code, err.to_string())
|
||||
}
|
||||
|
||||
pub fn bad_request<S: Display>(msg: S) -> Self {
|
||||
Self::new(StatusCode::BAD_REQUEST, msg.to_string())
|
||||
}
|
||||
|
||||
pub fn internal_with_message<S: Display>(msg: S) -> Self {
|
||||
Self::new(StatusCode::INTERNAL_SERVER_ERROR, msg.to_string())
|
||||
}
|
||||
|
||||
pub fn validation_error(msg: ValidationErrors) -> Self {
|
||||
let message =
|
||||
serde_json::to_string(&msg.field_errors()).unwrap_or("Validation Error".to_string());
|
||||
Self::new(StatusCode::BAD_REQUEST, message)
|
||||
}
|
||||
|
||||
pub fn not_found<S: Display>(resource: &str, identifier: S, service_version: u16) -> 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 {
|
||||
Self::bad_request(format!("invalid parameter {}: {}", name, value))
|
||||
}
|
||||
|
||||
pub fn invalid_request_body<S: Display>(msg: S) -> Self {
|
||||
Self::bad_request(format!("invalid request body: {}", msg))
|
||||
}
|
||||
|
||||
pub fn internal(err: anyhow::Error) -> Self {
|
||||
Self::from_anyhow_error(StatusCode::INTERNAL_SERVER_ERROR, err)
|
||||
}
|
||||
|
||||
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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}: {}", self.status_code(), &self.message)?;
|
||||
if let Some(val) = &self.sneedstr_version {
|
||||
write!(f, "\ndiem ledger version: {}", val)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Reject for Error {}
|
||||
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
Self::internal(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::error::Error> for Error {
|
||||
fn from(err: serde_json::error::Error) -> Self {
|
||||
Self::internal(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Error;
|
||||
use warp::http::StatusCode;
|
||||
|
||||
#[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")
|
||||
}
|
||||
|
||||
#[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")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_with_sneedstr_version() {
|
||||
let err =
|
||||
Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned()).sneedstr_version(123);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"400 Bad Request: invalid address\ndiem ledger version: 123"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_internal_error() {
|
||||
let err = Error::internal(anyhow::format_err!("hello"));
|
||||
assert_eq!(err.to_string(), "500 Internal Server Error: hello")
|
||||
}
|
||||
}
|
8
src/utils/filter.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use super::structs::Context;
|
||||
use warp::Filter;
|
||||
|
||||
pub fn with_context(
|
||||
context: Context,
|
||||
) -> impl Filter<Extract = (Context,), Error = std::convert::Infallible> + Clone {
|
||||
warp::any().map(move || context.clone())
|
||||
}
|
7
src/utils/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub mod crypto;
|
||||
pub mod error;
|
||||
pub mod filter;
|
||||
// mod nostr_filter_helpers;
|
||||
pub mod rejection_handler;
|
||||
pub mod response;
|
||||
pub mod structs;
|
156
src/utils/nostr_filter_helpers.rs
Normal file
|
@ -0,0 +1,156 @@
|
|||
use nostr::{Event, Filter, Kind, Tag};
|
||||
|
||||
fn ids_match(filter: &Filter, event: &Event) -> bool {
|
||||
if filter.ids.is_empty() {
|
||||
println!("[FILTER][IDS] skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
println!(
|
||||
"[FILTER][IDS] matched: {:?}",
|
||||
filter.ids.iter().any(|id| id == &event.id.to_string())
|
||||
);
|
||||
|
||||
filter.ids.iter().any(|id| id == &event.id.to_string())
|
||||
}
|
||||
|
||||
fn kind_match(filter: &Filter, kind: Kind) -> bool {
|
||||
if filter.kinds.is_empty() {
|
||||
println!("[FILTER][KINDS] skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
println!(
|
||||
"[FILTER][KIND] matched: {:?}",
|
||||
filter.kinds.iter().any(|k| k == &kind)
|
||||
);
|
||||
|
||||
filter.kinds.iter().any(|k| k == &kind)
|
||||
}
|
||||
|
||||
fn pubkeys_match(filter: &Filter, event: &Event) -> bool {
|
||||
if filter.pubkeys.is_empty() {
|
||||
println!("[FILTER][PUBKEYS] skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
println!(
|
||||
"[FILTER][PUBKEYS] matched: {:?}",
|
||||
filter.pubkeys.iter().any(|pk| pk == &event.pubkey)
|
||||
);
|
||||
filter.pubkeys.iter().any(|pk| pk == &event.pubkey)
|
||||
}
|
||||
|
||||
fn authors_match(filter: &Filter, event: &Event) -> bool {
|
||||
dbg!(filter);
|
||||
if filter.authors.is_empty() {
|
||||
println!("[FILTER][AUTHORS] skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
println!(
|
||||
"[FILTER][AUTHORS] matched: {:?}",
|
||||
filter
|
||||
.authors
|
||||
.iter()
|
||||
.any(|author| author == &event.pubkey.to_string())
|
||||
);
|
||||
filter
|
||||
.authors
|
||||
.iter()
|
||||
.any(|author| author == &event.pubkey.to_string())
|
||||
}
|
||||
|
||||
fn delegated_authors_match(filter: &Filter, event: &Event) -> bool {
|
||||
// Optional implementation
|
||||
|
||||
// let delegated_authors_match = filter.authors.iter().any(|author| {
|
||||
// event.tags.iter().any(|tag| match tag {
|
||||
// Tag::Delegation {
|
||||
// delegator_pk,
|
||||
// conditions,
|
||||
// sig,
|
||||
// } => filter
|
||||
// .authors
|
||||
// .iter()
|
||||
// .any(|author| author == &delegator_pk.to_string()),
|
||||
// _ => false,
|
||||
// })
|
||||
// });
|
||||
println!(
|
||||
"[FILTER][DELEGATED_AUTHORS] matched: {:?}",
|
||||
event.tags.iter().any(|tag| match tag {
|
||||
Tag::Delegation {
|
||||
delegator_pk,
|
||||
conditions,
|
||||
sig,
|
||||
} => filter
|
||||
.authors
|
||||
.iter()
|
||||
.any(|author| author == &delegator_pk.to_string()),
|
||||
_ => false,
|
||||
})
|
||||
);
|
||||
|
||||
event.tags.iter().any(|tag| match tag {
|
||||
Tag::Delegation {
|
||||
delegator_pk,
|
||||
conditions,
|
||||
sig,
|
||||
} => filter
|
||||
.authors
|
||||
.iter()
|
||||
.any(|author| author == &delegator_pk.to_string()),
|
||||
_ => true,
|
||||
})
|
||||
}
|
||||
|
||||
fn tag_match(filter: &Filter, event: &Event) -> bool {
|
||||
println!(
|
||||
"[FILTER][TAG] matched: {:?}",
|
||||
filter.generic_tags.iter().any(|(key, value)| {
|
||||
event.tags.iter().any(|tag| {
|
||||
let kv = tag.as_vec();
|
||||
key.to_string() == kv[0] && value.iter().any(|vv| vv == &kv[1])
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
filter.generic_tags.iter().any(|(key, value)| {
|
||||
event.tags.iter().any(|tag| {
|
||||
let kv = tag.as_vec();
|
||||
key.to_string() == kv[0] && value.iter().any(|vv| vv == &kv[1])
|
||||
})
|
||||
});
|
||||
|
||||
true // TODO: Fix delegated authors check
|
||||
}
|
||||
|
||||
pub fn interested_in_event(filter: &Filter, event: &Event) -> bool {
|
||||
ids_match(filter, event)
|
||||
&& filter.since.map_or(
|
||||
{
|
||||
println!("[FILTER][SINCE][default] matched: {:?}", true);
|
||||
true
|
||||
},
|
||||
|t| {
|
||||
println!("[FILTER][SINCE] matched: {:?}", event.created_at >= t);
|
||||
event.created_at >= t
|
||||
},
|
||||
)
|
||||
&& filter.until.map_or(
|
||||
{
|
||||
println!("[FILTER][UNTIL][default] matched: {:?}", true);
|
||||
true
|
||||
},
|
||||
|t| {
|
||||
println!("[FILTER][UNTIL] matched: {:?}", event.created_at <= t);
|
||||
event.created_at <= t
|
||||
},
|
||||
)
|
||||
&& kind_match(filter, event.kind)
|
||||
&& (pubkeys_match(filter, event)
|
||||
|| authors_match(filter, event)
|
||||
|| delegated_authors_match(filter, event))
|
||||
&& tag_match(filter, event)
|
||||
}
|
45
src/utils/rejection_handler.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use super::error::Error;
|
||||
|
||||
use std::convert::Infallible;
|
||||
use warp::{
|
||||
body::BodyDeserializeError,
|
||||
cors::CorsForbidden,
|
||||
http::StatusCode,
|
||||
reject::{LengthRequired, MethodNotAllowed, PayloadTooLarge, UnsupportedMediaType},
|
||||
reply, Rejection, Reply,
|
||||
};
|
||||
|
||||
pub async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
|
||||
let code;
|
||||
let body;
|
||||
|
||||
if err.is_not_found() {
|
||||
code = StatusCode::NOT_FOUND;
|
||||
body = reply::json(&Error::new(code, "Not Found".to_owned()));
|
||||
} else if let Some(error) = err.find::<Error>() {
|
||||
code = error.status_code();
|
||||
body = reply::json(error);
|
||||
} else if let Some(cause) = err.find::<CorsForbidden>() {
|
||||
code = StatusCode::FORBIDDEN;
|
||||
body = reply::json(&Error::new(code, cause.to_string()));
|
||||
} else if let Some(cause) = err.find::<BodyDeserializeError>() {
|
||||
code = StatusCode::BAD_REQUEST;
|
||||
body = reply::json(&Error::new(code, cause.to_string()));
|
||||
} else if let Some(cause) = err.find::<LengthRequired>() {
|
||||
code = StatusCode::LENGTH_REQUIRED;
|
||||
body = reply::json(&Error::new(code, cause.to_string()));
|
||||
} else if let Some(cause) = err.find::<PayloadTooLarge>() {
|
||||
code = StatusCode::PAYLOAD_TOO_LARGE;
|
||||
body = reply::json(&Error::new(code, cause.to_string()));
|
||||
} else if let Some(cause) = err.find::<UnsupportedMediaType>() {
|
||||
code = StatusCode::UNSUPPORTED_MEDIA_TYPE;
|
||||
body = reply::json(&Error::new(code, cause.to_string()));
|
||||
} else if let Some(cause) = err.find::<MethodNotAllowed>() {
|
||||
code = StatusCode::METHOD_NOT_ALLOWED;
|
||||
body = reply::json(&Error::new(code, cause.to_string()));
|
||||
} else {
|
||||
code = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
body = reply::json(&Error::new(code, format!("unexpected error: {:?}", err)));
|
||||
}
|
||||
Ok(reply::with_status(body, code))
|
||||
}
|
28
src/utils/response.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use super::error::Error;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use warp::http::header::{HeaderValue, CONTENT_TYPE};
|
||||
|
||||
pub struct Response {
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn new<T: Serialize>(body: &T) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
body: serde_json::to_vec(body)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl warp::Reply for Response {
|
||||
fn into_response(self) -> warp::reply::Response {
|
||||
let mut res = warp::reply::Response::new(self.body.into());
|
||||
let headers = res.headers_mut();
|
||||
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||
|
||||
res
|
||||
}
|
||||
}
|
135
src/utils/structs.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
use super::error::Error;
|
||||
// use super::nostr_filter_helpers;
|
||||
use crate::PubSub;
|
||||
use nostr::{Event, Filter, SubscriptionId};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use uuid::Uuid;
|
||||
use warp::ws::Message;
|
||||
|
||||
const MAX_SUBSCRIPTIONS: usize = 256;
|
||||
const MAX_SUBSCRIPTION_ID_LEN: usize = 256;
|
||||
|
||||
#[derive(serde::Serialize, PartialEq, Eq, Debug, Clone)]
|
||||
pub struct Subscription {
|
||||
pub id: SubscriptionId,
|
||||
pub filters: Vec<Filter>,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
pub fn new(id: SubscriptionId, filters: Vec<Filter>) -> Self {
|
||||
Self { id, filters }
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
|
||||
pub fn needs_historical_events(&self) -> bool {
|
||||
self.filters.iter().any(|f| f.limit != Some(0))
|
||||
}
|
||||
|
||||
pub fn interested_in_event(&self, event: &Event) -> bool {
|
||||
log::info!("[Subscription] Checking if client is interested in the new event");
|
||||
for filter in &self.filters {
|
||||
if filter.match_event(event) {
|
||||
log::info!("[Subscription] found filter that matches the event");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
client_ip_addr: String,
|
||||
pub client_id: Uuid,
|
||||
pub client_connection: Option<mpsc::UnboundedSender<Result<Message, Error>>>,
|
||||
pub subscriptions: HashMap<String, Subscription>,
|
||||
max_subs: usize,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(client_ip_addr: String) -> Self {
|
||||
Self {
|
||||
client_ip_addr,
|
||||
client_id: Uuid::new_v4(),
|
||||
client_connection: None,
|
||||
subscriptions: HashMap::new(),
|
||||
max_subs: MAX_SUBSCRIPTIONS,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ip(&self) -> &str {
|
||||
&self.client_ip_addr
|
||||
}
|
||||
|
||||
pub fn subscribe(&mut self, subscription: Subscription) -> Result<(), Error> {
|
||||
let k = subscription.get_id();
|
||||
let sub_id_len = k.len();
|
||||
if sub_id_len > MAX_SUBSCRIPTION_ID_LEN {
|
||||
log::debug!(
|
||||
"Ignoring sub request with excessive length: ({})",
|
||||
sub_id_len
|
||||
);
|
||||
|
||||
return Err(Error::bad_request("sub request is too long"));
|
||||
}
|
||||
|
||||
if self.subscriptions.contains_key(&k) {
|
||||
self.subscriptions.remove(&k);
|
||||
self.subscriptions.insert(k, subscription.clone());
|
||||
|
||||
log::debug!(
|
||||
"replaced existing subscription (cid: {}, sub: {:?})",
|
||||
self.client_id,
|
||||
subscription.get_id()
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.subscriptions.len() >= self.max_subs {
|
||||
return Err(Error::bad_request("max subs exceeded"));
|
||||
}
|
||||
|
||||
// Insert subscription
|
||||
self.subscriptions.insert(k, subscription);
|
||||
log::debug!(
|
||||
"registered new subscription, currently have {} active subs (cid: {})",
|
||||
self.subscriptions.len(),
|
||||
self.client_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unsubscribe(&mut self, subscription_id: SubscriptionId) {
|
||||
self.subscriptions.remove(&subscription_id.to_string());
|
||||
log::debug!(
|
||||
"removed subscription, currently have {} active subs (cid: {})",
|
||||
self.subscriptions.len(),
|
||||
self.client_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub pubsub: Arc<PubSub>,
|
||||
}
|
||||
|
||||
impl Default for Context {
|
||||
fn default() -> Self {
|
||||
panic!("Use Context::new() to initialize Contexr");
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new() -> Self {
|
||||
let pubsub = Arc::new(PubSub::new());
|
||||
|
||||
Self { pubsub }
|
||||
}
|
||||
}
|
66
templates/index.html
Normal file
|
@ -0,0 +1,66 @@
|
|||
<!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>
|
70
templates/index.stpl
Normal file
|
@ -0,0 +1,70 @@
|
|||
<!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>
|
BIN
templates/static/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
templates/static/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 235 KiB |
BIN
templates/static/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
templates/static/favicon-16x16.png
Normal file
After Width: | Height: | Size: 777 B |
BIN
templates/static/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
templates/static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
templates/static/site.webmanifest
Normal file
|
@ -0,0 +1 @@
|
|||
{"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"}
|
BIN
www/static/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
www/static/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
www/static/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
www/static/favicon-16x16.png
Normal file
After Width: | Height: | Size: 715 B |
BIN
www/static/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
www/static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
www/static/site.webmanifest
Normal file
|
@ -0,0 +1 @@
|
|||
{"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"}
|