1use jacquard_api::app_bsky::richtext::facet::{Facet as FacetMain, FacetFeaturesItem};
2use jacquard_common::types::blob::Blob;
3use jacquard_api::com_atproto::repo::strong_ref::StrongRef;
4use serde::{Deserialize as _, Deserializer};
5
6// see https://deer.social/profile/did:plc:63y3oh7iakdueqhlj6trojbq/post/3ltuv4skhqs2h
7pub fn safe_string<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
8 let str = Option::<String>::deserialize(deserializer)?;
9
10 Ok(str.map(|s| s.replace('\u{0000}', "")))
11}
12
13pub fn safe_string_required<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
14 let str = String::deserialize(deserializer)?;
15 Ok(str.replace('\u{0000}', ""))
16}
17
18pub fn strip_mime_params(mime_type: &str) -> String {
19 // Strip any parameters from the MIME type (e.g., "image/jpeg; quality=90" -> "image/jpeg")
20 mime_type.split(';').next().unwrap_or(mime_type).trim().to_string()
21}
22
23#[expect(clippy::single_option_map, reason = "Option::map() is clearer than and_then() for simple transformations")]
24pub fn blob_ref(blob: Option<&Blob>) -> Option<String> {
25 blob.map(|blob| blob.cid().to_string())
26}
27
28/// Convert a Blob to CID bytes (32-byte digest) for database storage
29pub fn blob_to_cid_bytes(blob: Option<&Blob>) -> Option<Vec<u8>> {
30 blob.and_then(|blob| {
31 parakeet_db::utils::cid::blob_to_cid_bytes(Some(blob))
32 })
33}
34
35pub fn strongref_to_parts(strongref: Option<&StrongRef>) -> (Option<String>, Option<String>) {
36 parakeet_db::utils::cid::strongref_to_parts(strongref)
37}
38
39/// Convert a StrongRef to (URI, CID digest bytes) for pending linkage
40pub fn strongref_to_parts_with_digest(
41 strongref: Option<&StrongRef>,
42) -> (Option<String>, Option<Vec<u8>>) {
43 parakeet_db::utils::cid::strongref_to_parts_with_digest(strongref)
44}
45
46pub fn at_uri_is_by(uri: &str, did: &str) -> bool {
47 let split_aturi = uri.rsplitn(4, '/').collect::<Vec<_>>();
48
49 did == split_aturi[2]
50}
51
52/// Extract the DID from an AT URI
53///
54/// AT URIs have the format: `at://{did}/{collection}/{rkey}`
55/// This function extracts the DID component.
56pub fn extract_did_from_uri(uri: &str) -> Option<&str> {
57 // AT URI format: at://{did}/{collection}/{rkey}
58 // Split from the right: [rkey, collection, did, "at:/"]
59 let split_aturi = uri.rsplitn(4, '/').collect::<Vec<_>>();
60
61 // Check that we have all 4 parts and it starts with "at:"
62 if split_aturi.len() == 4 && split_aturi[3].starts_with("at:") {
63 Some(split_aturi[2])
64 } else {
65 None
66 }
67}
68
69pub fn extract_mentions_and_tags(from: &[FacetMain]) -> (Vec<String>, Vec<String>) {
70 let (mentions, tags) = from
71 .iter()
72 .flat_map(|v| {
73 v.features.iter().filter_map(|feature| {
74 match feature {
75 FacetFeaturesItem::Mention(mention) => Some((Some(mention.did.to_string()), None)),
76 FacetFeaturesItem::Tag(tag) => Some((None, Some(tag.tag.to_string()))),
77 _ => None,
78 }
79 })
80 })
81 .unzip::<_, _, Vec<_>, Vec<_>>();
82
83 let mentions = mentions.into_iter().flatten().collect();
84 let tags = tags.into_iter().flatten().collect();
85
86 (mentions, tags)
87}
88
89pub fn merge_tags<T>(t1: Option<Vec<T>>, t2: Option<Vec<T>>) -> Vec<T> {
90 match (t1, t2) {
91 (Some(t1), None) => t1,
92 (None, Some(t2)) => t2,
93 (Some(mut t1), Some(t2)) => {
94 t1.extend(t2);
95 t1
96 }
97 _ => Vec::default(),
98 }
99}
100
101pub fn deserialize_optional_blob<'de, D>(deserializer: D) -> Result<Option<Blob<'static>>, D::Error>
102where
103 D: Deserializer<'de>,
104{
105 use jacquard_common::IntoStatic;
106 let opt: Option<Blob<'_>> = Option::deserialize(deserializer)?;
107 Ok(opt.map(|b| b.into_static()))
108}
109
110pub fn deserialize_optional_strongref<'de, D>(deserializer: D) -> Result<Option<StrongRef<'static>>, D::Error>
111where
112 D: Deserializer<'de>,
113{
114 use jacquard_common::IntoStatic;
115 let opt: Option<StrongRef<'_>> = Option::deserialize(deserializer)?;
116 Ok(opt.map(|s| s.into_static()))
117}