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    TransactionState, 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(
128                "account_namespace",
129                &self.account_namespace.as_ref().map(|_| "[redacted]"),
130            )
131            .finish()
132    }
133}
134
135/// Contact method used for client-owned Inline login.
136#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum AuthContactKind {
139    /// Email code login.
140    Email,
141    /// SMS code login.
142    Phone,
143}
144
145impl AuthContactKind {
146    /// Returns the stable wire string for this contact kind.
147    pub const fn as_str(self) -> &'static str {
148        match self {
149            Self::Email => "email",
150            Self::Phone => "phone",
151        }
152    }
153}
154
155/// Request to send an Inline login code.
156#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub struct AuthStartRequest {
158    /// Contact address or phone number.
159    pub contact: String,
160    /// Contact kind.
161    pub kind: AuthContactKind,
162    /// Optional human-readable device name.
163    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/// Response from sending an Inline login code.
177#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
178pub struct AuthStartResult {
179    /// Whether the contact belongs to an existing user.
180    pub existing_user: bool,
181    /// Whether the flow needs an invite code before login can continue.
182    pub needs_invite_code: bool,
183    /// Opaque challenge token required by some email verification flows.
184    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/// Request to verify an Inline login code.
201#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct AuthVerifyRequest {
203    /// Contact address or phone number used in the start step.
204    pub contact: String,
205    /// Contact kind.
206    pub kind: AuthContactKind,
207    /// Verification code from email or SMS.
208    pub code: String,
209    /// Opaque email challenge token from the start step, when present.
210    pub challenge_token: Option<String>,
211    /// Optional human-readable device name.
212    pub device_name: Option<String>,
213    /// Optional account/store namespace chosen by the host.
214    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/// Response from verifying an Inline login code.
237#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
238pub struct AuthVerifyResult {
239    /// Verified Inline user ID.
240    pub user_id: InlineId,
241    /// Account/store namespace persisted by the client.
242    pub account_namespace: String,
243    /// Updated client status after persisting the session.
244    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/// Request to list dialogs.
258#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
259pub struct DialogsRequest {
260    /// Optional page size.
261    pub limit: Option<u32>,
262    /// Optional opaque pagination cursor.
263    pub cursor: Option<String>,
264}
265
266/// Request to fetch chat history.
267#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
268pub struct HistoryRequest {
269    /// Inline chat ID.
270    pub chat_id: InlineId,
271    /// Optional page size.
272    pub limit: Option<u32>,
273    /// Optional exclusive upper message bound.
274    pub before_message_id: Option<InlineId>,
275    /// Optional exclusive lower message bound for newer-message startup catch-up.
276    pub after_message_id: Option<InlineId>,
277}
278
279/// Request to fetch chat participants.
280#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
281pub struct ChatParticipantsRequest {
282    /// Inline chat ID.
283    pub chat_id: InlineId,
284}
285
286/// Request to add a user to an Inline chat.
287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288pub struct AddChatParticipantRequest {
289    /// Inline chat ID.
290    pub chat_id: InlineId,
291    /// Inline user ID to add.
292    pub user_id: InlineId,
293}
294
295/// Request to remove a user from an Inline chat.
296#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
297pub struct RemoveChatParticipantRequest {
298    /// Inline chat ID.
299    pub chat_id: InlineId,
300    /// Inline user ID to remove.
301    pub user_id: InlineId,
302}
303
304/// Request to update mutable Inline chat metadata.
305///
306/// `None` leaves a field unchanged. An empty emoji clears the current icon.
307#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
308pub struct UpdateChatInfoRequest {
309    /// Inline chat ID.
310    pub chat_id: InlineId,
311    /// Replacement title, when changing it.
312    pub title: Option<String>,
313    /// Replacement emoji, or an empty string to clear it.
314    pub emoji: Option<String>,
315}
316
317/// Request to delete an Inline chat when the authenticated user is allowed to.
318#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
319pub struct DeleteChatRequest {
320    /// Inline chat ID.
321    pub chat_id: InlineId,
322}
323
324/// User participant supplied when creating an Inline chat.
325#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
326pub struct ChatCreateParticipant {
327    /// Inline user ID.
328    pub user_id: InlineId,
329}
330
331impl From<InlineId> for ChatCreateParticipant {
332    fn from(user_id: InlineId) -> Self {
333        Self { user_id }
334    }
335}
336
337/// Request to create or open a direct message chat.
338#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
339pub struct CreateDmRequest {
340    /// Inline user ID to chat with.
341    pub user_id: InlineId,
342}
343
344/// Request to create a regular Inline thread chat.
345#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
346pub struct CreateThreadRequest {
347    /// Optional explicit title.
348    pub title: Option<String>,
349    /// Optional parent space ID.
350    pub space_id: Option<InlineId>,
351    /// Optional description.
352    pub description: Option<String>,
353    /// Optional emoji/icon.
354    pub emoji: Option<String>,
355    /// Whether everyone in the parent space can access the chat.
356    pub is_public: bool,
357    /// Direct child participants.
358    #[serde(default)]
359    pub participants: Vec<ChatCreateParticipant>,
360}
361
362/// Request to create a child Inline thread, optionally anchored to a message.
363#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
364pub struct CreateReplyThreadRequest {
365    /// Required parent chat for inherited access and navigation.
366    pub parent_chat_id: InlineId,
367    /// Optional parent message anchor. When set, this is a reply thread.
368    pub parent_message_id: Option<InlineId>,
369    /// Optional explicit title.
370    pub title: Option<String>,
371    /// Optional description.
372    pub description: Option<String>,
373    /// Optional emoji/icon.
374    pub emoji: Option<String>,
375    /// Direct child participants.
376    #[serde(default)]
377    pub participants: Vec<ChatCreateParticipant>,
378}
379
380/// Request to send a text message.
381#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
382pub struct SendTextRequest {
383    /// Target peer.
384    pub peer: PeerRef,
385    /// Message text.
386    pub text: String,
387    /// Optional host-provided idempotency key.
388    pub external_id: Option<ExternalId>,
389    /// Optional deterministic random ID supplied by the host.
390    pub random_id: Option<RandomId>,
391    /// Optional reply target.
392    pub reply_to_message_id: Option<InlineId>,
393}
394
395impl SendTextRequest {
396    /// Creates a text-send request.
397    pub fn new(peer: PeerRef, text: impl Into<String>) -> Self {
398        Self {
399            peer,
400            text: text.into(),
401            external_id: None,
402            random_id: None,
403            reply_to_message_id: None,
404        }
405    }
406}
407
408impl fmt::Debug for SendTextRequest {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        f.debug_struct("SendTextRequest")
411            .field("peer", &self.peer)
412            .field("text_len", &self.text.len())
413            .field("external_id", &self.external_id)
414            .field("random_id", &self.random_id)
415            .field("reply_to_message_id", &self.reply_to_message_id)
416            .finish()
417    }
418}
419
420/// Request to edit a text message.
421#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
422pub struct EditMessageRequest {
423    /// Inline chat ID.
424    pub chat_id: InlineId,
425    /// Inline message ID.
426    pub message_id: InlineId,
427    /// Replacement message text.
428    pub text: String,
429    /// Optional host-provided idempotency key.
430    pub external_id: Option<ExternalId>,
431}
432
433impl fmt::Debug for EditMessageRequest {
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        f.debug_struct("EditMessageRequest")
436            .field("chat_id", &self.chat_id)
437            .field("message_id", &self.message_id)
438            .field("text_len", &self.text.len())
439            .field("external_id", &self.external_id)
440            .finish()
441    }
442}
443
444/// Request to delete or unsend a message.
445#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
446pub struct DeleteMessageRequest {
447    /// Inline chat ID.
448    pub chat_id: InlineId,
449    /// Inline message ID.
450    pub message_id: InlineId,
451    /// Optional host-provided idempotency key.
452    pub external_id: Option<ExternalId>,
453}
454
455/// Request to add or remove a reaction.
456#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
457pub struct ReactRequest {
458    /// Inline chat ID.
459    pub chat_id: InlineId,
460    /// Inline message ID.
461    pub message_id: InlineId,
462    /// Reaction key, usually an emoji.
463    pub reaction: String,
464    /// Whether to remove instead of add the reaction.
465    pub remove: bool,
466    /// Optional host-provided idempotency key.
467    pub external_id: Option<ExternalId>,
468}
469
470/// Request to mark messages read.
471#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
472pub struct ReadRequest {
473    /// Inline chat ID.
474    pub chat_id: InlineId,
475    /// Highest message ID to mark read, when known.
476    pub max_message_id: Option<InlineId>,
477}
478
479/// Request to set the explicit marked-unread state for a chat.
480#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
481pub struct SetMarkedUnreadRequest {
482    /// Inline chat ID.
483    pub chat_id: InlineId,
484    /// Whether the chat should be explicitly marked unread.
485    pub unread: bool,
486}
487
488/// Request to set or clear a per-dialog notification override.
489#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
490pub struct UpdateDialogNotificationsRequest {
491    /// Inline chat ID.
492    pub chat_id: InlineId,
493    /// Explicit mode, or `None` to inherit the account-wide setting.
494    pub mode: Option<DialogNotificationMode>,
495}
496
497/// Request to set typing state.
498#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
499pub struct TypingRequest {
500    /// Inline chat ID.
501    pub chat_id: InlineId,
502    /// Whether the current user is typing.
503    pub is_typing: bool,
504}
505
506/// Request to upload and send media.
507///
508/// Raw bytes are intentionally passed as a separate argument to
509/// [`crate::InlineClient::send_media`] so paths and transport-specific upload
510/// bodies do not become part of the reusable request type.
511#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
512pub struct UploadRequest {
513    /// Target peer.
514    pub peer: PeerRef,
515    /// Inline media kind requested by the host.
516    pub kind: MediaKind,
517    /// Optional original file name.
518    pub file_name: Option<String>,
519    /// Optional MIME type.
520    pub mime_type: Option<String>,
521    /// Optional content length in bytes.
522    pub size_bytes: Option<u64>,
523    /// Optional caption.
524    pub caption: Option<String>,
525    /// Optional media width in pixels.
526    pub width: Option<u32>,
527    /// Optional media height in pixels.
528    pub height: Option<u32>,
529    /// Optional media duration in milliseconds.
530    pub duration_ms: Option<u64>,
531    /// Optional host-provided idempotency key.
532    pub external_id: Option<ExternalId>,
533    /// Optional deterministic random ID supplied by the host.
534    pub random_id: Option<RandomId>,
535    /// Optional reply target.
536    pub reply_to_message_id: Option<InlineId>,
537}
538
539impl fmt::Debug for UploadRequest {
540    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
541        f.debug_struct("UploadRequest")
542            .field("peer", &self.peer)
543            .field("kind", &self.kind)
544            .field("file_name", &self.file_name)
545            .field("mime_type", &self.mime_type)
546            .field("size_bytes", &self.size_bytes)
547            .field("caption_len", &self.caption.as_ref().map(String::len))
548            .field("width", &self.width)
549            .field("height", &self.height)
550            .field("duration_ms", &self.duration_ms)
551            .field("external_id", &self.external_id)
552            .field("random_id", &self.random_id)
553            .field("reply_to_message_id", &self.reply_to_message_id)
554            .finish()
555    }
556}
557
558/// Snapshot returned by status APIs.
559#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
560pub struct ClientStatusSnapshot {
561    /// Current client status.
562    pub status: ClientStatus,
563    /// Optional redacted failure.
564    pub failure: Option<ClientFailure>,
565}
566
567impl ClientStatusSnapshot {
568    /// Creates a status snapshot.
569    pub fn current(status: ClientStatus) -> Self {
570        Self {
571            status,
572            failure: None,
573        }
574    }
575}
576
577/// Dialog item returned by the client.
578#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
579pub struct DialogRecord {
580    /// Inline chat ID.
581    pub chat_id: InlineId,
582    /// Other user ID when this dialog is a direct message.
583    #[serde(default, skip_serializing_if = "Option::is_none")]
584    pub peer_user_id: Option<InlineId>,
585    /// Display title, when known.
586    pub title: Option<String>,
587    /// Chat emoji/icon, when present.
588    #[serde(default, skip_serializing_if = "Option::is_none")]
589    pub emoji: Option<String>,
590    /// Last known message ID, when known.
591    pub last_message_id: Option<InlineId>,
592    /// Highest message ID currently stored by `inline-client`, when known.
593    pub synced_through_message_id: Option<InlineId>,
594    /// Unread count, when known.
595    pub unread_count: Option<u32>,
596    /// Parent Inline space ID, when this chat belongs to a space.
597    #[serde(default, skip_serializing_if = "Option::is_none")]
598    pub space_id: Option<InlineId>,
599    /// Whether the chat is visible to all eligible members of its parent space.
600    #[serde(default, skip_serializing_if = "Option::is_none")]
601    pub is_public: Option<bool>,
602    /// Whether the dialog is archived.
603    #[serde(default, skip_serializing_if = "Option::is_none")]
604    pub archived: Option<bool>,
605    /// Whether the dialog is pinned.
606    #[serde(default, skip_serializing_if = "Option::is_none")]
607    pub pinned: Option<bool>,
608    /// Whether the dialog belongs to the stable sidebar inbox.
609    #[serde(default, skip_serializing_if = "Option::is_none")]
610    pub open: Option<bool>,
611    /// Whether the dialog is hidden from normal chat lists.
612    #[serde(default, skip_serializing_if = "Option::is_none")]
613    pub chat_list_hidden: Option<bool>,
614    /// Stable fractional normal-list order, when assigned.
615    #[serde(default, skip_serializing_if = "Option::is_none")]
616    pub order: Option<String>,
617    /// Stable fractional pinned-list order, when assigned.
618    #[serde(default, skip_serializing_if = "Option::is_none")]
619    pub pinned_order: Option<String>,
620    /// Per-dialog notification override, when present.
621    #[serde(default, skip_serializing_if = "Option::is_none")]
622    pub notification_mode: Option<DialogNotificationMode>,
623    /// Reply-thread follow mode, when present.
624    #[serde(default, skip_serializing_if = "Option::is_none")]
625    pub follow_mode: Option<DialogFollowMode>,
626    /// Ordered pinned message IDs, newest first.
627    #[serde(default, skip_serializing_if = "Vec::is_empty")]
628    pub pinned_message_ids: Vec<InlineId>,
629}
630
631impl DialogRecord {
632    /// Creates an empty dialog record for a chat ID.
633    pub fn new(chat_id: InlineId) -> Self {
634        Self {
635            chat_id,
636            peer_user_id: None,
637            title: None,
638            emoji: None,
639            last_message_id: None,
640            synced_through_message_id: None,
641            unread_count: None,
642            space_id: None,
643            is_public: None,
644            archived: None,
645            pinned: None,
646            open: None,
647            chat_list_hidden: None,
648            order: None,
649            pinned_order: None,
650            notification_mode: None,
651            follow_mode: None,
652            pinned_message_ids: Vec::new(),
653        }
654    }
655}
656
657/// Per-dialog notification override.
658#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
659#[serde(rename_all = "snake_case")]
660pub enum DialogNotificationMode {
661    /// Notify for all messages.
662    All,
663    /// Notify only for mentions.
664    Mentions,
665    /// Do not notify for this dialog.
666    None,
667}
668
669/// Reply-thread automatic surfacing policy.
670#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
671#[serde(rename_all = "snake_case")]
672pub enum DialogFollowMode {
673    /// Automatically surface activity from this reply thread.
674    Following,
675}
676
677/// A page of dialogs.
678#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
679pub struct DialogsPage {
680    /// Dialogs in display order.
681    pub dialogs: Vec<DialogRecord>,
682    /// Users referenced by dialogs/messages in this page.
683    #[serde(default)]
684    pub users: Vec<UserRecord>,
685    /// Opaque cursor for the next page.
686    pub next_cursor: Option<String>,
687}
688
689/// User profile summary returned by the client.
690#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
691pub struct UserRecord {
692    /// Inline user ID.
693    pub user_id: InlineId,
694    /// Display name suitable for host-visible user profiles.
695    pub display_name: Option<String>,
696    /// Username, when available.
697    pub username: Option<String>,
698    /// First name, when available.
699    pub first_name: Option<String>,
700    /// Last name, when available.
701    pub last_name: Option<String>,
702    /// Profile/avatar CDN URL, when Inline exposes one.
703    pub avatar_url: Option<String>,
704    /// Whether this user is a bot.
705    pub is_bot: Option<bool>,
706}
707
708/// Durable Inline space summary.
709#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
710pub struct SpaceRecord {
711    /// Inline space ID.
712    pub space_id: InlineId,
713    /// Space display name.
714    pub name: String,
715    /// Whether the authenticated user created the space.
716    pub creator: bool,
717    /// Unix timestamp when the space was created.
718    pub date: i64,
719    /// Whether this is a public community space.
720    pub is_public: Option<bool>,
721}
722
723/// Inline space membership role.
724#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
725#[serde(rename_all = "snake_case")]
726pub enum SpaceMemberRole {
727    /// Space owner.
728    Owner,
729    /// Space administrator.
730    Admin,
731    /// Regular space member.
732    Member,
733}
734
735/// Durable Inline space member state.
736#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
737pub struct SpaceMemberRecord {
738    /// Inline space ID.
739    pub space_id: InlineId,
740    /// Inline user ID.
741    pub user_id: InlineId,
742    /// Membership role, when supplied by the server.
743    pub role: Option<SpaceMemberRole>,
744    /// Unix timestamp when the user joined.
745    pub date: i64,
746    /// Whether the member may access public chats in this space.
747    pub can_access_public_chats: bool,
748}
749
750/// Global notification policy.
751#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
752#[serde(rename_all = "snake_case")]
753pub enum NotificationMode {
754    /// Notify for all messages.
755    All,
756    /// Disable notifications.
757    None,
758    /// Notify for mentions.
759    Mentions,
760    /// Notify only for important messages.
761    ImportantOnly,
762    /// Notify only for direct mentions.
763    OnlyMentions,
764}
765
766/// Durable user notification settings relevant to generic clients.
767#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
768pub struct UserSettingsRecord {
769    /// Global notification mode, when set.
770    pub notification_mode: Option<NotificationMode>,
771    /// Whether notification sounds are disabled.
772    pub silent: Option<bool>,
773    /// Whether zen mode requires a mention.
774    pub zen_mode_requires_mention: Option<bool>,
775    /// Whether zen mode uses default rules.
776    pub zen_mode_uses_default_rules: Option<bool>,
777    /// Custom zen-mode rules.
778    pub zen_mode_custom_rules: Option<String>,
779    /// Whether direct-message notifications are disabled.
780    pub disable_dm_notifications: Option<bool>,
781}
782
783/// Message content returned by the client.
784#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
785#[serde(tag = "type", rename_all = "snake_case")]
786#[non_exhaustive]
787pub enum MessageContent {
788    /// Plain text content.
789    Text {
790        /// Message text.
791        text: String,
792    },
793    /// Media content represented by an Inline file ID or opaque media handle.
794    Media {
795        /// Inline media kind.
796        kind: MediaKind,
797        /// Opaque Inline file/media ID.
798        file_id: String,
799        /// Download URL, usually an Inline CDN URL.
800        url: Option<String>,
801        /// Optional MIME type.
802        mime_type: Option<String>,
803        /// Optional file name.
804        file_name: Option<String>,
805        /// Optional caption.
806        caption: Option<String>,
807        /// Optional content length in bytes.
808        size_bytes: Option<u64>,
809        /// Optional media width in pixels.
810        width: Option<u32>,
811        /// Optional media height in pixels.
812        height: Option<u32>,
813        /// Optional media duration in milliseconds.
814        duration_ms: Option<u64>,
815    },
816    /// Unsupported content placeholder.
817    Unsupported {
818        /// Redacted reason suitable for hosts.
819        reason: String,
820    },
821}
822
823/// Inline media kind exposed by client message descriptors.
824#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
825#[serde(rename_all = "snake_case")]
826#[non_exhaustive]
827pub enum MediaKind {
828    /// Photo/image media.
829    Photo,
830    /// Video media.
831    Video,
832    /// Document/file media.
833    Document,
834    /// Voice/audio media.
835    Voice,
836}
837
838impl fmt::Debug for MessageContent {
839    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
840        match self {
841            Self::Text { text } => f
842                .debug_struct("Text")
843                .field("text_len", &text.len())
844                .finish(),
845            Self::Media {
846                kind,
847                file_id,
848                url,
849                mime_type,
850                file_name,
851                caption,
852                size_bytes,
853                width,
854                height,
855                duration_ms,
856            } => f
857                .debug_struct("Media")
858                .field("kind", kind)
859                .field("file_id", file_id)
860                .field("has_url", &url.is_some())
861                .field("mime_type", mime_type)
862                .field("file_name", file_name)
863                .field("caption_len", &caption.as_ref().map(String::len))
864                .field("size_bytes", size_bytes)
865                .field("width", width)
866                .field("height", height)
867                .field("duration_ms", duration_ms)
868                .finish(),
869            Self::Unsupported { reason } => f
870                .debug_struct("Unsupported")
871                .field("reason", reason)
872                .finish(),
873        }
874    }
875}
876
877/// Message record returned by history/detail commands.
878#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
879pub struct MessageRecord {
880    /// Inline chat ID.
881    pub chat_id: InlineId,
882    /// Inline message ID.
883    pub message_id: InlineId,
884    /// Sender user ID.
885    pub sender_id: InlineId,
886    /// Unix timestamp in seconds.
887    pub timestamp: i64,
888    /// Whether the message was sent by the current user.
889    pub is_outgoing: bool,
890    /// Message content.
891    pub content: MessageContent,
892    /// Optional reply target.
893    pub reply_to_message_id: Option<InlineId>,
894    /// Optional transaction identity for local/pending sends.
895    pub transaction: Option<TransactionIdentity>,
896}
897
898/// A page of chat history.
899#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
900pub struct HistoryPage {
901    /// Messages in chronological order.
902    pub messages: Vec<MessageRecord>,
903    /// Users referenced by messages in this page, when available.
904    #[serde(default)]
905    pub users: Vec<UserRecord>,
906    /// Whether older history exists.
907    pub has_more: bool,
908    /// Opaque cursor for the next page, when available.
909    pub next_cursor: Option<String>,
910}
911
912/// Chat participant returned by the client.
913#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
914pub struct ChatParticipantRecord {
915    /// Inline user ID.
916    pub user_id: InlineId,
917    /// Unix timestamp in seconds when the participant was added, when known.
918    pub date: Option<i64>,
919}
920
921/// A chat participant snapshot.
922#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
923pub struct ChatParticipantsPage {
924    /// Users who currently have direct or group-derived access to the chat.
925    pub participants: Vec<ChatParticipantRecord>,
926    /// User profiles referenced by participants.
927    #[serde(default)]
928    pub users: Vec<UserRecord>,
929}
930
931/// Chat created or opened by the client.
932#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
933pub struct CreatedChat {
934    /// Inline chat ID.
935    pub chat_id: InlineId,
936    /// Display title, when known.
937    pub title: Option<String>,
938    /// Parent chat ID for child/reply threads.
939    pub parent_chat_id: Option<InlineId>,
940    /// Parent message ID for reply-thread chats.
941    pub parent_message_id: Option<InlineId>,
942}
943
944/// A message mutation acknowledgement.
945#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
946pub struct MessageMutation {
947    /// Transaction identity for reconciliation.
948    pub transaction: TransactionIdentity,
949    /// Final message ID, when already known.
950    pub message_id: Option<InlineId>,
951    /// Durable transaction state after this send attempt.
952    #[serde(default, skip_serializing_if = "Option::is_none")]
953    pub state: Option<TransactionState>,
954    /// Redacted terminal or retryable failure details, when present.
955    #[serde(default, skip_serializing_if = "Option::is_none")]
956    pub failure: Option<ClientFailure>,
957}
958
959/// Uploaded file/media handle returned by the client.
960#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
961pub struct UploadHandle {
962    /// Opaque Inline file/media ID.
963    pub file_id: String,
964    /// Optional MIME type.
965    pub mime_type: Option<String>,
966    /// Optional content length in bytes.
967    pub size_bytes: Option<u64>,
968}
969
970#[cfg(test)]
971mod tests {
972    use super::*;
973    use crate::{TransactionId, TransactionIdentity};
974
975    #[test]
976    fn auth_token_debug_is_redacted() {
977        let token = AuthToken::try_new("secret-token").unwrap();
978
979        assert_eq!(format!("{token:?}"), "AuthToken(\"[redacted]\")");
980        assert_eq!(token.expose_secret(), "secret-token");
981    }
982
983    #[test]
984    fn session_namespace_debug_is_redacted() {
985        let request = ConnectRequest::new(AuthCredential::AccessToken {
986            token: AuthToken::try_new("token").unwrap(),
987        })
988        .with_account_namespace("secret-namespace");
989
990        let rendered = format!("{request:?}");
991        assert!(rendered.contains("[redacted]"));
992        assert!(!rendered.contains("secret-namespace"));
993    }
994
995    #[test]
996    fn send_text_debug_redacts_body() {
997        let req = SendTextRequest::new(
998            PeerRef::User {
999                user_id: InlineId::new(42),
1000            },
1001            "hello private world",
1002        );
1003        let rendered = format!("{req:?}");
1004
1005        assert!(rendered.contains("text_len"));
1006        assert!(!rendered.contains("hello private world"));
1007    }
1008
1009    #[test]
1010    fn history_page_redacts_message_text_in_debug() {
1011        let message = MessageRecord {
1012            chat_id: InlineId::new(1),
1013            message_id: InlineId::new(2),
1014            sender_id: InlineId::new(3),
1015            timestamp: 1_783_452_698,
1016            is_outgoing: false,
1017            content: MessageContent::Text {
1018                text: "private message".to_owned(),
1019            },
1020            reply_to_message_id: None,
1021            transaction: Some(TransactionIdentity::new(
1022                TransactionId::try_new("txn-1").unwrap(),
1023                None,
1024                RandomId::new(1),
1025            )),
1026        };
1027        let rendered = format!("{message:?}");
1028
1029        assert!(rendered.contains("text_len"));
1030        assert!(!rendered.contains("private message"));
1031    }
1032}