use futures_util::stream::{self, Stream, StreamExt, TryStreamExt};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::client::Ghl;
use crate::error::Result;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(missing_docs)] pub struct Contact {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phone: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub first_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contact_name: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date_added: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
#[allow(missing_docs)] pub struct CreateContact {
pub location_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub first_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
#[allow(missing_docs)] pub struct UpdateContact {
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub first_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
}
#[derive(Deserialize)]
struct ContactEnvelope {
contact: Contact,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ContactPage {
#[serde(default)]
pub contacts: Vec<Contact>,
#[serde(default)]
pub meta: Option<ListMeta>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListMeta {
#[serde(default)]
pub start_after_id: Option<String>,
#[serde(default)]
pub start_after: Option<i64>,
#[serde(default)]
pub total: Option<i64>,
}
pub struct ContactsService {
pub(crate) client: Ghl,
}
impl ContactsService {
pub(crate) fn new(client: Ghl) -> Self {
Self { client }
}
pub async fn create(&self, contact: CreateContact) -> Result<Contact> {
let envelope: ContactEnvelope = self
.client
.send(Method::POST, "/contacts/", &[], Some(&contact))
.await?;
Ok(envelope.contact)
}
pub async fn get(&self, contact_id: &str) -> Result<Contact> {
let envelope: ContactEnvelope = self
.client
.send(
Method::GET,
&format!("/contacts/{contact_id}"),
&[],
None::<&()>,
)
.await?;
Ok(envelope.contact)
}
pub async fn update(&self, contact_id: &str, update: UpdateContact) -> Result<Contact> {
let envelope: ContactEnvelope = self
.client
.send(
Method::PUT,
&format!("/contacts/{contact_id}"),
&[],
Some(&update),
)
.await?;
Ok(envelope.contact)
}
pub async fn delete(&self, contact_id: &str) -> Result<()> {
let _: serde_json::Value = self
.client
.send(
Method::DELETE,
&format!("/contacts/{contact_id}"),
&[],
None::<&()>,
)
.await?;
Ok(())
}
pub fn list(&self, location_id: impl Into<String>) -> ListContacts {
ListContacts {
client: self.client.clone(),
location_id: location_id.into(),
limit: 20,
query: None,
start_after_id: None,
start_after: None,
}
}
}
#[derive(Clone)]
pub struct ListContacts {
client: Ghl,
location_id: String,
limit: u32,
query: Option<String>,
start_after_id: Option<String>,
start_after: Option<i64>,
}
impl ListContacts {
pub fn limit(mut self, limit: u32) -> Self {
self.limit = limit.clamp(1, 100);
self
}
pub fn query(mut self, query: impl Into<String>) -> Self {
self.query = Some(query.into());
self
}
pub fn start_after_id(mut self, cursor: impl Into<String>) -> Self {
self.start_after_id = Some(cursor.into());
self
}
pub async fn page(&self) -> Result<ContactPage> {
let mut query: Vec<(String, String)> = vec![
("locationId".into(), self.location_id.clone()),
("limit".into(), self.limit.to_string()),
];
if let Some(q) = &self.query {
query.push(("query".into(), q.clone()));
}
if let Some(id) = &self.start_after_id {
query.push(("startAfterId".into(), id.clone()));
}
if let Some(ts) = self.start_after {
query.push(("startAfter".into(), ts.to_string()));
}
self.client
.send(Method::GET, "/contacts/", &query, None::<&()>)
.await
}
pub fn stream(self) -> impl Stream<Item = Result<Contact>> {
stream::try_unfold(Some(self), |state| async move {
let Some(mut request) = state else {
return Ok::<_, crate::Error>(None);
};
let page = request.page().await?;
let full_page = page.contacts.len() as u32 >= request.limit;
let cursor = page.meta.as_ref().and_then(|m| m.start_after_id.clone());
let start_after = page.meta.as_ref().and_then(|m| m.start_after);
let next = match (full_page, cursor) {
(true, Some(cursor)) => {
request.start_after_id = Some(cursor);
request.start_after = start_after;
Some(request)
}
_ => None,
};
Ok(Some((
stream::iter(page.contacts.into_iter().map(Ok)),
next,
)))
})
.try_flatten()
.boxed()
}
}