commit 503c4a6e36791b1a3dafec56d3803b21860fcda2
Author: William Casarin <jb55@jb55.com>
Date: Sat, 16 Dec 2023 17:48:03 -0800
notecrumbs: initial commit
Diffstat:
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, ¬e);
+
+ 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);
+ }
+ });
+ }
+}