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 store;
24
25pub mod types;
27
28pub use backend::{
29 BackendError, BackendResult, ClientBackend, InMemoryBackend, OperationOutcome, SendTextOutcome,
30};
31pub use inline_sdk::ClientIdentity;
32pub use realtime::{
33 FakeRealtimeAttempt, FakeRealtimeConnector, RealtimeConnectRequest, RealtimeConnectionInfo,
34 RealtimeConnector, SdkRealtimeConnector,
35};
36pub use runtime::{
37 ClientCommandError, ClientRequestError, ClientRunner, DEFAULT_COMMAND_QUEUE_CAPACITY,
38 DEFAULT_EVENT_QUEUE_CAPACITY, InlineClient, InlineClientBuilder, InlineClientRuntime,
39};
40pub use sdk_backend::{SdkBackend, SdkBackendBuildError, SdkBackendBuilder};
41pub use store::{
42 ClientStore, InMemoryStore, SqliteStore, StoreError, StoreResult, StoredSession,
43 StoredTransaction,
44};
45pub use types::{
46 AuthContactKind, AuthCredential, AuthStartRequest, AuthStartResult, AuthToken,
47 AuthVerifyRequest, AuthVerifyResult, ChatCreateParticipant, ChatParticipantRecord,
48 ChatParticipantsPage, ChatParticipantsRequest, ClientStatusSnapshot, ConnectRequest,
49 CreateDmRequest, CreateReplyThreadRequest, CreateThreadRequest, CreatedChat,
50 DeleteMessageRequest, DialogRecord, DialogsPage, DialogsRequest, EditMessageRequest,
51 HistoryPage, HistoryRequest, MediaKind, MessageContent, MessageMutation, MessageRecord,
52 PeerRef, ReactRequest, ReadRequest, SendTextRequest, TypingRequest, UploadHandle,
53 UploadRequest, UserRecord,
54};
55
56pub const VERSION: &str = env!("CARGO_PKG_VERSION");
58
59#[derive(Clone, Debug, Error, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum ClientError {
63 #[error("{field} must not be empty")]
65 EmptyField {
66 field: &'static str,
68 },
69
70 #[error("{field} is too long ({len} > {max})")]
72 FieldTooLong {
73 field: &'static str,
75 len: usize,
77 max: usize,
79 },
80
81 #[error("{field} must not contain ASCII control characters")]
83 ControlCharacter {
84 field: &'static str,
86 },
87}
88
89fn validate_token<'a>(
90 field: &'static str,
91 value: &'a str,
92 max_len: usize,
93) -> Result<&'a str, ClientError> {
94 let trimmed = value.trim();
95 if trimmed.is_empty() {
96 return Err(ClientError::EmptyField { field });
97 }
98 if trimmed.len() > max_len {
99 return Err(ClientError::FieldTooLong {
100 field,
101 len: trimmed.len(),
102 max: max_len,
103 });
104 }
105 if trimmed.chars().any(|ch| ch.is_ascii_control()) {
106 return Err(ClientError::ControlCharacter { field });
107 }
108 Ok(trimmed)
109}
110
111#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
117#[serde(transparent)]
118pub struct InlineId(i64);
119
120impl InlineId {
121 pub const fn new(value: i64) -> Self {
123 Self(value)
124 }
125
126 pub const fn get(self) -> i64 {
128 self.0
129 }
130}
131
132impl From<i64> for InlineId {
133 fn from(value: i64) -> Self {
134 Self::new(value)
135 }
136}
137
138impl fmt::Display for InlineId {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 self.0.fmt(f)
141 }
142}
143
144#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
149#[serde(transparent)]
150pub struct RandomId(i64);
151
152impl RandomId {
153 pub const fn new(value: i64) -> Self {
155 Self(value)
156 }
157
158 pub const fn get(self) -> i64 {
160 self.0
161 }
162}
163
164impl From<i64> for RandomId {
165 fn from(value: i64) -> Self {
166 Self::new(value)
167 }
168}
169
170impl fmt::Display for RandomId {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 self.0.fmt(f)
173 }
174}
175
176#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
182pub struct ExternalId {
183 source: String,
184 id: String,
185}
186
187impl ExternalId {
188 pub const MAX_SOURCE_LEN: usize = 64;
190
191 pub const MAX_ID_LEN: usize = 512;
193
194 pub fn try_new(source: impl AsRef<str>, id: impl AsRef<str>) -> Result<Self, ClientError> {
196 let source = validate_token("source", source.as_ref(), Self::MAX_SOURCE_LEN)?;
197 let id = validate_token("id", id.as_ref(), Self::MAX_ID_LEN)?;
198 Ok(Self {
199 source: source.to_owned(),
200 id: id.to_owned(),
201 })
202 }
203
204 pub fn source(&self) -> &str {
206 &self.source
207 }
208
209 pub fn id(&self) -> &str {
211 &self.id
212 }
213}
214
215#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
217#[serde(transparent)]
218pub struct TransactionId(String);
219
220impl TransactionId {
221 pub const MAX_LEN: usize = 128;
223
224 pub fn try_new(value: impl AsRef<str>) -> Result<Self, ClientError> {
226 Ok(Self(
227 validate_token("transaction_id", value.as_ref(), Self::MAX_LEN)?.to_owned(),
228 ))
229 }
230
231 pub fn as_str(&self) -> &str {
233 &self.0
234 }
235}
236
237impl fmt::Display for TransactionId {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 self.0.fmt(f)
240 }
241}
242
243impl FromStr for TransactionId {
244 type Err = ClientError;
245
246 fn from_str(value: &str) -> Result<Self, Self::Err> {
247 Self::try_new(value)
248 }
249}
250
251#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
253pub struct TransactionIdentity {
254 pub transaction_id: TransactionId,
256 pub external_id: Option<ExternalId>,
258 pub random_id: RandomId,
260 pub temporary_message_id: Option<InlineId>,
262 pub final_message_id: Option<InlineId>,
264}
265
266impl TransactionIdentity {
267 pub fn new(
269 transaction_id: TransactionId,
270 external_id: Option<ExternalId>,
271 random_id: RandomId,
272 ) -> Self {
273 Self {
274 transaction_id,
275 external_id,
276 random_id,
277 temporary_message_id: None,
278 final_message_id: None,
279 }
280 }
281
282 pub fn with_temporary_message_id(mut self, message_id: impl Into<InlineId>) -> Self {
284 self.temporary_message_id = Some(message_id.into());
285 self
286 }
287
288 pub fn with_final_message_id(mut self, message_id: impl Into<InlineId>) -> Self {
290 self.final_message_id = Some(message_id.into());
291 self
292 }
293}
294
295#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
297#[non_exhaustive]
298pub enum ClientStatus {
299 Disconnected,
301 Connecting,
303 Connected,
305 Reconnecting,
307 AuthRequired,
309 AuthExpired,
311 LoggedOut,
313 ShuttingDown,
315}
316
317#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
319#[non_exhaustive]
320pub enum ClientErrorCategory {
321 InvalidInput,
323 AuthRequired,
325 AuthExpired,
327 ReloginRequired,
329 Network,
331 Timeout,
333 RateLimited,
335 ProtocolMismatch,
337 NotFound,
339 PermissionDenied,
341 Unsupported,
343 Conflict,
345 Internal,
347}
348
349#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
351pub struct ClientFailure {
352 pub category: ClientErrorCategory,
354 pub message: String,
356}
357
358impl ClientFailure {
359 pub fn new(category: ClientErrorCategory, message: impl Into<String>) -> Self {
361 Self {
362 category,
363 message: message.into(),
364 }
365 }
366}
367
368#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
370#[non_exhaustive]
371pub enum TransactionState {
372 Queued,
374 Sent,
376 Acked,
378 Completed,
380 Failed,
382 Cancelled,
384}
385
386#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
388pub struct TransactionEvent {
389 pub identity: TransactionIdentity,
391 pub state: TransactionState,
393 pub failure: Option<ClientFailure>,
395}
396
397#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
399pub enum EventReliability {
400 Lossless,
402 BestEffort,
404}
405
406#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
408#[non_exhaustive]
409pub enum ClientEvent {
410 StatusChanged {
412 status: ClientStatus,
414 failure: Option<ClientFailure>,
416 },
417 TransactionChanged(TransactionEvent),
419 ChatUpserted {
421 chat_id: InlineId,
423 },
424 UserUpserted {
426 user_id: InlineId,
428 },
429 MessageUpserted {
431 chat_id: InlineId,
433 message_id: InlineId,
435 },
436 MessageStored {
438 message: MessageRecord,
440 },
441 MessageDeleted {
443 chat_id: InlineId,
445 message_id: InlineId,
447 },
448 ReactionChanged {
450 chat_id: InlineId,
452 message_id: InlineId,
454 user_id: InlineId,
456 reaction: String,
458 removed: bool,
460 },
461 ReadStateChanged {
463 chat_id: InlineId,
465 },
466 Typing {
468 chat_id: InlineId,
470 user_id: InlineId,
472 is_typing: bool,
474 },
475}
476
477impl ClientEvent {
478 pub const fn reliability(&self) -> EventReliability {
480 match self {
481 Self::Typing { .. } => EventReliability::BestEffort,
482 Self::StatusChanged { .. }
483 | Self::TransactionChanged(_)
484 | Self::ChatUpserted { .. }
485 | Self::UserUpserted { .. }
486 | Self::MessageUpserted { .. }
487 | Self::MessageStored { .. }
488 | Self::MessageDeleted { .. }
489 | Self::ReactionChanged { .. }
490 | Self::ReadStateChanged { .. } => EventReliability::Lossless,
491 }
492 }
493}
494
495pub mod prelude {
497 pub use crate::{
498 AuthCredential, AuthToken, BackendError, BackendResult, ClientBackend, ClientCommandError,
499 ClientError, ClientErrorCategory, ClientEvent, ClientFailure, ClientIdentity,
500 ClientRequestError, ClientRunner, ClientStatus, ClientStore, ConnectRequest,
501 DEFAULT_COMMAND_QUEUE_CAPACITY, DEFAULT_EVENT_QUEUE_CAPACITY, DeleteMessageRequest,
502 DialogRecord, DialogsPage, DialogsRequest, EditMessageRequest, EventReliability,
503 ExternalId, FakeRealtimeAttempt, FakeRealtimeConnector, HistoryPage, HistoryRequest,
504 InMemoryBackend, InMemoryStore, InlineClient, InlineClientBuilder, InlineClientRuntime,
505 InlineId, MessageContent, MessageMutation, MessageRecord, PeerRef, RandomId, ReactRequest,
506 ReadRequest, RealtimeConnectRequest, RealtimeConnectionInfo, RealtimeConnector, SdkBackend,
507 SdkBackendBuildError, SdkBackendBuilder, SdkRealtimeConnector, SendTextOutcome,
508 SendTextRequest, SqliteStore, StoreError, StoreResult, StoredSession, StoredTransaction,
509 TransactionEvent, TransactionId, TransactionIdentity, TransactionState, TypingRequest,
510 UploadHandle, UploadRequest, VERSION,
511 };
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn external_id_rejects_empty_values() {
520 assert_eq!(
521 ExternalId::try_new("host-event", " "),
522 Err(ClientError::EmptyField { field: "id" })
523 );
524 }
525
526 #[test]
527 fn transaction_identity_records_message_ids() {
528 let identity = TransactionIdentity::new(
529 TransactionId::try_new("txn-1").unwrap(),
530 Some(ExternalId::try_new("host-event", "event-1").unwrap()),
531 RandomId::new(42),
532 )
533 .with_temporary_message_id(-1)
534 .with_final_message_id(100);
535
536 assert_eq!(identity.random_id.get(), 42);
537 assert_eq!(identity.temporary_message_id.unwrap().get(), -1);
538 assert_eq!(identity.final_message_id.unwrap().get(), 100);
539 }
540
541 #[test]
542 fn typing_is_best_effort() {
543 let event = ClientEvent::Typing {
544 chat_id: InlineId::new(1),
545 user_id: InlineId::new(2),
546 is_typing: true,
547 };
548
549 assert_eq!(event.reliability(), EventReliability::BestEffort);
550 }
551}