Skip to main content

inline_client/
sdk_backend.rs

1//! SDK-backed production backend.
2//!
3//! This backend owns the configured `inline-sdk` clients, durable store,
4//! multiplexed realtime session, bucket sync, and transaction state behind the
5//! native client API.
6
7use std::{
8    collections::{BTreeMap, HashMap, HashSet},
9    fmt,
10    sync::{
11        Arc,
12        atomic::{AtomicBool, Ordering},
13    },
14    time::{SystemTime, UNIX_EPOCH},
15};
16
17use futures_util::future::BoxFuture;
18use inline_sdk::{
19    ApiClient, ApiError, AuthMetadata, ClientIdentity, RealtimeError, RealtimeEvent,
20    RealtimeEventReceiver, RealtimeSession, RpcRequest, UploadFileBytesInput, UploadFileResult,
21    UploadFileType, UploadVideoMetadata, proto,
22};
23use serde_json::Value;
24use tokio::sync::Mutex;
25
26use crate::backend::retry_after_seconds_from_message;
27use crate::sync::{SyncHost, SyncManager};
28use crate::{
29    AccountStateSnapshot, AddChatParticipantRequest, AuthContactKind, AuthCredential,
30    AuthStartRequest, AuthStartResult, AuthToken, AuthVerifyRequest, AuthVerifyResult,
31    BackendError, BackendResult, ChatCreateParticipant, ChatParticipantRecord,
32    ChatParticipantsPage, ChatParticipantsRequest, ChatStateSnapshot, ClientBackend,
33    ClientErrorCategory, ClientEvent, ClientStatusSnapshot, ClientStore, ConnectRequest,
34    CreateDmRequest, CreateReplyThreadRequest, CreateThreadRequest, CreatedChat, DeleteChatRequest,
35    DeleteMessageRequest, DialogFollowMode, DialogNotificationMode, DialogRecord, DialogsPage,
36    DialogsRequest, EditMessageRequest, HistoryPage, HistoryRequest, InMemoryStore, InlineId,
37    MediaKind, MessageContent, MessageMutation, MessageRecord, NotificationMode, OperationOutcome,
38    PeerRef, RandomId, ReactRequest, ReadRequest, RealtimeConnectRequest, RealtimeConnector,
39    RemoveChatParticipantRequest, SendTextOutcome, SendTextRequest, SetMarkedUnreadRequest,
40    SpaceMemberRecord, SpaceMemberRole, SpaceRecord, StoreError, StoredReaction, StoredReadState,
41    StoredSession, StoredTransaction, SyncConfig, TransactionId, TransactionIdentity,
42    TransactionState, TypingRequest, UpdateChatInfoRequest, UpdateDialogNotificationsRequest,
43    UploadRequest, UserRecord, UserSettingsRecord, VERSION,
44};
45
46const DEFAULT_API_BASE_URL: &str = "https://api.inline.chat/v1";
47const DEFAULT_REALTIME_URL: &str = "wss://api.inline.chat/realtime";
48const SNAPSHOT_HISTORY_PAGE_LIMIT: i32 = 200;
49
50/// Error returned when building an [`SdkBackend`].
51#[derive(Debug, thiserror::Error)]
52#[non_exhaustive]
53pub enum SdkBackendBuildError {
54    /// API client configuration failed.
55    #[error("failed to build Inline API client: {0}")]
56    Api(#[from] ApiError),
57}
58
59/// Builder for [`SdkBackend`].
60#[derive(Clone)]
61pub struct SdkBackendBuilder {
62    api_base_url: String,
63    realtime_url: String,
64    identity: ClientIdentity,
65    store: Arc<dyn ClientStore>,
66    sync_config: SyncConfig,
67    realtime_handshake: bool,
68    realtime_connector: Option<Arc<dyn RealtimeConnector>>,
69}
70
71impl fmt::Debug for SdkBackendBuilder {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.debug_struct("SdkBackendBuilder")
74            .field("api_base_url", &self.api_base_url)
75            .field("realtime_url", &redacted_url_for_debug(&self.realtime_url))
76            .field("identity", &self.identity)
77            .field("store", &"<client-store>")
78            .field("sync_config", &self.sync_config)
79            .field("realtime_handshake", &self.realtime_handshake)
80            .field(
81                "realtime_connector",
82                &self
83                    .realtime_connector
84                    .as_ref()
85                    .map(|_| "<realtime-connector>"),
86            )
87            .finish()
88    }
89}
90
91impl Default for SdkBackendBuilder {
92    fn default() -> Self {
93        Self {
94            api_base_url: DEFAULT_API_BASE_URL.to_owned(),
95            realtime_url: DEFAULT_REALTIME_URL.to_owned(),
96            identity: ClientIdentity::new("inline-client", VERSION),
97            store: Arc::new(InMemoryStore::new()),
98            sync_config: SyncConfig::default(),
99            realtime_handshake: false,
100            realtime_connector: None,
101        }
102    }
103}
104
105impl SdkBackendBuilder {
106    /// Sets the Inline API base URL.
107    pub fn api_base_url(mut self, api_base_url: impl Into<String>) -> Self {
108        self.api_base_url = api_base_url.into();
109        self
110    }
111
112    /// Sets the Inline realtime WebSocket URL.
113    pub fn realtime_url(mut self, realtime_url: impl Into<String>) -> Self {
114        self.realtime_url = realtime_url.into();
115        self
116    }
117
118    /// Sets the client identity sent through SDK transports.
119    pub fn identity(mut self, identity: ClientIdentity) -> Self {
120        self.identity = identity;
121        self
122    }
123
124    /// Sets the client store.
125    pub fn store(mut self, store: impl ClientStore) -> Self {
126        self.store = Arc::new(store);
127        self
128    }
129
130    /// Sets a shared client store.
131    pub fn shared_store(mut self, store: Arc<dyn ClientStore>) -> Self {
132        self.store = store;
133        self
134    }
135
136    /// Sets Inline update discovery and bucket catch-up policy.
137    pub fn sync_config(mut self, config: SyncConfig) -> Self {
138        self.sync_config = config;
139        self
140    }
141
142    /// Enables realtime handshake using the SDK realtime connector.
143    pub fn enable_realtime_handshake(mut self) -> Self {
144        self.realtime_handshake = true;
145        self.realtime_connector = None;
146        self
147    }
148
149    /// Sets a custom realtime connector.
150    pub fn realtime_connector(mut self, connector: impl RealtimeConnector) -> Self {
151        self.realtime_handshake = true;
152        self.realtime_connector = Some(Arc::new(connector));
153        self
154    }
155
156    /// Sets a shared realtime connector.
157    pub fn shared_realtime_connector(mut self, connector: Arc<dyn RealtimeConnector>) -> Self {
158        self.realtime_handshake = true;
159        self.realtime_connector = Some(connector);
160        self
161    }
162
163    /// Disables realtime handshake on connect.
164    pub fn without_realtime_handshake(mut self) -> Self {
165        self.realtime_handshake = false;
166        self.realtime_connector = None;
167        self
168    }
169
170    /// Builds an SDK-backed backend.
171    pub fn build(self) -> Result<SdkBackend, SdkBackendBuildError> {
172        let api = ApiClient::try_new_with_identity(self.api_base_url, self.identity.clone())?;
173        let sync = Arc::new(SyncManager::new(self.store.clone(), self.sync_config));
174        Ok(SdkBackend {
175            api,
176            realtime_url: self.realtime_url,
177            identity: self.identity,
178            store: self.store,
179            sync,
180            sync_required: Arc::new(AtomicBool::new(true)),
181            realtime_handshake: self.realtime_handshake,
182            realtime_connector: self.realtime_connector,
183            realtime: Arc::new(Mutex::new(None)),
184            realtime_events: Arc::new(Mutex::new(None)),
185        })
186    }
187}
188
189/// Production-facing backend composed from `inline-sdk` and a client store.
190#[derive(Clone)]
191pub struct SdkBackend {
192    api: ApiClient,
193    realtime_url: String,
194    identity: ClientIdentity,
195    store: Arc<dyn ClientStore>,
196    sync: Arc<SyncManager>,
197    sync_required: Arc<AtomicBool>,
198    realtime_handshake: bool,
199    realtime_connector: Option<Arc<dyn RealtimeConnector>>,
200    realtime: Arc<Mutex<Option<RealtimeSession>>>,
201    realtime_events: Arc<Mutex<Option<RealtimeEventReceiver>>>,
202}
203
204impl fmt::Debug for SdkBackend {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        f.debug_struct("SdkBackend")
207            .field("api", &self.api)
208            .field("realtime_url", &redacted_url_for_debug(&self.realtime_url))
209            .field("identity", &self.identity)
210            .field("store", &"<client-store>")
211            .field("sync_required", &self.sync_required.load(Ordering::Relaxed))
212            .field("realtime_handshake", &self.realtime_handshake)
213            .field(
214                "realtime_connector",
215                &self
216                    .realtime_connector
217                    .as_ref()
218                    .map(|_| "<realtime-connector>"),
219            )
220            .finish()
221    }
222}
223
224impl SdkBackend {
225    /// Starts an SDK backend builder.
226    pub fn builder() -> SdkBackendBuilder {
227        SdkBackendBuilder::default()
228    }
229
230    /// Returns the configured API client.
231    pub fn api_client(&self) -> &ApiClient {
232        &self.api
233    }
234
235    /// Returns the configured realtime URL.
236    pub fn realtime_url(&self) -> &str {
237        &self.realtime_url
238    }
239
240    /// Returns the configured client identity.
241    pub fn identity(&self) -> &ClientIdentity {
242        &self.identity
243    }
244
245    /// Returns the shared store.
246    pub fn store(&self) -> Arc<dyn ClientStore> {
247        self.store.clone()
248    }
249
250    /// Returns whether connect will perform a realtime handshake.
251    pub fn realtime_handshake_enabled(&self) -> bool {
252        self.realtime_handshake
253    }
254
255    async fn require_session(&self) -> BackendResult<StoredSession> {
256        self.store
257            .load_session()
258            .await
259            .map_err(store_error_to_backend)?
260            .ok_or_else(|| {
261                BackendError::new(ClientErrorCategory::AuthRequired, "client is not connected")
262            })
263    }
264
265    async fn call_realtime<R>(
266        &self,
267        session: &StoredSession,
268        request: R,
269    ) -> BackendResult<R::Response>
270    where
271        R: RpcRequest,
272    {
273        let realtime = self.ensure_realtime(session).await?;
274        match realtime.call(request).await {
275            Ok(response) => Ok(response),
276            Err(error) => {
277                if realtime_error_closes_session(&error) {
278                    self.clear_realtime().await;
279                }
280                Err(realtime_error_to_backend(error))
281            }
282        }
283    }
284
285    async fn ensure_realtime(&self, session: &StoredSession) -> BackendResult<RealtimeSession> {
286        let mut realtime = self.realtime.lock().await;
287        if let Some(existing) = realtime.as_ref()
288            && !existing.is_closed()
289        {
290            return Ok(existing.clone());
291        }
292
293        let connected = RealtimeSession::connect_with_identity(
294            &self.realtime_url,
295            session.auth.access_token().expose_secret(),
296            self.identity.clone(),
297        )
298        .await
299        .map_err(realtime_error_to_backend)?;
300        let receiver = connected.subscribe();
301        *self.realtime_events.lock().await = Some(receiver);
302        *realtime = Some(connected.clone());
303        Ok(connected)
304    }
305
306    async fn install_realtime(&self, realtime: RealtimeSession) {
307        let receiver = realtime.subscribe();
308        *self.realtime.lock().await = Some(realtime);
309        *self.realtime_events.lock().await = Some(receiver);
310    }
311
312    async fn clear_realtime(&self) {
313        *self.realtime.lock().await = None;
314        *self.realtime_events.lock().await = None;
315        self.sync_required.store(true, Ordering::Release);
316    }
317
318    async fn receive_realtime_event(
319        &self,
320        session: &StoredSession,
321    ) -> BackendResult<RealtimeEvent> {
322        self.ensure_realtime(session).await?;
323        let mut receiver = self.realtime_events.lock().await;
324        let Some(events) = receiver.as_mut() else {
325            return Err(BackendError::new(
326                ClientErrorCategory::Internal,
327                "realtime event receiver was not initialized",
328            ));
329        };
330        match events.recv().await {
331            Ok(event) => Ok(event),
332            Err(error) => {
333                self.sync_required.store(true, Ordering::Release);
334                drop(receiver);
335                if realtime_error_closes_session(&error) {
336                    self.clear_realtime().await;
337                }
338                Err(realtime_error_to_backend(error))
339            }
340        }
341    }
342
343    async fn connect_with_auth(
344        &self,
345        request: ConnectRequest,
346    ) -> BackendResult<ClientStatusSnapshot> {
347        let existing_session = self
348            .store
349            .load_session()
350            .await
351            .map_err(store_error_to_backend)?;
352        let next_namespace = normalized_account_namespace(request.account_namespace.as_deref());
353        let account_changed = existing_session.as_ref().is_none_or(|session| {
354            normalized_account_namespace(session.account_namespace.as_deref()) != next_namespace
355        });
356        let connected = if self.realtime_handshake {
357            if let Some(connector) = &self.realtime_connector {
358                connector
359                    .connect(RealtimeConnectRequest::new(
360                        self.realtime_url.clone(),
361                        request.auth.access_token().clone(),
362                        self.identity.clone(),
363                    ))
364                    .await?;
365                None
366            } else {
367                Some(
368                    RealtimeSession::connect_with_identity(
369                        &self.realtime_url,
370                        request.auth.access_token().expose_secret(),
371                        self.identity.clone(),
372                    )
373                    .await
374                    .map_err(realtime_error_to_backend)?,
375                )
376            }
377        } else {
378            None
379        };
380        self.clear_realtime().await;
381        if account_changed {
382            self.store
383                .clear_account_data()
384                .await
385                .map_err(store_error_to_backend)?;
386        }
387        self.store
388            .save_session(StoredSession {
389                auth: request.auth,
390                account_namespace: request.account_namespace,
391            })
392            .await
393            .map_err(store_error_to_backend)?;
394        if let Some(connected) = connected {
395            self.install_realtime(connected).await;
396        }
397        Ok(ClientStatusSnapshot::current(
398            crate::ClientStatus::Connected,
399        ))
400    }
401
402    fn auth_metadata(
403        &self,
404        kind: AuthContactKind,
405        contact: &str,
406        device_name: Option<&str>,
407    ) -> AuthMetadata {
408        let device_id = format!(
409            "inline-client-{:016x}",
410            stable_hash(&format!("{}:{contact}", kind.as_str()))
411        );
412        let metadata = AuthMetadata::new(device_id, self.identity.clone());
413        match device_name
414            .map(str::trim)
415            .filter(|device_name| !device_name.is_empty())
416        {
417            Some(device_name) => metadata.with_device_name(device_name),
418            None => metadata.with_device_name(self.identity.client_type()),
419        }
420    }
421
422    async fn resume_stored_session(&self) -> BackendResult<ClientStatusSnapshot> {
423        let Some(session) = self
424            .store
425            .load_session()
426            .await
427            .map_err(store_error_to_backend)?
428        else {
429            self.clear_realtime().await;
430            return Ok(ClientStatusSnapshot::current(
431                crate::ClientStatus::AuthRequired,
432            ));
433        };
434
435        self.clear_realtime().await;
436        if self.realtime_handshake {
437            if let Some(connector) = &self.realtime_connector {
438                connector
439                    .connect(RealtimeConnectRequest::new(
440                        self.realtime_url.clone(),
441                        session.auth.access_token().clone(),
442                        self.identity.clone(),
443                    ))
444                    .await?;
445            } else {
446                let connected = RealtimeSession::connect_with_identity(
447                    &self.realtime_url,
448                    session.auth.access_token().expose_secret(),
449                    self.identity.clone(),
450                )
451                .await
452                .map_err(realtime_error_to_backend)?;
453                self.install_realtime(connected).await;
454            }
455        }
456        Ok(ClientStatusSnapshot::current(
457            crate::ClientStatus::Connected,
458        ))
459    }
460}
461
462impl ClientBackend for SdkBackend {
463    fn auth_start(
464        &self,
465        request: AuthStartRequest,
466    ) -> BoxFuture<'static, BackendResult<AuthStartResult>> {
467        let backend = self.clone();
468        Box::pin(async move {
469            let contact = request.contact.trim().to_owned();
470            if contact.is_empty() {
471                return Err(BackendError::new(
472                    ClientErrorCategory::InvalidInput,
473                    "auth contact must not be empty",
474                ));
475            }
476            let metadata =
477                backend.auth_metadata(request.kind, &contact, request.device_name.as_deref());
478            let result = match request.kind {
479                AuthContactKind::Email => backend
480                    .api
481                    .send_email_code(&contact, &metadata)
482                    .await
483                    .map_err(api_error_to_backend)?,
484                AuthContactKind::Phone => backend
485                    .api
486                    .send_sms_code(&contact, &metadata)
487                    .await
488                    .map_err(api_error_to_backend)?,
489            };
490            Ok(AuthStartResult {
491                existing_user: result.existing_user,
492                needs_invite_code: result.needs_invite_code,
493                challenge_token: result.challenge_token,
494            })
495        })
496    }
497
498    fn auth_verify(
499        &self,
500        request: AuthVerifyRequest,
501    ) -> BoxFuture<'static, BackendResult<AuthVerifyResult>> {
502        let backend = self.clone();
503        Box::pin(async move {
504            let contact = request.contact.trim().to_owned();
505            let code = request.code.trim().to_owned();
506            if contact.is_empty() {
507                return Err(BackendError::new(
508                    ClientErrorCategory::InvalidInput,
509                    "auth contact must not be empty",
510                ));
511            }
512            if code.is_empty() {
513                return Err(BackendError::new(
514                    ClientErrorCategory::InvalidInput,
515                    "verification code must not be empty",
516                ));
517            }
518
519            let metadata =
520                backend.auth_metadata(request.kind, &contact, request.device_name.as_deref());
521            let result = match request.kind {
522                AuthContactKind::Email => backend
523                    .api
524                    .verify_email_code(
525                        &contact,
526                        &code,
527                        request.challenge_token.as_deref(),
528                        &metadata,
529                    )
530                    .await
531                    .map_err(api_error_to_backend)?,
532                AuthContactKind::Phone => backend
533                    .api
534                    .verify_sms_code(&contact, &code, &metadata)
535                    .await
536                    .map_err(api_error_to_backend)?,
537            };
538            let token = AuthToken::try_new(result.token).map_err(|error| {
539                BackendError::new(ClientErrorCategory::ProtocolMismatch, error.to_string())
540            })?;
541            let account_namespace = request
542                .account_namespace
543                .map(|namespace| namespace.trim().to_owned())
544                .filter(|namespace| !namespace.is_empty())
545                .unwrap_or_else(|| result.user_id.to_string());
546            let status = backend
547                .connect_with_auth(
548                    ConnectRequest::new(AuthCredential::AccessToken { token })
549                        .with_account_namespace(account_namespace.clone()),
550                )
551                .await?;
552            Ok(AuthVerifyResult {
553                user_id: InlineId::new(result.user_id),
554                account_namespace,
555                status,
556            })
557        })
558    }
559
560    fn resume_session(&self) -> BoxFuture<'static, BackendResult<ClientStatusSnapshot>> {
561        let backend = self.clone();
562        Box::pin(async move { backend.resume_stored_session().await })
563    }
564
565    fn connect(
566        &self,
567        request: ConnectRequest,
568    ) -> BoxFuture<'static, BackendResult<ClientStatusSnapshot>> {
569        let backend = self.clone();
570        Box::pin(async move { backend.connect_with_auth(request).await })
571    }
572
573    fn logout(&self) -> BoxFuture<'static, BackendResult<()>> {
574        let backend = self.clone();
575        Box::pin(async move {
576            let session = backend
577                .store
578                .load_session()
579                .await
580                .map_err(store_error_to_backend)?;
581            backend.clear_realtime().await;
582            if let Some(session) = session
583                && let Err(error) = backend
584                    .api
585                    .logout(session.auth.access_token().expose_secret())
586                    .await
587            {
588                log::warn!("Inline remote session logout failed; clearing local session: {error}");
589            }
590            backend
591                .store
592                .clear_session()
593                .await
594                .map_err(store_error_to_backend)?;
595            backend
596                .store
597                .clear_account_data()
598                .await
599                .map_err(store_error_to_backend)
600        })
601    }
602
603    fn dialogs(&self, request: DialogsRequest) -> BoxFuture<'static, BackendResult<DialogsPage>> {
604        let backend = self.clone();
605        Box::pin(async move {
606            let session = backend.require_session().await?;
607            let result = backend
608                .call_realtime(&session, proto::GetChatsInput {})
609                .await?;
610            backend.apply_get_chats_snapshot(result).await?;
611            backend
612                .store
613                .dialogs(request)
614                .await
615                .map_err(store_error_to_backend)
616        })
617    }
618
619    fn cached_dialogs(
620        &self,
621        request: DialogsRequest,
622    ) -> BoxFuture<'static, BackendResult<DialogsPage>> {
623        let backend = self.clone();
624        Box::pin(async move {
625            backend
626                .store
627                .dialogs(request)
628                .await
629                .map_err(store_error_to_backend)
630        })
631    }
632
633    fn account_state(&self) -> BoxFuture<'static, BackendResult<AccountStateSnapshot>> {
634        let backend = self.clone();
635        Box::pin(async move {
636            let deleted_chat_ids = backend
637                .store
638                .deleted_chat_ids()
639                .await
640                .map_err(store_error_to_backend)?;
641            Ok(AccountStateSnapshot { deleted_chat_ids })
642        })
643    }
644
645    fn chat_state(
646        &self,
647        chat_id: InlineId,
648    ) -> BoxFuture<'static, BackendResult<ChatStateSnapshot>> {
649        let backend = self.clone();
650        Box::pin(async move {
651            let dialog = backend
652                .store
653                .dialog(chat_id)
654                .await
655                .map_err(store_error_to_backend)?;
656            let deleted = backend
657                .store
658                .deleted_chat_ids()
659                .await
660                .map_err(store_error_to_backend)?
661                .contains(&chat_id);
662            let deleted_message_ids = backend
663                .store
664                .deleted_message_ids(chat_id)
665                .await
666                .map_err(store_error_to_backend)?;
667            let reactions = backend
668                .store
669                .reactions_for_chat(chat_id)
670                .await
671                .map_err(store_error_to_backend)?;
672            let reaction_snapshot_message_ids = backend
673                .store
674                .reaction_snapshot_message_ids(chat_id)
675                .await
676                .map_err(store_error_to_backend)?;
677            let read_state = backend
678                .store
679                .read_state(chat_id)
680                .await
681                .map_err(store_error_to_backend)?;
682            let participants = backend
683                .store
684                .chat_participants(chat_id)
685                .await
686                .map_err(store_error_to_backend)?;
687            let participants_complete = backend
688                .store
689                .chat_participants_complete(chat_id)
690                .await
691                .map_err(store_error_to_backend)?;
692            Ok(ChatStateSnapshot {
693                chat_id,
694                dialog,
695                deleted,
696                deleted_message_ids,
697                reactions,
698                reaction_snapshot_message_ids,
699                read_state,
700                participants,
701                participants_complete,
702            })
703        })
704    }
705
706    fn history(&self, request: HistoryRequest) -> BoxFuture<'static, BackendResult<HistoryPage>> {
707        let backend = self.clone();
708        Box::pin(async move {
709            let session = backend.require_session().await?;
710            if request.before_message_id.is_some() && request.after_message_id.is_some() {
711                return Err(BackendError::new(
712                    ClientErrorCategory::InvalidInput,
713                    "history request cannot specify both before_message_id and after_message_id",
714                ));
715            }
716            let limit = request.limit.unwrap_or(50).max(1);
717            let fetch_limit = limit.saturating_add(1).min(i32::MAX as u32) as i32;
718            let result = backend
719                .call_realtime(&session, history_input_for_request(&request, fetch_limit))
720                .await?;
721            let mut records = Vec::with_capacity(result.messages.len());
722            for message in result.messages {
723                records.push(
724                    backend
725                        .record_proto_message(message, Some(request.chat_id), None)
726                        .await?,
727                );
728            }
729            let (records, has_more) =
730                normalize_live_history_records(records, limit, request.after_message_id.is_some());
731            Ok(HistoryPage {
732                messages: records,
733                users: Vec::new(),
734                has_more,
735                next_cursor: None,
736            })
737        })
738    }
739
740    fn cached_history(
741        &self,
742        request: HistoryRequest,
743    ) -> BoxFuture<'static, BackendResult<HistoryPage>> {
744        let backend = self.clone();
745        Box::pin(async move {
746            backend
747                .store
748                .history(request)
749                .await
750                .map_err(store_error_to_backend)
751        })
752    }
753
754    fn chat_participants(
755        &self,
756        request: ChatParticipantsRequest,
757    ) -> BoxFuture<'static, BackendResult<ChatParticipantsPage>> {
758        let backend = self.clone();
759        Box::pin(async move {
760            let session = backend.require_session().await?;
761            let result = backend
762                .call_realtime(
763                    &session,
764                    proto::GetChatParticipantsInput {
765                        chat_id: request.chat_id.get(),
766                    },
767                )
768                .await?;
769            let page = chat_participants_page_from_proto(result);
770            backend
771                .store
772                .record_users(page.users.clone())
773                .await
774                .map_err(store_error_to_backend)?;
775            backend
776                .store
777                .record_chat_participants(request.chat_id, page.participants.clone())
778                .await
779                .map_err(store_error_to_backend)?;
780            Ok(page)
781        })
782    }
783
784    fn add_chat_participant(
785        &self,
786        request: AddChatParticipantRequest,
787    ) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
788        let backend = self.clone();
789        Box::pin(async move {
790            validate_chat_and_user_ids(request.chat_id, request.user_id)?;
791            let session = backend.require_session().await?;
792            let result = backend
793                .call_realtime(
794                    &session,
795                    proto::AddChatParticipantInput {
796                        chat_id: request.chat_id.get(),
797                        user_id: request.user_id.get(),
798                    },
799                )
800                .await?;
801            let participant = result.participant.ok_or_else(|| {
802                BackendError::new(
803                    ClientErrorCategory::ProtocolMismatch,
804                    "add chat participant result did not include the participant",
805                )
806            })?;
807            backend
808                .store
809                .record_chat_participant(
810                    request.chat_id,
811                    ChatParticipantRecord {
812                        user_id: InlineId::new(participant.user_id),
813                        date: Some(participant.date),
814                    },
815                )
816                .await
817                .map_err(store_error_to_backend)?;
818            Ok(OperationOutcome::with_events(vec![
819                ClientEvent::ChatParticipantsChanged {
820                    chat_id: request.chat_id,
821                },
822            ]))
823        })
824    }
825
826    fn remove_chat_participant(
827        &self,
828        request: RemoveChatParticipantRequest,
829    ) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
830        let backend = self.clone();
831        Box::pin(async move {
832            validate_chat_and_user_ids(request.chat_id, request.user_id)?;
833            let session = backend.require_session().await?;
834            backend
835                .call_realtime(
836                    &session,
837                    proto::RemoveChatParticipantInput {
838                        chat_id: request.chat_id.get(),
839                        user_id: request.user_id.get(),
840                    },
841                )
842                .await?;
843            backend
844                .store
845                .remove_chat_participant(request.chat_id, request.user_id)
846                .await
847                .map_err(store_error_to_backend)?;
848            Ok(OperationOutcome::with_events(vec![
849                ClientEvent::ChatParticipantsChanged {
850                    chat_id: request.chat_id,
851                },
852            ]))
853        })
854    }
855
856    fn update_chat_info(
857        &self,
858        request: UpdateChatInfoRequest,
859    ) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
860        let backend = self.clone();
861        Box::pin(async move {
862            if request.chat_id.get() <= 0 {
863                return Err(BackendError::new(
864                    ClientErrorCategory::InvalidInput,
865                    "chat_id must be positive",
866                ));
867            }
868            if request.title.is_none() && request.emoji.is_none() {
869                return Err(BackendError::new(
870                    ClientErrorCategory::InvalidInput,
871                    "at least one chat info field must be provided",
872                ));
873            }
874            let title = request.title.map(|title| title.trim().to_owned());
875            if title.as_deref().is_some_and(str::is_empty) {
876                return Err(BackendError::new(
877                    ClientErrorCategory::InvalidInput,
878                    "chat title must not be empty",
879                ));
880            }
881            let emoji = request.emoji.map(|emoji| emoji.trim().to_owned());
882            let session = backend.require_session().await?;
883            let result = backend
884                .call_realtime(
885                    &session,
886                    proto::UpdateChatInfoInput {
887                        chat_id: request.chat_id.get(),
888                        title,
889                        emoji,
890                    },
891                )
892                .await?;
893            let chat = result.chat.ok_or_else(|| {
894                BackendError::new(
895                    ClientErrorCategory::ProtocolMismatch,
896                    "update chat info result did not include the chat",
897                )
898            })?;
899            Ok(OperationOutcome::with_events(
900                backend.record_chat_update(chat, None, None).await?,
901            ))
902        })
903    }
904
905    fn delete_chat(
906        &self,
907        request: DeleteChatRequest,
908    ) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
909        let backend = self.clone();
910        Box::pin(async move {
911            if request.chat_id.get() <= 0 {
912                return Err(BackendError::new(
913                    ClientErrorCategory::InvalidInput,
914                    "chat_id must be positive",
915                ));
916            }
917            let session = backend.require_session().await?;
918            backend
919                .call_realtime(
920                    &session,
921                    proto::DeleteChatInput {
922                        peer_id: Some(input_peer_for_chat(request.chat_id)),
923                    },
924                )
925                .await?;
926            backend
927                .store
928                .remove_dialog(request.chat_id)
929                .await
930                .map_err(store_error_to_backend)?;
931            Ok(OperationOutcome::with_events(vec![
932                ClientEvent::ChatDeleted {
933                    chat_id: request.chat_id,
934                },
935            ]))
936        })
937    }
938
939    fn create_dm(
940        &self,
941        request: CreateDmRequest,
942    ) -> BoxFuture<'static, BackendResult<CreatedChat>> {
943        let backend = self.clone();
944        Box::pin(async move {
945            if request.user_id.get() <= 0 {
946                return Err(BackendError::new(
947                    ClientErrorCategory::InvalidInput,
948                    "user_id must be positive",
949                ));
950            }
951            let session = backend.require_session().await?;
952            let result = backend
953                .api
954                .create_private_chat(
955                    session.auth.access_token().expose_secret(),
956                    request.user_id.get(),
957                )
958                .await
959                .map_err(api_error_to_backend)?;
960            let (created, user) = created_chat_from_private_chat_result(
961                result.chat,
962                result.dialog,
963                result.user,
964                request.user_id,
965            )?;
966            backend
967                .store
968                .record_dialog(DialogRecord {
969                    chat_id: created.chat_id,
970                    peer_user_id: Some(request.user_id),
971                    title: created.title.clone(),
972                    last_message_id: None,
973                    synced_through_message_id: None,
974                    unread_count: Some(0),
975                    ..DialogRecord::new(created.chat_id)
976                })
977                .await
978                .map_err(store_error_to_backend)?;
979            if let Some(user) = user {
980                backend
981                    .store
982                    .record_users(vec![user])
983                    .await
984                    .map_err(store_error_to_backend)?;
985            }
986            Ok(created)
987        })
988    }
989
990    fn create_thread(
991        &self,
992        request: CreateThreadRequest,
993    ) -> BoxFuture<'static, BackendResult<CreatedChat>> {
994        let backend = self.clone();
995        Box::pin(async move {
996            validate_create_participants(&request.participants)?;
997            let session = backend.require_session().await?;
998            let result = backend
999                .call_realtime(
1000                    &session,
1001                    proto::CreateChatInput {
1002                        title: trimmed_option(request.title),
1003                        space_id: request.space_id.map(InlineId::get),
1004                        description: trimmed_option(request.description),
1005                        emoji: trimmed_option(request.emoji),
1006                        is_public: request.is_public,
1007                        participants: request
1008                            .participants
1009                            .into_iter()
1010                            .map(|participant| proto::InputChatParticipant {
1011                                user_id: participant.user_id.get(),
1012                            })
1013                            .collect(),
1014                        reserved_chat_id: None,
1015                    },
1016                )
1017                .await?;
1018            let created = created_chat_from_proto(result.chat, result.dialog, None, None, None)?;
1019            backend
1020                .store
1021                .record_dialog(DialogRecord {
1022                    chat_id: created.chat_id,
1023                    peer_user_id: None,
1024                    title: created.title.clone(),
1025                    last_message_id: None,
1026                    synced_through_message_id: None,
1027                    unread_count: Some(0),
1028                    ..DialogRecord::new(created.chat_id)
1029                })
1030                .await
1031                .map_err(store_error_to_backend)?;
1032            Ok(created)
1033        })
1034    }
1035
1036    fn create_reply_thread(
1037        &self,
1038        request: CreateReplyThreadRequest,
1039    ) -> BoxFuture<'static, BackendResult<CreatedChat>> {
1040        let backend = self.clone();
1041        Box::pin(async move {
1042            if request.parent_chat_id.get() <= 0 {
1043                return Err(BackendError::new(
1044                    ClientErrorCategory::InvalidInput,
1045                    "parent_chat_id must be positive",
1046                ));
1047            }
1048            validate_create_participants(&request.participants)?;
1049            let session = backend.require_session().await?;
1050            let result = backend
1051                .call_realtime(
1052                    &session,
1053                    proto::CreateSubthreadInput {
1054                        parent_chat_id: request.parent_chat_id.get(),
1055                        parent_message_id: request.parent_message_id.map(InlineId::get),
1056                        title: trimmed_option(request.title),
1057                        description: trimmed_option(request.description),
1058                        emoji: trimmed_option(request.emoji),
1059                        participants: request
1060                            .participants
1061                            .into_iter()
1062                            .map(|participant| proto::InputChatParticipant {
1063                                user_id: participant.user_id.get(),
1064                            })
1065                            .collect(),
1066                    },
1067                )
1068                .await?;
1069            let created = created_chat_from_proto(
1070                result.chat,
1071                result.dialog,
1072                result.anchor_message,
1073                Some(request.parent_chat_id),
1074                request.parent_message_id,
1075            )?;
1076            backend
1077                .store
1078                .record_dialog(DialogRecord {
1079                    chat_id: created.chat_id,
1080                    peer_user_id: None,
1081                    title: created.title.clone(),
1082                    last_message_id: None,
1083                    synced_through_message_id: None,
1084                    unread_count: Some(0),
1085                    ..DialogRecord::new(created.chat_id)
1086                })
1087                .await
1088                .map_err(store_error_to_backend)?;
1089            Ok(created)
1090        })
1091    }
1092
1093    fn send_text(
1094        &self,
1095        request: SendTextRequest,
1096    ) -> BoxFuture<'static, BackendResult<SendTextOutcome>> {
1097        let backend = self.clone();
1098        Box::pin(async move {
1099            if request.text.trim().is_empty() {
1100                return Err(BackendError::new(
1101                    ClientErrorCategory::InvalidInput,
1102                    "message text must not be empty",
1103                ));
1104            }
1105
1106            let session = backend.require_session().await?;
1107            let initial_chat_id = chat_id_for_peer(request.peer);
1108            let random_id = request
1109                .random_id
1110                .unwrap_or_else(|| random_id_for_request(&request));
1111            let transaction_id = transaction_id_for_send(&request, random_id);
1112            let new_identity = TransactionIdentity::new(
1113                transaction_id.clone(),
1114                request.external_id.clone(),
1115                random_id,
1116            );
1117            let existing = backend
1118                .store
1119                .transaction(transaction_id.clone())
1120                .await
1121                .map_err(store_error_to_backend)?;
1122            if let Some(existing) = existing.as_ref()
1123                && !stored_transaction_needs_retry(existing)
1124            {
1125                return Ok(outcome_from_stored_transaction(
1126                    existing.clone(),
1127                    initial_chat_id,
1128                ));
1129            }
1130            let identity = existing
1131                .map(|transaction| transaction.identity)
1132                .unwrap_or(new_identity);
1133
1134            if backend
1135                .store
1136                .transaction(transaction_id)
1137                .await
1138                .map_err(store_error_to_backend)?
1139                .is_none()
1140            {
1141                backend
1142                    .store
1143                    .record_transaction(
1144                        StoredTransaction::new(identity.clone(), TransactionState::Queued)
1145                            .with_chat_id(initial_chat_id),
1146                    )
1147                    .await
1148                    .map_err(store_error_to_backend)?;
1149            }
1150
1151            backend
1152                .store
1153                .record_transaction(
1154                    StoredTransaction::new(identity.clone(), TransactionState::Sent)
1155                        .with_chat_id(initial_chat_id),
1156                )
1157                .await
1158                .map_err(store_error_to_backend)?;
1159
1160            let input = proto::SendMessageInput {
1161                peer_id: Some(input_peer_for_client_peer(request.peer)),
1162                message: Some(request.text.clone()),
1163                reply_to_msg_id: request.reply_to_message_id.map(InlineId::get),
1164                random_id: Some(random_id.get()),
1165                media: None,
1166                temporary_send_date: Some(now_seconds()),
1167                is_sticker: None,
1168                has_link: None,
1169                entities: None,
1170                parse_markdown: Some(false),
1171                send_mode: None,
1172                actions: None,
1173            };
1174            let send_result = match backend.call_realtime(&session, input).await {
1175                Ok(result) => result,
1176                Err(backend_error) => {
1177                    backend
1178                        .record_transaction_error(
1179                            identity,
1180                            initial_chat_id,
1181                            TransactionState::Sent,
1182                            backend_error.clone(),
1183                        )
1184                        .await?;
1185                    return Err(backend_error);
1186                }
1187            };
1188
1189            let applied =
1190                apply_send_message_updates(&request, identity, initial_chat_id, send_result);
1191            if let Some(message) = &applied.message {
1192                backend
1193                    .store
1194                    .record_message(message.clone())
1195                    .await
1196                    .map_err(store_error_to_backend)?;
1197            }
1198            backend
1199                .store
1200                .record_transaction(applied.transaction.clone())
1201                .await
1202                .map_err(store_error_to_backend)?;
1203
1204            Ok(SendTextOutcome::with_state(
1205                MessageMutation {
1206                    transaction: applied.transaction.identity,
1207                    message_id: applied.message_id,
1208                    state: None,
1209                    failure: None,
1210                },
1211                applied.chat_id,
1212                applied.message_id,
1213                applied.message,
1214                applied.transaction.state,
1215                applied.transaction.failure,
1216            ))
1217        })
1218    }
1219
1220    fn send_media(
1221        &self,
1222        request: UploadRequest,
1223        bytes: Vec<u8>,
1224    ) -> BoxFuture<'static, BackendResult<SendTextOutcome>> {
1225        let backend = self.clone();
1226        Box::pin(async move {
1227            if bytes.is_empty() {
1228                return Err(BackendError::new(
1229                    ClientErrorCategory::InvalidInput,
1230                    "media bytes must not be empty",
1231                ));
1232            }
1233
1234            let session = backend.require_session().await?;
1235            let initial_chat_id = chat_id_for_peer(request.peer);
1236            let random_id = request
1237                .random_id
1238                .unwrap_or_else(|| random_id_for_upload_request(&request, bytes.len()));
1239            let transaction_id = transaction_id_for_upload(&request, random_id);
1240            let new_identity = TransactionIdentity::new(
1241                transaction_id.clone(),
1242                request.external_id.clone(),
1243                random_id,
1244            );
1245            let existing = backend
1246                .store
1247                .transaction(transaction_id.clone())
1248                .await
1249                .map_err(store_error_to_backend)?;
1250            if let Some(existing) = existing.as_ref()
1251                && !stored_transaction_needs_retry(existing)
1252            {
1253                return Ok(outcome_from_stored_transaction(
1254                    existing.clone(),
1255                    initial_chat_id,
1256                ));
1257            }
1258            let identity = existing
1259                .map(|transaction| transaction.identity)
1260                .unwrap_or(new_identity);
1261
1262            if backend
1263                .store
1264                .transaction(transaction_id)
1265                .await
1266                .map_err(store_error_to_backend)?
1267                .is_none()
1268            {
1269                backend
1270                    .store
1271                    .record_transaction(
1272                        StoredTransaction::new(identity.clone(), TransactionState::Queued)
1273                            .with_chat_id(initial_chat_id),
1274                    )
1275                    .await
1276                    .map_err(store_error_to_backend)?;
1277            }
1278
1279            let upload_input = upload_input_for_request(&request, bytes);
1280            let upload = match backend
1281                .api
1282                .upload_file_bytes(session.auth.access_token().expose_secret(), upload_input)
1283                .await
1284            {
1285                Ok(upload) => upload,
1286                Err(error) => {
1287                    let backend_error = api_error_to_backend(error);
1288                    backend
1289                        .record_transaction_error(
1290                            identity,
1291                            initial_chat_id,
1292                            TransactionState::Queued,
1293                            backend_error.clone(),
1294                        )
1295                        .await?;
1296                    return Err(backend_error);
1297                }
1298            };
1299            let media = match input_media_from_upload(&upload) {
1300                Ok(media) => media,
1301                Err(error) => {
1302                    backend
1303                        .record_transaction_error(
1304                            identity,
1305                            initial_chat_id,
1306                            TransactionState::Failed,
1307                            error.clone(),
1308                        )
1309                        .await?;
1310                    return Err(error);
1311                }
1312            };
1313
1314            backend
1315                .store
1316                .record_transaction(
1317                    StoredTransaction::new(identity.clone(), TransactionState::Sent)
1318                        .with_chat_id(initial_chat_id),
1319                )
1320                .await
1321                .map_err(store_error_to_backend)?;
1322
1323            let input = proto::SendMessageInput {
1324                peer_id: Some(input_peer_for_client_peer(request.peer)),
1325                message: request.caption.clone(),
1326                reply_to_msg_id: request.reply_to_message_id.map(InlineId::get),
1327                random_id: Some(random_id.get()),
1328                media: Some(media),
1329                temporary_send_date: Some(now_seconds()),
1330                is_sticker: None,
1331                has_link: None,
1332                entities: None,
1333                parse_markdown: Some(false),
1334                send_mode: None,
1335                actions: None,
1336            };
1337            let send_result = match backend.call_realtime(&session, input).await {
1338                Ok(result) => result,
1339                Err(backend_error) => {
1340                    backend
1341                        .record_transaction_error(
1342                            identity,
1343                            initial_chat_id,
1344                            TransactionState::Sent,
1345                            backend_error.clone(),
1346                        )
1347                        .await?;
1348                    return Err(backend_error);
1349                }
1350            };
1351
1352            let applied =
1353                apply_upload_message_updates(&request, identity, initial_chat_id, send_result);
1354            if let Some(message) = &applied.message {
1355                backend
1356                    .store
1357                    .record_message(message.clone())
1358                    .await
1359                    .map_err(store_error_to_backend)?;
1360            }
1361            backend
1362                .store
1363                .record_transaction(applied.transaction.clone())
1364                .await
1365                .map_err(store_error_to_backend)?;
1366
1367            Ok(SendTextOutcome::with_state(
1368                MessageMutation {
1369                    transaction: applied.transaction.identity,
1370                    message_id: applied.message_id,
1371                    state: None,
1372                    failure: None,
1373                },
1374                applied.chat_id,
1375                applied.message_id,
1376                applied.message,
1377                applied.transaction.state,
1378                applied.transaction.failure,
1379            ))
1380        })
1381    }
1382
1383    fn edit_message(
1384        &self,
1385        request: EditMessageRequest,
1386    ) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
1387        let backend = self.clone();
1388        Box::pin(async move {
1389            if request.text.trim().is_empty() {
1390                return Err(BackendError::new(
1391                    ClientErrorCategory::InvalidInput,
1392                    "message text must not be empty",
1393                ));
1394            }
1395            let session = backend.require_session().await?;
1396            let result = backend
1397                .call_realtime(
1398                    &session,
1399                    proto::EditMessageInput {
1400                        message_id: request.message_id.get(),
1401                        peer_id: Some(input_peer_for_chat(request.chat_id)),
1402                        text: request.text,
1403                        entities: None,
1404                        parse_markdown: Some(false),
1405                        actions: None,
1406                    },
1407                )
1408                .await?;
1409            let events = backend
1410                .apply_updates(result.updates, Some(request.chat_id), None)
1411                .await?;
1412            Ok(OperationOutcome::with_events(events))
1413        })
1414    }
1415
1416    fn delete_message(
1417        &self,
1418        request: DeleteMessageRequest,
1419    ) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
1420        let backend = self.clone();
1421        Box::pin(async move {
1422            let session = backend.require_session().await?;
1423            let result = backend
1424                .call_realtime(
1425                    &session,
1426                    proto::DeleteMessagesInput {
1427                        message_ids: vec![request.message_id.get()],
1428                        peer_id: Some(input_peer_for_chat(request.chat_id)),
1429                    },
1430                )
1431                .await?;
1432            let mut events = backend
1433                .apply_updates(result.updates, Some(request.chat_id), None)
1434                .await?;
1435            if !events.iter().any(|event| {
1436                matches!(
1437                    event,
1438                    ClientEvent::MessageDeleted {
1439                        chat_id,
1440                        message_id
1441                    } if *chat_id == request.chat_id && *message_id == request.message_id
1442                )
1443            }) {
1444                events.push(ClientEvent::MessageDeleted {
1445                    chat_id: request.chat_id,
1446                    message_id: request.message_id,
1447                });
1448            }
1449            Ok(OperationOutcome::with_events(events))
1450        })
1451    }
1452
1453    fn react(&self, request: ReactRequest) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
1454        let backend = self.clone();
1455        Box::pin(async move {
1456            let reaction = request.reaction.trim().to_owned();
1457            if reaction.is_empty() {
1458                return Err(BackendError::new(
1459                    ClientErrorCategory::InvalidInput,
1460                    "reaction must not be empty",
1461                ));
1462            }
1463            let session = backend.require_session().await?;
1464            let updates = if request.remove {
1465                backend
1466                    .call_realtime(
1467                        &session,
1468                        proto::DeleteReactionInput {
1469                            emoji: reaction.clone(),
1470                            peer_id: Some(input_peer_for_chat(request.chat_id)),
1471                            message_id: request.message_id.get(),
1472                        },
1473                    )
1474                    .await?
1475                    .updates
1476            } else {
1477                backend
1478                    .call_realtime(
1479                        &session,
1480                        proto::AddReactionInput {
1481                            emoji: reaction.clone(),
1482                            message_id: request.message_id.get(),
1483                            peer_id: Some(input_peer_for_chat(request.chat_id)),
1484                        },
1485                    )
1486                    .await?
1487                    .updates
1488            };
1489            let mut events = backend
1490                .apply_updates(updates, Some(request.chat_id), None)
1491                .await?;
1492            if !events.iter().any(|event| {
1493                matches!(
1494                    event,
1495                    ClientEvent::ReactionChanged {
1496                        chat_id,
1497                        message_id,
1498                        ..
1499                    } if *chat_id == request.chat_id && *message_id == request.message_id
1500                )
1501            }) {
1502                events.push(ClientEvent::ReactionChanged {
1503                    chat_id: request.chat_id,
1504                    message_id: request.message_id,
1505                    user_id: InlineId::new(0),
1506                    reaction,
1507                    removed: request.remove,
1508                });
1509            }
1510            Ok(OperationOutcome::with_events(events))
1511        })
1512    }
1513
1514    fn read(&self, request: ReadRequest) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
1515        let backend = self.clone();
1516        Box::pin(async move {
1517            let session = backend.require_session().await?;
1518            let result = backend
1519                .call_realtime(
1520                    &session,
1521                    proto::ReadMessagesInput {
1522                        peer_id: Some(input_peer_for_chat(request.chat_id)),
1523                        max_id: request.max_message_id.map(InlineId::get),
1524                    },
1525                )
1526                .await?;
1527            let mut events = backend
1528                .apply_updates(result.updates, Some(request.chat_id), None)
1529                .await?;
1530            if !events.iter().any(|event| {
1531                matches!(event, ClientEvent::ReadStateChanged { chat_id } if *chat_id == request.chat_id)
1532            }) {
1533                events.push(ClientEvent::ReadStateChanged {
1534                    chat_id: request.chat_id,
1535                });
1536            }
1537            Ok(OperationOutcome::with_events(events))
1538        })
1539    }
1540
1541    fn set_marked_unread(
1542        &self,
1543        request: SetMarkedUnreadRequest,
1544    ) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
1545        let backend = self.clone();
1546        Box::pin(async move {
1547            let session = backend.require_session().await?;
1548            let updates = if request.unread {
1549                backend
1550                    .call_realtime(
1551                        &session,
1552                        proto::MarkAsUnreadInput {
1553                            peer_id: Some(input_peer_for_chat(request.chat_id)),
1554                        },
1555                    )
1556                    .await?
1557                    .updates
1558            } else {
1559                backend
1560                    .call_realtime(
1561                        &session,
1562                        proto::ReadMessagesInput {
1563                            peer_id: Some(input_peer_for_chat(request.chat_id)),
1564                            max_id: None,
1565                        },
1566                    )
1567                    .await?
1568                    .updates
1569            };
1570            let mut events = backend
1571                .apply_updates(updates, Some(request.chat_id), None)
1572                .await?;
1573            if !events.iter().any(|event| {
1574                matches!(event, ClientEvent::ReadStateChanged { chat_id } if *chat_id == request.chat_id)
1575            }) {
1576                events.push(ClientEvent::ReadStateChanged {
1577                    chat_id: request.chat_id,
1578                });
1579            }
1580            Ok(OperationOutcome::with_events(events))
1581        })
1582    }
1583
1584    fn update_dialog_notifications(
1585        &self,
1586        request: UpdateDialogNotificationsRequest,
1587    ) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
1588        let backend = self.clone();
1589        Box::pin(async move {
1590            let session = backend.require_session().await?;
1591            let result = backend
1592                .call_realtime(
1593                    &session,
1594                    proto::UpdateDialogNotificationSettingsInput {
1595                        peer_id: Some(input_peer_for_chat(request.chat_id)),
1596                        notification_settings: request.mode.map(|mode| {
1597                            proto::DialogNotificationSettings {
1598                                mode: Some(dialog_notification_mode_to_proto(mode) as i32),
1599                            }
1600                        }),
1601                    },
1602                )
1603                .await?;
1604            let mut events = backend
1605                .apply_updates(result.updates, Some(request.chat_id), None)
1606                .await?;
1607            backend
1608                .mutate_dialog(request.chat_id, |dialog| {
1609                    dialog.notification_mode = request.mode;
1610                })
1611                .await?;
1612            if !events.iter().any(|event| {
1613                matches!(event, ClientEvent::ChatUpserted { chat_id } if *chat_id == request.chat_id)
1614            }) {
1615                events.push(ClientEvent::ChatUpserted {
1616                    chat_id: request.chat_id,
1617                });
1618            }
1619            Ok(OperationOutcome::with_events(events))
1620        })
1621    }
1622
1623    fn typing(
1624        &self,
1625        request: TypingRequest,
1626    ) -> BoxFuture<'static, BackendResult<OperationOutcome>> {
1627        let backend = self.clone();
1628        Box::pin(async move {
1629            let session = backend.require_session().await?;
1630            backend
1631                .call_realtime(
1632                    &session,
1633                    proto::SendComposeActionInput {
1634                        peer_id: Some(input_peer_for_chat(request.chat_id)),
1635                        action: request
1636                            .is_typing
1637                            .then_some(proto::update_compose_action::ComposeAction::Typing as i32),
1638                    },
1639                )
1640                .await?;
1641            Ok(OperationOutcome::empty())
1642        })
1643    }
1644
1645    fn receive_events(&self) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>> {
1646        let backend = self.clone();
1647        Box::pin(async move {
1648            let session = backend.require_session().await?;
1649            if backend.sync_required.load(Ordering::Acquire) {
1650                let events = backend.sync.discover(&backend).await?;
1651                backend.sync_required.store(false, Ordering::Release);
1652                if !events.is_empty() {
1653                    return Ok(events);
1654                }
1655            }
1656            loop {
1657                match backend.receive_realtime_event(&session).await? {
1658                    RealtimeEvent::Updates(updates) => {
1659                        let events = match backend.sync.process_realtime(&backend, updates).await {
1660                            Ok(events) => events,
1661                            Err(error) => {
1662                                backend.sync_required.store(true, Ordering::Release);
1663                                return Err(error);
1664                            }
1665                        };
1666                        if !events.is_empty() {
1667                            return Ok(events);
1668                        }
1669                    }
1670                    RealtimeEvent::Ack { .. } | RealtimeEvent::Pong { .. } => {}
1671                    _ => {}
1672                }
1673            }
1674        })
1675    }
1676}
1677
1678impl SyncHost for SdkBackend {
1679    fn get_updates_state(
1680        &self,
1681        date: i64,
1682    ) -> BoxFuture<'static, BackendResult<proto::GetUpdatesStateResult>> {
1683        let backend = self.clone();
1684        Box::pin(async move {
1685            let session = backend.require_session().await?;
1686            backend
1687                .call_realtime(&session, proto::GetUpdatesStateInput { date })
1688                .await
1689        })
1690    }
1691
1692    fn get_updates(
1693        &self,
1694        input: proto::GetUpdatesInput,
1695    ) -> BoxFuture<'static, BackendResult<proto::GetUpdatesResult>> {
1696        let backend = self.clone();
1697        Box::pin(async move {
1698            let session = backend.require_session().await?;
1699            backend.call_realtime(&session, input).await
1700        })
1701    }
1702
1703    fn apply_sync_batch(
1704        &self,
1705        updates: Vec<proto::Update>,
1706        sidecars: Option<proto::UpdateSidecars>,
1707    ) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>> {
1708        let backend = self.clone();
1709        Box::pin(async move {
1710            let mut events = if let Some(sidecars) = sidecars {
1711                backend.apply_sidecars(sidecars).await?
1712            } else {
1713                Vec::new()
1714            };
1715            events.extend(backend.apply_updates(updates, None, None).await?);
1716            Ok(events)
1717        })
1718    }
1719
1720    fn repair_bucket(
1721        &self,
1722        key: crate::SyncBucketKey,
1723    ) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>> {
1724        let backend = self.clone();
1725        Box::pin(async move {
1726            match key {
1727                crate::SyncBucketKey::User => backend.repair_user_bucket().await,
1728                crate::SyncBucketKey::Space { space_id } => {
1729                    backend.repair_space_bucket(space_id).await
1730                }
1731                crate::SyncBucketKey::Chat { peer } => backend.repair_chat_bucket(peer).await,
1732            }
1733        })
1734    }
1735}
1736
1737impl SdkBackend {
1738    async fn repair_user_bucket(&self) -> BackendResult<Vec<ClientEvent>> {
1739        let session = self.require_session().await?;
1740        let chats = self
1741            .call_realtime(&session, proto::GetChatsInput {})
1742            .await?;
1743        let me = self.call_realtime(&session, proto::GetMeInput {}).await?;
1744        let settings = self
1745            .call_realtime(&session, proto::GetUserSettingsInput {})
1746            .await?;
1747        let chat_peers = chats
1748            .chats
1749            .iter()
1750            .map(sync_bucket_peer_for_chat)
1751            .collect::<HashSet<_>>();
1752        let space_ids = chats
1753            .spaces
1754            .iter()
1755            .map(|space| InlineId::new(space.id))
1756            .collect::<HashSet<_>>();
1757
1758        let user = me.user.ok_or_else(|| {
1759            BackendError::new(
1760                ClientErrorCategory::ProtocolMismatch,
1761                "user bucket snapshot did not include the current user",
1762            )
1763        })?;
1764        let settings = settings.user_settings.ok_or_else(|| {
1765            BackendError::new(
1766                ClientErrorCategory::ProtocolMismatch,
1767                "user bucket snapshot did not include user settings",
1768            )
1769        })?;
1770
1771        let mut events = self.apply_get_chats_snapshot(chats).await?;
1772        let user = user_record_from_proto(&user);
1773        self.store
1774            .record_users(vec![user.clone()])
1775            .await
1776            .map_err(store_error_to_backend)?;
1777        self.store
1778            .record_user_settings(user_settings_record_from_proto(&settings))
1779            .await
1780            .map_err(store_error_to_backend)?;
1781        events.push(ClientEvent::UserUpserted {
1782            user_id: user.user_id,
1783        });
1784        events.push(ClientEvent::UserSettingsChanged {});
1785        for space_id in space_ids {
1786            events.extend(self.repair_space_members(&session, space_id, true).await?);
1787        }
1788        for peer in chat_peers {
1789            events.extend(self.repair_chat_bucket(peer).await?);
1790        }
1791        Ok(events)
1792    }
1793
1794    async fn repair_space_bucket(&self, space_id: InlineId) -> BackendResult<Vec<ClientEvent>> {
1795        let session = self.require_session().await?;
1796        let chats = self
1797            .call_realtime(&session, proto::GetChatsInput {})
1798            .await?;
1799        let space_is_current = chats.spaces.iter().any(|space| space.id == space_id.get());
1800        let mut events = self.apply_get_chats_snapshot(chats).await?;
1801        events.extend(
1802            self.repair_space_members(&session, space_id, space_is_current)
1803                .await?,
1804        );
1805        Ok(events)
1806    }
1807
1808    async fn repair_space_members(
1809        &self,
1810        session: &StoredSession,
1811        space_id: InlineId,
1812        space_is_current: bool,
1813    ) -> BackendResult<Vec<ClientEvent>> {
1814        let members = if space_is_current {
1815            Some(
1816                self.call_realtime(
1817                    session,
1818                    proto::GetSpaceMembersInput {
1819                        space_id: space_id.get(),
1820                    },
1821                )
1822                .await?,
1823            )
1824        } else {
1825            None
1826        };
1827        let previous_members = self
1828            .store
1829            .space_members(space_id)
1830            .await
1831            .map_err(store_error_to_backend)?;
1832        let (current_members, users) = if let Some(result) = members {
1833            (
1834                result
1835                    .members
1836                    .iter()
1837                    .map(space_member_record_from_proto)
1838                    .collect::<Vec<_>>(),
1839                result
1840                    .users
1841                    .iter()
1842                    .map(user_record_from_proto)
1843                    .collect::<Vec<_>>(),
1844            )
1845        } else {
1846            (Vec::new(), Vec::new())
1847        };
1848        self.store
1849            .record_users(users.clone())
1850            .await
1851            .map_err(store_error_to_backend)?;
1852        self.store
1853            .record_space_members(space_id, current_members.clone())
1854            .await
1855            .map_err(store_error_to_backend)?;
1856
1857        let mut events = users
1858            .into_iter()
1859            .map(|user| ClientEvent::UserUpserted {
1860                user_id: user.user_id,
1861            })
1862            .collect::<Vec<_>>();
1863        let current_ids = current_members
1864            .iter()
1865            .map(|member| member.user_id)
1866            .collect::<HashSet<_>>();
1867        events.extend(
1868            current_members
1869                .into_iter()
1870                .map(|member| ClientEvent::SpaceMemberChanged {
1871                    space_id,
1872                    user_id: member.user_id,
1873                    removed: false,
1874                }),
1875        );
1876        events.extend(
1877            previous_members
1878                .into_iter()
1879                .filter(|member| !current_ids.contains(&member.user_id))
1880                .map(|member| ClientEvent::SpaceMemberChanged {
1881                    space_id,
1882                    user_id: member.user_id,
1883                    removed: true,
1884                }),
1885        );
1886        Ok(events)
1887    }
1888
1889    async fn repair_chat_bucket(
1890        &self,
1891        peer: crate::SyncBucketPeer,
1892    ) -> BackendResult<Vec<ClientEvent>> {
1893        let session = self.require_session().await?;
1894        let peer_id = input_peer_for_sync_bucket_peer(peer);
1895        let chat_result = match self
1896            .call_realtime(
1897                &session,
1898                proto::GetChatInput {
1899                    peer_id: Some(peer_id.clone()),
1900                },
1901            )
1902            .await
1903        {
1904            Ok(result) => result,
1905            Err(error) if error.category == ClientErrorCategory::NotFound => {
1906                let Some(chat_id) = self.stored_chat_id_for_sync_peer(peer).await? else {
1907                    return Ok(Vec::new());
1908                };
1909                self.store
1910                    .remove_dialog(chat_id)
1911                    .await
1912                    .map_err(store_error_to_backend)?;
1913                return Ok(vec![ClientEvent::ChatDeleted { chat_id }]);
1914            }
1915            Err(error) => return Err(error),
1916        };
1917        let chat = chat_result.chat.ok_or_else(|| {
1918            BackendError::new(
1919                ClientErrorCategory::ProtocolMismatch,
1920                "chat bucket snapshot did not include the chat",
1921            )
1922        })?;
1923        let chat_id = InlineId::new(chat.id);
1924        let participants = self
1925            .call_realtime(
1926                &session,
1927                proto::GetChatParticipantsInput {
1928                    chat_id: chat_id.get(),
1929                },
1930            )
1931            .await?;
1932        let history = self
1933            .fetch_complete_chat_history(&session, peer_id, chat_id)
1934            .await?;
1935        let previous = self
1936            .store
1937            .history(HistoryRequest {
1938                chat_id,
1939                limit: Some(u32::MAX),
1940                before_message_id: None,
1941                after_message_id: None,
1942            })
1943            .await
1944            .map_err(store_error_to_backend)?;
1945        let previous_ids = previous
1946            .messages
1947            .iter()
1948            .map(|message| message.message_id)
1949            .collect::<HashSet<_>>();
1950        let current_ids = history
1951            .iter()
1952            .map(|message| InlineId::new(message.id))
1953            .collect::<HashSet<_>>();
1954
1955        let dialog = chat_result.dialog;
1956        let read_state = dialog
1957            .as_ref()
1958            .and_then(|dialog| read_state_from_proto_dialog(chat_id, dialog));
1959        let pinned_message_ids = chat_result
1960            .pinned_message_ids
1961            .into_iter()
1962            .map(InlineId::new)
1963            .collect::<Vec<_>>();
1964        let mut events = self.record_chat_update(chat, dialog, None).await?;
1965        self.mutate_dialog(chat_id, |dialog| {
1966            dialog.pinned_message_ids = pinned_message_ids;
1967        })
1968        .await?;
1969
1970        let participant_page = chat_participants_page_from_proto(participants);
1971        self.store
1972            .record_users(participant_page.users.clone())
1973            .await
1974            .map_err(store_error_to_backend)?;
1975        self.store
1976            .record_chat_participants(chat_id, participant_page.participants)
1977            .await
1978            .map_err(store_error_to_backend)?;
1979        events.extend(
1980            participant_page
1981                .users
1982                .into_iter()
1983                .map(|user| ClientEvent::UserUpserted {
1984                    user_id: user.user_id,
1985                }),
1986        );
1987        events.push(ClientEvent::ChatParticipantsChanged { chat_id });
1988
1989        self.store
1990            .clear_chat_messages(chat_id, None)
1991            .await
1992            .map_err(store_error_to_backend)?;
1993        for message in history {
1994            let record = self
1995                .record_proto_message(message, Some(chat_id), None)
1996                .await?;
1997            events.push(ClientEvent::MessageStored { message: record });
1998        }
1999        events.extend(
2000            previous_ids
2001                .difference(&current_ids)
2002                .copied()
2003                .map(|message_id| ClientEvent::MessageDeleted {
2004                    chat_id,
2005                    message_id,
2006                }),
2007        );
2008        if let Some(read_state) = read_state {
2009            self.store
2010                .record_read_state(read_state)
2011                .await
2012                .map_err(store_error_to_backend)?;
2013            events.push(ClientEvent::ReadStateChanged { chat_id });
2014        }
2015        Ok(events)
2016    }
2017
2018    async fn fetch_complete_chat_history(
2019        &self,
2020        session: &StoredSession,
2021        peer_id: proto::InputPeer,
2022        chat_id: InlineId,
2023    ) -> BackendResult<Vec<proto::Message>> {
2024        let mut before_id = None;
2025        let mut messages = BTreeMap::new();
2026        for page in 0..100_000_u32 {
2027            let result = self
2028                .call_realtime(
2029                    session,
2030                    proto::GetChatHistoryInput {
2031                        peer_id: Some(peer_id.clone()),
2032                        offset_id: None,
2033                        limit: Some(SNAPSHOT_HISTORY_PAGE_LIMIT),
2034                        mode: Some(if before_id.is_some() {
2035                            proto::GetChatHistoryMode::HistoryModeOlder as i32
2036                        } else {
2037                            proto::GetChatHistoryMode::HistoryModeLatest as i32
2038                        }),
2039                        anchor_id: None,
2040                        before_id,
2041                        after_id: None,
2042                        before_limit: None,
2043                        after_limit: None,
2044                        include_anchor: None,
2045                    },
2046                )
2047                .await?;
2048            let count = result.messages.len();
2049            if count > SNAPSHOT_HISTORY_PAGE_LIMIT as usize {
2050                return Err(BackendError::new(
2051                    ClientErrorCategory::ProtocolMismatch,
2052                    "chat history snapshot exceeded the requested page size",
2053                ));
2054            }
2055            if count == 0 {
2056                log::debug!(
2057                    "completed Inline chat history snapshot chat_id={} pages={} messages={}",
2058                    chat_id.get(),
2059                    page + 1,
2060                    messages.len()
2061                );
2062                return Ok(messages.into_values().collect());
2063            }
2064            let next_before = result
2065                .messages
2066                .iter()
2067                .map(|message| message.id)
2068                .min()
2069                .filter(|message_id| *message_id > 0)
2070                .ok_or_else(|| {
2071                    BackendError::new(
2072                        ClientErrorCategory::ProtocolMismatch,
2073                        "chat history snapshot returned an invalid message id",
2074                    )
2075                })?;
2076            if before_id.is_some_and(|previous| next_before >= previous) {
2077                return Err(BackendError::new(
2078                    ClientErrorCategory::ProtocolMismatch,
2079                    "chat history snapshot did not advance",
2080                ));
2081            }
2082            for message in result.messages {
2083                messages.insert(message.id, message);
2084            }
2085            if count < SNAPSHOT_HISTORY_PAGE_LIMIT as usize {
2086                log::debug!(
2087                    "completed Inline chat history snapshot chat_id={} pages={} messages={}",
2088                    chat_id.get(),
2089                    page + 1,
2090                    messages.len()
2091                );
2092                return Ok(messages.into_values().collect());
2093            }
2094            before_id = Some(next_before);
2095        }
2096        Err(BackendError::new(
2097            ClientErrorCategory::ProtocolMismatch,
2098            "chat history snapshot exceeded the page safety limit",
2099        ))
2100    }
2101
2102    async fn apply_get_chats_snapshot(
2103        &self,
2104        result: proto::GetChatsResult,
2105    ) -> BackendResult<Vec<ClientEvent>> {
2106        let dialogs = dialog_records_from_get_chats(&result);
2107        let live_ids = dialogs
2108            .iter()
2109            .map(|dialog| dialog.chat_id)
2110            .collect::<HashSet<_>>();
2111        let stored_ids = self.stored_dialog_ids().await?;
2112        let read_states = result
2113            .chats
2114            .iter()
2115            .filter_map(|chat| {
2116                dialog_for_chat(&result, chat)
2117                    .and_then(|dialog| read_state_from_proto_dialog(InlineId::new(chat.id), dialog))
2118            })
2119            .collect::<Vec<_>>();
2120        let users = result
2121            .users
2122            .iter()
2123            .map(user_record_from_proto)
2124            .collect::<Vec<_>>();
2125        let spaces = result
2126            .spaces
2127            .iter()
2128            .map(space_record_from_proto)
2129            .collect::<Vec<_>>();
2130
2131        self.reconcile_live_dialogs(dialogs).await?;
2132        self.store
2133            .record_users(users.clone())
2134            .await
2135            .map_err(store_error_to_backend)?;
2136        for space in &spaces {
2137            self.store
2138                .record_space(space.clone())
2139                .await
2140                .map_err(store_error_to_backend)?;
2141        }
2142        for read_state in &read_states {
2143            self.store
2144                .record_read_state(*read_state)
2145                .await
2146                .map_err(store_error_to_backend)?;
2147        }
2148
2149        let mut events = stored_ids
2150            .difference(&live_ids)
2151            .copied()
2152            .map(|chat_id| ClientEvent::ChatDeleted { chat_id })
2153            .collect::<Vec<_>>();
2154        events.extend(
2155            live_ids
2156                .into_iter()
2157                .map(|chat_id| ClientEvent::ChatUpserted { chat_id }),
2158        );
2159        events.extend(users.into_iter().map(|user| ClientEvent::UserUpserted {
2160            user_id: user.user_id,
2161        }));
2162        events.extend(spaces.into_iter().map(|space| ClientEvent::SpaceUpserted {
2163            space_id: space.space_id,
2164        }));
2165        events.extend(
2166            read_states
2167                .into_iter()
2168                .map(|state| ClientEvent::ReadStateChanged {
2169                    chat_id: state.chat_id,
2170                }),
2171        );
2172        for message in result.messages {
2173            let record = self.record_proto_message(message, None, None).await?;
2174            events.push(ClientEvent::MessageStored { message: record });
2175        }
2176        Ok(events)
2177    }
2178
2179    async fn stored_dialog_ids(&self) -> BackendResult<HashSet<InlineId>> {
2180        let mut cursor = None;
2181        let mut ids = HashSet::new();
2182        loop {
2183            let page = self
2184                .store
2185                .dialogs(DialogsRequest {
2186                    limit: Some(1_000),
2187                    cursor: cursor.clone(),
2188                })
2189                .await
2190                .map_err(store_error_to_backend)?;
2191            ids.extend(page.dialogs.into_iter().map(|dialog| dialog.chat_id));
2192            let Some(next) = page.next_cursor else {
2193                return Ok(ids);
2194            };
2195            if cursor.as_ref() == Some(&next) {
2196                return Err(BackendError::new(
2197                    ClientErrorCategory::ProtocolMismatch,
2198                    "stored dialog pagination did not advance",
2199                ));
2200            }
2201            cursor = Some(next);
2202        }
2203    }
2204
2205    async fn stored_chat_id_for_sync_peer(
2206        &self,
2207        peer: crate::SyncBucketPeer,
2208    ) -> BackendResult<Option<InlineId>> {
2209        match peer {
2210            crate::SyncBucketPeer::Chat { chat_id } => Ok(Some(chat_id)),
2211            crate::SyncBucketPeer::User { user_id } => {
2212                let dialogs = self
2213                    .store
2214                    .dialogs(DialogsRequest {
2215                        limit: Some(u32::MAX),
2216                        cursor: None,
2217                    })
2218                    .await
2219                    .map_err(store_error_to_backend)?;
2220                Ok(dialogs
2221                    .dialogs
2222                    .into_iter()
2223                    .find(|dialog| dialog.peer_user_id == Some(user_id))
2224                    .map(|dialog| dialog.chat_id))
2225            }
2226        }
2227    }
2228
2229    async fn apply_sidecars(
2230        &self,
2231        sidecars: proto::UpdateSidecars,
2232    ) -> BackendResult<Vec<ClientEvent>> {
2233        let users = sidecars
2234            .users
2235            .iter()
2236            .map(user_record_from_proto)
2237            .collect::<Vec<_>>();
2238        self.store
2239            .record_users(users.clone())
2240            .await
2241            .map_err(store_error_to_backend)?;
2242
2243        let users_by_id = sidecars
2244            .users
2245            .iter()
2246            .map(|user| (user.id, user))
2247            .collect::<HashMap<_, _>>();
2248        for chat in &sidecars.chats {
2249            let dialog = sidecars.dialogs.iter().find(|dialog| {
2250                dialog.chat_id == Some(chat.id)
2251                    || dialog
2252                        .peer
2253                        .as_ref()
2254                        .zip(chat.peer_id.as_ref())
2255                        .is_some_and(|(dialog_peer, chat_peer)| dialog_peer == chat_peer)
2256            });
2257            self.store
2258                .record_dialog(DialogRecord {
2259                    chat_id: InlineId::new(chat.id),
2260                    peer_user_id: dialog_peer_user_id(dialog, chat),
2261                    title: Some(chat_title_from_proto(chat, &users_by_id)),
2262                    emoji: chat_emoji_from_proto(chat),
2263                    last_message_id: chat.last_msg_id.map(InlineId::new),
2264                    synced_through_message_id: None,
2265                    unread_count: dialog
2266                        .and_then(|dialog| dialog.unread_count)
2267                        .and_then(|count| u32::try_from(count).ok()),
2268                    space_id: dialog
2269                        .and_then(|dialog| dialog.space_id)
2270                        .or(chat.space_id)
2271                        .map(InlineId::new),
2272                    is_public: chat.is_public,
2273                    archived: dialog.and_then(|dialog| dialog.archived),
2274                    pinned: dialog.and_then(|dialog| dialog.pinned),
2275                    open: dialog.and_then(|dialog| dialog.open),
2276                    chat_list_hidden: dialog.and_then(|dialog| dialog.chat_list_hidden),
2277                    order: dialog.and_then(|dialog| dialog.order.clone()),
2278                    pinned_order: dialog.and_then(|dialog| dialog.pinned_order.clone()),
2279                    notification_mode: dialog.and_then(dialog_notification_mode_from_proto),
2280                    follow_mode: dialog.and_then(dialog_follow_mode_from_proto),
2281                    ..DialogRecord::new(InlineId::new(chat.id))
2282                })
2283                .await
2284                .map_err(store_error_to_backend)?;
2285        }
2286
2287        let mut events = users
2288            .into_iter()
2289            .map(|user| ClientEvent::UserUpserted {
2290                user_id: user.user_id,
2291            })
2292            .collect::<Vec<_>>();
2293        events.extend(
2294            sidecars
2295                .chats
2296                .into_iter()
2297                .map(|chat| ClientEvent::ChatUpserted {
2298                    chat_id: InlineId::new(chat.id),
2299                }),
2300        );
2301        for space in sidecars.spaces {
2302            let record = space_record_from_proto(&space);
2303            self.store
2304                .record_space(record.clone())
2305                .await
2306                .map_err(store_error_to_backend)?;
2307            events.push(ClientEvent::SpaceUpserted {
2308                space_id: record.space_id,
2309            });
2310        }
2311        Ok(events)
2312    }
2313
2314    async fn record_chat_update(
2315        &self,
2316        chat: proto::Chat,
2317        dialog: Option<proto::Dialog>,
2318        user: Option<proto::User>,
2319    ) -> BackendResult<Vec<ClientEvent>> {
2320        let user_record = user.as_ref().map(user_record_from_proto);
2321        if let Some(user) = user_record.as_ref() {
2322            self.store
2323                .record_users(vec![user.clone()])
2324                .await
2325                .map_err(store_error_to_backend)?;
2326        }
2327        let existing = self
2328            .store
2329            .dialog(InlineId::new(chat.id))
2330            .await
2331            .map_err(store_error_to_backend)?;
2332        let users_by_id = user
2333            .as_ref()
2334            .map(|user| HashMap::from([(user.id, user)]))
2335            .unwrap_or_default();
2336        let record = DialogRecord {
2337            chat_id: InlineId::new(chat.id),
2338            peer_user_id: dialog_peer_user_id(dialog.as_ref(), &chat)
2339                .or_else(|| existing.as_ref().and_then(|dialog| dialog.peer_user_id)),
2340            title: Some(chat_title_from_proto(&chat, &users_by_id)),
2341            emoji: chat_emoji_from_proto(&chat),
2342            last_message_id: chat
2343                .last_msg_id
2344                .map(InlineId::new)
2345                .or_else(|| existing.as_ref().and_then(|dialog| dialog.last_message_id)),
2346            synced_through_message_id: existing
2347                .as_ref()
2348                .and_then(|dialog| dialog.synced_through_message_id),
2349            unread_count: dialog
2350                .as_ref()
2351                .and_then(|dialog| dialog.unread_count)
2352                .and_then(|count| u32::try_from(count).ok())
2353                .or_else(|| existing.as_ref().and_then(|dialog| dialog.unread_count)),
2354            space_id: dialog
2355                .as_ref()
2356                .and_then(|dialog| dialog.space_id)
2357                .or(chat.space_id)
2358                .map(InlineId::new)
2359                .or_else(|| existing.as_ref().and_then(|dialog| dialog.space_id)),
2360            is_public: chat
2361                .is_public
2362                .or_else(|| existing.as_ref().and_then(|dialog| dialog.is_public)),
2363            archived: dialog
2364                .as_ref()
2365                .and_then(|dialog| dialog.archived)
2366                .or_else(|| existing.as_ref().and_then(|dialog| dialog.archived)),
2367            pinned: dialog
2368                .as_ref()
2369                .and_then(|dialog| dialog.pinned)
2370                .or_else(|| existing.as_ref().and_then(|dialog| dialog.pinned)),
2371            open: dialog
2372                .as_ref()
2373                .and_then(|dialog| dialog.open)
2374                .or_else(|| existing.as_ref().and_then(|dialog| dialog.open)),
2375            chat_list_hidden: dialog
2376                .as_ref()
2377                .and_then(|dialog| dialog.chat_list_hidden)
2378                .or_else(|| existing.as_ref().and_then(|dialog| dialog.chat_list_hidden)),
2379            order: dialog
2380                .as_ref()
2381                .and_then(|dialog| dialog.order.clone())
2382                .or_else(|| existing.as_ref().and_then(|dialog| dialog.order.clone())),
2383            pinned_order: dialog
2384                .as_ref()
2385                .and_then(|dialog| dialog.pinned_order.clone())
2386                .or_else(|| {
2387                    existing
2388                        .as_ref()
2389                        .and_then(|dialog| dialog.pinned_order.clone())
2390                }),
2391            notification_mode: dialog
2392                .as_ref()
2393                .and_then(dialog_notification_mode_from_proto)
2394                .or_else(|| {
2395                    existing
2396                        .as_ref()
2397                        .and_then(|dialog| dialog.notification_mode)
2398                }),
2399            follow_mode: dialog
2400                .as_ref()
2401                .and_then(dialog_follow_mode_from_proto)
2402                .or_else(|| existing.as_ref().and_then(|dialog| dialog.follow_mode)),
2403            ..existing
2404                .clone()
2405                .unwrap_or_else(|| DialogRecord::new(InlineId::new(chat.id)))
2406        };
2407        self.store
2408            .record_dialog(record.clone())
2409            .await
2410            .map_err(store_error_to_backend)?;
2411
2412        let mut events = vec![ClientEvent::ChatUpserted {
2413            chat_id: record.chat_id,
2414        }];
2415        if let Some(user) = user_record {
2416            events.push(ClientEvent::UserUpserted {
2417                user_id: user.user_id,
2418            });
2419        }
2420        Ok(events)
2421    }
2422
2423    async fn resolve_chat_id(
2424        &self,
2425        peer: Option<&proto::Peer>,
2426        fallback: Option<InlineId>,
2427    ) -> BackendResult<Option<InlineId>> {
2428        let Some(peer) = peer else {
2429            return Ok(fallback);
2430        };
2431        match peer.r#type.as_ref() {
2432            Some(proto::peer::Type::Chat(chat)) => Ok(Some(InlineId::new(chat.chat_id))),
2433            Some(proto::peer::Type::User(user)) => {
2434                let dialogs = self
2435                    .store
2436                    .dialogs(DialogsRequest {
2437                        limit: Some(u32::MAX),
2438                        cursor: None,
2439                    })
2440                    .await
2441                    .map_err(store_error_to_backend)?;
2442                Ok(dialogs
2443                    .dialogs
2444                    .into_iter()
2445                    .find(|dialog| dialog.peer_user_id == Some(InlineId::new(user.user_id)))
2446                    .map(|dialog| dialog.chat_id)
2447                    .or(fallback))
2448            }
2449            None => Ok(fallback),
2450        }
2451    }
2452
2453    async fn mutate_dialog(
2454        &self,
2455        chat_id: InlineId,
2456        mutate: impl FnOnce(&mut DialogRecord),
2457    ) -> BackendResult<()> {
2458        let mut dialog = self
2459            .store
2460            .dialog(chat_id)
2461            .await
2462            .map_err(store_error_to_backend)?
2463            .unwrap_or_else(|| DialogRecord::new(chat_id));
2464        mutate(&mut dialog);
2465        self.store
2466            .record_dialog(dialog)
2467            .await
2468            .map_err(store_error_to_backend)
2469    }
2470
2471    async fn reconcile_live_dialogs(&self, dialogs: Vec<DialogRecord>) -> BackendResult<()> {
2472        let live_ids = dialogs
2473            .iter()
2474            .map(|dialog| dialog.chat_id)
2475            .collect::<HashSet<_>>();
2476        for dialog in dialogs {
2477            self.store
2478                .record_dialog(dialog)
2479                .await
2480                .map_err(store_error_to_backend)?;
2481        }
2482
2483        let mut cursor = None;
2484        let mut stored_ids = Vec::new();
2485        loop {
2486            let page = self
2487                .store
2488                .dialogs(DialogsRequest {
2489                    limit: Some(1_000),
2490                    cursor: cursor.clone(),
2491                })
2492                .await
2493                .map_err(store_error_to_backend)?;
2494            stored_ids.extend(page.dialogs.into_iter().map(|dialog| dialog.chat_id));
2495            let Some(next) = page.next_cursor else {
2496                break;
2497            };
2498            if cursor.as_ref() == Some(&next) {
2499                return Err(BackendError::new(
2500                    ClientErrorCategory::ProtocolMismatch,
2501                    "stored dialog pagination did not advance",
2502                ));
2503            }
2504            cursor = Some(next);
2505        }
2506        for chat_id in stored_ids {
2507            if !live_ids.contains(&chat_id) {
2508                self.store
2509                    .remove_dialog(chat_id)
2510                    .await
2511                    .map_err(store_error_to_backend)?;
2512            }
2513        }
2514        Ok(())
2515    }
2516
2517    async fn record_proto_message(
2518        &self,
2519        message: proto::Message,
2520        fallback_chat_id: Option<InlineId>,
2521        transaction: Option<TransactionIdentity>,
2522    ) -> BackendResult<MessageRecord> {
2523        let reaction_snapshot = reaction_snapshot_from_proto_message(&message, fallback_chat_id);
2524        let record = message_record_from_proto_message(message, fallback_chat_id, transaction);
2525        self.store
2526            .record_message(record.clone())
2527            .await
2528            .map_err(store_error_to_backend)?;
2529        if let Some(reactions) = reaction_snapshot {
2530            self.store
2531                .replace_message_reactions(record.chat_id, record.message_id, reactions)
2532                .await
2533                .map_err(store_error_to_backend)?;
2534        }
2535        Ok(record)
2536    }
2537
2538    async fn record_transaction_error(
2539        &self,
2540        identity: TransactionIdentity,
2541        chat_id: InlineId,
2542        retry_state: TransactionState,
2543        error: BackendError,
2544    ) -> BackendResult<()> {
2545        let state = if retryable_transaction_category(error.category) {
2546            retry_state
2547        } else {
2548            TransactionState::Failed
2549        };
2550        self.store
2551            .record_transaction(
2552                StoredTransaction::new(identity, state)
2553                    .with_chat_id(chat_id)
2554                    .with_failure(error.into()),
2555            )
2556            .await
2557            .map_err(store_error_to_backend)
2558    }
2559
2560    async fn apply_updates(
2561        &self,
2562        updates: Vec<proto::Update>,
2563        fallback_chat_id: Option<InlineId>,
2564        transaction: Option<TransactionIdentity>,
2565    ) -> BackendResult<Vec<ClientEvent>> {
2566        let mut events = Vec::new();
2567        for update in updates {
2568            let seq = update.seq.unwrap_or_default();
2569            match update.update {
2570                Some(proto::update::Update::NewMessage(update)) => {
2571                    if let Some(message) = update.message {
2572                        let record = self
2573                            .record_proto_message(message, fallback_chat_id, None)
2574                            .await?;
2575                        events.push(ClientEvent::MessageStored { message: record });
2576                    }
2577                }
2578                Some(proto::update::Update::EditMessage(update)) => {
2579                    if let Some(message) = update.message {
2580                        let record = self
2581                            .record_proto_message(message, fallback_chat_id, None)
2582                            .await?;
2583                        events.push(ClientEvent::MessageStored { message: record });
2584                    }
2585                }
2586                Some(proto::update::Update::DeleteMessages(update)) => {
2587                    let chat_id = self
2588                        .resolve_chat_id(update.peer_id.as_ref(), fallback_chat_id)
2589                        .await?
2590                        .ok_or_else(|| {
2591                            BackendError::new(
2592                                ClientErrorCategory::ProtocolMismatch,
2593                                "delete update did not resolve to a stored chat",
2594                            )
2595                        })?;
2596                    for message_id in update.message_ids {
2597                        let message_id = InlineId::new(message_id);
2598                        self.store
2599                            .record_message_deleted(chat_id, message_id)
2600                            .await
2601                            .map_err(store_error_to_backend)?;
2602                        events.push(ClientEvent::MessageDeleted {
2603                            chat_id,
2604                            message_id,
2605                        });
2606                    }
2607                }
2608                Some(proto::update::Update::UpdateReaction(update)) => {
2609                    if let Some(reaction) = update.reaction {
2610                        let stored = StoredReaction {
2611                            chat_id: InlineId::new(reaction.chat_id),
2612                            message_id: InlineId::new(reaction.message_id),
2613                            user_id: InlineId::new(reaction.user_id),
2614                            reaction: reaction.emoji,
2615                        };
2616                        self.store
2617                            .record_reaction(stored.clone())
2618                            .await
2619                            .map_err(store_error_to_backend)?;
2620                        events.push(ClientEvent::ReactionChanged {
2621                            chat_id: stored.chat_id,
2622                            message_id: stored.message_id,
2623                            user_id: stored.user_id,
2624                            reaction: stored.reaction,
2625                            removed: false,
2626                        });
2627                    }
2628                }
2629                Some(proto::update::Update::DeleteReaction(update)) => {
2630                    let stored = StoredReaction {
2631                        chat_id: InlineId::new(update.chat_id),
2632                        message_id: InlineId::new(update.message_id),
2633                        user_id: InlineId::new(update.user_id),
2634                        reaction: update.emoji,
2635                    };
2636                    self.store
2637                        .remove_reaction(stored.clone())
2638                        .await
2639                        .map_err(store_error_to_backend)?;
2640                    events.push(ClientEvent::ReactionChanged {
2641                        chat_id: stored.chat_id,
2642                        message_id: stored.message_id,
2643                        user_id: stored.user_id,
2644                        reaction: stored.reaction,
2645                        removed: true,
2646                    });
2647                }
2648                Some(proto::update::Update::UpdateReadMaxId(update)) => {
2649                    let chat_id = self
2650                        .resolve_chat_id(update.peer_id.as_ref(), fallback_chat_id)
2651                        .await?;
2652                    if let Some(chat_id) = chat_id {
2653                        self.store
2654                            .record_read_state(StoredReadState {
2655                                chat_id,
2656                                read_max_id: Some(InlineId::new(update.read_max_id)),
2657                                unread_count: u32::try_from(update.unread_count).ok(),
2658                                marked_unread: false,
2659                            })
2660                            .await
2661                            .map_err(store_error_to_backend)?;
2662                        events.push(ClientEvent::ReadStateChanged { chat_id });
2663                    }
2664                }
2665                Some(proto::update::Update::MarkAsUnread(update)) => {
2666                    if let Some(chat_id) = self
2667                        .resolve_chat_id(update.peer_id.as_ref(), fallback_chat_id)
2668                        .await?
2669                    {
2670                        let current = self
2671                            .store
2672                            .read_state(chat_id)
2673                            .await
2674                            .map_err(store_error_to_backend)?;
2675                        self.store
2676                            .record_read_state(StoredReadState {
2677                                chat_id,
2678                                read_max_id: current.and_then(|state| state.read_max_id),
2679                                unread_count: current.and_then(|state| state.unread_count),
2680                                marked_unread: update.unread_mark,
2681                            })
2682                            .await
2683                            .map_err(store_error_to_backend)?;
2684                        events.push(ClientEvent::ReadStateChanged { chat_id });
2685                    }
2686                }
2687                Some(proto::update::Update::UpdateComposeAction(update)) => {
2688                    let chat_id = update
2689                        .peer_id
2690                        .as_ref()
2691                        .and_then(chat_id_from_peer)
2692                        .or(fallback_chat_id);
2693                    if let Some(chat_id) = chat_id {
2694                        events.push(ClientEvent::Typing {
2695                            chat_id,
2696                            user_id: InlineId::new(update.user_id),
2697                            is_typing: update.action
2698                                == proto::update_compose_action::ComposeAction::Typing as i32,
2699                        });
2700                    }
2701                }
2702                Some(proto::update::Update::UpdateMessageId(update)) => {
2703                    if let (Some(chat_id), Some(transaction)) =
2704                        (fallback_chat_id, transaction.as_ref())
2705                        && update.random_id == transaction.random_id.get()
2706                    {
2707                        events.push(ClientEvent::MessageUpserted {
2708                            chat_id,
2709                            message_id: InlineId::new(update.message_id),
2710                        });
2711                    }
2712                }
2713                Some(proto::update::Update::NewChat(update)) => {
2714                    if let Some(chat) = update.chat {
2715                        events.extend(self.record_chat_update(chat, None, update.user).await?);
2716                    }
2717                }
2718                Some(proto::update::Update::ChatOpen(update)) => {
2719                    if let Some(chat) = update.chat {
2720                        events.extend(
2721                            self.record_chat_update(chat, update.dialog, update.user)
2722                                .await?,
2723                        );
2724                    }
2725                }
2726                Some(proto::update::Update::ChatMoved(update)) => {
2727                    if let Some(chat) = update.chat {
2728                        events.extend(self.record_chat_update(chat, None, None).await?);
2729                    }
2730                }
2731                Some(proto::update::Update::ChatInfo(update)) => {
2732                    let chat_id = InlineId::new(update.chat_id);
2733                    let mut dialog = self
2734                        .store
2735                        .dialog(chat_id)
2736                        .await
2737                        .map_err(store_error_to_backend)?
2738                        .unwrap_or(DialogRecord {
2739                            chat_id,
2740                            peer_user_id: None,
2741                            title: None,
2742                            last_message_id: None,
2743                            synced_through_message_id: None,
2744                            unread_count: None,
2745                            ..DialogRecord::new(chat_id)
2746                        });
2747                    if let Some(title) = update.title {
2748                        dialog.title = trimmed_option(Some(title));
2749                    }
2750                    if let Some(emoji) = update.emoji {
2751                        dialog.emoji = non_empty_option(Some(&emoji));
2752                    }
2753                    self.store
2754                        .record_dialog(dialog)
2755                        .await
2756                        .map_err(store_error_to_backend)?;
2757                    events.push(ClientEvent::ChatUpserted { chat_id });
2758                }
2759                Some(proto::update::Update::ChatVisibility(update)) => {
2760                    let chat_id = InlineId::new(update.chat_id);
2761                    self.mutate_dialog(chat_id, |dialog| {
2762                        dialog.is_public = Some(update.is_public);
2763                    })
2764                    .await?;
2765                    events.push(ClientEvent::ChatUpserted { chat_id });
2766                }
2767                Some(proto::update::Update::DialogArchived(update)) => {
2768                    let chat_id = self
2769                        .resolve_chat_id(update.peer_id.as_ref(), fallback_chat_id)
2770                        .await?
2771                        .ok_or_else(|| unresolved_update_peer("dialog archived"))?;
2772                    self.mutate_dialog(chat_id, |dialog| {
2773                        dialog.archived = Some(update.archived);
2774                    })
2775                    .await?;
2776                    events.push(ClientEvent::ChatUpserted { chat_id });
2777                }
2778                Some(proto::update::Update::DialogNotificationSettings(update)) => {
2779                    let chat_id = self
2780                        .resolve_chat_id(update.peer_id.as_ref(), fallback_chat_id)
2781                        .await?
2782                        .ok_or_else(|| unresolved_update_peer("dialog notification settings"))?;
2783                    let mode = update
2784                        .notification_settings
2785                        .as_ref()
2786                        .and_then(dialog_notification_mode_from_settings);
2787                    self.mutate_dialog(chat_id, |dialog| {
2788                        dialog.notification_mode = mode;
2789                    })
2790                    .await?;
2791                    events.push(ClientEvent::ChatUpserted { chat_id });
2792                }
2793                Some(proto::update::Update::DialogFollowMode(update)) => {
2794                    let chat_id = self
2795                        .resolve_chat_id(update.peer_id.as_ref(), fallback_chat_id)
2796                        .await?
2797                        .ok_or_else(|| unresolved_update_peer("dialog follow mode"))?;
2798                    let mode = update.follow_mode.and_then(dialog_follow_mode_from_value);
2799                    self.mutate_dialog(chat_id, |dialog| {
2800                        dialog.follow_mode = mode;
2801                    })
2802                    .await?;
2803                    events.push(ClientEvent::ChatUpserted { chat_id });
2804                }
2805                Some(proto::update::Update::PinnedMessages(update)) => {
2806                    let chat_id = self
2807                        .resolve_chat_id(update.peer_id.as_ref(), fallback_chat_id)
2808                        .await?
2809                        .ok_or_else(|| unresolved_update_peer("pinned messages"))?;
2810                    let message_ids = update
2811                        .message_ids
2812                        .into_iter()
2813                        .map(InlineId::new)
2814                        .collect::<Vec<_>>();
2815                    self.mutate_dialog(chat_id, |dialog| {
2816                        dialog.pinned_message_ids = message_ids;
2817                    })
2818                    .await?;
2819                    events.push(ClientEvent::ChatUpserted { chat_id });
2820                }
2821                Some(proto::update::Update::MessageAttachment(update)) => {
2822                    let chat_id = (update.chat_id > 0)
2823                        .then(|| InlineId::new(update.chat_id))
2824                        .or(self
2825                            .resolve_chat_id(update.peer_id.as_ref(), fallback_chat_id)
2826                            .await?)
2827                        .ok_or_else(|| unresolved_update_peer("message attachment"))?;
2828                    events.push(ClientEvent::MessageUpserted {
2829                        chat_id,
2830                        message_id: InlineId::new(update.message_id),
2831                    });
2832                }
2833                Some(proto::update::Update::ClearChatHistory(update)) => {
2834                    let chat_ids = match update.target {
2835                        Some(proto::update_clear_chat_history::Target::PeerId(peer)) => vec![
2836                            self.resolve_chat_id(Some(&peer), fallback_chat_id)
2837                                .await?
2838                                .ok_or_else(|| unresolved_update_peer("clear history"))?,
2839                        ],
2840                        Some(proto::update_clear_chat_history::Target::SpaceId(space_id)) => self
2841                            .store
2842                            .chat_ids_in_space(InlineId::new(space_id))
2843                            .await
2844                            .map_err(store_error_to_backend)?,
2845                        None => {
2846                            return Err(BackendError::new(
2847                                ClientErrorCategory::ProtocolMismatch,
2848                                "clear history update had no target",
2849                            ));
2850                        }
2851                    };
2852                    for chat_id in chat_ids {
2853                        self.store
2854                            .clear_chat_messages(chat_id, update.before_date)
2855                            .await
2856                            .map_err(store_error_to_backend)?;
2857                        events.push(ClientEvent::ChatHistoryCleared {
2858                            chat_id,
2859                            before_date: update.before_date,
2860                        });
2861                    }
2862                    for deleted_chat_id in update.deleted_chat_ids {
2863                        let chat_id = InlineId::new(deleted_chat_id);
2864                        self.store
2865                            .remove_dialog(chat_id)
2866                            .await
2867                            .map_err(store_error_to_backend)?;
2868                        events.push(ClientEvent::ChatDeleted { chat_id });
2869                    }
2870                    for changed_chat_id in update
2871                        .orphaned_chat_ids
2872                        .into_iter()
2873                        .chain(update.detached_chat_ids)
2874                    {
2875                        events.push(ClientEvent::ChatUpserted {
2876                            chat_id: InlineId::new(changed_chat_id),
2877                        });
2878                    }
2879                }
2880                Some(proto::update::Update::DeleteChat(update)) => {
2881                    if let Some(chat_id) = self
2882                        .resolve_chat_id(update.peer_id.as_ref(), fallback_chat_id)
2883                        .await?
2884                    {
2885                        self.store
2886                            .remove_dialog(chat_id)
2887                            .await
2888                            .map_err(store_error_to_backend)?;
2889                        events.push(ClientEvent::ChatDeleted { chat_id });
2890                    }
2891                }
2892                Some(proto::update::Update::ParticipantAdd(update)) => {
2893                    let chat_id = InlineId::new(update.chat_id);
2894                    if let Some(participant) = update.participant {
2895                        self.store
2896                            .record_chat_participant(
2897                                chat_id,
2898                                ChatParticipantRecord {
2899                                    user_id: InlineId::new(participant.user_id),
2900                                    date: Some(participant.date),
2901                                },
2902                            )
2903                            .await
2904                            .map_err(store_error_to_backend)?;
2905                        events.push(ClientEvent::ChatParticipantsChanged { chat_id });
2906                    }
2907                }
2908                Some(proto::update::Update::ParticipantDelete(update)) => {
2909                    let chat_id = InlineId::new(update.chat_id);
2910                    self.store
2911                        .remove_chat_participant(chat_id, InlineId::new(update.user_id))
2912                        .await
2913                        .map_err(store_error_to_backend)?;
2914                    events.push(ClientEvent::ChatParticipantsChanged { chat_id });
2915                }
2916                Some(proto::update::Update::SpaceMemberAdd(update)) => {
2917                    let member = update.member.ok_or_else(|| {
2918                        BackendError::new(
2919                            ClientErrorCategory::ProtocolMismatch,
2920                            "space member add update had no member",
2921                        )
2922                    })?;
2923                    if let Some(user) = update.user {
2924                        let user = user_record_from_proto(&user);
2925                        self.store
2926                            .record_users(vec![user.clone()])
2927                            .await
2928                            .map_err(store_error_to_backend)?;
2929                        events.push(ClientEvent::UserUpserted {
2930                            user_id: user.user_id,
2931                        });
2932                    }
2933                    let member = space_member_record_from_proto(&member);
2934                    self.store
2935                        .record_space_member(member.clone())
2936                        .await
2937                        .map_err(store_error_to_backend)?;
2938                    events.push(ClientEvent::SpaceMemberChanged {
2939                        space_id: member.space_id,
2940                        user_id: member.user_id,
2941                        removed: false,
2942                    });
2943                }
2944                Some(proto::update::Update::SpaceMemberDelete(update)) => {
2945                    let space_id = InlineId::new(update.space_id);
2946                    let user_id = InlineId::new(update.user_id);
2947                    self.store
2948                        .remove_space_member(space_id, user_id)
2949                        .await
2950                        .map_err(store_error_to_backend)?;
2951                    events.push(ClientEvent::SpaceMemberChanged {
2952                        space_id,
2953                        user_id,
2954                        removed: true,
2955                    });
2956                }
2957                Some(proto::update::Update::SpaceMemberUpdate(update)) => {
2958                    let member = update.member.ok_or_else(|| {
2959                        BackendError::new(
2960                            ClientErrorCategory::ProtocolMismatch,
2961                            "space member update had no member",
2962                        )
2963                    })?;
2964                    let member = space_member_record_from_proto(&member);
2965                    self.store
2966                        .record_space_member(member.clone())
2967                        .await
2968                        .map_err(store_error_to_backend)?;
2969                    events.push(ClientEvent::SpaceMemberChanged {
2970                        space_id: member.space_id,
2971                        user_id: member.user_id,
2972                        removed: false,
2973                    });
2974                }
2975                Some(proto::update::Update::JoinSpace(update)) => {
2976                    let space = update.space.ok_or_else(|| {
2977                        BackendError::new(
2978                            ClientErrorCategory::ProtocolMismatch,
2979                            "join space update had no space",
2980                        )
2981                    })?;
2982                    let space = space_record_from_proto(&space);
2983                    self.store
2984                        .record_space(space.clone())
2985                        .await
2986                        .map_err(store_error_to_backend)?;
2987                    events.push(ClientEvent::SpaceUpserted {
2988                        space_id: space.space_id,
2989                    });
2990                    if let Some(member) = update.member {
2991                        let member = space_member_record_from_proto(&member);
2992                        self.store
2993                            .record_space_member(member.clone())
2994                            .await
2995                            .map_err(store_error_to_backend)?;
2996                        events.push(ClientEvent::SpaceMemberChanged {
2997                            space_id: member.space_id,
2998                            user_id: member.user_id,
2999                            removed: false,
3000                        });
3001                    }
3002                }
3003                Some(proto::update::Update::UpdateUserSettings(update)) => {
3004                    let settings = update.settings.ok_or_else(|| {
3005                        BackendError::new(
3006                            ClientErrorCategory::ProtocolMismatch,
3007                            "user settings update had no settings",
3008                        )
3009                    })?;
3010                    self.store
3011                        .record_user_settings(user_settings_record_from_proto(&settings))
3012                        .await
3013                        .map_err(store_error_to_backend)?;
3014                    events.push(ClientEvent::UserSettingsChanged {});
3015                }
3016                Some(proto::update::Update::MessageActionInvoked(update)) => {
3017                    events.push(ClientEvent::MessageActionInvoked {
3018                        interaction_id: InlineId::new(update.interaction_id),
3019                        chat_id: InlineId::new(update.chat_id),
3020                        message_id: InlineId::new(update.message_id),
3021                        actor_user_id: InlineId::new(update.actor_user_id),
3022                        action_id: update.action_id,
3023                        data: update.data,
3024                    });
3025                }
3026                Some(proto::update::Update::MessageActionAnswered(update)) => {
3027                    let toast = update.ui.and_then(|ui| match ui.kind {
3028                        Some(proto::message_action_response_ui::Kind::Toast(toast)) => {
3029                            trimmed_option(Some(toast.text))
3030                        }
3031                        None => None,
3032                    });
3033                    events.push(ClientEvent::MessageActionAnswered {
3034                        interaction_id: InlineId::new(update.interaction_id),
3035                        toast,
3036                    });
3037                }
3038                Some(proto::update::Update::UpdatedUser(update)) => {
3039                    if let Some(user) = update.user {
3040                        let record = user_record_from_proto(&user);
3041                        self.store
3042                            .record_users(vec![record.clone()])
3043                            .await
3044                            .map_err(store_error_to_backend)?;
3045                        events.push(ClientEvent::UserUpserted {
3046                            user_id: record.user_id,
3047                        });
3048                    }
3049                }
3050                Some(proto::update::Update::ChatSkipPts(_)) => {}
3051                Some(proto::update::Update::UpdateUserStatus(update)) => {
3052                    let status = update.status.as_ref();
3053                    let is_online = status.and_then(|status| {
3054                        match proto::user_status::Status::try_from(status.online).ok()? {
3055                            proto::user_status::Status::Online => Some(true),
3056                            proto::user_status::Status::Offline => Some(false),
3057                            proto::user_status::Status::Unknown => None,
3058                        }
3059                    });
3060                    events.push(ClientEvent::UserStatusChanged {
3061                        user_id: InlineId::new(update.user_id),
3062                        is_online,
3063                        last_online: status
3064                            .and_then(|status| status.last_online.as_ref())
3065                            .and_then(|last_online| last_online.date),
3066                    });
3067                }
3068                Some(proto::update::Update::BotPresence(update)) => {
3069                    let state = update.state.as_ref();
3070                    let kind = state
3071                        .and_then(|state| {
3072                            proto::bot_presence_state::Kind::try_from(state.kind).ok()
3073                        })
3074                        .map(|kind| kind.as_str_name().to_owned())
3075                        .unwrap_or_else(|| "KIND_UNSPECIFIED".to_owned());
3076                    let chat_id = self
3077                        .resolve_chat_id(update.peer_id.as_ref(), fallback_chat_id)
3078                        .await?;
3079                    events.push(ClientEvent::BotPresenceChanged {
3080                        bot_user_id: InlineId::new(update.bot_user_id),
3081                        chat_id,
3082                        kind,
3083                        comment: state.and_then(|state| trimmed_option(state.comment.clone())),
3084                        avatar_changed: update.avatar_changed,
3085                    });
3086                }
3087                Some(proto::update::Update::NewMessageNotification(update)) => {
3088                    if let Some(message) = update.message {
3089                        let message =
3090                            message_record_from_proto_message(message, fallback_chat_id, None);
3091                        let reason =
3092                            proto::update_new_message_notification::Reason::try_from(update.reason)
3093                                .map(|reason| reason.as_str_name().to_owned())
3094                                .unwrap_or_else(|_| "REASON_UNSPECIFIED".to_owned());
3095                        events.push(ClientEvent::NewMessageNotification { message, reason });
3096                    }
3097                }
3098                Some(other) => {
3099                    let kind = update_kind(&other);
3100                    return Err(BackendError::new(
3101                        ClientErrorCategory::Unsupported,
3102                        format!("Inline update is not implemented: {kind}"),
3103                    ));
3104                }
3105                None if seq > 0 => {
3106                    return Err(BackendError::new(
3107                        ClientErrorCategory::ProtocolMismatch,
3108                        "sequenced Inline update had no payload",
3109                    ));
3110                }
3111                None => {}
3112            }
3113        }
3114        Ok(events)
3115    }
3116}
3117
3118fn store_error_to_backend(error: StoreError) -> BackendError {
3119    BackendError::new(error.category, error.message)
3120}
3121
3122fn unresolved_update_peer(kind: &str) -> BackendError {
3123    BackendError::new(
3124        ClientErrorCategory::ProtocolMismatch,
3125        format!("{kind} update did not resolve to a stored chat"),
3126    )
3127}
3128
3129fn update_kind(update: &proto::update::Update) -> &'static str {
3130    use proto::update::Update;
3131    match update {
3132        Update::NewMessage(_) => "new_message",
3133        Update::EditMessage(_) => "edit_message",
3134        Update::UpdateMessageId(_) => "update_message_id",
3135        Update::DeleteMessages(_) => "delete_messages",
3136        Update::UpdateComposeAction(_) => "update_compose_action",
3137        Update::UpdateUserStatus(_) => "update_user_status",
3138        Update::MessageAttachment(_) => "message_attachment",
3139        Update::UpdateReaction(_) => "update_reaction",
3140        Update::DeleteReaction(_) => "delete_reaction",
3141        Update::ParticipantAdd(_) => "participant_add",
3142        Update::ParticipantDelete(_) => "participant_delete",
3143        Update::NewChat(_) => "new_chat",
3144        Update::DeleteChat(_) => "delete_chat",
3145        Update::SpaceMemberAdd(_) => "space_member_add",
3146        Update::SpaceMemberDelete(_) => "space_member_delete",
3147        Update::JoinSpace(_) => "join_space",
3148        Update::UpdateReadMaxId(_) => "update_read_max_id",
3149        Update::UpdateUserSettings(_) => "update_user_settings",
3150        Update::NewMessageNotification(_) => "new_message_notification",
3151        Update::MarkAsUnread(_) => "mark_as_unread",
3152        Update::ChatSkipPts(_) => "chat_skip_pts",
3153        Update::ChatHasNewUpdates(_) => "chat_has_new_updates",
3154        Update::SpaceHasNewUpdates(_) => "space_has_new_updates",
3155        Update::SpaceMemberUpdate(_) => "space_member_update",
3156        Update::ChatVisibility(_) => "chat_visibility",
3157        Update::DialogArchived(_) => "dialog_archived",
3158        Update::ChatInfo(_) => "chat_info",
3159        Update::PinnedMessages(_) => "pinned_messages",
3160        Update::ChatMoved(_) => "chat_moved",
3161        Update::DialogNotificationSettings(_) => "dialog_notification_settings",
3162        Update::ChatOpen(_) => "chat_open",
3163        Update::MessageActionInvoked(_) => "message_action_invoked",
3164        Update::MessageActionAnswered(_) => "message_action_answered",
3165        Update::ClearChatHistory(_) => "clear_chat_history",
3166        Update::BotPresence(_) => "bot_presence",
3167        Update::DialogFollowMode(_) => "dialog_follow_mode",
3168        Update::UpdatedUser(_) => "updated_user",
3169    }
3170}
3171
3172#[cfg(test)]
3173fn dialogs_page_from_get_chats(
3174    result: &proto::GetChatsResult,
3175    request: DialogsRequest,
3176) -> BackendResult<DialogsPage> {
3177    let start = request
3178        .cursor
3179        .as_deref()
3180        .map(str::trim)
3181        .filter(|cursor| !cursor.is_empty())
3182        .map(|cursor| {
3183            cursor.parse::<usize>().map_err(|_| {
3184                BackendError::new(
3185                    ClientErrorCategory::InvalidInput,
3186                    "invalid pagination cursor",
3187                )
3188            })
3189        })
3190        .transpose()?
3191        .unwrap_or(0);
3192    let limit = request.limit.unwrap_or(100).max(1) as usize;
3193
3194    let mut dialogs = dialog_records_from_get_chats(result);
3195    let total = dialogs.len();
3196    if start >= dialogs.len() {
3197        dialogs.clear();
3198    } else {
3199        dialogs = dialogs.into_iter().skip(start).take(limit).collect();
3200    }
3201    Ok(DialogsPage {
3202        dialogs,
3203        users: result.users.iter().map(user_record_from_proto).collect(),
3204        next_cursor: (start + limit < total).then(|| (start + limit).to_string()),
3205    })
3206}
3207
3208fn dialog_records_from_get_chats(result: &proto::GetChatsResult) -> Vec<DialogRecord> {
3209    let users_by_id = result
3210        .users
3211        .iter()
3212        .map(|user| (user.id, user))
3213        .collect::<HashMap<_, _>>();
3214    result
3215        .chats
3216        .iter()
3217        .map(|chat| {
3218            let dialog = dialog_for_chat(result, chat);
3219            DialogRecord {
3220                chat_id: InlineId::new(chat.id),
3221                peer_user_id: dialog_peer_user_id(dialog, chat),
3222                title: Some(chat_title_from_proto(chat, &users_by_id)),
3223                emoji: chat_emoji_from_proto(chat),
3224                last_message_id: chat.last_msg_id.map(InlineId::new),
3225                synced_through_message_id: None,
3226                unread_count: dialog
3227                    .and_then(|dialog| dialog.unread_count)
3228                    .and_then(|count| u32::try_from(count).ok()),
3229                space_id: dialog
3230                    .and_then(|dialog| dialog.space_id)
3231                    .or(chat.space_id)
3232                    .map(InlineId::new),
3233                is_public: chat.is_public,
3234                archived: dialog.and_then(|dialog| dialog.archived),
3235                pinned: dialog.and_then(|dialog| dialog.pinned),
3236                open: dialog.and_then(|dialog| dialog.open),
3237                chat_list_hidden: dialog.and_then(|dialog| dialog.chat_list_hidden),
3238                order: dialog.and_then(|dialog| dialog.order.clone()),
3239                pinned_order: dialog.and_then(|dialog| dialog.pinned_order.clone()),
3240                notification_mode: dialog.and_then(dialog_notification_mode_from_proto),
3241                follow_mode: dialog.and_then(dialog_follow_mode_from_proto),
3242                ..DialogRecord::new(InlineId::new(chat.id))
3243            }
3244        })
3245        .collect()
3246}
3247
3248fn dialog_for_chat<'a>(
3249    result: &'a proto::GetChatsResult,
3250    chat: &proto::Chat,
3251) -> Option<&'a proto::Dialog> {
3252    result.dialogs.iter().find(|dialog| {
3253        dialog.chat_id == Some(chat.id)
3254            || dialog
3255                .peer
3256                .as_ref()
3257                .zip(chat.peer_id.as_ref())
3258                .is_some_and(|(dialog_peer, chat_peer)| dialog_peer == chat_peer)
3259    })
3260}
3261
3262fn read_state_from_proto_dialog(
3263    chat_id: InlineId,
3264    dialog: &proto::Dialog,
3265) -> Option<StoredReadState> {
3266    if dialog.read_max_id.is_none() && dialog.unread_count.is_none() && dialog.unread_mark.is_none()
3267    {
3268        return None;
3269    }
3270    Some(StoredReadState {
3271        chat_id,
3272        read_max_id: dialog.read_max_id.map(InlineId::new),
3273        unread_count: dialog
3274            .unread_count
3275            .and_then(|count| u32::try_from(count).ok()),
3276        marked_unread: dialog.unread_mark.unwrap_or(false),
3277    })
3278}
3279
3280fn dialog_peer_user_id(dialog: Option<&proto::Dialog>, chat: &proto::Chat) -> Option<InlineId> {
3281    dialog
3282        .and_then(|dialog| dialog.peer.as_ref())
3283        .or(chat.peer_id.as_ref())
3284        .and_then(user_id_from_peer)
3285        .map(InlineId::new)
3286}
3287
3288fn dialog_notification_mode_from_proto(dialog: &proto::Dialog) -> Option<DialogNotificationMode> {
3289    dialog
3290        .notification_settings
3291        .as_ref()
3292        .and_then(dialog_notification_mode_from_settings)
3293}
3294
3295fn dialog_notification_mode_from_settings(
3296    settings: &proto::DialogNotificationSettings,
3297) -> Option<DialogNotificationMode> {
3298    match proto::dialog_notification_settings::Mode::try_from(settings.mode?).ok()? {
3299        proto::dialog_notification_settings::Mode::All => Some(DialogNotificationMode::All),
3300        proto::dialog_notification_settings::Mode::Mentions => {
3301            Some(DialogNotificationMode::Mentions)
3302        }
3303        proto::dialog_notification_settings::Mode::None => Some(DialogNotificationMode::None),
3304        proto::dialog_notification_settings::Mode::Unspecified => None,
3305    }
3306}
3307
3308fn dialog_notification_mode_to_proto(
3309    mode: DialogNotificationMode,
3310) -> proto::dialog_notification_settings::Mode {
3311    match mode {
3312        DialogNotificationMode::All => proto::dialog_notification_settings::Mode::All,
3313        DialogNotificationMode::Mentions => proto::dialog_notification_settings::Mode::Mentions,
3314        DialogNotificationMode::None => proto::dialog_notification_settings::Mode::None,
3315    }
3316}
3317
3318fn dialog_follow_mode_from_proto(dialog: &proto::Dialog) -> Option<DialogFollowMode> {
3319    dialog.follow_mode.and_then(dialog_follow_mode_from_value)
3320}
3321
3322fn dialog_follow_mode_from_value(value: i32) -> Option<DialogFollowMode> {
3323    match proto::DialogFollowMode::try_from(value).ok()? {
3324        proto::DialogFollowMode::Following => Some(DialogFollowMode::Following),
3325        proto::DialogFollowMode::Unspecified => None,
3326    }
3327}
3328
3329fn chat_title_from_proto(chat: &proto::Chat, users_by_id: &HashMap<i64, &proto::User>) -> String {
3330    chat.peer_id
3331        .as_ref()
3332        .and_then(user_id_from_peer)
3333        .and_then(|user_id| users_by_id.get(&user_id))
3334        .and_then(|user| user_display_name_from_proto(user))
3335        .unwrap_or_else(|| {
3336            let title = chat.title.trim();
3337            if title.is_empty() {
3338                format!("Chat {}", chat.id)
3339            } else {
3340                title.to_owned()
3341            }
3342        })
3343}
3344
3345fn chat_emoji_from_proto(chat: &proto::Chat) -> Option<String> {
3346    non_empty_option(chat.emoji.as_deref())
3347}
3348
3349fn user_record_from_proto(user: &proto::User) -> UserRecord {
3350    UserRecord {
3351        user_id: InlineId::new(user.id),
3352        display_name: user_display_name_from_proto(user),
3353        username: non_empty_option(user.username.as_deref()),
3354        first_name: non_empty_option(user.first_name.as_deref()),
3355        last_name: non_empty_option(user.last_name.as_deref()),
3356        avatar_url: user
3357            .profile_photo
3358            .as_ref()
3359            .and_then(|photo| non_empty_option(photo.cdn_url.as_deref()))
3360            .or_else(|| {
3361                user.bot_avatar
3362                    .as_ref()
3363                    .and_then(|avatar| non_empty_option(avatar.cdn_url.as_deref()))
3364            }),
3365        is_bot: user.bot,
3366    }
3367}
3368
3369fn space_record_from_proto(space: &proto::Space) -> SpaceRecord {
3370    SpaceRecord {
3371        space_id: InlineId::new(space.id),
3372        name: space.name.clone(),
3373        creator: space.creator,
3374        date: space.date,
3375        is_public: space.is_public,
3376    }
3377}
3378
3379fn space_member_record_from_proto(member: &proto::Member) -> SpaceMemberRecord {
3380    SpaceMemberRecord {
3381        space_id: InlineId::new(member.space_id),
3382        user_id: InlineId::new(member.user_id),
3383        role: member
3384            .role
3385            .and_then(|role| match proto::member::Role::try_from(role).ok()? {
3386                proto::member::Role::Owner => Some(SpaceMemberRole::Owner),
3387                proto::member::Role::Admin => Some(SpaceMemberRole::Admin),
3388                proto::member::Role::Member => Some(SpaceMemberRole::Member),
3389            }),
3390        date: member.date,
3391        can_access_public_chats: member.can_access_public_chats,
3392    }
3393}
3394
3395fn user_settings_record_from_proto(settings: &proto::UserSettings) -> UserSettingsRecord {
3396    let notifications = settings.notification_settings.as_ref();
3397    UserSettingsRecord {
3398        notification_mode: notifications
3399            .and_then(|settings| settings.mode)
3400            .and_then(
3401                |mode| match proto::notification_settings::Mode::try_from(mode).ok()? {
3402                    proto::notification_settings::Mode::All => Some(NotificationMode::All),
3403                    proto::notification_settings::Mode::None => Some(NotificationMode::None),
3404                    proto::notification_settings::Mode::Mentions => {
3405                        Some(NotificationMode::Mentions)
3406                    }
3407                    proto::notification_settings::Mode::ImportantOnly => {
3408                        Some(NotificationMode::ImportantOnly)
3409                    }
3410                    proto::notification_settings::Mode::OnlyMentions => {
3411                        Some(NotificationMode::OnlyMentions)
3412                    }
3413                    proto::notification_settings::Mode::Unspecified => None,
3414                },
3415            ),
3416        silent: notifications.and_then(|settings| settings.silent),
3417        zen_mode_requires_mention: notifications
3418            .and_then(|settings| settings.zen_mode_requires_mention),
3419        zen_mode_uses_default_rules: notifications
3420            .and_then(|settings| settings.zen_mode_uses_default_rules),
3421        zen_mode_custom_rules: notifications
3422            .and_then(|settings| settings.zen_mode_custom_rules.clone()),
3423        disable_dm_notifications: notifications
3424            .and_then(|settings| settings.disable_dm_notifications),
3425    }
3426}
3427
3428fn chat_participants_page_from_proto(
3429    result: proto::GetChatParticipantsResult,
3430) -> ChatParticipantsPage {
3431    let mut seen = std::collections::HashSet::new();
3432    let mut participants = Vec::new();
3433    for participant in result.participants {
3434        if seen.insert(participant.user_id) {
3435            participants.push(ChatParticipantRecord {
3436                user_id: InlineId::new(participant.user_id),
3437                date: Some(participant.date),
3438            });
3439        }
3440    }
3441    participants.sort_by_key(|participant| participant.user_id.get());
3442    ChatParticipantsPage {
3443        participants,
3444        users: result.users.iter().map(user_record_from_proto).collect(),
3445    }
3446}
3447
3448fn validate_create_participants(participants: &[ChatCreateParticipant]) -> BackendResult<()> {
3449    if participants
3450        .iter()
3451        .any(|participant| participant.user_id.get() <= 0)
3452    {
3453        return Err(BackendError::new(
3454            ClientErrorCategory::InvalidInput,
3455            "participant user_id must be positive",
3456        ));
3457    }
3458    Ok(())
3459}
3460
3461fn validate_chat_and_user_ids(chat_id: InlineId, user_id: InlineId) -> BackendResult<()> {
3462    if chat_id.get() <= 0 || user_id.get() <= 0 {
3463        return Err(BackendError::new(
3464            ClientErrorCategory::InvalidInput,
3465            "chat_id and user_id must be positive",
3466        ));
3467    }
3468    Ok(())
3469}
3470
3471fn created_chat_from_private_chat_result(
3472    chat: Value,
3473    dialog: Value,
3474    user: Value,
3475    user_id: InlineId,
3476) -> BackendResult<(CreatedChat, Option<UserRecord>)> {
3477    let chat_id = api_i64_field(&chat, &["id", "chatId", "chat_id"])
3478        .or_else(|| api_i64_field(&dialog, &["chatId", "chat_id"]))
3479        .ok_or_else(|| {
3480            BackendError::new(
3481                ClientErrorCategory::ProtocolMismatch,
3482                "create_private_chat result did not include a chat id",
3483            )
3484        })?;
3485    let user = user_record_from_api_value(&user).or(Some(UserRecord {
3486        user_id,
3487        display_name: None,
3488        username: None,
3489        first_name: None,
3490        last_name: None,
3491        avatar_url: None,
3492        is_bot: None,
3493    }));
3494    let title = api_string_field(&chat, &["title"])
3495        .or_else(|| user.as_ref().and_then(user_display_name_from_record));
3496    Ok((
3497        CreatedChat {
3498            chat_id: InlineId::new(chat_id),
3499            title,
3500            parent_chat_id: None,
3501            parent_message_id: None,
3502        },
3503        user,
3504    ))
3505}
3506
3507fn created_chat_from_proto(
3508    chat: Option<proto::Chat>,
3509    dialog: Option<proto::Dialog>,
3510    anchor_message: Option<proto::Message>,
3511    parent_chat_id: Option<InlineId>,
3512    parent_message_id: Option<InlineId>,
3513) -> BackendResult<CreatedChat> {
3514    let chat_id = chat
3515        .as_ref()
3516        .map(|chat| chat.id)
3517        .or_else(|| dialog.as_ref().and_then(|dialog| dialog.chat_id))
3518        .ok_or_else(|| {
3519            BackendError::new(
3520                ClientErrorCategory::ProtocolMismatch,
3521                "create chat result did not include a chat id",
3522            )
3523        })?;
3524    let title = chat
3525        .as_ref()
3526        .and_then(|chat| trimmed_option(Some(chat.title.clone())));
3527    let parent_chat_id = parent_chat_id.or_else(|| {
3528        chat.as_ref()
3529            .and_then(|chat| chat.parent_chat_id)
3530            .map(InlineId::new)
3531    });
3532    let parent_message_id = parent_message_id
3533        .or_else(|| {
3534            chat.as_ref()
3535                .and_then(|chat| chat.parent_message_id)
3536                .map(InlineId::new)
3537        })
3538        .or_else(|| anchor_message.map(|message| InlineId::new(message.id)));
3539
3540    Ok(CreatedChat {
3541        chat_id: InlineId::new(chat_id),
3542        title,
3543        parent_chat_id,
3544        parent_message_id,
3545    })
3546}
3547
3548fn user_record_from_api_value(value: &Value) -> Option<UserRecord> {
3549    let user_id = api_i64_field(value, &["id", "userId", "user_id"])?;
3550    Some(UserRecord {
3551        user_id: InlineId::new(user_id),
3552        display_name: api_string_field(value, &["displayName", "display_name", "name"]),
3553        username: api_string_field(value, &["username"]),
3554        first_name: api_string_field(value, &["firstName", "first_name"]),
3555        last_name: api_string_field(value, &["lastName", "last_name"]),
3556        avatar_url: api_string_field(value, &["avatarUrl", "avatar_url"])
3557            .or_else(|| api_nested_string_field(value, "profilePhoto", &["cdnUrl", "cdn_url"]))
3558            .or_else(|| api_nested_string_field(value, "profile_photo", &["cdnUrl", "cdn_url"]))
3559            .or_else(|| api_nested_string_field(value, "botAvatar", &["cdnUrl", "cdn_url"]))
3560            .or_else(|| api_nested_string_field(value, "bot_avatar", &["cdnUrl", "cdn_url"])),
3561        is_bot: api_bool_field(value, &["isBot", "is_bot", "bot"]),
3562    })
3563}
3564
3565fn user_display_name_from_record(user: &UserRecord) -> Option<String> {
3566    user.display_name
3567        .clone()
3568        .or_else(|| {
3569            let first = user.first_name.as_deref().unwrap_or_default().trim();
3570            let last = user.last_name.as_deref().unwrap_or_default().trim();
3571            let full = [first, last]
3572                .into_iter()
3573                .filter(|part| !part.is_empty())
3574                .collect::<Vec<_>>()
3575                .join(" ");
3576            (!full.is_empty()).then_some(full)
3577        })
3578        .or_else(|| user.username.clone())
3579}
3580
3581fn api_i64_field(value: &Value, fields: &[&str]) -> Option<i64> {
3582    fields
3583        .iter()
3584        .find_map(|field| value.get(*field).and_then(value_as_i64))
3585}
3586
3587fn api_bool_field(value: &Value, fields: &[&str]) -> Option<bool> {
3588    fields
3589        .iter()
3590        .find_map(|field| value.get(*field).and_then(Value::as_bool))
3591}
3592
3593fn api_string_field(value: &Value, fields: &[&str]) -> Option<String> {
3594    fields
3595        .iter()
3596        .find_map(|field| value.get(*field).and_then(value_as_string))
3597}
3598
3599fn api_nested_string_field(value: &Value, field: &str, nested_fields: &[&str]) -> Option<String> {
3600    value
3601        .get(field)
3602        .and_then(|nested| api_string_field(nested, nested_fields))
3603}
3604
3605fn value_as_i64(value: &Value) -> Option<i64> {
3606    value
3607        .as_i64()
3608        .or_else(|| value.as_u64().and_then(|value| i64::try_from(value).ok()))
3609        .or_else(|| value.as_str()?.trim().parse().ok())
3610}
3611
3612fn value_as_string(value: &Value) -> Option<String> {
3613    value
3614        .as_str()
3615        .map(str::trim)
3616        .filter(|value| !value.is_empty())
3617        .map(ToOwned::to_owned)
3618}
3619
3620fn trimmed_option(value: Option<String>) -> Option<String> {
3621    value
3622        .map(|value| value.trim().to_owned())
3623        .filter(|value| !value.is_empty())
3624}
3625
3626fn normalized_account_namespace(value: Option<&str>) -> Option<&str> {
3627    value.map(str::trim).filter(|value| !value.is_empty())
3628}
3629
3630fn user_display_name_from_proto(user: &proto::User) -> Option<String> {
3631    if let Some(bot_name) = user
3632        .bot_avatar
3633        .as_ref()
3634        .and_then(|avatar| non_empty_option(Some(&avatar.display_name)))
3635    {
3636        return Some(bot_name);
3637    }
3638    let first = user.first_name.as_deref().unwrap_or_default().trim();
3639    let last = user.last_name.as_deref().unwrap_or_default().trim();
3640    let full = [first, last]
3641        .into_iter()
3642        .filter(|part| !part.is_empty())
3643        .collect::<Vec<_>>()
3644        .join(" ");
3645    if !full.is_empty() {
3646        return Some(full);
3647    }
3648    non_empty_option(user.username.as_deref())
3649}
3650
3651fn user_id_from_peer(peer: &proto::Peer) -> Option<i64> {
3652    match &peer.r#type {
3653        Some(proto::peer::Type::User(user)) => Some(user.user_id),
3654        _ => None,
3655    }
3656}
3657
3658fn non_empty_option(value: Option<&str>) -> Option<String> {
3659    value
3660        .map(str::trim)
3661        .filter(|value| !value.is_empty())
3662        .map(ToOwned::to_owned)
3663}
3664
3665fn realtime_error_to_backend(error: RealtimeError) -> BackendError {
3666    match error {
3667        RealtimeError::InvalidUrl { message, .. } => {
3668            BackendError::new(ClientErrorCategory::InvalidInput, message)
3669        }
3670        RealtimeError::Timeout { .. } => {
3671            BackendError::new(ClientErrorCategory::Timeout, error.to_string())
3672        }
3673        RealtimeError::ConnectionError { .. } => {
3674            BackendError::new(ClientErrorCategory::AuthExpired, error.to_string())
3675        }
3676        RealtimeError::RpcError {
3677            code,
3678            error_name,
3679            friendly,
3680            ..
3681        } if code == 420 || code == 429 || is_rate_limit_error_name(&error_name) => {
3682            rate_limited_backend_error(friendly)
3683        }
3684        RealtimeError::RpcError {
3685            error_name,
3686            friendly,
3687            ..
3688        } if error_name == "UNAUTHENTICATED" => {
3689            BackendError::new(ClientErrorCategory::AuthExpired, friendly)
3690        }
3691        RealtimeError::RpcError { friendly, .. } => {
3692            BackendError::new(ClientErrorCategory::Internal, friendly)
3693        }
3694        RealtimeError::ConnectionClosed
3695        | RealtimeError::EventLagged { .. }
3696        | RealtimeError::WebSocket(_) => {
3697            BackendError::new(ClientErrorCategory::Network, error.to_string())
3698        }
3699        RealtimeError::InvalidHeaderValue { .. }
3700        | RealtimeError::Protocol(_)
3701        | RealtimeError::MissingResult
3702        | RealtimeError::UnexpectedResult { .. } => {
3703            BackendError::new(ClientErrorCategory::ProtocolMismatch, error.to_string())
3704        }
3705        _ => BackendError::new(ClientErrorCategory::Internal, error.to_string()),
3706    }
3707}
3708
3709fn realtime_error_closes_session(error: &RealtimeError) -> bool {
3710    matches!(
3711        error,
3712        RealtimeError::ConnectionClosed
3713            | RealtimeError::ConnectionError { .. }
3714            | RealtimeError::WebSocket(_)
3715    )
3716}
3717
3718fn api_error_to_backend(error: ApiError) -> BackendError {
3719    let rendered = error.to_string();
3720    match error {
3721        ApiError::InvalidBaseUrl { message, .. } | ApiError::InvalidInput { message } => {
3722            BackendError::new(ClientErrorCategory::InvalidInput, message)
3723        }
3724        ApiError::Status { status, .. } if status == 401 || status == 403 => {
3725            BackendError::new(ClientErrorCategory::AuthExpired, rendered)
3726        }
3727        ApiError::Status {
3728            status: 420 | 429, ..
3729        } => rate_limited_backend_error(rendered),
3730        ApiError::Api {
3731            status: Some(401) | Some(403),
3732            ..
3733        } => BackendError::new(ClientErrorCategory::AuthExpired, rendered),
3734        ApiError::Api {
3735            status: Some(420) | Some(429),
3736            ..
3737        } => rate_limited_backend_error(rendered),
3738        ApiError::Api { ref error, .. } if is_rate_limit_error_name(error) => {
3739            rate_limited_backend_error(rendered)
3740        }
3741        ApiError::Http(_) | ApiError::Status { .. } => {
3742            BackendError::new(ClientErrorCategory::Network, rendered)
3743        }
3744        ApiError::Json(_) => BackendError::new(ClientErrorCategory::ProtocolMismatch, rendered),
3745        ApiError::Io(_) | ApiError::Api { .. } => {
3746            BackendError::new(ClientErrorCategory::Internal, rendered)
3747        }
3748        _ => BackendError::new(ClientErrorCategory::Internal, rendered),
3749    }
3750}
3751
3752fn rate_limited_backend_error(message: String) -> BackendError {
3753    let retry_after_seconds = retry_after_seconds_from_message(&message);
3754    let error = BackendError::new(ClientErrorCategory::RateLimited, message);
3755    match retry_after_seconds {
3756        Some(seconds) => error.with_retry_after_seconds(seconds),
3757        None => error,
3758    }
3759}
3760
3761fn is_rate_limit_error_name(name: &str) -> bool {
3762    matches!(
3763        name.trim().to_ascii_uppercase().as_str(),
3764        "RATE_LIMIT" | "RATE_LIMITED" | "FLOOD_WAIT"
3765    )
3766}
3767
3768fn input_peer_for_client_peer(peer: PeerRef) -> proto::InputPeer {
3769    proto::InputPeer {
3770        r#type: Some(match peer {
3771            PeerRef::User { user_id } => proto::input_peer::Type::User(proto::InputPeerUser {
3772                user_id: user_id.get(),
3773            }),
3774            PeerRef::Chat { chat_id } => proto::input_peer::Type::Chat(proto::InputPeerChat {
3775                chat_id: chat_id.get(),
3776            }),
3777            PeerRef::Thread { thread_id } => proto::input_peer::Type::Chat(proto::InputPeerChat {
3778                chat_id: thread_id.get(),
3779            }),
3780        }),
3781    }
3782}
3783
3784fn input_peer_for_chat(chat_id: InlineId) -> proto::InputPeer {
3785    proto::InputPeer {
3786        r#type: Some(proto::input_peer::Type::Chat(proto::InputPeerChat {
3787            chat_id: chat_id.get(),
3788        })),
3789    }
3790}
3791
3792fn input_peer_for_sync_bucket_peer(peer: crate::SyncBucketPeer) -> proto::InputPeer {
3793    let r#type = match peer {
3794        crate::SyncBucketPeer::User { user_id } => {
3795            proto::input_peer::Type::User(proto::InputPeerUser {
3796                user_id: user_id.get(),
3797            })
3798        }
3799        crate::SyncBucketPeer::Chat { chat_id } => {
3800            proto::input_peer::Type::Chat(proto::InputPeerChat {
3801                chat_id: chat_id.get(),
3802            })
3803        }
3804    };
3805    proto::InputPeer {
3806        r#type: Some(r#type),
3807    }
3808}
3809
3810fn sync_bucket_peer_for_chat(chat: &proto::Chat) -> crate::SyncBucketPeer {
3811    match chat.peer_id.as_ref().and_then(|peer| peer.r#type.as_ref()) {
3812        Some(proto::peer::Type::User(user)) => crate::SyncBucketPeer::User {
3813            user_id: InlineId::new(user.user_id),
3814        },
3815        Some(proto::peer::Type::Chat(peer)) => crate::SyncBucketPeer::Chat {
3816            chat_id: InlineId::new(peer.chat_id),
3817        },
3818        None => crate::SyncBucketPeer::Chat {
3819            chat_id: InlineId::new(chat.id),
3820        },
3821    }
3822}
3823
3824fn history_input_for_request(
3825    request: &HistoryRequest,
3826    fetch_limit: i32,
3827) -> proto::GetChatHistoryInput {
3828    if let Some(after) = request.after_message_id {
3829        return proto::GetChatHistoryInput {
3830            peer_id: Some(input_peer_for_chat(request.chat_id)),
3831            offset_id: None,
3832            limit: Some(fetch_limit),
3833            mode: Some(proto::GetChatHistoryMode::HistoryModeNewer as i32),
3834            anchor_id: None,
3835            before_id: None,
3836            after_id: Some(after.get()),
3837            before_limit: None,
3838            after_limit: None,
3839            include_anchor: None,
3840        };
3841    }
3842
3843    proto::GetChatHistoryInput {
3844        peer_id: Some(input_peer_for_chat(request.chat_id)),
3845        offset_id: request.before_message_id.map(InlineId::get),
3846        limit: Some(fetch_limit),
3847        mode: None,
3848        anchor_id: None,
3849        before_id: None,
3850        after_id: None,
3851        before_limit: None,
3852        after_limit: None,
3853        include_anchor: None,
3854    }
3855}
3856
3857fn normalize_live_history_records(
3858    mut records: Vec<MessageRecord>,
3859    limit: u32,
3860    newer_page: bool,
3861) -> (Vec<MessageRecord>, bool) {
3862    records.sort_by_key(|message| (message.timestamp, message.message_id.get()));
3863    let limit = limit.max(1) as usize;
3864    let has_more = records.len() > limit;
3865    if has_more {
3866        if newer_page {
3867            records.truncate(limit);
3868        } else {
3869            records = records.split_off(records.len() - limit);
3870        }
3871    }
3872    (records, has_more)
3873}
3874
3875fn chat_id_from_peer(peer: &proto::Peer) -> Option<InlineId> {
3876    match &peer.r#type {
3877        Some(proto::peer::Type::Chat(chat)) => Some(InlineId::new(chat.chat_id)),
3878        Some(proto::peer::Type::User(user)) => Some(InlineId::new(user.user_id)),
3879        None => None,
3880    }
3881}
3882
3883fn chat_id_for_peer(peer: PeerRef) -> InlineId {
3884    match peer {
3885        PeerRef::User { user_id } => user_id,
3886        PeerRef::Chat { chat_id } => chat_id,
3887        PeerRef::Thread { thread_id } => thread_id,
3888    }
3889}
3890
3891fn transaction_id_for_send(request: &SendTextRequest, random_id: RandomId) -> TransactionId {
3892    let stable = request
3893        .external_id
3894        .as_ref()
3895        .map(|external| stable_hash(&format!("{}:{}", external.source(), external.id())))
3896        .unwrap_or_else(|| random_id.get() as u64);
3897    TransactionId::try_new(format!("sdk-send-{stable:016x}"))
3898        .expect("generated transaction ID should be valid")
3899}
3900
3901fn random_id_for_request(request: &SendTextRequest) -> RandomId {
3902    let seed = request
3903        .external_id
3904        .as_ref()
3905        .map(|external| format!("{}:{}:{}", external.source(), external.id(), request.text))
3906        .unwrap_or_else(|| format!("{}:{}", now_seconds(), request.text));
3907    RandomId::new((stable_hash(&seed) & 0x7fff_ffff_ffff_ffff) as i64)
3908}
3909
3910fn transaction_id_for_upload(request: &UploadRequest, random_id: RandomId) -> TransactionId {
3911    let stable = request
3912        .external_id
3913        .as_ref()
3914        .map(|external| stable_hash(&format!("{}:{}", external.source(), external.id())))
3915        .unwrap_or_else(|| random_id.get() as u64);
3916    TransactionId::try_new(format!("sdk-upload-{stable:016x}"))
3917        .expect("generated transaction ID should be valid")
3918}
3919
3920fn random_id_for_upload_request(request: &UploadRequest, size_bytes: usize) -> RandomId {
3921    let seed = request
3922        .external_id
3923        .as_ref()
3924        .map(|external| {
3925            format!(
3926                "{}:{}:{}",
3927                external.source(),
3928                external.id(),
3929                request.caption.as_deref().unwrap_or_default()
3930            )
3931        })
3932        .unwrap_or_else(|| {
3933            format!(
3934                "{}:{}:{}:{}",
3935                now_seconds(),
3936                request.file_name.as_deref().unwrap_or_default(),
3937                request.caption.as_deref().unwrap_or_default(),
3938                size_bytes
3939            )
3940        });
3941    RandomId::new((stable_hash(&seed) & 0x7fff_ffff_ffff_ffff) as i64)
3942}
3943
3944fn upload_input_for_request(request: &UploadRequest, bytes: Vec<u8>) -> UploadFileBytesInput {
3945    let file_name = request
3946        .file_name
3947        .as_deref()
3948        .map(str::trim)
3949        .filter(|value| !value.is_empty())
3950        .map(ToOwned::to_owned)
3951        .unwrap_or_else(|| default_upload_file_name(request));
3952    let file_type = upload_file_type_for_request(request);
3953    let mut input = UploadFileBytesInput::new(bytes, file_name, file_type);
3954    if let Some(mime_type) = request
3955        .mime_type
3956        .as_deref()
3957        .map(str::trim)
3958        .filter(|value| !value.is_empty())
3959    {
3960        input = input.with_mime_type(mime_type);
3961    }
3962    if file_type == UploadFileType::Video
3963        && let Some(metadata) = upload_video_metadata_for_request(request)
3964    {
3965        input = input.with_video_metadata(metadata);
3966    }
3967    input
3968}
3969
3970fn upload_file_type_for_request(request: &UploadRequest) -> UploadFileType {
3971    match request.kind {
3972        MediaKind::Photo => UploadFileType::Photo,
3973        MediaKind::Video if upload_video_metadata_for_request(request).is_some() => {
3974            UploadFileType::Video
3975        }
3976        MediaKind::Video | MediaKind::Document | MediaKind::Voice => UploadFileType::Document,
3977    }
3978}
3979
3980fn upload_video_metadata_for_request(request: &UploadRequest) -> Option<UploadVideoMetadata> {
3981    let width = i32::try_from(request.width?)
3982        .ok()
3983        .filter(|value| *value > 0)?;
3984    let height = i32::try_from(request.height?)
3985        .ok()
3986        .filter(|value| *value > 0)?;
3987    let duration = duration_ms_to_seconds_i32(request.duration_ms?)?;
3988    Some(UploadVideoMetadata::new(width, height, duration))
3989}
3990
3991fn duration_ms_to_seconds_i32(duration_ms: u64) -> Option<i32> {
3992    if duration_ms == 0 {
3993        return None;
3994    }
3995    let seconds = duration_ms.saturating_add(999) / 1_000;
3996    i32::try_from(seconds).ok().filter(|value| *value > 0)
3997}
3998
3999fn default_upload_file_name(request: &UploadRequest) -> String {
4000    let extension = match request.kind {
4001        MediaKind::Photo => extension_for_mime(request.mime_type.as_deref(), "jpg"),
4002        MediaKind::Video => extension_for_mime(request.mime_type.as_deref(), "mp4"),
4003        MediaKind::Voice => extension_for_mime(request.mime_type.as_deref(), "ogg"),
4004        MediaKind::Document => "bin",
4005    };
4006    format!("inline-upload.{}", extension.trim_start_matches('.'))
4007}
4008
4009fn extension_for_mime(mime_type: Option<&str>, fallback: &'static str) -> &'static str {
4010    match mime_type
4011        .unwrap_or_default()
4012        .trim()
4013        .to_ascii_lowercase()
4014        .as_str()
4015    {
4016        "image/png" => "png",
4017        "image/gif" => "gif",
4018        "image/webp" => "webp",
4019        "video/webm" => "webm",
4020        "audio/mpeg" => "mp3",
4021        "audio/mp4" | "audio/aac" => "m4a",
4022        _ => fallback,
4023    }
4024}
4025
4026fn input_media_from_upload(upload: &UploadFileResult) -> BackendResult<proto::InputMedia> {
4027    if let Some(photo_id) = upload.photo_id {
4028        return Ok(proto::InputMedia {
4029            media: Some(proto::input_media::Media::Photo(proto::InputMediaPhoto {
4030                photo_id,
4031            })),
4032        });
4033    }
4034    if let Some(video_id) = upload.video_id {
4035        return Ok(proto::InputMedia {
4036            media: Some(proto::input_media::Media::Video(proto::InputMediaVideo {
4037                video_id,
4038            })),
4039        });
4040    }
4041    if let Some(document_id) = upload.document_id {
4042        return Ok(proto::InputMedia {
4043            media: Some(proto::input_media::Media::Document(
4044                proto::InputMediaDocument { document_id },
4045            )),
4046        });
4047    }
4048    Err(BackendError::new(
4049        ClientErrorCategory::ProtocolMismatch,
4050        "uploadFile response did not include a media id",
4051    ))
4052}
4053
4054fn stable_hash(value: &str) -> u64 {
4055    let mut hash = 0xcbf2_9ce4_8422_2325u64;
4056    for byte in value.as_bytes() {
4057        hash ^= u64::from(*byte);
4058        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
4059    }
4060    hash
4061}
4062
4063#[derive(Debug)]
4064struct AppliedSendMessage {
4065    transaction: StoredTransaction,
4066    message: Option<MessageRecord>,
4067    chat_id: InlineId,
4068    message_id: Option<InlineId>,
4069}
4070
4071fn apply_send_message_updates(
4072    request: &SendTextRequest,
4073    identity: TransactionIdentity,
4074    fallback_chat_id: InlineId,
4075    result: proto::SendMessageResult,
4076) -> AppliedSendMessage {
4077    let mut final_message: Option<proto::Message> = None;
4078    let mut final_message_id: Option<InlineId> = None;
4079    for update in result.updates {
4080        match update.update {
4081            Some(proto::update::Update::NewMessage(update)) => {
4082                if let Some(message) = update.message {
4083                    final_message_id = Some(InlineId::new(message.id));
4084                    final_message = Some(message);
4085                }
4086            }
4087            Some(proto::update::Update::UpdateMessageId(update))
4088                if update.random_id == identity.random_id.get() =>
4089            {
4090                final_message_id = Some(InlineId::new(update.message_id));
4091            }
4092            _ => {}
4093        }
4094    }
4095
4096    let state = if final_message_id.is_some() {
4097        TransactionState::Completed
4098    } else {
4099        TransactionState::Acked
4100    };
4101    let mut identity = identity;
4102    if let Some(message_id) = final_message_id {
4103        identity = identity.with_final_message_id(message_id);
4104    }
4105    let message =
4106        final_message.map(|message| message_record_from_proto(message, request, identity.clone()));
4107    let chat_id = message
4108        .as_ref()
4109        .map(|message| message.chat_id)
4110        .unwrap_or(fallback_chat_id);
4111    let mut transaction = StoredTransaction::new(identity, state).with_chat_id(chat_id);
4112    if let Some(message_id) = final_message_id {
4113        transaction = transaction.with_message_id(message_id);
4114    }
4115
4116    AppliedSendMessage {
4117        transaction,
4118        message,
4119        chat_id,
4120        message_id: final_message_id,
4121    }
4122}
4123
4124fn apply_upload_message_updates(
4125    request: &UploadRequest,
4126    identity: TransactionIdentity,
4127    fallback_chat_id: InlineId,
4128    result: proto::SendMessageResult,
4129) -> AppliedSendMessage {
4130    let mut final_message: Option<proto::Message> = None;
4131    let mut final_message_id: Option<InlineId> = None;
4132    for update in result.updates {
4133        match update.update {
4134            Some(proto::update::Update::NewMessage(update)) => {
4135                if let Some(message) = update.message {
4136                    final_message_id = Some(InlineId::new(message.id));
4137                    final_message = Some(message);
4138                }
4139            }
4140            Some(proto::update::Update::UpdateMessageId(update))
4141                if update.random_id == identity.random_id.get() =>
4142            {
4143                final_message_id = Some(InlineId::new(update.message_id));
4144            }
4145            _ => {}
4146        }
4147    }
4148
4149    let state = if final_message_id.is_some() {
4150        TransactionState::Completed
4151    } else {
4152        TransactionState::Acked
4153    };
4154    let mut identity = identity;
4155    if let Some(message_id) = final_message_id {
4156        identity = identity.with_final_message_id(message_id);
4157    }
4158    let message = final_message.map(|message| {
4159        message_record_from_proto_message(
4160            message,
4161            Some(chat_id_for_peer(request.peer)),
4162            Some(identity.clone()),
4163        )
4164    });
4165    let chat_id = message
4166        .as_ref()
4167        .map(|message| message.chat_id)
4168        .unwrap_or(fallback_chat_id);
4169    let mut transaction = StoredTransaction::new(identity, state).with_chat_id(chat_id);
4170    if let Some(message_id) = final_message_id {
4171        transaction = transaction.with_message_id(message_id);
4172    }
4173
4174    AppliedSendMessage {
4175        transaction,
4176        message,
4177        chat_id,
4178        message_id: final_message_id,
4179    }
4180}
4181
4182fn message_record_from_proto(
4183    message: proto::Message,
4184    request: &SendTextRequest,
4185    transaction: TransactionIdentity,
4186) -> MessageRecord {
4187    message_record_from_proto_message(
4188        message,
4189        Some(chat_id_for_peer(request.peer)),
4190        Some(transaction),
4191    )
4192}
4193
4194fn message_record_from_proto_message(
4195    message: proto::Message,
4196    fallback_chat_id: Option<InlineId>,
4197    transaction: Option<TransactionIdentity>,
4198) -> MessageRecord {
4199    let chat_id = if message.chat_id != 0 {
4200        InlineId::new(message.chat_id)
4201    } else if let Some(peer) = &message.peer_id {
4202        chat_id_from_peer(peer).unwrap_or_else(|| fallback_chat_id.unwrap_or(InlineId::new(0)))
4203    } else {
4204        fallback_chat_id.unwrap_or(InlineId::new(0))
4205    };
4206    let content = content_from_proto_message(&message);
4207    MessageRecord {
4208        chat_id,
4209        message_id: InlineId::new(message.id),
4210        sender_id: InlineId::new(message.from_id),
4211        timestamp: message.date,
4212        is_outgoing: message.out,
4213        content,
4214        reply_to_message_id: message.reply_to_msg_id.map(InlineId::new),
4215        transaction,
4216    }
4217}
4218
4219fn reaction_snapshot_from_proto_message(
4220    message: &proto::Message,
4221    fallback_chat_id: Option<InlineId>,
4222) -> Option<Vec<StoredReaction>> {
4223    let snapshot = message.reactions.as_ref()?;
4224    let chat_id = if message.chat_id != 0 {
4225        InlineId::new(message.chat_id)
4226    } else if let Some(peer) = &message.peer_id {
4227        chat_id_from_peer(peer).unwrap_or_else(|| fallback_chat_id.unwrap_or(InlineId::new(0)))
4228    } else {
4229        fallback_chat_id.unwrap_or(InlineId::new(0))
4230    };
4231    Some(
4232        snapshot
4233            .reactions
4234            .iter()
4235            .filter(|reaction| !reaction.emoji.trim().is_empty())
4236            .map(|reaction| StoredReaction {
4237                chat_id: if reaction.chat_id != 0 {
4238                    InlineId::new(reaction.chat_id)
4239                } else {
4240                    chat_id
4241                },
4242                message_id: if reaction.message_id != 0 {
4243                    InlineId::new(reaction.message_id)
4244                } else {
4245                    InlineId::new(message.id)
4246                },
4247                user_id: InlineId::new(reaction.user_id),
4248                reaction: reaction.emoji.clone(),
4249            })
4250            .collect(),
4251    )
4252}
4253
4254fn content_from_proto_message(message: &proto::Message) -> MessageContent {
4255    match &message.media {
4256        None => match &message.message {
4257            Some(text) => MessageContent::Text { text: text.clone() },
4258            None => MessageContent::Unsupported {
4259                reason: "empty message content".to_owned(),
4260            },
4261        },
4262        Some(media) => media_content_from_proto(media, message.message.clone()),
4263    }
4264}
4265
4266fn media_content_from_proto(
4267    media: &proto::MessageMedia,
4268    caption: Option<String>,
4269) -> MessageContent {
4270    match &media.media {
4271        Some(proto::message_media::Media::Photo(photo)) => {
4272            if let Some(photo) = &photo.photo {
4273                let (url, size_bytes, width, height) = best_photo_size(photo);
4274                MessageContent::Media {
4275                    kind: MediaKind::Photo,
4276                    file_id: photo.id.to_string(),
4277                    url,
4278                    mime_type: photo_mime_type(photo),
4279                    file_name: None,
4280                    caption,
4281                    size_bytes,
4282                    width,
4283                    height,
4284                    duration_ms: None,
4285                }
4286            } else {
4287                empty_media_content(MediaKind::Photo, caption)
4288            }
4289        }
4290        Some(proto::message_media::Media::Video(video)) => {
4291            if let Some(video) = &video.video {
4292                MessageContent::Media {
4293                    kind: MediaKind::Video,
4294                    file_id: video.id.to_string(),
4295                    url: video.cdn_url.clone(),
4296                    mime_type: Some("video/mp4".to_owned()),
4297                    file_name: None,
4298                    caption,
4299                    size_bytes: positive_u64(video.size),
4300                    width: positive_u32(video.w),
4301                    height: positive_u32(video.h),
4302                    duration_ms: seconds_to_milliseconds(video.duration),
4303                }
4304            } else {
4305                empty_media_content(MediaKind::Video, caption)
4306            }
4307        }
4308        Some(proto::message_media::Media::Document(document)) => {
4309            if let Some(document) = &document.document {
4310                MessageContent::Media {
4311                    kind: MediaKind::Document,
4312                    file_id: document.id.to_string(),
4313                    url: document.cdn_url.clone(),
4314                    mime_type: non_empty_string(&document.mime_type),
4315                    file_name: non_empty_string(&document.file_name),
4316                    caption,
4317                    size_bytes: positive_u64(document.size),
4318                    width: None,
4319                    height: None,
4320                    duration_ms: None,
4321                }
4322            } else {
4323                empty_media_content(MediaKind::Document, caption)
4324            }
4325        }
4326        Some(proto::message_media::Media::Voice(voice)) => {
4327            if let Some(voice) = &voice.voice {
4328                MessageContent::Media {
4329                    kind: MediaKind::Voice,
4330                    file_id: voice.id.to_string(),
4331                    url: voice.cdn_url.clone(),
4332                    mime_type: non_empty_string(&voice.mime_type),
4333                    file_name: None,
4334                    caption,
4335                    size_bytes: positive_u64(voice.size),
4336                    width: None,
4337                    height: None,
4338                    duration_ms: seconds_to_milliseconds(voice.duration),
4339                }
4340            } else {
4341                empty_media_content(MediaKind::Voice, caption)
4342            }
4343        }
4344        Some(proto::message_media::Media::Nudge(_)) => MessageContent::Unsupported {
4345            reason: "nudge messages are not supported by inline-client yet".to_owned(),
4346        },
4347        None => MessageContent::Unsupported {
4348            reason: "empty media message".to_owned(),
4349        },
4350    }
4351}
4352
4353fn empty_media_content(kind: MediaKind, caption: Option<String>) -> MessageContent {
4354    MessageContent::Media {
4355        kind,
4356        file_id: String::new(),
4357        url: None,
4358        mime_type: None,
4359        file_name: None,
4360        caption,
4361        size_bytes: None,
4362        width: None,
4363        height: None,
4364        duration_ms: None,
4365    }
4366}
4367
4368fn best_photo_size(
4369    photo: &proto::Photo,
4370) -> (Option<String>, Option<u64>, Option<u32>, Option<u32>) {
4371    let mut best: Option<(&proto::PhotoSize, i64)> = None;
4372    for size in &photo.sizes {
4373        if size.cdn_url.is_none() {
4374            continue;
4375        }
4376        let area = i64::from(size.w) * i64::from(size.h);
4377        if best.is_none_or(|(_, best_area)| area > best_area) {
4378            best = Some((size, area));
4379        }
4380    }
4381    if let Some((size, _)) = best {
4382        return (
4383            size.cdn_url.clone(),
4384            positive_u64(size.size),
4385            positive_u32(size.w),
4386            positive_u32(size.h),
4387        );
4388    }
4389    (None, None, None, None)
4390}
4391
4392fn photo_mime_type(photo: &proto::Photo) -> Option<String> {
4393    match photo.format {
4394        1 => Some("image/jpeg".to_owned()),
4395        2 => Some("image/png".to_owned()),
4396        _ => None,
4397    }
4398}
4399
4400fn positive_u64(value: i32) -> Option<u64> {
4401    u64::try_from(value).ok().filter(|value| *value > 0)
4402}
4403
4404fn positive_u32(value: i32) -> Option<u32> {
4405    u32::try_from(value).ok().filter(|value| *value > 0)
4406}
4407
4408fn seconds_to_milliseconds(seconds: i32) -> Option<u64> {
4409    positive_u64(seconds).map(|seconds| seconds.saturating_mul(1_000))
4410}
4411
4412fn non_empty_string(value: &str) -> Option<String> {
4413    (!value.is_empty()).then(|| value.to_owned())
4414}
4415
4416fn outcome_from_stored_transaction(
4417    transaction: StoredTransaction,
4418    fallback_chat_id: InlineId,
4419) -> SendTextOutcome {
4420    let message_id = transaction
4421        .message_id
4422        .or(transaction.identity.final_message_id);
4423    SendTextOutcome::with_state(
4424        MessageMutation {
4425            transaction: transaction.identity.clone(),
4426            message_id,
4427            state: None,
4428            failure: None,
4429        },
4430        transaction.chat_id.unwrap_or(fallback_chat_id),
4431        message_id,
4432        None,
4433        transaction.state,
4434        transaction.failure,
4435    )
4436}
4437
4438fn stored_transaction_needs_retry(transaction: &StoredTransaction) -> bool {
4439    match transaction.state {
4440        TransactionState::Queued | TransactionState::Sent | TransactionState::Acked => true,
4441        TransactionState::Failed => transaction
4442            .failure
4443            .as_ref()
4444            .is_some_and(|failure| retryable_transaction_category(failure.category)),
4445        TransactionState::Completed | TransactionState::Cancelled => false,
4446    }
4447}
4448
4449fn retryable_transaction_category(category: ClientErrorCategory) -> bool {
4450    matches!(
4451        category,
4452        ClientErrorCategory::AuthRequired
4453            | ClientErrorCategory::AuthExpired
4454            | ClientErrorCategory::ReloginRequired
4455            | ClientErrorCategory::Network
4456            | ClientErrorCategory::Timeout
4457            | ClientErrorCategory::RateLimited
4458            | ClientErrorCategory::Internal
4459    )
4460}
4461
4462fn now_seconds() -> i64 {
4463    SystemTime::now()
4464        .duration_since(UNIX_EPOCH)
4465        .map(|duration| duration.as_secs() as i64)
4466        .unwrap_or_default()
4467}
4468
4469fn redacted_url_for_debug(url: &str) -> String {
4470    let without_fragment = url.split('#').next().unwrap_or(url);
4471    let without_query = without_fragment
4472        .split('?')
4473        .next()
4474        .unwrap_or(without_fragment);
4475    match without_query.split_once("://") {
4476        Some((scheme, rest)) => {
4477            let host_and_path = rest.rsplit_once('@').map_or(rest, |(_, tail)| tail);
4478            format!("{scheme}://{host_and_path}")
4479        }
4480        None => without_query.to_owned(),
4481    }
4482}
4483
4484#[cfg(test)]
4485mod tests {
4486    use futures_util::{SinkExt, StreamExt};
4487    use prost::Message;
4488    use tokio::{
4489        io::{AsyncReadExt, AsyncWriteExt},
4490        net::TcpListener,
4491    };
4492    use tokio_tungstenite::accept_async;
4493    use tokio_tungstenite::tungstenite::Message as WsMessage;
4494
4495    use crate::{
4496        AuthCredential, AuthToken, ClientBackend, ClientStatus, DialogRecord, DialogsRequest,
4497        ExternalId, FakeRealtimeConnector, HistoryRequest, InlineClient, InlineId, RandomId,
4498        SendTextRequest, StoredTransaction,
4499    };
4500
4501    use super::*;
4502
4503    fn connect_request() -> ConnectRequest {
4504        ConnectRequest::new(AuthCredential::AccessToken {
4505            token: AuthToken::try_new("secret-token").unwrap(),
4506        })
4507        .with_account_namespace("team")
4508    }
4509
4510    fn test_message_record(message_id: i64) -> MessageRecord {
4511        MessageRecord {
4512            chat_id: InlineId::new(7),
4513            message_id: InlineId::new(message_id),
4514            sender_id: InlineId::new(2),
4515            timestamp: message_id,
4516            is_outgoing: false,
4517            content: MessageContent::Text {
4518                text: format!("message {message_id}"),
4519            },
4520            reply_to_message_id: None,
4521            transaction: None,
4522        }
4523    }
4524
4525    #[test]
4526    fn proto_message_reaction_snapshot_preserves_complete_empty_and_nonempty_sets() {
4527        let mut message = proto::Message {
4528            id: 11,
4529            chat_id: 7,
4530            reactions: Some(proto::MessageReactions { reactions: vec![] }),
4531            ..Default::default()
4532        };
4533        assert_eq!(
4534            reaction_snapshot_from_proto_message(&message, None),
4535            Some(Vec::new())
4536        );
4537
4538        message.reactions = Some(proto::MessageReactions {
4539            reactions: vec![proto::Reaction {
4540                emoji: "👍".to_owned(),
4541                user_id: 2,
4542                message_id: 11,
4543                chat_id: 7,
4544                ..Default::default()
4545            }],
4546        });
4547        assert_eq!(
4548            reaction_snapshot_from_proto_message(&message, None),
4549            Some(vec![StoredReaction {
4550                chat_id: InlineId::new(7),
4551                message_id: InlineId::new(11),
4552                user_id: InlineId::new(2),
4553                reaction: "👍".to_owned(),
4554            }])
4555        );
4556    }
4557
4558    #[tokio::test]
4559    async fn live_dialog_reconciliation_tombstones_missing_chats() {
4560        let store = InMemoryStore::new();
4561        store
4562            .record_dialog(DialogRecord::new(InlineId::new(7)))
4563            .await
4564            .unwrap();
4565        store
4566            .record_dialog(DialogRecord::new(InlineId::new(8)))
4567            .await
4568            .unwrap();
4569        let removed_message = MessageRecord {
4570            chat_id: InlineId::new(8),
4571            ..test_message_record(10)
4572        };
4573        store.record_message(removed_message.clone()).await.unwrap();
4574        let backend = SdkBackend::builder().store(store.clone()).build().unwrap();
4575
4576        backend
4577            .reconcile_live_dialogs(vec![DialogRecord::new(InlineId::new(7))])
4578            .await
4579            .unwrap();
4580
4581        assert_eq!(
4582            store.deleted_chat_ids().await.unwrap(),
4583            vec![InlineId::new(8)]
4584        );
4585        assert_eq!(
4586            store.deleted_message_ids(InlineId::new(8)).await.unwrap(),
4587            vec![removed_message.message_id]
4588        );
4589    }
4590
4591    #[test]
4592    fn sdk_backend_debug_redacts_realtime_url_credentials() {
4593        let backend = SdkBackend::builder()
4594            .realtime_url("wss://user:secret@api.inline.chat/realtime?token=secret#frag")
4595            .build()
4596            .unwrap();
4597
4598        let rendered = format!("{backend:?}");
4599        assert!(rendered.contains("wss://api.inline.chat/realtime"));
4600        assert!(!rendered.contains("secret"));
4601        assert!(!rendered.contains("token="));
4602    }
4603
4604    #[test]
4605    fn sdk_backend_defaults_use_production_endpoints() {
4606        let backend = SdkBackend::builder().build().unwrap();
4607
4608        assert_eq!(
4609            backend.api_client().base_url(),
4610            "https://api.inline.chat/v1"
4611        );
4612        assert_eq!(backend.realtime_url(), "wss://api.inline.chat/realtime");
4613    }
4614
4615    #[tokio::test]
4616    async fn sdk_backend_connect_persists_session() {
4617        let store = InMemoryStore::new();
4618        let backend = SdkBackend::builder().store(store.clone()).build().unwrap();
4619
4620        let status = backend.connect(connect_request()).await.unwrap();
4621
4622        assert_eq!(status.status, crate::ClientStatus::Connected);
4623        let session = store.load_session().await.unwrap().unwrap();
4624        assert_eq!(session.account_namespace.as_deref(), Some("team"));
4625        let rendered = format!("{session:?}");
4626        assert!(!rendered.contains("secret-token"));
4627    }
4628
4629    #[tokio::test]
4630    async fn sdk_backend_connect_can_perform_realtime_handshake() {
4631        let store = InMemoryStore::new();
4632        let realtime = FakeRealtimeConnector::new();
4633        let backend = SdkBackend::builder()
4634            .store(store.clone())
4635            .realtime_connector(realtime.clone())
4636            .realtime_url("wss://api.inline.chat/realtime")
4637            .build()
4638            .unwrap();
4639
4640        assert!(backend.realtime_handshake_enabled());
4641        backend.connect(connect_request()).await.unwrap();
4642
4643        let attempts = realtime.attempts();
4644        assert_eq!(attempts.len(), 1);
4645        assert_eq!(attempts[0].realtime_url, "wss://api.inline.chat/realtime");
4646        assert!(store.load_session().await.unwrap().is_some());
4647    }
4648
4649    #[tokio::test]
4650    async fn sdk_backend_does_not_persist_session_when_realtime_handshake_fails() {
4651        let store = InMemoryStore::new();
4652        let backend = SdkBackend::builder()
4653            .store(store.clone())
4654            .realtime_connector(FakeRealtimeConnector::failing(BackendError::new(
4655                ClientErrorCategory::Network,
4656                "offline",
4657            )))
4658            .build()
4659            .unwrap();
4660
4661        let err = backend.connect(connect_request()).await.unwrap_err();
4662
4663        assert_eq!(err.category, ClientErrorCategory::Network);
4664        assert!(store.load_session().await.unwrap().is_none());
4665    }
4666
4667    #[tokio::test]
4668    async fn sdk_backend_resume_without_session_reports_auth_required() {
4669        let backend = SdkBackend::builder().build().unwrap();
4670
4671        let status = backend.resume_session().await.unwrap();
4672
4673        assert_eq!(status.status, crate::ClientStatus::AuthRequired);
4674    }
4675
4676    #[tokio::test]
4677    async fn sdk_backend_resume_uses_stored_session() {
4678        let store = InMemoryStore::new();
4679        store.save_session(connect_session()).await.unwrap();
4680        let realtime = FakeRealtimeConnector::new();
4681        let backend = SdkBackend::builder()
4682            .store(store)
4683            .realtime_connector(realtime.clone())
4684            .realtime_url("wss://api.inline.chat/realtime")
4685            .build()
4686            .unwrap();
4687
4688        let status = backend.resume_session().await.unwrap();
4689
4690        assert_eq!(status.status, crate::ClientStatus::Connected);
4691        let attempts = realtime.attempts();
4692        assert_eq!(attempts.len(), 1);
4693        assert_eq!(attempts[0].realtime_url, "wss://api.inline.chat/realtime");
4694    }
4695
4696    #[tokio::test]
4697    async fn live_dialog_failure_does_not_masquerade_as_cached_success() {
4698        let store = InMemoryStore::new();
4699        store.save_session(connect_session()).await.unwrap();
4700        store.upsert_dialog(DialogRecord {
4701            chat_id: InlineId::new(9),
4702            peer_user_id: Some(InlineId::new(10)),
4703            title: Some("general".to_owned()),
4704            last_message_id: None,
4705            synced_through_message_id: None,
4706            unread_count: Some(0),
4707            ..DialogRecord::new(InlineId::new(9))
4708        });
4709        let backend = SdkBackend::builder()
4710            .store(store)
4711            .realtime_url("not-a-websocket-url")
4712            .build()
4713            .unwrap();
4714
4715        backend.connect(connect_request()).await.unwrap();
4716        let error = backend
4717            .dialogs(DialogsRequest::default())
4718            .await
4719            .unwrap_err();
4720        assert_eq!(error.category, ClientErrorCategory::InvalidInput);
4721        let dialogs = backend
4722            .cached_dialogs(DialogsRequest::default())
4723            .await
4724            .unwrap();
4725
4726        assert_eq!(dialogs.dialogs.len(), 1);
4727        assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(9));
4728    }
4729
4730    #[tokio::test]
4731    async fn cached_dialogs_reads_durable_state_without_session_or_network() {
4732        let store = InMemoryStore::new();
4733        store
4734            .record_dialog(DialogRecord {
4735                title: Some("cached".to_owned()),
4736                ..DialogRecord::new(InlineId::new(9))
4737            })
4738            .await
4739            .unwrap();
4740        let backend = SdkBackend::builder()
4741            .store(store)
4742            .realtime_url("not-a-websocket-url")
4743            .build()
4744            .unwrap();
4745
4746        let dialogs = backend
4747            .cached_dialogs(DialogsRequest::default())
4748            .await
4749            .unwrap();
4750
4751        assert_eq!(dialogs.dialogs.len(), 1);
4752        assert_eq!(dialogs.dialogs[0].title.as_deref(), Some("cached"));
4753    }
4754
4755    #[tokio::test]
4756    async fn live_participant_failure_does_not_fall_back_to_cached_snapshot() {
4757        let store = InMemoryStore::new();
4758        store.save_session(connect_session()).await.unwrap();
4759        let backend = SdkBackend::builder()
4760            .store(store.clone())
4761            .realtime_url("not-a-websocket-url")
4762            .build()
4763            .unwrap();
4764        let request = ChatParticipantsRequest {
4765            chat_id: InlineId::new(7),
4766        };
4767
4768        assert!(backend.chat_participants(request.clone()).await.is_err());
4769
4770        store
4771            .record_chat_participants(
4772                InlineId::new(7),
4773                vec![ChatParticipantRecord {
4774                    user_id: InlineId::new(42),
4775                    date: Some(10),
4776                }],
4777            )
4778            .await
4779            .unwrap();
4780        assert!(backend.chat_participants(request).await.is_err());
4781        let cached = backend.chat_state(InlineId::new(7)).await.unwrap();
4782        assert_eq!(cached.participants.len(), 1);
4783        assert_eq!(cached.participants[0].user_id, InlineId::new(42));
4784    }
4785
4786    #[tokio::test]
4787    async fn live_history_failure_does_not_masquerade_as_cached_success() {
4788        let store = InMemoryStore::new();
4789        store.save_session(connect_session()).await.unwrap();
4790        store
4791            .record_message(MessageRecord {
4792                chat_id: InlineId::new(7),
4793                message_id: InlineId::new(11),
4794                sender_id: InlineId::new(42),
4795                timestamp: 100,
4796                is_outgoing: false,
4797                content: MessageContent::Text {
4798                    text: "cached".to_owned(),
4799                },
4800                reply_to_message_id: None,
4801                transaction: None,
4802            })
4803            .await
4804            .unwrap();
4805        let backend = SdkBackend::builder()
4806            .store(store)
4807            .realtime_url("not-a-websocket-url")
4808            .build()
4809            .unwrap();
4810        let request = HistoryRequest {
4811            chat_id: InlineId::new(7),
4812            limit: Some(10),
4813            before_message_id: None,
4814            after_message_id: None,
4815        };
4816
4817        let error = backend.history(request.clone()).await.unwrap_err();
4818        assert_eq!(error.category, ClientErrorCategory::InvalidInput);
4819        let cached = backend.cached_history(request).await.unwrap();
4820        assert_eq!(cached.messages.len(), 1);
4821        assert_eq!(cached.messages[0].message_id, InlineId::new(11));
4822    }
4823
4824    #[tokio::test]
4825    async fn sdk_backend_clears_account_data_when_namespace_changes_or_logs_out() {
4826        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4827        let api_addr = listener.local_addr().unwrap();
4828        let logout_server = tokio::spawn(async move {
4829            let (mut stream, _) = listener.accept().await.unwrap();
4830            let mut request = vec![0; 4096];
4831            let read = stream.read(&mut request).await.unwrap();
4832            let request = String::from_utf8_lossy(&request[..read]);
4833            assert!(request.starts_with("POST /v1/logout HTTP/1.1"));
4834            assert!(request.contains("authorization: Bearer secret-token"));
4835            let body = r#"{"ok":true,"result":null}"#;
4836            stream
4837                .write_all(
4838                    format!(
4839                        "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
4840                        body.len()
4841                    )
4842                    .as_bytes(),
4843                )
4844                .await
4845                .unwrap();
4846        });
4847        let store = InMemoryStore::new();
4848        store
4849            .save_session(StoredSession {
4850                auth: AuthCredential::AccessToken {
4851                    token: AuthToken::try_new("old-token").unwrap(),
4852                },
4853                account_namespace: Some("old-account".to_owned()),
4854            })
4855            .await
4856            .unwrap();
4857        store
4858            .record_dialog(DialogRecord::new(InlineId::new(9)))
4859            .await
4860            .unwrap();
4861        store
4862            .save_sync_bucket_state(
4863                crate::SyncBucketKey::User,
4864                crate::SyncBucketState { seq: 9, date: 9 },
4865            )
4866            .await
4867            .unwrap();
4868        let backend = SdkBackend::builder()
4869            .api_base_url(format!("http://{api_addr}/v1"))
4870            .store(store.clone())
4871            .build()
4872            .unwrap();
4873
4874        backend.connect(connect_request()).await.unwrap();
4875
4876        assert!(store.dialog(InlineId::new(9)).await.unwrap().is_none());
4877        assert_eq!(
4878            store
4879                .sync_bucket_state(crate::SyncBucketKey::User)
4880                .await
4881                .unwrap(),
4882            crate::SyncBucketState::default()
4883        );
4884        assert_eq!(
4885            store
4886                .load_session()
4887                .await
4888                .unwrap()
4889                .unwrap()
4890                .account_namespace
4891                .as_deref(),
4892            Some("team")
4893        );
4894
4895        store
4896            .record_dialog(DialogRecord::new(InlineId::new(10)))
4897            .await
4898            .unwrap();
4899        backend.logout().await.unwrap();
4900        logout_server.await.unwrap();
4901        assert!(store.load_session().await.unwrap().is_none());
4902        assert!(store.dialog(InlineId::new(10)).await.unwrap().is_none());
4903    }
4904
4905    #[tokio::test]
4906    async fn sdk_backend_reuses_realtime_connection_for_multiple_rpc_calls() {
4907        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4908        let addr = listener.local_addr().unwrap();
4909        let server = tokio::spawn(async move {
4910            let (stream, _) = listener.accept().await.unwrap();
4911            let mut ws = accept_async(stream).await.unwrap();
4912
4913            let init = read_test_client_message(&mut ws).await;
4914            assert!(matches!(
4915                init.body,
4916                Some(proto::client_message::Body::ConnectionInit(_))
4917            ));
4918            send_test_server_message(
4919                &mut ws,
4920                proto::ServerProtocolMessage {
4921                    id: 1,
4922                    body: Some(proto::server_protocol_message::Body::ConnectionOpen(
4923                        proto::ConnectionOpen {},
4924                    )),
4925                },
4926            )
4927            .await;
4928
4929            let get_chats = read_test_client_message(&mut ws).await;
4930            match &get_chats.body {
4931                Some(proto::client_message::Body::RpcCall(call)) => {
4932                    assert_eq!(call.method, proto::Method::GetChats as i32);
4933                }
4934                other => panic!("expected getChats RPC, got {other:?}"),
4935            }
4936            send_test_server_message(
4937                &mut ws,
4938                proto::ServerProtocolMessage {
4939                    id: 2,
4940                    body: Some(proto::server_protocol_message::Body::RpcResult(
4941                        proto::RpcResult {
4942                            req_msg_id: get_chats.id,
4943                            result: Some(proto::rpc_result::Result::GetChats(
4944                                proto::GetChatsResult::default(),
4945                            )),
4946                        },
4947                    )),
4948                },
4949            )
4950            .await;
4951
4952            let history = read_test_client_message(&mut ws).await;
4953            match &history.body {
4954                Some(proto::client_message::Body::RpcCall(call)) => {
4955                    assert_eq!(call.method, proto::Method::GetChatHistory as i32);
4956                }
4957                other => panic!("expected getChatHistory RPC, got {other:?}"),
4958            }
4959            send_test_server_message(
4960                &mut ws,
4961                proto::ServerProtocolMessage {
4962                    id: 3,
4963                    body: Some(proto::server_protocol_message::Body::RpcResult(
4964                        proto::RpcResult {
4965                            req_msg_id: history.id,
4966                            result: Some(proto::rpc_result::Result::GetChatHistory(
4967                                proto::GetChatHistoryResult::default(),
4968                            )),
4969                        },
4970                    )),
4971                },
4972            )
4973            .await;
4974        });
4975
4976        let store = InMemoryStore::new();
4977        store.save_session(connect_session()).await.unwrap();
4978        let backend = SdkBackend::builder()
4979            .store(store)
4980            .realtime_url(format!("ws://{addr}/realtime"))
4981            .without_realtime_handshake()
4982            .build()
4983            .unwrap();
4984
4985        let dialogs = backend.dialogs(DialogsRequest::default()).await.unwrap();
4986        assert!(dialogs.dialogs.is_empty());
4987        let history = tokio::time::timeout(
4988            std::time::Duration::from_secs(3),
4989            backend.history(HistoryRequest {
4990                chat_id: InlineId::new(7),
4991                limit: Some(10),
4992                before_message_id: None,
4993                after_message_id: None,
4994            }),
4995        )
4996        .await
4997        .expect("history should use the existing websocket")
4998        .unwrap();
4999        assert!(history.messages.is_empty());
5000        tokio::time::timeout(std::time::Duration::from_secs(3), server)
5001            .await
5002            .expect("server should observe both RPCs on one websocket")
5003            .unwrap();
5004    }
5005
5006    #[tokio::test]
5007    async fn inline_client_runner_allows_concurrent_rpcs_on_one_realtime_session() {
5008        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5009        let addr = listener.local_addr().unwrap();
5010        let server = tokio::spawn(async move {
5011            let (stream, _) = listener.accept().await.unwrap();
5012            let mut ws = accept_async(stream).await.unwrap();
5013            let _ = read_test_client_message(&mut ws).await;
5014            send_test_server_message(
5015                &mut ws,
5016                proto::ServerProtocolMessage {
5017                    id: 1,
5018                    body: Some(proto::server_protocol_message::Body::ConnectionOpen(
5019                        proto::ConnectionOpen {},
5020                    )),
5021                },
5022            )
5023            .await;
5024
5025            let first = read_test_client_message(&mut ws).await;
5026            let second = tokio::time::timeout(
5027                std::time::Duration::from_secs(1),
5028                read_test_client_message(&mut ws),
5029            )
5030            .await
5031            .expect("runner should issue the second RPC before the first completes");
5032            for (id, request) in [(2, first), (3, second)] {
5033                send_test_server_message(
5034                    &mut ws,
5035                    rpc_result_message(
5036                        id,
5037                        request.id,
5038                        proto::rpc_result::Result::GetChats(proto::GetChatsResult::default()),
5039                    ),
5040                )
5041                .await;
5042            }
5043        });
5044
5045        let store = InMemoryStore::new();
5046        store.save_session(connect_session()).await.unwrap();
5047        let backend = SdkBackend::builder()
5048            .store(store)
5049            .realtime_url(format!("ws://{addr}/realtime"))
5050            .without_realtime_handshake()
5051            .build()
5052            .unwrap();
5053        let client = InlineClient::builder()
5054            .backend(backend)
5055            .initial_status(ClientStatus::Connected)
5056            .build()
5057            .spawn();
5058
5059        let (first, second) = tokio::join!(
5060            client.dialogs(DialogsRequest::default()),
5061            client.dialogs(DialogsRequest::default())
5062        );
5063        assert!(first.unwrap().dialogs.is_empty());
5064        assert!(second.unwrap().dialogs.is_empty());
5065        server.await.unwrap();
5066    }
5067
5068    #[tokio::test]
5069    async fn inline_client_runner_bounds_in_flight_rpcs() {
5070        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5071        let addr = listener.local_addr().unwrap();
5072        let server = tokio::spawn(async move {
5073            let (stream, _) = listener.accept().await.unwrap();
5074            let mut ws = accept_async(stream).await.unwrap();
5075            let _ = read_test_client_message(&mut ws).await;
5076            send_test_server_message(
5077                &mut ws,
5078                proto::ServerProtocolMessage {
5079                    id: 1,
5080                    body: Some(proto::server_protocol_message::Body::ConnectionOpen(
5081                        proto::ConnectionOpen {},
5082                    )),
5083                },
5084            )
5085            .await;
5086
5087            let first = read_test_client_message(&mut ws).await;
5088            assert!(
5089                tokio::time::timeout(
5090                    std::time::Duration::from_millis(50),
5091                    read_test_client_message(&mut ws),
5092                )
5093                .await
5094                .is_err(),
5095                "runner issued a second RPC above its configured in-flight limit"
5096            );
5097            send_test_server_message(
5098                &mut ws,
5099                rpc_result_message(
5100                    2,
5101                    first.id,
5102                    proto::rpc_result::Result::GetChats(proto::GetChatsResult::default()),
5103                ),
5104            )
5105            .await;
5106
5107            let second = read_test_client_message(&mut ws).await;
5108            send_test_server_message(
5109                &mut ws,
5110                rpc_result_message(
5111                    3,
5112                    second.id,
5113                    proto::rpc_result::Result::GetChats(proto::GetChatsResult::default()),
5114                ),
5115            )
5116            .await;
5117        });
5118
5119        let store = InMemoryStore::new();
5120        store.save_session(connect_session()).await.unwrap();
5121        let backend = SdkBackend::builder()
5122            .store(store)
5123            .realtime_url(format!("ws://{addr}/realtime"))
5124            .without_realtime_handshake()
5125            .build()
5126            .unwrap();
5127        let client = InlineClient::builder()
5128            .backend(backend)
5129            .initial_status(ClientStatus::Connected)
5130            .max_concurrent_requests(1)
5131            .build()
5132            .spawn();
5133
5134        let (first, second) = tokio::join!(
5135            client.dialogs(DialogsRequest::default()),
5136            client.dialogs(DialogsRequest::default())
5137        );
5138        assert!(first.unwrap().dialogs.is_empty());
5139        assert!(second.unwrap().dialogs.is_empty());
5140        server.await.unwrap();
5141    }
5142
5143    #[tokio::test]
5144    async fn chat_management_rpcs_use_one_session_and_commit_durable_state() {
5145        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5146        let addr = listener.local_addr().unwrap();
5147        let server = tokio::spawn(async move {
5148            let (stream, _) = listener.accept().await.unwrap();
5149            let mut ws = accept_async(stream).await.unwrap();
5150
5151            let init = read_test_client_message(&mut ws).await;
5152            assert!(matches!(
5153                init.body,
5154                Some(proto::client_message::Body::ConnectionInit(_))
5155            ));
5156            send_test_server_message(
5157                &mut ws,
5158                proto::ServerProtocolMessage {
5159                    id: 1,
5160                    body: Some(proto::server_protocol_message::Body::ConnectionOpen(
5161                        proto::ConnectionOpen {},
5162                    )),
5163                },
5164            )
5165            .await;
5166
5167            let add = read_test_client_message(&mut ws).await;
5168            match &add.body {
5169                Some(proto::client_message::Body::RpcCall(call)) => match &call.input {
5170                    Some(proto::rpc_call::Input::AddChatParticipant(input)) => {
5171                        assert_eq!((input.chat_id, input.user_id), (7, 42));
5172                    }
5173                    other => panic!("expected add participant input, got {other:?}"),
5174                },
5175                other => panic!("expected add participant RPC, got {other:?}"),
5176            }
5177            send_test_server_message(
5178                &mut ws,
5179                rpc_result_message(
5180                    2,
5181                    add.id,
5182                    proto::rpc_result::Result::AddChatParticipant(
5183                        proto::AddChatParticipantResult {
5184                            participant: Some(proto::ChatParticipant {
5185                                user_id: 42,
5186                                date: 123,
5187                            }),
5188                        },
5189                    ),
5190                ),
5191            )
5192            .await;
5193
5194            let remove = read_test_client_message(&mut ws).await;
5195            match &remove.body {
5196                Some(proto::client_message::Body::RpcCall(call)) => match &call.input {
5197                    Some(proto::rpc_call::Input::RemoveChatParticipant(input)) => {
5198                        assert_eq!((input.chat_id, input.user_id), (7, 42));
5199                    }
5200                    other => panic!("expected remove participant input, got {other:?}"),
5201                },
5202                other => panic!("expected remove participant RPC, got {other:?}"),
5203            }
5204            send_test_server_message(
5205                &mut ws,
5206                rpc_result_message(
5207                    3,
5208                    remove.id,
5209                    proto::rpc_result::Result::RemoveChatParticipant(
5210                        proto::RemoveChatParticipantResult {},
5211                    ),
5212                ),
5213            )
5214            .await;
5215
5216            let update = read_test_client_message(&mut ws).await;
5217            match &update.body {
5218                Some(proto::client_message::Body::RpcCall(call)) => match &call.input {
5219                    Some(proto::rpc_call::Input::UpdateChatInfo(input)) => {
5220                        assert_eq!(input.chat_id, 7);
5221                        assert_eq!(input.title.as_deref(), Some("Renamed"));
5222                        assert_eq!(input.emoji.as_deref(), Some("✨"));
5223                    }
5224                    other => panic!("expected update chat info input, got {other:?}"),
5225                },
5226                other => panic!("expected update chat info RPC, got {other:?}"),
5227            }
5228            send_test_server_message(
5229                &mut ws,
5230                rpc_result_message(
5231                    4,
5232                    update.id,
5233                    proto::rpc_result::Result::UpdateChatInfo(proto::UpdateChatInfoResult {
5234                        chat: Some(proto::Chat {
5235                            id: 7,
5236                            title: "Renamed".to_owned(),
5237                            emoji: Some("✨".to_owned()),
5238                            ..Default::default()
5239                        }),
5240                    }),
5241                ),
5242            )
5243            .await;
5244
5245            let mark = read_test_client_message(&mut ws).await;
5246            match &mark.body {
5247                Some(proto::client_message::Body::RpcCall(call)) => match &call.input {
5248                    Some(proto::rpc_call::Input::MarkAsUnread(input)) => {
5249                        assert_eq!(
5250                            test_chat_id_from_input_peer(input.peer_id.as_ref()),
5251                            Some(7)
5252                        );
5253                    }
5254                    other => panic!("expected mark unread input, got {other:?}"),
5255                },
5256                other => panic!("expected mark unread RPC, got {other:?}"),
5257            }
5258            send_test_server_message(
5259                &mut ws,
5260                rpc_result_message(
5261                    5,
5262                    mark.id,
5263                    proto::rpc_result::Result::MarkAsUnread(proto::MarkAsUnreadResult::default()),
5264                ),
5265            )
5266            .await;
5267
5268            let clear_mark = read_test_client_message(&mut ws).await;
5269            match &clear_mark.body {
5270                Some(proto::client_message::Body::RpcCall(call)) => match &call.input {
5271                    Some(proto::rpc_call::Input::ReadMessages(input)) => {
5272                        assert_eq!(
5273                            test_chat_id_from_input_peer(input.peer_id.as_ref()),
5274                            Some(7)
5275                        );
5276                        assert_eq!(input.max_id, None);
5277                    }
5278                    other => panic!("expected read messages input, got {other:?}"),
5279                },
5280                other => panic!("expected read messages RPC, got {other:?}"),
5281            }
5282            send_test_server_message(
5283                &mut ws,
5284                rpc_result_message(
5285                    7,
5286                    clear_mark.id,
5287                    proto::rpc_result::Result::ReadMessages(proto::ReadMessagesResult::default()),
5288                ),
5289            )
5290            .await;
5291
5292            let notifications = read_test_client_message(&mut ws).await;
5293            match &notifications.body {
5294                Some(proto::client_message::Body::RpcCall(call)) => match &call.input {
5295                    Some(proto::rpc_call::Input::UpdateDialogNotificationSettings(input)) => {
5296                        assert_eq!(
5297                            test_chat_id_from_input_peer(input.peer_id.as_ref()),
5298                            Some(7)
5299                        );
5300                        assert_eq!(
5301                            input
5302                                .notification_settings
5303                                .as_ref()
5304                                .and_then(|settings| settings.mode),
5305                            Some(proto::dialog_notification_settings::Mode::None as i32)
5306                        );
5307                    }
5308                    other => panic!("expected notification settings input, got {other:?}"),
5309                },
5310                other => panic!("expected notification settings RPC, got {other:?}"),
5311            }
5312            send_test_server_message(
5313                &mut ws,
5314                rpc_result_message(
5315                    7,
5316                    notifications.id,
5317                    proto::rpc_result::Result::UpdateDialogNotificationSettings(
5318                        proto::UpdateDialogNotificationSettingsResult::default(),
5319                    ),
5320                ),
5321            )
5322            .await;
5323
5324            let delete = read_test_client_message(&mut ws).await;
5325            match &delete.body {
5326                Some(proto::client_message::Body::RpcCall(call)) => match &call.input {
5327                    Some(proto::rpc_call::Input::DeleteChat(input)) => {
5328                        assert_eq!(
5329                            test_chat_id_from_input_peer(input.peer_id.as_ref()),
5330                            Some(7)
5331                        );
5332                    }
5333                    other => panic!("expected delete chat input, got {other:?}"),
5334                },
5335                other => panic!("expected delete chat RPC, got {other:?}"),
5336            }
5337            send_test_server_message(
5338                &mut ws,
5339                rpc_result_message(
5340                    8,
5341                    delete.id,
5342                    proto::rpc_result::Result::DeleteChat(proto::DeleteChatResult {}),
5343                ),
5344            )
5345            .await;
5346        });
5347
5348        let store = InMemoryStore::new();
5349        store.save_session(connect_session()).await.unwrap();
5350        store
5351            .record_dialog(DialogRecord::new(InlineId::new(7)))
5352            .await
5353            .unwrap();
5354        let backend = SdkBackend::builder()
5355            .store(store.clone())
5356            .realtime_url(format!("ws://{addr}/realtime"))
5357            .without_realtime_handshake()
5358            .build()
5359            .unwrap();
5360
5361        backend
5362            .add_chat_participant(AddChatParticipantRequest {
5363                chat_id: InlineId::new(7),
5364                user_id: InlineId::new(42),
5365            })
5366            .await
5367            .unwrap();
5368        assert_eq!(
5369            store
5370                .chat_participants(InlineId::new(7))
5371                .await
5372                .unwrap()
5373                .len(),
5374            1
5375        );
5376        backend
5377            .remove_chat_participant(RemoveChatParticipantRequest {
5378                chat_id: InlineId::new(7),
5379                user_id: InlineId::new(42),
5380            })
5381            .await
5382            .unwrap();
5383        assert!(
5384            store
5385                .chat_participants(InlineId::new(7))
5386                .await
5387                .unwrap()
5388                .is_empty()
5389        );
5390        backend
5391            .update_chat_info(UpdateChatInfoRequest {
5392                chat_id: InlineId::new(7),
5393                title: Some(" Renamed ".to_owned()),
5394                emoji: Some(" ✨ ".to_owned()),
5395            })
5396            .await
5397            .unwrap();
5398        assert_eq!(
5399            store
5400                .dialog(InlineId::new(7))
5401                .await
5402                .unwrap()
5403                .unwrap()
5404                .title
5405                .as_deref(),
5406            Some("Renamed")
5407        );
5408        assert_eq!(
5409            store
5410                .dialog(InlineId::new(7))
5411                .await
5412                .unwrap()
5413                .unwrap()
5414                .emoji
5415                .as_deref(),
5416            Some("✨")
5417        );
5418        backend
5419            .set_marked_unread(SetMarkedUnreadRequest {
5420                chat_id: InlineId::new(7),
5421                unread: true,
5422            })
5423            .await
5424            .unwrap();
5425        backend
5426            .set_marked_unread(SetMarkedUnreadRequest {
5427                chat_id: InlineId::new(7),
5428                unread: false,
5429            })
5430            .await
5431            .unwrap();
5432        backend
5433            .update_dialog_notifications(UpdateDialogNotificationsRequest {
5434                chat_id: InlineId::new(7),
5435                mode: Some(DialogNotificationMode::None),
5436            })
5437            .await
5438            .unwrap();
5439        assert_eq!(
5440            store
5441                .dialog(InlineId::new(7))
5442                .await
5443                .unwrap()
5444                .unwrap()
5445                .notification_mode,
5446            Some(DialogNotificationMode::None)
5447        );
5448        backend
5449            .delete_chat(DeleteChatRequest {
5450                chat_id: InlineId::new(7),
5451            })
5452            .await
5453            .unwrap();
5454        assert_eq!(
5455            store.deleted_chat_ids().await.unwrap(),
5456            vec![InlineId::new(7)]
5457        );
5458        server.await.unwrap();
5459    }
5460
5461    #[tokio::test]
5462    async fn sdk_backend_recovers_hint_only_updates_on_the_multiplexed_connection() {
5463        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5464        let addr = listener.local_addr().unwrap();
5465        let server = tokio::spawn(async move {
5466            let (stream, _) = listener.accept().await.unwrap();
5467            let mut ws = accept_async(stream).await.unwrap();
5468            let _ = read_test_client_message(&mut ws).await;
5469            send_test_server_message(
5470                &mut ws,
5471                proto::ServerProtocolMessage {
5472                    id: 1,
5473                    body: Some(proto::server_protocol_message::Body::ConnectionOpen(
5474                        proto::ConnectionOpen {},
5475                    )),
5476                },
5477            )
5478            .await;
5479
5480            let state = read_test_client_message(&mut ws).await;
5481            send_test_server_message(
5482                &mut ws,
5483                rpc_result_message(
5484                    2,
5485                    state.id,
5486                    proto::rpc_result::Result::GetUpdatesState(proto::GetUpdatesStateResult {
5487                        date: 100,
5488                        updates_found: Some(true),
5489                    }),
5490                ),
5491            )
5492            .await;
5493
5494            let user_bucket = read_test_client_message(&mut ws).await;
5495            send_test_server_message(
5496                &mut ws,
5497                rpc_result_message(
5498                    3,
5499                    user_bucket.id,
5500                    proto::rpc_result::Result::GetUpdates(proto::GetUpdatesResult {
5501                        updates: Vec::new(),
5502                        seq: 1,
5503                        date: 100,
5504                        r#final: Some(true),
5505                        result_type: proto::get_updates_result::ResultType::Empty as i32,
5506                        sidecars: None,
5507                    }),
5508                ),
5509            )
5510            .await;
5511
5512            let peer = proto::Peer {
5513                r#type: Some(proto::peer::Type::Chat(proto::PeerChat { chat_id: 7 })),
5514            };
5515            send_test_server_message(
5516                &mut ws,
5517                proto::ServerProtocolMessage {
5518                    id: 10,
5519                    body: Some(proto::server_protocol_message::Body::Message(
5520                        proto::ServerMessage {
5521                            payload: Some(proto::server_message::Payload::Update(
5522                                proto::UpdatesPayload {
5523                                    updates: vec![proto::Update {
5524                                        seq: None,
5525                                        date: None,
5526                                        update: Some(proto::update::Update::ChatHasNewUpdates(
5527                                            proto::UpdateChatHasNewUpdates {
5528                                                chat_id: 7,
5529                                                update_seq: 2,
5530                                                peer_id: Some(peer.clone()),
5531                                            },
5532                                        )),
5533                                    }],
5534                                },
5535                            )),
5536                        },
5537                    )),
5538                },
5539            )
5540            .await;
5541            let ack = read_test_client_message(&mut ws).await;
5542            assert!(matches!(
5543                ack.body,
5544                Some(proto::client_message::Body::Ack(proto::Ack { msg_id: 10 }))
5545            ));
5546
5547            let chat_bucket = read_test_client_message(&mut ws).await;
5548            send_test_server_message(
5549                &mut ws,
5550                rpc_result_message(
5551                    4,
5552                    chat_bucket.id,
5553                    proto::rpc_result::Result::GetUpdates(proto::GetUpdatesResult {
5554                        updates: vec![proto::Update {
5555                            seq: Some(2),
5556                            date: Some(101),
5557                            update: Some(proto::update::Update::NewMessage(
5558                                proto::UpdateNewMessage {
5559                                    message: Some(proto::Message {
5560                                        id: 11,
5561                                        from_id: 2,
5562                                        peer_id: Some(peer),
5563                                        chat_id: 7,
5564                                        message: Some("recovered".to_owned()),
5565                                        date: 101,
5566                                        ..Default::default()
5567                                    }),
5568                                },
5569                            )),
5570                        }],
5571                        seq: 2,
5572                        date: 101,
5573                        r#final: Some(true),
5574                        result_type: proto::get_updates_result::ResultType::Slice as i32,
5575                        sidecars: None,
5576                    }),
5577                ),
5578            )
5579            .await;
5580        });
5581
5582        let store = InMemoryStore::new();
5583        store.save_session(connect_session()).await.unwrap();
5584        store
5585            .save_sync_bucket_state(
5586                crate::SyncBucketKey::User,
5587                crate::SyncBucketState { seq: 1, date: 100 },
5588            )
5589            .await
5590            .unwrap();
5591        store
5592            .save_sync_bucket_state(
5593                crate::SyncBucketKey::Chat {
5594                    peer: crate::SyncBucketPeer::Chat {
5595                        chat_id: InlineId::new(7),
5596                    },
5597                },
5598                crate::SyncBucketState { seq: 1, date: 100 },
5599            )
5600            .await
5601            .unwrap();
5602        let backend = SdkBackend::builder()
5603            .store(store.clone())
5604            .realtime_url(format!("ws://{addr}/realtime"))
5605            .without_realtime_handshake()
5606            .build()
5607            .unwrap();
5608
5609        let events =
5610            tokio::time::timeout(std::time::Duration::from_secs(3), backend.receive_events())
5611                .await
5612                .unwrap()
5613                .unwrap();
5614
5615        assert!(events.iter().any(|event| matches!(
5616            event,
5617            ClientEvent::MessageStored { message }
5618                if message.chat_id == InlineId::new(7)
5619                    && message.message_id == InlineId::new(11)
5620        )));
5621        assert_eq!(
5622            store
5623                .sync_bucket_state(crate::SyncBucketKey::Chat {
5624                    peer: crate::SyncBucketPeer::Chat {
5625                        chat_id: InlineId::new(7),
5626                    },
5627                })
5628                .await
5629                .unwrap(),
5630            crate::SyncBucketState { seq: 2, date: 101 }
5631        );
5632        server.await.unwrap();
5633    }
5634
5635    #[tokio::test]
5636    async fn sdk_backend_repairs_cold_chat_bucket_after_too_long() {
5637        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5638        let addr = listener.local_addr().unwrap();
5639        let server = tokio::spawn(async move {
5640            let (stream, _) = listener.accept().await.unwrap();
5641            let mut ws = accept_async(stream).await.unwrap();
5642            let _ = read_test_client_message(&mut ws).await;
5643            send_test_server_message(
5644                &mut ws,
5645                proto::ServerProtocolMessage {
5646                    id: 1,
5647                    body: Some(proto::server_protocol_message::Body::ConnectionOpen(
5648                        proto::ConnectionOpen {},
5649                    )),
5650                },
5651            )
5652            .await;
5653
5654            let state = read_test_client_message(&mut ws).await;
5655            send_test_server_message(
5656                &mut ws,
5657                rpc_result_message(
5658                    2,
5659                    state.id,
5660                    proto::rpc_result::Result::GetUpdatesState(proto::GetUpdatesStateResult {
5661                        date: 100,
5662                        updates_found: Some(true),
5663                    }),
5664                ),
5665            )
5666            .await;
5667            let user_bucket = read_test_client_message(&mut ws).await;
5668            send_test_server_message(
5669                &mut ws,
5670                rpc_result_message(
5671                    3,
5672                    user_bucket.id,
5673                    proto::rpc_result::Result::GetUpdates(proto::GetUpdatesResult {
5674                        updates: Vec::new(),
5675                        seq: 1,
5676                        date: 100,
5677                        r#final: Some(true),
5678                        result_type: proto::get_updates_result::ResultType::Empty as i32,
5679                        sidecars: None,
5680                    }),
5681                ),
5682            )
5683            .await;
5684
5685            let peer = proto::Peer {
5686                r#type: Some(proto::peer::Type::Chat(proto::PeerChat { chat_id: 7 })),
5687            };
5688            send_test_server_message(
5689                &mut ws,
5690                proto::ServerProtocolMessage {
5691                    id: 10,
5692                    body: Some(proto::server_protocol_message::Body::Message(
5693                        proto::ServerMessage {
5694                            payload: Some(proto::server_message::Payload::Update(
5695                                proto::UpdatesPayload {
5696                                    updates: vec![proto::Update {
5697                                        seq: None,
5698                                        date: None,
5699                                        update: Some(proto::update::Update::ChatHasNewUpdates(
5700                                            proto::UpdateChatHasNewUpdates {
5701                                                chat_id: 7,
5702                                                update_seq: 500,
5703                                                peer_id: Some(peer.clone()),
5704                                            },
5705                                        )),
5706                                    }],
5707                                },
5708                            )),
5709                        },
5710                    )),
5711                },
5712            )
5713            .await;
5714            let _ack = read_test_client_message(&mut ws).await;
5715
5716            let bucket = read_test_client_message(&mut ws).await;
5717            send_test_server_message(
5718                &mut ws,
5719                rpc_result_message(
5720                    4,
5721                    bucket.id,
5722                    proto::rpc_result::Result::GetUpdates(proto::GetUpdatesResult {
5723                        updates: Vec::new(),
5724                        seq: 400,
5725                        date: 101,
5726                        r#final: Some(false),
5727                        result_type: proto::get_updates_result::ResultType::TooLong as i32,
5728                        sidecars: None,
5729                    }),
5730                ),
5731            )
5732            .await;
5733
5734            let get_chat = read_test_client_message(&mut ws).await;
5735            assert!(matches!(
5736                &get_chat.body,
5737                Some(proto::client_message::Body::RpcCall(proto::RpcCall {
5738                    method,
5739                    input: Some(proto::rpc_call::Input::GetChat(_)),
5740                })) if *method == proto::Method::GetChat as i32
5741            ));
5742            send_test_server_message(
5743                &mut ws,
5744                rpc_result_message(
5745                    5,
5746                    get_chat.id,
5747                    proto::rpc_result::Result::GetChat(proto::GetChatResult {
5748                        chat: Some(proto::Chat {
5749                            id: 7,
5750                            title: "Repaired".to_owned(),
5751                            peer_id: Some(peer.clone()),
5752                            ..Default::default()
5753                        }),
5754                        dialog: Some(proto::Dialog {
5755                            chat_id: Some(7),
5756                            peer: Some(peer.clone()),
5757                            ..Default::default()
5758                        }),
5759                        pinned_message_ids: vec![11],
5760                        anchor_message: None,
5761                    }),
5762                ),
5763            )
5764            .await;
5765
5766            let participants = read_test_client_message(&mut ws).await;
5767            assert!(matches!(
5768                &participants.body,
5769                Some(proto::client_message::Body::RpcCall(proto::RpcCall {
5770                    method,
5771                    input: Some(proto::rpc_call::Input::GetChatParticipants(_)),
5772                })) if *method == proto::Method::GetChatParticipants as i32
5773            ));
5774            send_test_server_message(
5775                &mut ws,
5776                rpc_result_message(
5777                    6,
5778                    participants.id,
5779                    proto::rpc_result::Result::GetChatParticipants(
5780                        proto::GetChatParticipantsResult::default(),
5781                    ),
5782                ),
5783            )
5784            .await;
5785
5786            let history = read_test_client_message(&mut ws).await;
5787            assert!(matches!(
5788                &history.body,
5789                Some(proto::client_message::Body::RpcCall(proto::RpcCall {
5790                    method,
5791                    input: Some(proto::rpc_call::Input::GetChatHistory(_)),
5792                })) if *method == proto::Method::GetChatHistory as i32
5793            ));
5794            send_test_server_message(
5795                &mut ws,
5796                rpc_result_message(
5797                    6,
5798                    history.id,
5799                    proto::rpc_result::Result::GetChatHistory(proto::GetChatHistoryResult {
5800                        messages: vec![proto::Message {
5801                            id: 11,
5802                            chat_id: 7,
5803                            peer_id: Some(peer),
5804                            message: Some("snapshot".to_owned()),
5805                            date: 101,
5806                            ..Default::default()
5807                        }],
5808                    }),
5809                ),
5810            )
5811            .await;
5812        });
5813
5814        let store = InMemoryStore::new();
5815        store.save_session(connect_session()).await.unwrap();
5816        store
5817            .save_sync_bucket_state(
5818                crate::SyncBucketKey::User,
5819                crate::SyncBucketState { seq: 1, date: 100 },
5820            )
5821            .await
5822            .unwrap();
5823        let backend = SdkBackend::builder()
5824            .store(store.clone())
5825            .realtime_url(format!("ws://{addr}/realtime"))
5826            .without_realtime_handshake()
5827            .build()
5828            .unwrap();
5829
5830        let events =
5831            tokio::time::timeout(std::time::Duration::from_secs(3), backend.receive_events())
5832                .await
5833                .unwrap()
5834                .unwrap();
5835
5836        assert!(events.iter().any(|event| matches!(
5837            event,
5838            ClientEvent::MessageStored { message } if message.message_id == InlineId::new(11)
5839        )));
5840        let dialog = store.dialog(InlineId::new(7)).await.unwrap().unwrap();
5841        assert_eq!(dialog.title.as_deref(), Some("Repaired"));
5842        assert_eq!(dialog.pinned_message_ids, vec![InlineId::new(11)]);
5843        assert_eq!(
5844            store
5845                .sync_bucket_state(crate::SyncBucketKey::Chat {
5846                    peer: crate::SyncBucketPeer::Chat {
5847                        chat_id: InlineId::new(7),
5848                    },
5849                })
5850                .await
5851                .unwrap(),
5852            crate::SyncBucketState {
5853                seq: 500,
5854                date: 101
5855            }
5856        );
5857        server.await.unwrap();
5858    }
5859
5860    #[tokio::test]
5861    async fn sdk_backend_user_snapshot_rebuilds_authoritative_account_state() {
5862        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5863        let addr = listener.local_addr().unwrap();
5864        let server = tokio::spawn(async move {
5865            let (stream, _) = listener.accept().await.unwrap();
5866            let mut ws = accept_async(stream).await.unwrap();
5867            let init = read_test_client_message(&mut ws).await;
5868            assert!(matches!(
5869                init.body,
5870                Some(proto::client_message::Body::ConnectionInit(_))
5871            ));
5872            send_test_server_message(
5873                &mut ws,
5874                proto::ServerProtocolMessage {
5875                    id: 1,
5876                    body: Some(proto::server_protocol_message::Body::ConnectionOpen(
5877                        proto::ConnectionOpen {},
5878                    )),
5879                },
5880            )
5881            .await;
5882
5883            let chats = read_test_client_message(&mut ws).await;
5884            send_test_server_message(
5885                &mut ws,
5886                rpc_result_message(
5887                    2,
5888                    chats.id,
5889                    proto::rpc_result::Result::GetChats(proto::GetChatsResult::default()),
5890                ),
5891            )
5892            .await;
5893            let me = read_test_client_message(&mut ws).await;
5894            send_test_server_message(
5895                &mut ws,
5896                rpc_result_message(
5897                    3,
5898                    me.id,
5899                    proto::rpc_result::Result::GetMe(proto::GetMeResult {
5900                        user: Some(proto::User {
5901                            id: 42,
5902                            first_name: Some("Ada".to_owned()),
5903                            ..Default::default()
5904                        }),
5905                    }),
5906                ),
5907            )
5908            .await;
5909            let settings = read_test_client_message(&mut ws).await;
5910            send_test_server_message(
5911                &mut ws,
5912                rpc_result_message(
5913                    4,
5914                    settings.id,
5915                    proto::rpc_result::Result::GetUserSettings(proto::GetUserSettingsResult {
5916                        user_settings: Some(proto::UserSettings::default()),
5917                    }),
5918                ),
5919            )
5920            .await;
5921        });
5922
5923        let store = InMemoryStore::new();
5924        store.save_session(connect_session()).await.unwrap();
5925        store
5926            .record_dialog(DialogRecord::new(InlineId::new(7)))
5927            .await
5928            .unwrap();
5929        let backend = SdkBackend::builder()
5930            .store(store.clone())
5931            .realtime_url(format!("ws://{addr}/realtime"))
5932            .without_realtime_handshake()
5933            .build()
5934            .unwrap();
5935
5936        let events = backend.repair_user_bucket().await.unwrap();
5937
5938        assert!(events.iter().any(|event| matches!(
5939            event,
5940            ClientEvent::ChatDeleted { chat_id } if *chat_id == InlineId::new(7)
5941        )));
5942        assert!(events.iter().any(|event| matches!(
5943            event,
5944            ClientEvent::UserUpserted { user_id } if *user_id == InlineId::new(42)
5945        )));
5946        assert!(
5947            events
5948                .iter()
5949                .any(|event| matches!(event, ClientEvent::UserSettingsChanged {}))
5950        );
5951        assert_eq!(
5952            store.deleted_chat_ids().await.unwrap(),
5953            vec![InlineId::new(7)]
5954        );
5955        assert!(store.user_settings().await.unwrap().is_some());
5956        server.await.unwrap();
5957    }
5958
5959    #[test]
5960    fn dialogs_page_from_get_chats_uses_user_records() {
5961        let result = proto::GetChatsResult {
5962            dialogs: vec![proto::Dialog {
5963                peer: Some(proto::Peer {
5964                    r#type: Some(proto::peer::Type::User(proto::PeerUser { user_id: 42 })),
5965                }),
5966                chat_id: Some(7),
5967                unread_count: Some(3),
5968                ..Default::default()
5969            }],
5970            chats: vec![proto::Chat {
5971                id: 7,
5972                title: "Direct chat fallback".to_owned(),
5973                last_msg_id: Some(99),
5974                emoji: Some("*".to_owned()),
5975                peer_id: Some(proto::Peer {
5976                    r#type: Some(proto::peer::Type::User(proto::PeerUser { user_id: 42 })),
5977                }),
5978                ..Default::default()
5979            }],
5980            users: vec![proto::User {
5981                id: 42,
5982                first_name: Some("Ada".to_owned()),
5983                last_name: Some("Lovelace".to_owned()),
5984                username: Some("ada".to_owned()),
5985                profile_photo: Some(proto::UserProfilePhoto {
5986                    cdn_url: Some("https://cdn.inline.test/ada.jpg".to_owned()),
5987                    ..Default::default()
5988                }),
5989                bot: Some(false),
5990                ..Default::default()
5991            }],
5992            ..Default::default()
5993        };
5994
5995        let page = dialogs_page_from_get_chats(
5996            &result,
5997            DialogsRequest {
5998                limit: Some(10),
5999                cursor: None,
6000            },
6001        )
6002        .unwrap();
6003
6004        assert_eq!(page.dialogs.len(), 1);
6005        assert_eq!(page.dialogs[0].chat_id, InlineId::new(7));
6006        assert_eq!(page.dialogs[0].title.as_deref(), Some("Ada Lovelace"));
6007        assert_eq!(page.dialogs[0].emoji.as_deref(), Some("*"));
6008        assert_eq!(page.dialogs[0].peer_user_id, Some(InlineId::new(42)));
6009        assert_eq!(page.dialogs[0].last_message_id, Some(InlineId::new(99)));
6010        assert_eq!(page.dialogs[0].unread_count, Some(3));
6011        assert_eq!(page.users.len(), 1);
6012        assert_eq!(page.users[0].user_id, InlineId::new(42));
6013        assert_eq!(page.users[0].display_name.as_deref(), Some("Ada Lovelace"));
6014        assert_eq!(
6015            page.users[0].avatar_url.as_deref(),
6016            Some("https://cdn.inline.test/ada.jpg")
6017        );
6018        assert_eq!(page.users[0].is_bot, Some(false));
6019    }
6020
6021    #[test]
6022    fn chat_participants_page_uses_direct_participants() {
6023        let page = chat_participants_page_from_proto(proto::GetChatParticipantsResult {
6024            participants: vec![proto::ChatParticipant {
6025                user_id: 10,
6026                date: 100,
6027            }],
6028            users: vec![proto::User {
6029                id: 10,
6030                first_name: Some("Ada".to_owned()),
6031                ..Default::default()
6032            }],
6033        });
6034
6035        assert_eq!(page.participants.len(), 1);
6036        assert_eq!(page.participants[0].user_id, InlineId::new(10));
6037        assert_eq!(page.participants[0].date, Some(100));
6038        assert_eq!(page.users.len(), 1);
6039    }
6040
6041    #[tokio::test]
6042    async fn sdk_backend_requires_session_for_history() {
6043        let backend = SdkBackend::builder().build().unwrap();
6044
6045        let err = backend
6046            .history(HistoryRequest {
6047                chat_id: InlineId::new(1),
6048                limit: Some(10),
6049                before_message_id: None,
6050                after_message_id: None,
6051            })
6052            .await
6053            .expect_err("history should require connect");
6054
6055        assert_eq!(err.category, ClientErrorCategory::AuthRequired);
6056    }
6057
6058    #[tokio::test]
6059    async fn sdk_backend_send_text_requires_session() {
6060        let backend = SdkBackend::builder().build().unwrap();
6061
6062        let err = backend
6063            .send_text(SendTextRequest::new(
6064                crate::PeerRef::Chat {
6065                    chat_id: InlineId::new(1),
6066                },
6067                "hello",
6068            ))
6069            .await
6070            .expect_err("send_text should require connect");
6071
6072        assert_eq!(err.category, ClientErrorCategory::AuthRequired);
6073    }
6074
6075    #[tokio::test]
6076    async fn sdk_backend_send_text_rejects_empty_text_before_network() {
6077        let backend = SdkBackend::builder().build().unwrap();
6078
6079        let err = backend
6080            .send_text(SendTextRequest::new(
6081                crate::PeerRef::Chat {
6082                    chat_id: InlineId::new(1),
6083                },
6084                " ",
6085            ))
6086            .await
6087            .expect_err("empty message should fail before auth or network");
6088
6089        assert_eq!(err.category, ClientErrorCategory::InvalidInput);
6090    }
6091
6092    #[tokio::test]
6093    async fn sdk_backend_send_text_returns_existing_transaction_without_network() {
6094        let store = InMemoryStore::new();
6095        let request = SendTextRequest {
6096            peer: crate::PeerRef::Chat {
6097                chat_id: InlineId::new(7),
6098            },
6099            text: "hello".to_owned(),
6100            external_id: Some(ExternalId::try_new("host-event", "event-1").unwrap()),
6101            random_id: Some(RandomId::new(99)),
6102            reply_to_message_id: None,
6103        };
6104        let transaction_id = transaction_id_for_send(&request, request.random_id.unwrap());
6105        let identity = TransactionIdentity::new(
6106            transaction_id.clone(),
6107            request.external_id.clone(),
6108            request.random_id.unwrap(),
6109        )
6110        .with_final_message_id(InlineId::new(11));
6111        store.save_session(connect_session()).await.unwrap();
6112        store
6113            .record_transaction(
6114                StoredTransaction::new(identity, TransactionState::Completed)
6115                    .with_chat_id(InlineId::new(7))
6116                    .with_message_id(InlineId::new(11)),
6117            )
6118            .await
6119            .unwrap();
6120        let backend = SdkBackend::builder().store(store).build().unwrap();
6121
6122        let outcome = backend.send_text(request).await.unwrap();
6123
6124        assert_eq!(outcome.message_id, Some(InlineId::new(11)));
6125        assert_eq!(outcome.state, TransactionState::Completed);
6126        assert_eq!(outcome.mutation.transaction.transaction_id, transaction_id);
6127    }
6128
6129    #[tokio::test]
6130    async fn sdk_backend_retries_uncertain_stored_send_with_same_random_id() {
6131        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6132        let addr = listener.local_addr().unwrap();
6133        let server = tokio::spawn(async move {
6134            let (stream, _) = listener.accept().await.unwrap();
6135            let mut ws = accept_async(stream).await.unwrap();
6136            let init = read_test_client_message(&mut ws).await;
6137            assert!(matches!(
6138                init.body,
6139                Some(proto::client_message::Body::ConnectionInit(_))
6140            ));
6141            send_test_server_message(
6142                &mut ws,
6143                proto::ServerProtocolMessage {
6144                    id: 1,
6145                    body: Some(proto::server_protocol_message::Body::ConnectionOpen(
6146                        proto::ConnectionOpen {},
6147                    )),
6148                },
6149            )
6150            .await;
6151
6152            let send = read_test_client_message(&mut ws).await;
6153            let input = match &send.body {
6154                Some(proto::client_message::Body::RpcCall(proto::RpcCall {
6155                    method,
6156                    input: Some(proto::rpc_call::Input::SendMessage(input)),
6157                })) if *method == proto::Method::SendMessage as i32 => input,
6158                other => panic!("expected sendMessage retry, got {other:?}"),
6159            };
6160            assert_eq!(input.random_id, Some(99));
6161            send_test_server_message(
6162                &mut ws,
6163                rpc_result_message(
6164                    2,
6165                    send.id,
6166                    proto::rpc_result::Result::SendMessage(proto::SendMessageResult {
6167                        updates: vec![proto::Update {
6168                            seq: None,
6169                            date: None,
6170                            update: Some(proto::update::Update::UpdateMessageId(
6171                                proto::UpdateMessageId {
6172                                    message_id: 11,
6173                                    random_id: 99,
6174                                },
6175                            )),
6176                        }],
6177                    }),
6178                ),
6179            )
6180            .await;
6181        });
6182
6183        let store = InMemoryStore::new();
6184        store.save_session(connect_session()).await.unwrap();
6185        let request = SendTextRequest {
6186            peer: crate::PeerRef::Chat {
6187                chat_id: InlineId::new(7),
6188            },
6189            text: "hello".to_owned(),
6190            external_id: Some(ExternalId::try_new("host-event", "event-1").unwrap()),
6191            random_id: Some(RandomId::new(99)),
6192            reply_to_message_id: None,
6193        };
6194        let transaction_id = transaction_id_for_send(&request, RandomId::new(99));
6195        let identity = TransactionIdentity::new(
6196            transaction_id.clone(),
6197            request.external_id.clone(),
6198            RandomId::new(99),
6199        );
6200        store
6201            .record_transaction(
6202                StoredTransaction::new(identity, TransactionState::Sent)
6203                    .with_chat_id(InlineId::new(7))
6204                    .with_failure(crate::ClientFailure::new(
6205                        ClientErrorCategory::Timeout,
6206                        "send result timed out",
6207                    )),
6208            )
6209            .await
6210            .unwrap();
6211        let backend = SdkBackend::builder()
6212            .store(store.clone())
6213            .realtime_url(format!("ws://{addr}/realtime"))
6214            .without_realtime_handshake()
6215            .build()
6216            .unwrap();
6217
6218        let outcome = backend.send_text(request).await.unwrap();
6219
6220        assert_eq!(outcome.state, TransactionState::Completed);
6221        assert_eq!(outcome.message_id, Some(InlineId::new(11)));
6222        assert_eq!(outcome.mutation.transaction.transaction_id, transaction_id);
6223        assert_eq!(
6224            store
6225                .transaction(transaction_id)
6226                .await
6227                .unwrap()
6228                .unwrap()
6229                .state,
6230            TransactionState::Completed
6231        );
6232        server.await.unwrap();
6233    }
6234
6235    #[test]
6236    fn media_content_from_proto_preserves_document_descriptor() {
6237        let content = media_content_from_proto(
6238            &proto::MessageMedia {
6239                media: Some(proto::message_media::Media::Document(
6240                    proto::MessageDocument {
6241                        document: Some(proto::Document {
6242                            id: 55,
6243                            file_name: "report.pdf".to_owned(),
6244                            mime_type: "application/pdf".to_owned(),
6245                            size: 12_345,
6246                            cdn_url: Some("https://cdn.inline.test/report.pdf".to_owned()),
6247                            ..Default::default()
6248                        }),
6249                    },
6250                )),
6251            },
6252            Some("quarterly report".to_owned()),
6253        );
6254
6255        match content {
6256            MessageContent::Media {
6257                kind,
6258                file_id,
6259                url,
6260                mime_type,
6261                file_name,
6262                caption,
6263                size_bytes,
6264                width,
6265                height,
6266                duration_ms,
6267            } => {
6268                assert_eq!(kind, MediaKind::Document);
6269                assert_eq!(file_id, "55");
6270                assert_eq!(url.as_deref(), Some("https://cdn.inline.test/report.pdf"));
6271                assert_eq!(mime_type.as_deref(), Some("application/pdf"));
6272                assert_eq!(file_name.as_deref(), Some("report.pdf"));
6273                assert_eq!(caption.as_deref(), Some("quarterly report"));
6274                assert_eq!(size_bytes, Some(12_345));
6275                assert_eq!(width, None);
6276                assert_eq!(height, None);
6277                assert_eq!(duration_ms, None);
6278            }
6279            other => panic!("expected media content, got {other:?}"),
6280        }
6281    }
6282
6283    #[test]
6284    fn media_content_from_proto_picks_largest_photo_descriptor() {
6285        let content = media_content_from_proto(
6286            &proto::MessageMedia {
6287                media: Some(proto::message_media::Media::Photo(proto::MessagePhoto {
6288                    photo: Some(proto::Photo {
6289                        id: 9,
6290                        format: 2,
6291                        sizes: vec![
6292                            proto::PhotoSize {
6293                                w: 20,
6294                                h: 20,
6295                                size: 100,
6296                                cdn_url: Some("https://cdn.inline.test/small.png".to_owned()),
6297                                ..Default::default()
6298                            },
6299                            proto::PhotoSize {
6300                                w: 200,
6301                                h: 200,
6302                                size: 500,
6303                                cdn_url: None,
6304                                ..Default::default()
6305                            },
6306                            proto::PhotoSize {
6307                                w: 50,
6308                                h: 50,
6309                                size: 200,
6310                                cdn_url: Some("https://cdn.inline.test/large.png".to_owned()),
6311                                ..Default::default()
6312                            },
6313                        ],
6314                        ..Default::default()
6315                    }),
6316                })),
6317            },
6318            None,
6319        );
6320
6321        match content {
6322            MessageContent::Media {
6323                kind,
6324                file_id,
6325                url,
6326                mime_type,
6327                size_bytes,
6328                width,
6329                height,
6330                ..
6331            } => {
6332                assert_eq!(kind, MediaKind::Photo);
6333                assert_eq!(file_id, "9");
6334                assert_eq!(url.as_deref(), Some("https://cdn.inline.test/large.png"));
6335                assert_eq!(mime_type.as_deref(), Some("image/png"));
6336                assert_eq!(size_bytes, Some(200));
6337                assert_eq!(width, Some(50));
6338                assert_eq!(height, Some(50));
6339            }
6340            other => panic!("expected media content, got {other:?}"),
6341        }
6342    }
6343
6344    #[test]
6345    fn upload_input_for_video_without_complete_metadata_falls_back_to_document() {
6346        let request = UploadRequest {
6347            peer: crate::PeerRef::Chat {
6348                chat_id: InlineId::new(7),
6349            },
6350            kind: MediaKind::Video,
6351            file_name: Some("clip.mp4".to_owned()),
6352            mime_type: Some("video/mp4".to_owned()),
6353            size_bytes: Some(4),
6354            caption: None,
6355            width: Some(640),
6356            height: None,
6357            duration_ms: Some(1_500),
6358            external_id: None,
6359            random_id: None,
6360            reply_to_message_id: None,
6361        };
6362
6363        let input = upload_input_for_request(&request, vec![1, 2, 3, 4]);
6364
6365        assert_eq!(input.file_type, UploadFileType::Document);
6366        assert_eq!(input.file_name, "clip.mp4");
6367        assert_eq!(input.mime_type.as_deref(), Some("video/mp4"));
6368        assert_eq!(input.video_metadata, None);
6369    }
6370
6371    #[test]
6372    fn upload_input_for_video_with_complete_metadata_uses_video() {
6373        let request = UploadRequest {
6374            peer: crate::PeerRef::Chat {
6375                chat_id: InlineId::new(7),
6376            },
6377            kind: MediaKind::Video,
6378            file_name: Some("clip.mp4".to_owned()),
6379            mime_type: Some("video/mp4".to_owned()),
6380            size_bytes: Some(4),
6381            caption: None,
6382            width: Some(640),
6383            height: Some(480),
6384            duration_ms: Some(1_500),
6385            external_id: None,
6386            random_id: None,
6387            reply_to_message_id: None,
6388        };
6389
6390        let input = upload_input_for_request(&request, vec![1, 2, 3, 4]);
6391
6392        assert_eq!(input.file_type, UploadFileType::Video);
6393        assert_eq!(
6394            input.video_metadata,
6395            Some(UploadVideoMetadata::new(640, 480, 2))
6396        );
6397    }
6398
6399    #[test]
6400    fn history_input_for_after_message_id_uses_newer_mode() {
6401        let input = history_input_for_request(
6402            &HistoryRequest {
6403                chat_id: InlineId::new(7),
6404                limit: Some(20),
6405                before_message_id: None,
6406                after_message_id: Some(InlineId::new(10)),
6407            },
6408            21,
6409        );
6410
6411        assert_eq!(input.after_id, Some(10));
6412        assert_eq!(
6413            input.mode,
6414            Some(proto::GetChatHistoryMode::HistoryModeNewer as i32)
6415        );
6416        assert_eq!(input.offset_id, None);
6417        assert_eq!(input.limit, Some(21));
6418    }
6419
6420    #[test]
6421    fn history_input_for_before_message_id_uses_offset_id() {
6422        let input = history_input_for_request(
6423            &HistoryRequest {
6424                chat_id: InlineId::new(7),
6425                limit: Some(20),
6426                before_message_id: Some(InlineId::new(30)),
6427                after_message_id: None,
6428            },
6429            21,
6430        );
6431
6432        assert_eq!(input.offset_id, Some(30));
6433        assert_eq!(input.mode, None);
6434        assert_eq!(input.after_id, None);
6435        assert_eq!(input.limit, Some(21));
6436    }
6437
6438    #[test]
6439    fn normalize_live_history_latest_keeps_newest_messages() {
6440        let records = (1..=4).rev().map(test_message_record).collect::<Vec<_>>();
6441
6442        let (records, has_more) = normalize_live_history_records(records, 3, false);
6443
6444        assert!(has_more);
6445        assert_eq!(
6446            records
6447                .iter()
6448                .map(|message| message.message_id)
6449                .collect::<Vec<_>>(),
6450            vec![InlineId::new(2), InlineId::new(3), InlineId::new(4)]
6451        );
6452    }
6453
6454    #[test]
6455    fn normalize_live_history_newer_keeps_oldest_checkpoint_window() {
6456        let records = vec![
6457            test_message_record(8),
6458            test_message_record(7),
6459            test_message_record(6),
6460            test_message_record(5),
6461        ];
6462
6463        let (records, has_more) = normalize_live_history_records(records, 3, true);
6464
6465        assert!(has_more);
6466        assert_eq!(
6467            records
6468                .iter()
6469                .map(|message| message.message_id)
6470                .collect::<Vec<_>>(),
6471            vec![InlineId::new(5), InlineId::new(6), InlineId::new(7)]
6472        );
6473    }
6474
6475    #[test]
6476    fn normalize_live_history_older_keeps_newest_messages_below_cursor() {
6477        let records = vec![
6478            test_message_record(3),
6479            test_message_record(2),
6480            test_message_record(1),
6481        ];
6482
6483        let (records, has_more) = normalize_live_history_records(records, 2, false);
6484
6485        assert!(has_more);
6486        assert_eq!(
6487            records
6488                .iter()
6489                .map(|message| message.message_id)
6490                .collect::<Vec<_>>(),
6491            vec![InlineId::new(2), InlineId::new(3)]
6492        );
6493    }
6494
6495    #[test]
6496    fn apply_send_message_updates_extracts_final_message() {
6497        let request = SendTextRequest {
6498            peer: crate::PeerRef::Chat {
6499                chat_id: InlineId::new(7),
6500            },
6501            text: "hello".to_owned(),
6502            external_id: Some(ExternalId::try_new("host-event", "event-1").unwrap()),
6503            random_id: Some(RandomId::new(99)),
6504            reply_to_message_id: None,
6505        };
6506        let identity = TransactionIdentity::new(
6507            TransactionId::try_new("txn").unwrap(),
6508            request.external_id.clone(),
6509            request.random_id.unwrap(),
6510        );
6511        let result = proto::SendMessageResult {
6512            updates: vec![proto::Update {
6513                seq: Some(1),
6514                date: Some(1),
6515                update: Some(proto::update::Update::NewMessage(proto::UpdateNewMessage {
6516                    message: Some(proto::Message {
6517                        id: 11,
6518                        from_id: 2,
6519                        peer_id: None,
6520                        chat_id: 7,
6521                        message: Some("hello".to_owned()),
6522                        out: true,
6523                        date: 123,
6524                        mentioned: None,
6525                        reply_to_msg_id: None,
6526                        media: None,
6527                        edit_date: None,
6528                        grouped_id: None,
6529                        attachments: None,
6530                        reactions: None,
6531                        is_sticker: None,
6532                        has_link: None,
6533                        entities: None,
6534                        send_mode: None,
6535                        fwd_from: None,
6536                        replies: None,
6537                        actions: None,
6538                        rev: None,
6539                    }),
6540                })),
6541            }],
6542        };
6543
6544        let applied = apply_send_message_updates(&request, identity, InlineId::new(7), result);
6545
6546        assert_eq!(applied.message_id, Some(InlineId::new(11)));
6547        assert_eq!(applied.transaction.state, TransactionState::Completed);
6548        assert_eq!(applied.message.unwrap().message_id, InlineId::new(11));
6549    }
6550
6551    #[tokio::test]
6552    async fn apply_updates_persists_lossless_message_state_before_emitting() {
6553        let store = InMemoryStore::new();
6554        let backend = SdkBackend::builder().store(store.clone()).build().unwrap();
6555        let peer = proto::Peer {
6556            r#type: Some(proto::peer::Type::Chat(proto::PeerChat { chat_id: 7 })),
6557        };
6558        let updates = vec![
6559            proto::Update {
6560                seq: Some(1),
6561                date: Some(10),
6562                update: Some(proto::update::Update::NewMessage(proto::UpdateNewMessage {
6563                    message: Some(proto::Message {
6564                        id: 11,
6565                        from_id: 2,
6566                        peer_id: Some(peer.clone()),
6567                        chat_id: 7,
6568                        message: Some("hello".to_owned()),
6569                        date: 10,
6570                        ..Default::default()
6571                    }),
6572                })),
6573            },
6574            proto::Update {
6575                seq: Some(2),
6576                date: Some(11),
6577                update: Some(proto::update::Update::UpdateReaction(
6578                    proto::UpdateReaction {
6579                        reaction: Some(proto::Reaction {
6580                            emoji: "👍".to_owned(),
6581                            user_id: 2,
6582                            message_id: 11,
6583                            chat_id: 7,
6584                            date: 11,
6585                        }),
6586                    },
6587                )),
6588            },
6589            proto::Update {
6590                seq: Some(3),
6591                date: Some(12),
6592                update: Some(proto::update::Update::UpdateReadMaxId(
6593                    proto::UpdateReadMaxId {
6594                        peer_id: Some(peer.clone()),
6595                        read_max_id: 11,
6596                        unread_count: 1,
6597                    },
6598                )),
6599            },
6600            proto::Update {
6601                seq: Some(4),
6602                date: Some(13),
6603                update: Some(proto::update::Update::ParticipantAdd(
6604                    proto::UpdateChatParticipantAdd {
6605                        chat_id: 7,
6606                        participant: Some(proto::ChatParticipant {
6607                            user_id: 2,
6608                            date: 13,
6609                        }),
6610                    },
6611                )),
6612            },
6613            proto::Update {
6614                seq: Some(5),
6615                date: Some(14),
6616                update: Some(proto::update::Update::DeleteReaction(
6617                    proto::UpdateDeleteReaction {
6618                        emoji: "👍".to_owned(),
6619                        chat_id: 7,
6620                        message_id: 11,
6621                        user_id: 2,
6622                    },
6623                )),
6624            },
6625            proto::Update {
6626                seq: Some(6),
6627                date: Some(15),
6628                update: Some(proto::update::Update::DeleteMessages(
6629                    proto::UpdateDeleteMessages {
6630                        message_ids: vec![11],
6631                        peer_id: Some(peer),
6632                    },
6633                )),
6634            },
6635        ];
6636
6637        let events = backend.apply_updates(updates, None, None).await.unwrap();
6638
6639        assert!(events.iter().any(|event| matches!(
6640            event,
6641            ClientEvent::MessageDeleted { chat_id, message_id }
6642                if *chat_id == InlineId::new(7) && *message_id == InlineId::new(11)
6643        )));
6644        assert!(
6645            store
6646                .message_deleted(InlineId::new(7), InlineId::new(11))
6647                .await
6648                .unwrap()
6649        );
6650        assert!(
6651            store
6652                .reactions(InlineId::new(7), InlineId::new(11))
6653                .await
6654                .unwrap()
6655                .is_empty()
6656        );
6657        assert_eq!(
6658            store.read_state(InlineId::new(7)).await.unwrap(),
6659            Some(StoredReadState {
6660                chat_id: InlineId::new(7),
6661                read_max_id: Some(InlineId::new(11)),
6662                unread_count: Some(1),
6663                marked_unread: false,
6664            })
6665        );
6666        assert_eq!(
6667            store.chat_participants(InlineId::new(7)).await.unwrap(),
6668            vec![ChatParticipantRecord {
6669                user_id: InlineId::new(2),
6670                date: Some(13),
6671            }]
6672        );
6673    }
6674
6675    #[tokio::test]
6676    async fn apply_updates_persists_dialog_metadata_and_attachment_invalidation() {
6677        let store = InMemoryStore::new();
6678        let backend = SdkBackend::builder().store(store.clone()).build().unwrap();
6679        let peer = proto::Peer {
6680            r#type: Some(proto::peer::Type::Chat(proto::PeerChat { chat_id: 7 })),
6681        };
6682        store
6683            .record_dialog(DialogRecord::new(InlineId::new(7)))
6684            .await
6685            .unwrap();
6686        let updates = vec![
6687            proto::Update {
6688                update: Some(proto::update::Update::ChatInfo(proto::UpdateChatInfo {
6689                    chat_id: 7,
6690                    title: Some("General".to_owned()),
6691                    emoji: None,
6692                    untitled: None,
6693                })),
6694                ..Default::default()
6695            },
6696            proto::Update {
6697                update: Some(proto::update::Update::ChatInfo(proto::UpdateChatInfo {
6698                    chat_id: 7,
6699                    title: None,
6700                    emoji: Some("✨".to_owned()),
6701                    untitled: None,
6702                })),
6703                ..Default::default()
6704            },
6705            proto::Update {
6706                update: Some(proto::update::Update::ChatVisibility(
6707                    proto::UpdateChatVisibility {
6708                        chat_id: 7,
6709                        is_public: true,
6710                    },
6711                )),
6712                ..Default::default()
6713            },
6714            proto::Update {
6715                update: Some(proto::update::Update::DialogArchived(
6716                    proto::UpdateDialogArchived {
6717                        peer_id: Some(peer.clone()),
6718                        archived: true,
6719                    },
6720                )),
6721                ..Default::default()
6722            },
6723            proto::Update {
6724                update: Some(proto::update::Update::DialogNotificationSettings(
6725                    proto::UpdateDialogNotificationSettings {
6726                        peer_id: Some(peer.clone()),
6727                        notification_settings: Some(proto::DialogNotificationSettings {
6728                            mode: Some(proto::dialog_notification_settings::Mode::Mentions as i32),
6729                        }),
6730                    },
6731                )),
6732                ..Default::default()
6733            },
6734            proto::Update {
6735                update: Some(proto::update::Update::DialogFollowMode(
6736                    proto::UpdateDialogFollowMode {
6737                        peer_id: Some(peer.clone()),
6738                        follow_mode: Some(proto::DialogFollowMode::Following as i32),
6739                    },
6740                )),
6741                ..Default::default()
6742            },
6743            proto::Update {
6744                update: Some(proto::update::Update::PinnedMessages(
6745                    proto::UpdatePinnedMessages {
6746                        peer_id: Some(peer.clone()),
6747                        message_ids: vec![11, 10],
6748                    },
6749                )),
6750                ..Default::default()
6751            },
6752            proto::Update {
6753                update: Some(proto::update::Update::MessageAttachment(
6754                    proto::UpdateMessageAttachment {
6755                        attachment: None,
6756                        message_id: 11,
6757                        peer_id: Some(peer),
6758                        chat_id: 7,
6759                    },
6760                )),
6761                ..Default::default()
6762            },
6763        ];
6764
6765        let events = backend.apply_updates(updates, None, None).await.unwrap();
6766        let dialog = store.dialog(InlineId::new(7)).await.unwrap().unwrap();
6767        assert_eq!(dialog.title.as_deref(), Some("General"));
6768        assert_eq!(dialog.emoji.as_deref(), Some("✨"));
6769        assert_eq!(dialog.is_public, Some(true));
6770        assert_eq!(dialog.archived, Some(true));
6771        assert_eq!(
6772            dialog.notification_mode,
6773            Some(DialogNotificationMode::Mentions)
6774        );
6775        assert_eq!(dialog.follow_mode, Some(DialogFollowMode::Following));
6776        assert_eq!(
6777            dialog.pinned_message_ids,
6778            vec![InlineId::new(11), InlineId::new(10)]
6779        );
6780        assert!(events.iter().any(|event| matches!(
6781            event,
6782            ClientEvent::MessageUpserted { chat_id, message_id }
6783                if *chat_id == InlineId::new(7) && *message_id == InlineId::new(11)
6784        )));
6785    }
6786
6787    #[tokio::test]
6788    async fn apply_updates_rejects_still_unsupported_lossless_update() {
6789        let backend = SdkBackend::builder().build().unwrap();
6790        let update = proto::Update {
6791            seq: Some(1),
6792            date: Some(10),
6793            update: Some(proto::update::Update::ChatHasNewUpdates(
6794                proto::UpdateChatHasNewUpdates::default(),
6795            )),
6796        };
6797
6798        let error = backend
6799            .apply_updates(vec![update], None, None)
6800            .await
6801            .unwrap_err();
6802
6803        assert_eq!(error.category, ClientErrorCategory::Unsupported);
6804        assert!(error.message.contains("chat_has_new_updates"));
6805    }
6806
6807    #[tokio::test]
6808    async fn apply_updates_clears_history_and_applies_reply_thread_side_effects() {
6809        let store = InMemoryStore::new();
6810        for chat_id in [7, 8, 9] {
6811            store
6812                .record_dialog(DialogRecord::new(InlineId::new(chat_id)))
6813                .await
6814                .unwrap();
6815        }
6816        for (message_id, timestamp) in [(10, 100), (11, 200)] {
6817            store
6818                .record_message(MessageRecord {
6819                    chat_id: InlineId::new(7),
6820                    message_id: InlineId::new(message_id),
6821                    sender_id: InlineId::new(2),
6822                    timestamp,
6823                    is_outgoing: false,
6824                    content: MessageContent::Text {
6825                        text: format!("message {message_id}"),
6826                    },
6827                    reply_to_message_id: None,
6828                    transaction: None,
6829                })
6830                .await
6831                .unwrap();
6832        }
6833        let backend = SdkBackend::builder().store(store.clone()).build().unwrap();
6834        let peer = proto::Peer {
6835            r#type: Some(proto::peer::Type::Chat(proto::PeerChat { chat_id: 7 })),
6836        };
6837        let update = proto::Update {
6838            seq: Some(1),
6839            date: Some(201),
6840            update: Some(proto::update::Update::ClearChatHistory(
6841                proto::UpdateClearChatHistory {
6842                    target: Some(proto::update_clear_chat_history::Target::PeerId(peer)),
6843                    before_date: Some(150),
6844                    delete_reply_threads: true,
6845                    deleted_chat_ids: vec![8],
6846                    orphaned_chat_ids: vec![9],
6847                    detached_chat_ids: Vec::new(),
6848                },
6849            )),
6850        };
6851
6852        let events = backend
6853            .apply_updates(vec![update], None, None)
6854            .await
6855            .unwrap();
6856
6857        assert!(events.iter().any(|event| matches!(
6858            event,
6859            ClientEvent::ChatHistoryCleared { chat_id, before_date: Some(150) }
6860                if *chat_id == InlineId::new(7)
6861        )));
6862        assert!(events.iter().any(|event| matches!(
6863            event,
6864            ClientEvent::ChatDeleted { chat_id } if *chat_id == InlineId::new(8)
6865        )));
6866        assert!(events.iter().any(|event| matches!(
6867            event,
6868            ClientEvent::ChatUpserted { chat_id } if *chat_id == InlineId::new(9)
6869        )));
6870        assert!(store.dialog(InlineId::new(8)).await.unwrap().is_none());
6871        let history = store
6872            .history(HistoryRequest {
6873                chat_id: InlineId::new(7),
6874                limit: Some(10),
6875                before_message_id: None,
6876                after_message_id: None,
6877            })
6878            .await
6879            .unwrap();
6880        assert_eq!(history.messages[0].message_id, InlineId::new(11));
6881    }
6882
6883    #[tokio::test]
6884    async fn apply_updates_persists_spaces_members_and_user_settings() {
6885        let store = InMemoryStore::new();
6886        let backend = SdkBackend::builder().store(store.clone()).build().unwrap();
6887        let member = |user_id| proto::Member {
6888            id: user_id,
6889            space_id: 5,
6890            user_id,
6891            role: Some(proto::member::Role::Member as i32),
6892            date: 100 + user_id,
6893            can_access_public_chats: true,
6894        };
6895        let updates = vec![
6896            proto::Update {
6897                update: Some(proto::update::Update::JoinSpace(proto::UpdateJoinSpace {
6898                    space: Some(proto::Space {
6899                        id: 5,
6900                        name: "Engineering".to_owned(),
6901                        creator: true,
6902                        date: 100,
6903                        is_public: Some(false),
6904                    }),
6905                    member: Some(member(2)),
6906                })),
6907                ..Default::default()
6908            },
6909            proto::Update {
6910                update: Some(proto::update::Update::SpaceMemberAdd(
6911                    proto::UpdateSpaceMemberAdd {
6912                        member: Some(member(3)),
6913                        user: Some(proto::User {
6914                            id: 3,
6915                            first_name: Some("Ada".to_owned()),
6916                            ..Default::default()
6917                        }),
6918                    },
6919                )),
6920                ..Default::default()
6921            },
6922            proto::Update {
6923                update: Some(proto::update::Update::SpaceMemberDelete(
6924                    proto::UpdateSpaceMemberDelete {
6925                        space_id: 5,
6926                        user_id: 3,
6927                    },
6928                )),
6929                ..Default::default()
6930            },
6931            proto::Update {
6932                update: Some(proto::update::Update::UpdateUserSettings(
6933                    proto::UpdateUserSettings {
6934                        settings: Some(proto::UserSettings {
6935                            notification_settings: Some(proto::NotificationSettings {
6936                                mode: Some(proto::notification_settings::Mode::Mentions as i32),
6937                                silent: Some(true),
6938                                ..Default::default()
6939                            }),
6940                        }),
6941                    },
6942                )),
6943                ..Default::default()
6944            },
6945        ];
6946
6947        let events = backend.apply_updates(updates, None, None).await.unwrap();
6948
6949        assert_eq!(
6950            store.space(InlineId::new(5)).await.unwrap().unwrap().name,
6951            "Engineering"
6952        );
6953        assert_eq!(
6954            store.space_members(InlineId::new(5)).await.unwrap(),
6955            vec![SpaceMemberRecord {
6956                space_id: InlineId::new(5),
6957                user_id: InlineId::new(2),
6958                role: Some(SpaceMemberRole::Member),
6959                date: 102,
6960                can_access_public_chats: true,
6961            }]
6962        );
6963        assert_eq!(
6964            store
6965                .user_settings()
6966                .await
6967                .unwrap()
6968                .unwrap()
6969                .notification_mode,
6970            Some(NotificationMode::Mentions)
6971        );
6972        assert!(events.iter().any(|event| matches!(
6973            event,
6974            ClientEvent::SpaceUpserted { space_id } if *space_id == InlineId::new(5)
6975        )));
6976        assert!(
6977            events
6978                .iter()
6979                .any(|event| matches!(event, ClientEvent::UserSettingsChanged { .. }))
6980        );
6981    }
6982
6983    #[test]
6984    fn rest_rate_limit_errors_keep_category_and_retry_hint() {
6985        let error = api_error_to_backend(ApiError::Status {
6986            status: 420,
6987            message: "retry after 23 seconds".to_owned(),
6988            body: None,
6989        });
6990
6991        assert_eq!(error.category, ClientErrorCategory::RateLimited);
6992        assert_eq!(error.retry_after_seconds, Some(23));
6993    }
6994
6995    fn connect_session() -> StoredSession {
6996        StoredSession {
6997            auth: AuthCredential::AccessToken {
6998                token: AuthToken::try_new("secret-token").unwrap(),
6999            },
7000            account_namespace: Some("team".to_owned()),
7001        }
7002    }
7003
7004    fn test_chat_id_from_input_peer(peer: Option<&proto::InputPeer>) -> Option<i64> {
7005        match peer?.r#type.as_ref()? {
7006            proto::input_peer::Type::Chat(chat) => Some(chat.chat_id),
7007            proto::input_peer::Type::Self_(_) | proto::input_peer::Type::User(_) => None,
7008        }
7009    }
7010
7011    async fn read_test_client_message(
7012        ws: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
7013    ) -> proto::ClientMessage {
7014        match ws.next().await.unwrap().unwrap() {
7015            WsMessage::Binary(data) => proto::ClientMessage::decode(&*data).unwrap(),
7016            other => panic!("expected binary client message, got {other:?}"),
7017        }
7018    }
7019
7020    async fn send_test_server_message(
7021        ws: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
7022        message: proto::ServerProtocolMessage,
7023    ) {
7024        ws.send(WsMessage::Binary(message.encode_to_vec()))
7025            .await
7026            .unwrap();
7027    }
7028
7029    fn rpc_result_message(
7030        id: u64,
7031        request_id: u64,
7032        result: proto::rpc_result::Result,
7033    ) -> proto::ServerProtocolMessage {
7034        proto::ServerProtocolMessage {
7035            id,
7036            body: Some(proto::server_protocol_message::Body::RpcResult(
7037                proto::RpcResult {
7038                    req_msg_id: request_id,
7039                    result: Some(result),
7040                },
7041            )),
7042        }
7043    }
7044}