use jacquard_api::app_bsky::richtext::facet::{Facet as FacetMain, FacetFeaturesItem}; use jacquard_common::types::blob::Blob; use jacquard_api::com_atproto::repo::strong_ref::StrongRef; use serde::{Deserialize as _, Deserializer}; // see https://deer.social/profile/did:plc:63y3oh7iakdueqhlj6trojbq/post/3ltuv4skhqs2h pub fn safe_string<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { let str = Option::::deserialize(deserializer)?; Ok(str.map(|s| s.replace('\u{0000}', ""))) } pub fn safe_string_required<'de, D: Deserializer<'de>>(deserializer: D) -> Result { let str = String::deserialize(deserializer)?; Ok(str.replace('\u{0000}', "")) } pub fn strip_mime_params(mime_type: &str) -> String { // Strip any parameters from the MIME type (e.g., "image/jpeg; quality=90" -> "image/jpeg") mime_type.split(';').next().unwrap_or(mime_type).trim().to_string() } #[expect(clippy::single_option_map, reason = "Option::map() is clearer than and_then() for simple transformations")] pub fn blob_ref(blob: Option<&Blob>) -> Option { blob.map(|blob| blob.cid().to_string()) } /// Convert a Blob to CID bytes (32-byte digest) for database storage pub fn blob_to_cid_bytes(blob: Option<&Blob>) -> Option> { blob.and_then(|blob| { parakeet_db::utils::cid::blob_to_cid_bytes(Some(blob)) }) } pub fn strongref_to_parts(strongref: Option<&StrongRef>) -> (Option, Option) { parakeet_db::utils::cid::strongref_to_parts(strongref) } /// Convert a StrongRef to (URI, CID digest bytes) for pending linkage pub fn strongref_to_parts_with_digest( strongref: Option<&StrongRef>, ) -> (Option, Option>) { parakeet_db::utils::cid::strongref_to_parts_with_digest(strongref) } pub fn at_uri_is_by(uri: &str, did: &str) -> bool { let split_aturi = uri.rsplitn(4, '/').collect::>(); did == split_aturi[2] } /// Extract the DID from an AT URI /// /// AT URIs have the format: `at://{did}/{collection}/{rkey}` /// This function extracts the DID component. pub fn extract_did_from_uri(uri: &str) -> Option<&str> { // AT URI format: at://{did}/{collection}/{rkey} // Split from the right: [rkey, collection, did, "at:/"] let split_aturi = uri.rsplitn(4, '/').collect::>(); // Check that we have all 4 parts and it starts with "at:" if split_aturi.len() == 4 && split_aturi[3].starts_with("at:") { Some(split_aturi[2]) } else { None } } pub fn extract_mentions_and_tags(from: &[FacetMain]) -> (Vec, Vec) { let (mentions, tags) = from .iter() .flat_map(|v| { v.features.iter().filter_map(|feature| { match feature { FacetFeaturesItem::Mention(mention) => Some((Some(mention.did.to_string()), None)), FacetFeaturesItem::Tag(tag) => Some((None, Some(tag.tag.to_string()))), _ => None, } }) }) .unzip::<_, _, Vec<_>, Vec<_>>(); let mentions = mentions.into_iter().flatten().collect(); let tags = tags.into_iter().flatten().collect(); (mentions, tags) } pub fn merge_tags(t1: Option>, t2: Option>) -> Vec { match (t1, t2) { (Some(t1), None) => t1, (None, Some(t2)) => t2, (Some(mut t1), Some(t2)) => { t1.extend(t2); t1 } _ => Vec::default(), } } pub fn deserialize_optional_blob<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { use jacquard_common::IntoStatic; let opt: Option> = Option::deserialize(deserializer)?; Ok(opt.map(|b| b.into_static())) } pub fn deserialize_optional_strongref<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { use jacquard_common::IntoStatic; let opt: Option> = Option::deserialize(deserializer)?; Ok(opt.map(|s| s.into_static())) }