diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d714d71..e1edf59 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,6 +15,8 @@ jobs: runs-on: ubuntu-latest steps: + - name: Deps + run: sudo apt-get install libfontconfig1-dev libfreetype6-dev libssl-dev - uses: actions/checkout@v3 - name: Build run: cargo build --verbose diff --git a/Cargo.lock b/Cargo.lock index f1f78e9..1c47628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1618,6 +1618,7 @@ dependencies = [ "nostr", "nostr-sdk", "nostrdb", + "serde_json", "skia-safe", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 000d008..d9ebe87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,4 @@ lru = "0.12.1" bytes = "1.5.0" http = "1.0.0" html-escape = "0.2.13" +serde_json = "*" diff --git a/src/error.rs b/src/error.rs index 2de43ce..cacc4b9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,6 +14,7 @@ pub enum Error { NostrClient(nostr_sdk::client::Error), Recv(RecvError), Io(std::io::Error), + Json(serde_json::Error), Generic(String), Timeout(tokio::time::error::Elapsed), Image(image::error::ImageError), @@ -44,6 +45,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: serde_json::Error) -> Self { + Error::Json(err) + } +} + impl From for Error { fn from(err: nostr_sdk::secp256k1::Error) -> Self { Error::Secp(err) @@ -120,6 +127,7 @@ impl fmt::Display for Error { Error::NostrClient(e) => write!(f, "Nostr client error: {}", e), Error::NotFound => write!(f, "Not found"), Error::Recv(e) => write!(f, "Recieve error: {}", e), + Error::Json(e) => write!(f, "json error: {e}"), Error::InvalidNip19 => write!(f, "Invalid nip19 object"), Error::NothingToFetch => write!(f, "No data to fetch!"), Error::SliceErr => write!(f, "Array slice error"), diff --git a/src/html.rs b/src/html.rs index 50983fc..560fd87 100644 --- a/src/html.rs +++ b/src/html.rs @@ -7,10 +7,84 @@ use crate::{ use http_body_util::Full; use hyper::{body::Bytes, header, Request, Response, StatusCode}; use nostr_sdk::prelude::{Nip19, ToBech32}; -use nostrdb::{BlockType, Blocks, Mention, Note, Transaction}; +use nostrdb::{BlockType, Blocks, Filter, Mention, Ndb, Note, Transaction}; use std::io::Write; use tracing::{error, warn}; +fn blocktype_name(blocktype: &BlockType) -> &'static str { + match blocktype { + BlockType::MentionBech32 => "mention", + BlockType::Hashtag => "hashtag", + BlockType::Url => "url", + BlockType::Text => "text", + BlockType::MentionIndex => "indexed_mention", + BlockType::Invoice => "invoice", + } +} + +pub fn serve_note_json( + ndb: &Ndb, + note_rd: &NoteAndProfileRenderData, +) -> Result>, Error> { + let mut body: Vec = vec![]; + + let note_key = match note_rd.note_rd { + NoteRenderData::Note(note_key) => note_key, + NoteRenderData::Missing(note_id) => { + warn!("missing note_id {}", hex::encode(note_id)); + return Err(Error::NotFound); + } + }; + + let txn = Transaction::new(ndb)?; + + let note = if let Ok(note) = ndb.get_note_by_key(&txn, note_key) { + note + } else { + // 404 + return Err(Error::NotFound); + }; + + write!(body, "{{\"note\":{},\"parsed_content\":[", ¬e.json()?)?; + + if let Ok(blocks) = ndb.get_blocks_by_key(&txn, note_key) { + for (i, block) in blocks.iter(¬e).enumerate() { + if i != 0 { + write!(body, ",")?; + } + write!( + body, + "{{\"{}\":{}}}", + blocktype_name(&block.blocktype()), + serde_json::to_string(block.as_str())? + )?; + } + }; + + write!(body, "]")?; + + if let Ok(results) = ndb.query( + &txn, + &[Filter::new() + .authors([note.pubkey()]) + .kinds([0]) + .limit(1) + .build()], + 1, + ) { + if let Some(profile_note) = results.first() { + write!(body, ",\"profile\":{}", profile_note.note.json()?)?; + } + } + + writeln!(body, "}}")?; + + Ok(Response::builder() + .header(header::CONTENT_TYPE, "application/json; charset=utf-8") + .status(StatusCode::OK) + .body(Full::new(Bytes::from(body)))?) +} + pub fn render_note_content(body: &mut Vec, note: &Note, blocks: &Blocks) { for block in blocks.iter(note) { match block.blocktype() { @@ -84,7 +158,6 @@ pub fn serve_note_html( // 5: formatted date // 6: pfp url - let txn = Transaction::new(&app.ndb)?; let note_key = match note_rd.note_rd { NoteRenderData::Note(note_key) => note_key, NoteRenderData::Missing(note_id) => { @@ -93,6 +166,8 @@ pub fn serve_note_html( } }; + let txn = Transaction::new(&app.ndb)?; + let note = if let Ok(note) = app.ndb.get_note_by_key(&txn, note_key) { note } else { diff --git a/src/main.rs b/src/main.rs index fe3f60f..fc4bc91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -122,7 +122,14 @@ async fn serve( r: Request, ) -> Result>, Error> { let is_png = r.uri().path().ends_with(".png"); - let until = if is_png { 4 } else { 0 }; + let is_json = r.uri().path().ends_with(".json"); + let until = if is_png { + 4 + } else if is_json { + 5 + } else { + 0 + }; let path_len = r.uri().path().len(); let nip19 = match Nip19::from_bech32(&r.uri().path()[1..path_len - until]) { @@ -166,6 +173,15 @@ async fn serve( .header(header::CONTENT_TYPE, "image/png") .status(StatusCode::OK) .body(Full::new(Bytes::from(data)))?) + } else if is_json { + match render_data { + RenderData::Note(note_rd) => html::serve_note_json(&app.ndb, ¬e_rd), + RenderData::Profile(_profile_rd) => { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::from("todo: profile json")))?); + } + } } else { match render_data { RenderData::Note(note_rd) => html::serve_note_html(app, &nip19, ¬e_rd, r), diff --git a/src/render.rs b/src/render.rs index 237bb3e..5490ee9 100644 --- a/src/render.rs +++ b/src/render.rs @@ -251,6 +251,7 @@ pub async fn find_note( let _ = client.add_relay("wss://relay.damus.io").await; let _ = client.add_relay("wss://nostr.wine").await; let _ = client.add_relay("wss://nos.lol").await; + let expected_events = filters.len(); let other_relays = nip19::nip19_relays(nip19); for relay in other_relays { @@ -267,11 +268,18 @@ pub async fn find_note( .stream_events(filters, Some(std::time::Duration::from_millis(2000))) .await?; + let mut num_loops = 0; while let Some(event) = streamed_events.next().await { debug!("processing event {:?}", event); if let Err(err) = ndb.process_event(&event.as_json()) { error!("error processing event: {err}"); } + + num_loops += 1; + + if num_loops == expected_events { + break; + } } Ok(())