notecrumbs

a nostr opengraph server build on nostrdb and egui
git clone git://jb55.com/notecrumbs
Log | Files | Refs | README | LICENSE

commit dec66e4a8aa7a033441cb523f633e41e2d3ffea3
parent 7a7a04da39c84281f3e174ceabae21450997e162
Author: William Casarin <jb55@jb55.com>
Date:   Sun, 17 Dec 2023 13:02:21 -0800

fetch notes from relays if we don't have them

If we don't have a cache hit, try to find the note on other relays. Once
we find it, add it to nostrdb.

Diffstat:
MCargo.toml | 4++--
MREADME.md | 2+-
Msrc/main.rs | 122++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
3 files changed, 99 insertions(+), 29 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -13,8 +13,8 @@ hyper-util = { version = "0.1", features = ["full"] } http-body-util = "0.1" log = "0.4.20" env_logger = "0.10.1" -nostrdb = "0.1.3" -nostr-sdk = { git = "https://github.com/damus-io/nostr-sdk.git", rev = "bfd6ac4e111720dcecf8fc09d4ea76da4d971cf4" } +nostrdb = { git = "https://github.com/damus-io/nostrdb-rs.git", rev = "0545571827bf64e06250f094d65775acd2a1165e" } +nostr-sdk = { git = "https://github.com/damus-io/nostr-sdk.git", rev = "fc0dc7b38f5060f171228b976b9700c0135245d3" } hex = "0.4.3" egui = "0.21.0" egui_skia = { version = "0.4.0", features = ["cpu_fix"] } diff --git a/README.md b/README.md @@ -17,7 +17,7 @@ WIP! - [x] Local note fetching with nostrdb - [x] Basic note rendering -- [ ] Fetch notes from relays +- [x] Fetch notes from relays - [ ] Render profile pictures - [ ] Cache profile pictures - [ ] HTML note page diff --git a/src/main.rs b/src/main.rs @@ -7,20 +7,26 @@ use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Request, Response, StatusCode}; use hyper_util::rt::TokioIo; -use log::info; +use log::{error, info, warn}; use tokio::net::TcpListener; use crate::error::Error; use nostr_sdk::nips::nip19::Nip19; use nostr_sdk::prelude::*; -use nostrdb::{Config, Ndb, Note, Transaction}; +use nostrdb::{Config, Ndb, Transaction}; +use std::time::Duration; + +use nostr_sdk::Kind; mod error; #[derive(Debug, Clone)] struct Context { ndb: Ndb, - //font_data: egui::FontData, + keys: Keys, + + /// How long do we wait for remote note requests + timeout: Duration, } fn nip19_evid(nip19: &Nip19) -> Option<EventId> { @@ -31,8 +37,7 @@ fn nip19_evid(nip19: &Nip19) -> Option<EventId> { } } - -fn render_note<'a>(_app_ctx: &Context, note: &'a Note) -> Vec<u8> { +fn render_note<'a>(_app_ctx: &Context, content: &'a str) -> Vec<u8> { use egui::{FontId, RichText}; use egui_skia::{rasterize, RasterizeOptions}; use skia_safe::EncodedImageFormat; @@ -46,7 +51,7 @@ fn render_note<'a>(_app_ctx: &Context, note: &'a Note) -> Vec<u8> { ui.horizontal(|ui| { ui.label(RichText::new("✏").font(FontId::proportional(120.0))); ui.vertical(|ui| { - ui.label(RichText::new(note.content()).font(FontId::proportional(40.0))); + ui.label(RichText::new(content).font(FontId::proportional(40.0))); }); }) }); @@ -97,11 +102,57 @@ fn nip19_relays(nip19: &Nip19) -> Vec<String> { relays } +async fn find_note(ctx: &Context, nip19: &Nip19) -> Result<nostr_sdk::Event, Error> { + let opts = Options::new().shutdown_on_drop(true); + let client = Client::with_opts(&ctx.keys, opts); + + let _ = client.add_relay("wss://relay.damus.io").await; + + let other_relays = nip19_relays(nip19); + for relay in other_relays { + let _ = client.add_relay(relay).await; + } + + client.connect().await; + + let filters = nip19_to_filters(nip19)?; + + client + .req_events_of(filters.clone(), Some(ctx.timeout)) + .await; + + loop { + match client.notifications().recv().await? { + RelayPoolNotification::Event(_url, ev) => { + info!("got ev: {:?}", ev); + return Ok(ev); + } + RelayPoolNotification::RelayStatus { .. } => continue, + RelayPoolNotification::Message(_url, msg) => match msg { + RelayMessage::Event { event, .. } => return Ok(*event), + RelayMessage::EndOfStoredEvents(_) => return Err(Error::NotFound), + _ => continue, + }, + RelayPoolNotification::Stop | RelayPoolNotification::Shutdown => { + return Err(Error::NotFound); + } + } + } +} + async fn serve( ctx: &Context, r: Request<hyper::body::Incoming>, ) -> Result<Response<Full<Bytes>>, Error> { - let nip19 = Nip19::from_bech32(&r.uri().to_string()[1..])?; + let nip19 = match Nip19::from_bech32(&r.uri().to_string()[1..]) { + Ok(nip19) => nip19, + Err(_) => { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::from("Invalid url\n")))?); + } + }; + let evid = match nip19_evid(&nip19) { Some(evid) => evid, None => { @@ -111,21 +162,33 @@ async fn serve( } }; - let mut txn = Transaction::new(&ctx.ndb)?; - let note = match ctx - .ndb - .get_note_by_id(&mut txn, evid.as_bytes().try_into()?) - { - Ok(note) => note, - Err(nostrdb::Error::NotFound) => { - // query relays - return Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Full::new(Bytes::from(format!( - "noteid {} not found\n", - ::hex::encode(evid) - ))))?); - } + let content = { + let mut txn = Transaction::new(&ctx.ndb)?; + ctx.ndb + .get_note_by_id(&mut txn, evid.as_bytes().try_into()?) + .map(|n| { + info!("cache hit {:?}", nip19); + n.content().to_string() + }) + }; + + let content = match content { + Ok(content) => content, + Err(nostrdb::Error::NotFound) => match find_note(ctx, &nip19).await { + Ok(note) => { + ctx.ndb + .process_event(&json!(["EVENT", "s", note]).to_string()); + note.content + } + Err(err) => { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::from(format!( + "noteid {} not found\n", + ::hex::encode(evid) + ))))?); + } + }, Err(err) => { return Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) @@ -133,13 +196,20 @@ async fn serve( } }; - let data = render_note(&ctx, &note); + let data = render_note(&ctx, &content); Ok(Response::builder() .header(header::CONTENT_TYPE, "image/png") + .status(StatusCode::OK) .body(Full::new(Bytes::from(data)))?) } +fn get_env_timeout() -> Duration { + let timeout_env = std::env::var("TIMEOUT_MS").unwrap_or("2000".to_string()); + let timeout_ms: u64 = timeout_env.parse().unwrap_or(2000); + Duration::from_millis(timeout_ms) +} + #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { env_logger::init(); @@ -153,9 +223,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { let cfg = Config::new(); let ndb = Ndb::new(".", &cfg).expect("ndb failed to open"); //let font_data = egui::FontData::from_static(include_bytes!("../fonts/NotoSans-Regular.ttf")); - let ctx = Context { - ndb, /*, font_data */ - }; + let keys = Keys::generate(); + let timeout = get_env_timeout(); + let ctx = Context { ndb, keys, timeout }; // We start a loop to continuously accept incoming connections loop {