//! 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#""#
)
}
/// `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("