1use 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
22pub const DEFAULT_COMMAND_QUEUE_CAPACITY: usize = 128;
24
25pub const DEFAULT_EVENT_QUEUE_CAPACITY: usize = 1024;
27
28#[derive(Debug, thiserror::Error)]
30#[non_exhaustive]
31pub enum ClientCommandError {
32 #[error("inline client runner is closed")]
34 Closed,
35
36 #[error("inline client runner dropped command response")]
38 ResponseDropped,
39}
40
41#[derive(Debug, thiserror::Error)]
43#[non_exhaustive]
44pub enum ClientRequestError {
45 #[error(transparent)]
47 Command(#[from] ClientCommandError),
48
49 #[error(transparent)]
51 Backend(#[from] BackendError),
52}
53
54#[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 pub fn backend(mut self, backend: impl ClientBackend) -> Self {
66 self.backend = Arc::new(backend);
67 self
68 }
69
70 pub fn shared_backend(mut self, backend: Arc<dyn ClientBackend>) -> Self {
72 self.backend = backend;
73 self
74 }
75
76 pub fn command_queue_capacity(mut self, capacity: usize) -> Self {
78 self.command_queue_capacity = capacity.max(1);
79 self
80 }
81
82 pub fn event_queue_capacity(mut self, capacity: usize) -> Self {
84 self.event_queue_capacity = capacity.max(1);
85 self
86 }
87
88 pub fn initial_status(mut self, status: ClientStatus) -> Self {
90 self.initial_status = status;
91 self
92 }
93
94 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#[derive(Debug)]
134pub struct InlineClientRuntime {
135 pub client: InlineClient,
137 pub runner: ClientRunner,
139}
140
141impl InlineClientRuntime {
142 pub fn split(self) -> (InlineClient, ClientRunner) {
144 (self.client, self.runner)
145 }
146
147 pub fn spawn(self) -> InlineClient {
149 let (client, runner) = self.split();
150 tokio::spawn(runner.run());
151 client
152 }
153}
154
155#[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 pub fn builder() -> InlineClientBuilder {
167 InlineClientBuilder::default()
168 }
169
170 pub fn status(&self) -> ClientStatus {
172 *self.status_rx.borrow()
173 }
174
175 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 pub fn subscribe(&self) -> broadcast::Receiver<ClientEvent> {
185 self.event_tx.subscribe()
186 }
187
188 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[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 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 title: Some("general".to_owned()),
875 last_message_id: None,
876 synced_through_message_id: None,
877 unread_count: Some(0),
878 });
879 backend.insert_message(crate::MessageRecord {
880 chat_id: InlineId::new(7),
881 message_id: InlineId::new(1),
882 sender_id: InlineId::new(2),
883 timestamp: 1,
884 is_outgoing: false,
885 content: MessageContent::Text {
886 text: "hello".to_owned(),
887 },
888 reply_to_message_id: None,
889 transaction: None,
890 });
891 let client = InlineClient::builder().backend(backend).build().spawn();
892 client.connect(token_connect()).await.unwrap();
893
894 let dialogs = client.dialogs(DialogsRequest::default()).await.unwrap();
895 assert_eq!(dialogs.dialogs.len(), 1);
896 assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
897
898 let history = client
899 .history(HistoryRequest {
900 chat_id: InlineId::new(7),
901 limit: Some(10),
902 before_message_id: None,
903 after_message_id: None,
904 })
905 .await
906 .unwrap();
907 assert_eq!(history.messages.len(), 1);
908 assert_eq!(history.messages[0].message_id, InlineId::new(1));
909 }
910
911 #[tokio::test]
912 async fn send_text_returns_mutation_and_emits_events() {
913 let client = InlineClient::builder().build().spawn();
914 client.connect(token_connect()).await.unwrap();
915 let mut events = client.subscribe();
916
917 let mutation = client
918 .send_text(SendTextRequest::new(
919 PeerRef::Chat {
920 chat_id: InlineId::new(7),
921 },
922 "hello",
923 ))
924 .await
925 .unwrap();
926
927 assert_eq!(mutation.message_id, Some(InlineId::new(1)));
928 assert_eq!(
929 mutation.transaction.final_message_id,
930 Some(InlineId::new(1))
931 );
932
933 let events = [
934 events.recv().await.unwrap(),
935 events.recv().await.unwrap(),
936 events.recv().await.unwrap(),
937 ];
938 assert!(
939 events
940 .iter()
941 .any(|event| matches!(event, ClientEvent::TransactionChanged(_)))
942 );
943 assert!(events.iter().any(|event| matches!(
944 event,
945 ClientEvent::MessageUpserted {
946 chat_id: InlineId(7),
947 message_id: InlineId(1)
948 }
949 )));
950 assert!(events.iter().any(|event| matches!(
951 event,
952 ClientEvent::MessageStored { message }
953 if message.chat_id == InlineId::new(7)
954 && message.message_id == InlineId::new(1)
955 )));
956 }
957
958 #[tokio::test]
959 async fn edit_message_emits_stored_upsert_event() {
960 let backend = InMemoryBackend::new();
961 backend.insert_message(crate::MessageRecord {
962 chat_id: InlineId::new(7),
963 message_id: InlineId::new(1),
964 sender_id: InlineId::new(2),
965 timestamp: 1,
966 is_outgoing: false,
967 content: MessageContent::Text {
968 text: "old".to_owned(),
969 },
970 reply_to_message_id: None,
971 transaction: None,
972 });
973 let client = InlineClient::builder().backend(backend).build().spawn();
974 client.connect(token_connect()).await.unwrap();
975 let mut events = client.subscribe();
976
977 client
978 .edit_message(EditMessageRequest {
979 chat_id: InlineId::new(7),
980 message_id: InlineId::new(1),
981 text: "edited".to_owned(),
982 external_id: None,
983 })
984 .await
985 .unwrap();
986
987 match events.recv().await.unwrap() {
988 ClientEvent::MessageStored { message } => {
989 assert_eq!(message.chat_id, InlineId::new(7));
990 assert_eq!(message.message_id, InlineId::new(1));
991 assert_eq!(
992 message.content,
993 MessageContent::Text {
994 text: "edited".to_owned()
995 }
996 );
997 }
998 other => panic!("unexpected event: {other:?}"),
999 }
1000 }
1001
1002 #[tokio::test]
1003 async fn send_media_emits_message_events() {
1004 let client = InlineClient::builder().build().spawn();
1005 client.connect(token_connect()).await.unwrap();
1006 let mut events = client.subscribe();
1007
1008 let mutation = client
1009 .send_media(
1010 UploadRequest {
1011 peer: PeerRef::Chat {
1012 chat_id: InlineId::new(7),
1013 },
1014 kind: MediaKind::Photo,
1015 file_name: Some("image.png".to_owned()),
1016 mime_type: Some("image/png".to_owned()),
1017 size_bytes: Some(4),
1018 caption: Some("caption".to_owned()),
1019 width: Some(10),
1020 height: Some(10),
1021 duration_ms: None,
1022 external_id: None,
1023 random_id: None,
1024 reply_to_message_id: None,
1025 },
1026 vec![1, 2, 3, 4],
1027 )
1028 .await
1029 .unwrap();
1030
1031 assert_eq!(mutation.message_id, Some(InlineId::new(1)));
1032
1033 let _transaction = events.recv().await.unwrap();
1034 let _upsert = events.recv().await.unwrap();
1035 match events.recv().await.unwrap() {
1036 ClientEvent::MessageStored { message } => {
1037 assert_eq!(message.chat_id, InlineId::new(7));
1038 assert_eq!(message.message_id, InlineId::new(1));
1039 match message.content {
1040 MessageContent::Media {
1041 kind,
1042 file_name,
1043 caption,
1044 ..
1045 } => {
1046 assert_eq!(kind, MediaKind::Photo);
1047 assert_eq!(file_name.as_deref(), Some("image.png"));
1048 assert_eq!(caption.as_deref(), Some("caption"));
1049 }
1050 other => panic!("unexpected content: {other:?}"),
1051 }
1052 }
1053 other => panic!("unexpected event: {other:?}"),
1054 }
1055 }
1056
1057 #[tokio::test]
1058 async fn shutdown_stops_runner() {
1059 let client = InlineClient::builder().build().spawn();
1060
1061 client.shutdown().await.unwrap();
1062 let err = client
1063 .dialogs(DialogsRequest::default())
1064 .await
1065 .expect_err("runner should reject commands after shutdown");
1066
1067 assert!(matches!(
1068 err,
1069 ClientRequestError::Command(
1070 ClientCommandError::Closed | ClientCommandError::ResponseDropped
1071 )
1072 ));
1073 }
1074}