Add iOS-facing core session facade
This commit is contained in:
933
crates/tele-core/src/session.rs
Normal file
933
crates/tele-core/src/session.rs
Normal file
@@ -0,0 +1,933 @@
|
||||
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<C> {
|
||||
client: C,
|
||||
events: VecDeque<CoreEvent>,
|
||||
}
|
||||
|
||||
impl<C> CoreSession<C> {
|
||||
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<CoreEvent> {
|
||||
self.events.drain(..).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TdClientTrait> CoreSession<C> {
|
||||
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<Vec<CoreChatSummary>, 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<Vec<CoreChatSummary>, 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<CoreChatSummary> {
|
||||
self.client
|
||||
.chats()
|
||||
.iter()
|
||||
.map(CoreChatSummary::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn folders(&self) -> Vec<CoreFolder> {
|
||||
self.client.folders().iter().map(CoreFolder::from).collect()
|
||||
}
|
||||
|
||||
pub async fn open_chat_history(
|
||||
&mut self,
|
||||
chat_id: ChatId,
|
||||
limit: i32,
|
||||
) -> Result<Vec<CoreMessage>, 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<MessageId>,
|
||||
reply: Option<ReplyInfo>,
|
||||
) -> Result<CoreMessage, String> {
|
||||
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<CoreMessage, String> {
|
||||
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<MessageId>,
|
||||
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<MessageId>,
|
||||
) -> 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<Vec<CoreReaction>, String> {
|
||||
self.client
|
||||
.toggle_reaction(chat_id, message_id, reaction)
|
||||
.await?;
|
||||
|
||||
let reactions: Vec<CoreReaction> = 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<CoreDownloadedFile, String> {
|
||||
self.client
|
||||
.download_file(file_id)
|
||||
.await
|
||||
.map(|path| CoreDownloadedFile { file_id, path })
|
||||
}
|
||||
|
||||
pub async fn download_voice(&self, file_id: i32) -> Result<CoreDownloadedFile, String> {
|
||||
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<Vec<CoreSearchResult>, 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 open_profile(&mut self, chat_id: ChatId) -> Result<CoreProfile, String> {
|
||||
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<CoreEvent> {
|
||||
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<String>,
|
||||
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<i32>,
|
||||
pub is_muted: bool,
|
||||
pub draft: Option<CoreDraft>,
|
||||
}
|
||||
|
||||
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<i32>,
|
||||
pub media_album_id: Option<i64>,
|
||||
pub text: String,
|
||||
pub media: Option<CoreMedia>,
|
||||
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<CoreReply>,
|
||||
pub forward: Option<CoreForward>,
|
||||
pub reactions: Vec<CoreReaction>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
pub bio: Option<String>,
|
||||
pub phone_number: Option<String>,
|
||||
pub chat_type: String,
|
||||
pub member_count: Option<i32>,
|
||||
pub description: Option<String>,
|
||||
pub invite_link: Option<String>,
|
||||
pub is_group: bool,
|
||||
pub online_status: Option<String>,
|
||||
}
|
||||
|
||||
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<CoreChatSummary>),
|
||||
FolderListChanged(Vec<CoreFolder>),
|
||||
MessageAdded {
|
||||
chat_id: ChatId,
|
||||
message: CoreMessage,
|
||||
},
|
||||
MessageUpdated {
|
||||
chat_id: ChatId,
|
||||
message: CoreMessage,
|
||||
},
|
||||
MessageDeleted {
|
||||
chat_id: ChatId,
|
||||
message_ids: Vec<MessageId>,
|
||||
},
|
||||
ReactionChanged {
|
||||
chat_id: ChatId,
|
||||
message_id: MessageId,
|
||||
reactions: Vec<CoreReaction>,
|
||||
},
|
||||
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();
|
||||
session
|
||||
.delete_messages(chat_id, vec![MessageId::new(10)], true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(sent.text, "Hello");
|
||||
assert_eq!(edited.text, "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 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");
|
||||
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();
|
||||
|
||||
assert_eq!(downloaded.path, "/tmp/photo.jpg");
|
||||
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 }]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user