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    /// Ordering contract used for this traversal.
265    #[serde(default)]
266    pub order: DialogsOrder,
267}
268
269/// Dialog traversal ordering.
270#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
271#[serde(rename_all = "snake_case")]
272pub enum DialogsOrder {
273    /// User-facing recent-activity order. Offset cursors can shift while chats
274    /// are active and should not be used for full reconciliation scans.
275    #[default]
276    RecentActivity,
277    /// Stable chat-ID keyset order for mutation-safe reconciliation scans.
278    StableChatId,
279}
280
281/// Request to fetch chat history.
282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
283pub struct HistoryRequest {
284    /// Inline chat ID.
285    pub chat_id: InlineId,
286    /// Optional page size.
287    pub limit: Option<u32>,
288    /// Optional exclusive upper message bound.
289    pub before_message_id: Option<InlineId>,
290    /// Optional exclusive lower message bound for newer-message startup catch-up.
291    pub after_message_id: Option<InlineId>,
292}
293
294/// Request to fetch chat participants.
295#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
296pub struct ChatParticipantsRequest {
297    /// Inline chat ID.
298    pub chat_id: InlineId,
299}
300
301/// Request to add a user to an Inline chat.
302#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
303pub struct AddChatParticipantRequest {
304    /// Inline chat ID.
305    pub chat_id: InlineId,
306    /// Inline user ID to add.
307    pub user_id: InlineId,
308}
309
310/// Request to remove a user from an Inline chat.
311#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
312pub struct RemoveChatParticipantRequest {
313    /// Inline chat ID.
314    pub chat_id: InlineId,
315    /// Inline user ID to remove.
316    pub user_id: InlineId,
317}
318
319/// Request to update mutable Inline chat metadata.
320///
321/// `None` leaves a field unchanged. An empty emoji clears the current icon.
322#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
323pub struct UpdateChatInfoRequest {
324    /// Inline chat ID.
325    pub chat_id: InlineId,
326    /// Replacement title, when changing it.
327    pub title: Option<String>,
328    /// Replacement emoji, or an empty string to clear it.
329    pub emoji: Option<String>,
330}
331
332/// Request to delete an Inline chat when the authenticated user is allowed to.
333#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
334pub struct DeleteChatRequest {
335    /// Inline chat ID.
336    pub chat_id: InlineId,
337}
338
339/// User participant supplied when creating an Inline chat.
340#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
341pub struct ChatCreateParticipant {
342    /// Inline user ID.
343    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/// Request to create or open a direct message chat.
353#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
354pub struct CreateDmRequest {
355    /// Inline user ID to chat with.
356    pub user_id: InlineId,
357}
358
359/// Request to create a regular Inline thread chat.
360#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
361pub struct CreateThreadRequest {
362    /// Optional explicit title.
363    pub title: Option<String>,
364    /// Optional parent space ID.
365    pub space_id: Option<InlineId>,
366    /// Optional description.
367    pub description: Option<String>,
368    /// Optional emoji/icon.
369    pub emoji: Option<String>,
370    /// Whether everyone in the parent space can access the chat.
371    pub is_public: bool,
372    /// Direct child participants.
373    #[serde(default)]
374    pub participants: Vec<ChatCreateParticipant>,
375}
376
377/// Request to create a child Inline thread, optionally anchored to a message.
378#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
379pub struct CreateReplyThreadRequest {
380    /// Required parent chat for inherited access and navigation.
381    pub parent_chat_id: InlineId,
382    /// Optional parent message anchor. When set, this is a reply thread.
383    pub parent_message_id: Option<InlineId>,
384    /// Optional explicit title.
385    pub title: Option<String>,
386    /// Optional description.
387    pub description: Option<String>,
388    /// Optional emoji/icon.
389    pub emoji: Option<String>,
390    /// Direct child participants.
391    #[serde(default)]
392    pub participants: Vec<ChatCreateParticipant>,
393}
394
395/// Request to send a text message.
396#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
397pub struct SendTextRequest {
398    /// Target peer.
399    pub peer: PeerRef,
400    /// Message text.
401    pub text: String,
402    /// Optional host-provided idempotency key.
403    pub external_id: Option<ExternalId>,
404    /// Optional deterministic random ID supplied by the host.
405    pub random_id: Option<RandomId>,
406    /// Optional reply target.
407    pub reply_to_message_id: Option<InlineId>,
408}
409
410impl SendTextRequest {
411    /// Creates a text-send request.
412    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/// Request to edit a text message.
436#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
437pub struct EditMessageRequest {
438    /// Inline chat ID.
439    pub chat_id: InlineId,
440    /// Inline message ID.
441    pub message_id: InlineId,
442    /// Replacement message text.
443    pub text: String,
444    /// Optional host-provided idempotency key.
445    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/// Request to delete or unsend a message.
460#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
461pub struct DeleteMessageRequest {
462    /// Inline chat ID.
463    pub chat_id: InlineId,
464    /// Inline message ID.
465    pub message_id: InlineId,
466    /// Optional host-provided idempotency key.
467    pub external_id: Option<ExternalId>,
468}
469
470/// Request to add or remove a reaction.
471#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
472pub struct ReactRequest {
473    /// Inline chat ID.
474    pub chat_id: InlineId,
475    /// Inline message ID.
476    pub message_id: InlineId,
477    /// Reaction key, usually an emoji.
478    pub reaction: String,
479    /// Whether to remove instead of add the reaction.
480    pub remove: bool,
481    /// Optional host-provided idempotency key.
482    pub external_id: Option<ExternalId>,
483}
484
485/// Request to mark messages read.
486#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
487pub struct ReadRequest {
488    /// Inline chat ID.
489    pub chat_id: InlineId,
490    /// Highest message ID to mark read, when known.
491    pub max_message_id: Option<InlineId>,
492}
493
494/// Request to set the explicit marked-unread state for a chat.
495#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
496pub struct SetMarkedUnreadRequest {
497    /// Inline chat ID.
498    pub chat_id: InlineId,
499    /// Whether the chat should be explicitly marked unread.
500    pub unread: bool,
501}
502
503/// Request to set or clear a per-dialog notification override.
504#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
505pub struct UpdateDialogNotificationsRequest {
506    /// Inline chat ID.
507    pub chat_id: InlineId,
508    /// Explicit mode, or `None` to inherit the account-wide setting.
509    pub mode: Option<DialogNotificationMode>,
510}
511
512/// Request to set typing state.
513#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
514pub struct TypingRequest {
515    /// Inline chat ID.
516    pub chat_id: InlineId,
517    /// Whether the current user is typing.
518    pub is_typing: bool,
519}
520
521/// Request to upload and send media.
522///
523/// Raw bytes are intentionally passed as a separate argument to
524/// [`crate::InlineClient::send_media`] so paths and transport-specific upload
525/// bodies do not become part of the reusable request type.
526#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
527pub struct UploadRequest {
528    /// Target peer.
529    pub peer: PeerRef,
530    /// Inline media kind requested by the host.
531    pub kind: MediaKind,
532    /// Optional original file name.
533    pub file_name: Option<String>,
534    /// Optional MIME type.
535    pub mime_type: Option<String>,
536    /// Optional content length in bytes.
537    pub size_bytes: Option<u64>,
538    /// Optional caption.
539    pub caption: Option<String>,
540    /// Optional media width in pixels.
541    pub width: Option<u32>,
542    /// Optional media height in pixels.
543    pub height: Option<u32>,
544    /// Optional media duration in milliseconds.
545    pub duration_ms: Option<u64>,
546    /// Optional host-provided idempotency key.
547    pub external_id: Option<ExternalId>,
548    /// Optional deterministic random ID supplied by the host.
549    pub random_id: Option<RandomId>,
550    /// Optional reply target.
551    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/// Snapshot returned by status APIs.
574#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
575pub struct ClientStatusSnapshot {
576    /// Current client status.
577    pub status: ClientStatus,
578    /// Optional redacted failure.
579    pub failure: Option<ClientFailure>,
580}
581
582impl ClientStatusSnapshot {
583    /// Creates a status snapshot.
584    pub fn current(status: ClientStatus) -> Self {
585        Self {
586            status,
587            failure: None,
588        }
589    }
590}
591
592/// Dialog item returned by the client.
593#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
594pub struct DialogRecord {
595    /// Inline chat ID.
596    pub chat_id: InlineId,
597    /// Other user ID when this dialog is a direct message.
598    #[serde(default, skip_serializing_if = "Option::is_none")]
599    pub peer_user_id: Option<InlineId>,
600    /// Display title, when known.
601    pub title: Option<String>,
602    /// Chat emoji/icon, when present.
603    #[serde(default, skip_serializing_if = "Option::is_none")]
604    pub emoji: Option<String>,
605    /// Last known message ID, when known.
606    pub last_message_id: Option<InlineId>,
607    /// Highest message ID currently stored by `inline-client`, when known.
608    pub synced_through_message_id: Option<InlineId>,
609    /// Unread count, when known.
610    pub unread_count: Option<u32>,
611    /// Parent Inline space ID, when this chat belongs to a space.
612    #[serde(default, skip_serializing_if = "Option::is_none")]
613    pub space_id: Option<InlineId>,
614    /// Whether the chat is visible to all eligible members of its parent space.
615    #[serde(default, skip_serializing_if = "Option::is_none")]
616    pub is_public: Option<bool>,
617    /// Whether the dialog is archived.
618    #[serde(default, skip_serializing_if = "Option::is_none")]
619    pub archived: Option<bool>,
620    /// Whether the dialog is pinned.
621    #[serde(default, skip_serializing_if = "Option::is_none")]
622    pub pinned: Option<bool>,
623    /// Whether the dialog belongs to the stable sidebar inbox.
624    #[serde(default, skip_serializing_if = "Option::is_none")]
625    pub open: Option<bool>,
626    /// Whether the dialog is hidden from normal chat lists.
627    #[serde(default, skip_serializing_if = "Option::is_none")]
628    pub chat_list_hidden: Option<bool>,
629    /// Stable fractional normal-list order, when assigned.
630    #[serde(default, skip_serializing_if = "Option::is_none")]
631    pub order: Option<String>,
632    /// Stable fractional pinned-list order, when assigned.
633    #[serde(default, skip_serializing_if = "Option::is_none")]
634    pub pinned_order: Option<String>,
635    /// Per-dialog notification override, when present.
636    #[serde(default, skip_serializing_if = "Option::is_none")]
637    pub notification_mode: Option<DialogNotificationMode>,
638    /// Reply-thread follow mode, when present.
639    #[serde(default, skip_serializing_if = "Option::is_none")]
640    pub follow_mode: Option<DialogFollowMode>,
641    /// Ordered pinned message IDs, newest first.
642    #[serde(default, skip_serializing_if = "Vec::is_empty")]
643    pub pinned_message_ids: Vec<InlineId>,
644}
645
646impl DialogRecord {
647    /// Creates an empty dialog record for a chat ID.
648    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/// Per-dialog notification override.
673#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
674#[serde(rename_all = "snake_case")]
675pub enum DialogNotificationMode {
676    /// Notify for all messages.
677    All,
678    /// Notify only for mentions.
679    Mentions,
680    /// Do not notify for this dialog.
681    None,
682}
683
684/// Reply-thread automatic surfacing policy.
685#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
686#[serde(rename_all = "snake_case")]
687pub enum DialogFollowMode {
688    /// Automatically surface activity from this reply thread.
689    Following,
690    /// Explicitly opt out of automatic surfacing for ordinary activity.
691    Unfollowed,
692}
693
694/// A page of dialogs.
695#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
696pub struct DialogsPage {
697    /// Dialogs in display order.
698    pub dialogs: Vec<DialogRecord>,
699    /// Users referenced by dialogs/messages in this page.
700    #[serde(default)]
701    pub users: Vec<UserRecord>,
702    /// Opaque cursor for the next page.
703    pub next_cursor: Option<String>,
704}
705
706/// User profile summary returned by the client.
707#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
708pub struct UserRecord {
709    /// Inline user ID.
710    pub user_id: InlineId,
711    /// Display name suitable for host-visible user profiles.
712    pub display_name: Option<String>,
713    /// Username, when available.
714    pub username: Option<String>,
715    /// First name, when available.
716    pub first_name: Option<String>,
717    /// Last name, when available.
718    pub last_name: Option<String>,
719    /// Profile/avatar CDN URL, when Inline exposes one.
720    pub avatar_url: Option<String>,
721    /// Whether this user is a bot.
722    pub is_bot: Option<bool>,
723}
724
725/// Durable Inline space summary.
726#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
727pub struct SpaceRecord {
728    /// Inline space ID.
729    pub space_id: InlineId,
730    /// Space display name.
731    pub name: String,
732    /// Whether the authenticated user created the space.
733    pub creator: bool,
734    /// Unix timestamp when the space was created.
735    pub date: i64,
736    /// Whether this is a public community space.
737    pub is_public: Option<bool>,
738    /// Whether the space uses grid layout, when the setting has been loaded.
739    #[serde(default, skip_serializing_if = "Option::is_none")]
740    pub grid_enabled: Option<bool>,
741}
742
743/// Inline space membership role.
744#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
745#[serde(rename_all = "snake_case")]
746pub enum SpaceMemberRole {
747    /// Space owner.
748    Owner,
749    /// Space administrator.
750    Admin,
751    /// Regular space member.
752    Member,
753}
754
755/// Durable Inline space member state.
756#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
757pub struct SpaceMemberRecord {
758    /// Inline space ID.
759    pub space_id: InlineId,
760    /// Inline user ID.
761    pub user_id: InlineId,
762    /// Membership role, when supplied by the server.
763    pub role: Option<SpaceMemberRole>,
764    /// Unix timestamp when the user joined.
765    pub date: i64,
766    /// Whether the member may access public chats in this space.
767    pub can_access_public_chats: bool,
768}
769
770/// Global notification policy.
771#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
772#[serde(rename_all = "snake_case")]
773pub enum NotificationMode {
774    /// Notify for all messages.
775    All,
776    /// Disable notifications.
777    None,
778    /// Notify for mentions.
779    Mentions,
780    /// Notify only for important messages.
781    ImportantOnly,
782    /// Notify only for direct mentions.
783    OnlyMentions,
784}
785
786/// Durable user notification settings relevant to generic clients.
787#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
788pub struct UserSettingsRecord {
789    /// Global notification mode, when set.
790    pub notification_mode: Option<NotificationMode>,
791    /// Whether notification sounds are disabled.
792    pub silent: Option<bool>,
793    /// Whether zen mode requires a mention.
794    pub zen_mode_requires_mention: Option<bool>,
795    /// Whether zen mode uses default rules.
796    pub zen_mode_uses_default_rules: Option<bool>,
797    /// Custom zen-mode rules.
798    pub zen_mode_custom_rules: Option<String>,
799    /// Whether direct-message notifications are disabled.
800    pub disable_dm_notifications: Option<bool>,
801}
802
803/// Message content returned by the client.
804#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(tag = "type", rename_all = "snake_case")]
806#[non_exhaustive]
807pub enum MessageContent {
808    /// Plain text content.
809    Text {
810        /// Message text.
811        text: String,
812    },
813    /// Media content represented by an Inline file ID or opaque media handle.
814    Media {
815        /// Inline media kind.
816        kind: MediaKind,
817        /// Opaque Inline file/media ID.
818        file_id: String,
819        /// Download URL, usually an Inline CDN URL.
820        url: Option<String>,
821        /// Optional MIME type.
822        mime_type: Option<String>,
823        /// Optional file name.
824        file_name: Option<String>,
825        /// Optional caption.
826        caption: Option<String>,
827        /// Optional content length in bytes.
828        size_bytes: Option<u64>,
829        /// Optional media width in pixels.
830        width: Option<u32>,
831        /// Optional media height in pixels.
832        height: Option<u32>,
833        /// Optional media duration in milliseconds.
834        duration_ms: Option<u64>,
835    },
836    /// Unsupported content placeholder.
837    Unsupported {
838        /// Redacted reason suitable for hosts.
839        reason: String,
840    },
841}
842
843/// Inline media kind exposed by client message descriptors.
844#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
845#[serde(rename_all = "snake_case")]
846#[non_exhaustive]
847pub enum MediaKind {
848    /// Photo/image media.
849    Photo,
850    /// Video media.
851    Video,
852    /// Document/file media.
853    Document,
854    /// Voice/audio media.
855    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/// Message record returned by history/detail commands.
898#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
899pub struct MessageRecord {
900    /// Inline chat ID.
901    pub chat_id: InlineId,
902    /// Inline message ID.
903    pub message_id: InlineId,
904    /// Sender user ID.
905    pub sender_id: InlineId,
906    /// Unix timestamp in seconds.
907    pub timestamp: i64,
908    /// Whether the message was sent by the current user.
909    pub is_outgoing: bool,
910    /// Message content.
911    pub content: MessageContent,
912    /// Optional reply target.
913    pub reply_to_message_id: Option<InlineId>,
914    /// Optional transaction identity for local/pending sends.
915    pub transaction: Option<TransactionIdentity>,
916}
917
918/// A page of chat history.
919#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
920pub struct HistoryPage {
921    /// Messages in chronological order.
922    pub messages: Vec<MessageRecord>,
923    /// Users referenced by messages in this page, when available.
924    #[serde(default)]
925    pub users: Vec<UserRecord>,
926    /// Whether older history exists.
927    pub has_more: bool,
928    /// Opaque cursor for the next page, when available.
929    pub next_cursor: Option<String>,
930}
931
932/// Chat participant returned by the client.
933#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
934pub struct ChatParticipantRecord {
935    /// Inline user ID.
936    pub user_id: InlineId,
937    /// Unix timestamp in seconds when the participant was added, when known.
938    pub date: Option<i64>,
939}
940
941/// A chat participant snapshot.
942#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
943pub struct ChatParticipantsPage {
944    /// Users who currently have direct or group-derived access to the chat.
945    pub participants: Vec<ChatParticipantRecord>,
946    /// User profiles referenced by participants.
947    #[serde(default)]
948    pub users: Vec<UserRecord>,
949}
950
951/// Chat created or opened by the client.
952#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
953pub struct CreatedChat {
954    /// Inline chat ID.
955    pub chat_id: InlineId,
956    /// Display title, when known.
957    pub title: Option<String>,
958    /// Parent chat ID for child/reply threads.
959    pub parent_chat_id: Option<InlineId>,
960    /// Parent message ID for reply-thread chats.
961    pub parent_message_id: Option<InlineId>,
962}
963
964/// A message mutation acknowledgement.
965#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
966pub struct MessageMutation {
967    /// Transaction identity for reconciliation.
968    pub transaction: TransactionIdentity,
969    /// Final message ID, when already known.
970    pub message_id: Option<InlineId>,
971    /// Durable transaction state after this send attempt.
972    #[serde(default, skip_serializing_if = "Option::is_none")]
973    pub state: Option<TransactionState>,
974    /// Redacted terminal or retryable failure details, when present.
975    #[serde(default, skip_serializing_if = "Option::is_none")]
976    pub failure: Option<ClientFailure>,
977}
978
979/// Uploaded file/media handle returned by the client.
980#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
981pub struct UploadHandle {
982    /// Opaque Inline file/media ID.
983    pub file_id: String,
984    /// Optional MIME type.
985    pub mime_type: Option<String>,
986    /// Optional content length in bytes.
987    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}