Skip to main content

jmap_chat_client/methods/
contact.rs

1//! JMAP Chat — ChatContact/* method implementations on SessionClient.
2//!
3//! Spec: JMAP Chat draft §5 (ChatContact/get, /changes, /set, /query, /queryChanges).
4
5use jmap_types::{Id, PatchObject, State};
6
7use super::{
8    ChangesResponse, ChatContactPatch, ChatContactQueryInput, GetResponse, QueryChangesResponse,
9    QueryResponse, SetResponse,
10};
11
12impl super::SessionClient {
13    /// Fetch ChatContact objects by IDs (JMAP Chat §5 ChatContact/get).
14    ///
15    /// If `ids` is `None`, returns all ChatContacts for the account.
16    pub async fn chat_contact_get(
17        &self,
18        ids: Option<&[Id]>,
19        properties: Option<&[&str]>,
20    ) -> Result<GetResponse<jmap_chat_types::ChatContact>, jmap_base_client::ClientError> {
21        let (api_url, account_id) = self.session_parts()?;
22        // Omit `ids` / `properties` when None — see the matching comment on
23        // `chat_get` for the rationale (consistent with set/changes/query).
24        let mut args = serde_json::json!({ "accountId": account_id });
25        if let Some(id_slice) = ids {
26            args["ids"] = serde_json::to_value(id_slice).expect("Id slice Serialize is infallible");
27        }
28        if let Some(props) = properties {
29            args["properties"] = serde_json::Value::Array(
30                props.iter().copied().map(serde_json::Value::from).collect(),
31            );
32        }
33        let req = super::build_request("ChatContact/get", args, super::USING_CHAT);
34        let resp = self.call_internal(api_url, &req).await?;
35        jmap_base_client::extract_response(&resp, super::CALL_ID)
36    }
37
38    /// Fetch changes to ChatContact objects since `since_state` (RFC 8620 §5.2).
39    pub async fn chat_contact_changes(
40        &self,
41        since_state: &State,
42        max_changes: Option<u64>,
43    ) -> Result<ChangesResponse, jmap_base_client::ClientError> {
44        // Defence-in-depth: see `chat_changes`.
45        if since_state.as_ref().is_empty() {
46            return Err(jmap_base_client::ClientError::InvalidArgument(
47                "chat_contact_changes: since_state may not be empty".into(),
48            ));
49        }
50        let (api_url, account_id) = self.session_parts()?;
51        let mut args = serde_json::json!({
52            "accountId": account_id,
53            "sinceState": since_state,
54        });
55        if let Some(mc) = max_changes {
56            args["maxChanges"] = mc.into();
57        }
58        let req = super::build_request("ChatContact/changes", args, super::USING_CHAT);
59        let resp = self.call_internal(api_url, &req).await?;
60        jmap_base_client::extract_response(&resp, super::CALL_ID)
61    }
62
63    /// Update ChatContact properties (JMAP Chat §ChatContact/set).
64    ///
65    /// Supports `blocked` (Boolean) and `displayName` (nullable String).
66    /// Create and destroy are not supported by spec; the server returns `forbidden`.
67    pub async fn chat_contact_update(
68        &self,
69        id: &Id,
70        patch: &ChatContactPatch<'_>,
71    ) -> Result<SetResponse, jmap_base_client::ClientError> {
72        let (api_url, account_id) = self.session_parts()?;
73        let mut patch_map = serde_json::Map::new();
74        if let Some(b) = patch.blocked {
75            patch_map.insert("blocked".into(), b.into());
76        }
77        if let Some(entry) = patch
78            .display_name
79            .map_entry()
80            .map_err(jmap_base_client::ClientError::Parse)?
81        {
82            patch_map.insert("displayName".into(), entry);
83        }
84        // Wrap the constructed map in a PatchObject (RFC 8620 §5.3) before
85        // serializing. Wire bytes are unchanged because PatchObject is
86        // #[serde(transparent)]; the typed boundary documents the contract.
87        let patch_value = serde_json::Value::Object(PatchObject::from_map(patch_map).into_inner());
88        let args = serde_json::json!({
89            "accountId": account_id,
90            "update": { id.as_ref(): patch_value },
91        });
92        let req = super::build_request("ChatContact/set", args, super::USING_CHAT);
93        let resp = self.call_internal(api_url, &req).await?;
94        jmap_base_client::extract_response(&resp, super::CALL_ID)
95    }
96
97    /// Query ChatContact IDs with optional filter (JMAP Chat §ChatContact/query).
98    ///
99    /// Supported filter keys: `blocked`, `presence`. Supported sort properties:
100    /// `"lastSeenAt"`, `"login"`, `"lastActiveAt"`.
101    pub async fn chat_contact_query(
102        &self,
103        input: &ChatContactQueryInput,
104    ) -> Result<QueryResponse, jmap_base_client::ClientError> {
105        let (api_url, account_id) = self.session_parts()?;
106        let mut filter = serde_json::Map::new();
107        if let Some(b) = input.filter_blocked {
108            filter.insert("blocked".into(), b.into());
109        }
110        if let Some(p) = &input.filter_presence {
111            filter.insert(
112                "presence".into(),
113                serde_json::to_value(p).map_err(jmap_base_client::ClientError::Parse)?,
114            );
115        }
116        let filter_val = if filter.is_empty() {
117            serde_json::Value::Null
118        } else {
119            serde_json::Value::Object(filter)
120        };
121        let mut args = serde_json::json!({
122            "accountId": account_id,
123            "filter": filter_val,
124        });
125        if let Some(sp) = &input.sort_property {
126            let property =
127                serde_json::to_value(sp).map_err(jmap_base_client::ClientError::Parse)?;
128            args["sort"] = serde_json::json!([{
129                "property": property,
130                "isAscending": input.sort_ascending.unwrap_or(false),
131            }]);
132        }
133        if let Some(p) = input.position {
134            args["position"] = p.into();
135        }
136        if let Some(l) = input.limit {
137            args["limit"] = l.into();
138        }
139        let req = super::build_request("ChatContact/query", args, super::USING_CHAT);
140        let resp = self.call_internal(api_url, &req).await?;
141        jmap_base_client::extract_response(&resp, super::CALL_ID)
142    }
143
144    /// Fetch query-result changes for ChatContact since `since_query_state`
145    /// (RFC 8620 §5.6 / ChatContact/queryChanges).
146    pub async fn chat_contact_query_changes(
147        &self,
148        since_query_state: &State,
149        max_changes: Option<u64>,
150    ) -> Result<QueryChangesResponse, jmap_base_client::ClientError> {
151        // Defence-in-depth: see `chat_changes`.
152        if since_query_state.as_ref().is_empty() {
153            return Err(jmap_base_client::ClientError::InvalidArgument(
154                "chat_contact_query_changes: since_query_state may not be empty".into(),
155            ));
156        }
157        let (api_url, account_id) = self.session_parts()?;
158        let mut args = serde_json::json!({
159            "accountId": account_id,
160            "sinceQueryState": since_query_state,
161        });
162        if let Some(mc) = max_changes {
163            args["maxChanges"] = mc.into();
164        }
165        let req = super::build_request("ChatContact/queryChanges", args, super::USING_CHAT);
166        let resp = self.call_internal(api_url, &req).await?;
167        jmap_base_client::extract_response(&resp, super::CALL_ID)
168    }
169}