Skip to content

Commit

Permalink
move around some things, add code for pfps
Browse files Browse the repository at this point in the history
  • Loading branch information
jb55 committed Dec 18, 2023
1 parent 9cf11d9 commit 927ba5b
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 87 deletions.
112 changes: 25 additions & 87 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

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)));
});
})
});
}

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()
}
type ImageCache = LruCache<XOnlyPublicKey, egui::TextureHandle>;

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),
}
}
#[derive(Debug, Clone)]
pub struct Notecrumbs {
ndb: Ndb,
keys: Keys,
img_cache: Arc<ImageCache>,

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> {
Expand All @@ -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))
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
46 changes: 46 additions & 0 deletions src/nip19.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 77 additions & 0 deletions src/pfp.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 44 additions & 0 deletions src/render.rs
Original file line number Diff line number Diff line change
@@ -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()
}

0 comments on commit 927ba5b

Please sign in to comment.