use std::fmt;
use serde::{Deserialize, Serialize};
use crate::{
ClientError, ClientFailure, ClientStatus, ExternalId, InlineId, RandomId, TransactionIdentity,
TransactionState, validate_token,
};
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AuthToken(String);
impl AuthToken {
pub const MAX_LEN: usize = 8192;
pub fn try_new(value: impl AsRef<str>) -> Result<Self, ClientError> {
Ok(Self(
validate_token("auth_token", value.as_ref(), Self::MAX_LEN)?.to_owned(),
))
}
pub fn expose_secret(&self) -> &str {
&self.0
}
}
impl fmt::Debug for AuthToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("AuthToken(\"[redacted]\")")
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AuthCredential {
AccessToken {
token: AuthToken,
},
}
impl AuthCredential {
pub fn access_token(&self) -> &AuthToken {
match self {
Self::AccessToken { token } => token,
}
}
}
impl fmt::Debug for AuthCredential {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AccessToken { .. } => f
.debug_struct("AccessToken")
.field("token", &"[redacted]")
.finish(),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PeerRef {
User {
user_id: InlineId,
},
Chat {
chat_id: InlineId,
},
Thread {
thread_id: InlineId,
},
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConnectRequest {
pub auth: AuthCredential,
pub account_namespace: Option<String>,
}
impl ConnectRequest {
pub fn new(auth: AuthCredential) -> Self {
Self {
auth,
account_namespace: None,
}
}
pub fn with_account_namespace(mut self, namespace: impl Into<String>) -> Self {
self.account_namespace = Some(namespace.into());
self
}
}
impl fmt::Debug for ConnectRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConnectRequest")
.field("auth", &self.auth)
.field(
"account_namespace",
&self.account_namespace.as_ref().map(|_| "[redacted]"),
)
.finish()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthContactKind {
Email,
Phone,
}
impl AuthContactKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Email => "email",
Self::Phone => "phone",
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthStartRequest {
pub contact: String,
pub kind: AuthContactKind,
pub device_name: Option<String>,
}
impl fmt::Debug for AuthStartRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuthStartRequest")
.field("contact", &"[redacted]")
.field("kind", &self.kind)
.field("device_name", &self.device_name)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthStartResult {
pub existing_user: bool,
pub needs_invite_code: bool,
pub challenge_token: Option<String>,
}
impl fmt::Debug for AuthStartResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuthStartResult")
.field("existing_user", &self.existing_user)
.field("needs_invite_code", &self.needs_invite_code)
.field(
"challenge_token",
&self.challenge_token.as_ref().map(|_| "[redacted]"),
)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthVerifyRequest {
pub contact: String,
pub kind: AuthContactKind,
pub code: String,
pub challenge_token: Option<String>,
pub device_name: Option<String>,
pub account_namespace: Option<String>,
}
impl fmt::Debug for AuthVerifyRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuthVerifyRequest")
.field("contact", &"[redacted]")
.field("kind", &self.kind)
.field("code", &"[redacted]")
.field(
"challenge_token",
&self.challenge_token.as_ref().map(|_| "[redacted]"),
)
.field("device_name", &self.device_name)
.field(
"account_namespace",
&self.account_namespace.as_ref().map(|_| "[redacted]"),
)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthVerifyResult {
pub user_id: InlineId,
pub account_namespace: String,
pub status: ClientStatusSnapshot,
}
impl fmt::Debug for AuthVerifyResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuthVerifyResult")
.field("user_id", &self.user_id)
.field("account_namespace", &"[redacted]")
.field("status", &self.status)
.finish()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DialogsRequest {
pub limit: Option<u32>,
pub cursor: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HistoryRequest {
pub chat_id: InlineId,
pub limit: Option<u32>,
pub before_message_id: Option<InlineId>,
pub after_message_id: Option<InlineId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatParticipantsRequest {
pub chat_id: InlineId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AddChatParticipantRequest {
pub chat_id: InlineId,
pub user_id: InlineId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RemoveChatParticipantRequest {
pub chat_id: InlineId,
pub user_id: InlineId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateChatInfoRequest {
pub chat_id: InlineId,
pub title: Option<String>,
pub emoji: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeleteChatRequest {
pub chat_id: InlineId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatCreateParticipant {
pub user_id: InlineId,
}
impl From<InlineId> for ChatCreateParticipant {
fn from(user_id: InlineId) -> Self {
Self { user_id }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateDmRequest {
pub user_id: InlineId,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateThreadRequest {
pub title: Option<String>,
pub space_id: Option<InlineId>,
pub description: Option<String>,
pub emoji: Option<String>,
pub is_public: bool,
#[serde(default)]
pub participants: Vec<ChatCreateParticipant>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateReplyThreadRequest {
pub parent_chat_id: InlineId,
pub parent_message_id: Option<InlineId>,
pub title: Option<String>,
pub description: Option<String>,
pub emoji: Option<String>,
#[serde(default)]
pub participants: Vec<ChatCreateParticipant>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SendTextRequest {
pub peer: PeerRef,
pub text: String,
pub external_id: Option<ExternalId>,
pub random_id: Option<RandomId>,
pub reply_to_message_id: Option<InlineId>,
}
impl SendTextRequest {
pub fn new(peer: PeerRef, text: impl Into<String>) -> Self {
Self {
peer,
text: text.into(),
external_id: None,
random_id: None,
reply_to_message_id: None,
}
}
}
impl fmt::Debug for SendTextRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SendTextRequest")
.field("peer", &self.peer)
.field("text_len", &self.text.len())
.field("external_id", &self.external_id)
.field("random_id", &self.random_id)
.field("reply_to_message_id", &self.reply_to_message_id)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EditMessageRequest {
pub chat_id: InlineId,
pub message_id: InlineId,
pub text: String,
pub external_id: Option<ExternalId>,
}
impl fmt::Debug for EditMessageRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EditMessageRequest")
.field("chat_id", &self.chat_id)
.field("message_id", &self.message_id)
.field("text_len", &self.text.len())
.field("external_id", &self.external_id)
.finish()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeleteMessageRequest {
pub chat_id: InlineId,
pub message_id: InlineId,
pub external_id: Option<ExternalId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReactRequest {
pub chat_id: InlineId,
pub message_id: InlineId,
pub reaction: String,
pub remove: bool,
pub external_id: Option<ExternalId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadRequest {
pub chat_id: InlineId,
pub max_message_id: Option<InlineId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetMarkedUnreadRequest {
pub chat_id: InlineId,
pub unread: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateDialogNotificationsRequest {
pub chat_id: InlineId,
pub mode: Option<DialogNotificationMode>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TypingRequest {
pub chat_id: InlineId,
pub is_typing: bool,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UploadRequest {
pub peer: PeerRef,
pub kind: MediaKind,
pub file_name: Option<String>,
pub mime_type: Option<String>,
pub size_bytes: Option<u64>,
pub caption: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub duration_ms: Option<u64>,
pub external_id: Option<ExternalId>,
pub random_id: Option<RandomId>,
pub reply_to_message_id: Option<InlineId>,
}
impl fmt::Debug for UploadRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UploadRequest")
.field("peer", &self.peer)
.field("kind", &self.kind)
.field("file_name", &self.file_name)
.field("mime_type", &self.mime_type)
.field("size_bytes", &self.size_bytes)
.field("caption_len", &self.caption.as_ref().map(String::len))
.field("width", &self.width)
.field("height", &self.height)
.field("duration_ms", &self.duration_ms)
.field("external_id", &self.external_id)
.field("random_id", &self.random_id)
.field("reply_to_message_id", &self.reply_to_message_id)
.finish()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientStatusSnapshot {
pub status: ClientStatus,
pub failure: Option<ClientFailure>,
}
impl ClientStatusSnapshot {
pub fn current(status: ClientStatus) -> Self {
Self {
status,
failure: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DialogRecord {
pub chat_id: InlineId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer_user_id: Option<InlineId>,
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub emoji: Option<String>,
pub last_message_id: Option<InlineId>,
pub synced_through_message_id: Option<InlineId>,
pub unread_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub space_id: Option<InlineId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_public: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub archived: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pinned: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub open: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_list_hidden: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub order: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pinned_order: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notification_mode: Option<DialogNotificationMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub follow_mode: Option<DialogFollowMode>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pinned_message_ids: Vec<InlineId>,
}
impl DialogRecord {
pub fn new(chat_id: InlineId) -> Self {
Self {
chat_id,
peer_user_id: None,
title: None,
emoji: None,
last_message_id: None,
synced_through_message_id: None,
unread_count: None,
space_id: None,
is_public: None,
archived: None,
pinned: None,
open: None,
chat_list_hidden: None,
order: None,
pinned_order: None,
notification_mode: None,
follow_mode: None,
pinned_message_ids: Vec::new(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DialogNotificationMode {
All,
Mentions,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DialogFollowMode {
Following,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DialogsPage {
pub dialogs: Vec<DialogRecord>,
#[serde(default)]
pub users: Vec<UserRecord>,
pub next_cursor: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserRecord {
pub user_id: InlineId,
pub display_name: Option<String>,
pub username: Option<String>,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub avatar_url: Option<String>,
pub is_bot: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpaceRecord {
pub space_id: InlineId,
pub name: String,
pub creator: bool,
pub date: i64,
pub is_public: Option<bool>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SpaceMemberRole {
Owner,
Admin,
Member,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpaceMemberRecord {
pub space_id: InlineId,
pub user_id: InlineId,
pub role: Option<SpaceMemberRole>,
pub date: i64,
pub can_access_public_chats: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NotificationMode {
All,
None,
Mentions,
ImportantOnly,
OnlyMentions,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserSettingsRecord {
pub notification_mode: Option<NotificationMode>,
pub silent: Option<bool>,
pub zen_mode_requires_mention: Option<bool>,
pub zen_mode_uses_default_rules: Option<bool>,
pub zen_mode_custom_rules: Option<String>,
pub disable_dm_notifications: Option<bool>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum MessageContent {
Text {
text: String,
},
Media {
kind: MediaKind,
file_id: String,
url: Option<String>,
mime_type: Option<String>,
file_name: Option<String>,
caption: Option<String>,
size_bytes: Option<u64>,
width: Option<u32>,
height: Option<u32>,
duration_ms: Option<u64>,
},
Unsupported {
reason: String,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum MediaKind {
Photo,
Video,
Document,
Voice,
}
impl fmt::Debug for MessageContent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Text { text } => f
.debug_struct("Text")
.field("text_len", &text.len())
.finish(),
Self::Media {
kind,
file_id,
url,
mime_type,
file_name,
caption,
size_bytes,
width,
height,
duration_ms,
} => f
.debug_struct("Media")
.field("kind", kind)
.field("file_id", file_id)
.field("has_url", &url.is_some())
.field("mime_type", mime_type)
.field("file_name", file_name)
.field("caption_len", &caption.as_ref().map(String::len))
.field("size_bytes", size_bytes)
.field("width", width)
.field("height", height)
.field("duration_ms", duration_ms)
.finish(),
Self::Unsupported { reason } => f
.debug_struct("Unsupported")
.field("reason", reason)
.finish(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageRecord {
pub chat_id: InlineId,
pub message_id: InlineId,
pub sender_id: InlineId,
pub timestamp: i64,
pub is_outgoing: bool,
pub content: MessageContent,
pub reply_to_message_id: Option<InlineId>,
pub transaction: Option<TransactionIdentity>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HistoryPage {
pub messages: Vec<MessageRecord>,
#[serde(default)]
pub users: Vec<UserRecord>,
pub has_more: bool,
pub next_cursor: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatParticipantRecord {
pub user_id: InlineId,
pub date: Option<i64>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatParticipantsPage {
pub participants: Vec<ChatParticipantRecord>,
#[serde(default)]
pub users: Vec<UserRecord>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreatedChat {
pub chat_id: InlineId,
pub title: Option<String>,
pub parent_chat_id: Option<InlineId>,
pub parent_message_id: Option<InlineId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageMutation {
pub transaction: TransactionIdentity,
pub message_id: Option<InlineId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<TransactionState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failure: Option<ClientFailure>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UploadHandle {
pub file_id: String,
pub mime_type: Option<String>,
pub size_bytes: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{TransactionId, TransactionIdentity};
#[test]
fn auth_token_debug_is_redacted() {
let token = AuthToken::try_new("secret-token").unwrap();
assert_eq!(format!("{token:?}"), "AuthToken(\"[redacted]\")");
assert_eq!(token.expose_secret(), "secret-token");
}
#[test]
fn session_namespace_debug_is_redacted() {
let request = ConnectRequest::new(AuthCredential::AccessToken {
token: AuthToken::try_new("token").unwrap(),
})
.with_account_namespace("secret-namespace");
let rendered = format!("{request:?}");
assert!(rendered.contains("[redacted]"));
assert!(!rendered.contains("secret-namespace"));
}
#[test]
fn send_text_debug_redacts_body() {
let req = SendTextRequest::new(
PeerRef::User {
user_id: InlineId::new(42),
},
"hello private world",
);
let rendered = format!("{req:?}");
assert!(rendered.contains("text_len"));
assert!(!rendered.contains("hello private world"));
}
#[test]
fn history_page_redacts_message_text_in_debug() {
let message = MessageRecord {
chat_id: InlineId::new(1),
message_id: InlineId::new(2),
sender_id: InlineId::new(3),
timestamp: 1_783_452_698,
is_outgoing: false,
content: MessageContent::Text {
text: "private message".to_owned(),
},
reply_to_message_id: None,
transaction: Some(TransactionIdentity::new(
TransactionId::try_new("txn-1").unwrap(),
None,
RandomId::new(1),
)),
};
let rendered = format!("{message:?}");
assert!(rendered.contains("text_len"));
assert!(!rendered.contains("private message"));
}
}