jmap_chat_client/methods/custom_emoji.rs
1//! CustomEmoji method implementations for SessionClient.
2//!
3//! Spec: JMAP Chat draft §4.16 (CustomEmoji/get, /changes, /set, /query, /queryChanges)
4//! Capability: urn:ietf:params:jmap:chat
5
6use jmap_types::{Id, State};
7
8use super::{
9 ChangesResponse, CustomEmojiCreateInput, CustomEmojiQueryInput, GetResponse,
10 QueryChangesResponse, QueryResponse, SetResponse,
11};
12
13impl super::SessionClient {
14 /// Fetch CustomEmoji objects by IDs (JMAP Chat §4.16 CustomEmoji/get).
15 ///
16 /// If `ids` is `None`, returns all CustomEmoji objects visible to the account
17 /// (Space-specific and server-global).
18 ///
19 /// # Errors
20 ///
21 /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
22 /// if the bound session has no primary account for
23 /// `urn:ietf:params:jmap:chat`.
24 /// - Any transport / protocol variant returned by
25 /// [`JmapClient::call`](jmap_base_client::JmapClient::call):
26 /// [`Http`](jmap_base_client::ClientError::Http),
27 /// [`Parse`](jmap_base_client::ClientError::Parse),
28 /// [`AuthFailed`](jmap_base_client::ClientError::AuthFailed),
29 /// [`MethodError`](jmap_base_client::ClientError::MethodError)
30 /// (wraps RFC 8620 §3.6.2 method-level errors such as
31 /// `accountNotFound`, `invalidArguments`, `serverFail`),
32 /// [`MethodNotFound`](jmap_base_client::ClientError::MethodNotFound),
33 /// [`ResponseTooLarge`](jmap_base_client::ClientError::ResponseTooLarge),
34 /// or
35 /// [`UnexpectedResponse`](jmap_base_client::ClientError::UnexpectedResponse).
36 pub async fn custom_emoji_get(
37 &self,
38 ids: Option<&[Id]>,
39 properties: Option<&[&str]>,
40 ) -> Result<GetResponse<jmap_chat_types::CustomEmoji>, jmap_base_client::ClientError> {
41 let (api_url, account_id) = self.session_parts()?;
42 // Omit `ids` / `properties` when None — see the matching comment on
43 // `chat_get` for the rationale (consistent with set/changes/query).
44 let mut args = serde_json::json!({ "accountId": account_id });
45 if let Some(id_slice) = ids {
46 args["ids"] = serde_json::to_value(id_slice)
47 .map_err(jmap_base_client::ClientError::from_parse)?;
48 }
49 if let Some(props) = properties {
50 args["properties"] =
51 serde_json::to_value(props).map_err(jmap_base_client::ClientError::from_parse)?;
52 }
53 let req = super::build_request("CustomEmoji/get", args, super::USING_CHAT);
54 let resp = self.call_internal(api_url, &req).await?;
55 jmap_base_client::extract_response(&resp, super::CALL_ID)
56 }
57
58 /// Fetch changes to CustomEmoji objects since `since_state`
59 /// (RFC 8620 §5.2 / JMAP Chat §4.16 CustomEmoji/changes).
60 ///
61 /// # Errors
62 ///
63 /// - [`ClientError::InvalidArgument`](jmap_base_client::ClientError::InvalidArgument)
64 /// if `since_state` is the empty string (defence-in-depth —
65 /// `State` constructed via [`State::from`](jmap_types::State::from)
66 /// accepts empty strings, but an empty `sinceState` is never
67 /// useful and would otherwise generate a wasted round-trip).
68 /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
69 /// if the bound session has no primary account for
70 /// `urn:ietf:params:jmap:chat`.
71 /// - Any transport / protocol variant returned by
72 /// [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
73 /// the matching error list on [`Self::custom_emoji_get`].
74 pub async fn custom_emoji_changes(
75 &self,
76 since_state: &State,
77 max_changes: Option<u64>,
78 ) -> Result<ChangesResponse, jmap_base_client::ClientError> {
79 // Defence-in-depth: see `chat_changes`.
80 if since_state.as_ref().is_empty() {
81 return Err(jmap_base_client::ClientError::InvalidArgument(
82 "custom_emoji_changes: since_state may not be empty".into(),
83 ));
84 }
85 let (api_url, account_id) = self.session_parts()?;
86 let mut args = serde_json::json!({
87 "accountId": account_id,
88 "sinceState": since_state,
89 });
90 if let Some(mc) = max_changes {
91 args["maxChanges"] = mc.into();
92 }
93 let req = super::build_request("CustomEmoji/changes", args, super::USING_CHAT);
94 let resp = self.call_internal(api_url, &req).await?;
95 jmap_base_client::extract_response(&resp, super::CALL_ID)
96 }
97
98 /// Create a CustomEmoji (RFC 8620 §5.3 / JMAP Chat §4.16 CustomEmoji/set create).
99 ///
100 /// When `input.client_id` is `None`, a ULID is generated automatically.
101 ///
102 /// # Errors
103 ///
104 /// - [`ClientError::InvalidArgument`](jmap_base_client::ClientError::InvalidArgument)
105 /// if `input.name` is empty (caller-precondition guard — emoji
106 /// shortcodes require a non-empty name).
107 /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
108 /// if the bound session has no primary account for
109 /// `urn:ietf:params:jmap:chat`.
110 /// - Any transport / protocol variant returned by
111 /// [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
112 /// the matching error list on [`Self::custom_emoji_get`]. /set
113 /// create errors (e.g. `invalidProperties`, `forbidden`,
114 /// `alreadyExists`) appear in
115 /// [`SetResponse::not_created`] rather than as [`Err`].
116 pub async fn custom_emoji_create(
117 &self,
118 input: &CustomEmojiCreateInput<'_>,
119 ) -> Result<SetResponse, jmap_base_client::ClientError> {
120 if input.name.is_empty() {
121 return Err(jmap_base_client::ClientError::InvalidArgument(
122 "custom_emoji_create: name may not be empty".into(),
123 ));
124 }
125 let (api_url, account_id) = self.session_parts()?;
126 let mut create_obj = serde_json::json!({
127 "name": input.name,
128 "blobId": input.blob_id,
129 });
130 if let Some(sid) = input.space_id {
131 create_obj["spaceId"] = sid.as_ref().into();
132 }
133 let client_id = super::resolve_client_id(input.client_id);
134 let args = serde_json::json!({
135 "accountId": account_id,
136 "create": { client_id: create_obj },
137 });
138 let req = super::build_request("CustomEmoji/set", args, super::USING_CHAT);
139 let resp = self.call_internal(api_url, &req).await?;
140 jmap_base_client::extract_response(&resp, super::CALL_ID)
141 }
142
143 /// Destroy CustomEmoji objects (RFC 8620 §5.3 / JMAP Chat §4.16 CustomEmoji/set destroy).
144 ///
145 /// `ids` must be non-empty; the guard fires before any network call.
146 ///
147 /// # Errors
148 ///
149 /// - [`ClientError::InvalidArgument`](jmap_base_client::ClientError::InvalidArgument)
150 /// if `ids` is empty (caller-precondition guard — a no-op destroy
151 /// is never useful).
152 /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
153 /// if the bound session has no primary account for
154 /// `urn:ietf:params:jmap:chat`.
155 /// - Any transport / protocol variant returned by
156 /// [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
157 /// the matching error list on [`Self::custom_emoji_get`]. /set
158 /// destroy errors appear in
159 /// [`SetResponse::not_destroyed`] rather than as [`Err`].
160 pub async fn custom_emoji_destroy(
161 &self,
162 ids: &[Id],
163 ) -> Result<SetResponse, jmap_base_client::ClientError> {
164 if ids.is_empty() {
165 return Err(jmap_base_client::ClientError::InvalidArgument(
166 "custom_emoji_destroy: ids may not be empty".into(),
167 ));
168 }
169 let (api_url, account_id) = self.session_parts()?;
170 let args = serde_json::json!({
171 "accountId": account_id,
172 "destroy": ids,
173 });
174 let req = super::build_request("CustomEmoji/set", args, super::USING_CHAT);
175 let resp = self.call_internal(api_url, &req).await?;
176 jmap_base_client::extract_response(&resp, super::CALL_ID)
177 }
178
179 /// Query CustomEmoji IDs (RFC 8620 §5.5 / JMAP Chat §4.16 CustomEmoji/query).
180 ///
181 /// # Errors
182 ///
183 /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
184 /// if the bound session has no primary account for
185 /// `urn:ietf:params:jmap:chat`.
186 /// - Any transport / protocol variant returned by
187 /// [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
188 /// the matching error list on [`Self::custom_emoji_get`]. RFC
189 /// 8620 §5.5 defines additional /query method-level errors
190 /// (`anchorNotFound`, `unsupportedFilter`, `unsupportedSort`,
191 /// `tooManyChanges`) that surface as
192 /// [`MethodError`](jmap_base_client::ClientError::MethodError).
193 pub async fn custom_emoji_query(
194 &self,
195 input: &CustomEmojiQueryInput<'_>,
196 ) -> Result<QueryResponse, jmap_base_client::ClientError> {
197 let (api_url, account_id) = self.session_parts()?;
198 let mut args = serde_json::json!({
199 "accountId": account_id,
200 });
201 if let Some(sid) = input.filter_space_id {
202 args["filter"] = serde_json::json!({ "spaceId": sid });
203 }
204 if let Some(p) = input.position {
205 args["position"] = p.into();
206 }
207 if let Some(l) = input.limit {
208 args["limit"] = l.into();
209 }
210 let req = super::build_request("CustomEmoji/query", args, super::USING_CHAT);
211 let resp = self.call_internal(api_url, &req).await?;
212 jmap_base_client::extract_response(&resp, super::CALL_ID)
213 }
214
215 /// Fetch query-result changes for CustomEmoji since `since_query_state`
216 /// (RFC 8620 §5.6 / JMAP Chat §4.16 CustomEmoji/queryChanges).
217 ///
218 /// `filter` and `sort` MUST match the `filter` / `sort` passed to the
219 /// original `CustomEmoji/query` call that returned `since_query_state`
220 /// — RFC 8620 §5.6 is explicit that the server uses them to compute
221 /// which entries entered or left the result set.
222 ///
223 /// `up_to_id` is the highest-index id the client has cached;
224 /// `calculate_total` requests the new total result count.
225 ///
226 /// # Errors
227 ///
228 /// - [`ClientError::InvalidArgument`](jmap_base_client::ClientError::InvalidArgument)
229 /// if `since_query_state` is the empty string (defence-in-depth
230 /// empty-state guard; see [`Self::custom_emoji_changes`]).
231 /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
232 /// if the bound session has no primary account for
233 /// `urn:ietf:params:jmap:chat`.
234 /// - Any transport / protocol variant returned by
235 /// [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
236 /// the matching error list on [`Self::custom_emoji_get`]. RFC
237 /// 8620 §5.6 also defines `cannotCalculateChanges` (returned when
238 /// the server cannot honour the request given the supplied
239 /// filter / sort); it surfaces as
240 /// [`MethodError`](jmap_base_client::ClientError::MethodError).
241 pub async fn custom_emoji_query_changes(
242 &self,
243 since_query_state: &State,
244 max_changes: Option<u64>,
245 filter: Option<serde_json::Value>,
246 sort: Option<serde_json::Value>,
247 up_to_id: Option<&Id>,
248 calculate_total: Option<bool>,
249 ) -> Result<QueryChangesResponse, jmap_base_client::ClientError> {
250 // Defence-in-depth: see `chat_changes`.
251 if since_query_state.as_ref().is_empty() {
252 return Err(jmap_base_client::ClientError::InvalidArgument(
253 "custom_emoji_query_changes: since_query_state may not be empty".into(),
254 ));
255 }
256 let (api_url, account_id) = self.session_parts()?;
257 let mut args = serde_json::json!({
258 "accountId": account_id,
259 "sinceQueryState": since_query_state,
260 });
261 if let Some(f) = filter {
262 args["filter"] = f;
263 }
264 if let Some(s) = sort {
265 args["sort"] = s;
266 }
267 if let Some(mc) = max_changes {
268 args["maxChanges"] = mc.into();
269 }
270 if let Some(uti) = up_to_id {
271 args["upToId"] =
272 serde_json::to_value(uti).map_err(jmap_base_client::ClientError::from_parse)?;
273 }
274 if let Some(ct) = calculate_total {
275 args["calculateTotal"] = ct.into();
276 }
277 let req = super::build_request("CustomEmoji/queryChanges", args, super::USING_CHAT);
278 let resp = self.call_internal(api_url, &req).await?;
279 jmap_base_client::extract_response(&resp, super::CALL_ID)
280 }
281}