forked from nostr/notecrumbs
move around some things, add code for pfps
This commit is contained in:
parent
9cf11d953a
commit
927ba5b137
112
src/main.rs
112
src/main.rs
@ -8,104 +8,36 @@ use hyper::service::service_fn;
|
|||||||
use hyper::{Request, Response, StatusCode};
|
use hyper::{Request, Response, StatusCode};
|
||||||
use hyper_util::rt::TokioIo;
|
use hyper_util::rt::TokioIo;
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
|
use std::sync::Arc;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use nostr_sdk::nips::nip19::Nip19;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use nostrdb::{Config, Ndb, Transaction};
|
use nostrdb::{Config, Ndb, Transaction};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use nostr_sdk::Kind;
|
use lru::LruCache;
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
|
mod nip19;
|
||||||
|
mod pfp;
|
||||||
|
mod render;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
pub enum Target {
|
||||||
struct Notecrumbs {
|
|
||||||
ndb: Ndb,
|
|
||||||
keys: Keys,
|
|
||||||
|
|
||||||
/// How long do we wait for remote note requests
|
|
||||||
timeout: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Target {
|
|
||||||
Profile(XOnlyPublicKey),
|
Profile(XOnlyPublicKey),
|
||||||
Event(EventId),
|
Event(EventId),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nip19_target(nip19: &Nip19) -> Option<Target> {
|
type ImageCache = LruCache<XOnlyPublicKey, egui::TextureHandle>;
|
||||||
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) {
|
#[derive(Debug, Clone)]
|
||||||
use egui::{FontId, RichText};
|
pub struct Notecrumbs {
|
||||||
|
ndb: Ndb,
|
||||||
|
keys: Keys,
|
||||||
|
img_cache: Arc<ImageCache>,
|
||||||
|
|
||||||
egui::CentralPanel::default().show(&ctx, |ui| {
|
/// How long do we wait for remote note requests
|
||||||
ui.horizontal(|ui| {
|
timeout: Duration,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<nostr_sdk::Event, Error> {
|
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 _ = 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 {
|
for relay in other_relays {
|
||||||
let _ = client.add_relay(relay).await;
|
let _ = client.add_relay(relay).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
|
|
||||||
let filters = nip19_to_filters(nip19)?;
|
let filters = nip19::to_filters(nip19)?;
|
||||||
|
|
||||||
client
|
client
|
||||||
.req_events_of(filters.clone(), Some(app.timeout))
|
.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,
|
Some(target) => target,
|
||||||
None => {
|
None => {
|
||||||
return Ok(Response::builder()
|
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()
|
Ok(Response::builder()
|
||||||
.header(header::CONTENT_TYPE, "image/png")
|
.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 ndb = Ndb::new(".", &cfg).expect("ndb failed to open");
|
||||||
let keys = Keys::generate();
|
let keys = Keys::generate();
|
||||||
let timeout = get_env_timeout();
|
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
|
// We start a loop to continuously accept incoming connections
|
||||||
loop {
|
loop {
|
||||||
|
46
src/nip19.rs
Normal file
46
src/nip19.rs
Normal 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
77
src/pfp.rs
Normal 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
44
src/render.rs
Normal 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()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user