1use futures_util::stream::{self, Stream, StreamExt, TryStreamExt};
63use reqwest::Method;
64use serde::{Deserialize, Serialize};
65
66use crate::client::Ghl;
67use crate::error::Result;
68
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75#[allow(missing_docs)] pub struct Contact {
77 pub id: String,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub location_id: Option<String>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub email: Option<String>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub phone: Option<String>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub first_name: Option<String>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub last_name: Option<String>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub contact_name: Option<String>,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
91 pub tags: Vec<String>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub source: Option<String>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub date_added: Option<String>,
96 #[serde(flatten)]
98 pub extra: serde_json::Map<String, serde_json::Value>,
99}
100
101#[derive(Debug, Clone, Default, Serialize)]
103#[serde(rename_all = "camelCase")]
104#[allow(missing_docs)] pub struct CreateContact {
106 pub location_id: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub email: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub phone: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub first_name: Option<String>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub last_name: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub name: Option<String>,
117 #[serde(skip_serializing_if = "Vec::is_empty", default)]
118 pub tags: Vec<String>,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub source: Option<String>,
121}
122
123#[derive(Debug, Clone, Default, Serialize)]
125#[serde(rename_all = "camelCase")]
126#[allow(missing_docs)] pub struct UpdateContact {
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub email: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub phone: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub first_name: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub last_name: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub name: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub tags: Option<Vec<String>>,
140}
141
142#[derive(Deserialize)]
143struct ContactEnvelope {
144 contact: Contact,
145}
146
147#[derive(Debug, Clone, Deserialize)]
149pub struct ContactPage {
150 #[serde(default)]
152 pub contacts: Vec<Contact>,
153 #[serde(default)]
155 pub meta: Option<ListMeta>,
156}
157
158#[derive(Debug, Clone, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct ListMeta {
162 #[serde(default)]
164 pub start_after_id: Option<String>,
165 #[serde(default)]
167 pub start_after: Option<i64>,
168 #[serde(default)]
170 pub total: Option<i64>,
171}
172
173pub struct ContactsService {
175 pub(crate) client: Ghl,
176}
177
178impl ContactsService {
179 pub(crate) fn new(client: Ghl) -> Self {
180 Self { client }
181 }
182
183 pub async fn create(&self, contact: CreateContact) -> Result<Contact> {
185 let envelope: ContactEnvelope = self
186 .client
187 .send(Method::POST, "/contacts/", &[], Some(&contact))
188 .await?;
189 Ok(envelope.contact)
190 }
191
192 pub async fn get(&self, contact_id: &str) -> Result<Contact> {
194 let envelope: ContactEnvelope = self
195 .client
196 .send(
197 Method::GET,
198 &format!("/contacts/{contact_id}"),
199 &[],
200 None::<&()>,
201 )
202 .await?;
203 Ok(envelope.contact)
204 }
205
206 pub async fn update(&self, contact_id: &str, update: UpdateContact) -> Result<Contact> {
208 let envelope: ContactEnvelope = self
209 .client
210 .send(
211 Method::PUT,
212 &format!("/contacts/{contact_id}"),
213 &[],
214 Some(&update),
215 )
216 .await?;
217 Ok(envelope.contact)
218 }
219
220 pub async fn delete(&self, contact_id: &str) -> Result<()> {
222 let _: serde_json::Value = self
223 .client
224 .send(
225 Method::DELETE,
226 &format!("/contacts/{contact_id}"),
227 &[],
228 None::<&()>,
229 )
230 .await?;
231 Ok(())
232 }
233
234 pub fn list(&self, location_id: impl Into<String>) -> ListContacts {
236 ListContacts {
237 client: self.client.clone(),
238 location_id: location_id.into(),
239 limit: 20,
240 query: None,
241 start_after_id: None,
242 start_after: None,
243 }
244 }
245}
246
247#[derive(Clone)]
250pub struct ListContacts {
251 client: Ghl,
252 location_id: String,
253 limit: u32,
254 query: Option<String>,
255 start_after_id: Option<String>,
256 start_after: Option<i64>,
257}
258
259impl ListContacts {
260 pub fn limit(mut self, limit: u32) -> Self {
262 self.limit = limit.clamp(1, 100);
263 self
264 }
265
266 pub fn query(mut self, query: impl Into<String>) -> Self {
268 self.query = Some(query.into());
269 self
270 }
271
272 pub fn start_after_id(mut self, cursor: impl Into<String>) -> Self {
274 self.start_after_id = Some(cursor.into());
275 self
276 }
277
278 pub async fn page(&self) -> Result<ContactPage> {
280 let mut query: Vec<(String, String)> = vec![
281 ("locationId".into(), self.location_id.clone()),
282 ("limit".into(), self.limit.to_string()),
283 ];
284 if let Some(q) = &self.query {
285 query.push(("query".into(), q.clone()));
286 }
287 if let Some(id) = &self.start_after_id {
288 query.push(("startAfterId".into(), id.clone()));
289 }
290 if let Some(ts) = self.start_after {
291 query.push(("startAfter".into(), ts.to_string()));
292 }
293 self.client
294 .send(Method::GET, "/contacts/", &query, None::<&()>)
295 .await
296 }
297
298 pub fn stream(self) -> impl Stream<Item = Result<Contact>> {
300 stream::try_unfold(Some(self), |state| async move {
301 let Some(mut request) = state else {
302 return Ok::<_, crate::Error>(None);
303 };
304 let page = request.page().await?;
305 let full_page = page.contacts.len() as u32 >= request.limit;
306 let cursor = page.meta.as_ref().and_then(|m| m.start_after_id.clone());
307 let start_after = page.meta.as_ref().and_then(|m| m.start_after);
308
309 let next = match (full_page, cursor) {
310 (true, Some(cursor)) => {
311 request.start_after_id = Some(cursor);
312 request.start_after = start_after;
313 Some(request)
314 }
315 _ => None,
316 };
317 Ok(Some((
318 stream::iter(page.contacts.into_iter().map(Ok)),
319 next,
320 )))
321 })
322 .try_flatten()
323 .boxed()
324 }
325}