hinge_rs/client/
profiles.rs1use 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}