1use super::{
2 AuthApi, ChatApi, ConnectionsApi, LikesApi, PersistenceApi, ProfilesApi, PromptsApi,
3 RatingsApi, RawApi, RecommendationsApi, SettingsApi,
4};
5use crate::client::{DEFAULT_PUBLIC_IDS_BATCH_SIZE, HingeClient, RecsFetchConfig};
6use crate::errors::HingeError;
7use crate::settings::Settings;
8use crate::storage::{FsStorage, SecretStore, Storage};
9use secrecy::SecretString;
10use std::sync::Arc;
11
12#[derive(Clone, Debug)]
13pub struct Config {
14 pub settings: Settings,
15 pub recs_fetch_config: RecsFetchConfig,
16 pub public_ids_batch_size: usize,
17}
18
19impl Default for Config {
20 fn default() -> Self {
21 Self {
22 settings: Settings::default(),
23 recs_fetch_config: RecsFetchConfig::default(),
24 public_ids_batch_size: DEFAULT_PUBLIC_IDS_BATCH_SIZE,
25 }
26 }
27}
28
29#[derive(Clone, Debug)]
30pub struct DeviceProfile {
31 pub device_id: String,
32 pub install_id: String,
33 pub session_id: String,
34 pub installed: bool,
35}
36
37#[derive(Clone)]
38pub struct Session {
39 pub phone_number: String,
40 pub device: DeviceProfile,
41 pub hinge_identity_id: Option<String>,
42 pub hinge_auth_token: Option<SecretString>,
43 pub sendbird_auth_token: Option<SecretString>,
44 pub sendbird_session_key: Option<SecretString>,
45}
46
47impl std::fmt::Debug for Session {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("Session")
50 .field("phone_number", &self.phone_number)
51 .field("device", &self.device)
52 .field("hinge_identity_id", &self.hinge_identity_id)
53 .field(
54 "hinge_auth_token",
55 &self.hinge_auth_token.as_ref().map(|_| "[redacted]"),
56 )
57 .field(
58 "sendbird_auth_token",
59 &self.sendbird_auth_token.as_ref().map(|_| "[redacted]"),
60 )
61 .field(
62 "sendbird_session_key",
63 &self.sendbird_session_key.as_ref().map(|_| "[redacted]"),
64 )
65 .finish()
66 }
67}
68
69impl Session {
70 pub fn from_inner<S: Storage + Clone>(client: &HingeClient<S>) -> Self {
71 Self {
72 phone_number: client.phone_number.clone(),
73 device: DeviceProfile {
74 device_id: client.device_id.clone(),
75 install_id: client.install_id.clone(),
76 session_id: client.session_id.clone(),
77 installed: client.installed,
78 },
79 hinge_identity_id: client
80 .hinge_auth
81 .as_ref()
82 .map(|token| token.identity_id.clone()),
83 hinge_auth_token: client
84 .hinge_auth
85 .as_ref()
86 .map(|token| SecretString::new(token.token.clone().into())),
87 sendbird_auth_token: client
88 .sendbird_auth
89 .as_ref()
90 .map(|token| SecretString::new(token.token.clone().into())),
91 sendbird_session_key: client
92 .sendbird_session_key
93 .as_ref()
94 .map(|key| SecretString::new(key.clone().into())),
95 }
96 }
97}
98
99pub struct ClientBuilder {
100 phone_number: Option<String>,
101 config: Config,
102 secret_store: Option<Arc<dyn SecretStore>>,
103}
104
105impl ClientBuilder {
106 pub fn new() -> Self {
107 Self {
108 phone_number: None,
109 config: Config::default(),
110 secret_store: None,
111 }
112 }
113
114 pub fn phone_number(mut self, phone_number: impl Into<String>) -> Self {
115 self.phone_number = Some(phone_number.into());
116 self
117 }
118
119 pub fn settings(mut self, settings: Settings) -> Self {
120 self.config.settings = settings;
121 self
122 }
123
124 pub fn recs_fetch_config(mut self, config: RecsFetchConfig) -> Self {
125 self.config.recs_fetch_config = config;
126 self
127 }
128
129 pub fn public_ids_batch_size(mut self, batch_size: usize) -> Self {
130 self.config.public_ids_batch_size = batch_size.max(1);
131 self
132 }
133
134 pub fn secret_store(mut self, store: Arc<dyn SecretStore>) -> Self {
135 self.secret_store = Some(store);
136 self
137 }
138
139 pub fn build(self) -> Result<Client<FsStorage>, HingeError> {
140 let phone_number = self
141 .phone_number
142 .filter(|value| !value.trim().is_empty())
143 .ok_or_else(|| HingeError::Auth("phone number is required".into()))?;
144 let mut client = Client::with_storage(phone_number, FsStorage, self.config);
145 if let Some(store) = self.secret_store {
146 client.inner = client.inner.with_secret_store(store);
147 }
148 Ok(client)
149 }
150}
151
152impl Default for ClientBuilder {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158#[derive(Clone)]
159pub struct Client<S: Storage + Clone = FsStorage> {
160 pub(super) inner: HingeClient<S>,
161}
162
163impl Client<FsStorage> {
164 pub fn builder() -> ClientBuilder {
165 ClientBuilder::new()
166 }
167}
168
169impl<S: Storage + Clone> Client<S> {
170 pub fn with_storage(phone_number: impl Into<String>, storage: S, config: Config) -> Self {
171 let mut inner = HingeClient::new(phone_number, storage, Some(config.settings));
172 inner.set_recs_fetch_config(config.recs_fetch_config);
173 inner.set_public_ids_batch_size(config.public_ids_batch_size);
174 Self { inner }
175 }
176
177 pub fn from_inner(inner: HingeClient<S>) -> Self {
178 Self { inner }
179 }
180
181 pub fn with_secret_store(mut self, store: Arc<dyn SecretStore>) -> Self {
182 self.inner = self.inner.with_secret_store(store);
183 self
184 }
185
186 pub fn inner(&self) -> &HingeClient<S> {
187 &self.inner
188 }
189
190 pub fn inner_mut(&mut self) -> &mut HingeClient<S> {
191 &mut self.inner
192 }
193
194 pub fn into_inner(self) -> HingeClient<S> {
195 self.inner
196 }
197
198 pub fn session(&self) -> Session {
199 Session::from_inner(&self.inner)
200 }
201
202 pub fn set_recs_fetch_config(&mut self, config: RecsFetchConfig) {
203 self.inner.set_recs_fetch_config(config);
204 }
205
206 pub fn set_public_ids_batch_size(&mut self, batch_size: usize) {
207 self.inner.set_public_ids_batch_size(batch_size);
208 }
209
210 pub fn auth(&mut self) -> AuthApi<'_, S> {
211 AuthApi {
212 client: &mut self.inner,
213 }
214 }
215
216 pub fn recommendations(&mut self) -> RecommendationsApi<'_, S> {
217 RecommendationsApi {
218 client: &mut self.inner,
219 }
220 }
221
222 pub fn profiles(&mut self) -> ProfilesApi<'_, S> {
223 ProfilesApi {
224 client: &mut self.inner,
225 }
226 }
227
228 pub fn likes(&mut self) -> LikesApi<'_, S> {
229 LikesApi {
230 client: &mut self.inner,
231 }
232 }
233
234 pub fn ratings(&mut self) -> RatingsApi<'_, S> {
235 RatingsApi {
236 client: &mut self.inner,
237 }
238 }
239
240 pub fn prompts(&mut self) -> PromptsApi<'_, S> {
241 PromptsApi {
242 client: &mut self.inner,
243 }
244 }
245
246 pub fn connections(&mut self) -> ConnectionsApi<'_, S> {
247 ConnectionsApi {
248 client: &mut self.inner,
249 }
250 }
251
252 pub fn settings(&mut self) -> SettingsApi<'_, S> {
253 SettingsApi {
254 client: &mut self.inner,
255 }
256 }
257
258 pub fn chat(&mut self) -> ChatApi<'_, S> {
259 ChatApi {
260 client: &mut self.inner,
261 }
262 }
263
264 pub fn persistence(&mut self) -> PersistenceApi<'_, S> {
265 PersistenceApi {
266 client: &mut self.inner,
267 }
268 }
269
270 pub fn raw(&mut self) -> RawApi<'_, S> {
271 RawApi {
272 client: &mut self.inner,
273 }
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::models::{HingeAuthToken, SendbirdAuthToken};
281 use chrono::Utc;
282
283 #[test]
284 fn session_debug_redacts_secrets() {
285 let mut inner = HingeClient::new("+15555550123", FsStorage, None);
286 inner.hinge_auth = Some(HingeAuthToken {
287 identity_id: "user-1".into(),
288 token: "hinge-secret-token".into(),
289 expires: Utc::now(),
290 });
291 inner.sendbird_auth = Some(SendbirdAuthToken {
292 token: "sendbird-secret-token".into(),
293 expires: Utc::now(),
294 });
295 inner.sendbird_session_key = Some("session-secret-key".into());
296
297 let session = Session::from_inner(&inner);
298 let debug = format!("{session:?}");
299
300 assert!(debug.contains("[redacted]"));
301 assert!(!debug.contains("hinge-secret-token"));
302 assert!(!debug.contains("sendbird-secret-token"));
303 assert!(!debug.contains("session-secret-key"));
304 }
305
306 #[test]
307 fn builder_requires_phone_number() {
308 let error = match Client::builder().build() {
309 Ok(_) => panic!("builder unexpectedly succeeded without a phone number"),
310 Err(error) => error,
311 };
312 assert!(error.to_string().contains("phone number is required"));
313 }
314}