1use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 ClientError, ClientFailure, ClientStatus, ExternalId, InlineId, RandomId, TransactionIdentity,
13 TransactionState, 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(
128 "account_namespace",
129 &self.account_namespace.as_ref().map(|_| "[redacted]"),
130 )
131 .finish()
132 }
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum AuthContactKind {
139 Email,
141 Phone,
143}
144
145impl AuthContactKind {
146 pub const fn as_str(self) -> &'static str {
148 match self {
149 Self::Email => "email",
150 Self::Phone => "phone",
151 }
152 }
153}
154
155#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub struct AuthStartRequest {
158 pub contact: String,
160 pub kind: AuthContactKind,
162 pub device_name: Option<String>,
164}
165
166impl fmt::Debug for AuthStartRequest {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 f.debug_struct("AuthStartRequest")
169 .field("contact", &"[redacted]")
170 .field("kind", &self.kind)
171 .field("device_name", &self.device_name)
172 .finish()
173 }
174}
175
176#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
178pub struct AuthStartResult {
179 pub existing_user: bool,
181 pub needs_invite_code: bool,
183 pub challenge_token: Option<String>,
185}
186
187impl fmt::Debug for AuthStartResult {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 f.debug_struct("AuthStartResult")
190 .field("existing_user", &self.existing_user)
191 .field("needs_invite_code", &self.needs_invite_code)
192 .field(
193 "challenge_token",
194 &self.challenge_token.as_ref().map(|_| "[redacted]"),
195 )
196 .finish()
197 }
198}
199
200#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct AuthVerifyRequest {
203 pub contact: String,
205 pub kind: AuthContactKind,
207 pub code: String,
209 pub challenge_token: Option<String>,
211 pub device_name: Option<String>,
213 pub account_namespace: Option<String>,
215}
216
217impl fmt::Debug for AuthVerifyRequest {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 f.debug_struct("AuthVerifyRequest")
220 .field("contact", &"[redacted]")
221 .field("kind", &self.kind)
222 .field("code", &"[redacted]")
223 .field(
224 "challenge_token",
225 &self.challenge_token.as_ref().map(|_| "[redacted]"),
226 )
227 .field("device_name", &self.device_name)
228 .field(
229 "account_namespace",
230 &self.account_namespace.as_ref().map(|_| "[redacted]"),
231 )
232 .finish()
233 }
234}
235
236#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
238pub struct AuthVerifyResult {
239 pub user_id: InlineId,
241 pub account_namespace: String,
243 pub status: ClientStatusSnapshot,
245}
246
247impl fmt::Debug for AuthVerifyResult {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 f.debug_struct("AuthVerifyResult")
250 .field("user_id", &self.user_id)
251 .field("account_namespace", &"[redacted]")
252 .field("status", &self.status)
253 .finish()
254 }
255}
256
257#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
259pub struct DialogsRequest {
260 pub limit: Option<u32>,
262 pub cursor: Option<String>,
264 #[serde(default)]
266 pub order: DialogsOrder,
267}
268
269#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
271#[serde(rename_all = "snake_case")]
272pub enum DialogsOrder {
273 #[default]
276 RecentActivity,
277 StableChatId,
279}
280
281#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
283pub struct HistoryRequest {
284 pub chat_id: InlineId,
286 pub limit: Option<u32>,
288 pub before_message_id: Option<InlineId>,
290 pub after_message_id: Option<InlineId>,
292}
293
294#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
296pub struct ChatParticipantsRequest {
297 pub chat_id: InlineId,
299}
300
301#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
303pub struct AddChatParticipantRequest {
304 pub chat_id: InlineId,
306 pub user_id: InlineId,
308}
309
310#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
312pub struct RemoveChatParticipantRequest {
313 pub chat_id: InlineId,
315 pub user_id: InlineId,
317}
318
319#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
323pub struct UpdateChatInfoRequest {
324 pub chat_id: InlineId,
326 pub title: Option<String>,
328 pub emoji: Option<String>,
330}
331
332#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
334pub struct DeleteChatRequest {
335 pub chat_id: InlineId,
337}
338
339#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
341pub struct ChatCreateParticipant {
342 pub user_id: InlineId,
344}
345
346impl From<InlineId> for ChatCreateParticipant {
347 fn from(user_id: InlineId) -> Self {
348 Self { user_id }
349 }
350}
351
352#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
354pub struct CreateDmRequest {
355 pub user_id: InlineId,
357}
358
359#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
361pub struct CreateThreadRequest {
362 pub title: Option<String>,
364 pub space_id: Option<InlineId>,
366 pub description: Option<String>,
368 pub emoji: Option<String>,
370 pub is_public: bool,
372 #[serde(default)]
374 pub participants: Vec<ChatCreateParticipant>,
375}
376
377#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
379pub struct CreateReplyThreadRequest {
380 pub parent_chat_id: InlineId,
382 pub parent_message_id: Option<InlineId>,
384 pub title: Option<String>,
386 pub description: Option<String>,
388 pub emoji: Option<String>,
390 #[serde(default)]
392 pub participants: Vec<ChatCreateParticipant>,
393}
394
395#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
397pub struct SendTextRequest {
398 pub peer: PeerRef,
400 pub text: String,
402 pub external_id: Option<ExternalId>,
404 pub random_id: Option<RandomId>,
406 pub reply_to_message_id: Option<InlineId>,
408}
409
410impl SendTextRequest {
411 pub fn new(peer: PeerRef, text: impl Into<String>) -> Self {
413 Self {
414 peer,
415 text: text.into(),
416 external_id: None,
417 random_id: None,
418 reply_to_message_id: None,
419 }
420 }
421}
422
423impl fmt::Debug for SendTextRequest {
424 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425 f.debug_struct("SendTextRequest")
426 .field("peer", &self.peer)
427 .field("text_len", &self.text.len())
428 .field("external_id", &self.external_id)
429 .field("random_id", &self.random_id)
430 .field("reply_to_message_id", &self.reply_to_message_id)
431 .finish()
432 }
433}
434
435#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
437pub struct EditMessageRequest {
438 pub chat_id: InlineId,
440 pub message_id: InlineId,
442 pub text: String,
444 pub external_id: Option<ExternalId>,
446}
447
448impl fmt::Debug for EditMessageRequest {
449 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450 f.debug_struct("EditMessageRequest")
451 .field("chat_id", &self.chat_id)
452 .field("message_id", &self.message_id)
453 .field("text_len", &self.text.len())
454 .field("external_id", &self.external_id)
455 .finish()
456 }
457}
458
459#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
461pub struct DeleteMessageRequest {
462 pub chat_id: InlineId,
464 pub message_id: InlineId,
466 pub external_id: Option<ExternalId>,
468}
469
470#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
472pub struct ReactRequest {
473 pub chat_id: InlineId,
475 pub message_id: InlineId,
477 pub reaction: String,
479 pub remove: bool,
481 pub external_id: Option<ExternalId>,
483}
484
485#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
487pub struct ReadRequest {
488 pub chat_id: InlineId,
490 pub max_message_id: Option<InlineId>,
492}
493
494#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
496pub struct SetMarkedUnreadRequest {
497 pub chat_id: InlineId,
499 pub unread: bool,
501}
502
503#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
505pub struct UpdateDialogNotificationsRequest {
506 pub chat_id: InlineId,
508 pub mode: Option<DialogNotificationMode>,
510}
511
512#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
514pub struct TypingRequest {
515 pub chat_id: InlineId,
517 pub is_typing: bool,
519}
520
521#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
527pub struct UploadRequest {
528 pub peer: PeerRef,
530 pub kind: MediaKind,
532 pub file_name: Option<String>,
534 pub mime_type: Option<String>,
536 pub size_bytes: Option<u64>,
538 pub caption: Option<String>,
540 pub width: Option<u32>,
542 pub height: Option<u32>,
544 pub duration_ms: Option<u64>,
546 pub external_id: Option<ExternalId>,
548 pub random_id: Option<RandomId>,
550 pub reply_to_message_id: Option<InlineId>,
552}
553
554impl fmt::Debug for UploadRequest {
555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556 f.debug_struct("UploadRequest")
557 .field("peer", &self.peer)
558 .field("kind", &self.kind)
559 .field("file_name", &self.file_name)
560 .field("mime_type", &self.mime_type)
561 .field("size_bytes", &self.size_bytes)
562 .field("caption_len", &self.caption.as_ref().map(String::len))
563 .field("width", &self.width)
564 .field("height", &self.height)
565 .field("duration_ms", &self.duration_ms)
566 .field("external_id", &self.external_id)
567 .field("random_id", &self.random_id)
568 .field("reply_to_message_id", &self.reply_to_message_id)
569 .finish()
570 }
571}
572
573#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
575pub struct ClientStatusSnapshot {
576 pub status: ClientStatus,
578 pub failure: Option<ClientFailure>,
580}
581
582impl ClientStatusSnapshot {
583 pub fn current(status: ClientStatus) -> Self {
585 Self {
586 status,
587 failure: None,
588 }
589 }
590}
591
592#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
594pub struct DialogRecord {
595 pub chat_id: InlineId,
597 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub peer_user_id: Option<InlineId>,
600 pub title: Option<String>,
602 #[serde(default, skip_serializing_if = "Option::is_none")]
604 pub emoji: Option<String>,
605 pub last_message_id: Option<InlineId>,
607 pub synced_through_message_id: Option<InlineId>,
609 pub unread_count: Option<u32>,
611 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub space_id: Option<InlineId>,
614 #[serde(default, skip_serializing_if = "Option::is_none")]
616 pub is_public: Option<bool>,
617 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub archived: Option<bool>,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub pinned: Option<bool>,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub open: Option<bool>,
626 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub chat_list_hidden: Option<bool>,
629 #[serde(default, skip_serializing_if = "Option::is_none")]
631 pub order: Option<String>,
632 #[serde(default, skip_serializing_if = "Option::is_none")]
634 pub pinned_order: Option<String>,
635 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub notification_mode: Option<DialogNotificationMode>,
638 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub follow_mode: Option<DialogFollowMode>,
641 #[serde(default, skip_serializing_if = "Vec::is_empty")]
643 pub pinned_message_ids: Vec<InlineId>,
644}
645
646impl DialogRecord {
647 pub fn new(chat_id: InlineId) -> Self {
649 Self {
650 chat_id,
651 peer_user_id: None,
652 title: None,
653 emoji: None,
654 last_message_id: None,
655 synced_through_message_id: None,
656 unread_count: None,
657 space_id: None,
658 is_public: None,
659 archived: None,
660 pinned: None,
661 open: None,
662 chat_list_hidden: None,
663 order: None,
664 pinned_order: None,
665 notification_mode: None,
666 follow_mode: None,
667 pinned_message_ids: Vec::new(),
668 }
669 }
670}
671
672#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
674#[serde(rename_all = "snake_case")]
675pub enum DialogNotificationMode {
676 All,
678 Mentions,
680 None,
682}
683
684#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
686#[serde(rename_all = "snake_case")]
687pub enum DialogFollowMode {
688 Following,
690 Unfollowed,
692}
693
694#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
696pub struct DialogsPage {
697 pub dialogs: Vec<DialogRecord>,
699 #[serde(default)]
701 pub users: Vec<UserRecord>,
702 pub next_cursor: Option<String>,
704}
705
706#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
708pub struct UserRecord {
709 pub user_id: InlineId,
711 pub display_name: Option<String>,
713 pub username: Option<String>,
715 pub first_name: Option<String>,
717 pub last_name: Option<String>,
719 pub avatar_url: Option<String>,
721 pub is_bot: Option<bool>,
723}
724
725#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
727pub struct SpaceRecord {
728 pub space_id: InlineId,
730 pub name: String,
732 pub creator: bool,
734 pub date: i64,
736 pub is_public: Option<bool>,
738 #[serde(default, skip_serializing_if = "Option::is_none")]
740 pub grid_enabled: Option<bool>,
741}
742
743#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
745#[serde(rename_all = "snake_case")]
746pub enum SpaceMemberRole {
747 Owner,
749 Admin,
751 Member,
753}
754
755#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
757pub struct SpaceMemberRecord {
758 pub space_id: InlineId,
760 pub user_id: InlineId,
762 pub role: Option<SpaceMemberRole>,
764 pub date: i64,
766 pub can_access_public_chats: bool,
768}
769
770#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
772#[serde(rename_all = "snake_case")]
773pub enum NotificationMode {
774 All,
776 None,
778 Mentions,
780 ImportantOnly,
782 OnlyMentions,
784}
785
786#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
788pub struct UserSettingsRecord {
789 pub notification_mode: Option<NotificationMode>,
791 pub silent: Option<bool>,
793 pub zen_mode_requires_mention: Option<bool>,
795 pub zen_mode_uses_default_rules: Option<bool>,
797 pub zen_mode_custom_rules: Option<String>,
799 pub disable_dm_notifications: Option<bool>,
801}
802
803#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(tag = "type", rename_all = "snake_case")]
806#[non_exhaustive]
807pub enum MessageContent {
808 Text {
810 text: String,
812 },
813 Media {
815 kind: MediaKind,
817 file_id: String,
819 url: Option<String>,
821 mime_type: Option<String>,
823 file_name: Option<String>,
825 caption: Option<String>,
827 size_bytes: Option<u64>,
829 width: Option<u32>,
831 height: Option<u32>,
833 duration_ms: Option<u64>,
835 },
836 Unsupported {
838 reason: String,
840 },
841}
842
843#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
845#[serde(rename_all = "snake_case")]
846#[non_exhaustive]
847pub enum MediaKind {
848 Photo,
850 Video,
852 Document,
854 Voice,
856}
857
858impl fmt::Debug for MessageContent {
859 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
860 match self {
861 Self::Text { text } => f
862 .debug_struct("Text")
863 .field("text_len", &text.len())
864 .finish(),
865 Self::Media {
866 kind,
867 file_id,
868 url,
869 mime_type,
870 file_name,
871 caption,
872 size_bytes,
873 width,
874 height,
875 duration_ms,
876 } => f
877 .debug_struct("Media")
878 .field("kind", kind)
879 .field("file_id", file_id)
880 .field("has_url", &url.is_some())
881 .field("mime_type", mime_type)
882 .field("file_name", file_name)
883 .field("caption_len", &caption.as_ref().map(String::len))
884 .field("size_bytes", size_bytes)
885 .field("width", width)
886 .field("height", height)
887 .field("duration_ms", duration_ms)
888 .finish(),
889 Self::Unsupported { reason } => f
890 .debug_struct("Unsupported")
891 .field("reason", reason)
892 .finish(),
893 }
894 }
895}
896
897#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
899pub struct MessageRecord {
900 pub chat_id: InlineId,
902 pub message_id: InlineId,
904 pub sender_id: InlineId,
906 pub timestamp: i64,
908 pub is_outgoing: bool,
910 pub content: MessageContent,
912 pub reply_to_message_id: Option<InlineId>,
914 pub transaction: Option<TransactionIdentity>,
916}
917
918#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
920pub struct HistoryPage {
921 pub messages: Vec<MessageRecord>,
923 #[serde(default)]
925 pub users: Vec<UserRecord>,
926 pub has_more: bool,
928 pub next_cursor: Option<String>,
930}
931
932#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
934pub struct ChatParticipantRecord {
935 pub user_id: InlineId,
937 pub date: Option<i64>,
939}
940
941#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
943pub struct ChatParticipantsPage {
944 pub participants: Vec<ChatParticipantRecord>,
946 #[serde(default)]
948 pub users: Vec<UserRecord>,
949}
950
951#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
953pub struct CreatedChat {
954 pub chat_id: InlineId,
956 pub title: Option<String>,
958 pub parent_chat_id: Option<InlineId>,
960 pub parent_message_id: Option<InlineId>,
962}
963
964#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
966pub struct MessageMutation {
967 pub transaction: TransactionIdentity,
969 pub message_id: Option<InlineId>,
971 #[serde(default, skip_serializing_if = "Option::is_none")]
973 pub state: Option<TransactionState>,
974 #[serde(default, skip_serializing_if = "Option::is_none")]
976 pub failure: Option<ClientFailure>,
977}
978
979#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
981pub struct UploadHandle {
982 pub file_id: String,
984 pub mime_type: Option<String>,
986 pub size_bytes: Option<u64>,
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993 use crate::{TransactionId, TransactionIdentity};
994
995 #[test]
996 fn auth_token_debug_is_redacted() {
997 let token = AuthToken::try_new("secret-token").unwrap();
998
999 assert_eq!(format!("{token:?}"), "AuthToken(\"[redacted]\")");
1000 assert_eq!(token.expose_secret(), "secret-token");
1001 }
1002
1003 #[test]
1004 fn session_namespace_debug_is_redacted() {
1005 let request = ConnectRequest::new(AuthCredential::AccessToken {
1006 token: AuthToken::try_new("token").unwrap(),
1007 })
1008 .with_account_namespace("secret-namespace");
1009
1010 let rendered = format!("{request:?}");
1011 assert!(rendered.contains("[redacted]"));
1012 assert!(!rendered.contains("secret-namespace"));
1013 }
1014
1015 #[test]
1016 fn send_text_debug_redacts_body() {
1017 let req = SendTextRequest::new(
1018 PeerRef::User {
1019 user_id: InlineId::new(42),
1020 },
1021 "hello private world",
1022 );
1023 let rendered = format!("{req:?}");
1024
1025 assert!(rendered.contains("text_len"));
1026 assert!(!rendered.contains("hello private world"));
1027 }
1028
1029 #[test]
1030 fn history_page_redacts_message_text_in_debug() {
1031 let message = MessageRecord {
1032 chat_id: InlineId::new(1),
1033 message_id: InlineId::new(2),
1034 sender_id: InlineId::new(3),
1035 timestamp: 1_783_452_698,
1036 is_outgoing: false,
1037 content: MessageContent::Text {
1038 text: "private message".to_owned(),
1039 },
1040 reply_to_message_id: None,
1041 transaction: Some(TransactionIdentity::new(
1042 TransactionId::try_new("txn-1").unwrap(),
1043 None,
1044 RandomId::new(1),
1045 )),
1046 };
1047 let rendered = format!("{message:?}");
1048
1049 assert!(rendered.contains("text_len"));
1050 assert!(!rendered.contains("private message"));
1051 }
1052}