use crate::tdlib::types::ForwardInfo; use crate::tdlib::{ AuthState, ChatInfo, FolderInfo, MediaInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo, TdClientTrait, }; use crate::types::{ChatId, MessageId, UserId}; use std::collections::VecDeque; /// Platform-neutral Telegram session facade for native clients. #[derive(Debug, Clone)] pub struct CoreSession { client: C, events: VecDeque, } impl CoreSession { pub fn new(client: C) -> Self { Self { client, events: VecDeque::new() } } pub fn client(&self) -> &C { &self.client } pub fn client_mut(&mut self) -> &mut C { &mut self.client } pub fn into_client(self) -> C { self.client } pub fn enqueue_event(&mut self, event: CoreEvent) { self.events.push_back(event); } pub fn poll_events(&mut self) -> Vec { self.events.drain(..).collect() } } impl CoreSession { pub fn auth_state(&self) -> CoreAuthState { CoreAuthState::from(self.client.auth_state()) } pub fn network_state(&self) -> CoreNetworkState { CoreNetworkState::from(&self.client.network_state()) } pub fn emit_auth_state(&mut self) -> CoreAuthState { let state = self.auth_state(); self.enqueue_event(CoreEvent::AuthChanged(state.clone())); state } pub fn emit_network_state(&mut self) -> CoreNetworkState { let state = self.network_state(); self.enqueue_event(CoreEvent::NetworkChanged(state.clone())); state } pub async fn send_phone_number(&self, phone: String) -> Result<(), String> { self.client.send_phone_number(phone).await } pub async fn send_code(&self, code: String) -> Result<(), String> { self.client.send_code(code).await } pub async fn send_password(&self, password: String) -> Result<(), String> { self.client.send_password(password).await } pub async fn load_chats(&mut self, limit: i32) -> Result, String> { self.client.load_chats(limit).await?; let chats = self.chat_summaries(); self.enqueue_event(CoreEvent::ChatListChanged(chats.clone())); Ok(chats) } pub async fn load_folder_chats( &mut self, folder_id: i32, limit: i32, ) -> Result, String> { self.client.load_folder_chats(folder_id, limit).await?; let chats = self.chat_summaries(); self.enqueue_event(CoreEvent::ChatListChanged(chats.clone())); Ok(chats) } pub fn chat_summaries(&self) -> Vec { self.client .chats() .iter() .map(CoreChatSummary::from) .collect() } pub fn folders(&self) -> Vec { self.client.folders().iter().map(CoreFolder::from).collect() } pub async fn open_chat_history( &mut self, chat_id: ChatId, limit: i32, ) -> Result, String> { self.client.set_current_chat_id(Some(chat_id)); let messages = self.client.get_chat_history(chat_id, limit).await?; let messages = messages.iter().map(CoreMessage::from).collect(); Ok(messages) } pub async fn send_text_message( &mut self, chat_id: ChatId, text: String, reply_to_message_id: Option, reply: Option, ) -> Result { let message = self .client .send_message(chat_id, text, reply_to_message_id, reply) .await?; let message = CoreMessage::from(&message); self.enqueue_event(CoreEvent::MessageAdded { chat_id, message: message.clone() }); Ok(message) } pub async fn edit_text_message( &mut self, chat_id: ChatId, message_id: MessageId, text: String, ) -> Result { let message = self.client.edit_message(chat_id, message_id, text).await?; let message = CoreMessage::from(&message); self.enqueue_event(CoreEvent::MessageUpdated { chat_id, message: message.clone() }); Ok(message) } pub async fn delete_messages( &mut self, chat_id: ChatId, message_ids: Vec, revoke: bool, ) -> Result<(), String> { self.client .delete_messages(chat_id, message_ids.clone(), revoke) .await?; self.enqueue_event(CoreEvent::MessageDeleted { chat_id, message_ids }); Ok(()) } pub async fn forward_messages( &mut self, to_chat_id: ChatId, from_chat_id: ChatId, message_ids: Vec, ) -> Result<(), String> { self.client .forward_messages(to_chat_id, from_chat_id, message_ids) .await } pub async fn toggle_reaction( &mut self, chat_id: ChatId, message_id: MessageId, reaction: String, ) -> Result, String> { self.client .toggle_reaction(chat_id, message_id, reaction) .await?; let reactions: Vec = self .client .get_chat_history(chat_id, i32::MAX) .await? .into_iter() .find(|message| message.id() == message_id) .map(|message| message.reactions().iter().map(CoreReaction::from).collect()) .unwrap_or_default(); self.enqueue_event(CoreEvent::ReactionChanged { chat_id, message_id, reactions: reactions.clone(), }); Ok(reactions) } pub async fn download_photo(&self, file_id: i32) -> Result { self.client .download_file(file_id) .await .map(|path| CoreDownloadedFile { file_id, path }) } pub async fn download_voice(&self, file_id: i32) -> Result { self.client .download_voice_note(file_id) .await .map(|path| CoreDownloadedFile { file_id, path }) } pub async fn search_messages( &self, chat_id: ChatId, query: &str, ) -> Result, String> { let messages = self.client.search_messages(chat_id, query).await?; Ok(messages .iter() .map(|message| CoreSearchResult { chat_id, message: CoreMessage::from(message) }) .collect()) } pub async fn pinned_messages(&mut self, chat_id: ChatId) -> Result, String> { self.client .get_pinned_messages(chat_id) .await .map(|messages| messages.iter().map(CoreMessage::from).collect()) } pub async fn copy_payload( &mut self, chat_id: ChatId, message_id: MessageId, ) -> Result { self.client .get_chat_history(chat_id, i32::MAX) .await? .into_iter() .find(|message| message.id() == message_id) .map(|message| message.text().to_string()) .ok_or_else(|| "message not found".to_string()) } pub async fn open_profile(&mut self, chat_id: ChatId) -> Result { let profile = self .client .get_profile_info(chat_id) .await .map(|profile| CoreProfile::from(&profile))?; self.enqueue_event(CoreEvent::ProfileLoaded(profile.clone())); Ok(profile) } pub async fn set_draft(&mut self, chat_id: ChatId, text: String) -> Result<(), String> { self.client.set_draft_message(chat_id, text.clone()).await?; self.enqueue_event(CoreEvent::DraftChanged(CoreDraft { chat_id, text })); Ok(()) } pub fn drain_client_events(&mut self) -> Vec { let events: Vec<_> = self .client .drain_incoming_message_events() .into_iter() .map(|event| { CoreEvent::IncomingNotificationCandidate(CoreNotificationCandidate { chat: CoreChatSummary::from(&event.chat), message: CoreMessage::from(&event.message), sender_name: event.sender_name, }) }) .collect(); self.events.extend(events.iter().cloned()); events } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreAccount { pub id: String, pub display_name: String, pub is_active: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum CoreAuthState { WaitTdlibParameters, WaitPhoneNumber, WaitCode, WaitPassword, Ready, Closed, Error { message: String }, } impl From<&AuthState> for CoreAuthState { fn from(value: &AuthState) -> Self { match value { AuthState::WaitTdlibParameters => Self::WaitTdlibParameters, AuthState::WaitPhoneNumber => Self::WaitPhoneNumber, AuthState::WaitCode => Self::WaitCode, AuthState::WaitPassword => Self::WaitPassword, AuthState::Ready => Self::Ready, AuthState::Closed => Self::Closed, AuthState::Error(message) => Self::Error { message: message.clone() }, } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreChatSummary { pub id: ChatId, pub title: String, pub username: Option, pub last_message: String, pub last_message_date: i32, pub unread_count: i32, pub unread_mention_count: i32, pub is_pinned: bool, pub order: i64, pub last_read_outbox_message_id: MessageId, pub folder_ids: Vec, pub is_muted: bool, pub draft: Option, } impl From<&ChatInfo> for CoreChatSummary { fn from(value: &ChatInfo) -> Self { Self { id: value.id, title: value.title.clone(), username: value.username.clone(), last_message: value.last_message.clone(), last_message_date: value.last_message_date, unread_count: value.unread_count, unread_mention_count: value.unread_mention_count, is_pinned: value.is_pinned, order: value.order, last_read_outbox_message_id: value.last_read_outbox_message_id, folder_ids: value.folder_ids.clone(), is_muted: value.is_muted, draft: value .draft_text .as_ref() .map(|text| CoreDraft { chat_id: value.id, text: text.clone() }), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreFolder { pub id: i32, pub name: String, } impl From<&FolderInfo> for CoreFolder { fn from(value: &FolderInfo) -> Self { Self { id: value.id, name: value.name.clone() } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreMessage { pub id: MessageId, pub sender_name: String, pub date: i32, pub edit_date: Option, pub media_album_id: Option, pub text: String, pub media: Option, pub is_outgoing: bool, pub is_read: bool, pub can_be_edited: bool, pub can_be_deleted_only_for_self: bool, pub can_be_deleted_for_all_users: bool, pub reply: Option, pub forward: Option, pub reactions: Vec, } impl From<&MessageInfo> for CoreMessage { fn from(value: &MessageInfo) -> Self { Self { id: value.id(), sender_name: value.sender_name().to_string(), date: value.date(), edit_date: value.is_edited().then_some(value.metadata.edit_date), media_album_id: (value.media_album_id() != 0).then_some(value.media_album_id()), text: value.text().to_string(), media: value.content.media.as_ref().map(CoreMedia::from), is_outgoing: value.is_outgoing(), is_read: value.is_read(), can_be_edited: value.can_be_edited(), can_be_deleted_only_for_self: value.can_be_deleted_only_for_self(), can_be_deleted_for_all_users: value.can_be_deleted_for_all_users(), reply: value.reply_to().map(CoreReply::from), forward: value.forward_from().map(CoreForward::from), reactions: value.reactions().iter().map(CoreReaction::from).collect(), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreReply { pub message_id: MessageId, pub sender_name: String, pub text: String, } impl From<&ReplyInfo> for CoreReply { fn from(value: &ReplyInfo) -> Self { Self { message_id: value.message_id, sender_name: value.sender_name.clone(), text: value.text.clone(), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreForward { pub sender_name: String, } impl From<&ForwardInfo> for CoreForward { fn from(value: &ForwardInfo) -> Self { Self { sender_name: value.sender_name.clone() } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreReaction { pub emoji: String, pub count: i32, pub is_chosen: bool, } impl From<&crate::tdlib::types::ReactionInfo> for CoreReaction { fn from(value: &crate::tdlib::types::ReactionInfo) -> Self { Self { emoji: value.emoji.clone(), count: value.count, is_chosen: value.is_chosen, } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum CoreMedia { Photo(CorePhoto), Voice(CoreVoice), } impl From<&MediaInfo> for CoreMedia { fn from(value: &MediaInfo) -> Self { match value { MediaInfo::Photo(photo) => Self::Photo(CorePhoto { file_id: photo.file_id, width: photo.width, height: photo.height, download_state: CoreDownloadState::from(&photo.download_state), }), MediaInfo::Voice(voice) => Self::Voice(CoreVoice { file_id: voice.file_id, duration: voice.duration, mime_type: voice.mime_type.clone(), waveform: voice.waveform.clone(), download_state: CoreDownloadState::from(&voice.download_state), }), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CorePhoto { pub file_id: i32, pub width: i32, pub height: i32, pub download_state: CoreDownloadState, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreVoice { pub file_id: i32, pub duration: i32, pub mime_type: String, pub waveform: String, pub download_state: CoreDownloadState, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum CoreDownloadState { NotDownloaded, Downloading, Downloaded { path: String }, Error { message: String }, } impl From<&crate::tdlib::PhotoDownloadState> for CoreDownloadState { fn from(value: &crate::tdlib::PhotoDownloadState) -> Self { match value { crate::tdlib::PhotoDownloadState::NotDownloaded => Self::NotDownloaded, crate::tdlib::PhotoDownloadState::Downloading => Self::Downloading, crate::tdlib::PhotoDownloadState::Downloaded(path) => { Self::Downloaded { path: path.clone() } } crate::tdlib::PhotoDownloadState::Error(message) => { Self::Error { message: message.clone() } } } } } impl From<&crate::tdlib::VoiceDownloadState> for CoreDownloadState { fn from(value: &crate::tdlib::VoiceDownloadState) -> Self { match value { crate::tdlib::VoiceDownloadState::NotDownloaded => Self::NotDownloaded, crate::tdlib::VoiceDownloadState::Downloading => Self::Downloading, crate::tdlib::VoiceDownloadState::Downloaded(path) => { Self::Downloaded { path: path.clone() } } crate::tdlib::VoiceDownloadState::Error(message) => { Self::Error { message: message.clone() } } } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreProfile { pub chat_id: ChatId, pub title: String, pub username: Option, pub bio: Option, pub phone_number: Option, pub chat_type: String, pub member_count: Option, pub description: Option, pub invite_link: Option, pub is_group: bool, pub online_status: Option, } impl From<&ProfileInfo> for CoreProfile { fn from(value: &ProfileInfo) -> Self { Self { chat_id: value.chat_id, title: value.title.clone(), username: value.username.clone(), bio: value.bio.clone(), phone_number: value.phone_number.clone(), chat_type: value.chat_type.clone(), member_count: value.member_count, description: value.description.clone(), invite_link: value.invite_link.clone(), is_group: value.is_group, online_status: value.online_status.clone(), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreDraft { pub chat_id: ChatId, pub text: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreSearchResult { pub chat_id: ChatId, pub message: CoreMessage, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreDownloadedFile { pub file_id: i32, pub path: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum CoreNetworkState { WaitingForNetwork, ConnectingToProxy, Connecting, Updating, Ready, } impl From<&NetworkState> for CoreNetworkState { fn from(value: &NetworkState) -> Self { match value { NetworkState::WaitingForNetwork => Self::WaitingForNetwork, NetworkState::ConnectingToProxy => Self::ConnectingToProxy, NetworkState::Connecting => Self::Connecting, NetworkState::Updating => Self::Updating, NetworkState::Ready => Self::Ready, } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum CoreTypingState { Idle, Typing { chat_id: ChatId, user_id: UserId, text: String, }, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CoreNotificationCandidate { pub chat: CoreChatSummary, pub message: CoreMessage, pub sender_name: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum CoreEvent { AuthChanged(CoreAuthState), ChatListChanged(Vec), FolderListChanged(Vec), MessageAdded { chat_id: ChatId, message: CoreMessage, }, MessageUpdated { chat_id: ChatId, message: CoreMessage, }, MessageDeleted { chat_id: ChatId, message_ids: Vec, }, ReactionChanged { chat_id: ChatId, message_id: MessageId, reactions: Vec, }, MediaDownloadProgress { file_id: i32, downloaded_size: i64, total_size: i64, }, IncomingNotificationCandidate(CoreNotificationCandidate), NetworkChanged(CoreNetworkState), TypingChanged(CoreTypingState), DraftChanged(CoreDraft), ProfileLoaded(CoreProfile), } #[cfg(test)] mod tests { use super::*; use crate::tdlib::types::ReactionInfo; use crate::tdlib::{ AuthState, ChatInfo, FolderInfo, MessageBuilder, NetworkState, ProfileInfo, }; use crate::test_support::FakeTdClient; use crate::types::{ChatId, MessageId}; fn sample_chat() -> ChatInfo { ChatInfo { id: ChatId::new(42), title: "Team".to_string(), username: Some("team_chat".to_string()), last_message: "Latest".to_string(), last_message_date: 1_700_000_000, unread_count: 3, unread_mention_count: 1, is_pinned: true, order: 99, last_read_outbox_message_id: MessageId::new(7), folder_ids: vec![0, 2], is_muted: true, draft_text: Some("Draft".to_string()), } } #[test] fn auth_state_mapping_is_stable_for_swift() { assert_eq!( CoreAuthState::from(&AuthState::WaitPhoneNumber), CoreAuthState::WaitPhoneNumber ); assert_eq!(CoreAuthState::from(&AuthState::WaitCode), CoreAuthState::WaitCode); assert_eq!(CoreAuthState::from(&AuthState::WaitPassword), CoreAuthState::WaitPassword); assert_eq!(CoreAuthState::from(&AuthState::Ready), CoreAuthState::Ready); assert_eq!( CoreAuthState::from(&AuthState::Error("bad code".to_string())), CoreAuthState::Error { message: "bad code".to_string() } ); } #[test] fn chat_summary_preserves_ios_relevant_state() { let chat = CoreChatSummary::from(&sample_chat()); assert_eq!(chat.id, ChatId::new(42)); assert_eq!(chat.title, "Team"); assert_eq!(chat.username.as_deref(), Some("team_chat")); assert_eq!(chat.last_message, "Latest"); assert_eq!(chat.unread_count, 3); assert_eq!(chat.unread_mention_count, 1); assert!(chat.is_pinned); assert!(chat.is_muted); assert_eq!(chat.folder_ids, vec![0, 2]); assert_eq!(chat.draft.as_ref().map(|draft| draft.text.as_str()), Some("Draft")); } #[test] fn message_mapping_preserves_reply_reactions_and_state() { let message = MessageBuilder::new(MessageId::new(100)) .sender_name("Alice") .text("Hello") .date(1_700_000_001) .edit_date(1_700_000_002) .reply_to(crate::tdlib::ReplyInfo { message_id: MessageId::new(90), sender_name: "Bob".to_string(), text: "Original".to_string(), }) .reactions(vec![ReactionInfo { emoji: "👍".to_string(), count: 2, is_chosen: true }]) .outgoing() .read() .build(); let mapped = CoreMessage::from(&message); assert_eq!(mapped.id, MessageId::new(100)); assert_eq!(mapped.sender_name, "Alice"); assert_eq!(mapped.text, "Hello"); assert!(mapped.is_outgoing); assert!(mapped.is_read); assert_eq!(mapped.edit_date, Some(1_700_000_002)); assert_eq!(mapped.reply.as_ref().map(|reply| reply.message_id), Some(MessageId::new(90))); assert_eq!(mapped.reactions[0].emoji, "👍"); assert!(mapped.reactions[0].is_chosen); } #[test] fn session_event_queue_drains_in_fifo_order() { let mut session = CoreSession::new(()); session.enqueue_event(CoreEvent::AuthChanged(CoreAuthState::WaitCode)); session.enqueue_event(CoreEvent::NetworkChanged(CoreNetworkState::Ready)); assert_eq!( session.poll_events(), vec![ CoreEvent::AuthChanged(CoreAuthState::WaitCode), CoreEvent::NetworkChanged(CoreNetworkState::Ready), ] ); assert!(session.poll_events().is_empty()); } #[test] fn session_drains_incoming_message_events_as_notification_candidates() { let chat = sample_chat(); let client = FakeTdClient::new().with_chat(chat.clone()); client.simulate_incoming_message(chat.id, "Ping".to_string(), "Alice"); let mut session = CoreSession::new(client); let events = session.drain_client_events(); assert_eq!(events.len(), 1); let CoreEvent::IncomingNotificationCandidate(candidate) = &events[0] else { panic!("expected incoming notification candidate"); }; assert_eq!(candidate.chat.id, chat.id); assert_eq!(candidate.message.text, "Ping"); assert_eq!(candidate.sender_name, "Alice"); assert_eq!(session.poll_events(), events); } #[test] fn events_cover_chat_message_profile_and_folder_shapes() { let chat = CoreChatSummary::from(&sample_chat()); let message = CoreMessage::from( &MessageBuilder::new(MessageId::new(10)) .sender_name("Alice") .text("Hi") .build(), ); let folder = CoreFolder::from(&FolderInfo { id: 2, name: "Work".to_string() }); let profile = CoreProfile::from(&ProfileInfo { chat_id: ChatId::new(42), title: "Team".to_string(), username: Some("team_chat".to_string()), bio: None, phone_number: None, chat_type: "Group".to_string(), member_count: Some(10), description: Some("Project group".to_string()), invite_link: None, is_group: true, online_status: None, }); assert_eq!( CoreEvent::ChatListChanged(vec![chat.clone()]), CoreEvent::ChatListChanged(vec![chat]) ); assert_eq!( CoreEvent::MessageAdded { chat_id: ChatId::new(42), message: message.clone() }, CoreEvent::MessageAdded { chat_id: ChatId::new(42), message } ); assert_eq!(folder.name, "Work"); assert_eq!(profile.member_count, Some(10)); assert_eq!( CoreNetworkState::from(&NetworkState::WaitingForNetwork), CoreNetworkState::WaitingForNetwork ); } #[tokio::test] async fn facade_methods_enqueue_state_profile_and_draft_events() { let profile = ProfileInfo { chat_id: ChatId::new(42), title: "Team".to_string(), username: Some("team_chat".to_string()), bio: None, phone_number: None, chat_type: "Group".to_string(), member_count: Some(10), description: None, invite_link: None, is_group: true, online_status: None, }; let client = FakeTdClient::new() .with_auth_state(AuthState::WaitPassword) .with_network_state(NetworkState::Connecting) .with_profile(42, profile); let mut session = CoreSession::new(client); session.emit_auth_state(); session.emit_network_state(); let loaded_profile = session.open_profile(ChatId::new(42)).await.unwrap(); session .set_draft(ChatId::new(42), "Later".to_string()) .await .unwrap(); assert_eq!(loaded_profile.title, "Team"); assert_eq!( session.poll_events(), vec![ CoreEvent::AuthChanged(CoreAuthState::WaitPassword), CoreEvent::NetworkChanged(CoreNetworkState::Connecting), CoreEvent::ProfileLoaded(CoreProfile { chat_id: ChatId::new(42), title: "Team".to_string(), username: Some("team_chat".to_string()), bio: None, phone_number: None, chat_type: "Group".to_string(), member_count: Some(10), description: None, invite_link: None, is_group: true, online_status: None, }), CoreEvent::DraftChanged(CoreDraft { chat_id: ChatId::new(42), text: "Later".to_string(), }), ] ); } #[tokio::test] async fn message_mutations_return_models_and_enqueue_events() { let chat_id = ChatId::new(42); let original = MessageBuilder::new(MessageId::new(10)) .sender_name("Me") .text("Before") .outgoing() .build(); let client = FakeTdClient::new() .with_chat(sample_chat()) .with_message(chat_id.as_i64(), original); let mut session = CoreSession::new(client); let sent = session .send_text_message(chat_id, "Hello".to_string(), None, None) .await .unwrap(); let edited = session .edit_text_message(chat_id, MessageId::new(10), "After".to_string()) .await .unwrap(); let copied = session .copy_payload(chat_id, MessageId::new(10)) .await .unwrap(); session .delete_messages(chat_id, vec![MessageId::new(10)], true) .await .unwrap(); assert_eq!(sent.text, "Hello"); assert_eq!(edited.text, "After"); assert_eq!(copied, "After"); assert_eq!( session.poll_events(), vec![ CoreEvent::MessageAdded { chat_id, message: sent }, CoreEvent::MessageUpdated { chat_id, message: edited }, CoreEvent::MessageDeleted { chat_id, message_ids: vec![MessageId::new(10)] }, ] ); } #[tokio::test] async fn pinned_messages_are_mapped_for_native_clients() { let chat_id = ChatId::new(42); let pinned = MessageBuilder::new(MessageId::new(10)) .sender_name("Alice") .text("Pinned") .build(); let mut client = FakeTdClient::new(); client.set_current_pinned_message(Some(pinned)); let mut session = CoreSession::new(client); let pinned = session.pinned_messages(chat_id).await.unwrap(); assert_eq!(pinned.len(), 1); assert_eq!(pinned[0].id, MessageId::new(10)); assert_eq!(pinned[0].text, "Pinned"); } #[tokio::test] async fn facade_delegates_auth_forward_reactions_and_downloads() { let chat_id = ChatId::new(42); let other_chat_id = ChatId::new(100); let message = MessageBuilder::new(MessageId::new(10)) .sender_name("Alice") .text("React here") .build(); let client = FakeTdClient::new() .with_message(chat_id.as_i64(), message) .with_downloaded_file(77, "/tmp/photo.jpg") .with_downloaded_file(88, "/tmp/voice.ogg"); let mut session = CoreSession::new(client); session .send_phone_number("+10000000000".to_string()) .await .unwrap(); session.send_code("12345".to_string()).await.unwrap(); session.send_password("secret".to_string()).await.unwrap(); session .forward_messages(other_chat_id, chat_id, vec![MessageId::new(10)]) .await .unwrap(); let reactions = session .toggle_reaction(chat_id, MessageId::new(10), "👍".to_string()) .await .unwrap(); let downloaded = session.download_photo(77).await.unwrap(); let downloaded_voice = session.download_voice(88).await.unwrap(); assert_eq!(downloaded.path, "/tmp/photo.jpg"); assert_eq!(downloaded_voice.path, "/tmp/voice.ogg"); assert_eq!(session.client().get_forwarded_messages().len(), 1); assert_eq!( reactions, vec![CoreReaction { emoji: "👍".to_string(), count: 1, is_chosen: true }] ); assert_eq!( session.poll_events(), vec![CoreEvent::ReactionChanged { chat_id, message_id: MessageId::new(10), reactions }] ); } }