move around some things, add code for pfps

This commit is contained in:
William Casarin 2023-12-18 08:52:19 -08:00
parent 9cf11d953a
commit 927ba5b137
4 changed files with 192 additions and 87 deletions

View File

@ -8,104 +8,36 @@ use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use log::{debug, info};
use std::sync::Arc;
use tokio::net::TcpListener;
use crate::error::Error;
use nostr_sdk::nips::nip19::Nip19;
use nostr_sdk::prelude::*;
use nostrdb::{Config, Ndb, Transaction};
use std::time::Duration;
use nostr_sdk::Kind;
use lru::LruCache;
mod error;
mod nip19;
mod pfp;
mod render;
#[derive(Debug, Clone)]
struct Notecrumbs {
ndb: Ndb,
keys: Keys,
/// How long do we wait for remote note requests
timeout: Duration,
}
enum Target {
pub enum Target {
Profile(XOnlyPublicKey),
Event(EventId),
}
fn nip19_target(nip19: &Nip19) -> Option<Target> {
match nip19 {
Nip19::Event(ev) => Some(Target::Event(ev.event_id)),
Nip19::EventId(evid) => Some(Target::Event(*evid)),
Nip19::Profile(prof) => Some(Target::Profile(prof.public_key)),
Nip19::Pubkey(pk) => Some(Target::Profile(*pk)),
Nip19::Secret(_) => None,
}
}
type ImageCache = LruCache<XOnlyPublicKey, egui::TextureHandle>;
fn note_ui(app: &Notecrumbs, ctx: &egui::Context, content: &str) {
use egui::{FontId, RichText};
#[derive(Debug, Clone)]
pub struct Notecrumbs {
ndb: Ndb,
keys: Keys,
img_cache: Arc<ImageCache>,
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(content).font(FontId::proportional(40.0)));
});
})
});
}
fn render_note(app: &Notecrumbs, content: &str) -> Vec<u8> {
use egui_skia::{rasterize, RasterizeOptions};
use skia_safe::EncodedImageFormat;
let options = RasterizeOptions {
pixels_per_point: 1.0,
frames_before_screenshot: 1,
};
let mut surface = rasterize((1200, 630), |ctx| note_ui(app, ctx, content), Some(options));
surface
.image_snapshot()
.encode_to_data(EncodedImageFormat::PNG)
.expect("expected image")
.as_bytes()
.into()
}
fn nip19_to_filters(nip19: &Nip19) -> Result<Vec<Filter>, Error> {
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),
}
}
fn nip19_relays(nip19: &Nip19) -> Vec<String> {
let mut relays: Vec<String> = vec![];
match nip19 {
Nip19::Event(ev) => relays.extend(ev.relays.clone()),
Nip19::Profile(p) => relays.extend(p.relays.clone()),
_ => (),
}
relays
/// How long do we wait for remote note requests
timeout: Duration,
}
async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<nostr_sdk::Event, Error> {
@ -114,14 +46,14 @@ async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<nostr_sdk::Event,
let _ = client.add_relay("wss://relay.damus.io").await;
let other_relays = nip19_relays(nip19);
let other_relays = nip19::to_relays(nip19);
for relay in other_relays {
let _ = client.add_relay(relay).await;
}
client.connect().await;
let filters = nip19_to_filters(nip19)?;
let filters = nip19::to_filters(nip19)?;
client
.req_events_of(filters.clone(), Some(app.timeout))
@ -159,7 +91,7 @@ async fn serve(
}
};
let target = match nip19_target(&nip19) {
let target = match nip19::to_target(&nip19) {
Some(target) => target,
None => {
return Ok(Response::builder()
@ -218,7 +150,7 @@ async fn serve(
}
};
let data = render_note(&app, &content);
let data = render::render_note(&app, &content);
Ok(Response::builder()
.header(header::CONTENT_TYPE, "image/png")
@ -248,7 +180,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let ndb = Ndb::new(".", &cfg).expect("ndb failed to open");
let keys = Keys::generate();
let timeout = get_env_timeout();
let app = Notecrumbs { ndb, keys, timeout };
let img_cache = Arc::new(LruCache::new(std::num::NonZeroUsize::new(64).unwrap()));
let app = Notecrumbs {
ndb,
keys,
timeout,
img_cache,
};
// We start a loop to continuously accept incoming connections
loop {

46
src/nip19.rs Normal file
View File

@ -0,0 +1,46 @@
use crate::error::Error;
use crate::Target;
use nostr_sdk::nips::nip19::Nip19;
use nostr_sdk::prelude::*;
pub fn to_target(nip19: &Nip19) -> Option<Target> {
match nip19 {
Nip19::Event(ev) => Some(Target::Event(ev.event_id)),
Nip19::EventId(evid) => Some(Target::Event(*evid)),
Nip19::Profile(prof) => Some(Target::Profile(prof.public_key)),
Nip19::Pubkey(pk) => Some(Target::Profile(*pk)),
Nip19::Secret(_) => None,
}
}
pub fn to_filters(nip19: &Nip19) -> Result<Vec<Filter>, Error> {
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),
}
}
pub fn to_relays(nip19: &Nip19) -> Vec<String> {
let mut relays: Vec<String> = vec![];
match nip19 {
Nip19::Event(ev) => relays.extend(ev.relays.clone()),
Nip19::Profile(p) => relays.extend(p.relays.clone()),
_ => (),
}
relays
}

77
src/pfp.rs Normal file
View File

@ -0,0 +1,77 @@
use egui::{Color32, ColorImage};
use image::imageops::FilterType;
// 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;
for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
// y coordinate
let uy = pixnum / image.size[0];
let y = uy as f32;
let y_offset = edge_radius - y;
// x coordinate
let ux = pixnum % image.size[0];
let x = ux as f32;
let x_offset = edge_radius - x;
// The radius to this pixel (may be inside or outside the circle)
let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
// If inside of the avatar circle
if pixel_radius_squared <= edge_radius_squared {
// squareroot to find how many pixels we are from the edge
let pixel_radius: f32 = pixel_radius_squared.sqrt();
let distance = edge_radius - pixel_radius;
// If we are within 1 pixel of the edge, we should fade, to
// antialias the edge of the circle. 1 pixel from the edge should
// be 100% of the original color, and right on the edge should be
// 0% of the original color.
if distance <= 1.0 {
*pixel = Color32::from_rgba_premultiplied(
(pixel.r() as f32 * distance) as u8,
(pixel.g() as f32 * distance) as u8,
(pixel.b() as f32 * distance) as u8,
(pixel.a() as f32 * distance) as u8,
);
}
} else {
// Outside of the avatar circle
*pixel = Color32::TRANSPARENT;
}
}
}
fn process_pfp_bitmap(size: u32, image: &mut image::DynamicImage) -> ColorImage {
#[cfg(features = "profiling")]
puffin::profile_function!();
// Crop square
let smaller = image.width().min(image.height());
if image.width() > smaller {
let excess = image.width() - smaller;
*image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
} else if image.height() > smaller {
let excess = image.height() - smaller;
*image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
}
let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
let mut color_image = ColorImage::from_rgba_unmultiplied(
[
image_buffer.width() as usize,
image_buffer.height() as usize,
],
image_buffer.as_flat_samples().as_slice(),
);
round_image(&mut color_image);
color_image
}

44
src/render.rs Normal file
View File

@ -0,0 +1,44 @@
struct ProfileRenderData {}
use crate::Notecrumbs;
struct NoteRenderData {
content: String,
profile: ProfileRenderData,
}
enum RenderData {
Note(NoteRenderData),
}
fn note_ui(app: &Notecrumbs, ctx: &egui::Context, content: &str) {
use egui::{FontId, RichText};
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(content).font(FontId::proportional(40.0)));
});
})
});
}
pub fn render_note(app: &Notecrumbs, content: &str) -> Vec<u8> {
use egui_skia::{rasterize, RasterizeOptions};
use skia_safe::EncodedImageFormat;
let options = RasterizeOptions {
pixels_per_point: 1.0,
frames_before_screenshot: 1,
};
let mut surface = rasterize((1200, 630), |ctx| note_ui(app, ctx, content), Some(options));
surface
.image_snapshot()
.encode_to_data(EncodedImageFormat::PNG)
.expect("expected image")
.as_bytes()
.into()
}