mirror of
https://github.com/damus-io/notecrumbs.git
synced 2024-09-29 18:10:43 +00:00
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_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
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