1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3#![forbid(unsafe_code)]
4
5use std::{fmt, str::FromStr};
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10pub mod backend;
12
13pub mod runtime;
15
16pub mod realtime;
18
19pub mod sdk_backend;
21
22pub mod sync;
24
25pub mod store;
27
28pub mod types;
30
31pub use backend::{
32 BackendError, BackendResult, ClientBackend, InMemoryBackend, OperationOutcome, SendTextOutcome,
33};
34pub use inline_sdk::ClientIdentity;
35pub use realtime::{
36 FakeRealtimeAttempt, FakeRealtimeConnector, RealtimeConnectRequest, RealtimeConnectionInfo,
37 RealtimeConnector, SdkRealtimeConnector,
38};
39pub use runtime::{
40 ClientCommandError, ClientRequestError, ClientRunner, DEFAULT_COMMAND_QUEUE_CAPACITY,
41 DEFAULT_EVENT_QUEUE_CAPACITY, DEFAULT_LOSSLESS_EVENT_QUEUE_CAPACITY,
42 DEFAULT_MAX_CONCURRENT_REQUESTS, InlineClient, InlineClientBuilder, InlineClientRuntime,
43 LosslessEventReceiver, ReconnectPolicy,
44};
45pub use sdk_backend::{SdkBackend, SdkBackendBuildError, SdkBackendBuilder};
46pub use store::{
47 AccountStateSnapshot, ChatStateSnapshot, ClientStore, InMemoryStore, PendingSyncBatch,
48 SqliteStore, StoreError, StoreResult, StoredReaction, StoredReadState, StoredSession,
49 StoredTransaction, SyncBucketKey, SyncBucketPeer, SyncBucketState, SyncState,
50};
51pub use sync::SyncConfig;
52pub use types::{
53 AddChatParticipantRequest, AuthContactKind, AuthCredential, AuthStartRequest, AuthStartResult,
54 AuthToken, AuthVerifyRequest, AuthVerifyResult, ChatCreateParticipant, ChatParticipantRecord,
55 ChatParticipantsPage, ChatParticipantsRequest, ClientStatusSnapshot, ConnectRequest,
56 CreateDmRequest, CreateReplyThreadRequest, CreateThreadRequest, CreatedChat, DeleteChatRequest,
57 DeleteMessageRequest, DialogFollowMode, DialogNotificationMode, DialogRecord, DialogsPage,
58 DialogsRequest, EditMessageRequest, HistoryPage, HistoryRequest, MediaKind, MessageContent,
59 MessageMutation, MessageRecord, NotificationMode, PeerRef, ReactRequest, ReadRequest,
60 RemoveChatParticipantRequest, SendTextRequest, SetMarkedUnreadRequest, SpaceMemberRecord,
61 SpaceMemberRole, SpaceRecord, TypingRequest, UpdateChatInfoRequest,
62 UpdateDialogNotificationsRequest, UploadHandle, UploadRequest, UserRecord, UserSettingsRecord,
63};
64
65pub const VERSION: &str = env!("CARGO_PKG_VERSION");
67
68#[derive(Clone, Debug, Error, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum ClientError {
72 #[error("{field} must not be empty")]
74 EmptyField {
75 field: &'static str,
77 },
78
79 #[error("{field} is too long ({len} > {max})")]
81 FieldTooLong {
82 field: &'static str,
84 len: usize,
86 max: usize,
88 },
89
90 #[error("{field} must not contain ASCII control characters")]
92 ControlCharacter {
93 field: &'static str,
95 },
96}
97
98fn validate_token<'a>(
99 field: &'static str,
100 value: &'a str,
101 max_len: usize,
102) -> Result<&'a str, ClientError> {
103 let trimmed = value.trim();
104 if trimmed.is_empty() {
105 return Err(ClientError::EmptyField { field });
106 }
107 if trimmed.len() > max_len {
108 return Err(ClientError::FieldTooLong {
109 field,
110 len: trimmed.len(),
111 max: max_len,
112 });
113 }
114 if trimmed.chars().any(|ch| ch.is_ascii_control()) {
115 return Err(ClientError::ControlCharacter { field });
116 }
117 Ok(trimmed)
118}
119
120#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
126#[serde(transparent)]
127pub struct InlineId(i64);
128
129impl InlineId {
130 pub const fn new(value: i64) -> Self {
132 Self(value)
133 }
134
135 pub const fn get(self) -> i64 {
137 self.0
138 }
139}
140
141impl From<i64> for InlineId {
142 fn from(value: i64) -> Self {
143 Self::new(value)
144 }
145}
146
147impl fmt::Display for InlineId {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 self.0.fmt(f)
150 }
151}
152
153#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
158#[serde(transparent)]
159pub struct RandomId(i64);
160
161impl RandomId {
162 pub const fn new(value: i64) -> Self {
164 Self(value)
165 }
166
167 pub const fn get(self) -> i64 {
169 self.0
170 }
171}
172
173impl From<i64> for RandomId {
174 fn from(value: i64) -> Self {
175 Self::new(value)
176 }
177}
178
179impl fmt::Display for RandomId {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 self.0.fmt(f)
182 }
183}
184
185#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
191pub struct ExternalId {
192 source: String,
193 id: String,
194}
195
196impl ExternalId {
197 pub const MAX_SOURCE_LEN: usize = 64;
199
200 pub const MAX_ID_LEN: usize = 512;
202
203 pub fn try_new(source: impl AsRef<str>, id: impl AsRef<str>) -> Result<Self, ClientError> {
205 let source = validate_token("source", source.as_ref(), Self::MAX_SOURCE_LEN)?;
206 let id = validate_token("id", id.as_ref(), Self::MAX_ID_LEN)?;
207 Ok(Self {
208 source: source.to_owned(),
209 id: id.to_owned(),
210 })
211 }
212
213 pub fn source(&self) -> &str {
215 &self.source
216 }
217
218 pub fn id(&self) -> &str {
220 &self.id
221 }
222}
223
224#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
226#[serde(transparent)]
227pub struct TransactionId(String);
228
229impl TransactionId {
230 pub const MAX_LEN: usize = 128;
232
233 pub fn try_new(value: impl AsRef<str>) -> Result<Self, ClientError> {
235 Ok(Self(
236 validate_token("transaction_id", value.as_ref(), Self::MAX_LEN)?.to_owned(),
237 ))
238 }
239
240 pub fn as_str(&self) -> &str {
242 &self.0
243 }
244}
245
246impl fmt::Display for TransactionId {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 self.0.fmt(f)
249 }
250}
251
252impl FromStr for TransactionId {
253 type Err = ClientError;
254
255 fn from_str(value: &str) -> Result<Self, Self::Err> {
256 Self::try_new(value)
257 }
258}
259
260#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
262pub struct TransactionIdentity {
263 pub transaction_id: TransactionId,
265 pub external_id: Option<ExternalId>,
267 pub random_id: RandomId,
269 pub temporary_message_id: Option<InlineId>,
271 pub final_message_id: Option<InlineId>,
273}
274
275impl TransactionIdentity {
276 pub fn new(
278 transaction_id: TransactionId,
279 external_id: Option<ExternalId>,
280 random_id: RandomId,
281 ) -> Self {
282 Self {
283 transaction_id,
284 external_id,
285 random_id,
286 temporary_message_id: None,
287 final_message_id: None,
288 }
289 }
290
291 pub fn with_temporary_message_id(mut self, message_id: impl Into<InlineId>) -> Self {
293 self.temporary_message_id = Some(message_id.into());
294 self
295 }
296
297 pub fn with_final_message_id(mut self, message_id: impl Into<InlineId>) -> Self {
299 self.final_message_id = Some(message_id.into());
300 self
301 }
302}
303
304#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
306#[non_exhaustive]
307pub enum ClientStatus {
308 Disconnected,
310 Connecting,
312 Connected,
314 Reconnecting,
316 AuthRequired,
318 AuthExpired,
320 LoggedOut,
322 ShuttingDown,
324}
325
326#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
328#[non_exhaustive]
329pub enum ClientErrorCategory {
330 InvalidInput,
332 AuthRequired,
334 AuthExpired,
336 ReloginRequired,
338 Network,
340 Timeout,
342 RateLimited,
344 ProtocolMismatch,
346 NotFound,
348 PermissionDenied,
350 Unsupported,
352 Conflict,
354 Internal,
356}
357
358#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
360pub struct ClientFailure {
361 pub category: ClientErrorCategory,
363 pub message: String,
365}
366
367impl ClientFailure {
368 pub fn new(category: ClientErrorCategory, message: impl Into<String>) -> Self {
370 Self {
371 category,
372 message: message.into(),
373 }
374 }
375}
376
377#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
379#[non_exhaustive]
380pub enum TransactionState {
381 Queued,
383 Sent,
385 Acked,
387 Completed,
389 Failed,
391 Cancelled,
393}
394
395#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
397pub struct TransactionEvent {
398 pub identity: TransactionIdentity,
400 pub state: TransactionState,
402 pub failure: Option<ClientFailure>,
404}
405
406#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
408pub enum EventReliability {
409 Lossless,
411 BestEffort,
413}
414
415#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
417#[non_exhaustive]
418pub enum ClientEvent {
419 StatusChanged {
421 status: ClientStatus,
423 failure: Option<ClientFailure>,
425 },
426 TransactionChanged(TransactionEvent),
428 ChatUpserted {
430 chat_id: InlineId,
432 },
433 ChatDeleted {
435 chat_id: InlineId,
437 },
438 ChatParticipantsChanged {
440 chat_id: InlineId,
442 },
443 UserUpserted {
445 user_id: InlineId,
447 },
448 SpaceUpserted {
450 space_id: InlineId,
452 },
453 SpaceMemberChanged {
455 space_id: InlineId,
457 user_id: InlineId,
459 removed: bool,
461 },
462 UserSettingsChanged {},
464 MessageActionInvoked {
466 interaction_id: InlineId,
468 chat_id: InlineId,
470 message_id: InlineId,
472 actor_user_id: InlineId,
474 action_id: String,
476 data: Vec<u8>,
478 },
479 MessageActionAnswered {
481 interaction_id: InlineId,
483 toast: Option<String>,
485 },
486 MessageUpserted {
488 chat_id: InlineId,
490 message_id: InlineId,
492 },
493 MessageStored {
495 message: MessageRecord,
497 },
498 MessageDeleted {
500 chat_id: InlineId,
502 message_id: InlineId,
504 },
505 ChatHistoryCleared {
507 chat_id: InlineId,
509 before_date: Option<i64>,
511 },
512 ReactionChanged {
514 chat_id: InlineId,
516 message_id: InlineId,
518 user_id: InlineId,
520 reaction: String,
522 removed: bool,
524 },
525 ReadStateChanged {
527 chat_id: InlineId,
529 },
530 Typing {
532 chat_id: InlineId,
534 user_id: InlineId,
536 is_typing: bool,
538 },
539 UserStatusChanged {
541 user_id: InlineId,
543 is_online: Option<bool>,
545 last_online: Option<i64>,
547 },
548 BotPresenceChanged {
550 bot_user_id: InlineId,
552 chat_id: Option<InlineId>,
554 kind: String,
556 comment: Option<String>,
558 avatar_changed: bool,
560 },
561 NewMessageNotification {
563 message: MessageRecord,
565 reason: String,
567 },
568}
569
570impl ClientEvent {
571 pub const fn reliability(&self) -> EventReliability {
573 match self {
574 Self::Typing { .. }
575 | Self::UserStatusChanged { .. }
576 | Self::BotPresenceChanged { .. }
577 | Self::NewMessageNotification { .. } => EventReliability::BestEffort,
578 Self::StatusChanged { .. }
579 | Self::TransactionChanged(_)
580 | Self::ChatUpserted { .. }
581 | Self::ChatDeleted { .. }
582 | Self::ChatParticipantsChanged { .. }
583 | Self::UserUpserted { .. }
584 | Self::SpaceUpserted { .. }
585 | Self::SpaceMemberChanged { .. }
586 | Self::UserSettingsChanged { .. }
587 | Self::MessageActionInvoked { .. }
588 | Self::MessageActionAnswered { .. }
589 | Self::MessageUpserted { .. }
590 | Self::MessageStored { .. }
591 | Self::MessageDeleted { .. }
592 | Self::ChatHistoryCleared { .. }
593 | Self::ReactionChanged { .. }
594 | Self::ReadStateChanged { .. } => EventReliability::Lossless,
595 }
596 }
597}
598
599pub mod prelude {
601 pub use crate::{
602 AddChatParticipantRequest, AuthCredential, AuthToken, BackendError, BackendResult,
603 ClientBackend, ClientCommandError, ClientError, ClientErrorCategory, ClientEvent,
604 ClientFailure, ClientIdentity, ClientRequestError, ClientRunner, ClientStatus, ClientStore,
605 ConnectRequest, DEFAULT_COMMAND_QUEUE_CAPACITY, DEFAULT_EVENT_QUEUE_CAPACITY,
606 DEFAULT_LOSSLESS_EVENT_QUEUE_CAPACITY, DeleteChatRequest, DeleteMessageRequest,
607 DialogFollowMode, DialogNotificationMode, DialogRecord, DialogsPage, DialogsRequest,
608 EditMessageRequest, EventReliability, ExternalId, FakeRealtimeAttempt,
609 FakeRealtimeConnector, HistoryPage, HistoryRequest, InMemoryBackend, InMemoryStore,
610 InlineClient, InlineClientBuilder, InlineClientRuntime, InlineId, LosslessEventReceiver,
611 MessageContent, MessageMutation, MessageRecord, NotificationMode, PeerRef, RandomId,
612 ReactRequest, ReadRequest, RealtimeConnectRequest, RealtimeConnectionInfo,
613 RealtimeConnector, ReconnectPolicy, RemoveChatParticipantRequest, SdkBackend,
614 SdkBackendBuildError, SdkBackendBuilder, SdkRealtimeConnector, SendTextOutcome,
615 SendTextRequest, SetMarkedUnreadRequest, SpaceMemberRecord, SpaceMemberRole, SpaceRecord,
616 SqliteStore, StoreError, StoreResult, StoredReaction, StoredReadState, StoredSession,
617 StoredTransaction, SyncBucketKey, SyncBucketPeer, SyncBucketState, SyncConfig, SyncState,
618 TransactionEvent, TransactionId, TransactionIdentity, TransactionState, TypingRequest,
619 UpdateChatInfoRequest, UpdateDialogNotificationsRequest, UploadHandle, UploadRequest,
620 UserSettingsRecord, VERSION,
621 };
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn external_id_rejects_empty_values() {
630 assert_eq!(
631 ExternalId::try_new("host-event", " "),
632 Err(ClientError::EmptyField { field: "id" })
633 );
634 }
635
636 #[test]
637 fn transaction_identity_records_message_ids() {
638 let identity = TransactionIdentity::new(
639 TransactionId::try_new("txn-1").unwrap(),
640 Some(ExternalId::try_new("host-event", "event-1").unwrap()),
641 RandomId::new(42),
642 )
643 .with_temporary_message_id(-1)
644 .with_final_message_id(100);
645
646 assert_eq!(identity.random_id.get(), 42);
647 assert_eq!(identity.temporary_message_id.unwrap().get(), -1);
648 assert_eq!(identity.final_message_id.unwrap().get(), 100);
649 }
650
651 #[test]
652 fn typing_is_best_effort() {
653 let event = ClientEvent::Typing {
654 chat_id: InlineId::new(1),
655 user_id: InlineId::new(2),
656 is_typing: true,
657 };
658
659 assert_eq!(event.reliability(), EventReliability::BestEffort);
660 }
661}