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 LosslessEventDelivery, LosslessEventReceiver, ReconnectPolicy,
44};
45pub use sdk_backend::{SdkBackend, SdkBackendBuildError, SdkBackendBuilder};
46pub use store::{
47 AccountStateSnapshot, ChatStateSnapshot, ClientEventDelivery, ClientStore, InMemoryStore,
48 PendingSyncBatch, SqliteStore, StoreError, StoreResult, StoredReaction, StoredReadState,
49 StoredSession, 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, DialogsOrder,
58 DialogsPage, DialogsRequest, EditMessageRequest, HistoryPage, HistoryRequest, MediaKind,
59 MessageContent, MessageMutation, MessageRecord, NotificationMode, PeerRef, ReactRequest,
60 ReadRequest, RemoveChatParticipantRequest, SendTextRequest, SetMarkedUnreadRequest,
61 SpaceMemberRecord, SpaceMemberRole, SpaceRecord, TypingRequest, UpdateChatInfoRequest,
62 UpdateDialogNotificationsRequest, UploadHandle, UploadRequest, UserRecord, UserSettingsRecord,
63};
64
65pub const VERSION: &str = env!("CARGO_PKG_VERSION");
67
68pub const CORE_SYNC_SCHEMA_REVISION: u32 = 1;
70
71#[derive(Clone, Debug, Error, PartialEq, Eq)]
73#[non_exhaustive]
74pub enum ClientError {
75 #[error("{field} must not be empty")]
77 EmptyField {
78 field: &'static str,
80 },
81
82 #[error("{field} is too long ({len} > {max})")]
84 FieldTooLong {
85 field: &'static str,
87 len: usize,
89 max: usize,
91 },
92
93 #[error("{field} must not contain ASCII control characters")]
95 ControlCharacter {
96 field: &'static str,
98 },
99}
100
101fn validate_token<'a>(
102 field: &'static str,
103 value: &'a str,
104 max_len: usize,
105) -> Result<&'a str, ClientError> {
106 let trimmed = value.trim();
107 if trimmed.is_empty() {
108 return Err(ClientError::EmptyField { field });
109 }
110 if trimmed.len() > max_len {
111 return Err(ClientError::FieldTooLong {
112 field,
113 len: trimmed.len(),
114 max: max_len,
115 });
116 }
117 if trimmed.chars().any(|ch| ch.is_ascii_control()) {
118 return Err(ClientError::ControlCharacter { field });
119 }
120 Ok(trimmed)
121}
122
123#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
129#[serde(transparent)]
130pub struct InlineId(i64);
131
132impl InlineId {
133 pub const fn new(value: i64) -> Self {
135 Self(value)
136 }
137
138 pub const fn get(self) -> i64 {
140 self.0
141 }
142}
143
144impl From<i64> for InlineId {
145 fn from(value: i64) -> Self {
146 Self::new(value)
147 }
148}
149
150impl fmt::Display for InlineId {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 self.0.fmt(f)
153 }
154}
155
156#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
161#[serde(transparent)]
162pub struct RandomId(i64);
163
164impl RandomId {
165 pub const fn new(value: i64) -> Self {
167 Self(value)
168 }
169
170 pub const fn get(self) -> i64 {
172 self.0
173 }
174}
175
176impl From<i64> for RandomId {
177 fn from(value: i64) -> Self {
178 Self::new(value)
179 }
180}
181
182impl fmt::Display for RandomId {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 self.0.fmt(f)
185 }
186}
187
188#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
194pub struct ExternalId {
195 source: String,
196 id: String,
197}
198
199impl ExternalId {
200 pub const MAX_SOURCE_LEN: usize = 64;
202
203 pub const MAX_ID_LEN: usize = 512;
205
206 pub fn try_new(source: impl AsRef<str>, id: impl AsRef<str>) -> Result<Self, ClientError> {
208 let source = validate_token("source", source.as_ref(), Self::MAX_SOURCE_LEN)?;
209 let id = validate_token("id", id.as_ref(), Self::MAX_ID_LEN)?;
210 Ok(Self {
211 source: source.to_owned(),
212 id: id.to_owned(),
213 })
214 }
215
216 pub fn source(&self) -> &str {
218 &self.source
219 }
220
221 pub fn id(&self) -> &str {
223 &self.id
224 }
225}
226
227#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
229#[serde(transparent)]
230pub struct TransactionId(String);
231
232impl TransactionId {
233 pub const MAX_LEN: usize = 128;
235
236 pub fn try_new(value: impl AsRef<str>) -> Result<Self, ClientError> {
238 Ok(Self(
239 validate_token("transaction_id", value.as_ref(), Self::MAX_LEN)?.to_owned(),
240 ))
241 }
242
243 pub fn as_str(&self) -> &str {
245 &self.0
246 }
247}
248
249impl fmt::Display for TransactionId {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 self.0.fmt(f)
252 }
253}
254
255impl FromStr for TransactionId {
256 type Err = ClientError;
257
258 fn from_str(value: &str) -> Result<Self, Self::Err> {
259 Self::try_new(value)
260 }
261}
262
263#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
265pub struct TransactionIdentity {
266 pub transaction_id: TransactionId,
268 pub external_id: Option<ExternalId>,
270 pub random_id: RandomId,
272 pub temporary_message_id: Option<InlineId>,
274 pub final_message_id: Option<InlineId>,
276}
277
278impl TransactionIdentity {
279 pub fn new(
281 transaction_id: TransactionId,
282 external_id: Option<ExternalId>,
283 random_id: RandomId,
284 ) -> Self {
285 Self {
286 transaction_id,
287 external_id,
288 random_id,
289 temporary_message_id: None,
290 final_message_id: None,
291 }
292 }
293
294 pub fn with_temporary_message_id(mut self, message_id: impl Into<InlineId>) -> Self {
296 self.temporary_message_id = Some(message_id.into());
297 self
298 }
299
300 pub fn with_final_message_id(mut self, message_id: impl Into<InlineId>) -> Self {
302 self.final_message_id = Some(message_id.into());
303 self
304 }
305}
306
307#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
309#[non_exhaustive]
310pub enum ClientStatus {
311 Disconnected,
313 Connecting,
315 Connected,
317 Reconnecting,
319 AuthRequired,
321 AuthExpired,
323 LoggedOut,
325 ShuttingDown,
327}
328
329#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
331#[non_exhaustive]
332pub enum ClientErrorCategory {
333 InvalidInput,
335 AuthRequired,
337 AuthExpired,
339 ReloginRequired,
341 Network,
343 Timeout,
345 RateLimited,
347 ProtocolMismatch,
349 NotFound,
351 PermissionDenied,
353 Unsupported,
355 Conflict,
357 Internal,
359}
360
361#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
363pub struct ClientFailure {
364 pub category: ClientErrorCategory,
366 pub message: String,
368}
369
370impl ClientFailure {
371 pub fn new(category: ClientErrorCategory, message: impl Into<String>) -> Self {
373 Self {
374 category,
375 message: message.into(),
376 }
377 }
378}
379
380#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
382#[non_exhaustive]
383pub enum TransactionState {
384 Queued,
386 Sent,
388 Acked,
390 Completed,
392 Failed,
394 Cancelled,
396}
397
398#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
400pub struct TransactionEvent {
401 pub identity: TransactionIdentity,
403 pub state: TransactionState,
405 pub failure: Option<ClientFailure>,
407}
408
409#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
411pub enum EventReliability {
412 Lossless,
414 BestEffort,
416}
417
418#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
420#[non_exhaustive]
421pub enum ClientEvent {
422 StatusChanged {
424 status: ClientStatus,
426 failure: Option<ClientFailure>,
428 },
429 TransactionChanged(TransactionEvent),
431 ChatUpserted {
433 chat_id: InlineId,
435 },
436 ChatDeleted {
438 chat_id: InlineId,
440 },
441 ChatParticipantsChanged {
443 chat_id: InlineId,
445 },
446 UserUpserted {
448 user_id: InlineId,
450 },
451 SpaceUpserted {
453 space_id: InlineId,
455 },
456 SpaceMemberChanged {
458 space_id: InlineId,
460 user_id: InlineId,
462 removed: bool,
464 },
465 UserSettingsChanged {},
467 MessageActionInvoked {
469 interaction_id: InlineId,
471 chat_id: InlineId,
473 message_id: InlineId,
475 actor_user_id: InlineId,
477 action_id: String,
479 data: Vec<u8>,
481 },
482 MessageActionAnswered {
484 interaction_id: InlineId,
486 toast: Option<String>,
488 },
489 MessageUpserted {
491 chat_id: InlineId,
493 message_id: InlineId,
495 },
496 MessageStored {
498 message: MessageRecord,
500 },
501 MessageDeleted {
503 chat_id: InlineId,
505 message_id: InlineId,
507 },
508 ChatHistoryCleared {
510 chat_id: InlineId,
512 before_date: Option<i64>,
514 },
515 ReactionChanged {
517 chat_id: InlineId,
519 message_id: InlineId,
521 user_id: InlineId,
523 reaction: String,
525 removed: bool,
527 },
528 ReadStateChanged {
530 chat_id: InlineId,
532 },
533 Typing {
535 chat_id: InlineId,
537 user_id: InlineId,
539 is_typing: bool,
541 },
542 UserStatusChanged {
544 user_id: InlineId,
546 is_online: Option<bool>,
548 last_online: Option<i64>,
550 },
551 BotPresenceChanged {
553 bot_user_id: InlineId,
555 chat_id: Option<InlineId>,
557 kind: String,
559 comment: Option<String>,
561 avatar_changed: bool,
563 },
564 NewMessageNotification {
566 message: MessageRecord,
568 reason: String,
570 },
571}
572
573impl ClientEvent {
574 pub const fn reliability(&self) -> EventReliability {
576 match self {
577 Self::Typing { .. }
578 | Self::UserStatusChanged { .. }
579 | Self::BotPresenceChanged { .. }
580 | Self::NewMessageNotification { .. } => EventReliability::BestEffort,
581 Self::StatusChanged { .. }
582 | Self::TransactionChanged(_)
583 | Self::ChatUpserted { .. }
584 | Self::ChatDeleted { .. }
585 | Self::ChatParticipantsChanged { .. }
586 | Self::UserUpserted { .. }
587 | Self::SpaceUpserted { .. }
588 | Self::SpaceMemberChanged { .. }
589 | Self::UserSettingsChanged { .. }
590 | Self::MessageActionInvoked { .. }
591 | Self::MessageActionAnswered { .. }
592 | Self::MessageUpserted { .. }
593 | Self::MessageStored { .. }
594 | Self::MessageDeleted { .. }
595 | Self::ChatHistoryCleared { .. }
596 | Self::ReactionChanged { .. }
597 | Self::ReadStateChanged { .. } => EventReliability::Lossless,
598 }
599 }
600}
601
602pub mod prelude {
604 pub use crate::{
605 AddChatParticipantRequest, AuthCredential, AuthToken, BackendError, BackendResult,
606 ClientBackend, ClientCommandError, ClientError, ClientErrorCategory, ClientEvent,
607 ClientEventDelivery, ClientFailure, ClientIdentity, ClientRequestError, ClientRunner,
608 ClientStatus, ClientStore, ConnectRequest, DEFAULT_COMMAND_QUEUE_CAPACITY,
609 DEFAULT_EVENT_QUEUE_CAPACITY, DEFAULT_LOSSLESS_EVENT_QUEUE_CAPACITY, DeleteChatRequest,
610 DeleteMessageRequest, DialogFollowMode, DialogNotificationMode, DialogRecord, DialogsPage,
611 DialogsRequest, EditMessageRequest, EventReliability, ExternalId, FakeRealtimeAttempt,
612 FakeRealtimeConnector, HistoryPage, HistoryRequest, InMemoryBackend, InMemoryStore,
613 InlineClient, InlineClientBuilder, InlineClientRuntime, InlineId, LosslessEventDelivery,
614 LosslessEventReceiver, MessageContent, MessageMutation, MessageRecord, NotificationMode,
615 PeerRef, RandomId, ReactRequest, ReadRequest, RealtimeConnectRequest,
616 RealtimeConnectionInfo, RealtimeConnector, ReconnectPolicy, RemoveChatParticipantRequest,
617 SdkBackend, SdkBackendBuildError, SdkBackendBuilder, SdkRealtimeConnector, SendTextOutcome,
618 SendTextRequest, SetMarkedUnreadRequest, SpaceMemberRecord, SpaceMemberRole, SpaceRecord,
619 SqliteStore, StoreError, StoreResult, StoredReaction, StoredReadState, StoredSession,
620 StoredTransaction, SyncBucketKey, SyncBucketPeer, SyncBucketState, SyncConfig, SyncState,
621 TransactionEvent, TransactionId, TransactionIdentity, TransactionState, TypingRequest,
622 UpdateChatInfoRequest, UpdateDialogNotificationsRequest, UploadHandle, UploadRequest,
623 UserSettingsRecord, VERSION,
624 };
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 #[test]
632 fn external_id_rejects_empty_values() {
633 assert_eq!(
634 ExternalId::try_new("host-event", " "),
635 Err(ClientError::EmptyField { field: "id" })
636 );
637 }
638
639 #[test]
640 fn transaction_identity_records_message_ids() {
641 let identity = TransactionIdentity::new(
642 TransactionId::try_new("txn-1").unwrap(),
643 Some(ExternalId::try_new("host-event", "event-1").unwrap()),
644 RandomId::new(42),
645 )
646 .with_temporary_message_id(-1)
647 .with_final_message_id(100);
648
649 assert_eq!(identity.random_id.get(), 42);
650 assert_eq!(identity.temporary_message_id.unwrap().get(), -1);
651 assert_eq!(identity.final_message_id.unwrap().get(), 100);
652 }
653
654 #[test]
655 fn typing_is_best_effort() {
656 let event = ClientEvent::Typing {
657 chat_id: InlineId::new(1),
658 user_id: InlineId::new(2),
659 is_typing: true,
660 };
661
662 assert_eq!(event.reliability(), EventReliability::BestEffort);
663 }
664}