notecrumbs

a nostr opengraph server build on nostrdb and egui
git clone git://jb55.com/notecrumbs
Log | Files | Refs | README

commit 503c4a6e36791b1a3dafec56d3803b21860fcda2
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 16 Dec 2023 17:48:03 -0800

notecrumbs: initial commit

Diffstat:
A.envrc | 1+
A.gitignore | 6++++++
A.rustfmt.toml | 1+
ACargo.toml | 21+++++++++++++++++++++
AREADME.md | 12++++++++++++
Ashell.nix | 5+++++
Asrc/error.rs | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib.rs | 1+
Asrc/main.rs | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 278 insertions(+), 0 deletions(-)

diff --git a/.envrc b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore @@ -0,0 +1,6 @@ +/target +/.direnv +data.mdb +lock.mdb +.build-result +.buildcmd diff --git a/.rustfmt.toml b/.rustfmt.toml @@ -0,0 +1 @@ +edition = "2021" diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "notecrumbs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +#nostrdb = "0.1.2" +hyper = { version = "1", features = ["full"] } +tokio = { version = "1", features = ["full"] } +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" } +hex = "0.4.3" +egui = "0.21.0" +egui_skia = { version = "0.4.0", features = ["cpu_fix"] } +skia-safe = "0.58.0" diff --git a/README.md b/README.md @@ -0,0 +1,12 @@ + +# notecrumbs + +A nostr opengraph server build on [nostrdb][nostrdb], [egui][egui], and +[skia][egui-skia]. It renders notes using the CPU + +WIP! + +[nostrdb]: https://github.com/damus-io/nostrdb +[egui]: https://github.com/emilk/egui +[egui-skia]: https://github.com/lucasmerlin/egui_skia + diff --git a/shell.nix b/shell.nix @@ -0,0 +1,5 @@ +{ pkgs ? import <nixpkgs> {} }: +with pkgs; +mkShell { + nativeBuildInputs = [ gdb cargo rustc rustfmt libiconv pkg-config fontconfig freetype ]; +} diff --git a/src/error.rs b/src/error.rs @@ -0,0 +1,60 @@ +use nostr_sdk::nips::nip19; +use std::array::TryFromSliceError; +use std::error::Error as StdError; +use std::fmt; + +#[derive(Debug)] +pub enum Error { + Nip19(nip19::Error), + Http(hyper::http::Error), + Nostrdb(nostrdb::Error), + SliceErr, +} + +impl From<TryFromSliceError> for Error { + fn from(_: TryFromSliceError) -> Self { + Error::SliceErr + } +} + +impl From<nip19::Error> for Error { + fn from(err: nip19::Error) -> Self { + Error::Nip19(err) + } +} + +impl From<hyper::http::Error> for Error { + fn from(err: hyper::http::Error) -> Self { + Error::Http(err) + } +} + +impl From<nostrdb::Error> for Error { + fn from(err: nostrdb::Error) -> Self { + Error::Nostrdb(err) + } +} + +// Implementing `Display` +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Nip19(e) => write!(f, "Nip19 error: {}", e), + Error::Http(e) => write!(f, "HTTP error: {}", e), + Error::Nostrdb(e) => write!(f, "Nostrdb error: {}", e), + Error::SliceErr => write!(f, "Array slice error"), + } + } +} + +// Implementing `StdError` +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Error::Nip19(e) => Some(e), + Error::Http(e) => Some(e), + Error::Nostrdb(e) => Some(e), + Error::SliceErr => None, + } + } +} diff --git a/src/lib.rs b/src/lib.rs @@ -0,0 +1 @@ + diff --git a/src/main.rs b/src/main.rs @@ -0,0 +1,171 @@ +use std::net::SocketAddr; + +use http_body_util::Full; +use hyper::body::Bytes; +use hyper::header; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Request, Response, StatusCode}; +use hyper_util::rt::TokioIo; +use log::info; +use tokio::net::TcpListener; + +use crate::error::Error; +use nostr_sdk::nips::nip19::Nip19; +use nostr_sdk::prelude::*; +use nostrdb::{Config, Ndb, Note, Transaction}; + +mod error; + +#[derive(Debug, Clone)] +struct Context { + ndb: Ndb, + //font_data: egui::FontData, +} + +fn nip19_evid(nip19: &Nip19) -> Option<EventId> { + match nip19 { + Nip19::Event(ev) => Some(ev.event_id), + Nip19::EventId(evid) => Some(*evid), + _ => None, + } +} + +/* +fn setup_fonts(font_data: &egui::FontData, ctx: &egui::Context) { + let mut fonts = egui::FontDefinitions::default(); + + // Install my own font (maybe supporting non-latin characters). + // .ttf and .otf files supported. + fonts + .font_data + .insert("my_font".to_owned(), font_data.clone()); + + // Put my font first (highest priority) for proportional text: + fonts + .families + .entry(egui::FontFamily::Proportional) + .or_default() + .insert(0, "my_font".to_owned()); + + // Tell egui to use these fonts: + ctx.set_fonts(fonts); +} +*/ + +fn render_note<'a>(_app_ctx: &Context, note: &'a Note) -> Vec<u8> { + use egui::{FontId, RichText}; + use egui_skia::{rasterize, RasterizeOptions}; + use skia_safe::EncodedImageFormat; + + let mut surface = rasterize( + (1200, 630), + |ctx| { + //setup_fonts(&app_ctx.font_data, ctx); + + egui::CentralPanel::default().show(&ctx, |ui| { + 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))); + }); + }) + }); + }, + Some(RasterizeOptions { + pixels_per_point: 1.0, + frames_before_screenshot: 1, + }), + ); + + surface + .image_snapshot() + .encode_to_data(EncodedImageFormat::PNG) + .expect("expected image") + .as_bytes() + .into() +} + +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 evid = match nip19_evid(&nip19) { + Some(evid) => evid, + None => { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::from("\n")))?) + } + }; + + 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) + ))))?); + } + Err(err) => { + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Full::new(Bytes::from(format!("{}\n", err))))?); + } + }; + + let data = render_note(&ctx, &note); + + Ok(Response::builder() + .header(header::CONTENT_TYPE, "image/png") + .body(Full::new(Bytes::from(data)))?) +} + +#[tokio::main] +async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + env_logger::init(); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + + // We create a TcpListener and bind it to 127.0.0.1:3000 + let listener = TcpListener::bind(addr).await?; + info!("Listening on 127.0.0.1:3000"); + + 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 */ + }; + + // We start a loop to continuously accept incoming connections + loop { + let (stream, _) = listener.accept().await?; + + // Use an adapter to access something implementing `tokio::io` traits as if they implement + // `hyper::rt` IO traits. + let io = TokioIo::new(stream); + + let ctx_copy = ctx.clone(); + + // Spawn a tokio task to serve multiple connections concurrently + tokio::task::spawn(async move { + // Finally, we bind the incoming connection to our `hello` service + if let Err(err) = http1::Builder::new() + // `service_fn` converts our function in a `Service` + .serve_connection(io, service_fn(|req| serve(&ctx_copy, req))) + .await + { + println!("Error serving connection: {:?}", err); + } + }); + } +}