1use std::{sync::Arc, time::Duration};
9
10use tokio::sync::{broadcast, mpsc, oneshot, watch};
11use tokio::task::JoinHandle;
12
13use crate::{
14 AuthStartRequest, AuthStartResult, AuthVerifyRequest, AuthVerifyResult, BackendError,
15 BackendResult, ChatParticipantsPage, ChatParticipantsRequest, ClientBackend,
16 ClientErrorCategory, ClientEvent, ClientFailure, ClientStatus, ClientStatusSnapshot,
17 ConnectRequest, CreateDmRequest, CreateReplyThreadRequest, CreateThreadRequest, CreatedChat,
18 DeleteMessageRequest, DialogsPage, DialogsRequest, EditMessageRequest, HistoryPage,
19 HistoryRequest, InMemoryBackend, MessageMutation, OperationOutcome, ReactRequest, ReadRequest,
20 SendTextOutcome, SendTextRequest, TypingRequest, UploadRequest,
21};
22
23pub const DEFAULT_COMMAND_QUEUE_CAPACITY: usize = 128;
25
26pub const DEFAULT_EVENT_QUEUE_CAPACITY: usize = 1024;
28const EVENT_RETRY_DELAY: Duration = Duration::from_secs(5);
29const EVENT_RATE_LIMIT_RETRY_DELAY: Duration = Duration::from_secs(30);
30
31#[derive(Debug, thiserror::Error)]
33#[non_exhaustive]
34pub enum ClientCommandError {
35 #[error("inline client runner is closed")]
37 Closed,
38
39 #[error("inline client runner dropped command response")]
41 ResponseDropped,
42}
43
44#[derive(Debug, thiserror::Error)]
46#[non_exhaustive]
47pub enum ClientRequestError {
48 #[error(transparent)]
50 Command(#[from] ClientCommandError),
51
52 #[error(transparent)]
54 Backend(#[from] BackendError),
55}
56
57#[derive(Clone, Debug)]
59pub struct InlineClientBuilder {
60 command_queue_capacity: usize,
61 event_queue_capacity: usize,
62 initial_status: ClientStatus,
63 backend: Arc<dyn ClientBackend>,
64}
65
66impl InlineClientBuilder {
67 pub fn backend(mut self, backend: impl ClientBackend) -> Self {
69 self.backend = Arc::new(backend);
70 self
71 }
72
73 pub fn shared_backend(mut self, backend: Arc<dyn ClientBackend>) -> Self {
75 self.backend = backend;
76 self
77 }
78
79 pub fn command_queue_capacity(mut self, capacity: usize) -> Self {
81 self.command_queue_capacity = capacity.max(1);
82 self
83 }
84
85 pub fn event_queue_capacity(mut self, capacity: usize) -> Self {
87 self.event_queue_capacity = capacity.max(1);
88 self
89 }
90
91 pub fn initial_status(mut self, status: ClientStatus) -> Self {
93 self.initial_status = status;
94 self
95 }
96
97 pub fn build(self) -> InlineClientRuntime {
99 let (command_tx, command_rx) = mpsc::channel(self.command_queue_capacity);
100 let (event_tx, _) = broadcast::channel(self.event_queue_capacity);
101 let (status_tx, status_rx) = watch::channel(self.initial_status);
102 let (failure_tx, failure_rx) = watch::channel(None);
103 let (signal_tx, signal_rx) = mpsc::channel(self.command_queue_capacity);
104
105 let client = InlineClient {
106 command_tx,
107 event_tx: event_tx.clone(),
108 status_rx,
109 failure_rx,
110 };
111 let runner = ClientRunner {
112 command_rx,
113 event_tx,
114 status_tx,
115 failure_tx,
116 status: self.initial_status,
117 failure: None,
118 backend: self.backend,
119 signal_tx,
120 signal_rx,
121 event_task: None,
122 };
123
124 InlineClientRuntime { client, runner }
125 }
126}
127
128impl Default for InlineClientBuilder {
129 fn default() -> Self {
130 Self {
131 command_queue_capacity: DEFAULT_COMMAND_QUEUE_CAPACITY,
132 event_queue_capacity: DEFAULT_EVENT_QUEUE_CAPACITY,
133 initial_status: ClientStatus::Disconnected,
134 backend: Arc::new(InMemoryBackend::default()),
135 }
136 }
137}
138
139#[derive(Debug)]
141pub struct InlineClientRuntime {
142 pub client: InlineClient,
144 pub runner: ClientRunner,
146}
147
148impl InlineClientRuntime {
149 pub fn split(self) -> (InlineClient, ClientRunner) {
151 (self.client, self.runner)
152 }
153
154 pub fn spawn(self) -> InlineClient {
156 let (client, runner) = self.split();
157 tokio::spawn(runner.run());
158 client
159 }
160}
161
162#[derive(Clone, Debug)]
164pub struct InlineClient {
165 command_tx: mpsc::Sender<ClientCommand>,
166 event_tx: broadcast::Sender<ClientEvent>,
167 status_rx: watch::Receiver<ClientStatus>,
168 failure_rx: watch::Receiver<Option<ClientFailure>>,
169}
170
171impl InlineClient {
172 pub fn builder() -> InlineClientBuilder {
174 InlineClientBuilder::default()
175 }
176
177 pub fn status(&self) -> ClientStatus {
179 *self.status_rx.borrow()
180 }
181
182 pub fn status_snapshot(&self) -> ClientStatusSnapshot {
184 ClientStatusSnapshot {
185 status: *self.status_rx.borrow(),
186 failure: self.failure_rx.borrow().clone(),
187 }
188 }
189
190 pub fn subscribe(&self) -> broadcast::Receiver<ClientEvent> {
192 self.event_tx.subscribe()
193 }
194
195 pub async fn auth_start(
197 &self,
198 request: AuthStartRequest,
199 ) -> Result<AuthStartResult, ClientRequestError> {
200 match self.request(ClientRequest::AuthStart(request)).await? {
201 ClientResponse::AuthStart(result) => Ok(result),
202 other => unreachable!("auth_start returned {other:?}"),
203 }
204 }
205
206 pub async fn auth_verify(
208 &self,
209 request: AuthVerifyRequest,
210 ) -> Result<AuthVerifyResult, ClientRequestError> {
211 match self.request(ClientRequest::AuthVerify(request)).await? {
212 ClientResponse::AuthVerify(result) => Ok(result),
213 other => unreachable!("auth_verify returned {other:?}"),
214 }
215 }
216
217 pub async fn resume_session(&self) -> Result<ClientStatusSnapshot, ClientRequestError> {
219 match self.request(ClientRequest::Resume).await? {
220 ClientResponse::Status(status) => Ok(status),
221 other => unreachable!("resume_session returned {other:?}"),
222 }
223 }
224
225 pub async fn connect(
227 &self,
228 request: ConnectRequest,
229 ) -> Result<ClientStatusSnapshot, ClientRequestError> {
230 match self.request(ClientRequest::Connect(request)).await? {
231 ClientResponse::Status(status) => Ok(status),
232 other => unreachable!("connect returned {other:?}"),
233 }
234 }
235
236 pub async fn logout(&self) -> Result<(), ClientRequestError> {
238 match self.request(ClientRequest::Logout).await? {
239 ClientResponse::Empty => Ok(()),
240 other => unreachable!("logout returned {other:?}"),
241 }
242 }
243
244 pub async fn dialogs(
246 &self,
247 request: DialogsRequest,
248 ) -> Result<DialogsPage, ClientRequestError> {
249 match self.request(ClientRequest::Dialogs(request)).await? {
250 ClientResponse::Dialogs(page) => Ok(page),
251 other => unreachable!("dialogs returned {other:?}"),
252 }
253 }
254
255 pub async fn history(
257 &self,
258 request: HistoryRequest,
259 ) -> Result<HistoryPage, ClientRequestError> {
260 match self.request(ClientRequest::History(request)).await? {
261 ClientResponse::History(page) => Ok(page),
262 other => unreachable!("history returned {other:?}"),
263 }
264 }
265
266 pub async fn chat_participants(
268 &self,
269 request: ChatParticipantsRequest,
270 ) -> Result<ChatParticipantsPage, ClientRequestError> {
271 match self
272 .request(ClientRequest::ChatParticipants(request))
273 .await?
274 {
275 ClientResponse::ChatParticipants(page) => Ok(page),
276 other => unreachable!("chat_participants returned {other:?}"),
277 }
278 }
279
280 pub async fn create_dm(
282 &self,
283 request: CreateDmRequest,
284 ) -> Result<CreatedChat, ClientRequestError> {
285 match self.request(ClientRequest::CreateDm(request)).await? {
286 ClientResponse::CreatedChat(chat) => Ok(chat),
287 other => unreachable!("create_dm returned {other:?}"),
288 }
289 }
290
291 pub async fn create_thread(
293 &self,
294 request: CreateThreadRequest,
295 ) -> Result<CreatedChat, ClientRequestError> {
296 match self.request(ClientRequest::CreateThread(request)).await? {
297 ClientResponse::CreatedChat(chat) => Ok(chat),
298 other => unreachable!("create_thread returned {other:?}"),
299 }
300 }
301
302 pub async fn create_reply_thread(
304 &self,
305 request: CreateReplyThreadRequest,
306 ) -> Result<CreatedChat, ClientRequestError> {
307 match self
308 .request(ClientRequest::CreateReplyThread(request))
309 .await?
310 {
311 ClientResponse::CreatedChat(chat) => Ok(chat),
312 other => unreachable!("create_reply_thread returned {other:?}"),
313 }
314 }
315
316 pub async fn send_text(
318 &self,
319 request: SendTextRequest,
320 ) -> Result<MessageMutation, ClientRequestError> {
321 match self.request(ClientRequest::SendText(request)).await? {
322 ClientResponse::Message(mutation) => Ok(mutation),
323 other => unreachable!("send_text returned {other:?}"),
324 }
325 }
326
327 pub async fn send_media(
329 &self,
330 request: UploadRequest,
331 bytes: Vec<u8>,
332 ) -> Result<MessageMutation, ClientRequestError> {
333 match self
334 .request(ClientRequest::SendMedia { request, bytes })
335 .await?
336 {
337 ClientResponse::Message(mutation) => Ok(mutation),
338 other => unreachable!("send_media returned {other:?}"),
339 }
340 }
341
342 pub async fn edit_message(
344 &self,
345 request: EditMessageRequest,
346 ) -> Result<(), ClientRequestError> {
347 match self.request(ClientRequest::EditMessage(request)).await? {
348 ClientResponse::Empty => Ok(()),
349 other => unreachable!("edit_message returned {other:?}"),
350 }
351 }
352
353 pub async fn delete_message(
355 &self,
356 request: DeleteMessageRequest,
357 ) -> Result<(), ClientRequestError> {
358 match self.request(ClientRequest::DeleteMessage(request)).await? {
359 ClientResponse::Empty => Ok(()),
360 other => unreachable!("delete_message returned {other:?}"),
361 }
362 }
363
364 pub async fn react(&self, request: ReactRequest) -> Result<(), ClientRequestError> {
366 match self.request(ClientRequest::React(request)).await? {
367 ClientResponse::Empty => Ok(()),
368 other => unreachable!("react returned {other:?}"),
369 }
370 }
371
372 pub async fn read(&self, request: ReadRequest) -> Result<(), ClientRequestError> {
374 match self.request(ClientRequest::Read(request)).await? {
375 ClientResponse::Empty => Ok(()),
376 other => unreachable!("read returned {other:?}"),
377 }
378 }
379
380 pub async fn typing(&self, request: TypingRequest) -> Result<(), ClientRequestError> {
382 match self.request(ClientRequest::Typing(request)).await? {
383 ClientResponse::Empty => Ok(()),
384 other => unreachable!("typing returned {other:?}"),
385 }
386 }
387
388 pub async fn set_status(
394 &self,
395 status: ClientStatus,
396 failure: Option<ClientFailure>,
397 ) -> Result<(), ClientCommandError> {
398 let (respond_to, response) = oneshot::channel();
399 self.command_tx
400 .send(ClientCommand::SetStatus {
401 status,
402 failure,
403 respond_to,
404 })
405 .await
406 .map_err(|_| ClientCommandError::Closed)?;
407 response
408 .await
409 .map_err(|_| ClientCommandError::ResponseDropped)
410 }
411
412 pub async fn shutdown(&self) -> Result<(), ClientCommandError> {
414 let (respond_to, response) = oneshot::channel();
415 self.command_tx
416 .send(ClientCommand::Shutdown { respond_to })
417 .await
418 .map_err(|_| ClientCommandError::Closed)?;
419 response
420 .await
421 .map_err(|_| ClientCommandError::ResponseDropped)
422 }
423
424 async fn request(&self, request: ClientRequest) -> Result<ClientResponse, ClientRequestError> {
425 let (respond_to, response) = oneshot::channel();
426 self.command_tx
427 .send(ClientCommand::Request {
428 request: Box::new(request),
429 respond_to,
430 })
431 .await
432 .map_err(|_| ClientCommandError::Closed)?;
433 response
434 .await
435 .map_err(|_| ClientCommandError::ResponseDropped)?
436 .map_err(ClientRequestError::Backend)
437 }
438}
439
440#[derive(Debug)]
442pub struct ClientRunner {
443 command_rx: mpsc::Receiver<ClientCommand>,
444 event_tx: broadcast::Sender<ClientEvent>,
445 status_tx: watch::Sender<ClientStatus>,
446 failure_tx: watch::Sender<Option<ClientFailure>>,
447 status: ClientStatus,
448 failure: Option<ClientFailure>,
449 backend: Arc<dyn ClientBackend>,
450 signal_tx: mpsc::Sender<RunnerSignal>,
451 signal_rx: mpsc::Receiver<RunnerSignal>,
452 event_task: Option<JoinHandle<()>>,
453}
454
455impl ClientRunner {
456 pub async fn run(mut self) {
458 log::debug!("inline client runner started");
459 loop {
460 tokio::select! {
461 command = self.command_rx.recv() => {
462 if !self.handle_optional_command(command).await {
463 break;
464 }
465 }
466 signal = self.signal_rx.recv() => {
467 if let Some(signal) = signal {
468 self.handle_signal(signal);
469 }
470 }
471 }
472 }
473 self.stop_event_receiver();
474 log::debug!("inline client runner stopped");
475 }
476
477 async fn handle_optional_command(&mut self, command: Option<ClientCommand>) -> bool {
478 let Some(command) = command else {
479 return false;
480 };
481 self.handle_command(command).await
482 }
483
484 async fn handle_command(&mut self, command: ClientCommand) -> bool {
485 match command {
486 ClientCommand::Request {
487 request,
488 respond_to,
489 } => {
490 let response = self.handle_request(*request).await;
491 let _ = respond_to.send(response);
492 true
493 }
494 ClientCommand::SetStatus {
495 status,
496 failure,
497 respond_to,
498 } => {
499 self.update_status(status, failure);
500 let _ = respond_to.send(());
501 true
502 }
503 ClientCommand::Shutdown { respond_to } => {
504 log::debug!("inline client runner shutdown requested");
505 self.update_status(ClientStatus::ShuttingDown, None);
506 let _ = respond_to.send(());
507 false
508 }
509 }
510 }
511
512 const fn should_receive_events(&self) -> bool {
513 matches!(
514 self.status,
515 ClientStatus::Connected | ClientStatus::Reconnecting
516 )
517 }
518
519 fn emit_received_events(&self, events: Vec<ClientEvent>) {
520 for event in events {
521 let _ = self.event_tx.send(event);
522 }
523 }
524
525 fn handle_signal(&mut self, signal: RunnerSignal) {
526 match signal {
527 RunnerSignal::Events(events) => {
528 if self.status == ClientStatus::Reconnecting {
529 self.update_status(ClientStatus::Connected, None);
530 }
531 self.emit_received_events(events);
532 }
533 RunnerSignal::ReceiveError(error) => {
534 self.update_status_for_backend_error(&error);
535 }
536 }
537 }
538
539 async fn handle_request(&mut self, request: ClientRequest) -> BackendResult<ClientResponse> {
540 log::debug!("handling inline client request: {}", request.kind());
541 match request {
542 ClientRequest::AuthStart(auth) => self
543 .backend
544 .auth_start(auth)
545 .await
546 .map(ClientResponse::AuthStart),
547 ClientRequest::AuthVerify(auth) => {
548 let result = self.backend.auth_verify(auth).await?;
549 let status = result.status.clone();
550 self.update_status(status.status, status.failure.clone());
551 Ok(ClientResponse::AuthVerify(result))
552 }
553 ClientRequest::Resume => match self.backend.resume_session().await {
554 Ok(status) => {
555 self.update_status(status.status, status.failure.clone());
556 Ok(ClientResponse::Status(status))
557 }
558 Err(error) => {
559 self.update_status_for_backend_error(&error);
560 Err(error)
561 }
562 },
563 ClientRequest::Connect(connect) => {
564 let status = self.backend.connect(connect).await?;
565 self.update_status(status.status, status.failure.clone());
566 Ok(ClientResponse::Status(status))
567 }
568 ClientRequest::Logout => {
569 self.backend.logout().await?;
570 self.update_status(ClientStatus::Disconnected, None);
571 Ok(ClientResponse::Empty)
572 }
573 ClientRequest::Dialogs(dialogs) => self
574 .backend
575 .dialogs(dialogs)
576 .await
577 .map(ClientResponse::Dialogs),
578 ClientRequest::History(history) => self
579 .backend
580 .history(history)
581 .await
582 .map(ClientResponse::History),
583 ClientRequest::ChatParticipants(participants) => self
584 .backend
585 .chat_participants(participants)
586 .await
587 .map(ClientResponse::ChatParticipants),
588 ClientRequest::CreateDm(request) => self
589 .backend
590 .create_dm(request)
591 .await
592 .map(ClientResponse::CreatedChat),
593 ClientRequest::CreateThread(request) => self
594 .backend
595 .create_thread(request)
596 .await
597 .map(ClientResponse::CreatedChat),
598 ClientRequest::CreateReplyThread(request) => self
599 .backend
600 .create_reply_thread(request)
601 .await
602 .map(ClientResponse::CreatedChat),
603 ClientRequest::SendText(send) => {
604 let outcome = self.backend.send_text(send).await?;
605 Ok(ClientResponse::Message(self.emit_send_outcome(outcome)))
606 }
607 ClientRequest::SendMedia { request, bytes } => {
608 let outcome = self.backend.send_media(request, bytes).await?;
609 Ok(ClientResponse::Message(self.emit_send_outcome(outcome)))
610 }
611 ClientRequest::EditMessage(edit) => {
612 let outcome = self.backend.edit_message(edit).await?;
613 self.emit_operation_events(outcome);
614 Ok(ClientResponse::Empty)
615 }
616 ClientRequest::DeleteMessage(delete) => {
617 let outcome = self.backend.delete_message(delete).await?;
618 self.emit_operation_events(outcome);
619 Ok(ClientResponse::Empty)
620 }
621 ClientRequest::React(react) => {
622 let outcome = self.backend.react(react).await?;
623 self.emit_operation_events(outcome);
624 Ok(ClientResponse::Empty)
625 }
626 ClientRequest::Read(read) => {
627 let outcome = self.backend.read(read).await?;
628 self.emit_operation_events(outcome);
629 Ok(ClientResponse::Empty)
630 }
631 ClientRequest::Typing(typing) => {
632 let outcome = self.backend.typing(typing).await?;
633 self.emit_operation_events(outcome);
634 Ok(ClientResponse::Empty)
635 }
636 }
637 }
638
639 fn emit_operation_events(&self, outcome: OperationOutcome) {
640 for event in outcome.events {
641 let _ = self.event_tx.send(event);
642 }
643 }
644
645 fn emit_send_outcome(&self, outcome: SendTextOutcome) -> MessageMutation {
646 let transaction = outcome.transaction_event();
647 let message_id = outcome.message_id;
648 let chat_id = outcome.chat_id;
649 let message = outcome.message;
650 let mutation = outcome.mutation;
651 let _ = self
652 .event_tx
653 .send(ClientEvent::TransactionChanged(transaction));
654 if let Some(message_id) = message_id {
655 let _ = self.event_tx.send(ClientEvent::MessageUpserted {
656 chat_id,
657 message_id,
658 });
659 }
660 if let Some(message) = message {
661 let _ = self.event_tx.send(ClientEvent::MessageStored { message });
662 }
663 mutation
664 }
665
666 fn update_status_for_backend_error(&mut self, error: &BackendError) {
667 let status = match error.category {
668 ClientErrorCategory::AuthRequired => ClientStatus::AuthRequired,
669 ClientErrorCategory::AuthExpired => ClientStatus::AuthExpired,
670 ClientErrorCategory::Network
671 | ClientErrorCategory::Timeout
672 | ClientErrorCategory::RateLimited => ClientStatus::Reconnecting,
673 _ => ClientStatus::Disconnected,
674 };
675 self.update_status(
676 status,
677 Some(ClientFailure::new(error.category, error.message.clone())),
678 );
679 }
680
681 fn update_status(&mut self, status: ClientStatus, failure: Option<ClientFailure>) {
682 log::debug!("inline client status changed: {status:?}");
683 self.status = status;
684 self.failure = failure.clone();
685 let _ = self.status_tx.send(status);
686 let _ = self.failure_tx.send(failure.clone());
687 let _ = self
688 .event_tx
689 .send(ClientEvent::StatusChanged { status, failure });
690 self.sync_event_receiver_to_status();
691 }
692
693 fn sync_event_receiver_to_status(&mut self) {
694 if self.should_receive_events() {
695 self.start_event_receiver();
696 } else {
697 self.stop_event_receiver();
698 }
699 }
700
701 fn start_event_receiver(&mut self) {
702 if self.event_task.is_some() {
703 return;
704 }
705 let backend = self.backend.clone();
706 let signal_tx = self.signal_tx.clone();
707 self.event_task = Some(tokio::spawn(run_backend_event_receiver(backend, signal_tx)));
708 }
709
710 fn stop_event_receiver(&mut self) {
711 if let Some(task) = self.event_task.take() {
712 task.abort();
713 }
714 }
715}
716
717async fn run_backend_event_receiver(
718 backend: Arc<dyn ClientBackend>,
719 signal_tx: mpsc::Sender<RunnerSignal>,
720) {
721 loop {
722 match backend.receive_events().await {
723 Ok(events) => {
724 if signal_tx.send(RunnerSignal::Events(events)).await.is_err() {
725 break;
726 }
727 }
728 Err(error) => {
729 let retry = should_retry_event_receive(&error);
730 let delay = event_retry_delay(&error);
731 if signal_tx
732 .send(RunnerSignal::ReceiveError(error))
733 .await
734 .is_err()
735 {
736 break;
737 }
738 if !retry {
739 break;
740 }
741 tokio::time::sleep(delay).await;
742 }
743 }
744 }
745}
746
747enum ClientCommand {
748 Request {
749 request: Box<ClientRequest>,
750 respond_to: oneshot::Sender<BackendResult<ClientResponse>>,
751 },
752 SetStatus {
753 status: ClientStatus,
754 failure: Option<ClientFailure>,
755 respond_to: oneshot::Sender<()>,
756 },
757 Shutdown {
758 respond_to: oneshot::Sender<()>,
759 },
760}
761
762#[derive(Debug)]
763enum RunnerSignal {
764 Events(Vec<ClientEvent>),
765 ReceiveError(BackendError),
766}
767
768impl std::fmt::Debug for ClientCommand {
769 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
770 match self {
771 Self::Request { request, .. } => f
772 .debug_struct("Request")
773 .field("request", request)
774 .finish_non_exhaustive(),
775 Self::SetStatus {
776 status, failure, ..
777 } => f
778 .debug_struct("SetStatus")
779 .field("status", status)
780 .field("failure", failure)
781 .finish_non_exhaustive(),
782 Self::Shutdown { .. } => f.debug_struct("Shutdown").finish_non_exhaustive(),
783 }
784 }
785}
786
787#[derive(Debug)]
788enum ClientRequest {
789 AuthStart(AuthStartRequest),
790 AuthVerify(AuthVerifyRequest),
791 Resume,
792 Connect(ConnectRequest),
793 Logout,
794 Dialogs(DialogsRequest),
795 History(HistoryRequest),
796 ChatParticipants(ChatParticipantsRequest),
797 CreateDm(CreateDmRequest),
798 CreateThread(CreateThreadRequest),
799 CreateReplyThread(CreateReplyThreadRequest),
800 SendText(SendTextRequest),
801 SendMedia {
802 request: UploadRequest,
803 bytes: Vec<u8>,
804 },
805 EditMessage(EditMessageRequest),
806 DeleteMessage(DeleteMessageRequest),
807 React(ReactRequest),
808 Read(ReadRequest),
809 Typing(TypingRequest),
810}
811
812impl ClientRequest {
813 const fn kind(&self) -> &'static str {
814 match self {
815 Self::AuthStart(_) => "auth_start",
816 Self::AuthVerify(_) => "auth_verify",
817 Self::Resume => "resume",
818 Self::Connect(_) => "connect",
819 Self::Logout => "logout",
820 Self::Dialogs(_) => "dialogs",
821 Self::History(_) => "history",
822 Self::ChatParticipants(_) => "chat_participants",
823 Self::CreateDm(_) => "create_dm",
824 Self::CreateThread(_) => "create_thread",
825 Self::CreateReplyThread(_) => "create_reply_thread",
826 Self::SendText(_) => "send_text",
827 Self::SendMedia { .. } => "send_media",
828 Self::EditMessage(_) => "edit_message",
829 Self::DeleteMessage(_) => "delete_message",
830 Self::React(_) => "react",
831 Self::Read(_) => "read",
832 Self::Typing(_) => "typing",
833 }
834 }
835}
836
837const fn event_retry_delay(error: &BackendError) -> Duration {
838 match error.category {
839 ClientErrorCategory::RateLimited => EVENT_RATE_LIMIT_RETRY_DELAY,
840 _ => EVENT_RETRY_DELAY,
841 }
842}
843
844const fn should_retry_event_receive(error: &BackendError) -> bool {
845 matches!(
846 error.category,
847 ClientErrorCategory::Network
848 | ClientErrorCategory::Timeout
849 | ClientErrorCategory::RateLimited
850 )
851}
852
853#[derive(Debug)]
854enum ClientResponse {
855 Empty,
856 Status(ClientStatusSnapshot),
857 AuthStart(AuthStartResult),
858 AuthVerify(AuthVerifyResult),
859 Dialogs(DialogsPage),
860 History(HistoryPage),
861 ChatParticipants(ChatParticipantsPage),
862 CreatedChat(CreatedChat),
863 Message(MessageMutation),
864}
865
866#[cfg(test)]
867mod tests {
868 use crate::{
869 AuthContactKind, AuthCredential, AuthToken, DialogRecord, HistoryRequest, InlineId,
870 MediaKind, MessageContent, PeerRef,
871 };
872
873 use super::*;
874
875 fn token_connect() -> ConnectRequest {
876 ConnectRequest::new(AuthCredential::AccessToken {
877 token: AuthToken::try_new("token").unwrap(),
878 })
879 }
880
881 fn auth_verify_request() -> AuthVerifyRequest {
882 AuthVerifyRequest {
883 contact: "mo@example.com".to_owned(),
884 kind: AuthContactKind::Email,
885 code: "123456".to_owned(),
886 challenge_token: None,
887 device_name: Some("inline-client test".to_owned()),
888 account_namespace: None,
889 }
890 }
891
892 #[tokio::test]
893 async fn status_snapshot_returns_current_status() {
894 let client = InlineClient::builder()
895 .initial_status(ClientStatus::Connected)
896 .build()
897 .spawn();
898
899 let status = client.status_snapshot();
900
901 assert_eq!(status.status, ClientStatus::Connected);
902 assert_eq!(status.failure, None);
903 }
904
905 #[tokio::test]
906 async fn set_status_emits_lossless_event() {
907 let client = InlineClient::builder().build().spawn();
908 let mut events = client.subscribe();
909
910 client
911 .set_status(
912 ClientStatus::AuthExpired,
913 Some(ClientFailure::new(
914 ClientErrorCategory::AuthExpired,
915 "relogin required",
916 )),
917 )
918 .await
919 .unwrap();
920
921 let event = events.recv().await.unwrap();
922 assert_eq!(event.reliability(), crate::EventReliability::Lossless);
923 assert!(matches!(
924 event,
925 ClientEvent::StatusChanged {
926 status: ClientStatus::AuthExpired,
927 ..
928 }
929 ));
930 assert_eq!(client.status(), ClientStatus::AuthExpired);
931 assert_eq!(
932 client.status_snapshot().failure.unwrap().category,
933 ClientErrorCategory::AuthExpired
934 );
935 }
936
937 #[tokio::test]
938 async fn connect_updates_status_and_emits_event() {
939 let client = InlineClient::builder().build().spawn();
940 let mut events = client.subscribe();
941
942 let status = client.connect(token_connect()).await.unwrap();
943
944 assert_eq!(status.status, ClientStatus::Connected);
945 assert_eq!(client.status(), ClientStatus::Connected);
946
947 let event = events.recv().await.unwrap();
948 assert!(matches!(
949 event,
950 ClientEvent::StatusChanged {
951 status: ClientStatus::Connected,
952 ..
953 }
954 ));
955 }
956
957 #[tokio::test]
958 async fn runner_emits_backend_pushed_events_while_connected() {
959 let backend = InMemoryBackend::new();
960 let client = InlineClient::builder()
961 .backend(backend.clone())
962 .build()
963 .spawn();
964 let mut events = client.subscribe();
965
966 client.connect(token_connect()).await.unwrap();
967 backend.push_event_batch(vec![ClientEvent::MessageDeleted {
968 chat_id: InlineId::new(7),
969 message_id: InlineId::new(99),
970 }]);
971
972 let event = recv_until_event(&mut events, |event| {
973 matches!(
974 event,
975 ClientEvent::MessageDeleted {
976 chat_id,
977 message_id,
978 } if *chat_id == InlineId::new(7) && *message_id == InlineId::new(99)
979 )
980 })
981 .await;
982 assert_eq!(
983 event,
984 ClientEvent::MessageDeleted {
985 chat_id: InlineId::new(7),
986 message_id: InlineId::new(99),
987 }
988 );
989 }
990
991 #[tokio::test]
992 async fn runner_marks_reconnecting_when_event_receive_is_rate_limited() {
993 let backend = InMemoryBackend::new();
994 let client = InlineClient::builder()
995 .backend(backend.clone())
996 .build()
997 .spawn();
998 let mut events = client.subscribe();
999
1000 client.connect(token_connect()).await.unwrap();
1001 backend.push_event_error(BackendError::new(
1002 ClientErrorCategory::RateLimited,
1003 "rate limited",
1004 ));
1005
1006 let event = recv_until_event(&mut events, |event| {
1007 matches!(
1008 event,
1009 ClientEvent::StatusChanged {
1010 status: ClientStatus::Reconnecting,
1011 ..
1012 }
1013 )
1014 })
1015 .await;
1016 assert!(matches!(
1017 event,
1018 ClientEvent::StatusChanged {
1019 status: ClientStatus::Reconnecting,
1020 ..
1021 }
1022 ));
1023 assert_eq!(client.status(), ClientStatus::Reconnecting);
1024 }
1025
1026 #[tokio::test]
1027 async fn auth_start_and_verify_flow_through_backend() {
1028 let client = InlineClient::builder().build().spawn();
1029 let mut events = client.subscribe();
1030
1031 let started = client
1032 .auth_start(AuthStartRequest {
1033 contact: "mo@example.com".to_owned(),
1034 kind: AuthContactKind::Email,
1035 device_name: Some("inline-client test".to_owned()),
1036 })
1037 .await
1038 .unwrap();
1039 assert!(started.existing_user);
1040 assert!(!started.needs_invite_code);
1041
1042 let verified = client.auth_verify(auth_verify_request()).await.unwrap();
1043 assert_eq!(verified.user_id, InlineId::new(1));
1044 assert_eq!(verified.account_namespace, "1");
1045 assert_eq!(verified.status.status, ClientStatus::Connected);
1046 assert_eq!(client.status(), ClientStatus::Connected);
1047
1048 let event = events.recv().await.unwrap();
1049 assert!(matches!(
1050 event,
1051 ClientEvent::StatusChanged {
1052 status: ClientStatus::Connected,
1053 ..
1054 }
1055 ));
1056 }
1057
1058 #[tokio::test]
1059 async fn resume_without_session_reports_auth_required() {
1060 let client = InlineClient::builder().build().spawn();
1061 let mut events = client.subscribe();
1062
1063 let status = client.resume_session().await.unwrap();
1064
1065 assert_eq!(status.status, ClientStatus::AuthRequired);
1066 assert_eq!(client.status(), ClientStatus::AuthRequired);
1067
1068 let event = events.recv().await.unwrap();
1069 assert!(matches!(
1070 event,
1071 ClientEvent::StatusChanged {
1072 status: ClientStatus::AuthRequired,
1073 ..
1074 }
1075 ));
1076 }
1077
1078 #[tokio::test]
1079 async fn dialogs_and_history_flow_through_backend() {
1080 let backend = InMemoryBackend::new();
1081 backend.upsert_dialog(DialogRecord {
1082 chat_id: InlineId::new(7),
1083 peer_user_id: None,
1084 title: Some("general".to_owned()),
1085 last_message_id: None,
1086 synced_through_message_id: None,
1087 unread_count: Some(0),
1088 });
1089 backend.insert_message(crate::MessageRecord {
1090 chat_id: InlineId::new(7),
1091 message_id: InlineId::new(1),
1092 sender_id: InlineId::new(2),
1093 timestamp: 1,
1094 is_outgoing: false,
1095 content: MessageContent::Text {
1096 text: "hello".to_owned(),
1097 },
1098 reply_to_message_id: None,
1099 transaction: None,
1100 });
1101 let client = InlineClient::builder().backend(backend).build().spawn();
1102 client.connect(token_connect()).await.unwrap();
1103
1104 let dialogs = client.dialogs(DialogsRequest::default()).await.unwrap();
1105 assert_eq!(dialogs.dialogs.len(), 1);
1106 assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
1107
1108 let history = client
1109 .history(HistoryRequest {
1110 chat_id: InlineId::new(7),
1111 limit: Some(10),
1112 before_message_id: None,
1113 after_message_id: None,
1114 })
1115 .await
1116 .unwrap();
1117 assert_eq!(history.messages.len(), 1);
1118 assert_eq!(history.messages[0].message_id, InlineId::new(1));
1119 }
1120
1121 #[tokio::test]
1122 async fn send_text_returns_mutation_and_emits_events() {
1123 let client = InlineClient::builder().build().spawn();
1124 client.connect(token_connect()).await.unwrap();
1125 let mut events = client.subscribe();
1126
1127 let mutation = client
1128 .send_text(SendTextRequest::new(
1129 PeerRef::Chat {
1130 chat_id: InlineId::new(7),
1131 },
1132 "hello",
1133 ))
1134 .await
1135 .unwrap();
1136
1137 assert_eq!(mutation.message_id, Some(InlineId::new(1)));
1138 assert_eq!(
1139 mutation.transaction.final_message_id,
1140 Some(InlineId::new(1))
1141 );
1142
1143 let events = [
1144 events.recv().await.unwrap(),
1145 events.recv().await.unwrap(),
1146 events.recv().await.unwrap(),
1147 ];
1148 assert!(
1149 events
1150 .iter()
1151 .any(|event| matches!(event, ClientEvent::TransactionChanged(_)))
1152 );
1153 assert!(events.iter().any(|event| matches!(
1154 event,
1155 ClientEvent::MessageUpserted {
1156 chat_id: InlineId(7),
1157 message_id: InlineId(1)
1158 }
1159 )));
1160 assert!(events.iter().any(|event| matches!(
1161 event,
1162 ClientEvent::MessageStored { message }
1163 if message.chat_id == InlineId::new(7)
1164 && message.message_id == InlineId::new(1)
1165 )));
1166 }
1167
1168 #[tokio::test]
1169 async fn edit_message_emits_stored_upsert_event() {
1170 let backend = InMemoryBackend::new();
1171 backend.insert_message(crate::MessageRecord {
1172 chat_id: InlineId::new(7),
1173 message_id: InlineId::new(1),
1174 sender_id: InlineId::new(2),
1175 timestamp: 1,
1176 is_outgoing: false,
1177 content: MessageContent::Text {
1178 text: "old".to_owned(),
1179 },
1180 reply_to_message_id: None,
1181 transaction: None,
1182 });
1183 let client = InlineClient::builder().backend(backend).build().spawn();
1184 client.connect(token_connect()).await.unwrap();
1185 let mut events = client.subscribe();
1186
1187 client
1188 .edit_message(EditMessageRequest {
1189 chat_id: InlineId::new(7),
1190 message_id: InlineId::new(1),
1191 text: "edited".to_owned(),
1192 external_id: None,
1193 })
1194 .await
1195 .unwrap();
1196
1197 match events.recv().await.unwrap() {
1198 ClientEvent::MessageStored { message } => {
1199 assert_eq!(message.chat_id, InlineId::new(7));
1200 assert_eq!(message.message_id, InlineId::new(1));
1201 assert_eq!(
1202 message.content,
1203 MessageContent::Text {
1204 text: "edited".to_owned()
1205 }
1206 );
1207 }
1208 other => panic!("unexpected event: {other:?}"),
1209 }
1210 }
1211
1212 #[tokio::test]
1213 async fn send_media_emits_message_events() {
1214 let client = InlineClient::builder().build().spawn();
1215 client.connect(token_connect()).await.unwrap();
1216 let mut events = client.subscribe();
1217
1218 let mutation = client
1219 .send_media(
1220 UploadRequest {
1221 peer: PeerRef::Chat {
1222 chat_id: InlineId::new(7),
1223 },
1224 kind: MediaKind::Photo,
1225 file_name: Some("image.png".to_owned()),
1226 mime_type: Some("image/png".to_owned()),
1227 size_bytes: Some(4),
1228 caption: Some("caption".to_owned()),
1229 width: Some(10),
1230 height: Some(10),
1231 duration_ms: None,
1232 external_id: None,
1233 random_id: None,
1234 reply_to_message_id: None,
1235 },
1236 vec![1, 2, 3, 4],
1237 )
1238 .await
1239 .unwrap();
1240
1241 assert_eq!(mutation.message_id, Some(InlineId::new(1)));
1242
1243 let _transaction = events.recv().await.unwrap();
1244 let _upsert = events.recv().await.unwrap();
1245 match events.recv().await.unwrap() {
1246 ClientEvent::MessageStored { message } => {
1247 assert_eq!(message.chat_id, InlineId::new(7));
1248 assert_eq!(message.message_id, InlineId::new(1));
1249 match message.content {
1250 MessageContent::Media {
1251 kind,
1252 file_name,
1253 caption,
1254 ..
1255 } => {
1256 assert_eq!(kind, MediaKind::Photo);
1257 assert_eq!(file_name.as_deref(), Some("image.png"));
1258 assert_eq!(caption.as_deref(), Some("caption"));
1259 }
1260 other => panic!("unexpected content: {other:?}"),
1261 }
1262 }
1263 other => panic!("unexpected event: {other:?}"),
1264 }
1265 }
1266
1267 #[tokio::test]
1268 async fn shutdown_stops_runner() {
1269 let client = InlineClient::builder().build().spawn();
1270
1271 client.shutdown().await.unwrap();
1272 let err = client
1273 .dialogs(DialogsRequest::default())
1274 .await
1275 .expect_err("runner should reject commands after shutdown");
1276
1277 assert!(matches!(
1278 err,
1279 ClientRequestError::Command(
1280 ClientCommandError::Closed | ClientCommandError::ResponseDropped
1281 )
1282 ));
1283 }
1284
1285 async fn recv_until_event(
1286 events: &mut broadcast::Receiver<ClientEvent>,
1287 matches: impl Fn(&ClientEvent) -> bool,
1288 ) -> ClientEvent {
1289 tokio::time::timeout(Duration::from_secs(3), async {
1290 loop {
1291 let event = events.recv().await.unwrap();
1292 if matches(&event) {
1293 return event;
1294 }
1295 }
1296 })
1297 .await
1298 .expect("expected matching client event")
1299 }
1300}