use std::collections::HashMap; use std::fmt; use std::io::Result as IoResult; use std::sync::OnceLock; use rand::distributions::{Alphanumeric, DistString}; use reqwest::Result as RequestResult; use tokio::fs::read_to_string; use tracing::error; #[derive(Eq, PartialEq, Hash)] pub enum AvatarFeeling { Ok, Warning, Alerting, } impl fmt::Display for AvatarFeeling { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let repr = match self { Self::Ok => "Ok", Self::Warning => "Warning", Self::Alerting => "Alerting", }; write!(f, "{repr}") } } pub struct AvatarConfig<'a> { feeling: AvatarFeeling, background_color: &'a str, } impl<'a> AvatarConfig<'a> { pub fn new(feeling: AvatarFeeling, background_color: &'a str) -> Self { Self { feeling, background_color, } } } enum DicebearType { Notionists, Shapes, } impl fmt::Display for DicebearType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let repr = match self { Self::Notionists => "notionists", Self::Shapes => "shapes", }; write!(f, "{repr}") } } struct DicebearConfig<'a> { gesture: &'a str, browns: Vec, eyes: Vec, lips: Vec, } fn dicebear_variants() -> &'static HashMap> { static HASHMAP: OnceLock> = OnceLock::new(); HASHMAP.get_or_init(|| { let mut variants = HashMap::new(); variants.insert( AvatarFeeling::Alerting, DicebearConfig { gesture: "wavePointLongArms", browns: vec![2, 6, 11, 13], eyes: vec![2, 4], lips: vec![1, 2, 7, 11, 19, 20, 24, 27], }, ); variants.insert( AvatarFeeling::Warning, DicebearConfig { gesture: "pointLongArm", browns: vec![2, 5, 10, 13], eyes: vec![1, 3], lips: vec![1, 2, 4, 8, 10, 13, 18, 21, 29], }, ); variants.insert( AvatarFeeling::Ok, DicebearConfig { gesture: "okLongArm", browns: vec![1, 3, 4, 7, 8, 9, 12], eyes: vec![5], lips: vec![3, 5, 9, 14, 17, 22, 23, 25, 30], }, ); variants }) } fn render_dicebear_variants(values: &[u32]) -> String { values .iter() .map(|l| format!("variant{:02}", l)) .collect::>() .join(",") } async fn fetch_text(req: String) -> RequestResult { reqwest::get(req).await?.text().await } async fn read_file(path: &str) -> IoResult { read_to_string(path).await } async fn fetch_dicebear_svg( r#type: &DicebearType, req_fields: &Vec, placeholder_path: Option<&str>, ) -> String { // TODO: Use configuration file let url = "dicebear.tools.adrien.run"; let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); let type_str = r#type.to_string(); let url = format!( "https://{url}/7.x/{type_str}/svg?seed={seed}{}{}", if !req_fields.is_empty() { "&" } else { " " }, req_fields.join("&") ); let text = match fetch_text(url).await { Ok(text) => Some(text), Err(err) => { error!("Error during placeholder loading: {}", err); match placeholder_path { Some(placeholder_path) => match read_file(placeholder_path).await { Ok(content) => Some(content), Err(err) => { error!( "Error during to read {placeholder_path} file: {}", err.to_string() ); None } }, None => None, } } }; text.unwrap_or("".to_string()) } pub async fn generate_random_svg_avatar<'a>(config: Option<&'a AvatarConfig<'a>>) -> String { let (variant, feeling) = match config { Some(config) => (dicebear_variants().get(&config.feeling), &config.feeling), None => (None, &AvatarFeeling::Alerting), }; let mut req_fields = Vec::::new(); if let Some(config) = config { req_fields.push(format!("backgroundColor={}", config.background_color)); } if let Some(variant) = variant { req_fields.push(format!( "gestureProbability=100&gesture={}", &variant.gesture )); req_fields.push(format!( "&browsProbability=100&brows={}", render_dicebear_variants(&variant.browns) )); req_fields.push(format!( "&eyesProbability=100&eyes={}", render_dicebear_variants(&variant.eyes) )); req_fields.push(format!( "&lipsProbability=100&lips={}", render_dicebear_variants(&variant.lips) )); } let placeholder_path = match feeling { AvatarFeeling::Ok => "./images/modal-default-ok-icon.svg", AvatarFeeling::Warning => "./images/modal-default-warning-icon.svg", AvatarFeeling::Alerting => "./images/modal-default-critical-icon.svg", }; fetch_dicebear_svg( &DicebearType::Notionists, &req_fields, Some(placeholder_path), ) .await } pub struct ShapeConfig<'a> { background_color: &'a str, shape_1_color: &'a str, shape_2_color: &'a str, shape_3_color: &'a str, } impl<'a> ShapeConfig<'a> { pub fn new( background_color: &'a str, shape_1_color: &'a str, shape_2_color: &'a str, shape_3_color: &'a str, ) -> Self { Self { background_color, shape_1_color, shape_2_color, shape_3_color, } } } pub async fn generate_random_svg_shape<'a>(config: Option<&'a ShapeConfig<'a>>) -> String { let mut req_fields = Vec::::new(); if let Some(config) = config { req_fields.push(format!("backgroundColor={}", config.background_color)); req_fields.push(format!("shape1Color={}", config.shape_1_color)); req_fields.push(format!("shape2Color={}", config.shape_2_color)); req_fields.push(format!("shape3Color={}", config.shape_3_color)); } fetch_dicebear_svg(&DicebearType::Shapes, &req_fields, None).await }