Skip to main content

inline_client/
runtime.rs

1//! Async client facade and runner skeleton.
2//!
3//! This module establishes the TDLib-style host shape: cheap cloneable handle,
4//! single owner runner, bounded command queue, and broadcast committed events.
5//! Transport, sync, store, and transaction managers plug into this runner
6//! instead of being spread across bridges or agents.
7
8use std::sync::Arc;
9
10use tokio::sync::{broadcast, mpsc, oneshot, watch};
11
12use crate::{
13    AuthStartRequest, AuthStartResult, AuthVerifyRequest, AuthVerifyResult, BackendError,
14    BackendResult, ChatParticipantsPage, ChatParticipantsRequest, ClientBackend,
15    ClientErrorCategory, ClientEvent, ClientFailure, ClientStatus, ClientStatusSnapshot,
16    ConnectRequest, CreateDmRequest, CreateReplyThreadRequest, CreateThreadRequest, CreatedChat,
17    DeleteMessageRequest, DialogsPage, DialogsRequest, EditMessageRequest, HistoryPage,
18    HistoryRequest, InMemoryBackend, MessageMutation, OperationOutcome, ReactRequest, ReadRequest,
19    SendTextOutcome, SendTextRequest, TypingRequest, UploadRequest,
20};
21
22/// Default bounded command queue capacity.
23pub const DEFAULT_COMMAND_QUEUE_CAPACITY: usize = 128;
24
25/// Default broadcast event queue capacity.
26pub const DEFAULT_EVENT_QUEUE_CAPACITY: usize = 1024;
27
28/// Errors returned by the async client handle before an operation reaches the backend.
29#[derive(Debug, thiserror::Error)]
30#[non_exhaustive]
31pub enum ClientCommandError {
32    /// The client runner has stopped accepting commands.
33    #[error("inline client runner is closed")]
34    Closed,
35
36    /// The client runner dropped a command response before completing it.
37    #[error("inline client runner dropped command response")]
38    ResponseDropped,
39}
40
41/// Errors returned by typed client operations.
42#[derive(Debug, thiserror::Error)]
43#[non_exhaustive]
44pub enum ClientRequestError {
45    /// The operation could not be delivered to the runner.
46    #[error(transparent)]
47    Command(#[from] ClientCommandError),
48
49    /// The backend rejected or failed the operation.
50    #[error(transparent)]
51    Backend(#[from] BackendError),
52}
53
54/// Builder for an [`InlineClient`] runtime.
55#[derive(Clone, Debug)]
56pub struct InlineClientBuilder {
57    command_queue_capacity: usize,
58    event_queue_capacity: usize,
59    initial_status: ClientStatus,
60    backend: Arc<dyn ClientBackend>,
61}
62
63impl InlineClientBuilder {
64    /// Sets the backend used by the runner.
65    pub fn backend(mut self, backend: impl ClientBackend) -> Self {
66        self.backend = Arc::new(backend);
67        self
68    }
69
70    /// Sets the shared backend used by the runner.
71    pub fn shared_backend(mut self, backend: Arc<dyn ClientBackend>) -> Self {
72        self.backend = backend;
73        self
74    }
75
76    /// Sets the bounded command queue capacity.
77    pub fn command_queue_capacity(mut self, capacity: usize) -> Self {
78        self.command_queue_capacity = capacity.max(1);
79        self
80    }
81
82    /// Sets the broadcast event queue capacity.
83    pub fn event_queue_capacity(mut self, capacity: usize) -> Self {
84        self.event_queue_capacity = capacity.max(1);
85        self
86    }
87
88    /// Sets the initial client status.
89    pub fn initial_status(mut self, status: ClientStatus) -> Self {
90        self.initial_status = status;
91        self
92    }
93
94    /// Builds a client handle and runner pair.
95    pub fn build(self) -> InlineClientRuntime {
96        let (command_tx, command_rx) = mpsc::channel(self.command_queue_capacity);
97        let (event_tx, _) = broadcast::channel(self.event_queue_capacity);
98        let (status_tx, status_rx) = watch::channel(self.initial_status);
99        let (failure_tx, failure_rx) = watch::channel(None);
100
101        let client = InlineClient {
102            command_tx,
103            event_tx: event_tx.clone(),
104            status_rx,
105            failure_rx,
106        };
107        let runner = ClientRunner {
108            command_rx,
109            event_tx,
110            status_tx,
111            failure_tx,
112            status: self.initial_status,
113            failure: None,
114            backend: self.backend,
115        };
116
117        InlineClientRuntime { client, runner }
118    }
119}
120
121impl Default for InlineClientBuilder {
122    fn default() -> Self {
123        Self {
124            command_queue_capacity: DEFAULT_COMMAND_QUEUE_CAPACITY,
125            event_queue_capacity: DEFAULT_EVENT_QUEUE_CAPACITY,
126            initial_status: ClientStatus::Disconnected,
127            backend: Arc::new(InMemoryBackend::default()),
128        }
129    }
130}
131
132/// Built client runtime before the runner is hosted.
133#[derive(Debug)]
134pub struct InlineClientRuntime {
135    /// Cloneable client handle.
136    pub client: InlineClient,
137    /// Single-owner client runner.
138    pub runner: ClientRunner,
139}
140
141impl InlineClientRuntime {
142    /// Splits the runtime into handle and runner.
143    pub fn split(self) -> (InlineClient, ClientRunner) {
144        (self.client, self.runner)
145    }
146
147    /// Spawns the runner on the current Tokio runtime and returns the handle.
148    pub fn spawn(self) -> InlineClient {
149        let (client, runner) = self.split();
150        tokio::spawn(runner.run());
151        client
152    }
153}
154
155/// Cheap cloneable handle for apps, bridges, agents, and tests.
156#[derive(Clone, Debug)]
157pub struct InlineClient {
158    command_tx: mpsc::Sender<ClientCommand>,
159    event_tx: broadcast::Sender<ClientEvent>,
160    status_rx: watch::Receiver<ClientStatus>,
161    failure_rx: watch::Receiver<Option<ClientFailure>>,
162}
163
164impl InlineClient {
165    /// Creates a default client runtime builder.
166    pub fn builder() -> InlineClientBuilder {
167        InlineClientBuilder::default()
168    }
169
170    /// Returns the latest observed client status.
171    pub fn status(&self) -> ClientStatus {
172        *self.status_rx.borrow()
173    }
174
175    /// Returns the latest observed status snapshot.
176    pub fn status_snapshot(&self) -> ClientStatusSnapshot {
177        ClientStatusSnapshot {
178            status: *self.status_rx.borrow(),
179            failure: self.failure_rx.borrow().clone(),
180        }
181    }
182
183    /// Subscribes to committed client events.
184    pub fn subscribe(&self) -> broadcast::Receiver<ClientEvent> {
185        self.event_tx.subscribe()
186    }
187
188    /// Sends an Inline login code.
189    pub async fn auth_start(
190        &self,
191        request: AuthStartRequest,
192    ) -> Result<AuthStartResult, ClientRequestError> {
193        match self.request(ClientRequest::AuthStart(request)).await? {
194            ClientResponse::AuthStart(result) => Ok(result),
195            other => unreachable!("auth_start returned {other:?}"),
196        }
197    }
198
199    /// Verifies an Inline login code and persists the resulting session.
200    pub async fn auth_verify(
201        &self,
202        request: AuthVerifyRequest,
203    ) -> Result<AuthVerifyResult, ClientRequestError> {
204        match self.request(ClientRequest::AuthVerify(request)).await? {
205            ClientResponse::AuthVerify(result) => Ok(result),
206            other => unreachable!("auth_verify returned {other:?}"),
207        }
208    }
209
210    /// Resumes a previously stored session, if available.
211    pub async fn resume_session(&self) -> Result<ClientStatusSnapshot, ClientRequestError> {
212        match self.request(ClientRequest::Resume).await? {
213            ClientResponse::Status(status) => Ok(status),
214            other => unreachable!("resume_session returned {other:?}"),
215        }
216    }
217
218    /// Connects or reconnects the client.
219    pub async fn connect(
220        &self,
221        request: ConnectRequest,
222    ) -> Result<ClientStatusSnapshot, ClientRequestError> {
223        match self.request(ClientRequest::Connect(request)).await? {
224            ClientResponse::Status(status) => Ok(status),
225            other => unreachable!("connect returned {other:?}"),
226        }
227    }
228
229    /// Logs out the current account.
230    pub async fn logout(&self) -> Result<(), ClientRequestError> {
231        match self.request(ClientRequest::Logout).await? {
232            ClientResponse::Empty => Ok(()),
233            other => unreachable!("logout returned {other:?}"),
234        }
235    }
236
237    /// Lists dialogs.
238    pub async fn dialogs(
239        &self,
240        request: DialogsRequest,
241    ) -> Result<DialogsPage, ClientRequestError> {
242        match self.request(ClientRequest::Dialogs(request)).await? {
243            ClientResponse::Dialogs(page) => Ok(page),
244            other => unreachable!("dialogs returned {other:?}"),
245        }
246    }
247
248    /// Fetches chat history.
249    pub async fn history(
250        &self,
251        request: HistoryRequest,
252    ) -> Result<HistoryPage, ClientRequestError> {
253        match self.request(ClientRequest::History(request)).await? {
254            ClientResponse::History(page) => Ok(page),
255            other => unreachable!("history returned {other:?}"),
256        }
257    }
258
259    /// Fetches chat participants.
260    pub async fn chat_participants(
261        &self,
262        request: ChatParticipantsRequest,
263    ) -> Result<ChatParticipantsPage, ClientRequestError> {
264        match self
265            .request(ClientRequest::ChatParticipants(request))
266            .await?
267        {
268            ClientResponse::ChatParticipants(page) => Ok(page),
269            other => unreachable!("chat_participants returned {other:?}"),
270        }
271    }
272
273    /// Creates or opens a direct message chat.
274    pub async fn create_dm(
275        &self,
276        request: CreateDmRequest,
277    ) -> Result<CreatedChat, ClientRequestError> {
278        match self.request(ClientRequest::CreateDm(request)).await? {
279            ClientResponse::CreatedChat(chat) => Ok(chat),
280            other => unreachable!("create_dm returned {other:?}"),
281        }
282    }
283
284    /// Creates a regular Inline thread chat.
285    pub async fn create_thread(
286        &self,
287        request: CreateThreadRequest,
288    ) -> Result<CreatedChat, ClientRequestError> {
289        match self.request(ClientRequest::CreateThread(request)).await? {
290            ClientResponse::CreatedChat(chat) => Ok(chat),
291            other => unreachable!("create_thread returned {other:?}"),
292        }
293    }
294
295    /// Creates a child/reply Inline thread chat.
296    pub async fn create_reply_thread(
297        &self,
298        request: CreateReplyThreadRequest,
299    ) -> Result<CreatedChat, ClientRequestError> {
300        match self
301            .request(ClientRequest::CreateReplyThread(request))
302            .await?
303        {
304            ClientResponse::CreatedChat(chat) => Ok(chat),
305            other => unreachable!("create_reply_thread returned {other:?}"),
306        }
307    }
308
309    /// Sends a text message.
310    pub async fn send_text(
311        &self,
312        request: SendTextRequest,
313    ) -> Result<MessageMutation, ClientRequestError> {
314        match self.request(ClientRequest::SendText(request)).await? {
315            ClientResponse::Message(mutation) => Ok(mutation),
316            other => unreachable!("send_text returned {other:?}"),
317        }
318    }
319
320    /// Uploads and sends a media message.
321    pub async fn send_media(
322        &self,
323        request: UploadRequest,
324        bytes: Vec<u8>,
325    ) -> Result<MessageMutation, ClientRequestError> {
326        match self
327            .request(ClientRequest::SendMedia { request, bytes })
328            .await?
329        {
330            ClientResponse::Message(mutation) => Ok(mutation),
331            other => unreachable!("send_media returned {other:?}"),
332        }
333    }
334
335    /// Edits a text message.
336    pub async fn edit_message(
337        &self,
338        request: EditMessageRequest,
339    ) -> Result<(), ClientRequestError> {
340        match self.request(ClientRequest::EditMessage(request)).await? {
341            ClientResponse::Empty => Ok(()),
342            other => unreachable!("edit_message returned {other:?}"),
343        }
344    }
345
346    /// Deletes or unsends a message.
347    pub async fn delete_message(
348        &self,
349        request: DeleteMessageRequest,
350    ) -> Result<(), ClientRequestError> {
351        match self.request(ClientRequest::DeleteMessage(request)).await? {
352            ClientResponse::Empty => Ok(()),
353            other => unreachable!("delete_message returned {other:?}"),
354        }
355    }
356
357    /// Adds or removes a reaction.
358    pub async fn react(&self, request: ReactRequest) -> Result<(), ClientRequestError> {
359        match self.request(ClientRequest::React(request)).await? {
360            ClientResponse::Empty => Ok(()),
361            other => unreachable!("react returned {other:?}"),
362        }
363    }
364
365    /// Marks messages read.
366    pub async fn read(&self, request: ReadRequest) -> Result<(), ClientRequestError> {
367        match self.request(ClientRequest::Read(request)).await? {
368            ClientResponse::Empty => Ok(()),
369            other => unreachable!("read returned {other:?}"),
370        }
371    }
372
373    /// Sends a typing state.
374    pub async fn typing(&self, request: TypingRequest) -> Result<(), ClientRequestError> {
375        match self.request(ClientRequest::Typing(request)).await? {
376            ClientResponse::Empty => Ok(()),
377            other => unreachable!("typing returned {other:?}"),
378        }
379    }
380
381    /// Updates status through the runner.
382    ///
383    /// This is public so early hosts and tests can exercise the event/status
384    /// pipeline before the real transport manager is wired in. Future transport
385    /// code should call the same internal command path.
386    pub async fn set_status(
387        &self,
388        status: ClientStatus,
389        failure: Option<ClientFailure>,
390    ) -> Result<(), ClientCommandError> {
391        let (respond_to, response) = oneshot::channel();
392        self.command_tx
393            .send(ClientCommand::SetStatus {
394                status,
395                failure,
396                respond_to,
397            })
398            .await
399            .map_err(|_| ClientCommandError::Closed)?;
400        response
401            .await
402            .map_err(|_| ClientCommandError::ResponseDropped)
403    }
404
405    /// Requests runner shutdown.
406    pub async fn shutdown(&self) -> Result<(), ClientCommandError> {
407        let (respond_to, response) = oneshot::channel();
408        self.command_tx
409            .send(ClientCommand::Shutdown { respond_to })
410            .await
411            .map_err(|_| ClientCommandError::Closed)?;
412        response
413            .await
414            .map_err(|_| ClientCommandError::ResponseDropped)
415    }
416
417    async fn request(&self, request: ClientRequest) -> Result<ClientResponse, ClientRequestError> {
418        let (respond_to, response) = oneshot::channel();
419        self.command_tx
420            .send(ClientCommand::Request {
421                request: Box::new(request),
422                respond_to,
423            })
424            .await
425            .map_err(|_| ClientCommandError::Closed)?;
426        response
427            .await
428            .map_err(|_| ClientCommandError::ResponseDropped)?
429            .map_err(ClientRequestError::Backend)
430    }
431}
432
433/// Single-owner async client runner.
434#[derive(Debug)]
435pub struct ClientRunner {
436    command_rx: mpsc::Receiver<ClientCommand>,
437    event_tx: broadcast::Sender<ClientEvent>,
438    status_tx: watch::Sender<ClientStatus>,
439    failure_tx: watch::Sender<Option<ClientFailure>>,
440    status: ClientStatus,
441    failure: Option<ClientFailure>,
442    backend: Arc<dyn ClientBackend>,
443}
444
445impl ClientRunner {
446    /// Runs the client command loop until shutdown or all handles are dropped.
447    pub async fn run(mut self) {
448        log::debug!("inline client runner started");
449        while let Some(command) = self.command_rx.recv().await {
450            match command {
451                ClientCommand::Request {
452                    request,
453                    respond_to,
454                } => {
455                    let response = self.handle_request(*request).await;
456                    let _ = respond_to.send(response);
457                }
458                ClientCommand::SetStatus {
459                    status,
460                    failure,
461                    respond_to,
462                } => {
463                    self.update_status(status, failure);
464                    let _ = respond_to.send(());
465                }
466                ClientCommand::Shutdown { respond_to } => {
467                    log::debug!("inline client runner shutdown requested");
468                    self.update_status(ClientStatus::ShuttingDown, None);
469                    let _ = respond_to.send(());
470                    break;
471                }
472            }
473        }
474        log::debug!("inline client runner stopped");
475    }
476
477    async fn handle_request(&mut self, request: ClientRequest) -> BackendResult<ClientResponse> {
478        log::debug!("handling inline client request: {}", request.kind());
479        match request {
480            ClientRequest::AuthStart(auth) => self
481                .backend
482                .auth_start(auth)
483                .await
484                .map(ClientResponse::AuthStart),
485            ClientRequest::AuthVerify(auth) => {
486                let result = self.backend.auth_verify(auth).await?;
487                let status = result.status.clone();
488                self.update_status(status.status, status.failure.clone());
489                Ok(ClientResponse::AuthVerify(result))
490            }
491            ClientRequest::Resume => match self.backend.resume_session().await {
492                Ok(status) => {
493                    self.update_status(status.status, status.failure.clone());
494                    Ok(ClientResponse::Status(status))
495                }
496                Err(error) => {
497                    self.update_status_for_backend_error(&error);
498                    Err(error)
499                }
500            },
501            ClientRequest::Connect(connect) => {
502                let status = self.backend.connect(connect).await?;
503                self.update_status(status.status, status.failure.clone());
504                Ok(ClientResponse::Status(status))
505            }
506            ClientRequest::Logout => {
507                self.backend.logout().await?;
508                self.update_status(ClientStatus::Disconnected, None);
509                Ok(ClientResponse::Empty)
510            }
511            ClientRequest::Dialogs(dialogs) => self
512                .backend
513                .dialogs(dialogs)
514                .await
515                .map(ClientResponse::Dialogs),
516            ClientRequest::History(history) => self
517                .backend
518                .history(history)
519                .await
520                .map(ClientResponse::History),
521            ClientRequest::ChatParticipants(participants) => self
522                .backend
523                .chat_participants(participants)
524                .await
525                .map(ClientResponse::ChatParticipants),
526            ClientRequest::CreateDm(request) => self
527                .backend
528                .create_dm(request)
529                .await
530                .map(ClientResponse::CreatedChat),
531            ClientRequest::CreateThread(request) => self
532                .backend
533                .create_thread(request)
534                .await
535                .map(ClientResponse::CreatedChat),
536            ClientRequest::CreateReplyThread(request) => self
537                .backend
538                .create_reply_thread(request)
539                .await
540                .map(ClientResponse::CreatedChat),
541            ClientRequest::SendText(send) => {
542                let outcome = self.backend.send_text(send).await?;
543                Ok(ClientResponse::Message(self.emit_send_outcome(outcome)))
544            }
545            ClientRequest::SendMedia { request, bytes } => {
546                let outcome = self.backend.send_media(request, bytes).await?;
547                Ok(ClientResponse::Message(self.emit_send_outcome(outcome)))
548            }
549            ClientRequest::EditMessage(edit) => {
550                let outcome = self.backend.edit_message(edit).await?;
551                self.emit_operation_events(outcome);
552                Ok(ClientResponse::Empty)
553            }
554            ClientRequest::DeleteMessage(delete) => {
555                let outcome = self.backend.delete_message(delete).await?;
556                self.emit_operation_events(outcome);
557                Ok(ClientResponse::Empty)
558            }
559            ClientRequest::React(react) => {
560                let outcome = self.backend.react(react).await?;
561                self.emit_operation_events(outcome);
562                Ok(ClientResponse::Empty)
563            }
564            ClientRequest::Read(read) => {
565                let outcome = self.backend.read(read).await?;
566                self.emit_operation_events(outcome);
567                Ok(ClientResponse::Empty)
568            }
569            ClientRequest::Typing(typing) => {
570                let outcome = self.backend.typing(typing).await?;
571                self.emit_operation_events(outcome);
572                Ok(ClientResponse::Empty)
573            }
574        }
575    }
576
577    fn emit_operation_events(&self, outcome: OperationOutcome) {
578        for event in outcome.events {
579            let _ = self.event_tx.send(event);
580        }
581    }
582
583    fn emit_send_outcome(&self, outcome: SendTextOutcome) -> MessageMutation {
584        let transaction = outcome.transaction_event();
585        let message_id = outcome.message_id;
586        let chat_id = outcome.chat_id;
587        let message = outcome.message;
588        let mutation = outcome.mutation;
589        let _ = self
590            .event_tx
591            .send(ClientEvent::TransactionChanged(transaction));
592        if let Some(message_id) = message_id {
593            let _ = self.event_tx.send(ClientEvent::MessageUpserted {
594                chat_id,
595                message_id,
596            });
597        }
598        if let Some(message) = message {
599            let _ = self.event_tx.send(ClientEvent::MessageStored { message });
600        }
601        mutation
602    }
603
604    fn update_status_for_backend_error(&mut self, error: &BackendError) {
605        let status = match error.category {
606            ClientErrorCategory::AuthRequired => ClientStatus::AuthRequired,
607            ClientErrorCategory::AuthExpired => ClientStatus::AuthExpired,
608            ClientErrorCategory::Network => ClientStatus::Reconnecting,
609            _ => ClientStatus::Disconnected,
610        };
611        self.update_status(
612            status,
613            Some(ClientFailure::new(error.category, error.message.clone())),
614        );
615    }
616
617    fn update_status(&mut self, status: ClientStatus, failure: Option<ClientFailure>) {
618        log::debug!("inline client status changed: {status:?}");
619        self.status = status;
620        self.failure = failure.clone();
621        let _ = self.status_tx.send(status);
622        let _ = self.failure_tx.send(failure.clone());
623        let _ = self
624            .event_tx
625            .send(ClientEvent::StatusChanged { status, failure });
626    }
627}
628
629enum ClientCommand {
630    Request {
631        request: Box<ClientRequest>,
632        respond_to: oneshot::Sender<BackendResult<ClientResponse>>,
633    },
634    SetStatus {
635        status: ClientStatus,
636        failure: Option<ClientFailure>,
637        respond_to: oneshot::Sender<()>,
638    },
639    Shutdown {
640        respond_to: oneshot::Sender<()>,
641    },
642}
643
644impl std::fmt::Debug for ClientCommand {
645    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
646        match self {
647            Self::Request { request, .. } => f
648                .debug_struct("Request")
649                .field("request", request)
650                .finish_non_exhaustive(),
651            Self::SetStatus {
652                status, failure, ..
653            } => f
654                .debug_struct("SetStatus")
655                .field("status", status)
656                .field("failure", failure)
657                .finish_non_exhaustive(),
658            Self::Shutdown { .. } => f.debug_struct("Shutdown").finish_non_exhaustive(),
659        }
660    }
661}
662
663#[derive(Debug)]
664enum ClientRequest {
665    AuthStart(AuthStartRequest),
666    AuthVerify(AuthVerifyRequest),
667    Resume,
668    Connect(ConnectRequest),
669    Logout,
670    Dialogs(DialogsRequest),
671    History(HistoryRequest),
672    ChatParticipants(ChatParticipantsRequest),
673    CreateDm(CreateDmRequest),
674    CreateThread(CreateThreadRequest),
675    CreateReplyThread(CreateReplyThreadRequest),
676    SendText(SendTextRequest),
677    SendMedia {
678        request: UploadRequest,
679        bytes: Vec<u8>,
680    },
681    EditMessage(EditMessageRequest),
682    DeleteMessage(DeleteMessageRequest),
683    React(ReactRequest),
684    Read(ReadRequest),
685    Typing(TypingRequest),
686}
687
688impl ClientRequest {
689    const fn kind(&self) -> &'static str {
690        match self {
691            Self::AuthStart(_) => "auth_start",
692            Self::AuthVerify(_) => "auth_verify",
693            Self::Resume => "resume",
694            Self::Connect(_) => "connect",
695            Self::Logout => "logout",
696            Self::Dialogs(_) => "dialogs",
697            Self::History(_) => "history",
698            Self::ChatParticipants(_) => "chat_participants",
699            Self::CreateDm(_) => "create_dm",
700            Self::CreateThread(_) => "create_thread",
701            Self::CreateReplyThread(_) => "create_reply_thread",
702            Self::SendText(_) => "send_text",
703            Self::SendMedia { .. } => "send_media",
704            Self::EditMessage(_) => "edit_message",
705            Self::DeleteMessage(_) => "delete_message",
706            Self::React(_) => "react",
707            Self::Read(_) => "read",
708            Self::Typing(_) => "typing",
709        }
710    }
711}
712
713#[derive(Debug)]
714enum ClientResponse {
715    Empty,
716    Status(ClientStatusSnapshot),
717    AuthStart(AuthStartResult),
718    AuthVerify(AuthVerifyResult),
719    Dialogs(DialogsPage),
720    History(HistoryPage),
721    ChatParticipants(ChatParticipantsPage),
722    CreatedChat(CreatedChat),
723    Message(MessageMutation),
724}
725
726#[cfg(test)]
727mod tests {
728    use crate::{
729        AuthContactKind, AuthCredential, AuthToken, DialogRecord, HistoryRequest, InlineId,
730        MediaKind, MessageContent, PeerRef,
731    };
732
733    use super::*;
734
735    fn token_connect() -> ConnectRequest {
736        ConnectRequest::new(AuthCredential::AccessToken {
737            token: AuthToken::try_new("token").unwrap(),
738        })
739    }
740
741    fn auth_verify_request() -> AuthVerifyRequest {
742        AuthVerifyRequest {
743            contact: "mo@example.com".to_owned(),
744            kind: AuthContactKind::Email,
745            code: "123456".to_owned(),
746            challenge_token: None,
747            device_name: Some("inline-client test".to_owned()),
748            account_namespace: None,
749        }
750    }
751
752    #[tokio::test]
753    async fn status_snapshot_returns_current_status() {
754        let client = InlineClient::builder()
755            .initial_status(ClientStatus::Connected)
756            .build()
757            .spawn();
758
759        let status = client.status_snapshot();
760
761        assert_eq!(status.status, ClientStatus::Connected);
762        assert_eq!(status.failure, None);
763    }
764
765    #[tokio::test]
766    async fn set_status_emits_lossless_event() {
767        let client = InlineClient::builder().build().spawn();
768        let mut events = client.subscribe();
769
770        client
771            .set_status(
772                ClientStatus::AuthExpired,
773                Some(ClientFailure::new(
774                    ClientErrorCategory::AuthExpired,
775                    "relogin required",
776                )),
777            )
778            .await
779            .unwrap();
780
781        let event = events.recv().await.unwrap();
782        assert_eq!(event.reliability(), crate::EventReliability::Lossless);
783        assert!(matches!(
784            event,
785            ClientEvent::StatusChanged {
786                status: ClientStatus::AuthExpired,
787                ..
788            }
789        ));
790        assert_eq!(client.status(), ClientStatus::AuthExpired);
791        assert_eq!(
792            client.status_snapshot().failure.unwrap().category,
793            ClientErrorCategory::AuthExpired
794        );
795    }
796
797    #[tokio::test]
798    async fn connect_updates_status_and_emits_event() {
799        let client = InlineClient::builder().build().spawn();
800        let mut events = client.subscribe();
801
802        let status = client.connect(token_connect()).await.unwrap();
803
804        assert_eq!(status.status, ClientStatus::Connected);
805        assert_eq!(client.status(), ClientStatus::Connected);
806
807        let event = events.recv().await.unwrap();
808        assert!(matches!(
809            event,
810            ClientEvent::StatusChanged {
811                status: ClientStatus::Connected,
812                ..
813            }
814        ));
815    }
816
817    #[tokio::test]
818    async fn auth_start_and_verify_flow_through_backend() {
819        let client = InlineClient::builder().build().spawn();
820        let mut events = client.subscribe();
821
822        let started = client
823            .auth_start(AuthStartRequest {
824                contact: "mo@example.com".to_owned(),
825                kind: AuthContactKind::Email,
826                device_name: Some("inline-client test".to_owned()),
827            })
828            .await
829            .unwrap();
830        assert!(started.existing_user);
831        assert!(!started.needs_invite_code);
832
833        let verified = client.auth_verify(auth_verify_request()).await.unwrap();
834        assert_eq!(verified.user_id, InlineId::new(1));
835        assert_eq!(verified.account_namespace, "1");
836        assert_eq!(verified.status.status, ClientStatus::Connected);
837        assert_eq!(client.status(), ClientStatus::Connected);
838
839        let event = events.recv().await.unwrap();
840        assert!(matches!(
841            event,
842            ClientEvent::StatusChanged {
843                status: ClientStatus::Connected,
844                ..
845            }
846        ));
847    }
848
849    #[tokio::test]
850    async fn resume_without_session_reports_auth_required() {
851        let client = InlineClient::builder().build().spawn();
852        let mut events = client.subscribe();
853
854        let status = client.resume_session().await.unwrap();
855
856        assert_eq!(status.status, ClientStatus::AuthRequired);
857        assert_eq!(client.status(), ClientStatus::AuthRequired);
858
859        let event = events.recv().await.unwrap();
860        assert!(matches!(
861            event,
862            ClientEvent::StatusChanged {
863                status: ClientStatus::AuthRequired,
864                ..
865            }
866        ));
867    }
868
869    #[tokio::test]
870    async fn dialogs_and_history_flow_through_backend() {
871        let backend = InMemoryBackend::new();
872        backend.upsert_dialog(DialogRecord {
873            chat_id: InlineId::new(7),
874            peer_user_id: None,
875            title: Some("general".to_owned()),
876            last_message_id: None,
877            synced_through_message_id: None,
878            unread_count: Some(0),
879        });
880        backend.insert_message(crate::MessageRecord {
881            chat_id: InlineId::new(7),
882            message_id: InlineId::new(1),
883            sender_id: InlineId::new(2),
884            timestamp: 1,
885            is_outgoing: false,
886            content: MessageContent::Text {
887                text: "hello".to_owned(),
888            },
889            reply_to_message_id: None,
890            transaction: None,
891        });
892        let client = InlineClient::builder().backend(backend).build().spawn();
893        client.connect(token_connect()).await.unwrap();
894
895        let dialogs = client.dialogs(DialogsRequest::default()).await.unwrap();
896        assert_eq!(dialogs.dialogs.len(), 1);
897        assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
898
899        let history = client
900            .history(HistoryRequest {
901                chat_id: InlineId::new(7),
902                limit: Some(10),
903                before_message_id: None,
904                after_message_id: None,
905            })
906            .await
907            .unwrap();
908        assert_eq!(history.messages.len(), 1);
909        assert_eq!(history.messages[0].message_id, InlineId::new(1));
910    }
911
912    #[tokio::test]
913    async fn send_text_returns_mutation_and_emits_events() {
914        let client = InlineClient::builder().build().spawn();
915        client.connect(token_connect()).await.unwrap();
916        let mut events = client.subscribe();
917
918        let mutation = client
919            .send_text(SendTextRequest::new(
920                PeerRef::Chat {
921                    chat_id: InlineId::new(7),
922                },
923                "hello",
924            ))
925            .await
926            .unwrap();
927
928        assert_eq!(mutation.message_id, Some(InlineId::new(1)));
929        assert_eq!(
930            mutation.transaction.final_message_id,
931            Some(InlineId::new(1))
932        );
933
934        let events = [
935            events.recv().await.unwrap(),
936            events.recv().await.unwrap(),
937            events.recv().await.unwrap(),
938        ];
939        assert!(
940            events
941                .iter()
942                .any(|event| matches!(event, ClientEvent::TransactionChanged(_)))
943        );
944        assert!(events.iter().any(|event| matches!(
945            event,
946            ClientEvent::MessageUpserted {
947                chat_id: InlineId(7),
948                message_id: InlineId(1)
949            }
950        )));
951        assert!(events.iter().any(|event| matches!(
952            event,
953            ClientEvent::MessageStored { message }
954                if message.chat_id == InlineId::new(7)
955                    && message.message_id == InlineId::new(1)
956        )));
957    }
958
959    #[tokio::test]
960    async fn edit_message_emits_stored_upsert_event() {
961        let backend = InMemoryBackend::new();
962        backend.insert_message(crate::MessageRecord {
963            chat_id: InlineId::new(7),
964            message_id: InlineId::new(1),
965            sender_id: InlineId::new(2),
966            timestamp: 1,
967            is_outgoing: false,
968            content: MessageContent::Text {
969                text: "old".to_owned(),
970            },
971            reply_to_message_id: None,
972            transaction: None,
973        });
974        let client = InlineClient::builder().backend(backend).build().spawn();
975        client.connect(token_connect()).await.unwrap();
976        let mut events = client.subscribe();
977
978        client
979            .edit_message(EditMessageRequest {
980                chat_id: InlineId::new(7),
981                message_id: InlineId::new(1),
982                text: "edited".to_owned(),
983                external_id: None,
984            })
985            .await
986            .unwrap();
987
988        match events.recv().await.unwrap() {
989            ClientEvent::MessageStored { message } => {
990                assert_eq!(message.chat_id, InlineId::new(7));
991                assert_eq!(message.message_id, InlineId::new(1));
992                assert_eq!(
993                    message.content,
994                    MessageContent::Text {
995                        text: "edited".to_owned()
996                    }
997                );
998            }
999            other => panic!("unexpected event: {other:?}"),
1000        }
1001    }
1002
1003    #[tokio::test]
1004    async fn send_media_emits_message_events() {
1005        let client = InlineClient::builder().build().spawn();
1006        client.connect(token_connect()).await.unwrap();
1007        let mut events = client.subscribe();
1008
1009        let mutation = client
1010            .send_media(
1011                UploadRequest {
1012                    peer: PeerRef::Chat {
1013                        chat_id: InlineId::new(7),
1014                    },
1015                    kind: MediaKind::Photo,
1016                    file_name: Some("image.png".to_owned()),
1017                    mime_type: Some("image/png".to_owned()),
1018                    size_bytes: Some(4),
1019                    caption: Some("caption".to_owned()),
1020                    width: Some(10),
1021                    height: Some(10),
1022                    duration_ms: None,
1023                    external_id: None,
1024                    random_id: None,
1025                    reply_to_message_id: None,
1026                },
1027                vec![1, 2, 3, 4],
1028            )
1029            .await
1030            .unwrap();
1031
1032        assert_eq!(mutation.message_id, Some(InlineId::new(1)));
1033
1034        let _transaction = events.recv().await.unwrap();
1035        let _upsert = events.recv().await.unwrap();
1036        match events.recv().await.unwrap() {
1037            ClientEvent::MessageStored { message } => {
1038                assert_eq!(message.chat_id, InlineId::new(7));
1039                assert_eq!(message.message_id, InlineId::new(1));
1040                match message.content {
1041                    MessageContent::Media {
1042                        kind,
1043                        file_name,
1044                        caption,
1045                        ..
1046                    } => {
1047                        assert_eq!(kind, MediaKind::Photo);
1048                        assert_eq!(file_name.as_deref(), Some("image.png"));
1049                        assert_eq!(caption.as_deref(), Some("caption"));
1050                    }
1051                    other => panic!("unexpected content: {other:?}"),
1052                }
1053            }
1054            other => panic!("unexpected event: {other:?}"),
1055        }
1056    }
1057
1058    #[tokio::test]
1059    async fn shutdown_stops_runner() {
1060        let client = InlineClient::builder().build().spawn();
1061
1062        client.shutdown().await.unwrap();
1063        let err = client
1064            .dialogs(DialogsRequest::default())
1065            .await
1066            .expect_err("runner should reject commands after shutdown");
1067
1068        assert!(matches!(
1069            err,
1070            ClientRequestError::Command(
1071                ClientCommandError::Closed | ClientCommandError::ResponseDropped
1072            )
1073        ));
1074    }
1075}