Skip to main content

jmap_chat_client/methods/
misc.rs

1//! Miscellaneous JMAP Chat method implementations on SessionClient.
2//!
3//! Covers the smaller object surfaces — `ReadPosition/*`, `PresenceStatus/*`,
4//! and `PushSubscription/*` (RFC 8620 §7.2 plus the JMAP Chat Push extension,
5//! draft-atwood-jmap-chat-push-00).
6
7use jmap_types::{Id, PatchObject, State};
8
9use super::{
10    ChangesResponse, GetResponse, Patch, PresenceStatusPatch, PushSubscriptionCreateInput,
11    PushSubscriptionCreateResponse, PushSubscriptionPatch, SetResponse,
12};
13
14impl super::SessionClient {
15    /// Fetch ReadPosition objects by IDs (JMAP Chat §5 ReadPosition/get).
16    ///
17    /// If `ids` is `None`, returns all ReadPosition records for the account.
18    /// The server creates one ReadPosition per Chat automatically.
19    ///
20    /// # Errors
21    ///
22    /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
23    ///   if the bound session has no primary account for
24    ///   `urn:ietf:params:jmap:chat`.
25    /// - Any transport / protocol variant returned by
26    ///   [`JmapClient::call`](jmap_base_client::JmapClient::call):
27    ///   [`Http`](jmap_base_client::ClientError::Http),
28    ///   [`Parse`](jmap_base_client::ClientError::Parse),
29    ///   [`AuthFailed`](jmap_base_client::ClientError::AuthFailed),
30    ///   [`MethodError`](jmap_base_client::ClientError::MethodError)
31    ///   (wraps RFC 8620 §3.6.2 method-level errors such as
32    ///   `accountNotFound`, `invalidArguments`, `serverFail`),
33    ///   [`MethodNotFound`](jmap_base_client::ClientError::MethodNotFound),
34    ///   [`ResponseTooLarge`](jmap_base_client::ClientError::ResponseTooLarge),
35    ///   or
36    ///   [`UnexpectedResponse`](jmap_base_client::ClientError::UnexpectedResponse).
37    pub async fn read_position_get(
38        &self,
39        ids: Option<&[Id]>,
40    ) -> Result<GetResponse<jmap_chat_types::ReadPosition>, jmap_base_client::ClientError> {
41        let (api_url, account_id) = self.session_parts()?;
42        // Omit `ids` when None — see the matching comment on `chat_get` for
43        // the rationale. ReadPosition/get has no `properties` parameter.
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        let req = super::build_request("ReadPosition/get", args, super::USING_CHAT);
50        let resp = self.call_internal(api_url, &req).await?;
51        jmap_base_client::extract_response(&resp, super::CALL_ID)
52    }
53
54    /// Update the read position for a Chat (JMAP Chat §5 ReadPosition/set).
55    ///
56    /// `read_position_id` is the server-assigned ReadPosition.id (from
57    /// `read_position_get`). `last_read_message_id` is the Message.id of the
58    /// most recent message read. The server updates `lastReadAt` and
59    /// recomputes `Chat.unreadCount`.
60    ///
61    /// `create` and `destroy` are forbidden by the spec; only `update` is issued.
62    ///
63    /// # Errors
64    ///
65    /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
66    ///   if the bound session has no primary account for
67    ///   `urn:ietf:params:jmap:chat`.
68    /// - Any transport / protocol variant returned by
69    ///   [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
70    ///   the matching error list on [`Self::read_position_get`]. /set
71    ///   update errors appear in [`SetResponse::not_updated`] rather
72    ///   than as [`Err`].
73    pub async fn read_position_update(
74        &self,
75        read_position_id: &Id,
76        last_read_message_id: &Id,
77    ) -> Result<SetResponse, jmap_base_client::ClientError> {
78        let (api_url, account_id) = self.session_parts()?;
79        let args = serde_json::json!({
80            "accountId": account_id,
81            "update": {
82                read_position_id.as_ref(): { "lastReadMessageId": last_read_message_id }
83            },
84        });
85        let req = super::build_request("ReadPosition/set", args, super::USING_CHAT);
86        let resp = self.call_internal(api_url, &req).await?;
87        jmap_base_client::extract_response(&resp, super::CALL_ID)
88    }
89
90    /// Fetch the singleton PresenceStatus record (JMAP Chat §5 PresenceStatus/get).
91    ///
92    /// Per spec there is exactly one PresenceStatus per account; `ids: null`
93    /// retrieves it.
94    ///
95    /// # Errors
96    ///
97    /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
98    ///   if the bound session has no primary account for
99    ///   `urn:ietf:params:jmap:chat`.
100    /// - Any transport / protocol variant returned by
101    ///   [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
102    ///   the matching error list on [`Self::read_position_get`].
103    pub async fn presence_status_get(
104        &self,
105    ) -> Result<GetResponse<jmap_chat_types::PresenceStatus>, jmap_base_client::ClientError> {
106        let (api_url, account_id) = self.session_parts()?;
107        let args = serde_json::json!({
108            "accountId": account_id,
109            "ids": None::<&[Id]>,
110        });
111        let req = super::build_request("PresenceStatus/get", args, super::USING_CHAT);
112        let resp = self.call_internal(api_url, &req).await?;
113        jmap_base_client::extract_response(&resp, super::CALL_ID)
114    }
115
116    /// Fetch changes to ReadPosition records since `since_state` (JMAP Chat §5 ReadPosition/changes).
117    ///
118    /// `max_changes` may be `None` to let the server choose the limit (RFC 8620 §5.2).
119    ///
120    /// # Errors
121    ///
122    /// - [`ClientError::InvalidArgument`](jmap_base_client::ClientError::InvalidArgument)
123    ///   if `since_state` is the empty string (defence-in-depth —
124    ///   `State` constructed via [`State::from`](jmap_types::State::from)
125    ///   accepts empty strings, but an empty `sinceState` is never
126    ///   useful and would otherwise generate a wasted round-trip).
127    /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
128    ///   if the bound session has no primary account for
129    ///   `urn:ietf:params:jmap:chat`.
130    /// - Any transport / protocol variant returned by
131    ///   [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
132    ///   the matching error list on [`Self::read_position_get`].
133    pub async fn read_position_changes(
134        &self,
135        since_state: &State,
136        max_changes: Option<u64>,
137    ) -> Result<ChangesResponse, jmap_base_client::ClientError> {
138        // Defence-in-depth: see `chat_changes`.
139        if since_state.as_ref().is_empty() {
140            return Err(jmap_base_client::ClientError::InvalidArgument(
141                "read_position_changes: since_state may not be empty".into(),
142            ));
143        }
144        let (api_url, account_id) = self.session_parts()?;
145        let mut args = serde_json::json!({
146            "accountId": account_id,
147            "sinceState": since_state,
148        });
149        if let Some(mc) = max_changes {
150            args["maxChanges"] = mc.into();
151        }
152        let req = super::build_request("ReadPosition/changes", args, super::USING_CHAT);
153        let resp = self.call_internal(api_url, &req).await?;
154        jmap_base_client::extract_response(&resp, super::CALL_ID)
155    }
156
157    /// Update the PresenceStatus record (JMAP Chat §5 PresenceStatus/set).
158    ///
159    /// Only `update` is issued; `create` and `destroy` are forbidden by the spec.
160    /// Fields absent from `patch` (i.e. `Patch::Keep` or `None`) are omitted from
161    /// the patch and left unchanged server-side.
162    ///
163    /// # Errors
164    ///
165    /// - [`ClientError::Parse`](jmap_base_client::ClientError::Parse) if
166    ///   serializing the typed `presence` enum or any `Clearable`
167    ///   entry (`status_text`, `status_emoji`, `expires_at`) fails
168    ///   (pathological conditions only).
169    /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
170    ///   if the bound session has no primary account for
171    ///   `urn:ietf:params:jmap:chat`.
172    /// - Any transport / protocol variant returned by
173    ///   [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
174    ///   the matching error list on [`Self::read_position_get`]. /set
175    ///   update errors appear in [`SetResponse::not_updated`] rather
176    ///   than as [`Err`].
177    pub async fn presence_status_update(
178        &self,
179        id: &Id,
180        patch: &PresenceStatusPatch<'_>,
181    ) -> Result<SetResponse, jmap_base_client::ClientError> {
182        let (api_url, account_id) = self.session_parts()?;
183        let mut patch_map = serde_json::Map::new();
184        if let Some(p) = &patch.presence {
185            patch_map.insert(
186                "presence".into(),
187                serde_json::to_value(p).map_err(jmap_base_client::ClientError::from_parse)?,
188            );
189        }
190        if let Some(entry) = patch
191            .status_text
192            .map_entry()
193            .map_err(jmap_base_client::ClientError::from_parse)?
194        {
195            patch_map.insert("statusText".into(), entry);
196        }
197        if let Some(entry) = patch
198            .status_emoji
199            .map_entry()
200            .map_err(jmap_base_client::ClientError::from_parse)?
201        {
202            patch_map.insert("statusEmoji".into(), entry);
203        }
204        if let Some(entry) = patch
205            .expires_at
206            .map_entry()
207            .map_err(jmap_base_client::ClientError::from_parse)?
208        {
209            patch_map.insert("expiresAt".into(), entry);
210        }
211        if let Some(rs) = patch.receipt_sharing {
212            patch_map.insert("receiptSharing".into(), rs.into());
213        }
214        // Wrap the constructed map in a PatchObject (RFC 8620 §5.3) before
215        // serializing. Wire bytes are unchanged because PatchObject is
216        // #[serde(transparent)]; the typed boundary documents the contract.
217        let patch_value = serde_json::Value::Object(PatchObject::from_map(patch_map).into_inner());
218        let args = serde_json::json!({
219            "accountId": account_id,
220            "update": { id.as_ref(): patch_value },
221        });
222        let req = super::build_request("PresenceStatus/set", args, super::USING_CHAT);
223        let resp = self.call_internal(api_url, &req).await?;
224        jmap_base_client::extract_response(&resp, super::CALL_ID)
225    }
226
227    /// Fetch changes to PresenceStatus records since `since_state` (JMAP Chat §5 PresenceStatus/changes).
228    ///
229    /// `max_changes` may be `None` to let the server choose the limit (RFC 8620 §5.2).
230    ///
231    /// # Errors
232    ///
233    /// - [`ClientError::InvalidArgument`](jmap_base_client::ClientError::InvalidArgument)
234    ///   if `since_state` is the empty string (defence-in-depth
235    ///   empty-state guard).
236    /// - [`ClientError::InvalidSession`](jmap_base_client::ClientError::InvalidSession)
237    ///   if the bound session has no primary account for
238    ///   `urn:ietf:params:jmap:chat`.
239    /// - Any transport / protocol variant returned by
240    ///   [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
241    ///   the matching error list on [`Self::read_position_get`].
242    pub async fn presence_status_changes(
243        &self,
244        since_state: &State,
245        max_changes: Option<u64>,
246    ) -> Result<ChangesResponse, jmap_base_client::ClientError> {
247        // Defence-in-depth: see `chat_changes`.
248        if since_state.as_ref().is_empty() {
249            return Err(jmap_base_client::ClientError::InvalidArgument(
250                "presence_status_changes: since_state may not be empty".into(),
251            ));
252        }
253        let (api_url, account_id) = self.session_parts()?;
254        let mut args = serde_json::json!({
255            "accountId": account_id,
256            "sinceState": since_state,
257        });
258        if let Some(mc) = max_changes {
259            args["maxChanges"] = mc.into();
260        }
261        let req = super::build_request("PresenceStatus/changes", args, super::USING_CHAT);
262        let resp = self.call_internal(api_url, &req).await?;
263        jmap_base_client::extract_response(&resp, super::CALL_ID)
264    }
265
266    /// Create a PushSubscription with the optional `chatPush` extension
267    /// (RFC 8620 §7.2 / draft-atwood-jmap-chat-push-00 §3).
268    ///
269    /// PushSubscriptions are account-independent: no `accountId` is included
270    /// in the request (RFC 8620 §7.2). When `input.chat_push` is `Some`, the
271    /// `using` array includes `urn:ietf:params:jmap:chat:push` (RFC 8620 §3.3:
272    /// capabilities MUST only be declared when used); otherwise `urn:ietf:params:jmap:core`
273    /// alone is used.
274    ///
275    /// This method issues a `create` operation only. To extend `expires`, set
276    /// the verification code, change `types`, or update `chatPush`, use
277    /// [`push_subscription_update`](Self::push_subscription_update). To
278    /// unsubscribe, use [`push_subscription_destroy`](Self::push_subscription_destroy).
279    ///
280    /// When `input.client_id` is `None`, a ULID is generated automatically.
281    ///
282    /// # Errors
283    ///
284    /// - [`ClientError::InvalidArgument`](jmap_base_client::ClientError::InvalidArgument)
285    ///   if `input.device_client_id` or `input.url` is empty
286    ///   (caller-precondition guards), or if `input.chat_push` carries
287    ///   duplicate `accountId` keys in the slice.
288    /// - [`ClientError::Parse`](jmap_base_client::ClientError::Parse) if
289    ///   serializing any typed [`ChatPushConfig`](jmap_chat_types::ChatPushConfig)
290    ///   value fails (pathological conditions only).
291    /// - Any transport / protocol variant returned by
292    ///   [`JmapClient::call`](jmap_base_client::JmapClient::call):
293    ///   [`Http`](jmap_base_client::ClientError::Http),
294    ///   [`Parse`](jmap_base_client::ClientError::Parse),
295    ///   [`AuthFailed`](jmap_base_client::ClientError::AuthFailed),
296    ///   [`MethodError`](jmap_base_client::ClientError::MethodError)
297    ///   (wraps RFC 8620 §3.6.2 method-level errors; servers that do
298    ///   not advertise the `chatPush` capability return
299    ///   `unknownCapability` when the input includes a chat-push
300    ///   sub-object),
301    ///   [`MethodNotFound`](jmap_base_client::ClientError::MethodNotFound),
302    ///   [`ResponseTooLarge`](jmap_base_client::ClientError::ResponseTooLarge),
303    ///   or
304    ///   [`UnexpectedResponse`](jmap_base_client::ClientError::UnexpectedResponse).
305    ///
306    /// Note: `push_subscription_*` methods are not account-scoped (RFC
307    /// 8620 §7.2) and therefore do NOT call `session_parts()`. The
308    /// session-derived account check that other `Self::*` methods make
309    /// before any wire call is skipped here; missing-account problems
310    /// surface only as transport / method-level errors from the server.
311    pub async fn push_subscription_create(
312        &self,
313        input: &PushSubscriptionCreateInput<'_>,
314    ) -> Result<PushSubscriptionCreateResponse, jmap_base_client::ClientError> {
315        if input.device_client_id.is_empty() {
316            return Err(jmap_base_client::ClientError::InvalidArgument(
317                "push_subscription_create: device_client_id may not be empty".into(),
318            ));
319        }
320        if input.url.is_empty() {
321            return Err(jmap_base_client::ClientError::InvalidArgument(
322                "push_subscription_create: url may not be empty".into(),
323            ));
324        }
325        // PushSubscriptions are not account-scoped; use api_url without session_parts().
326        let api_url = self.api_url();
327        let client_id = super::resolve_client_id(input.client_id);
328        let mut create_obj = serde_json::json!({
329            "deviceClientId": input.device_client_id,
330            "url": input.url,
331        });
332        if let Some(exp) = input.expires {
333            create_obj["expires"] = exp.as_ref().into();
334        }
335        if let Some(types) = input.types {
336            create_obj["types"] = serde_json::Value::Array(
337                types.iter().copied().map(serde_json::Value::from).collect(),
338            );
339        }
340        let has_chat_push = input.chat_push.is_some();
341        if let Some(cp) = input.chat_push {
342            let chat_push_map = build_chat_push_map(cp, "push_subscription_create")?;
343            create_obj["chatPush"] = serde_json::Value::Object(chat_push_map);
344        }
345        let args = serde_json::json!({
346            "create": { client_id: create_obj }
347        });
348        // RFC 8620 §3.3: only declare the chatPush capability when it is actually used.
349        let using = if has_chat_push {
350            super::USING_CHAT_PUSH
351        } else {
352            super::USING_CORE
353        };
354        let req = super::build_request("PushSubscription/set", args, using);
355        let resp = self.call_internal(api_url, &req).await?;
356        jmap_base_client::extract_response(&resp, super::CALL_ID)
357    }
358
359    /// Update a PushSubscription (RFC 8620 §7.2.2 `PushSubscription/set` update).
360    ///
361    /// Issues a `PushSubscription/set` request with only the `update` sub-map
362    /// populated. RFC 8620 §7.2 declares `url`, `keys`, and `deviceClientId`
363    /// immutable; to change those, destroy the subscription and create a new
364    /// one. The patchable properties are exposed via [`PushSubscriptionPatch`]:
365    /// `verificationCode`, `expires`, `types`, and the JMAP Chat Push
366    /// extension's `chatPush`.
367    ///
368    /// PushSubscriptions are not account-scoped (RFC 8620 §7.2): no
369    /// `accountId` is sent. When the patch touches `chat_push` at all
370    /// (any non-[`Patch::Keep`] value), the `using` array includes
371    /// `urn:ietf:params:jmap:chat:push`; otherwise only
372    /// `urn:ietf:params:jmap:core` is declared (RFC 8620 §3.3).
373    ///
374    /// # Errors
375    ///
376    /// - [`ClientError::InvalidArgument`](jmap_base_client::ClientError::InvalidArgument)
377    ///   if `id` is empty (defence-in-depth — typed `&Id` does not
378    ///   itself prevent empty values), or if `patch.chat_push` is
379    ///   [`Patch::Set`] and the slice contains duplicate `accountId`
380    ///   keys.
381    /// - [`ClientError::Parse`](jmap_base_client::ClientError::Parse) if
382    ///   serializing the `expires` `Clearable` entry or any
383    ///   [`ChatPushConfig`](jmap_chat_types::ChatPushConfig) value
384    ///   fails (pathological conditions only).
385    /// - Any transport / protocol variant returned by
386    ///   [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
387    ///   the matching error list on [`Self::push_subscription_create`].
388    ///   /set update errors appear in [`SetResponse::not_updated`]
389    ///   rather than as [`Err`].
390    pub async fn push_subscription_update(
391        &self,
392        id: &Id,
393        patch: &PushSubscriptionPatch<'_>,
394    ) -> Result<SetResponse, jmap_base_client::ClientError> {
395        // Defence-in-depth: typed &Id does not prevent empty Id values.
396        if id.as_ref().is_empty() {
397            return Err(jmap_base_client::ClientError::InvalidArgument(
398                "push_subscription_update: id may not be empty".into(),
399            ));
400        }
401
402        let api_url = self.api_url();
403        let mut patch_map = serde_json::Map::new();
404        if let Some(code) = patch.verification_code {
405            patch_map.insert("verificationCode".into(), code.into());
406        }
407        if let Some(entry) = patch
408            .expires
409            .map_entry()
410            .map_err(jmap_base_client::ClientError::from_parse)?
411        {
412            patch_map.insert("expires".into(), entry);
413        }
414        match &patch.types {
415            Patch::Keep => {}
416            Patch::Clear => {
417                patch_map.insert("types".into(), serde_json::Value::Null);
418            }
419            Patch::Set(types) => {
420                patch_map.insert(
421                    "types".into(),
422                    serde_json::Value::Array(
423                        types.iter().copied().map(serde_json::Value::from).collect(),
424                    ),
425                );
426            }
427        }
428        match &patch.chat_push {
429            Patch::Keep => {}
430            Patch::Clear => {
431                patch_map.insert("chatPush".into(), serde_json::Value::Null);
432            }
433            Patch::Set(cp) => {
434                let chat_push_map = build_chat_push_map(cp, "push_subscription_update")?;
435                patch_map.insert("chatPush".into(), serde_json::Value::Object(chat_push_map));
436            }
437        }
438
439        let patch_value = serde_json::Value::Object(PatchObject::from_map(patch_map).into_inner());
440        let args = serde_json::json!({
441            "update": { id.as_ref(): patch_value }
442        });
443        let using = if patch.chat_push.is_keep() {
444            super::USING_CORE
445        } else {
446            super::USING_CHAT_PUSH
447        };
448        let req = super::build_request("PushSubscription/set", args, using);
449        let resp = self.call_internal(api_url, &req).await?;
450        jmap_base_client::extract_response(&resp, super::CALL_ID)
451    }
452
453    /// Destroy one or more PushSubscriptions (RFC 8620 §7.2.2 `PushSubscription/set` destroy).
454    ///
455    /// Issues a `PushSubscription/set` request with only the `destroy` array
456    /// populated. PushSubscriptions are not account-scoped (RFC 8620 §7.2):
457    /// no `accountId` is sent. Only `urn:ietf:params:jmap:core` is declared
458    /// — destroying never requires the chatPush capability since it is a
459    /// property-blind operation.
460    ///
461    /// Clients SHOULD NOT destroy a PushSubscription they did not create —
462    /// RFC 8620 §7.2 reserves that to clients that recognise the
463    /// `deviceClientId`. This client does not enforce that rule; the server
464    /// may reject the call.
465    ///
466    /// # Errors
467    ///
468    /// - [`ClientError::InvalidArgument`](jmap_base_client::ClientError::InvalidArgument)
469    ///   if `ids` is empty (a destroy call with no ids would be a no-op
470    ///   round-trip).
471    /// - Any transport / protocol variant returned by
472    ///   [`JmapClient::call`](jmap_base_client::JmapClient::call) — see
473    ///   the matching error list on [`Self::push_subscription_create`].
474    ///   /set destroy errors appear in [`SetResponse::not_destroyed`]
475    ///   rather than as [`Err`].
476    pub async fn push_subscription_destroy(
477        &self,
478        ids: &[Id],
479    ) -> Result<SetResponse, jmap_base_client::ClientError> {
480        if ids.is_empty() {
481            return Err(jmap_base_client::ClientError::InvalidArgument(
482                "push_subscription_destroy: ids may not be empty".into(),
483            ));
484        }
485        let api_url = self.api_url();
486        let args = serde_json::json!({
487            "destroy": ids,
488        });
489        let req = super::build_request("PushSubscription/set", args, super::USING_CORE);
490        let resp = self.call_internal(api_url, &req).await?;
491        jmap_base_client::extract_response(&resp, super::CALL_ID)
492    }
493}
494
495// ---------------------------------------------------------------------------
496// chat_push map builder (shared by push_subscription_create + _update)
497// ---------------------------------------------------------------------------
498
499/// Build a `chatPush` wire object from a slice of `(accountId, config)`
500/// pairs (draft-atwood-jmap-chat-push-00 §3.1).
501///
502/// Returns `InvalidArgument` if the slice contains a duplicate `accountId`
503/// — duplicate accountIds in the patch are an argument-validation error
504/// regardless of whether the request can otherwise be sent. The
505/// `context` string is included in the error message so callers can
506/// distinguish `push_subscription_create` from `push_subscription_update`
507/// in diagnostics.
508///
509/// Single-pass: each insert detects collision via the returned previous
510/// value, replacing the previous two-pass (HashSet duplicate check then
511/// serialize-and-insert) idiom that was hand-duplicated across two
512/// call sites (bd:JMAP-26di.55).
513fn build_chat_push_map(
514    cp: &[(&Id, jmap_chat_types::ChatPushConfig)],
515    context: &'static str,
516) -> Result<serde_json::Map<String, serde_json::Value>, jmap_base_client::ClientError> {
517    let mut chat_push_map = serde_json::Map::new();
518    for (account_id, config) in cp {
519        let key = account_id.as_ref().to_owned();
520        let value =
521            serde_json::to_value(config).map_err(jmap_base_client::ClientError::from_parse)?;
522        if chat_push_map.insert(key, value).is_some() {
523            return Err(jmap_base_client::ClientError::InvalidArgument(format!(
524                "{context}: duplicate accountId '{account_id}' in chat_push"
525            )));
526        }
527    }
528    Ok(chat_push_map)
529}