main.rs (9260B)
1 use std::net::SocketAddr; 2 3 use http_body_util::Full; 4 use hyper::body::Bytes; 5 use hyper::header; 6 use hyper::server::conn::http1; 7 use hyper::service::service_fn; 8 use hyper::{Request, Response, StatusCode}; 9 use hyper_util::rt::TokioIo; 10 use log::{debug, info}; 11 use std::io::Write; 12 use std::sync::Arc; 13 use tokio::net::TcpListener; 14 15 use crate::error::Error; 16 use crate::render::RenderData; 17 use nostr_sdk::prelude::*; 18 use nostrdb::{Config, Ndb}; 19 use std::time::Duration; 20 21 use lru::LruCache; 22 23 mod abbrev; 24 mod error; 25 mod fonts; 26 mod gradient; 27 mod html; 28 mod nip19; 29 mod pfp; 30 mod render; 31 32 type ImageCache = LruCache<XOnlyPublicKey, egui::TextureHandle>; 33 34 #[derive(Clone)] 35 pub struct Notecrumbs { 36 ndb: Ndb, 37 keys: Keys, 38 font_data: egui::FontData, 39 img_cache: Arc<ImageCache>, 40 default_pfp: egui::ImageData, 41 background: egui::ImageData, 42 43 /// How long do we wait for remote note requests 44 timeout: Duration, 45 } 46 47 pub struct FindNoteResult { 48 note: Option<Event>, 49 profile: Option<Event>, 50 } 51 52 pub async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<FindNoteResult, Error> { 53 let opts = Options::new().shutdown_on_drop(true); 54 let client = Client::with_opts(&app.keys, opts); 55 56 let mut num_relays: i32 = 2; 57 let _ = client.add_relay("wss://relay.damus.io"); 58 let _ = client.add_relay("wss://relay.nostr.band").await; 59 60 let other_relays = nip19::to_relays(nip19); 61 for relay in other_relays { 62 let _ = client.add_relay(relay).await; 63 num_relays += 1; 64 } 65 66 client.connect().await; 67 68 let filters = nip19::to_filters(nip19)?; 69 70 client 71 .req_events_of(filters.clone(), Some(app.timeout)) 72 .await; 73 74 let mut note: Option<Event> = None; 75 let mut profile: Option<Event> = None; 76 let mut ends: i32 = 0; 77 78 loop { 79 match client.notifications().recv().await? { 80 RelayPoolNotification::Event { event, .. } => { 81 debug!("got event 1 {:?}", event); 82 note = Some(event); 83 return Ok(FindNoteResult { note, profile }); 84 } 85 RelayPoolNotification::RelayStatus { .. } => continue, 86 RelayPoolNotification::Message { message, .. } => match message { 87 RelayMessage::Event { event, .. } => { 88 if event.kind == Kind::Metadata { 89 debug!("got profile {:?}", event); 90 profile = Some(*event); 91 } else { 92 debug!("got event {:?}", event); 93 note = Some(*event); 94 } 95 } 96 RelayMessage::EndOfStoredEvents(_) => { 97 ends += 1; 98 let has_any = note.is_some() || profile.is_some(); 99 if has_any || ends >= num_relays { 100 return Ok(FindNoteResult { note, profile }); 101 } 102 } 103 _ => continue, 104 }, 105 RelayPoolNotification::Stop | RelayPoolNotification::Shutdown => { 106 return Err(Error::NotFound); 107 } 108 } 109 } 110 } 111 112 #[inline] 113 pub fn floor_char_boundary(s: &str, index: usize) -> usize { 114 if index >= s.len() { 115 s.len() 116 } else { 117 let lower_bound = index.saturating_sub(3); 118 let new_index = s.as_bytes()[lower_bound..=index] 119 .iter() 120 .rposition(|b| is_utf8_char_boundary(*b)); 121 122 // SAFETY: we know that the character boundary will be within four bytes 123 unsafe { lower_bound + new_index.unwrap_unchecked() } 124 } 125 } 126 127 #[inline] 128 fn is_utf8_char_boundary(c: u8) -> bool { 129 // This is bit magic equivalent to: b < 128 || b >= 192 130 (c as i8) >= -0x40 131 } 132 133 fn serve_profile_html( 134 app: &Notecrumbs, 135 nip: &Nip19, 136 profile: &render::ProfileRenderData, 137 r: Request<hyper::body::Incoming>, 138 ) -> Result<Response<Full<Bytes>>, Error> { 139 let mut data = Vec::new(); 140 write!(data, "TODO: profile pages\n"); 141 142 Ok(Response::builder() 143 .header(header::CONTENT_TYPE, "text/html") 144 .status(StatusCode::OK) 145 .body(Full::new(Bytes::from(data)))?) 146 } 147 148 async fn serve( 149 app: &Notecrumbs, 150 r: Request<hyper::body::Incoming>, 151 ) -> Result<Response<Full<Bytes>>, Error> { 152 let is_png = r.uri().path().ends_with(".png"); 153 let until = if is_png { 4 } else { 0 }; 154 155 let path_len = r.uri().path().len(); 156 let nip19 = match Nip19::from_bech32(&r.uri().path()[1..path_len - until]) { 157 Ok(nip19) => nip19, 158 Err(_) => { 159 return Ok(Response::builder() 160 .status(StatusCode::NOT_FOUND) 161 .body(Full::new(Bytes::from("Invalid url\n")))?); 162 } 163 }; 164 165 // render_data is always returned, it just might be empty 166 let partial_render_data = match render::get_render_data(&app, &nip19) { 167 Err(_err) => { 168 return Ok(Response::builder() 169 .status(StatusCode::BAD_REQUEST) 170 .body(Full::new(Bytes::from( 171 "nsecs are not supported, what were you thinking!?\n", 172 )))?); 173 } 174 Ok(render_data) => render_data, 175 }; 176 177 // fetch extra data if we are missing it 178 let render_data = partial_render_data.complete(&app, &nip19).await; 179 180 if is_png { 181 let data = render::render_note(&app, &render_data); 182 183 Ok(Response::builder() 184 .header(header::CONTENT_TYPE, "image/png") 185 .status(StatusCode::OK) 186 .body(Full::new(Bytes::from(data)))?) 187 } else { 188 match render_data { 189 RenderData::Note(note_rd) => html::serve_note_html(app, &nip19, ¬e_rd, r), 190 RenderData::Profile(profile_rd) => serve_profile_html(app, &nip19, &profile_rd, r), 191 } 192 } 193 } 194 195 fn get_env_timeout() -> Duration { 196 let timeout_env = std::env::var("TIMEOUT_MS").unwrap_or("2000".to_string()); 197 let timeout_ms: u64 = timeout_env.parse().unwrap_or(2000); 198 Duration::from_millis(timeout_ms) 199 } 200 201 fn get_gradient() -> egui::ColorImage { 202 use egui::{Color32, ColorImage}; 203 //use egui::pos2; 204 use gradient::Gradient; 205 206 //let gradient = Gradient::linear(Color32::LIGHT_GRAY, Color32::DARK_GRAY); 207 //let size = pfp::PFP_SIZE as usize; 208 //let radius = (pfp::PFP_SIZE as f32) / 2.0; 209 //let center = pos2(radius, radius); 210 211 let scol = [0x1C, 0x55, 0xFF]; 212 //let ecol = [0xFA, 0x0D, 0xD4]; 213 let mcol = [0x7F, 0x35, 0xAB]; 214 //let ecol = [0xFF, 0x0B, 0xD6]; 215 let ecol = [0xC0, 0x2A, 0xBE]; 216 217 // TODO: skia has r/b colors swapped for some reason, fix this 218 let start_color = Color32::from_rgb(scol[2], scol[1], scol[0]); 219 let mid_color = Color32::from_rgb(mcol[2], mcol[1], mcol[0]); 220 let end_color = Color32::from_rgb(ecol[2], ecol[1], ecol[0]); 221 222 let gradient = Gradient::linear_many(vec![start_color, mid_color, end_color]); 223 let pixels = gradient.to_pixel_row(); 224 let width = pixels.len(); 225 let height = 1; 226 227 ColorImage { 228 size: [width, height], 229 pixels, 230 } 231 } 232 233 fn get_default_pfp() -> egui::ColorImage { 234 let img = std::fs::read("assets/default_pfp.jpg").expect("default pfp missing"); 235 let mut dyn_image = image::load_from_memory(&img).expect("failed to load default pfp"); 236 pfp::process_pfp_bitmap(&mut dyn_image) 237 } 238 239 #[tokio::main] 240 async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 241 env_logger::init(); 242 243 let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); 244 245 // We create a TcpListener and bind it to 127.0.0.1:3000 246 let listener = TcpListener::bind(addr).await?; 247 info!("Listening on 0.0.0.0:3000"); 248 249 // Since ndk-sdk will verify for us, we don't need to do it on the db side 250 let mut cfg = Config::new(); 251 cfg.skip_validation(true); 252 let ndb = Ndb::new(".", &cfg).expect("ndb failed to open"); 253 let keys = Keys::generate(); 254 let timeout = get_env_timeout(); 255 let img_cache = Arc::new(LruCache::new(std::num::NonZeroUsize::new(64).unwrap())); 256 let default_pfp = egui::ImageData::Color(Arc::new(get_default_pfp())); 257 let background = egui::ImageData::Color(Arc::new(get_gradient())); 258 let font_data = egui::FontData::from_static(include_bytes!("../fonts/NotoSans-Regular.ttf")); 259 260 let app = Notecrumbs { 261 ndb, 262 keys, 263 timeout, 264 img_cache, 265 background, 266 font_data, 267 default_pfp, 268 }; 269 270 // We start a loop to continuously accept incoming connections 271 loop { 272 let (stream, _) = listener.accept().await?; 273 274 // Use an adapter to access something implementing `tokio::io` traits as if they implement 275 // `hyper::rt` IO traits. 276 let io = TokioIo::new(stream); 277 278 let app_copy = app.clone(); 279 280 // Spawn a tokio task to serve multiple connections concurrently 281 tokio::task::spawn(async move { 282 // Finally, we bind the incoming connection to our `hello` service 283 if let Err(err) = http1::Builder::new() 284 // `service_fn` converts our function in a `Service` 285 .serve_connection(io, service_fn(|req| serve(&app_copy, req))) 286 .await 287 { 288 println!("Error serving connection: {:?}", err); 289 } 290 }); 291 } 292 }