Skip to main content

nexus_common/models/user/
details.rs

1use super::UserSearch;
2use crate::db::{exec_single_row, queries, RedisOps};
3use crate::models::traits::Collection;
4use crate::types::DynError;
5use async_trait::async_trait;
6use chrono::Utc;
7use neo4rs::Query;
8use pubky_app_specs::{PubkyAppUser, PubkyAppUserLink, PubkyId};
9use serde::{Deserialize, Deserializer, Serialize};
10use serde_json;
11use utoipa::ToSchema;
12
13#[async_trait]
14impl RedisOps for UserDetails {}
15
16#[async_trait]
17impl Collection<&str> for UserDetails {
18    fn collection_details_graph_query(id_list: &[&str]) -> Query {
19        queries::get::get_users_details_by_ids(id_list)
20    }
21
22    fn put_graph_query(&self) -> Result<Query, DynError> {
23        queries::put::create_user(self)
24    }
25
26    async fn extend_on_index_miss(details: &[std::option::Option<Self>]) -> Result<(), DynError> {
27        let user_details_refs: Vec<&UserDetails> = details
28            .iter()
29            .filter_map(|detail| detail.as_ref())
30            .collect();
31
32        UserSearch::put_to_index(&user_details_refs).await
33    }
34}
35
36/// Represents user data with name, bio, image, links, and status.
37#[derive(Serialize, Deserialize, ToSchema, Default, Clone, Debug)]
38pub struct UserDetails {
39    pub name: String,
40    pub bio: Option<String>,
41    pub id: PubkyId,
42    #[serde(deserialize_with = "deserialize_user_links")]
43    pub links: Option<Vec<PubkyAppUserLink>>,
44    pub status: Option<String>,
45    pub image: Option<String>,
46    pub indexed_at: i64,
47}
48
49fn deserialize_user_links<'de, D>(
50    deserializer: D,
51) -> Result<Option<Vec<PubkyAppUserLink>>, D::Error>
52where
53    D: Deserializer<'de>,
54{
55    // Deserialize into serde_json::Value first
56    let value = serde_json::Value::deserialize(deserializer)?;
57
58    // Handle both cases
59    match value {
60        serde_json::Value::String(s) => {
61            // If it's a string, parse the string as JSON
62            let urls: Option<Vec<PubkyAppUserLink>> =
63                serde_json::from_str(&s).map_err(serde::de::Error::custom)?;
64            Ok(urls)
65        }
66        serde_json::Value::Array(arr) => {
67            // If it's already an array, deserialize it directly
68            let urls: Vec<PubkyAppUserLink> = serde_json::from_value(serde_json::Value::Array(arr))
69                .map_err(serde::de::Error::custom)?;
70            Ok(Some(urls))
71        }
72        serde_json::Value::Null => Ok(None),
73        _ => Err(serde::de::Error::custom(
74            "Expected either a string, an array or null",
75        )),
76    }
77}
78
79impl UserDetails {
80    /// Retrieves details by user ID, first trying to get from Redis, then from Neo4j if not found.
81    pub async fn get_by_id(user_id: &str) -> Result<Option<Self>, DynError> {
82        // Delegate to UserDetailsCollection::get_by_ids for single item retrieval
83        let details_collection = Self::get_by_ids(&[user_id]).await?;
84        Ok(details_collection.into_iter().flatten().next())
85    }
86
87    pub async fn from_homeserver(
88        homeserver_user: PubkyAppUser,
89        user_id: &PubkyId,
90    ) -> Result<Self, DynError> {
91        Ok(UserDetails {
92            name: homeserver_user.name,
93            bio: homeserver_user.bio,
94            status: homeserver_user.status,
95            links: homeserver_user.links,
96            image: homeserver_user.image,
97            id: user_id.clone(),
98            indexed_at: Utc::now().timestamp_millis(),
99        })
100    }
101
102    pub async fn delete(user_id: &str) -> Result<(), DynError> {
103        // Delete user_details on Redis
104        Self::remove_from_index_multiple_json(&[&[user_id]]).await?;
105        // Delete user graph node;
106        exec_single_row(queries::del::delete_user(user_id)).await?;
107
108        Ok(())
109    }
110}
111
112// #[cfg(test)]
113// mod tests {
114
115//     use super::*;
116//     use crate::_service::NexusApi;
117
118//     const USER_IDS: [&str; 8] = [
119//         "4snwyct86m383rsduhw5xgcxpw7c63j3pq8x4ycqikxgik8y64ro",
120//         "3iwsuz58pgrf7nw4kx8mg3fib1kqyi4oxqmuqxzsau1mpn5weipo",
121//         "3qgon1apkcmp63xbqpkrb3zzrja3nq9wou4u5bf7uu8rc9ehfo3y",
122//         "nope_it_does_not_exist", // Does not exist
123//         "4nacrqeuwh35kwrziy4m376uuyi7czazubgtyog4adm77ayqigxo",
124//         "5g3fwnue819wfdjwiwm8qr35ww6uxxgbzrigrtdgmbi19ksioeoy",
125//         "4p1qa1ko7wuta4f1qm8io495cqsmefbgfp85wtnm9bj55gqbhjpo",
126//         "not_existing_user_id_either", // Does not exist
127//     ];
128
129//     #[tokio_shared_rt::test(shared)]
130//     async fn test_get_by_ids_from_redis() {
131//         NexusApi::builder().init_stack().await;
132
133//         let user_details = UserDetails::get_by_ids(&USER_IDS).await.unwrap();
134//         assert_eq!(user_details.len(), USER_IDS.len());
135
136//         for details in user_details[0..3].iter() {
137//             assert!(details.is_some());
138//         }
139//         for details in user_details[4..7].iter() {
140//             assert!(details.is_some());
141//         }
142//         assert!(user_details[3].is_none());
143//         assert!(user_details[7].is_none());
144
145//         assert_eq!(user_details[0].as_ref().unwrap().name, "Aldert");
146//         assert_eq!(user_details[5].as_ref().unwrap().name, "Flavio");
147
148//         assert_eq!(
149//             user_details[5]
150//                 .as_ref()
151//                 .unwrap()
152//                 .links
153//                 .as_ref()
154//                 .unwrap()
155//                 .len(),
156//             4
157//         );
158//         assert_eq!(
159//             user_details[0]
160//                 .as_ref()
161//                 .unwrap()
162//                 .links
163//                 .as_ref()
164//                 .unwrap()
165//                 .len(),
166//             2
167//         );
168
169//         for (i, details) in user_details.iter().enumerate() {
170//             if let Some(details) = details {
171//                 assert_eq!(details.id.as_ref(), USER_IDS[i]);
172//             }
173//         }
174//     }
175// }