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    /// Other user ID when this dialog is a direct message.
511    #[serde(default, skip_serializing_if = "Option::is_none")]
512    pub peer_user_id: Option<InlineId>,
513    /// Display title, when known.
514    pub title: Option<String>,
515    /// Last known message ID, when known.
516    pub last_message_id: Option<InlineId>,
517    /// Highest message ID currently stored by `inline-client`, when known.
518    pub synced_through_message_id: Option<InlineId>,
519    /// Unread count, when known.
520    pub unread_count: Option<u32>,
521}
522
523/// A page of dialogs.
524#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
525pub struct DialogsPage {
526    /// Dialogs in display order.
527    pub dialogs: Vec<DialogRecord>,
528    /// Users referenced by dialogs/messages in this page.
529    #[serde(default)]
530    pub users: Vec<UserRecord>,
531    /// Opaque cursor for the next page.
532    pub next_cursor: Option<String>,
533}
534
535/// User profile summary returned by the client.
536#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
537pub struct UserRecord {
538    /// Inline user ID.
539    pub user_id: InlineId,
540    /// Display name suitable for host-visible user profiles.
541    pub display_name: Option<String>,
542    /// Username, when available.
543    pub username: Option<String>,
544    /// First name, when available.
545    pub first_name: Option<String>,
546    /// Last name, when available.
547    pub last_name: Option<String>,
548    /// Profile/avatar CDN URL, when Inline exposes one.
549    pub avatar_url: Option<String>,
550    /// Whether this user is a bot.
551    pub is_bot: Option<bool>,
552}
553
554/// Message content returned by the client.
555#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
556#[serde(tag = "type", rename_all = "snake_case")]
557#[non_exhaustive]
558pub enum MessageContent {
559    /// Plain text content.
560    Text {
561        /// Message text.
562        text: String,
563    },
564    /// Media content represented by an Inline file ID or opaque media handle.
565    Media {
566        /// Inline media kind.
567        kind: MediaKind,
568        /// Opaque Inline file/media ID.
569        file_id: String,
570        /// Download URL, usually an Inline CDN URL.
571        url: Option<String>,
572        /// Optional MIME type.
573        mime_type: Option<String>,
574        /// Optional file name.
575        file_name: Option<String>,
576        /// Optional caption.
577        caption: Option<String>,
578        /// Optional content length in bytes.
579        size_bytes: Option<u64>,
580        /// Optional media width in pixels.
581        width: Option<u32>,
582        /// Optional media height in pixels.
583        height: Option<u32>,
584        /// Optional media duration in milliseconds.
585        duration_ms: Option<u64>,
586    },
587    /// Unsupported content placeholder.
588    Unsupported {
589        /// Redacted reason suitable for hosts.
590        reason: String,
591    },
592}
593
594/// Inline media kind exposed by client message descriptors.
595#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
596#[serde(rename_all = "snake_case")]
597#[non_exhaustive]
598pub enum MediaKind {
599    /// Photo/image media.
600    Photo,
601    /// Video media.
602    Video,
603    /// Document/file media.
604    Document,
605    /// Voice/audio media.
606    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/// Message record returned by history/detail commands.
649#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
650pub struct MessageRecord {
651    /// Inline chat ID.
652    pub chat_id: InlineId,
653    /// Inline message ID.
654    pub message_id: InlineId,
655    /// Sender user ID.
656    pub sender_id: InlineId,
657    /// Unix timestamp in seconds.
658    pub timestamp: i64,
659    /// Whether the message was sent by the current user.
660    pub is_outgoing: bool,
661    /// Message content.
662    pub content: MessageContent,
663    /// Optional reply target.
664    pub reply_to_message_id: Option<InlineId>,
665    /// Optional transaction identity for local/pending sends.
666    pub transaction: Option<TransactionIdentity>,
667}
668
669/// A page of chat history.
670#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
671pub struct HistoryPage {
672    /// Messages in chronological order.
673    pub messages: Vec<MessageRecord>,
674    /// Users referenced by messages in this page, when available.
675    #[serde(default)]
676    pub users: Vec<UserRecord>,
677    /// Whether older history exists.
678    pub has_more: bool,
679    /// Opaque cursor for the next page, when available.
680    pub next_cursor: Option<String>,
681}
682
683/// Chat participant returned by the client.
684#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
685pub struct ChatParticipantRecord {
686    /// Inline user ID.
687    pub user_id: InlineId,
688    /// Unix timestamp in seconds when the participant was added, when known.
689    pub date: Option<i64>,
690}
691
692/// A chat participant snapshot.
693#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
694pub struct ChatParticipantsPage {
695    /// Users who currently have direct or group-derived access to the chat.
696    pub participants: Vec<ChatParticipantRecord>,
697    /// User profiles referenced by participants.
698    #[serde(default)]
699    pub users: Vec<UserRecord>,
700}
701
702/// Chat created or opened by the client.
703#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
704pub struct CreatedChat {
705    /// Inline chat ID.
706    pub chat_id: InlineId,
707    /// Display title, when known.
708    pub title: Option<String>,
709    /// Parent chat ID for child/reply threads.
710    pub parent_chat_id: Option<InlineId>,
711    /// Parent message ID for reply-thread chats.
712    pub parent_message_id: Option<InlineId>,
713}
714
715/// A message mutation acknowledgement.
716#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
717pub struct MessageMutation {
718    /// Transaction identity for reconciliation.
719    pub transaction: TransactionIdentity,
720    /// Final message ID, when already known.
721    pub message_id: Option<InlineId>,
722}
723
724/// Uploaded file/media handle returned by the client.
725#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
726pub struct UploadHandle {
727    /// Opaque Inline file/media ID.
728    pub file_id: String,
729    /// Optional MIME type.
730    pub mime_type: Option<String>,
731    /// Optional content length in bytes.
732    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}