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;
73
74#[derive(Clone, Debug, Error, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum ClientError {
78 #[error("{field} must not be empty")]
80 EmptyField {
81 field: &'static str,
83 },
84
85 #[error("{field} is too long ({len} > {max})")]
87 FieldTooLong {
88 field: &'static str,
90 len: usize,
92 max: usize,
94 },
95
96 #[error("{field} must not contain ASCII control characters")]
98 ControlCharacter {
99 field: &'static str,
101 },
102}
103
104fn validate_token<'a>(
105 field: &'static str,
106 value: &'a str,
107 max_len: usize,
108) -> Result<&'a str, ClientError> {
109 let trimmed = value.trim();
110 if trimmed.is_empty() {
111 return Err(ClientError::EmptyField { field });
112 }
113 if trimmed.len() > max_len {
114 return Err(ClientError::FieldTooLong {
115 field,
116 len: trimmed.len(),
117 max: max_len,
118 });
119 }
120 if trimmed.chars().any(|ch| ch.is_ascii_control()) {
121 return Err(ClientError::ControlCharacter { field });
122 }
123 Ok(trimmed)
124}
125
126#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
132#[serde(transparent)]
133pub struct InlineId(i64);
134
135impl InlineId {
136 pub const fn new(value: i64) -> Self {
138 Self(value)
139 }
140
141 pub const fn get(self) -> i64 {
143 self.0
144 }
145}
146
147impl From<i64> for InlineId {
148 fn from(value: i64) -> Self {
149 Self::new(value)
150 }
151}
152
153impl fmt::Display for InlineId {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 self.0.fmt(f)
156 }
157}
158
159#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
164#[serde(transparent)]
165pub struct RandomId(i64);
166
167impl RandomId {
168 pub const fn new(value: i64) -> Self {
170 Self(value)
171 }
172
173 pub const fn get(self) -> i64 {
175 self.0
176 }
177}
178
179impl From<i64> for RandomId {
180 fn from(value: i64) -> Self {
181 Self::new(value)
182 }
183}
184
185impl fmt::Display for RandomId {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 self.0.fmt(f)
188 }
189}
190
191#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
197pub struct ExternalId {
198 source: String,
199 id: String,
200}
201
202impl ExternalId {
203 pub const MAX_SOURCE_LEN: usize = 64;
205
206 pub const MAX_ID_LEN: usize = 512;
208
209 pub fn try_new(source: impl AsRef<str>, id: impl AsRef<str>) -> Result<Self, ClientError> {
211 let source = validate_token("source", source.as_ref(), Self::MAX_SOURCE_LEN)?;
212 let id = validate_token("id", id.as_ref(), Self::MAX_ID_LEN)?;
213 Ok(Self {
214 source: source.to_owned(),
215 id: id.to_owned(),
216 })
217 }
218
219 pub fn source(&self) -> &str {
221 &self.source
222 }
223
224 pub fn id(&self) -> &str {
226 &self.id
227 }
228}
229
230#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
232#[serde(transparent)]
233pub struct TransactionId(String);
234
235impl TransactionId {
236 pub const MAX_LEN: usize = 128;
238
239 pub fn try_new(value: impl AsRef<str>) -> Result<Self, ClientError> {
241 Ok(Self(
242 validate_token("transaction_id", value.as_ref(), Self::MAX_LEN)?.to_owned(),
243 ))
244 }
245
246 pub fn as_str(&self) -> &str {
248 &self.0
249 }
250}
251
252impl fmt::Display for TransactionId {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 self.0.fmt(f)
255 }
256}
257
258impl FromStr for TransactionId {
259 type Err = ClientError;
260
261 fn from_str(value: &str) -> Result<Self, Self::Err> {
262 Self::try_new(value)
263 }
264}
265
266#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
268pub struct TransactionIdentity {
269 pub transaction_id: TransactionId,
271 pub external_id: Option<ExternalId>,
273 pub random_id: RandomId,
275 pub temporary_message_id: Option<InlineId>,
277 pub final_message_id: Option<InlineId>,
279}
280
281impl TransactionIdentity {
282 pub fn new(
284 transaction_id: TransactionId,
285 external_id: Option<ExternalId>,
286 random_id: RandomId,
287 ) -> Self {
288 Self {
289 transaction_id,
290 external_id,
291 random_id,
292 temporary_message_id: None,
293 final_message_id: None,
294 }
295 }
296
297 pub fn with_temporary_message_id(mut self, message_id: impl Into<InlineId>) -> Self {
299 self.temporary_message_id = Some(message_id.into());
300 self
301 }
302
303 pub fn with_final_message_id(mut self, message_id: impl Into<InlineId>) -> Self {
305 self.final_message_id = Some(message_id.into());
306 self
307 }
308}
309
310#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
312#[non_exhaustive]
313pub enum ClientStatus {
314 Disconnected,
316 Connecting,
318 Connected,
320 Reconnecting,
322 AuthRequired,
324 AuthExpired,
326 LoggedOut,
328 ShuttingDown,
330}
331
332#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
334#[non_exhaustive]
335pub enum ClientErrorCategory {
336 InvalidInput,
338 AuthRequired,
340 AuthExpired,
342 ReloginRequired,
344 Network,
346 Timeout,
348 RateLimited,
350 ProtocolMismatch,
352 NotFound,
354 PermissionDenied,
356 Unsupported,
358 Conflict,
360 Internal,
362}
363
364#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
366pub struct ClientFailure {
367 pub category: ClientErrorCategory,
369 pub message: String,
371}
372
373impl ClientFailure {
374 pub fn new(category: ClientErrorCategory, message: impl Into<String>) -> Self {
376 Self {
377 category,
378 message: message.into(),
379 }
380 }
381}
382
383#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
385#[non_exhaustive]
386pub enum TransactionState {
387 Queued,
389 Sent,
391 Acked,
393 Completed,
395 Failed,
397 Cancelled,
399}
400
401#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
403pub struct TransactionEvent {
404 pub identity: TransactionIdentity,
406 pub state: TransactionState,
408 pub failure: Option<ClientFailure>,
410}
411
412#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
414pub enum EventReliability {
415 Lossless,
417 BestEffort,
419}
420
421#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
423#[non_exhaustive]
424pub enum ClientEvent {
425 StatusChanged {
427 status: ClientStatus,
429 failure: Option<ClientFailure>,
431 },
432 TransactionChanged(TransactionEvent),
434 ChatUpserted {
436 chat_id: InlineId,
438 },
439 ChatDeleted {
441 chat_id: InlineId,
443 },
444 ChatParticipantsChanged {
446 chat_id: InlineId,
448 },
449 UserUpserted {
451 user_id: InlineId,
453 },
454 SpaceUpserted {
456 space_id: InlineId,
458 },
459 SpaceMemberChanged {
461 space_id: InlineId,
463 user_id: InlineId,
465 removed: bool,
467 },
468 UserSettingsChanged {},
470 MessageActionInvoked {
472 interaction_id: InlineId,
474 chat_id: InlineId,
476 message_id: InlineId,
478 actor_user_id: InlineId,
480 action_id: String,
482 data: Vec<u8>,
484 },
485 MessageActionAnswered {
487 interaction_id: InlineId,
489 toast: Option<String>,
491 },
492 MessageUpserted {
494 chat_id: InlineId,
496 message_id: InlineId,
498 },
499 MessageStored {
501 message: MessageRecord,
503 },
504 MessageDeleted {
506 chat_id: InlineId,
508 message_id: InlineId,
510 },
511 ChatHistoryCleared {
513 chat_id: InlineId,
515 before_date: Option<i64>,
517 },
518 ReactionChanged {
520 chat_id: InlineId,
522 message_id: InlineId,
524 user_id: InlineId,
526 reaction: String,
528 removed: bool,
530 },
531 ReadStateChanged {
533 chat_id: InlineId,
535 },
536 Typing {
538 chat_id: InlineId,
540 user_id: InlineId,
542 is_typing: bool,
544 },
545 UserStatusChanged {
547 user_id: InlineId,
549 is_online: Option<bool>,
551 last_online: Option<i64>,
553 },
554 BotPresenceChanged {
556 bot_user_id: InlineId,
558 chat_id: Option<InlineId>,
560 kind: String,
562 comment: Option<String>,
564 avatar_changed: bool,
566 },
567 NewMessageNotification {
569 message: MessageRecord,
571 reason: String,
573 },
574}
575
576impl ClientEvent {
577 pub const fn reliability(&self) -> EventReliability {
579 match self {
580 Self::Typing { .. }
581 | Self::UserStatusChanged { .. }
582 | Self::BotPresenceChanged { .. }
583 | Self::NewMessageNotification { .. } => EventReliability::BestEffort,
584 Self::StatusChanged { .. }
585 | Self::TransactionChanged(_)
586 | Self::ChatUpserted { .. }
587 | Self::ChatDeleted { .. }
588 | Self::ChatParticipantsChanged { .. }
589 | Self::UserUpserted { .. }
590 | Self::SpaceUpserted { .. }
591 | Self::SpaceMemberChanged { .. }
592 | Self::UserSettingsChanged { .. }
593 | Self::MessageActionInvoked { .. }
594 | Self::MessageActionAnswered { .. }
595 | Self::MessageUpserted { .. }
596 | Self::MessageStored { .. }
597 | Self::MessageDeleted { .. }
598 | Self::ChatHistoryCleared { .. }
599 | Self::ReactionChanged { .. }
600 | Self::ReadStateChanged { .. } => EventReliability::Lossless,
601 }
602 }
603}
604
605pub mod prelude {
607 pub use crate::{
608 AddChatParticipantRequest, AuthCredential, AuthToken, BackendError, BackendResult,
609 ClientBackend, ClientCommandError, ClientError, ClientErrorCategory, ClientEvent,
610 ClientEventDelivery, ClientFailure, ClientIdentity, ClientRequestError, ClientRunner,
611 ClientStatus, ClientStore, ConnectRequest, DEFAULT_COMMAND_QUEUE_CAPACITY,
612 DEFAULT_EVENT_QUEUE_CAPACITY, DEFAULT_LOSSLESS_EVENT_QUEUE_CAPACITY, DeleteChatRequest,
613 DeleteMessageRequest, DialogFollowMode, DialogNotificationMode, DialogRecord, DialogsPage,
614 DialogsRequest, EditMessageRequest, EventReliability, ExternalId, FakeRealtimeAttempt,
615 FakeRealtimeConnector, HistoryPage, HistoryRequest, InMemoryBackend, InMemoryStore,
616 InlineClient, InlineClientBuilder, InlineClientRuntime, InlineId, LosslessEventDelivery,
617 LosslessEventReceiver, MessageContent, MessageMutation, MessageRecord, NotificationMode,
618 PeerRef, RandomId, ReactRequest, ReadRequest, RealtimeConnectRequest,
619 RealtimeConnectionInfo, RealtimeConnector, ReconnectPolicy, RemoveChatParticipantRequest,
620 SdkBackend, SdkBackendBuildError, SdkBackendBuilder, SdkRealtimeConnector, SendTextOutcome,
621 SendTextRequest, SetMarkedUnreadRequest, SpaceMemberRecord, SpaceMemberRole, SpaceRecord,
622 SqliteStore, StoreError, StoreResult, StoredReaction, StoredReadState, StoredSession,
623 StoredTransaction, SyncBucketKey, SyncBucketPeer, SyncBucketState, SyncConfig, SyncState,
624 TransactionEvent, TransactionId, TransactionIdentity, TransactionState, TypingRequest,
625 UpdateChatInfoRequest, UpdateDialogNotificationsRequest, UploadHandle, UploadRequest,
626 UserSettingsRecord, VERSION,
627 };
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633
634 #[test]
635 fn external_id_rejects_empty_values() {
636 assert_eq!(
637 ExternalId::try_new("host-event", " "),
638 Err(ClientError::EmptyField { field: "id" })
639 );
640 }
641
642 #[test]
643 fn transaction_identity_records_message_ids() {
644 let identity = TransactionIdentity::new(
645 TransactionId::try_new("txn-1").unwrap(),
646 Some(ExternalId::try_new("host-event", "event-1").unwrap()),
647 RandomId::new(42),
648 )
649 .with_temporary_message_id(-1)
650 .with_final_message_id(100);
651
652 assert_eq!(identity.random_id.get(), 42);
653 assert_eq!(identity.temporary_message_id.unwrap().get(), -1);
654 assert_eq!(identity.final_message_id.unwrap().get(), 100);
655 }
656
657 #[test]
658 fn typing_is_best_effort() {
659 let event = ClientEvent::Typing {
660 chat_id: InlineId::new(1),
661 user_id: InlineId::new(2),
662 is_typing: true,
663 };
664
665 assert_eq!(event.reliability(), EventReliability::BestEffort);
666 }
667}