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// }