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