hey_sdk/services/
contacts.rs1use std::fmt;
4
5use serde::de::DeserializeOwned;
6
7use crate::error::Error;
8use crate::generated::routes;
9use crate::generated::types::{
10 ConflictErrorResponseContent, Contact, ContactDetail, ContactNote, ContactNotePayload,
11 ContactNoteRequestContent, ContactPayload, ContactRequestContent, CreateContactRequestContent,
12 UnprocessableEntityErrorResponseContent, UpdateContactClearanceRequestContent,
13};
14use crate::operation::Operation;
15use crate::services::clearances::ClearanceStatus;
16use crate::types::SensitiveString;
17
18pub use crate::generated::services::contacts::*;
19
20#[derive(Debug, Clone, Default, PartialEq)]
22pub struct ContactParams {
23 pub name: String,
25 pub email_address: String,
27 pub alias_email_addresses: Option<Vec<String>>,
31 pub account_user_id: Option<i64>,
36}
37
38#[derive(Debug, Clone, Default, PartialEq, Eq)]
47#[non_exhaustive]
48pub struct ContactConflict {
49 pub contact_id: i64,
51 pub conflicting_contact_ids: Vec<i64>,
53}
54
55impl ContactConflict {
56 pub fn from_error(error: &Error) -> Option<&ContactConflict> {
58 std::error::Error::source(error)?.downcast_ref::<ContactConflict>()
59 }
60}
61
62impl Contacts<'_> {
63 pub async fn create_contact(&self, params: &ContactParams) -> Result<Contact, Error> {
68 let body = CreateContactRequestContent {
69 acting_user_id: self.acting_user_id(params.account_user_id).await?,
70 contact: contact_payload(params),
71 };
72 let mut operation = self.client().operation(&routes::CREATE_CONTACT, &[]);
73 operation.json(&body)?;
74 self.write(operation).await
75 }
76
77 pub async fn update_contact(
90 &self,
91 contact_id: i64,
92 params: &ContactParams,
93 ) -> Result<Contact, Error> {
94 let current = self.get(contact_id, &GetContactParams::default()).await?;
95 let body = ContactRequestContent {
96 contact: merged_payload(params, ¤t),
97 };
98 let mut operation = self
99 .client()
100 .operation(&routes::UPDATE_CONTACT, &[&contact_id]);
101 operation.resource_id(contact_id);
102 operation.json(&body)?;
103 self.write(operation).await
104 }
105
106 pub async fn screen(&self, contact_id: i64, status: ClearanceStatus) -> Result<(), Error> {
108 let body = UpdateContactClearanceRequestContent {
109 status: status.as_str().to_string(),
110 };
111 self.update_clearance(contact_id, &body).await
112 }
113
114 pub async fn set_note(&self, contact_id: i64, note: &str) -> Result<ContactNote, Error> {
117 let body = ContactNoteRequestContent {
118 contact: ContactNotePayload {
119 note: note.to_string(),
120 },
121 };
122 let mut operation = self
123 .client()
124 .operation(&routes::UPDATE_CONTACT_NOTE, &[&contact_id]);
125 operation.json(&body)?;
126 self.write(operation).await
127 }
128
129 async fn acting_user_id(&self, chosen: Option<i64>) -> Result<Option<i64>, Error> {
130 match self.client().account_id() {
131 None => Ok(chosen),
132 Some(account_id) => {
133 let account_user_id = self.client().account_user_id().await?;
134 match chosen {
135 Some(chosen) if chosen != account_user_id => Err(Error::usage(format!(
136 "account user {chosen} does not belong to selected account {account_id}"
137 ))),
138 _ => Ok(Some(account_user_id)),
139 }
140 }
141 }
142 }
143
144 async fn write<T: DeserializeOwned>(&self, operation: Operation) -> Result<T, Error> {
149 match self.client().execute(operation).await {
150 Ok(response) => response.json(),
151 Err(error) if error.http_status() == Some(409) => Err(clash(&error)),
152 Err(error) if error.http_status() == Some(422) => Err(rejection(&error)),
153 Err(error) => Err(error),
154 }
155 }
156}
157
158fn contact_payload(params: &ContactParams) -> ContactPayload {
159 let email_address = if params.email_address.is_empty() {
160 None
161 } else {
162 Some(SensitiveString::new(params.email_address.as_str()))
163 };
164 ContactPayload {
165 name: params.name.clone(),
166 email_address,
167 alias_email_addresses: params.alias_email_addresses.clone(),
168 }
169}
170
171fn merged_payload(params: &ContactParams, current: &ContactDetail) -> ContactPayload {
180 let mut payload = contact_payload(params);
181 if payload.name.is_empty() {
182 payload.name = current.name.clone().unwrap_or_default();
183 }
184 if payload.email_address.is_none() {
185 payload.email_address.clone_from(¤t.email_address);
186 }
187 if payload.alias_email_addresses.is_none() {
188 payload.alias_email_addresses = Some(current_aliases(current));
189 }
190 payload
191}
192
193fn current_aliases(current: &ContactDetail) -> Vec<String> {
194 current
195 .aliases
196 .iter()
197 .flatten()
198 .filter_map(|alias| alias.email_address.as_ref())
199 .map(|address| address.expose().to_string())
200 .collect()
201}
202
203fn clash(error: &Error) -> Error {
204 let payload: ConflictErrorResponseContent = error.body_json().unwrap_or_default();
205 Error::conflict(conflict_message(&payload)).with_source(ContactConflict {
206 contact_id: payload.contact_id.unwrap_or_default(),
207 conflicting_contact_ids: payload.conflicting_contact_ids.unwrap_or_default(),
208 })
209}
210
211fn rejection(error: &Error) -> Error {
212 let payload: UnprocessableEntityErrorResponseContent = error.body_json().unwrap_or_default();
213 Error::validation(&payload.errors.unwrap_or_default())
214}
215
216fn conflict_message(payload: &ConflictErrorResponseContent) -> String {
220 let messages = payload.errors.as_deref().unwrap_or_default();
221 if !messages.is_empty() {
222 messages.join("; ")
223 } else if let Some(message) = &payload.error {
224 message.clone()
225 } else {
226 "the contact conflicts with one that already exists".to_string()
227 }
228}
229
230impl fmt::Display for ContactConflict {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 if self.conflicting_contact_ids.is_empty() {
233 write!(
234 f,
235 "contact {} conflicts with one that already exists",
236 self.contact_id
237 )
238 } else {
239 let ids: Vec<String> = self
240 .conflicting_contact_ids
241 .iter()
242 .map(i64::to_string)
243 .collect();
244 write!(
245 f,
246 "contact {} conflicts with {}",
247 self.contact_id,
248 ids.join(", ")
249 )
250 }
251 }
252}
253
254impl std::error::Error for ContactConflict {}