Skip to main content

hinge_rs/api/
client.rs

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}