Add UniFFI iOS bridge crate
This commit is contained in:
345
Cargo.lock
generated
345
Cargo.lock
generated
@@ -203,6 +203,48 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"itoa",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"basic-toml",
|
||||
"memchr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -227,6 +269,19 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compat"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.3"
|
||||
@@ -375,7 +430,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"log",
|
||||
"nom",
|
||||
"nom 8.0.0",
|
||||
"num-rational",
|
||||
"v_frame",
|
||||
]
|
||||
@@ -405,6 +460,15 @@ dependencies = [
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "basic-toml"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.3"
|
||||
@@ -518,6 +582,38 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo-platform"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo_metadata"
|
||||
version = "0.19.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"cargo-platform",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
@@ -1408,6 +1504,15 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs-err"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs2"
|
||||
version = "0.4.3"
|
||||
@@ -1538,6 +1643,23 @@ dependencies = [
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "goblin"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
|
||||
dependencies = [
|
||||
"log",
|
||||
"plain",
|
||||
"scroll",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
@@ -2330,6 +2452,12 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2414,6 +2542,16 @@ dependencies = [
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
@@ -2833,6 +2971,12 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
@@ -3342,6 +3486,12 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3479,6 +3629,26 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "scroll"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
|
||||
dependencies = [
|
||||
"scroll_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scroll_derive"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
@@ -3507,6 +3677,10 @@ name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
@@ -3776,6 +3950,12 @@ version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
@@ -3788,6 +3968,12 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.2"
|
||||
@@ -3958,6 +4144,16 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tele-ios-ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"tele-core",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tele-tui"
|
||||
version = "0.1.0"
|
||||
@@ -4036,6 +4232,15 @@ dependencies = [
|
||||
"vt100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -4474,6 +4679,135 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "uniffi"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc5f2297ee5b893405bed1a6929faec4713a061df158ecf5198089f23910d470"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
"cargo_metadata",
|
||||
"clap",
|
||||
"uniffi_bindgen",
|
||||
"uniffi_core",
|
||||
"uniffi_macros",
|
||||
"uniffi_pipeline",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi-bindgen-swift"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_bindgen"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bc0c60a9607e7ab77a2ad47ec5530178015014839db25af7512447d2238016c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askama",
|
||||
"camino",
|
||||
"cargo_metadata",
|
||||
"fs-err",
|
||||
"glob",
|
||||
"goblin",
|
||||
"heck",
|
||||
"indexmap 2.13.0",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"textwrap",
|
||||
"toml",
|
||||
"uniffi_internal_macros",
|
||||
"uniffi_meta",
|
||||
"uniffi_pipeline",
|
||||
"uniffi_udl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_core"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77baf5d539fe2e1ad6805e942dbc5dbdeb2b83eb5f2b3a6535d422ca4b02a12f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compat",
|
||||
"bytes",
|
||||
"once_cell",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_internal_macros"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4b42137524f4be6400fcaca9d02c1d4ecb6ad917e4013c0b93235526d8396e5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap 2.13.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_macros"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9273ec45330d8fe9a3701b7b983cea7a4e218503359831967cb95d26b873561"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"fs-err",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn",
|
||||
"toml",
|
||||
"uniffi_meta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_meta"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "431d2f443e7828a6c29d188de98b6771a6491ee98bba2d4372643bf93f988a18"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"siphasher",
|
||||
"uniffi_internal_macros",
|
||||
"uniffi_pipeline",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_pipeline"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "761ef74f6175e15603d0424cc5f98854c5baccfe7bf4ccb08e5816f9ab8af689"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap 2.13.0",
|
||||
"tempfile",
|
||||
"uniffi_internal_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_udl"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68773ec0e1c067b6505a73bbf6a5782f31a7f9209333a0df97b87565c46bf370"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"textwrap",
|
||||
"uniffi_meta",
|
||||
"weedle2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
@@ -4692,6 +5026,15 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weedle2"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
[workspace]
|
||||
members = ["crates/tele-core", "crates/tele-tui"]
|
||||
members = [
|
||||
"crates/tele-core",
|
||||
"crates/tele-ios-ffi",
|
||||
"crates/tele-tui",
|
||||
"tools/uniffi-bindgen-swift",
|
||||
]
|
||||
default-members = ["crates/tele-tui"]
|
||||
resolver = "2"
|
||||
|
||||
20
crates/tele-ios-ffi/Cargo.toml
Normal file
20
crates/tele-ios-ffi/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "tele-ios-ffi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
description = "UniFFI bridge for the iOS Telegram client"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/your-username/tele-tui"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
tele-core = { path = "../tele-core", features = ["test-support"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
thiserror = "1.0"
|
||||
uniffi = { version = "0.31.1", features = ["tokio"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tele-core = { path = "../tele-core", features = ["test-support"] }
|
||||
25
crates/tele-ios-ffi/README.md
Normal file
25
crates/tele-ios-ffi/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# tele-ios-ffi
|
||||
|
||||
UniFFI bridge for the future native iOS app.
|
||||
|
||||
Current scope:
|
||||
|
||||
- Exposes a fake-backed `SessionHandle` for Swift integration tests and app shell work.
|
||||
- Mirrors the `tele-core::session` DTO/event model with UniFFI-compatible records and enums.
|
||||
- Keeps real TDLib session creation out of this crate until iOS simulator/device linking is validated.
|
||||
|
||||
Generate Swift bindings and headers:
|
||||
|
||||
```bash
|
||||
scripts/generate-ios-ffi-bindings.sh
|
||||
```
|
||||
|
||||
The script builds `target/release/libtele_ios_ffi.a` and writes Swift sources,
|
||||
headers, a Swift typecheck-friendly `tele_ios_ffiFFI` module map, and an
|
||||
XCFramework-compatible module map under `build/ios-ffi/`.
|
||||
|
||||
Known blocker:
|
||||
|
||||
- `xcodebuild -version` currently fails on this machine because only Command Line Tools are selected:
|
||||
`xcode-select: error: tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance`.
|
||||
- Real TDLib iOS simulator/device linking therefore is not validated in this phase.
|
||||
809
crates/tele-ios-ffi/src/lib.rs
Normal file
809
crates/tele-ios-ffi/src/lib.rs
Normal file
@@ -0,0 +1,809 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use tele_core::session::{
|
||||
CoreAuthState, CoreChatSummary, CoreDownloadedFile, CoreDraft, CoreEvent, CoreFolder,
|
||||
CoreMedia, CoreMessage, CoreNetworkState, CoreProfile, CoreReaction, CoreSearchResult,
|
||||
CoreSession,
|
||||
};
|
||||
use tele_core::tdlib::{ChatInfo, MessageBuilder, NetworkState, ProfileInfo};
|
||||
use tele_core::test_support::FakeTdClient;
|
||||
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 },
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
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)]
|
||||
pub struct SessionHandle {
|
||||
session: Mutex<CoreSession<FakeTdClient>>,
|
||||
runtime: tokio::runtime::Runtime,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
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]
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
#[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"));
|
||||
}
|
||||
}
|
||||
26
scripts/generate-ios-ffi-bindings.sh
Executable file
26
scripts/generate-ios-ffi-bindings.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
OUT_DIR="${1:-$ROOT_DIR/build/ios-ffi}"
|
||||
LIB_PATH="$ROOT_DIR/target/release/libtele_ios_ffi.a"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
cargo build -p tele-ios-ffi --release
|
||||
rm -rf "$OUT_DIR"
|
||||
mkdir -p "$OUT_DIR/Swift" "$OUT_DIR/Headers" "$OUT_DIR/Modules"
|
||||
|
||||
cargo run -p uniffi-bindgen-swift -- "$LIB_PATH" "$OUT_DIR/Swift" --swift-sources
|
||||
cargo run -p uniffi-bindgen-swift -- "$LIB_PATH" "$OUT_DIR/Headers" --headers
|
||||
cargo run -p uniffi-bindgen-swift -- "$LIB_PATH" "$OUT_DIR/Headers" \
|
||||
--modulemap \
|
||||
--module-name tele_ios_ffiFFI \
|
||||
--modulemap-filename module.modulemap
|
||||
cargo run -p uniffi-bindgen-swift -- "$LIB_PATH" "$OUT_DIR/Modules" \
|
||||
--xcframework \
|
||||
--modulemap \
|
||||
--module-name tele_ios_ffiFFI \
|
||||
--modulemap-filename module.modulemap
|
||||
|
||||
printf 'Generated UniFFI Swift bindings in %s\n' "$OUT_DIR"
|
||||
8
tools/uniffi-bindgen-swift/Cargo.toml
Normal file
8
tools/uniffi-bindgen-swift/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "uniffi-bindgen-swift"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
uniffi = { version = "0.31.1", features = ["cli"] }
|
||||
3
tools/uniffi-bindgen-swift/src/main.rs
Normal file
3
tools/uniffi-bindgen-swift/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_swift()
|
||||
}
|
||||
Reference in New Issue
Block a user