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.
This commit is contained in:
William Casarin 2023-12-17 13:02:21 -08:00
parent 7a7a04da39
commit dec66e4a8a
3 changed files with 99 additions and 29 deletions

View File

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

View File

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

View File

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