Skip to main content

inline_client/
types.rs

1//! Native Inline client request and record types.
2//!
3//! These are first-class Inline concepts. Bridge protocol envelopes, HTTP
4//! routes, adapter DTOs, and process-management details belong in adapter
5//! adapter crates, not in `inline-client`.
6
7use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    ClientError, ClientFailure, ClientStatus, ExternalId, InlineId, RandomId, TransactionIdentity,
13    validate_token,
14};
15
16/// Secret bearer-style auth token accepted by the client connect command.
17#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct AuthToken(String);
20
21impl AuthToken {
22    /// Maximum token length in bytes.
23    pub const MAX_LEN: usize = 8192;
24
25    /// Creates a validated auth token.
26    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    /// Exposes the token for transport/auth code.
33    ///
34    /// Callers should avoid logging this value.
35    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/// Credential supplied during client connect.
47#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(tag = "type", rename_all = "snake_case")]
49pub enum AuthCredential {
50    /// Existing Inline access token.
51    AccessToken {
52        /// Secret access token.
53        token: AuthToken,
54    },
55}
56
57impl AuthCredential {
58    /// Returns the access token for credential types that carry one.
59    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/// Inline peer reference used by client operations.
78#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
79#[serde(tag = "type", rename_all = "snake_case")]
80pub enum PeerRef {
81    /// Direct-message peer by user ID.
82    User {
83        /// Inline user ID.
84        user_id: InlineId,
85    },
86    /// Chat peer by chat ID.
87    Chat {
88        /// Inline chat ID.
89        chat_id: InlineId,
90    },
91    /// Thread peer by thread ID, used by existing Inline APIs that expose threads.
92    Thread {
93        /// Inline thread ID.
94        thread_id: InlineId,
95    },
96}
97
98/// Request to connect or reconnect the client.
99#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct ConnectRequest {
101    /// Auth credential.
102    pub auth: AuthCredential,
103    /// Optional account/store namespace chosen by the host.
104    pub account_namespace: Option<String>,
105}
106
107impl ConnectRequest {
108    /// Creates a connect request.
109    pub fn new(auth: AuthCredential) -> Self {
110        Self {
111            auth,
112            account_namespace: None,
113        }
114    }
115
116    /// Sets the account/store namespace.
117    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/// Contact method used for client-owned Inline login.
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum AuthContactKind {
136    /// Email code login.
137    Email,
138    /// SMS code login.
139    Phone,
140}
141
142impl AuthContactKind {
143    /// Returns the stable wire string for this contact kind.
144    pub const fn as_str(self) -> &'static str {
145        match self {
146            Self::Email => "email",
147            Self::Phone => "phone",
148        }
149    }
150}
151
152/// Request to send an Inline login code.
153#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct AuthStartRequest {
155    /// Contact address or phone number.
156    pub contact: String,
157    /// Contact kind.
158    pub kind: AuthContactKind,
159    /// Optional human-readable device name.
160    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/// Response from sending an Inline login code.
174#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct AuthStartResult {
176    /// Whether the contact belongs to an existing user.
177    pub existing_user: bool,
178    /// Whether the flow needs an invite code before login can continue.
179    pub needs_invite_code: bool,
180    /// Opaque challenge token required by some email verification flows.
181    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/// Request to verify an Inline login code.
198#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct AuthVerifyRequest {
200    /// Contact address or phone number used in the start step.
201    pub contact: String,
202    /// Contact kind.
203    pub kind: AuthContactKind,
204    /// Verification code from email or SMS.
205    pub code: String,
206    /// Opaque email challenge token from the start step, when present.
207    pub challenge_token: Option<String>,
208    /// Optional human-readable device name.
209    pub device_name: Option<String>,
210    /// Optional account/store namespace chosen by the host.
211    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/// Response from verifying an Inline login code.
231#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
232pub struct AuthVerifyResult {
233    /// Verified Inline user ID.
234    pub user_id: InlineId,
235    /// Account/store namespace persisted by the client.
236    pub account_namespace: String,
237    /// Updated client status after persisting the session.
238    pub status: ClientStatusSnapshot,
239}
240
241/// Request to list dialogs.
242#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
243pub struct DialogsRequest {
244    /// Optional page size.
245    pub limit: Option<u32>,
246    /// Optional opaque pagination cursor.
247    pub cursor: Option<String>,
248}
249
250/// Request to fetch chat history.
251#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
252pub struct HistoryRequest {
253    /// Inline chat ID.
254    pub chat_id: InlineId,
255    /// Optional page size.
256    pub limit: Option<u32>,
257    /// Optional exclusive upper message bound.
258    pub before_message_id: Option<InlineId>,
259    /// Optional exclusive lower message bound for newer-message startup catch-up.
260    pub after_message_id: Option<InlineId>,
261}
262
263/// Request to fetch chat participants.
264#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
265pub struct ChatParticipantsRequest {
266    /// Inline chat ID.
267    pub chat_id: InlineId,
268}
269
270/// User participant supplied when creating an Inline chat.
271#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
272pub struct ChatCreateParticipant {
273    /// Inline user ID.
274    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/// Request to create or open a direct message chat.
284#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
285pub struct CreateDmRequest {
286    /// Inline user ID to chat with.
287    pub user_id: InlineId,
288}
289
290/// Request to create a regular Inline thread chat.
291#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
292pub struct CreateThreadRequest {
293    /// Optional explicit title.
294    pub title: Option<String>,
295    /// Optional parent space ID.
296    pub space_id: Option<InlineId>,
297    /// Optional description.
298    pub description: Option<String>,
299    /// Optional emoji/icon.
300    pub emoji: Option<String>,
301    /// Whether everyone in the parent space can access the chat.
302    pub is_public: bool,
303    /// Direct child participants.
304    #[serde(default)]
305    pub participants: Vec<ChatCreateParticipant>,
306}
307
308/// Request to create a child Inline thread, optionally anchored to a message.
309#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
310pub struct CreateReplyThreadRequest {
311    /// Required parent chat for inherited access and navigation.
312    pub parent_chat_id: InlineId,
313    /// Optional parent message anchor. When set, this is a reply thread.
314    pub parent_message_id: Option<InlineId>,
315    /// Optional explicit title.
316    pub title: Option<String>,
317    /// Optional description.
318    pub description: Option<String>,
319    /// Optional emoji/icon.
320    pub emoji: Option<String>,
321    /// Direct child participants.
322    #[serde(default)]
323    pub participants: Vec<ChatCreateParticipant>,
324}
325
326/// Request to send a text message.
327#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
328pub struct SendTextRequest {
329    /// Target peer.
330    pub peer: PeerRef,
331    /// Message text.
332    pub text: String,
333    /// Optional host-provided idempotency key.
334    pub external_id: Option<ExternalId>,
335    /// Optional deterministic random ID supplied by the host.
336    pub random_id: Option<RandomId>,
337    /// Optional reply target.
338    pub reply_to_message_id: Option<InlineId>,
339}
340
341impl SendTextRequest {
342    /// Creates a text-send request.
343    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/// Request to edit a text message.
367#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
368pub struct EditMessageRequest {
369    /// Inline chat ID.
370    pub chat_id: InlineId,
371    /// Inline message ID.
372    pub message_id: InlineId,
373    /// Replacement message text.
374    pub text: String,
375    /// Optional host-provided idempotency key.
376    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/// Request to delete or unsend a message.
391#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
392pub struct DeleteMessageRequest {
393    /// Inline chat ID.
394    pub chat_id: InlineId,
395    /// Inline message ID.
396    pub message_id: InlineId,
397    /// Optional host-provided idempotency key.
398    pub external_id: Option<ExternalId>,
399}
400
401/// Request to add or remove a reaction.
402#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
403pub struct ReactRequest {
404    /// Inline chat ID.
405    pub chat_id: InlineId,
406    /// Inline message ID.
407    pub message_id: InlineId,
408    /// Reaction key, usually an emoji.
409    pub reaction: String,
410    /// Whether to remove instead of add the reaction.
411    pub remove: bool,
412    /// Optional host-provided idempotency key.
413    pub external_id: Option<ExternalId>,
414}
415
416/// Request to mark messages read.
417#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
418pub struct ReadRequest {
419    /// Inline chat ID.
420    pub chat_id: InlineId,
421    /// Highest message ID to mark read, when known.
422    pub max_message_id: Option<InlineId>,
423}
424
425/// Request to set typing state.
426#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
427pub struct TypingRequest {
428    /// Inline chat ID.
429    pub chat_id: InlineId,
430    /// Whether the current user is typing.
431    pub is_typing: bool,
432}
433
434/// Request to upload and send media.
435///
436/// Raw bytes are intentionally passed as a separate argument to
437/// [`crate::InlineClient::send_media`] so paths and transport-specific upload
438/// bodies do not become part of the reusable request type.
439#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
440pub struct UploadRequest {
441    /// Target peer.
442    pub peer: PeerRef,
443    /// Inline media kind requested by the host.
444    pub kind: MediaKind,
445    /// Optional original file name.
446    pub file_name: Option<String>,
447    /// Optional MIME type.
448    pub mime_type: Option<String>,
449    /// Optional content length in bytes.
450    pub size_bytes: Option<u64>,
451    /// Optional caption.
452    pub caption: Option<String>,
453    /// Optional media width in pixels.
454    pub width: Option<u32>,
455    /// Optional media height in pixels.
456    pub height: Option<u32>,
457    /// Optional media duration in milliseconds.
458    pub duration_ms: Option<u64>,
459    /// Optional host-provided idempotency key.
460    pub external_id: Option<ExternalId>,
461    /// Optional deterministic random ID supplied by the host.
462    pub random_id: Option<RandomId>,
463    /// Optional reply target.
464    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/// Snapshot returned by status APIs.
487#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
488pub struct ClientStatusSnapshot {
489    /// Current client status.
490    pub status: ClientStatus,
491    /// Optional redacted failure.
492    pub failure: Option<ClientFailure>,
493}
494
495impl ClientStatusSnapshot {
496    /// Creates a status snapshot.
497    pub fn current(status: ClientStatus) -> Self {
498        Self {
499            status,
500            failure: None,
501        }
502    }
503}
504
505/// Dialog item returned by the client.
506#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
507pub struct DialogRecord {
508    /// Inline chat ID.
509    pub chat_id: InlineId,
510    /// Display title, when known.
511    pub title: Option<String>,
512    /// Last known message ID, when known.
513    pub last_message_id: Option<InlineId>,
514    /// Highest message ID currently stored by `inline-client`, when known.
515    pub synced_through_message_id: Option<InlineId>,
516    /// Unread count, when known.
517    pub unread_count: Option<u32>,
518}
519
520/// A page of dialogs.
521#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
522pub struct DialogsPage {
523    /// Dialogs in display order.
524    pub dialogs: Vec<DialogRecord>,
525    /// Users referenced by dialogs/messages in this page.
526    #[serde(default)]
527    pub users: Vec<UserRecord>,
528    /// Opaque cursor for the next page.
529    pub next_cursor: Option<String>,
530}
531
532/// User profile summary returned by the client.
533#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
534pub struct UserRecord {
535    /// Inline user ID.
536    pub user_id: InlineId,
537    /// Display name suitable for host-visible user profiles.
538    pub display_name: Option<String>,
539    /// Username, when available.
540    pub username: Option<String>,
541    /// First name, when available.
542    pub first_name: Option<String>,
543    /// Last name, when available.
544    pub last_name: Option<String>,
545    /// Profile/avatar CDN URL, when Inline exposes one.
546    pub avatar_url: Option<String>,
547    /// Whether this user is a bot.
548    pub is_bot: Option<bool>,
549}
550
551/// Message content returned by the client.
552#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
553#[serde(tag = "type", rename_all = "snake_case")]
554#[non_exhaustive]
555pub enum MessageContent {
556    /// Plain text content.
557    Text {
558        /// Message text.
559        text: String,
560    },
561    /// Media content represented by an Inline file ID or opaque media handle.
562    Media {
563        /// Inline media kind.
564        kind: MediaKind,
565        /// Opaque Inline file/media ID.
566        file_id: String,
567        /// Download URL, usually an Inline CDN URL.
568        url: Option<String>,
569        /// Optional MIME type.
570        mime_type: Option<String>,
571        /// Optional file name.
572        file_name: Option<String>,
573        /// Optional caption.
574        caption: Option<String>,
575        /// Optional content length in bytes.
576        size_bytes: Option<u64>,
577        /// Optional media width in pixels.
578        width: Option<u32>,
579        /// Optional media height in pixels.
580        height: Option<u32>,
581        /// Optional media duration in milliseconds.
582        duration_ms: Option<u64>,
583    },
584    /// Unsupported content placeholder.
585    Unsupported {
586        /// Redacted reason suitable for hosts.
587        reason: String,
588    },
589}
590
591/// Inline media kind exposed by client message descriptors.
592#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
593#[serde(rename_all = "snake_case")]
594#[non_exhaustive]
595pub enum MediaKind {
596    /// Photo/image media.
597    Photo,
598    /// Video media.
599    Video,
600    /// Document/file media.
601    Document,
602    /// Voice/audio media.
603    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/// Message record returned by history/detail commands.
646#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
647pub struct MessageRecord {
648    /// Inline chat ID.
649    pub chat_id: InlineId,
650    /// Inline message ID.
651    pub message_id: InlineId,
652    /// Sender user ID.
653    pub sender_id: InlineId,
654    /// Unix timestamp in seconds.
655    pub timestamp: i64,
656    /// Whether the message was sent by the current user.
657    pub is_outgoing: bool,
658    /// Message content.
659    pub content: MessageContent,
660    /// Optional reply target.
661    pub reply_to_message_id: Option<InlineId>,
662    /// Optional transaction identity for local/pending sends.
663    pub transaction: Option<TransactionIdentity>,
664}
665
666/// A page of chat history.
667#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
668pub struct HistoryPage {
669    /// Messages in chronological order.
670    pub messages: Vec<MessageRecord>,
671    /// Users referenced by messages in this page, when available.
672    #[serde(default)]
673    pub users: Vec<UserRecord>,
674    /// Whether older history exists.
675    pub has_more: bool,
676    /// Opaque cursor for the next page, when available.
677    pub next_cursor: Option<String>,
678}
679
680/// Chat participant returned by the client.
681#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
682pub struct ChatParticipantRecord {
683    /// Inline user ID.
684    pub user_id: InlineId,
685    /// Unix timestamp in seconds when the participant was added, when known.
686    pub date: Option<i64>,
687}
688
689/// A chat participant snapshot.
690#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
691pub struct ChatParticipantsPage {
692    /// Users who currently have direct or group-derived access to the chat.
693    pub participants: Vec<ChatParticipantRecord>,
694    /// User profiles referenced by participants.
695    #[serde(default)]
696    pub users: Vec<UserRecord>,
697}
698
699/// Chat created or opened by the client.
700#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
701pub struct CreatedChat {
702    /// Inline chat ID.
703    pub chat_id: InlineId,
704    /// Display title, when known.
705    pub title: Option<String>,
706    /// Parent chat ID for child/reply threads.
707    pub parent_chat_id: Option<InlineId>,
708    /// Parent message ID for reply-thread chats.
709    pub parent_message_id: Option<InlineId>,
710}
711
712/// A message mutation acknowledgement.
713#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
714pub struct MessageMutation {
715    /// Transaction identity for reconciliation.
716    pub transaction: TransactionIdentity,
717    /// Final message ID, when already known.
718    pub message_id: Option<InlineId>,
719}
720
721/// Uploaded file/media handle returned by the client.
722#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
723pub struct UploadHandle {
724    /// Opaque Inline file/media ID.
725    pub file_id: String,
726    /// Optional MIME type.
727    pub mime_type: Option<String>,
728    /// Optional content length in bytes.
729    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}