1307 lines
42 KiB
Rust
1307 lines
42 KiB
Rust
use std::sync::Mutex;
|
|
|
|
#[cfg(not(feature = "core-session"))]
|
|
use std::collections::HashMap;
|
|
|
|
#[cfg(feature = "core-session")]
|
|
use tele_core::session::{
|
|
CoreAuthState, CoreChatSummary, CoreDownloadedFile, CoreDraft, CoreEvent, CoreFolder,
|
|
CoreMedia, CoreMessage, CoreNetworkState, CoreProfile, CoreReaction, CoreSearchResult,
|
|
CoreSession,
|
|
};
|
|
#[cfg(feature = "core-session")]
|
|
use tele_core::tdlib::{ChatInfo, MessageBuilder, NetworkState, ProfileInfo};
|
|
#[cfg(feature = "core-session")]
|
|
use tele_core::test_support::FakeTdClient;
|
|
#[cfg(feature = "core-session")]
|
|
use tele_core::types::{ChatId, MessageId};
|
|
|
|
uniffi::setup_scaffolding!();
|
|
|
|
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
|
pub enum IosFfiError {
|
|
#[error("{message}")]
|
|
Operation { message: String },
|
|
}
|
|
|
|
impl From<String> for IosFfiError {
|
|
fn from(message: String) -> Self {
|
|
Self::Operation { message }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, uniffi::Record)]
|
|
pub struct IosSessionConfig {
|
|
pub account_id: String,
|
|
pub display_name: String,
|
|
pub database_path: String,
|
|
pub use_fake_tdlib: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)]
|
|
pub enum IosAuthState {
|
|
WaitTdlibParameters,
|
|
WaitPhoneNumber,
|
|
WaitCode,
|
|
WaitPassword,
|
|
Ready,
|
|
Closed,
|
|
Error { message: String },
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreAuthState> for IosAuthState {
|
|
fn from(value: CoreAuthState) -> Self {
|
|
match value {
|
|
CoreAuthState::WaitTdlibParameters => Self::WaitTdlibParameters,
|
|
CoreAuthState::WaitPhoneNumber => Self::WaitPhoneNumber,
|
|
CoreAuthState::WaitCode => Self::WaitCode,
|
|
CoreAuthState::WaitPassword => Self::WaitPassword,
|
|
CoreAuthState::Ready => Self::Ready,
|
|
CoreAuthState::Closed => Self::Closed,
|
|
CoreAuthState::Error { message } => Self::Error { message },
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)]
|
|
pub enum IosNetworkState {
|
|
WaitingForNetwork,
|
|
ConnectingToProxy,
|
|
Connecting,
|
|
Updating,
|
|
Ready,
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreNetworkState> for IosNetworkState {
|
|
fn from(value: CoreNetworkState) -> Self {
|
|
match value {
|
|
CoreNetworkState::WaitingForNetwork => Self::WaitingForNetwork,
|
|
CoreNetworkState::ConnectingToProxy => Self::ConnectingToProxy,
|
|
CoreNetworkState::Connecting => Self::Connecting,
|
|
CoreNetworkState::Updating => Self::Updating,
|
|
CoreNetworkState::Ready => Self::Ready,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)]
|
|
pub enum IosDownloadState {
|
|
NotDownloaded,
|
|
Downloading,
|
|
Downloaded { path: String },
|
|
Error { message: String },
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosFolder {
|
|
pub id: i32,
|
|
pub name: String,
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreFolder> for IosFolder {
|
|
fn from(value: CoreFolder) -> Self {
|
|
Self { id: value.id, name: value.name }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosDraft {
|
|
pub chat_id: i64,
|
|
pub text: String,
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreDraft> for IosDraft {
|
|
fn from(value: CoreDraft) -> Self {
|
|
Self { chat_id: value.chat_id.as_i64(), text: value.text }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosChatSummary {
|
|
pub id: i64,
|
|
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: i64,
|
|
pub folder_ids: Vec<i32>,
|
|
pub is_muted: bool,
|
|
pub draft: Option<IosDraft>,
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreChatSummary> for IosChatSummary {
|
|
fn from(value: CoreChatSummary) -> Self {
|
|
Self {
|
|
id: value.id.as_i64(),
|
|
title: value.title,
|
|
username: value.username,
|
|
last_message: value.last_message,
|
|
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.as_i64(),
|
|
folder_ids: value.folder_ids,
|
|
is_muted: value.is_muted,
|
|
draft: value.draft.map(IosDraft::from),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosReaction {
|
|
pub emoji: String,
|
|
pub count: i32,
|
|
pub is_chosen: bool,
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreReaction> for IosReaction {
|
|
fn from(value: CoreReaction) -> Self {
|
|
Self {
|
|
emoji: value.emoji,
|
|
count: value.count,
|
|
is_chosen: value.is_chosen,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosReply {
|
|
pub message_id: i64,
|
|
pub sender_name: String,
|
|
pub text: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosForward {
|
|
pub sender_name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosMedia {
|
|
pub kind: String,
|
|
pub file_id: i32,
|
|
pub width: Option<i32>,
|
|
pub height: Option<i32>,
|
|
pub duration: Option<i32>,
|
|
pub mime_type: Option<String>,
|
|
pub waveform: Option<String>,
|
|
pub download_state: IosDownloadState,
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreMedia> for IosMedia {
|
|
fn from(value: CoreMedia) -> Self {
|
|
match value {
|
|
CoreMedia::Photo(photo) => Self {
|
|
kind: "photo".to_string(),
|
|
file_id: photo.file_id,
|
|
width: Some(photo.width),
|
|
height: Some(photo.height),
|
|
duration: None,
|
|
mime_type: None,
|
|
waveform: None,
|
|
download_state: match photo.download_state {
|
|
tele_core::session::CoreDownloadState::NotDownloaded => {
|
|
IosDownloadState::NotDownloaded
|
|
}
|
|
tele_core::session::CoreDownloadState::Downloading => {
|
|
IosDownloadState::Downloading
|
|
}
|
|
tele_core::session::CoreDownloadState::Downloaded { path } => {
|
|
IosDownloadState::Downloaded { path }
|
|
}
|
|
tele_core::session::CoreDownloadState::Error { message } => {
|
|
IosDownloadState::Error { message }
|
|
}
|
|
},
|
|
},
|
|
CoreMedia::Voice(voice) => Self {
|
|
kind: "voice".to_string(),
|
|
file_id: voice.file_id,
|
|
width: None,
|
|
height: None,
|
|
duration: Some(voice.duration),
|
|
mime_type: Some(voice.mime_type),
|
|
waveform: Some(voice.waveform),
|
|
download_state: match voice.download_state {
|
|
tele_core::session::CoreDownloadState::NotDownloaded => {
|
|
IosDownloadState::NotDownloaded
|
|
}
|
|
tele_core::session::CoreDownloadState::Downloading => {
|
|
IosDownloadState::Downloading
|
|
}
|
|
tele_core::session::CoreDownloadState::Downloaded { path } => {
|
|
IosDownloadState::Downloaded { path }
|
|
}
|
|
tele_core::session::CoreDownloadState::Error { message } => {
|
|
IosDownloadState::Error { message }
|
|
}
|
|
},
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosMessage {
|
|
pub id: i64,
|
|
pub sender_name: String,
|
|
pub date: i32,
|
|
pub edit_date: Option<i32>,
|
|
pub media_album_id: Option<i64>,
|
|
pub text: String,
|
|
pub media: Option<IosMedia>,
|
|
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<IosReply>,
|
|
pub forward: Option<IosForward>,
|
|
pub reactions: Vec<IosReaction>,
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreMessage> for IosMessage {
|
|
fn from(value: CoreMessage) -> Self {
|
|
Self {
|
|
id: value.id.as_i64(),
|
|
sender_name: value.sender_name,
|
|
date: value.date,
|
|
edit_date: value.edit_date,
|
|
media_album_id: value.media_album_id,
|
|
text: value.text,
|
|
media: value.media.map(IosMedia::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.map(|reply| IosReply {
|
|
message_id: reply.message_id.as_i64(),
|
|
sender_name: reply.sender_name,
|
|
text: reply.text,
|
|
}),
|
|
forward: value
|
|
.forward
|
|
.map(|forward| IosForward { sender_name: forward.sender_name }),
|
|
reactions: value.reactions.into_iter().map(IosReaction::from).collect(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosSearchResult {
|
|
pub chat_id: i64,
|
|
pub message: IosMessage,
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreSearchResult> for IosSearchResult {
|
|
fn from(value: CoreSearchResult) -> Self {
|
|
Self {
|
|
chat_id: value.chat_id.as_i64(),
|
|
message: IosMessage::from(value.message),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosProfile {
|
|
pub chat_id: i64,
|
|
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>,
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreProfile> for IosProfile {
|
|
fn from(value: CoreProfile) -> Self {
|
|
Self {
|
|
chat_id: value.chat_id.as_i64(),
|
|
title: value.title,
|
|
username: value.username,
|
|
bio: value.bio,
|
|
phone_number: value.phone_number,
|
|
chat_type: value.chat_type,
|
|
member_count: value.member_count,
|
|
description: value.description,
|
|
invite_link: value.invite_link,
|
|
is_group: value.is_group,
|
|
online_status: value.online_status,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
|
|
pub struct IosDownloadedFile {
|
|
pub file_id: i32,
|
|
pub path: String,
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreDownloadedFile> for IosDownloadedFile {
|
|
fn from(value: CoreDownloadedFile) -> Self {
|
|
Self { file_id: value.file_id, path: value.path }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)]
|
|
pub enum IosEvent {
|
|
AuthChanged {
|
|
state: IosAuthState,
|
|
},
|
|
ChatListChanged {
|
|
chats: Vec<IosChatSummary>,
|
|
},
|
|
FolderListChanged {
|
|
folders: Vec<IosFolder>,
|
|
},
|
|
MessageAdded {
|
|
chat_id: i64,
|
|
message: IosMessage,
|
|
},
|
|
MessageUpdated {
|
|
chat_id: i64,
|
|
message: IosMessage,
|
|
},
|
|
MessageDeleted {
|
|
chat_id: i64,
|
|
message_ids: Vec<i64>,
|
|
},
|
|
ReactionChanged {
|
|
chat_id: i64,
|
|
message_id: i64,
|
|
reactions: Vec<IosReaction>,
|
|
},
|
|
IncomingNotificationCandidate {
|
|
chat: IosChatSummary,
|
|
message: IosMessage,
|
|
sender_name: String,
|
|
},
|
|
NetworkChanged {
|
|
state: IosNetworkState,
|
|
},
|
|
DraftChanged {
|
|
draft: IosDraft,
|
|
},
|
|
ProfileLoaded {
|
|
profile: IosProfile,
|
|
},
|
|
MediaDownloadProgress {
|
|
file_id: i32,
|
|
downloaded_size: i64,
|
|
total_size: i64,
|
|
},
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
impl From<CoreEvent> for IosEvent {
|
|
fn from(value: CoreEvent) -> Self {
|
|
match value {
|
|
CoreEvent::AuthChanged(state) => Self::AuthChanged { state: state.into() },
|
|
CoreEvent::ChatListChanged(chats) => Self::ChatListChanged {
|
|
chats: chats.into_iter().map(IosChatSummary::from).collect(),
|
|
},
|
|
CoreEvent::FolderListChanged(folders) => Self::FolderListChanged {
|
|
folders: folders.into_iter().map(IosFolder::from).collect(),
|
|
},
|
|
CoreEvent::MessageAdded { chat_id, message } => {
|
|
Self::MessageAdded { chat_id: chat_id.as_i64(), message: message.into() }
|
|
}
|
|
CoreEvent::MessageUpdated { chat_id, message } => {
|
|
Self::MessageUpdated { chat_id: chat_id.as_i64(), message: message.into() }
|
|
}
|
|
CoreEvent::MessageDeleted { chat_id, message_ids } => Self::MessageDeleted {
|
|
chat_id: chat_id.as_i64(),
|
|
message_ids: message_ids.into_iter().map(|id| id.as_i64()).collect(),
|
|
},
|
|
CoreEvent::ReactionChanged { chat_id, message_id, reactions } => {
|
|
Self::ReactionChanged {
|
|
chat_id: chat_id.as_i64(),
|
|
message_id: message_id.as_i64(),
|
|
reactions: reactions.into_iter().map(IosReaction::from).collect(),
|
|
}
|
|
}
|
|
CoreEvent::IncomingNotificationCandidate(candidate) => {
|
|
Self::IncomingNotificationCandidate {
|
|
chat: candidate.chat.into(),
|
|
message: candidate.message.into(),
|
|
sender_name: candidate.sender_name,
|
|
}
|
|
}
|
|
CoreEvent::NetworkChanged(state) => Self::NetworkChanged { state: state.into() },
|
|
CoreEvent::DraftChanged(draft) => Self::DraftChanged { draft: draft.into() },
|
|
CoreEvent::ProfileLoaded(profile) => Self::ProfileLoaded { profile: profile.into() },
|
|
CoreEvent::MediaDownloadProgress { file_id, downloaded_size, total_size } => {
|
|
Self::MediaDownloadProgress { file_id, downloaded_size, total_size }
|
|
}
|
|
CoreEvent::TypingChanged(_) => Self::NetworkChanged { state: IosNetworkState::Ready },
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(uniffi::Object)]
|
|
#[cfg(feature = "core-session")]
|
|
pub struct SessionHandle {
|
|
session: Mutex<CoreSession<FakeTdClient>>,
|
|
runtime: tokio::runtime::Runtime,
|
|
}
|
|
|
|
#[uniffi::export]
|
|
#[cfg(feature = "core-session")]
|
|
pub fn create_session(config: IosSessionConfig) -> Result<SessionHandle, IosFfiError> {
|
|
if !config.use_fake_tdlib {
|
|
return Err(IosFfiError::Operation {
|
|
message: "real TDLib sessions are not exposed by the iOS FFI crate yet".to_string(),
|
|
});
|
|
}
|
|
|
|
let client = seeded_fake_client();
|
|
Ok(SessionHandle {
|
|
session: Mutex::new(CoreSession::new(client)),
|
|
runtime: tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
.map_err(|error| IosFfiError::Operation { message: error.to_string() })?,
|
|
})
|
|
}
|
|
|
|
#[uniffi::export]
|
|
#[cfg(feature = "core-session")]
|
|
impl SessionHandle {
|
|
pub fn auth_state(&self) -> IosAuthState {
|
|
self.with_session(|session| session.auth_state().into())
|
|
}
|
|
|
|
pub fn poll_events(&self) -> Vec<IosEvent> {
|
|
self.with_session(|session| {
|
|
session.drain_client_events();
|
|
session
|
|
.poll_events()
|
|
.into_iter()
|
|
.map(IosEvent::from)
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
pub fn send_phone_number(&self, phone: String) -> Result<(), IosFfiError> {
|
|
let session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.send_phone_number(phone))
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn send_code(&self, code: String) -> Result<(), IosFfiError> {
|
|
let session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.send_code(code))
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn send_password(&self, password: String) -> Result<(), IosFfiError> {
|
|
let session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.send_password(password))
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn load_chats(&self, limit: i32) -> Result<Vec<IosChatSummary>, IosFfiError> {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
let chats = self
|
|
.runtime
|
|
.block_on(session.client().load_chats(limit as usize))
|
|
.map_err(IosFfiError::from)?;
|
|
let core_chats: Vec<_> = chats.iter().map(CoreChatSummary::from).collect();
|
|
session.enqueue_event(CoreEvent::ChatListChanged(core_chats.clone()));
|
|
Ok(core_chats.into_iter().map(IosChatSummary::from).collect())
|
|
}
|
|
|
|
pub fn load_folders(&self) -> Vec<IosFolder> {
|
|
self.with_session(|session| {
|
|
session
|
|
.client()
|
|
.get_folders()
|
|
.into_iter()
|
|
.map(|folder| IosFolder::from(CoreFolder::from(&folder)))
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
pub fn load_folder_chats(
|
|
&self,
|
|
folder_id: i32,
|
|
limit: i32,
|
|
) -> Result<Vec<IosChatSummary>, IosFfiError> {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(
|
|
session
|
|
.client()
|
|
.load_folder_chats(folder_id, limit as usize),
|
|
)
|
|
.map_err(IosFfiError::from)?;
|
|
let chats: Vec<_> = session
|
|
.client()
|
|
.get_chats()
|
|
.into_iter()
|
|
.filter(|chat| chat.folder_ids.contains(&folder_id))
|
|
.map(|chat| CoreChatSummary::from(&chat))
|
|
.collect();
|
|
session.enqueue_event(CoreEvent::ChatListChanged(chats.clone()));
|
|
Ok(chats.into_iter().map(IosChatSummary::from).collect())
|
|
}
|
|
|
|
pub fn load_history(&self, chat_id: i64, limit: i32) -> Result<Vec<IosMessage>, IosFfiError> {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.open_chat_history(ChatId::new(chat_id), limit))
|
|
.map(|messages| messages.into_iter().map(IosMessage::from).collect())
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn search_messages(
|
|
&self,
|
|
chat_id: i64,
|
|
query: String,
|
|
) -> Result<Vec<IosSearchResult>, IosFfiError> {
|
|
let session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.search_messages(ChatId::new(chat_id), &query))
|
|
.map(|results| results.into_iter().map(IosSearchResult::from).collect())
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn open_profile(&self, chat_id: i64) -> Result<IosProfile, IosFfiError> {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.open_profile(ChatId::new(chat_id)))
|
|
.map(IosProfile::from)
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn send_message(
|
|
&self,
|
|
chat_id: i64,
|
|
text: String,
|
|
reply_to_message_id: Option<i64>,
|
|
) -> Result<IosMessage, IosFfiError> {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.send_text_message(
|
|
ChatId::new(chat_id),
|
|
text,
|
|
reply_to_message_id.map(MessageId::new),
|
|
None,
|
|
))
|
|
.map(IosMessage::from)
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn edit_message(
|
|
&self,
|
|
chat_id: i64,
|
|
message_id: i64,
|
|
text: String,
|
|
) -> Result<IosMessage, IosFfiError> {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.edit_text_message(
|
|
ChatId::new(chat_id),
|
|
MessageId::new(message_id),
|
|
text,
|
|
))
|
|
.map(IosMessage::from)
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn delete_messages(
|
|
&self,
|
|
chat_id: i64,
|
|
message_ids: Vec<i64>,
|
|
revoke: bool,
|
|
) -> Result<(), IosFfiError> {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.delete_messages(
|
|
ChatId::new(chat_id),
|
|
message_ids.into_iter().map(MessageId::new).collect(),
|
|
revoke,
|
|
))
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn forward_messages(
|
|
&self,
|
|
to_chat_id: i64,
|
|
from_chat_id: i64,
|
|
message_ids: Vec<i64>,
|
|
) -> Result<(), IosFfiError> {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.forward_messages(
|
|
ChatId::new(to_chat_id),
|
|
ChatId::new(from_chat_id),
|
|
message_ids.into_iter().map(MessageId::new).collect(),
|
|
))
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn react(
|
|
&self,
|
|
chat_id: i64,
|
|
message_id: i64,
|
|
reaction: String,
|
|
) -> Result<Vec<IosReaction>, IosFfiError> {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.toggle_reaction(
|
|
ChatId::new(chat_id),
|
|
MessageId::new(message_id),
|
|
reaction,
|
|
))
|
|
.map(|reactions| reactions.into_iter().map(IosReaction::from).collect())
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn set_draft(&self, chat_id: i64, text: String) -> Result<(), IosFfiError> {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.set_draft(ChatId::new(chat_id), text))
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn download_photo(&self, file_id: i32) -> Result<IosDownloadedFile, IosFfiError> {
|
|
let session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.download_photo(file_id))
|
|
.map(IosDownloadedFile::from)
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn download_voice(&self, file_id: i32) -> Result<IosDownloadedFile, IosFfiError> {
|
|
let session = self.session.lock().expect("session mutex poisoned");
|
|
self.runtime
|
|
.block_on(session.download_voice(file_id))
|
|
.map(IosDownloadedFile::from)
|
|
.map_err(IosFfiError::from)
|
|
}
|
|
|
|
pub fn simulate_incoming_message(&self, chat_id: i64, text: String, sender_name: String) {
|
|
self.with_session(|session| {
|
|
session
|
|
.client()
|
|
.simulate_incoming_message(ChatId::new(chat_id), text, &sender_name);
|
|
});
|
|
}
|
|
}
|
|
|
|
impl SessionHandle {
|
|
#[cfg(feature = "core-session")]
|
|
fn with_session<R>(&self, f: impl FnOnce(&mut CoreSession<FakeTdClient>) -> R) -> R {
|
|
let mut session = self.session.lock().expect("session mutex poisoned");
|
|
f(&mut session)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "core-session")]
|
|
fn seeded_fake_client() -> FakeTdClient {
|
|
let chat = ChatInfo {
|
|
id: ChatId::new(1),
|
|
title: "Saved Messages".to_string(),
|
|
username: Some("saved".to_string()),
|
|
last_message: "Hello from fake TDLib".to_string(),
|
|
last_message_date: 1_700_000_000,
|
|
unread_count: 1,
|
|
unread_mention_count: 0,
|
|
is_pinned: true,
|
|
order: 1,
|
|
last_read_outbox_message_id: MessageId::new(1),
|
|
folder_ids: vec![0],
|
|
is_muted: false,
|
|
draft_text: None,
|
|
};
|
|
let message = MessageBuilder::new(MessageId::new(1))
|
|
.sender_name("Alice")
|
|
.text("Hello from fake TDLib")
|
|
.date(1_700_000_000)
|
|
.build();
|
|
let profile = ProfileInfo {
|
|
chat_id: chat.id,
|
|
title: chat.title.clone(),
|
|
username: chat.username.clone(),
|
|
bio: Some("Fake profile for iOS bridge tests".to_string()),
|
|
phone_number: None,
|
|
chat_type: "Private".to_string(),
|
|
member_count: None,
|
|
description: None,
|
|
invite_link: None,
|
|
is_group: false,
|
|
online_status: Some("online".to_string()),
|
|
};
|
|
|
|
FakeTdClient::new()
|
|
.with_chat(chat.clone())
|
|
.with_message(chat.id.as_i64(), message)
|
|
.with_profile(chat.id.as_i64(), profile)
|
|
.with_network_state(NetworkState::Ready)
|
|
.with_downloaded_file(100, "/tmp/fake-photo.jpg")
|
|
.with_downloaded_file(200, "/tmp/fake-voice.ogg")
|
|
}
|
|
|
|
#[derive(uniffi::Object)]
|
|
#[cfg(not(feature = "core-session"))]
|
|
pub struct SessionHandle {
|
|
state: Mutex<StandaloneFakeState>,
|
|
}
|
|
|
|
#[cfg(not(feature = "core-session"))]
|
|
struct StandaloneFakeState {
|
|
auth: IosAuthState,
|
|
chats: Vec<IosChatSummary>,
|
|
folders: Vec<IosFolder>,
|
|
messages: HashMap<i64, Vec<IosMessage>>,
|
|
profiles: HashMap<i64, IosProfile>,
|
|
events: Vec<IosEvent>,
|
|
downloaded_files: HashMap<i32, String>,
|
|
next_message_id: i64,
|
|
}
|
|
|
|
#[cfg(not(feature = "core-session"))]
|
|
impl StandaloneFakeState {
|
|
fn seeded() -> Self {
|
|
let chat = IosChatSummary {
|
|
id: 1,
|
|
title: "Saved Messages".to_string(),
|
|
username: Some("saved".to_string()),
|
|
last_message: "Hello from fake TDLib".to_string(),
|
|
last_message_date: 1_700_000_000,
|
|
unread_count: 1,
|
|
unread_mention_count: 0,
|
|
is_pinned: true,
|
|
order: 1,
|
|
last_read_outbox_message_id: 1,
|
|
folder_ids: vec![0],
|
|
is_muted: false,
|
|
draft: None,
|
|
};
|
|
let message = IosMessage {
|
|
id: 1,
|
|
sender_name: "Alice".to_string(),
|
|
date: 1_700_000_000,
|
|
edit_date: None,
|
|
media_album_id: None,
|
|
text: "Hello from fake TDLib".to_string(),
|
|
media: None,
|
|
is_outgoing: false,
|
|
is_read: false,
|
|
can_be_edited: false,
|
|
can_be_deleted_only_for_self: true,
|
|
can_be_deleted_for_all_users: false,
|
|
reply: None,
|
|
forward: None,
|
|
reactions: Vec::new(),
|
|
};
|
|
let profile = IosProfile {
|
|
chat_id: chat.id,
|
|
title: chat.title.clone(),
|
|
username: chat.username.clone(),
|
|
bio: Some("Fake profile for iOS bridge tests".to_string()),
|
|
phone_number: None,
|
|
chat_type: "Private".to_string(),
|
|
member_count: None,
|
|
description: None,
|
|
invite_link: None,
|
|
is_group: false,
|
|
online_status: Some("online".to_string()),
|
|
};
|
|
|
|
let mut messages = HashMap::new();
|
|
messages.insert(chat.id, vec![message]);
|
|
|
|
let mut profiles = HashMap::new();
|
|
profiles.insert(chat.id, profile);
|
|
|
|
let mut downloaded_files = HashMap::new();
|
|
downloaded_files.insert(100, "/tmp/fake-photo.jpg".to_string());
|
|
downloaded_files.insert(200, "/tmp/fake-voice.ogg".to_string());
|
|
|
|
Self {
|
|
auth: IosAuthState::WaitPhoneNumber,
|
|
chats: vec![chat.clone()],
|
|
folders: vec![IosFolder { id: 0, name: "All".to_string() }],
|
|
messages,
|
|
profiles,
|
|
events: vec![IosEvent::ChatListChanged { chats: vec![chat] }],
|
|
downloaded_files,
|
|
next_message_id: 2,
|
|
}
|
|
}
|
|
|
|
fn message_mut(
|
|
&mut self,
|
|
chat_id: i64,
|
|
message_id: i64,
|
|
) -> Result<&mut IosMessage, IosFfiError> {
|
|
self.messages
|
|
.get_mut(&chat_id)
|
|
.and_then(|messages| messages.iter_mut().find(|message| message.id == message_id))
|
|
.ok_or_else(|| IosFfiError::Operation { message: "message not found".to_string() })
|
|
}
|
|
}
|
|
|
|
#[uniffi::export]
|
|
#[cfg(not(feature = "core-session"))]
|
|
pub fn create_session(config: IosSessionConfig) -> Result<SessionHandle, IosFfiError> {
|
|
if !config.use_fake_tdlib {
|
|
return Err(IosFfiError::Operation {
|
|
message: "real TDLib sessions require the core-session build".to_string(),
|
|
});
|
|
}
|
|
|
|
Ok(SessionHandle { state: Mutex::new(StandaloneFakeState::seeded()) })
|
|
}
|
|
|
|
#[uniffi::export]
|
|
#[cfg(not(feature = "core-session"))]
|
|
impl SessionHandle {
|
|
pub fn auth_state(&self) -> IosAuthState {
|
|
self.state
|
|
.lock()
|
|
.expect("session mutex poisoned")
|
|
.auth
|
|
.clone()
|
|
}
|
|
|
|
pub fn poll_events(&self) -> Vec<IosEvent> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
std::mem::take(&mut state.events)
|
|
}
|
|
|
|
pub fn send_phone_number(&self, _phone: String) -> Result<(), IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
state.auth = IosAuthState::WaitCode;
|
|
let auth = state.auth.clone();
|
|
state.events.push(IosEvent::AuthChanged { state: auth });
|
|
Ok(())
|
|
}
|
|
|
|
pub fn send_code(&self, _code: String) -> Result<(), IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
state.auth = IosAuthState::WaitPassword;
|
|
let auth = state.auth.clone();
|
|
state.events.push(IosEvent::AuthChanged { state: auth });
|
|
Ok(())
|
|
}
|
|
|
|
pub fn send_password(&self, _password: String) -> Result<(), IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
state.auth = IosAuthState::Ready;
|
|
let auth = state.auth.clone();
|
|
state.events.push(IosEvent::AuthChanged { state: auth });
|
|
Ok(())
|
|
}
|
|
|
|
pub fn load_chats(&self, limit: i32) -> Result<Vec<IosChatSummary>, IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
let chats: Vec<_> = state
|
|
.chats
|
|
.iter()
|
|
.take(limit.max(0) as usize)
|
|
.cloned()
|
|
.collect();
|
|
state
|
|
.events
|
|
.push(IosEvent::ChatListChanged { chats: chats.clone() });
|
|
Ok(chats)
|
|
}
|
|
|
|
pub fn load_folders(&self) -> Vec<IosFolder> {
|
|
self.state
|
|
.lock()
|
|
.expect("session mutex poisoned")
|
|
.folders
|
|
.clone()
|
|
}
|
|
|
|
pub fn load_folder_chats(
|
|
&self,
|
|
folder_id: i32,
|
|
limit: i32,
|
|
) -> Result<Vec<IosChatSummary>, IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
let chats: Vec<_> = state
|
|
.chats
|
|
.iter()
|
|
.filter(|chat| chat.folder_ids.contains(&folder_id))
|
|
.take(limit.max(0) as usize)
|
|
.cloned()
|
|
.collect();
|
|
state
|
|
.events
|
|
.push(IosEvent::ChatListChanged { chats: chats.clone() });
|
|
Ok(chats)
|
|
}
|
|
|
|
pub fn load_history(&self, chat_id: i64, limit: i32) -> Result<Vec<IosMessage>, IosFfiError> {
|
|
let state = self.state.lock().expect("session mutex poisoned");
|
|
Ok(state
|
|
.messages
|
|
.get(&chat_id)
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.take(limit.max(0) as usize)
|
|
.collect())
|
|
}
|
|
|
|
pub fn search_messages(
|
|
&self,
|
|
chat_id: i64,
|
|
query: String,
|
|
) -> Result<Vec<IosSearchResult>, IosFfiError> {
|
|
let state = self.state.lock().expect("session mutex poisoned");
|
|
Ok(state
|
|
.messages
|
|
.get(&chat_id)
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter(|message| {
|
|
query.is_empty()
|
|
|| message.text.contains(&query)
|
|
|| message.sender_name.contains(&query)
|
|
})
|
|
.map(|message| IosSearchResult { chat_id, message })
|
|
.collect())
|
|
}
|
|
|
|
pub fn open_profile(&self, chat_id: i64) -> Result<IosProfile, IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
let profile = state
|
|
.profiles
|
|
.get(&chat_id)
|
|
.cloned()
|
|
.ok_or_else(|| IosFfiError::Operation { message: "profile not found".to_string() })?;
|
|
state
|
|
.events
|
|
.push(IosEvent::ProfileLoaded { profile: profile.clone() });
|
|
Ok(profile)
|
|
}
|
|
|
|
pub fn send_message(
|
|
&self,
|
|
chat_id: i64,
|
|
text: String,
|
|
reply_to_message_id: Option<i64>,
|
|
) -> Result<IosMessage, IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
let message = IosMessage {
|
|
id: state.next_message_id,
|
|
sender_name: "Me".to_string(),
|
|
date: 1_700_000_001,
|
|
edit_date: None,
|
|
media_album_id: None,
|
|
text: text.clone(),
|
|
media: None,
|
|
is_outgoing: true,
|
|
is_read: true,
|
|
can_be_edited: true,
|
|
can_be_deleted_only_for_self: true,
|
|
can_be_deleted_for_all_users: true,
|
|
reply: reply_to_message_id.map(|message_id| IosReply {
|
|
message_id,
|
|
sender_name: "Alice".to_string(),
|
|
text: format!("Reply to #{message_id}"),
|
|
}),
|
|
forward: None,
|
|
reactions: Vec::new(),
|
|
};
|
|
state.next_message_id += 1;
|
|
state
|
|
.messages
|
|
.entry(chat_id)
|
|
.or_default()
|
|
.push(message.clone());
|
|
if let Some(chat) = state.chats.iter_mut().find(|chat| chat.id == chat_id) {
|
|
chat.last_message = text;
|
|
chat.draft = None;
|
|
}
|
|
state
|
|
.events
|
|
.push(IosEvent::MessageAdded { chat_id, message: message.clone() });
|
|
Ok(message)
|
|
}
|
|
|
|
pub fn edit_message(
|
|
&self,
|
|
chat_id: i64,
|
|
message_id: i64,
|
|
text: String,
|
|
) -> Result<IosMessage, IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
let updated = {
|
|
let message = state.message_mut(chat_id, message_id)?;
|
|
message.text = text;
|
|
message.edit_date = Some(1_700_000_002);
|
|
message.clone()
|
|
};
|
|
state
|
|
.events
|
|
.push(IosEvent::MessageUpdated { chat_id, message: updated.clone() });
|
|
Ok(updated)
|
|
}
|
|
|
|
pub fn delete_messages(
|
|
&self,
|
|
chat_id: i64,
|
|
message_ids: Vec<i64>,
|
|
_revoke: bool,
|
|
) -> Result<(), IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
state
|
|
.messages
|
|
.entry(chat_id)
|
|
.or_default()
|
|
.retain(|message| !message_ids.contains(&message.id));
|
|
state
|
|
.events
|
|
.push(IosEvent::MessageDeleted { chat_id, message_ids });
|
|
Ok(())
|
|
}
|
|
|
|
pub fn forward_messages(
|
|
&self,
|
|
to_chat_id: i64,
|
|
from_chat_id: i64,
|
|
message_ids: Vec<i64>,
|
|
) -> Result<(), IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
let source_messages: Vec<_> = state
|
|
.messages
|
|
.get(&from_chat_id)
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter(|message| message_ids.contains(&message.id))
|
|
.collect();
|
|
for source in source_messages {
|
|
let forwarded = IosMessage {
|
|
id: state.next_message_id,
|
|
sender_name: "Me".to_string(),
|
|
date: 1_700_000_003,
|
|
edit_date: None,
|
|
media_album_id: source.media_album_id,
|
|
text: source.text,
|
|
media: source.media,
|
|
is_outgoing: true,
|
|
is_read: true,
|
|
can_be_edited: true,
|
|
can_be_deleted_only_for_self: true,
|
|
can_be_deleted_for_all_users: true,
|
|
reply: None,
|
|
forward: Some(IosForward { sender_name: source.sender_name }),
|
|
reactions: Vec::new(),
|
|
};
|
|
state.next_message_id += 1;
|
|
state
|
|
.messages
|
|
.entry(to_chat_id)
|
|
.or_default()
|
|
.push(forwarded.clone());
|
|
state
|
|
.events
|
|
.push(IosEvent::MessageAdded { chat_id: to_chat_id, message: forwarded });
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn react(
|
|
&self,
|
|
chat_id: i64,
|
|
message_id: i64,
|
|
reaction: String,
|
|
) -> Result<Vec<IosReaction>, IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
let reactions = {
|
|
let message = state.message_mut(chat_id, message_id)?;
|
|
if let Some(index) = message
|
|
.reactions
|
|
.iter()
|
|
.position(|item| item.emoji == reaction)
|
|
{
|
|
message.reactions.remove(index);
|
|
} else {
|
|
message
|
|
.reactions
|
|
.push(IosReaction { emoji: reaction, count: 1, is_chosen: true });
|
|
}
|
|
message.reactions.clone()
|
|
};
|
|
state.events.push(IosEvent::ReactionChanged {
|
|
chat_id,
|
|
message_id,
|
|
reactions: reactions.clone(),
|
|
});
|
|
Ok(reactions)
|
|
}
|
|
|
|
pub fn set_draft(&self, chat_id: i64, text: String) -> Result<(), IosFfiError> {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
let draft = IosDraft { chat_id, text };
|
|
if let Some(chat) = state.chats.iter_mut().find(|chat| chat.id == chat_id) {
|
|
chat.draft = Some(draft.clone());
|
|
}
|
|
state.events.push(IosEvent::DraftChanged { draft });
|
|
Ok(())
|
|
}
|
|
|
|
pub fn download_photo(&self, file_id: i32) -> Result<IosDownloadedFile, IosFfiError> {
|
|
self.standalone_download_file(file_id)
|
|
}
|
|
|
|
pub fn download_voice(&self, file_id: i32) -> Result<IosDownloadedFile, IosFfiError> {
|
|
self.standalone_download_file(file_id)
|
|
}
|
|
|
|
pub fn simulate_incoming_message(&self, chat_id: i64, text: String, sender_name: String) {
|
|
let mut state = self.state.lock().expect("session mutex poisoned");
|
|
let message = IosMessage {
|
|
id: state.next_message_id,
|
|
sender_name: sender_name.clone(),
|
|
date: 1_700_000_004,
|
|
edit_date: None,
|
|
media_album_id: None,
|
|
text: text.clone(),
|
|
media: None,
|
|
is_outgoing: false,
|
|
is_read: false,
|
|
can_be_edited: false,
|
|
can_be_deleted_only_for_self: true,
|
|
can_be_deleted_for_all_users: false,
|
|
reply: None,
|
|
forward: None,
|
|
reactions: Vec::new(),
|
|
};
|
|
state.next_message_id += 1;
|
|
state
|
|
.messages
|
|
.entry(chat_id)
|
|
.or_default()
|
|
.push(message.clone());
|
|
let notification_chat =
|
|
if let Some(chat) = state.chats.iter_mut().find(|chat| chat.id == chat_id) {
|
|
chat.last_message = text;
|
|
Some(chat.clone())
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(chat) = notification_chat {
|
|
state.events.push(IosEvent::IncomingNotificationCandidate {
|
|
chat,
|
|
message: message.clone(),
|
|
sender_name,
|
|
});
|
|
}
|
|
state
|
|
.events
|
|
.push(IosEvent::MessageAdded { chat_id, message });
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "core-session"))]
|
|
impl SessionHandle {
|
|
fn standalone_download_file(&self, file_id: i32) -> Result<IosDownloadedFile, IosFfiError> {
|
|
let state = self.state.lock().expect("session mutex poisoned");
|
|
state
|
|
.downloaded_files
|
|
.get(&file_id)
|
|
.cloned()
|
|
.map(|path| IosDownloadedFile { file_id, path })
|
|
.ok_or_else(|| IosFfiError::Operation { message: "file not found".to_string() })
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn fake_session_can_load_send_react_search_and_poll_events() {
|
|
let session = create_session(IosSessionConfig {
|
|
account_id: "fake".to_string(),
|
|
display_name: "Fake".to_string(),
|
|
database_path: "/tmp/fake".to_string(),
|
|
use_fake_tdlib: true,
|
|
})
|
|
.unwrap();
|
|
|
|
let chats = session.load_chats(20).unwrap();
|
|
assert_eq!(chats.len(), 1);
|
|
|
|
let history = session.load_history(chats[0].id, 20).unwrap();
|
|
assert_eq!(history[0].text, "Hello from fake TDLib");
|
|
|
|
let sent = session
|
|
.send_message(chats[0].id, "Hi from Swift".to_string(), None)
|
|
.unwrap();
|
|
assert_eq!(sent.text, "Hi from Swift");
|
|
|
|
let reactions = session
|
|
.react(chats[0].id, sent.id, "👍".to_string())
|
|
.unwrap();
|
|
assert_eq!(reactions[0].emoji, "👍");
|
|
|
|
let results = session
|
|
.search_messages(chats[0].id, "Swift".to_string())
|
|
.unwrap();
|
|
assert_eq!(results.len(), 1);
|
|
|
|
session.simulate_incoming_message(chats[0].id, "Incoming".to_string(), "Bob".to_string());
|
|
let events = session.poll_events();
|
|
assert!(events
|
|
.iter()
|
|
.any(|event| matches!(event, IosEvent::MessageAdded { .. })));
|
|
assert!(events
|
|
.iter()
|
|
.any(|event| matches!(event, IosEvent::ReactionChanged { .. })));
|
|
assert!(events
|
|
.iter()
|
|
.any(|event| matches!(event, IosEvent::IncomingNotificationCandidate { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn real_tdlib_sessions_are_reported_as_not_yet_exposed() {
|
|
let error = match create_session(IosSessionConfig {
|
|
account_id: "real".to_string(),
|
|
display_name: "Real".to_string(),
|
|
database_path: "/tmp/tdlib".to_string(),
|
|
use_fake_tdlib: false,
|
|
}) {
|
|
Ok(_) => panic!("real TDLib session should not be exposed yet"),
|
|
Err(error) => error,
|
|
};
|
|
|
|
assert!(format!("{error}").contains("real TDLib sessions"));
|
|
}
|
|
}
|