//! Identicons (hub spec ยง6): a five-by-five mirrored pattern and a colour //! drawn from BLAKE3 of the lower-cased name, so every account and //! organisation has a stable picture without uploads. use axum::extract::Path; use axum::http::header; use axum::response::{IntoResponse, Response}; /// The SVG for `name`: 15 bits choose the cells of the left three columns /// (mirrored to five), two bytes choose the hue. pub fn identicon(name: &str) -> String { let h = blake3::hash(name.to_ascii_lowercase().as_bytes()); let b = h.as_bytes(); let hue = u16::from_le_bytes([b[0], b[1]]) % 360; let bits = u32::from_le_bytes([b[2], b[3], b[4], b[5]]); let mut cells = String::new(); for row in 0..5u32 { for col in 0..3u32 { if bits >> (row * 3 + col) & 1 == 1 { for x in [col, 4 - col] { cells.push_str(&format!(r#""#, x + 1, row + 1)); if x == 2 { break; } } } } } format!( r#"{cells}"# ) } /// `GET /avatar/{name}`: the identicon, cached for a day. pub async fn avatar(Path(name): Path) -> Response { let name = name.trim_end_matches(".svg"); ([(header::CONTENT_TYPE, "image/svg+xml"), (header::CACHE_CONTROL, "public, max-age=86400")], identicon(name)).into_response() } #[cfg(test)] mod tests { use super::identicon; #[test] fn identicons_are_stable_symmetric_and_case_blind() { let a = identicon("andy"); assert_eq!(a, identicon("Andy"), "names are case-insensitive"); assert_ne!(a, identicon("bob")); assert!(a.starts_with("")); // Every cell has its mirror image. let cells: Vec<(u32, u32)> = a .split(r#"