From f1a80c0793176c0a97e2ef760272c223e52c2db6 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Mon, 16 Dec 2024 21:17:51 -0800 Subject: [PATCH] Switch to async nostrdb This updates notecrumbs to use the new async nostrdb apis. It is still not ideal, we should try to fetch pubkeys immediately after we fetch the note, but this is ok for now. Signed-off-by: William Casarin --- Cargo.lock | 114 ++++---- Cargo.toml | 2 +- shell.nix | 2 +- src/abbrev.rs | 2 +- src/error.rs | 19 +- src/gradient.rs | 6 +- src/html.rs | 90 ++++--- src/main.rs | 166 ++++++------ src/nip19.rs | 42 +-- src/pfp.rs | 30 +-- src/render.rs | 690 +++++++++++++++++++++++++++++------------------- 11 files changed, 653 insertions(+), 510 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 250043d..4e8fe44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,7 +101,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -246,7 +246,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.90", + "syn 2.0.92", "which", ] @@ -375,9 +375,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.4" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" +checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" dependencies = [ "shlex", ] @@ -567,7 +567,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -651,7 +651,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -662,7 +662,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -779,9 +779,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] name = "form_urlencoded" @@ -848,7 +848,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -1251,7 +1251,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -1405,9 +1405,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -1497,9 +1497,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", "simd-adler32", @@ -1621,15 +1621,15 @@ dependencies = [ [[package]] name = "nostrdb" -version = "0.5.0" -source = "git+https://github.com/damus-io/nostrdb-rs?rev=d7ad4a44b929157795601a68542490b4184ae657#d7ad4a44b929157795601a68542490b4184ae657" +version = "0.5.1" +source = "git+https://github.com/damus-io/nostrdb-rs?rev=b4dfb0b29d6537d9f5f852028bd34c5ccff4c839#b4dfb0b29d6537d9f5f852028bd34c5ccff4c839" dependencies = [ "bindgen 0.69.5", "cc", "flatbuffers", "futures", "libc", - "thiserror 2.0.7", + "thiserror 2.0.9", "tokio", "tracing", "tracing-subscriber", @@ -1681,9 +1681,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -1791,9 +1791,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "png" -version = "0.17.15" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67582bd5b65bdff614270e2ea89a1cf15bef71245cc1e5f7ea126977144211d" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -1829,7 +1829,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -1852,9 +1852,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2105,7 +2105,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes 0.14.0", "rand", "secp256k1-sys", "serde", @@ -2128,29 +2128,29 @@ checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "indexmap", "itoa", @@ -2329,9 +2329,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126" dependencies = [ "proc-macro2", "quote", @@ -2346,7 +2346,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -2380,11 +2380,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.7" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" dependencies = [ - "thiserror-impl 2.0.7", + "thiserror-impl 2.0.9", ] [[package]] @@ -2395,18 +2395,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] name = "thiserror-impl" -version = "2.0.7" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -2467,9 +2467,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -2506,7 +2506,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -2605,7 +2605,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -2683,9 +2683,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" @@ -2838,7 +2838,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", "wasm-bindgen-shared", ] @@ -2873,7 +2873,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3083,7 +3083,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", "synstructure", ] @@ -3105,7 +3105,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] @@ -3125,7 +3125,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", "synstructure", ] @@ -3154,7 +3154,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.92", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 91cf856..9811ee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ hyper-util = { version = "0.1.1", features = ["full"] } http-body-util = "0.1" log = "0.4.20" env_logger = "0.10.1" -nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "d7ad4a44b929157795601a68542490b4184ae657" } +nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "b4dfb0b29d6537d9f5f852028bd34c5ccff4c839" } #nostrdb = { path = "/home/jb55/src/rust/nostrdb-rs" } #nostrdb = "0.1.6" #nostr-sdk = { git = "https://github.com/damus-io/nostr-sdk.git", rev = "fc0dc7b38f5060f171228b976b9700c0135245d3" } diff --git a/shell.nix b/shell.nix index 32290c9..25ce8ec 100644 --- a/shell.nix +++ b/shell.nix @@ -1,5 +1,5 @@ { pkgs ? import {} }: with pkgs; mkShell { - nativeBuildInputs = [ gdb cargo rustc rustfmt libiconv pkg-config fontconfig freetype ]; + nativeBuildInputs = [ libiconv pkg-config fontconfig freetype ]; } diff --git a/src/abbrev.rs b/src/abbrev.rs index 1f7d5e6..7ce3574 100644 --- a/src/abbrev.rs +++ b/src/abbrev.rs @@ -30,7 +30,7 @@ pub fn abbrev_str(name: &str) -> String { } } -pub fn abbreviate<'a>(text: &'a str, len: usize) -> &'a str { +pub fn abbreviate(text: &str, len: usize) -> &str { let closest = floor_char_boundary(text, len); &text[..closest] } diff --git a/src/error.rs b/src/error.rs index e4f4810..2de43ce 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,8 @@ use std::array::TryFromSliceError; use std::fmt; use tokio::sync::broadcast::error::RecvError; +pub type Result = std::result::Result; + #[derive(Debug)] pub enum Error { Nip19(nip19::Error), @@ -13,14 +15,20 @@ pub enum Error { Recv(RecvError), Io(std::io::Error), Generic(String), + Timeout(tokio::time::error::Elapsed), Image(image::error::ImageError), Secp(nostr_sdk::secp256k1::Error), InvalidUri, NotFound, /// Profile picture is too big + #[allow(dead_code)] TooBig, + #[allow(dead_code)] InvalidNip19, + NothingToFetch, + #[allow(dead_code)] InvalidProfilePic, + CantRender, SliceErr, } @@ -31,7 +39,7 @@ impl From for Error { } impl From for Error { - fn from(err: http::uri::InvalidUri) -> Self { + fn from(_err: http::uri::InvalidUri) -> Self { Error::InvalidUri } } @@ -60,6 +68,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: tokio::time::error::Elapsed) -> Self { + Error::Timeout(err) + } +} + impl From for Error { fn from(err: hyper::Error) -> Self { Error::Hyper(err) @@ -107,10 +121,13 @@ impl fmt::Display for Error { Error::NotFound => write!(f, "Not found"), Error::Recv(e) => write!(f, "Recieve error: {}", e), Error::InvalidNip19 => write!(f, "Invalid nip19 object"), + Error::NothingToFetch => write!(f, "No data to fetch!"), Error::SliceErr => write!(f, "Array slice error"), Error::TooBig => write!(f, "Profile picture is too big"), Error::InvalidProfilePic => write!(f, "Profile picture is corrupt"), + Error::CantRender => write!(f, "Error rendering"), Error::Image(err) => write!(f, "Image error: {}", err), + Error::Timeout(elapsed) => write!(f, "Timeout error: {}", elapsed), Error::InvalidUri => write!(f, "Invalid url"), Error::Hyper(err) => write!(f, "Hyper error: {}", err), Error::Generic(err) => write!(f, "Generic error: {}", err), diff --git a/src/gradient.rs b/src/gradient.rs index 22b7d04..7a6c423 100644 --- a/src/gradient.rs +++ b/src/gradient.rs @@ -4,7 +4,7 @@ use egui::{lerp, Color32, Pos2, Rgba}; pub struct Gradient(pub Vec); impl Gradient { - pub fn linear(left: Color32, right: Color32) -> Self { + pub fn _linear(left: Color32, right: Color32) -> Self { let left = Rgba::from(left); let right = Rgba::from(right); @@ -44,7 +44,7 @@ impl Gradient { Self(result) } - pub fn radial_alpha_gradient( + pub fn _radial_alpha_gradient( center: Pos2, radius: f32, start_color: Color32, @@ -86,7 +86,7 @@ impl Gradient { /// Do premultiplied alpha-aware blending of the gradient on top of the fill color /// in gamma-space. - pub fn with_bg_fill(self, bg: Color32) -> Self { + pub fn _with_bg_fill(self, bg: Color32) -> Self { Self( self.0 .into_iter() diff --git a/src/html.rs b/src/html.rs index 9ad65fc..aafa4f5 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,56 +1,51 @@ use crate::Error; use crate::{ abbrev::{abbrev_str, abbreviate}, - render, Notecrumbs, + render::{NoteAndProfileRenderData, NoteRenderData, ProfileRenderData}, + Notecrumbs, }; -use html_escape; use http_body_util::Full; -use hyper::{ - body::Bytes, header, server::conn::http1, service::service_fn, Request, Response, StatusCode, -}; -use hyper_util::rt::TokioIo; -use log::error; +use hyper::{body::Bytes, header, Request, Response, StatusCode}; +use log::{error, warn}; use nostr_sdk::prelude::{Nip19, ToBech32}; -use nostrdb::{BlockType, Blocks, Mention, Ndb, Note, Transaction}; +use nostrdb::{BlockType, Blocks, Mention, Note, Transaction}; use std::io::Write; -pub fn render_note_content(body: &mut Vec, ndb: &Ndb, note: &Note, blocks: &Blocks) { +pub fn render_note_content(body: &mut Vec, note: &Note, blocks: &Blocks) { for block in blocks.iter(note) { - let blocktype = block.blocktype(); - match block.blocktype() { BlockType::Url => { let url = html_escape::encode_text(block.as_str()); - write!(body, r#"{}"#, url, url); + let _ = write!(body, r#"{}"#, url, url); } BlockType::Hashtag => { let hashtag = html_escape::encode_text(block.as_str()); - write!(body, r#"#{}"#, hashtag); + let _ = write!(body, r#"#{}"#, hashtag); } BlockType::Text => { let text = html_escape::encode_text(block.as_str()); - write!(body, r"{}", text); + let _ = write!(body, r"{}", text); } BlockType::Invoice => { - write!(body, r"{}", block.as_str()); + let _ = write!(body, r"{}", block.as_str()); } BlockType::MentionIndex => { - write!(body, r"@nostrich"); + let _ = write!(body, r"@nostrich"); } BlockType::MentionBech32 => { - let pk = match block.as_mention().unwrap() { + match block.as_mention().unwrap() { Mention::Event(_) | Mention::Note(_) | Mention::Profile(_) | Mention::Pubkey(_) | Mention::Secret(_) | Mention::Addr(_) => { - write!( + let _ = write!( body, r#"@{}"#, block.as_str(), @@ -59,7 +54,7 @@ pub fn render_note_content(body: &mut Vec, ndb: &Ndb, note: &Note, blocks: & } Mention::Relay(relay) => { - write!( + let _ = write!( body, r#"{}"#, block.as_str(), @@ -75,8 +70,8 @@ pub fn render_note_content(body: &mut Vec, ndb: &Ndb, note: &Note, blocks: & pub fn serve_note_html( app: &Notecrumbs, nip19: &Nip19, - note_data: &render::NoteRenderData, - r: Request, + note_rd: &NoteAndProfileRenderData, + _r: Request, ) -> Result>, Error> { let mut data = Vec::new(); @@ -89,9 +84,39 @@ 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) => { + warn!("missing note_id {}", hex::encode(note_id)); + return Err(Error::NotFound); + } + }; + + let note = if let Ok(note) = app.ndb.get_note_by_key(&txn, note_key) { + note + } else { + // 404 + return Err(Error::NotFound); + }; + + let profile = note_rd.profile_rd.as_ref().and_then(|profile_rd| { + match profile_rd { + // we probably wouldn't have it here, but we query just in case? + ProfileRenderData::Missing(pk) => app.ndb.get_profile_by_pubkey(&txn, pk).ok(), + ProfileRenderData::Profile(key) => app.ndb.get_profile_by_key(&txn, *key).ok(), + } + }); + let hostname = "https://damus.io"; - let abbrev_content = html_escape::encode_text(abbreviate(¬e_data.note.content, 64)); - let profile_name = html_escape::encode_text(¬e_data.profile.name); + let abbrev_content = html_escape::encode_text(abbreviate(note.content(), 64)); + let profile = profile.and_then(|pr| pr.record().profile()); + let default_pfp_url = "https://damus.io/img/no-profile.svg"; + let pfp_url = profile.and_then(|p| p.picture()).unwrap_or(default_pfp_url); + let profile_name = { + let name = profile.and_then(|p| p.name()).unwrap_or("nostrich"); + html_escape::encode_text(name) + }; let bech32 = nip19.to_bech32().unwrap(); write!( @@ -150,31 +175,26 @@ pub fn serve_note_html( abbrev_content, hostname, bech32, - note_data.note.timestamp, - note_data.profile.pfp_url, + note.created_at(), + pfp_url, )?; let ok = (|| -> Result<(), nostrdb::Error> { - let txn = Transaction::new(&app.ndb)?; - let note_id = note_data.note.id.ok_or(nostrdb::Error::NotFound)?; - let note = app.ndb.get_note_by_id(&txn, ¬e_id)?; + let note_id = note.id(); + let note = app.ndb.get_note_by_id(&txn, note_id)?; let blocks = app.ndb.get_blocks_by_key(&txn, note.key().unwrap())?; - render_note_content(&mut data, &app.ndb, ¬e, &blocks); + render_note_content(&mut data, ¬e, &blocks); Ok(()) })(); if let Err(err) = ok { error!("error rendering html: {}", err); - write!( - data, - "{}", - html_escape::encode_text(¬e_data.note.content) - ); + let _ = write!(data, "{}", html_escape::encode_text(¬e.content())); } - write!( + let _ = write!( data, r#" diff --git a/src/main.rs b/src/main.rs index 66cd6d9..6755572 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,15 +7,17 @@ use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Request, Response, StatusCode}; use hyper_util::rt::TokioIo; -use log::{debug, info}; +use log::{error, info}; use std::io::Write; use std::sync::Arc; use tokio::net::TcpListener; -use crate::error::Error; -use crate::render::RenderData; +use crate::{ + error::Error, + render::{ProfileRenderData, RenderData}, +}; use nostr_sdk::prelude::*; -use nostrdb::{Config, Ndb}; +use nostrdb::{Config, Ndb, Transaction}; use std::time::Duration; use lru::LruCache; @@ -29,84 +31,21 @@ mod nip19; mod pfp; mod render; +use crate::secp256k1::XOnlyPublicKey; + type ImageCache = LruCache; #[derive(Clone)] pub struct Notecrumbs { - ndb: Ndb, + pub ndb: Ndb, keys: Keys, font_data: egui::FontData, - img_cache: Arc, + _img_cache: Arc, default_pfp: egui::ImageData, background: egui::ImageData, /// How long do we wait for remote note requests - timeout: Duration, -} - -pub struct FindNoteResult { - note: Option, - profile: Option, -} - -pub async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result { - let opts = Options::new().shutdown_on_drop(true); - let client = Client::with_opts(&app.keys, opts); - - let mut num_relays: i32 = 2; - let _ = client.add_relay("wss://relay.damus.io"); - let _ = client.add_relay("wss://relay.nostr.band").await; - - let other_relays = nip19::to_relays(nip19); - for relay in other_relays { - let _ = client.add_relay(relay).await; - num_relays += 1; - } - - client.connect().await; - - let filters = nip19::to_filters(nip19)?; - - client - .req_events_of(filters.clone(), Some(app.timeout)) - .await; - - let mut note: Option = None; - let mut profile: Option = None; - let mut ends: i32 = 0; - - loop { - match client.notifications().recv().await? { - RelayPoolNotification::Event { event, .. } => { - debug!("got event 1 {:?}", event); - note = Some(event); - return Ok(FindNoteResult { note, profile }); - } - RelayPoolNotification::RelayStatus { .. } => continue, - RelayPoolNotification::Message { message, .. } => match message { - RelayMessage::Event { event, .. } => { - if event.kind == Kind::Metadata { - debug!("got profile {:?}", event); - profile = Some(*event); - } else { - debug!("got event {:?}", event); - note = Some(*event); - } - } - RelayMessage::EndOfStoredEvents(_) => { - ends += 1; - let has_any = note.is_some() || profile.is_some(); - if has_any || ends >= num_relays { - return Ok(FindNoteResult { note, profile }); - } - } - _ => continue, - }, - RelayPoolNotification::Stop | RelayPoolNotification::Shutdown => { - return Err(Error::NotFound); - } - } - } + _timeout: Duration, } #[inline] @@ -132,12 +71,45 @@ fn is_utf8_char_boundary(c: u8) -> bool { fn serve_profile_html( app: &Notecrumbs, - nip: &Nip19, - profile: &render::ProfileRenderData, - r: Request, + _nip: &Nip19, + profile_rd: Option<&ProfileRenderData>, + _r: Request, ) -> Result>, Error> { let mut data = Vec::new(); - write!(data, "TODO: profile pages\n"); + + let profile_key = match profile_rd { + None | Some(ProfileRenderData::Missing(_)) => { + let _ = write!(data, "Profile not found :("); + return Ok(Response::builder() + .header(header::CONTENT_TYPE, "text/html") + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::from(data)))?); + } + + Some(ProfileRenderData::Profile(profile_key)) => *profile_key, + }; + + let txn = Transaction::new(&app.ndb)?; + + let profile_rec = if let Ok(profile_rec) = app.ndb.get_profile_by_key(&txn, profile_key) { + profile_rec + } else { + let _ = write!(data, "Profile not found :("); + return Ok(Response::builder() + .header(header::CONTENT_TYPE, "text/html") + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::from(data)))?); + }; + + let _ = write!( + data, + "{}", + profile_rec + .record() + .profile() + .and_then(|p| p.name()) + .unwrap_or("nostrich") + ); Ok(Response::builder() .header(header::CONTENT_TYPE, "text/html") @@ -163,22 +135,32 @@ async fn serve( }; // render_data is always returned, it just might be empty - let partial_render_data = match render::get_render_data(&app, &nip19) { - Err(_err) => { - return Ok(Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Full::new(Bytes::from( - "nsecs are not supported, what were you thinking!?\n", - )))?); + let mut render_data = { + let txn = Transaction::new(&app.ndb)?; + match render::get_render_data(&app.ndb, &txn, &nip19) { + Err(_err) => { + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::new(Bytes::from( + "nsecs are not supported, what were you thinking!?\n", + )))?); + } + Ok(render_data) => render_data, } - Ok(render_data) => render_data, }; // fetch extra data if we are missing it - let render_data = partial_render_data.complete(&app, &nip19).await; + if !render_data.is_complete() { + if let Err(err) = render_data + .complete(app.ndb.clone(), app.keys.clone(), nip19.clone()) + .await + { + error!("Error fetching completion data: {err}"); + } + } if is_png { - let data = render::render_note(&app, &render_data); + let data = render::render_note(app, &render_data); Ok(Response::builder() .header(header::CONTENT_TYPE, "image/png") @@ -187,7 +169,9 @@ async fn serve( } else { match render_data { RenderData::Note(note_rd) => html::serve_note_html(app, &nip19, ¬e_rd, r), - RenderData::Profile(profile_rd) => serve_profile_html(app, &nip19, &profile_rd, r), + RenderData::Profile(profile_rd) => { + serve_profile_html(app, &nip19, profile_rd.as_ref(), r) + } } } } @@ -232,7 +216,7 @@ fn get_gradient() -> egui::ColorImage { fn get_default_pfp() -> egui::ColorImage { let img = std::fs::read("assets/default_pfp.jpg").expect("default pfp missing"); - let mut dyn_image = image::load_from_memory(&img).expect("failed to load default pfp"); + let mut dyn_image = ::image::load_from_memory(&img).expect("failed to load default pfp"); pfp::process_pfp_bitmap(&mut dyn_image) } @@ -247,7 +231,7 @@ async fn main() -> Result<(), Box> { info!("Listening on 0.0.0.0:3000"); // Since ndk-sdk will verify for us, we don't need to do it on the db side - let mut cfg = Config::new(); + let cfg = Config::new(); cfg.skip_validation(true); let ndb = Ndb::new(".", &cfg).expect("ndb failed to open"); let keys = Keys::generate(); @@ -260,8 +244,8 @@ async fn main() -> Result<(), Box> { let app = Notecrumbs { ndb, keys, - timeout, - img_cache, + _timeout: timeout, + _img_cache: img_cache, background, font_data, default_pfp, diff --git a/src/nip19.rs b/src/nip19.rs index d89a2ba..8848bb0 100644 --- a/src/nip19.rs +++ b/src/nip19.rs @@ -1,36 +1,16 @@ -use crate::error::Error; -use nostr_sdk::nips::nip19::Nip19; +use nostr::nips::nip19::Nip19; use nostr_sdk::prelude::*; -pub fn to_filters(nip19: &Nip19) -> Result, Error> { +/// Do we have relays for this request? If so we can use these when +/// looking for missing data +pub fn nip19_relays(nip19: &Nip19) -> Vec { match nip19 { - Nip19::Event(ev) => { - let mut filters = vec![Filter::new().id(ev.event_id).limit(1)]; - if let Some(author) = ev.author { - filters.push(Filter::new().author(author).kind(Kind::Metadata).limit(1)) - } - Ok(filters) - } - Nip19::EventId(evid) => Ok(vec![Filter::new().id(*evid).limit(1)]), - Nip19::Profile(prof) => Ok(vec![Filter::new() - .author(prof.public_key) - .kind(Kind::Metadata) - .limit(1)]), - Nip19::Pubkey(pk) => Ok(vec![Filter::new() - .author(*pk) - .kind(Kind::Metadata) - .limit(1)]), - Nip19::Secret(_sec) => Err(Error::InvalidNip19), - Nip19::Coordinate(_coord) => Err(Error::InvalidNip19), + Nip19::Event(ev) => ev + .relays + .iter() + .filter_map(|r| RelayUrl::parse(r).ok()) + .collect(), + Nip19::Profile(p) => p.relays.clone(), + _ => vec![], } } - -pub fn to_relays(nip19: &Nip19) -> Vec { - let mut relays: Vec = vec![]; - match nip19 { - Nip19::Event(ev) => relays.extend(ev.relays.clone()), - Nip19::Profile(p) => relays.extend(p.relays.clone()), - _ => (), - } - relays -} diff --git a/src/pfp.rs b/src/pfp.rs index e486fa2..239e087 100644 --- a/src/pfp.rs +++ b/src/pfp.rs @@ -8,9 +8,6 @@ pub const PFP_SIZE: u32 = 64; // Thank to gossip for this one! pub fn round_image(image: &mut ColorImage) { - #[cfg(feature = "profiling")] - puffin::profile_function!(); - // The radius to the edge of of the avatar circle let edge_radius = image.size[0] as f32 / 2.0; let edge_radius_squared = edge_radius * edge_radius; @@ -55,9 +52,6 @@ pub fn round_image(image: &mut ColorImage) { } pub fn process_pfp_bitmap(image: &mut image::DynamicImage) -> ColorImage { - #[cfg(features = "profiling")] - puffin::profile_function!(); - let size = PFP_SIZE; // Crop square @@ -83,7 +77,7 @@ pub fn process_pfp_bitmap(image: &mut image::DynamicImage) -> ColorImage { color_image } -async fn fetch_url(url: &str) -> Result<(Vec, hyper::Response), Error> { +async fn _fetch_url(url: &str) -> Result<(Vec, hyper::Response), Error> { use http_body_util::BodyExt; use http_body_util::Empty; use hyper::Request; @@ -131,20 +125,17 @@ async fn fetch_url(url: &str) -> Result<(Vec, hyper::Response), Er Ok((data, res)) } -pub async fn fetch_pfp(url: &str) -> Result { - let (data, res) = fetch_url(url).await?; - parse_img_response(data, res) +pub async fn _fetch_pfp(url: &str) -> Result { + let (data, res) = _fetch_url(url).await?; + _parse_img_response(data, res) } -fn parse_img_response( +fn _parse_img_response( data: Vec, response: hyper::Response, ) -> Result { use egui_extras::image::FitTo; - #[cfg(feature = "profiling")] - puffin::profile_function!(); - let content_type = response.headers()["content-type"] .to_str() .unwrap_or_default(); @@ -152,18 +143,11 @@ fn parse_img_response( let size = PFP_SIZE; if content_type.starts_with("image/svg") { - #[cfg(feature = "profiling")] - puffin::profile_scope!("load_svg"); - - let mut color_image = egui_extras::image::load_svg_bytes_with_size( - &data, - FitTo::Size(size as u32, size as u32), - )?; + let mut color_image = + egui_extras::image::load_svg_bytes_with_size(&data, FitTo::Size(size, size))?; round_image(&mut color_image); Ok(color_image) } else if content_type.starts_with("image/") { - #[cfg(feature = "profiling")] - puffin::profile_scope!("load_from_memory"); let mut dyn_image = image::load_from_memory(&data)?; Ok(process_pfp_bitmap(&mut dyn_image)) } else { diff --git a/src/render.rs b/src/render.rs index a3abfec..2e12429 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,4 +1,4 @@ -use crate::{abbrev::abbrev_str, fonts, Error, Notecrumbs}; +use crate::{abbrev::abbrev_str, error::Result, fonts, nip19, Error, Notecrumbs}; use egui::epaint::Shadow; use egui::{ pos2, @@ -6,311 +6,453 @@ use egui::{ Color32, FontFamily, FontId, Mesh, Rect, RichText, Rounding, Shape, TextureHandle, Vec2, Visuals, }; -use log::{debug, info, warn}; +use log::{debug, error, warn}; +use nostr::event::kind::Kind; +use nostr::types::{SingleLetterTag, Timestamp}; +use nostr_sdk::async_utility::futures_util::StreamExt; use nostr_sdk::nips::nip19::Nip19; -use nostr_sdk::prelude::{json, Event, EventId, Nip19Event, XOnlyPublicKey}; -use nostrdb::{Block, BlockType, Blocks, Mention, Ndb, Note, Transaction}; +use nostr_sdk::prelude::{Client, EventId, Keys, PublicKey}; +use nostrdb::{ + Block, BlockType, Blocks, FilterElement, FilterField, Mention, Ndb, Note, NoteKey, ProfileKey, + ProfileRecord, Transaction, +}; +use std::collections::{BTreeMap, BTreeSet}; +use tokio::time::{timeout, Duration}; const PURPLE: Color32 = Color32::from_rgb(0xcc, 0x43, 0xc5); -//use egui::emath::Rot2; -//use std::f32::consts::PI; +pub enum NoteRenderData { + Missing([u8; 32]), + Note(NoteKey), +} -impl ProfileRenderData { - pub fn default(pfp: egui::ImageData) -> Self { - ProfileRenderData { - name: "nostrich".to_string(), - display_name: None, - about: "A am a nosy nostrich".to_string(), - pfp_url: "https://damus.io/img/no-profile.svg".to_owned(), - pfp: pfp, +impl NoteRenderData { + pub fn needs_note(&self) -> bool { + match self { + NoteRenderData::Missing(_) => true, + NoteRenderData::Note(_) => false, } } -} -#[derive(Debug, Clone)] -pub struct NoteData { - pub id: Option<[u8; 32]>, - pub content: String, - pub timestamp: u64, + pub fn lookup<'a>( + &self, + txn: &'a Transaction, + ndb: &Ndb, + ) -> std::result::Result, nostrdb::Error> { + match self { + NoteRenderData::Missing(note_id) => ndb.get_note_by_id(txn, note_id), + NoteRenderData::Note(note_key) => ndb.get_note_by_key(txn, *note_key), + } + } } -pub struct ProfileRenderData { - pub name: String, - pub display_name: Option, - pub about: String, - pub pfp_url: String, - pub pfp: egui::ImageData, +pub struct NoteAndProfileRenderData { + pub note_rd: NoteRenderData, + pub profile_rd: Option, } -pub struct NoteRenderData { - pub note: NoteData, - pub profile: ProfileRenderData, +impl NoteAndProfileRenderData { + pub fn new(note_rd: NoteRenderData, profile_rd: Option) -> Self { + Self { + note_rd, + profile_rd, + } + } } -pub struct PartialNoteRenderData { - pub note: Option, - pub profile: Option, +pub enum ProfileRenderData { + Missing([u8; 32]), + Profile(ProfileKey), } -pub enum PartialRenderData { - Note(PartialNoteRenderData), - Profile(Option), +impl ProfileRenderData { + pub fn lookup<'a>( + &self, + txn: &'a Transaction, + ndb: &Ndb, + ) -> std::result::Result, nostrdb::Error> { + match self { + ProfileRenderData::Missing(pk) => ndb.get_profile_by_pubkey(txn, pk), + ProfileRenderData::Profile(key) => ndb.get_profile_by_key(txn, *key), + } + } + + pub fn needs_profile(&self) -> bool { + match self { + ProfileRenderData::Missing(_) => true, + ProfileRenderData::Profile(_) => false, + } + } } +/// Primary keys for the data we're interested in rendering pub enum RenderData { - Note(NoteRenderData), - Profile(ProfileRenderData), + Profile(Option), + Note(NoteAndProfileRenderData), } -#[derive(Debug)] -pub enum EventSource { - Nip19(Nip19Event), - Id(EventId), -} +impl RenderData { + pub fn note(note_rd: NoteRenderData, profile_rd: Option) -> Self { + Self::Note(NoteAndProfileRenderData::new(note_rd, profile_rd)) + } + + pub fn profile(profile_rd: Option) -> Self { + Self::Profile(profile_rd) + } -impl EventSource { - fn id(&self) -> EventId { + pub fn is_complete(&self) -> bool { + !(self.needs_profile() || self.needs_note()) + } + + pub fn note_render_data(&self) -> Option<&NoteRenderData> { match self { - EventSource::Nip19(ev) => ev.event_id, - EventSource::Id(id) => *id, + Self::Note(nrd) => Some(&nrd.note_rd), + Self::Profile(_) => None, } } - fn author(&self) -> Option { + pub fn profile_render_data(&self) -> Option<&ProfileRenderData> { match self { - EventSource::Nip19(ev) => ev.author, - EventSource::Id(_) => None, + Self::Note(nrd) => nrd.profile_rd.as_ref(), + Self::Profile(prd) => prd.as_ref(), } } -} -impl From for EventSource { - fn from(event: Nip19Event) -> EventSource { - EventSource::Nip19(event) + pub fn needs_profile(&self) -> bool { + match self { + RenderData::Profile(profile_rd) => profile_rd + .as_ref() + .map(|prd| prd.needs_profile()) + .unwrap_or(true), + RenderData::Note(note) => note + .profile_rd + .as_ref() + .map(|prd| prd.needs_profile()) + .unwrap_or(true), + } } -} -impl From for EventSource { - fn from(event_id: EventId) -> EventSource { - EventSource::Id(event_id) + pub fn needs_note(&self) -> bool { + match self { + RenderData::Profile(_pkey) => false, + RenderData::Note(rd) => rd.note_rd.needs_note(), + } } } -impl NoteData { - fn default() -> Self { - let content = "".to_string(); - let timestamp = 0; - NoteData { - content, - timestamp, - id: None, - } +fn renderdata_to_filter(render_data: &RenderData) -> Vec { + if render_data.is_complete() { + return vec![]; } -} -impl PartialRenderData { - pub async fn complete(self, app: &Notecrumbs, nip19: &Nip19) -> RenderData { - match self { - PartialRenderData::Note(partial) => { - RenderData::Note(partial.complete(app, nip19).await) - } + let mut filters = Vec::with_capacity(2); - PartialRenderData::Profile(Some(profile)) => RenderData::Profile(profile), + match render_data.note_render_data() { + Some(NoteRenderData::Missing(note_id)) => { + filters.push(nostrdb::Filter::new().ids([note_id]).limit(1).build()); + } + None | Some(NoteRenderData::Note(_)) => {} + } - PartialRenderData::Profile(None) => { - warn!("TODO: implement profile data completion"); - RenderData::Profile(ProfileRenderData::default(app.default_pfp.clone())) - } + match render_data.profile_render_data() { + Some(ProfileRenderData::Missing(pubkey)) => { + filters.push( + nostrdb::Filter::new() + .authors([pubkey]) + .kinds([0]) + .limit(1) + .build(), + ); } + None | Some(ProfileRenderData::Profile(_)) => {} } + + filters } -impl PartialNoteRenderData { - pub async fn complete(self, app: &Notecrumbs, nip19: &Nip19) -> NoteRenderData { - // we have everything, all done! - match (self.note, self.profile) { - (Some(note), Some(profile)) => { - return NoteRenderData { note, profile }; +fn convert_filter(ndb_filter: &nostrdb::Filter) -> nostr::types::Filter { + let mut filter = nostr::types::Filter::new(); + + for element in ndb_filter { + match element { + FilterField::Ids(id_elems) => { + let event_ids = id_elems + .into_iter() + .map(|id| EventId::from_slice(id).expect("event id")); + filter = filter.ids(event_ids); } - // Don't hold ourselves up on profile data for notes. We can spin - // off a background task to find the profile though. - (Some(note), None) => { - warn!("TODO: spin off profile query when missing note profile"); - let profile = ProfileRenderData::default(app.default_pfp.clone()); - return NoteRenderData { note, profile }; + FilterField::Authors(authors) => { + let authors = authors + .into_iter() + .map(|id| PublicKey::from_slice(id).expect("ok")); + filter = filter.authors(authors); } - _ => (), - } + FilterField::Kinds(int_elems) => { + let kinds = int_elems.into_iter().map(|knd| Kind::from_u16(knd as u16)); + filter = filter.kinds(kinds); + } - debug!("Finding {:?}", nip19); - - match crate::find_note(app, &nip19).await { - Ok(note_res) => { - let note = match note_res.note { - Some(note) => { - debug!("saving {:?} to nostrdb", ¬e); - let _ = app - .ndb - .process_event(&json!(["EVENT", "s", note]).to_string()); - sdk_note_to_note_data(¬e) - } - None => NoteData::default(), + FilterField::Tags(chr, tag_elems) => { + let single_letter = if let Ok(single) = SingleLetterTag::from_char(chr) { + single + } else { + warn!("failed to adding char filter element: '{}", chr); + continue; }; - let profile = match note_res.profile { - Some(profile) => { - debug!("saving profile to nostrdb: {:?}", &profile); - let _ = app - .ndb - .process_event(&json!(["EVENT", "s", profile]).to_string()); - // TODO: wire profile to profile data, download pfp - ProfileRenderData::default(app.default_pfp.clone()) + let mut tags: BTreeMap> = BTreeMap::new(); + let mut elems: BTreeSet = BTreeSet::new(); + + for elem in tag_elems { + if let FilterElement::Str(s) = elem { + elems.insert(s.to_string()); + } else { + warn!( + "not adding non-string element from filter tag '{}", + single_letter + ); } - None => ProfileRenderData::default(app.default_pfp.clone()), - }; + } + + tags.insert(single_letter, elems); - NoteRenderData { note, profile } + filter.generic_tags = tags; } - Err(_err) => { - let note = NoteData::default(); - let profile = ProfileRenderData::default(app.default_pfp.clone()); - NoteRenderData { note, profile } + + FilterField::Since(since) => { + filter.since = Some(Timestamp::from_secs(since)); + } + + FilterField::Until(until) => { + filter.until = Some(Timestamp::from_secs(until)); + } + + FilterField::Limit(limit) => { + filter.limit = Some(limit as usize); } } } -} -fn get_profile_render_data( - txn: &Transaction, - app: &Notecrumbs, - pubkey: &XOnlyPublicKey, -) -> Result { - let profile = app.ndb.get_profile_by_pubkey(&txn, &pubkey.serialize())?; - info!("profile cache hit {:?}", pubkey); - - let profile = profile.record.profile().ok_or(nostrdb::Error::NotFound)?; - let name = profile.name().unwrap_or("").to_string(); - let about = profile.about().unwrap_or("").to_string(); - let display_name = profile.display_name().as_ref().map(|a| a.to_string()); - let pfp = app.default_pfp.clone(); - let pfp_url = profile - .picture() - .unwrap_or("https://damus.io/img/no-profile.svg") - .to_string(); - - Ok(ProfileRenderData { - name, - pfp, - about, - pfp_url, - display_name, - }) + filter } -fn ndb_note_to_data(note: &Note) -> NoteData { - let content = note.content().to_string(); - let id = Some(*note.id()); - let timestamp = note.created_at(); - NoteData { - content, - timestamp, - id, +pub async fn find_note( + ndb: Ndb, + keys: Keys, + filters: Vec, + nip19: &Nip19, +) -> Result<()> { + use nostr_sdk::JsonUtil; + + let client = Client::builder().signer(keys).build(); + + 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 other_relays = nip19::nip19_relays(nip19); + for relay in other_relays { + let _ = client.add_relay(relay).await; } -} -fn sdk_note_to_note_data(note: &Event) -> NoteData { - let content = note.content.clone(); - let timestamp = note.created_at.as_u64(); - NoteData { - content, - timestamp, - id: Some(note.id.to_bytes()), + client + .connect_with_timeout(std::time::Duration::from_millis(800)) + .await; + + debug!("finding note(s) with filters: {:?}", filters); + + let mut streamed_events = client + .stream_events(filters, Some(std::time::Duration::from_millis(800))) + .await?; + + 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}"); + } } + + Ok(()) } -fn get_note_render_data( - app: &Notecrumbs, - source: &EventSource, -) -> Result { - debug!("got here a"); - let txn = Transaction::new(&app.ndb)?; - let m_note = app - .ndb - .get_note_by_id(&txn, source.id().as_bytes().try_into()?) - .map_err(Error::Nostrdb); +impl RenderData { + fn set_profile_key(&mut self, key: ProfileKey) { + match self { + RenderData::Profile(pk) => { + *pk = Some(ProfileRenderData::Profile(key)); + } + RenderData::Note(note_rd) => { + note_rd.profile_rd = Some(ProfileRenderData::Profile(key)); + } + }; + } - debug!("note cached? {:?}", m_note); + fn set_note_key(&mut self, key: NoteKey) { + match self { + RenderData::Profile(_pk) => {} + RenderData::Note(note) => { + note.note_rd = NoteRenderData::Note(key); + } + }; + } - // It's possible we have an author pk in an nevent, let's use it if we do. - // This gives us the opportunity to load the profile picture earlier if we - // have a cached profile - let mut profile: Option = None; + pub async fn complete(&mut self, mut ndb: Ndb, keys: Keys, nip19: Nip19) -> Result<()> { + let (mut stream, sub_id) = { + let filter = renderdata_to_filter(self); + if filter.is_empty() { + // should really never happen unless someone broke + // needs_note and needs_profile + return Err(Error::NothingToFetch); + } + let sub_id = ndb.subscribe(&filter)?; - let m_note_pk = m_note - .as_ref() - .ok() - .and_then(|n| XOnlyPublicKey::from_slice(n.pubkey()).ok()); - - let m_pk = m_note_pk.or(source.author()); - - // get profile render data if we can - if let Some(pk) = m_pk { - match get_profile_render_data(&txn, app, &pk) { - Err(err) => warn!( - "No profile found for {} for note {}: {}", - &pk, - &source.id(), - err - ), - Ok(record) => { - debug!("profile record found for note"); - profile = Some(record); + let stream = sub_id.stream(&ndb).notes_per_await(2); + + let filters = filter.iter().map(convert_filter).collect(); + let ndb = ndb.clone(); + tokio::spawn(async move { find_note(ndb, keys, filters, &nip19).await }); + (stream, sub_id) + }; + + let wait_for = Duration::from_secs(1); + let mut loops = 0; + + loop { + if loops == 2 { + break; + } + + let note_keys = if let Some(note_keys) = timeout(wait_for, stream.next()).await? { + note_keys + } else { + // end of stream? + break; + }; + + let note_keys_len = note_keys.len(); + + { + let txn = Transaction::new(&ndb)?; + + for note_key in note_keys { + let note = if let Ok(note) = ndb.get_note_by_key(&txn, note_key) { + note + } else { + error!("race condition in RenderData::complete?"); + continue; + }; + + if note.kind() == 0 { + if let Ok(profile_key) = ndb.get_profilekey_by_pubkey(&txn, note.pubkey()) { + self.set_profile_key(profile_key); + } + } else { + self.set_note_key(note_key); + } + } + } + + if note_keys_len >= 2 { + break; } + + loops += 1; } - } - let note = m_note.map(|n| ndb_note_to_data(&n)).ok(); - Ok(PartialNoteRenderData { profile, note }) + if let Err(err) = ndb.unsubscribe(sub_id) { + error!("error unsubscribing: {err}"); + } + Ok(()) + } } -pub fn get_render_data(app: &Notecrumbs, target: &Nip19) -> Result { - match target { - Nip19::Profile(profile) => { - let txn = Transaction::new(&app.ndb)?; - Ok(PartialRenderData::Profile( - get_profile_render_data(&txn, app, &profile.public_key).ok(), - )) +/// Attempt to locate the render data locally. Anything missing from +/// render data will be fetched. +pub fn get_render_data(ndb: &Ndb, txn: &Transaction, nip19: &Nip19) -> Result { + match nip19 { + Nip19::Event(nevent) => { + let m_note = ndb.get_note_by_id(txn, nevent.event_id.as_bytes()).ok(); + + let pk = if let Some(pk) = m_note.as_ref().map(|note| note.pubkey()) { + Some(*pk) + } else { + nevent.author.map(|a| a.serialize()) + }; + + let profile_rd = pk.as_ref().map(|pubkey| { + if let Ok(profile_key) = ndb.get_profilekey_by_pubkey(txn, pubkey) { + ProfileRenderData::Profile(profile_key) + } else { + ProfileRenderData::Missing(*pubkey) + } + }); + + let note_rd = if let Some(note_key) = m_note.and_then(|n| n.key()) { + NoteRenderData::Note(note_key) + } else { + NoteRenderData::Missing(*nevent.event_id.as_bytes()) + }; + + Ok(RenderData::note(note_rd, profile_rd)) } - Nip19::Pubkey(pk) => { - let txn = Transaction::new(&app.ndb)?; - Ok(PartialRenderData::Profile( - get_profile_render_data(&txn, app, pk).ok(), - )) + Nip19::EventId(evid) => { + let m_note = ndb.get_note_by_id(txn, evid.as_bytes()).ok(); + let note_key = m_note.as_ref().and_then(|n| n.key()); + let pk = m_note.map(|note| note.pubkey()); + + let profile_rd = pk.map(|pubkey| { + if let Ok(profile_key) = ndb.get_profilekey_by_pubkey(txn, pubkey) { + ProfileRenderData::Profile(profile_key) + } else { + ProfileRenderData::Missing(*pubkey) + } + }); + + let note_rd = if let Some(note_key) = note_key { + NoteRenderData::Note(note_key) + } else { + NoteRenderData::Missing(*evid.as_bytes()) + }; + + Ok(RenderData::note(note_rd, profile_rd)) } - Nip19::Event(event) => Ok(PartialRenderData::Note(get_note_render_data( - app, - &EventSource::Nip19(event.clone()), - )?)), + Nip19::Profile(nprofile) => { + let pubkey = nprofile.public_key.serialize(); + let profile_rd = if let Ok(profile_key) = ndb.get_profilekey_by_pubkey(txn, &pubkey) { + ProfileRenderData::Profile(profile_key) + } else { + ProfileRenderData::Missing(pubkey) + }; - Nip19::EventId(evid) => Ok(PartialRenderData::Note(get_note_render_data( - app, - &EventSource::Id(*evid), - )?)), + Ok(RenderData::profile(Some(profile_rd))) + } - Nip19::Secret(_nsec) => Err(Error::InvalidNip19), - Nip19::Coordinate(_coord) => Err(Error::InvalidNip19), + Nip19::Pubkey(public_key) => { + let pubkey = public_key.serialize(); + let profile_rd = if let Ok(profile_key) = ndb.get_profilekey_by_pubkey(txn, &pubkey) { + ProfileRenderData::Profile(profile_key) + } else { + ProfileRenderData::Missing(pubkey) + }; + + Ok(RenderData::profile(Some(profile_rd))) + } + + _ => Err(Error::CantRender), } } -fn render_username(ui: &mut egui::Ui, profile: &ProfileRenderData) { - #[cfg(feature = "profiling")] - puffin::profile_function!(); - let name = format!("@{}", profile.name); +fn render_username(ui: &mut egui::Ui, profile: Option<&ProfileRecord>) { + let name = format!( + "@{}", + profile + .and_then(|pr| pr.record().profile().and_then(|p| p.name())) + .unwrap_or("nostrich") + ); ui.label(RichText::new(&name).size(40.0).color(Color32::LIGHT_GRAY)); } @@ -340,9 +482,9 @@ fn push_job_user_mention( txn: &Transaction, pk: &[u8; 32], ) { - let record = ndb.get_profile_by_pubkey(&txn, pk); + let record = ndb.get_profile_by_pubkey(txn, pk); if let Ok(record) = record { - let profile = record.record.profile().unwrap(); + let profile = record.record().profile().unwrap(); push_job_text( job, &format!("@{}", &abbrev_str(profile.name().unwrap_or("nostrich"))), @@ -360,13 +502,15 @@ fn wrapped_body_blocks( blocks: &Blocks, txn: &Transaction, ) { - let mut job = LayoutJob::default(); - job.justify = false; - job.halign = egui::Align::LEFT; - job.wrap = egui::text::TextWrapping { - max_rows: 5, - break_anywhere: false, - overflow_character: Some('…'), + let mut job = LayoutJob { + justify: false, + halign: egui::Align::LEFT, + wrap: egui::text::TextWrapping { + max_rows: 5, + break_anywhere: false, + overflow_character: Some('…'), + ..Default::default() + }, ..Default::default() }; @@ -380,7 +524,7 @@ fn wrapped_body_blocks( } BlockType::MentionBech32 => { - let pk = match block.as_mention().unwrap() { + match block.as_mention().unwrap() { Mention::Event(_ev) => push_job_text( &mut job, &format!("@{}", &abbrev_str(block.as_str())), @@ -394,16 +538,16 @@ fn wrapped_body_blocks( ); } Mention::Profile(nprofile) => { - push_job_user_mention(&mut job, ndb, &block, &txn, nprofile.pubkey()) + push_job_user_mention(&mut job, ndb, &block, txn, nprofile.pubkey()) } Mention::Pubkey(npub) => { - push_job_user_mention(&mut job, ndb, &block, &txn, npub.pubkey()) + push_job_user_mention(&mut job, ndb, &block, txn, npub.pubkey()) } - Mention::Secret(sec) => push_job_text(&mut job, "--redacted--", PURPLE), - Mention::Relay(relay) => { + Mention::Secret(_sec) => push_job_text(&mut job, "--redacted--", PURPLE), + Mention::Relay(_relay) => { push_job_text(&mut job, &abbrev_str(block.as_str()), PURPLE) } - Mention::Addr(addr) => { + Mention::Addr(_addr) => { push_job_text(&mut job, &abbrev_str(block.as_str()), PURPLE) } }; @@ -455,7 +599,7 @@ fn note_frame_align() -> egui::Layout { } } -fn note_ui(app: &Notecrumbs, ctx: &egui::Context, note: &NoteRenderData) { +fn note_ui(app: &Notecrumbs, ctx: &egui::Context, rd: &NoteAndProfileRenderData) -> Result<()> { setup_visuals(&app.font_data, ctx); let outer_margin = 60.0; @@ -465,7 +609,19 @@ fn note_ui(app: &Notecrumbs, ctx: &egui::Context, note: &NoteRenderData) { //let canvas_size = Vec2::new(canvas_width, canvas_height); let total_margin = outer_margin + inner_margin; - let pfp = ctx.load_texture("pfp", note.profile.pfp.clone(), Default::default()); + let txn = Transaction::new(&app.ndb)?; + let profile_record = rd + .profile_rd + .as_ref() + .and_then(|profile_rd| match profile_rd { + ProfileRenderData::Missing(pk) => app.ndb.get_profile_by_pubkey(&txn, pk).ok(), + ProfileRenderData::Profile(key) => app.ndb.get_profile_by_key(&txn, *key).ok(), + }); + //let _profile = profile_record.and_then(|pr| pr.record().profile()); + //let pfp_url = profile.and_then(|p| p.picture()); + + // TODO: async pfp loading using notedeck browser context? + let pfp = ctx.load_texture("pfp", app.default_pfp.clone(), Default::default()); let bg = ctx.load_texture("background", app.background.clone(), Default::default()); egui::CentralPanel::default() @@ -474,7 +630,7 @@ fn note_ui(app: &Notecrumbs, ctx: &egui::Context, note: &NoteRenderData) { //.fill(Color32::from_rgb(0x43, 0x20, 0x62) .fill(Color32::from_rgb(0x00, 0x00, 0x00)), ) - .show(&ctx, |ui| { + .show(ctx, |ui| { background_texture(ui, &bg); egui::Frame::none() .fill(Color32::from_rgb(0x0F, 0x0F, 0x0F)) @@ -500,31 +656,28 @@ fn note_ui(app: &Notecrumbs, ctx: &egui::Context, note: &NoteRenderData) { ui.set_max_size(desired); ui.set_min_size(desired); - let ok = (|| -> Result<(), nostrdb::Error> { - let txn = Transaction::new(&app.ndb)?; - let note_id = note.note.id.ok_or(nostrdb::Error::NotFound)?; - let note = app.ndb.get_note_by_id(&txn, ¬e_id)?; - let blocks = - app.ndb.get_blocks_by_key(&txn, note.key().unwrap())?; - - wrapped_body_blocks(ui, &app.ndb, ¬e, &blocks, &txn); - - Ok(()) - })(); - - if let Err(_) = ok { - wrapped_body_text(ui, ¬e.note.content); + if let Ok(note) = rd.note_rd.lookup(&txn, &app.ndb) { + if let Some(blocks) = note + .key() + .and_then(|nk| app.ndb.get_blocks_by_key(&txn, nk).ok()) + { + wrapped_body_blocks(ui, &app.ndb, ¬e, &blocks, &txn); + } else { + wrapped_body_text(ui, note.content()); + } } }); ui.horizontal(|ui| { ui.image(&pfp); - render_username(ui, ¬e.profile); + render_username(ui, profile_record.as_ref()); ui.with_layout(right_aligned(), discuss_on_damus); }); }); }); }); + + Ok(()) } fn background_texture(ui: &mut egui::Ui, texture: &TextureHandle) { @@ -572,22 +725,25 @@ fn discuss_on_damus(ui: &mut egui::Ui) { ui.add(button); } -fn profile_ui(app: &Notecrumbs, ctx: &egui::Context, profile: &ProfileRenderData) { - let pfp = ctx.load_texture("pfp", profile.pfp.clone(), Default::default()); +fn profile_ui(app: &Notecrumbs, ctx: &egui::Context, profile_rd: Option<&ProfileRenderData>) { + let pfp = ctx.load_texture("pfp", app.default_pfp.clone(), Default::default()); setup_visuals(&app.font_data, ctx); - egui::CentralPanel::default().show(&ctx, |ui| { + egui::CentralPanel::default().show(ctx, |ui| { ui.vertical(|ui| { ui.horizontal(|ui| { ui.image(&pfp); - render_username(ui, &profile); + if let Ok(txn) = Transaction::new(&app.ndb) { + let profile = profile_rd.and_then(|prd| prd.lookup(&txn, &app.ndb).ok()); + render_username(ui, profile.as_ref()); + } }); //body(ui, &profile.about); }); }); } -pub fn render_note(app: &Notecrumbs, render_data: &RenderData) -> Vec { +pub fn render_note(ndb: &Notecrumbs, render_data: &RenderData) -> Vec { use egui_skia::{rasterize, RasterizeOptions}; use skia_safe::EncodedImageFormat; @@ -599,13 +755,15 @@ pub fn render_note(app: &Notecrumbs, render_data: &RenderData) -> Vec { let mut surface = match render_data { RenderData::Note(note_render_data) => rasterize( (1200, 600), - |ctx| note_ui(app, ctx, note_render_data), + |ctx| { + let _ = note_ui(ndb, ctx, note_render_data); + }, Some(options), ), - RenderData::Profile(profile_render_data) => rasterize( + RenderData::Profile(profile_rd) => rasterize( (1200, 600), - |ctx| profile_ui(app, ctx, profile_render_data), + |ctx| profile_ui(ndb, ctx, profile_rd.as_ref()), Some(options), ), };