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