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 pub title: Option<String>,
512 pub last_message_id: Option<InlineId>,
514 pub synced_through_message_id: Option<InlineId>,
516 pub unread_count: Option<u32>,
518}
519
520#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
522pub struct DialogsPage {
523 pub dialogs: Vec<DialogRecord>,
525 #[serde(default)]
527 pub users: Vec<UserRecord>,
528 pub next_cursor: Option<String>,
530}
531
532#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
534pub struct UserRecord {
535 pub user_id: InlineId,
537 pub display_name: Option<String>,
539 pub username: Option<String>,
541 pub first_name: Option<String>,
543 pub last_name: Option<String>,
545 pub avatar_url: Option<String>,
547 pub is_bot: Option<bool>,
549}
550
551#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
553#[serde(tag = "type", rename_all = "snake_case")]
554#[non_exhaustive]
555pub enum MessageContent {
556 Text {
558 text: String,
560 },
561 Media {
563 kind: MediaKind,
565 file_id: String,
567 url: Option<String>,
569 mime_type: Option<String>,
571 file_name: Option<String>,
573 caption: Option<String>,
575 size_bytes: Option<u64>,
577 width: Option<u32>,
579 height: Option<u32>,
581 duration_ms: Option<u64>,
583 },
584 Unsupported {
586 reason: String,
588 },
589}
590
591#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
593#[serde(rename_all = "snake_case")]
594#[non_exhaustive]
595pub enum MediaKind {
596 Photo,
598 Video,
600 Document,
602 Voice,
604}
605
606impl fmt::Debug for MessageContent {
607 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608 match self {
609 Self::Text { text } => f
610 .debug_struct("Text")
611 .field("text_len", &text.len())
612 .finish(),
613 Self::Media {
614 kind,
615 file_id,
616 url,
617 mime_type,
618 file_name,
619 caption,
620 size_bytes,
621 width,
622 height,
623 duration_ms,
624 } => f
625 .debug_struct("Media")
626 .field("kind", kind)
627 .field("file_id", file_id)
628 .field("has_url", &url.is_some())
629 .field("mime_type", mime_type)
630 .field("file_name", file_name)
631 .field("caption_len", &caption.as_ref().map(String::len))
632 .field("size_bytes", size_bytes)
633 .field("width", width)
634 .field("height", height)
635 .field("duration_ms", duration_ms)
636 .finish(),
637 Self::Unsupported { reason } => f
638 .debug_struct("Unsupported")
639 .field("reason", reason)
640 .finish(),
641 }
642 }
643}
644
645#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
647pub struct MessageRecord {
648 pub chat_id: InlineId,
650 pub message_id: InlineId,
652 pub sender_id: InlineId,
654 pub timestamp: i64,
656 pub is_outgoing: bool,
658 pub content: MessageContent,
660 pub reply_to_message_id: Option<InlineId>,
662 pub transaction: Option<TransactionIdentity>,
664}
665
666#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
668pub struct HistoryPage {
669 pub messages: Vec<MessageRecord>,
671 #[serde(default)]
673 pub users: Vec<UserRecord>,
674 pub has_more: bool,
676 pub next_cursor: Option<String>,
678}
679
680#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
682pub struct ChatParticipantRecord {
683 pub user_id: InlineId,
685 pub date: Option<i64>,
687}
688
689#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
691pub struct ChatParticipantsPage {
692 pub participants: Vec<ChatParticipantRecord>,
694 #[serde(default)]
696 pub users: Vec<UserRecord>,
697}
698
699#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
701pub struct CreatedChat {
702 pub chat_id: InlineId,
704 pub title: Option<String>,
706 pub parent_chat_id: Option<InlineId>,
708 pub parent_message_id: Option<InlineId>,
710}
711
712#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
714pub struct MessageMutation {
715 pub transaction: TransactionIdentity,
717 pub message_id: Option<InlineId>,
719}
720
721#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
723pub struct UploadHandle {
724 pub file_id: String,
726 pub mime_type: Option<String>,
728 pub size_bytes: Option<u64>,
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use crate::{TransactionId, TransactionIdentity};
736
737 #[test]
738 fn auth_token_debug_is_redacted() {
739 let token = AuthToken::try_new("secret-token").unwrap();
740
741 assert_eq!(format!("{token:?}"), "AuthToken(\"[redacted]\")");
742 assert_eq!(token.expose_secret(), "secret-token");
743 }
744
745 #[test]
746 fn send_text_debug_redacts_body() {
747 let req = SendTextRequest::new(
748 PeerRef::User {
749 user_id: InlineId::new(42),
750 },
751 "hello private world",
752 );
753 let rendered = format!("{req:?}");
754
755 assert!(rendered.contains("text_len"));
756 assert!(!rendered.contains("hello private world"));
757 }
758
759 #[test]
760 fn history_page_redacts_message_text_in_debug() {
761 let message = MessageRecord {
762 chat_id: InlineId::new(1),
763 message_id: InlineId::new(2),
764 sender_id: InlineId::new(3),
765 timestamp: 1_783_452_698,
766 is_outgoing: false,
767 content: MessageContent::Text {
768 text: "private message".to_owned(),
769 },
770 reply_to_message_id: None,
771 transaction: Some(TransactionIdentity::new(
772 TransactionId::try_new("txn-1").unwrap(),
773 None,
774 RandomId::new(1),
775 )),
776 };
777 let rendered = format!("{message:?}");
778
779 assert!(rendered.contains("text_len"));
780 assert!(!rendered.contains("private message"));
781 }
782}