Skip to main content

hinge_rs/client/
profiles.rs

1use super::HingeClient;
2use super::payload::{preferences_to_api_json, profile_update_to_api_json};
3use super::render::render_profile;
4use crate::errors::HingeError;
5use crate::models::{
6    AnswerContentPayload, Preferences, PreferencesResponse, ProfileContentFull, ProfileUpdate,
7    PublicUserProfile, SelfContentResponse, SelfProfileResponse,
8};
9use crate::storage::Storage;
10use std::collections::HashSet;
11
12impl<S: Storage + Clone> HingeClient<S> {
13    pub async fn rendered_profile_text_for_user(
14        &mut self,
15        user_id: &str,
16    ) -> Result<String, HingeError> {
17        let uid = user_id.trim();
18        if uid.is_empty() {
19            return Ok(String::new());
20        }
21
22        let prompts_manager = match self.fetch_prompts_manager().await {
23            Ok(mgr) => Some(mgr),
24            Err(err) => {
25                log::warn!("Failed to prefetch prompts for rendered profile: {}", err);
26                None
27            }
28        };
29        let profile = self
30            .get_profiles(vec![uid.to_string()])
31            .await?
32            .into_iter()
33            .next();
34        let profile_content = self
35            .get_profile_content(vec![uid.to_string()])
36            .await?
37            .into_iter()
38            .next();
39
40        Ok(render_profile(
41            profile.as_ref(),
42            profile_content.as_ref(),
43            prompts_manager.as_ref(),
44        ))
45    }
46
47    pub async fn get_self_profile(&self) -> Result<SelfProfileResponse, HingeError> {
48        let url = format!("{}/user/v3", self.settings.base_url);
49        let res = self.http_get(&url).await?;
50        self.parse_response::<SelfProfileResponse>(res).await
51    }
52
53    pub async fn get_self_content(&self) -> Result<SelfContentResponse, HingeError> {
54        let url = format!("{}/content/v2", self.settings.base_url);
55        let res = self.http_get(&url).await?;
56        self.parse_response::<SelfContentResponse>(res).await
57    }
58
59    pub async fn get_self_preferences(&self) -> Result<PreferencesResponse, HingeError> {
60        let url = format!("{}/preference/v2/selected", self.settings.base_url);
61        let res = self.http_get(&url).await?;
62        self.parse_response::<PreferencesResponse>(res).await
63    }
64
65    pub async fn get_profiles_public_raw_unfiltered(
66        &self,
67        ids: Vec<String>,
68    ) -> Result<serde_json::Value, HingeError> {
69        let url = format!(
70            "{}/user/v3/public?ids={}",
71            self.settings.base_url,
72            ids.join(",")
73        );
74        let res = self.http_get(&url).await?;
75        self.parse_response(res).await
76    }
77
78    pub async fn get_content_public_raw_unfiltered(
79        &self,
80        ids: Vec<String>,
81    ) -> Result<serde_json::Value, HingeError> {
82        let url = format!(
83            "{}/content/v2/public?ids={}",
84            self.settings.base_url,
85            ids.join(",")
86        );
87        let res = self.http_get(&url).await?;
88        self.parse_response(res).await
89    }
90
91    pub async fn get_profiles(
92        &self,
93        user_ids: Vec<String>,
94    ) -> Result<Vec<PublicUserProfile>, HingeError> {
95        let chunks = self.prepare_user_id_chunks(user_ids);
96        if chunks.is_empty() {
97            return Ok(Vec::new());
98        }
99
100        let mut aggregated: Vec<PublicUserProfile> = Vec::new();
101        for batch in chunks {
102            let url = format!(
103                "{}/user/v3/public?ids={}",
104                self.settings.base_url,
105                batch.join(",")
106            );
107            let res = self.http_get(&url).await?;
108            let mut part: Vec<PublicUserProfile> = self.parse_response(res).await?;
109            aggregated.append(&mut part);
110        }
111        Ok(aggregated)
112    }
113
114    pub async fn get_profile_content(
115        &self,
116        user_ids: Vec<String>,
117    ) -> Result<Vec<ProfileContentFull>, HingeError> {
118        let chunks = self.prepare_user_id_chunks(user_ids);
119        if chunks.is_empty() {
120            return Ok(Vec::new());
121        }
122
123        let mut aggregated: Vec<ProfileContentFull> = Vec::new();
124        for batch in chunks {
125            let url = format!(
126                "{}/content/v2/public?ids={}",
127                self.settings.base_url,
128                batch.join(",")
129            );
130            let res = self.http_get(&url).await?;
131            let mut part: Vec<ProfileContentFull> = self.parse_response(res).await?;
132            aggregated.append(&mut part);
133        }
134        Ok(aggregated)
135    }
136
137    pub async fn update_self_preferences(
138        &self,
139        preferences: Preferences,
140    ) -> Result<serde_json::Value, HingeError> {
141        let url = format!("{}/preference/v2/selected", self.settings.base_url);
142        let prefs_json = preferences_to_api_json(&preferences);
143        let payload = serde_json::json!([prefs_json]);
144        let res = self.http_patch(&url, &payload).await?;
145        self.parse_response(res).await
146    }
147
148    pub async fn update_self_profile(
149        &self,
150        profile_updates: ProfileUpdate,
151    ) -> Result<serde_json::Value, HingeError> {
152        let url = format!("{}/user/v3", self.settings.base_url);
153        let profile_json = profile_update_to_api_json(&profile_updates);
154        let payload = serde_json::json!({ "profile": profile_json });
155        let res = self.http_patch(&url, &payload).await?;
156        self.parse_response(res).await
157    }
158
159    pub async fn update_answers(
160        &self,
161        answers: Vec<AnswerContentPayload>,
162    ) -> Result<serde_json::Value, HingeError> {
163        let url = format!("{}/content/v1/answers", self.settings.base_url);
164        let res = self
165            .http
166            .put(url)
167            .headers(self.default_headers()?)
168            .json(&answers)
169            .send()
170            .await?;
171        self.parse_response(res).await
172    }
173
174    pub async fn delete_content(&self, content_ids: Vec<String>) -> Result<(), HingeError> {
175        let url = format!(
176            "{}/content/v1?ids={}",
177            self.settings.base_url,
178            content_ids.join(",")
179        );
180        let res = self
181            .http
182            .delete(url)
183            .headers(self.default_headers()?)
184            .send()
185            .await?;
186        if !res.status().is_success() {
187            return Err(HingeError::Http(format!("status {}", res.status())));
188        }
189        Ok(())
190    }
191
192    fn prepare_user_id_chunks(&self, user_ids: Vec<String>) -> Vec<Vec<String>> {
193        fn is_user_id_like(id: &str) -> bool {
194            if id.is_empty() {
195                return false;
196            }
197            let trimmed = id.trim();
198            if trimmed.chars().all(|c| c.is_ascii_digit()) {
199                return true;
200            }
201            trimmed.len() == 32 && trimmed.chars().all(|c| c.is_ascii_hexdigit())
202        }
203
204        let (mut accepted, mut dropped) = (Vec::new(), 0usize);
205        let mut seen: HashSet<String> = HashSet::new();
206        for raw in user_ids {
207            let id = raw.trim().to_string();
208            if is_user_id_like(&id) && seen.insert(id.clone()) {
209                accepted.push(id);
210            } else {
211                dropped += 1;
212            }
213        }
214
215        if accepted.is_empty() {
216            log::warn!("No valid user IDs to fetch (dropped {})", dropped);
217            return Vec::new();
218        }
219        if dropped > 0 {
220            log::debug!("Dropped {} non user-like IDs from public fetch", dropped);
221        }
222
223        let batch_size = self.public_ids_batch_size.max(1);
224        let mut out: Vec<Vec<String>> = Vec::new();
225        let mut idx = 0usize;
226        while idx < accepted.len() {
227            let end = (idx + batch_size).min(accepted.len());
228            out.push(accepted[idx..end].to_vec());
229            idx = end;
230        }
231        if out.len() > 1 {
232            log::info!(
233                "Fetching public user data in {} batches of up to {} IDs",
234                out.len(),
235                batch_size
236            );
237        }
238        out
239    }
240}