1use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 ClientError, ClientFailure, ClientStatus, ExternalId, InlineId, RandomId, TransactionIdentity,
13 validate_token,
14};
15
16#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct AuthToken(String);
20
21impl AuthToken {
22 pub const MAX_LEN: usize = 8192;
24
25 pub fn try_new(value: impl AsRef<str>) -> Result<Self, ClientError> {
27 Ok(Self(
28 validate_token("auth_token", value.as_ref(), Self::MAX_LEN)?.to_owned(),
29 ))
30 }
31
32 pub fn expose_secret(&self) -> &str {
36 &self.0
37 }
38}
39
40impl fmt::Debug for AuthToken {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 f.write_str("AuthToken(\"[redacted]\")")
43 }
44}
45
46#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(tag = "type", rename_all = "snake_case")]
49pub enum AuthCredential {
50 AccessToken {
52 token: AuthToken,
54 },
55}
56
57impl AuthCredential {
58 pub fn access_token(&self) -> &AuthToken {
60 match self {
61 Self::AccessToken { token } => token,
62 }
63 }
64}
65
66impl fmt::Debug for AuthCredential {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 Self::AccessToken { .. } => f
70 .debug_struct("AccessToken")
71 .field("token", &"[redacted]")
72 .finish(),
73 }
74 }
75}
76
77#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
79#[serde(tag = "type", rename_all = "snake_case")]
80pub enum PeerRef {
81 User {
83 user_id: InlineId,
85 },
86 Chat {
88 chat_id: InlineId,
90 },
91 Thread {
93 thread_id: InlineId,
95 },
96}
97
98#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct ConnectRequest {
101 pub auth: AuthCredential,
103 pub account_namespace: Option<String>,
105}
106
107impl ConnectRequest {
108 pub fn new(auth: AuthCredential) -> Self {
110 Self {
111 auth,
112 account_namespace: None,
113 }
114 }
115
116 pub fn with_account_namespace(mut self, namespace: impl Into<String>) -> Self {
118 self.account_namespace = Some(namespace.into());
119 self
120 }
121}
122
123impl fmt::Debug for ConnectRequest {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 f.debug_struct("ConnectRequest")
126 .field("auth", &self.auth)
127 .field("account_namespace", &self.account_namespace)
128 .finish()
129 }
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum AuthContactKind {
136 Email,
138 Phone,
140}
141
142impl AuthContactKind {
143 pub const fn as_str(self) -> &'static str {
145 match self {
146 Self::Email => "email",
147 Self::Phone => "phone",
148 }
149 }
150}
151
152#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct AuthStartRequest {
155 pub contact: String,
157 pub kind: AuthContactKind,
159 pub device_name: Option<String>,
161}
162
163impl fmt::Debug for AuthStartRequest {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 f.debug_struct("AuthStartRequest")
166 .field("contact", &"[redacted]")
167 .field("kind", &self.kind)
168 .field("device_name", &self.device_name)
169 .finish()
170 }
171}
172
173#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct AuthStartResult {
176 pub existing_user: bool,
178 pub needs_invite_code: bool,
180 pub challenge_token: Option<String>,
182}
183
184impl fmt::Debug for AuthStartResult {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 f.debug_struct("AuthStartResult")
187 .field("existing_user", &self.existing_user)
188 .field("needs_invite_code", &self.needs_invite_code)
189 .field(
190 "challenge_token",
191 &self.challenge_token.as_ref().map(|_| "[redacted]"),
192 )
193 .finish()
194 }
195}
196
197#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct AuthVerifyRequest {
200 pub contact: String,
202 pub kind: AuthContactKind,
204 pub code: String,
206 pub challenge_token: Option<String>,
208 pub device_name: Option<String>,
210 pub account_namespace: Option<String>,
212}
213
214impl fmt::Debug for AuthVerifyRequest {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 f.debug_struct("AuthVerifyRequest")
217 .field("contact", &"[redacted]")
218 .field("kind", &self.kind)
219 .field("code", &"[redacted]")
220 .field(
221 "challenge_token",
222 &self.challenge_token.as_ref().map(|_| "[redacted]"),
223 )
224 .field("device_name", &self.device_name)
225 .field("account_namespace", &self.account_namespace)
226 .finish()
227 }
228}
229
230#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
232pub struct AuthVerifyResult {
233 pub user_id: InlineId,
235 pub account_namespace: String,
237 pub status: ClientStatusSnapshot,
239}
240
241#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
243pub struct DialogsRequest {
244 pub limit: Option<u32>,
246 pub cursor: Option<String>,
248}
249
250#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
252pub struct HistoryRequest {
253 pub chat_id: InlineId,
255 pub limit: Option<u32>,
257 pub before_message_id: Option<InlineId>,
259 pub after_message_id: Option<InlineId>,
261}
262
263#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
265pub struct ChatParticipantsRequest {
266 pub chat_id: InlineId,
268}
269
270#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
272pub struct ChatCreateParticipant {
273 pub user_id: InlineId,
275}
276
277impl From<InlineId> for ChatCreateParticipant {
278 fn from(user_id: InlineId) -> Self {
279 Self { user_id }
280 }
281}
282
283#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
285pub struct CreateDmRequest {
286 pub user_id: InlineId,
288}
289
290#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
292pub struct CreateThreadRequest {
293 pub title: Option<String>,
295 pub space_id: Option<InlineId>,
297 pub description: Option<String>,
299 pub emoji: Option<String>,
301 pub is_public: bool,
303 #[serde(default)]
305 pub participants: Vec<ChatCreateParticipant>,
306}
307
308#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
310pub struct CreateReplyThreadRequest {
311 pub parent_chat_id: InlineId,
313 pub parent_message_id: Option<InlineId>,
315 pub title: Option<String>,
317 pub description: Option<String>,
319 pub emoji: Option<String>,
321 #[serde(default)]
323 pub participants: Vec<ChatCreateParticipant>,
324}
325
326#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
328pub struct SendTextRequest {
329 pub peer: PeerRef,
331 pub text: String,
333 pub external_id: Option<ExternalId>,
335 pub random_id: Option<RandomId>,
337 pub reply_to_message_id: Option<InlineId>,
339}
340
341impl SendTextRequest {
342 pub fn new(peer: PeerRef, text: impl Into<String>) -> Self {
344 Self {
345 peer,
346 text: text.into(),
347 external_id: None,
348 random_id: None,
349 reply_to_message_id: None,
350 }
351 }
352}
353
354impl fmt::Debug for SendTextRequest {
355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356 f.debug_struct("SendTextRequest")
357 .field("peer", &self.peer)
358 .field("text_len", &self.text.len())
359 .field("external_id", &self.external_id)
360 .field("random_id", &self.random_id)
361 .field("reply_to_message_id", &self.reply_to_message_id)
362 .finish()
363 }
364}
365
366#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
368pub struct EditMessageRequest {
369 pub chat_id: InlineId,
371 pub message_id: InlineId,
373 pub text: String,
375 pub external_id: Option<ExternalId>,
377}
378
379impl fmt::Debug for EditMessageRequest {
380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381 f.debug_struct("EditMessageRequest")
382 .field("chat_id", &self.chat_id)
383 .field("message_id", &self.message_id)
384 .field("text_len", &self.text.len())
385 .field("external_id", &self.external_id)
386 .finish()
387 }
388}
389
390#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
392pub struct DeleteMessageRequest {
393 pub chat_id: InlineId,
395 pub message_id: InlineId,
397 pub external_id: Option<ExternalId>,
399}
400
401#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
403pub struct ReactRequest {
404 pub chat_id: InlineId,
406 pub message_id: InlineId,
408 pub reaction: String,
410 pub remove: bool,
412 pub external_id: Option<ExternalId>,
414}
415
416#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
418pub struct ReadRequest {
419 pub chat_id: InlineId,
421 pub max_message_id: Option<InlineId>,
423}
424
425#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
427pub struct TypingRequest {
428 pub chat_id: InlineId,
430 pub is_typing: bool,
432}
433
434#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
440pub struct UploadRequest {
441 pub peer: PeerRef,
443 pub kind: MediaKind,
445 pub file_name: Option<String>,
447 pub mime_type: Option<String>,
449 pub size_bytes: Option<u64>,
451 pub caption: Option<String>,
453 pub width: Option<u32>,
455 pub height: Option<u32>,
457 pub duration_ms: Option<u64>,
459 pub external_id: Option<ExternalId>,
461 pub random_id: Option<RandomId>,
463 pub reply_to_message_id: Option<InlineId>,
465}
466
467impl fmt::Debug for UploadRequest {
468 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469 f.debug_struct("UploadRequest")
470 .field("peer", &self.peer)
471 .field("kind", &self.kind)
472 .field("file_name", &self.file_name)
473 .field("mime_type", &self.mime_type)
474 .field("size_bytes", &self.size_bytes)
475 .field("caption_len", &self.caption.as_ref().map(String::len))
476 .field("width", &self.width)
477 .field("height", &self.height)
478 .field("duration_ms", &self.duration_ms)
479 .field("external_id", &self.external_id)
480 .field("random_id", &self.random_id)
481 .field("reply_to_message_id", &self.reply_to_message_id)
482 .finish()
483 }
484}
485
486#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
488pub struct ClientStatusSnapshot {
489 pub status: ClientStatus,
491 pub failure: Option<ClientFailure>,
493}
494
495impl ClientStatusSnapshot {
496 pub fn current(status: ClientStatus) -> Self {
498 Self {
499 status,
500 failure: None,
501 }
502 }
503}
504
505#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
507pub struct DialogRecord {
508 pub chat_id: InlineId,
510 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub peer_user_id: Option<InlineId>,
513 pub title: Option<String>,
515 pub last_message_id: Option<InlineId>,
517 pub synced_through_message_id: Option<InlineId>,
519 pub unread_count: Option<u32>,
521}
522
523#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
525pub struct DialogsPage {
526 pub dialogs: Vec<DialogRecord>,
528 #[serde(default)]
530 pub users: Vec<UserRecord>,
531 pub next_cursor: Option<String>,
533}
534
535#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
537pub struct UserRecord {
538 pub user_id: InlineId,
540 pub display_name: Option<String>,
542 pub username: Option<String>,
544 pub first_name: Option<String>,
546 pub last_name: Option<String>,
548 pub avatar_url: Option<String>,
550 pub is_bot: Option<bool>,
552}
553
554#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
556#[serde(tag = "type", rename_all = "snake_case")]
557#[non_exhaustive]
558pub enum MessageContent {
559 Text {
561 text: String,
563 },
564 Media {
566 kind: MediaKind,
568 file_id: String,
570 url: Option<String>,
572 mime_type: Option<String>,
574 file_name: Option<String>,
576 caption: Option<String>,
578 size_bytes: Option<u64>,
580 width: Option<u32>,
582 height: Option<u32>,
584 duration_ms: Option<u64>,
586 },
587 Unsupported {
589 reason: String,
591 },
592}
593
594#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
596#[serde(rename_all = "snake_case")]
597#[non_exhaustive]
598pub enum MediaKind {
599 Photo,
601 Video,
603 Document,
605 Voice,
607}
608
609impl fmt::Debug for MessageContent {
610 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
611 match self {
612 Self::Text { text } => f
613 .debug_struct("Text")
614 .field("text_len", &text.len())
615 .finish(),
616 Self::Media {
617 kind,
618 file_id,
619 url,
620 mime_type,
621 file_name,
622 caption,
623 size_bytes,
624 width,
625 height,
626 duration_ms,
627 } => f
628 .debug_struct("Media")
629 .field("kind", kind)
630 .field("file_id", file_id)
631 .field("has_url", &url.is_some())
632 .field("mime_type", mime_type)
633 .field("file_name", file_name)
634 .field("caption_len", &caption.as_ref().map(String::len))
635 .field("size_bytes", size_bytes)
636 .field("width", width)
637 .field("height", height)
638 .field("duration_ms", duration_ms)
639 .finish(),
640 Self::Unsupported { reason } => f
641 .debug_struct("Unsupported")
642 .field("reason", reason)
643 .finish(),
644 }
645 }
646}
647
648#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
650pub struct MessageRecord {
651 pub chat_id: InlineId,
653 pub message_id: InlineId,
655 pub sender_id: InlineId,
657 pub timestamp: i64,
659 pub is_outgoing: bool,
661 pub content: MessageContent,
663 pub reply_to_message_id: Option<InlineId>,
665 pub transaction: Option<TransactionIdentity>,
667}
668
669#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
671pub struct HistoryPage {
672 pub messages: Vec<MessageRecord>,
674 #[serde(default)]
676 pub users: Vec<UserRecord>,
677 pub has_more: bool,
679 pub next_cursor: Option<String>,
681}
682
683#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
685pub struct ChatParticipantRecord {
686 pub user_id: InlineId,
688 pub date: Option<i64>,
690}
691
692#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
694pub struct ChatParticipantsPage {
695 pub participants: Vec<ChatParticipantRecord>,
697 #[serde(default)]
699 pub users: Vec<UserRecord>,
700}
701
702#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
704pub struct CreatedChat {
705 pub chat_id: InlineId,
707 pub title: Option<String>,
709 pub parent_chat_id: Option<InlineId>,
711 pub parent_message_id: Option<InlineId>,
713}
714
715#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
717pub struct MessageMutation {
718 pub transaction: TransactionIdentity,
720 pub message_id: Option<InlineId>,
722}
723
724#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
726pub struct UploadHandle {
727 pub file_id: String,
729 pub mime_type: Option<String>,
731 pub size_bytes: Option<u64>,
733}
734
735#[cfg(test)]
736mod tests {
737 use super::*;
738 use crate::{TransactionId, TransactionIdentity};
739
740 #[test]
741 fn auth_token_debug_is_redacted() {
742 let token = AuthToken::try_new("secret-token").unwrap();
743
744 assert_eq!(format!("{token:?}"), "AuthToken(\"[redacted]\")");
745 assert_eq!(token.expose_secret(), "secret-token");
746 }
747
748 #[test]
749 fn send_text_debug_redacts_body() {
750 let req = SendTextRequest::new(
751 PeerRef::User {
752 user_id: InlineId::new(42),
753 },
754 "hello private world",
755 );
756 let rendered = format!("{req:?}");
757
758 assert!(rendered.contains("text_len"));
759 assert!(!rendered.contains("hello private world"));
760 }
761
762 #[test]
763 fn history_page_redacts_message_text_in_debug() {
764 let message = MessageRecord {
765 chat_id: InlineId::new(1),
766 message_id: InlineId::new(2),
767 sender_id: InlineId::new(3),
768 timestamp: 1_783_452_698,
769 is_outgoing: false,
770 content: MessageContent::Text {
771 text: "private message".to_owned(),
772 },
773 reply_to_message_id: None,
774 transaction: Some(TransactionIdentity::new(
775 TransactionId::try_new("txn-1").unwrap(),
776 None,
777 RandomId::new(1),
778 )),
779 };
780 let rendered = format!("{message:?}");
781
782 assert!(rendered.contains("text_len"));
783 assert!(!rendered.contains("private message"));
784 }
785}