Compare commits

...

7 Commits

Author SHA1 Message Date
Lonami Exo 912949a079 Use snake_case in UDL
uniffi automatically converts to camelCase for Kotlin,
but it won't convert to snake_case for Rust.
2022-10-27 19:48:47 +02:00
Lonami Exo 1574ce683c Inline native methods
The split no longer makes sense now that uniffi is the bridge.
2022-10-27 19:44:48 +02:00
Lonami Exo d11a00d062 Make use of errors in the UDL 2022-10-27 19:42:21 +02:00
Lonami Exo 004a921299 Use UDL dictionaries to simplify return values 2022-10-27 18:42:48 +02:00
Lonami Exo 1a56b03614 Move MessageAck enum to UDL 2022-10-27 18:08:57 +02:00
Lonami Exo 812597f027 Replace manual native bindings with UniFFI 2022-10-27 17:42:55 +02:00
Lonami Exo fd1dac1045 Use trailing comma to improve future diffs 2022-10-25 11:33:45 +02:00
18 changed files with 234 additions and 403 deletions

View File

@ -1,6 +1,7 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">

View File

@ -57,6 +57,7 @@ dependencies {
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation 'androidx.compose.material:material:1.2.1' implementation 'androidx.compose.material:material:1.2.1'
implementation "androidx.navigation:navigation-compose:2.5.2" implementation "androidx.navigation:navigation-compose:2.5.2"
implementation "net.java.dev.jna:jna:5.12.0@aar"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@ -68,7 +69,7 @@ dependencies {
// See https://github.com/mozilla/rust-android-gradle for other targets and required toolchains // See https://github.com/mozilla/rust-android-gradle for other targets and required toolchains
cargo { cargo {
module = "../native" module = "../native"
libname = "talaria" libname = "uniffi_talaria"
targets = ["arm64"] targets = ["arm64"]
profile = 'release' profile = 'release'
} }
@ -78,3 +79,14 @@ tasks.whenTaskAdded { task ->
task.dependsOn 'cargoBuild' task.dependsOn 'cargoBuild'
} }
} }
// See https://mozilla.github.io/uniffi-rs/kotlin/gradle.html for more details on this snippet
android.applicationVariants.all { variant ->
def t = tasks.register("generate${variant.name.capitalize()}UniFFIBindings", Exec) {
workingDir "${project.projectDir}"
commandLine 'uniffi-bindgen', 'generate', '../native/src/talaria.udl', '--language', 'kotlin', '--no-format', '--out-dir', "${buildDir}/generated/source/uniffi/${variant.name}/java"
}
variant.javaCompileProvider.get().dependsOn(t)
def sourceSet = variant.sourceSets.find { it.name == variant.name }
sourceSet.java.srcDir new File(buildDir, "generated/source/uniffi/${variant.name}/java")
}

View File

@ -7,8 +7,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.ui.theme.TalariaTheme import dev.lonami.talaria.ui.theme.TalariaTheme
import uniffi.talaria.initClient
import uniffi.talaria.initDatabase
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -24,7 +25,7 @@ class MainActivity : ComponentActivity() {
} }
} }
Native.initDatabase(getDatabasePath("talaria.db").path) initDatabase(getDatabasePath("talaria.db").path)
Native.initClient() initClient()
} }
} }

View File

@ -18,11 +18,11 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.ui.screens.ChatScreen import dev.lonami.talaria.ui.screens.ChatScreen
import dev.lonami.talaria.ui.screens.DialogScreen import dev.lonami.talaria.ui.screens.DialogScreen
import dev.lonami.talaria.ui.screens.LoginScreen import dev.lonami.talaria.ui.screens.LoginScreen
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uniffi.talaria.needLogin
enum class TalariaScreen(@StringRes val title: Int) { enum class TalariaScreen(@StringRes val title: Int) {
Login(title = R.string.app_name), Login(title = R.string.app_name),
@ -148,7 +148,7 @@ fun TalariaApp(modifier: Modifier = Modifier) {
val currentScreen = val currentScreen =
TalariaScreen.valueOf(backStackEntry?.destination?.route ?: TalariaScreen.Login.name) TalariaScreen.valueOf(backStackEntry?.destination?.route ?: TalariaScreen.Login.name)
val loggedIn by remember { mutableStateOf(!Native.needLogin()) } val loggedIn by remember { mutableStateOf(!needLogin()) }
var selectedDialog by remember { mutableStateOf("") } var selectedDialog by remember { mutableStateOf("") }
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)

View File

@ -1,24 +0,0 @@
package dev.lonami.talaria.bindings
object Native {
init {
System.loadLibrary("talaria")
}
external fun initDatabase(path: String)
external fun initClient()
external fun needLogin(): Boolean
external fun requestLoginCode(phone: String): Long
external fun signIn(tokenPtr: Long, code: String)
external fun getDialogs(): Long
external fun dialogCount(dialogsPtr: Long): Int
external fun dialogPacked(dialogsPtr: Long, index: Int): String
external fun dialogTitle(dialogsPtr: Long, index: Int): String
external fun dialogSender(dialogsPtr: Long, index: Int): String
external fun dialogText(dialogsPtr: Long, index: Int): String
external fun dialogTime(dialogsPtr: Long, index: Int): String
external fun dialogAck(dialogsPtr: Long, index: Int): Int
external fun dialogPin(dialogsPtr: Long, index: Int): Int
external fun freeDialogs(dialogsPtr: Long)
external fun sendMessage(packed: String, text: String)
}

View File

@ -1,11 +1,10 @@
package dev.lonami.talaria.data package dev.lonami.talaria.data
import dev.lonami.talaria.bindings.Native import uniffi.talaria.Dialog
import dev.lonami.talaria.models.Dialog import uniffi.talaria.MessageAck
import dev.lonami.talaria.models.MessageAck import uniffi.talaria.MessagePreview
import dev.lonami.talaria.models.MessagePreview import uniffi.talaria.getDialogs
import java.time.LocalDateTime import java.time.Instant
import java.time.format.DateTimeFormatter
interface DialogRepository { interface DialogRepository {
fun loadDialogs(): List<Dialog> fun loadDialogs(): List<Dialog>
@ -13,39 +12,7 @@ interface DialogRepository {
class NativeDialogRepository : DialogRepository { class NativeDialogRepository : DialogRepository {
override fun loadDialogs(): List<Dialog> { override fun loadDialogs(): List<Dialog> {
val dialogs = mutableListOf<Dialog>() return getDialogs()
val dialogPtr = Native.getDialogs()
try {
val dialogCount = Native.dialogCount(dialogPtr)
for (i in 0 until dialogCount) {
dialogs.add(
Dialog(
id = Native.dialogPacked(dialogPtr, i),
title = Native.dialogTitle(dialogPtr, i),
lastMessage = MessagePreview(
sender = Native.dialogSender(dialogPtr, i),
text = Native.dialogText(dialogPtr, i),
date = LocalDateTime.parse(
Native.dialogTime(dialogPtr, i),
DateTimeFormatter.ISO_OFFSET_DATE_TIME
),
ack = when (Native.dialogAck(dialogPtr, i)) {
0 -> MessageAck.RECEIVED
1 -> MessageAck.SENT
2 -> MessageAck.SEEN
else -> MessageAck.RECEIVED
}
),
pinned = Native.dialogPin(dialogPtr, i) != 0
)
)
}
} finally {
Native.freeDialogs(dialogPtr)
}
return dialogs
} }
} }
@ -71,7 +38,7 @@ class MockDialogRepository : DialogRepository {
} else { } else {
"Sample Message $i" "Sample Message $i"
}, },
date = LocalDateTime.now(), date = Instant.now(),
ack = when (i % 3) { ack = when (i % 3) {
0 -> MessageAck.RECEIVED 0 -> MessageAck.RECEIVED
1 -> MessageAck.SENT 1 -> MessageAck.SENT

View File

@ -1,8 +0,0 @@
package dev.lonami.talaria.models
data class Dialog(
val id: String,
val title: String,
val lastMessage: MessagePreview?,
val pinned: Boolean
)

View File

@ -1,7 +0,0 @@
package dev.lonami.talaria.models
enum class MessageAck {
RECEIVED,
SENT,
SEEN,
}

View File

@ -1,10 +0,0 @@
package dev.lonami.talaria.models
import java.time.LocalDateTime
data class MessagePreview(
val sender: String,
val text: String,
val date: LocalDateTime,
val ack: MessageAck
)

View File

@ -23,10 +23,12 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import dev.lonami.talaria.R import dev.lonami.talaria.R
import dev.lonami.talaria.data.MockDialogRepository import dev.lonami.talaria.data.MockDialogRepository
import dev.lonami.talaria.models.Dialog
import dev.lonami.talaria.models.MessageAck
import dev.lonami.talaria.ui.state.DialogViewModel import dev.lonami.talaria.ui.state.DialogViewModel
import dev.lonami.talaria.ui.theme.TalariaTheme import dev.lonami.talaria.ui.theme.TalariaTheme
import uniffi.talaria.Dialog
import uniffi.talaria.MessageAck
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
@ -54,8 +56,8 @@ fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit, modifier: Modifier = Mo
Text( Text(
stringResource( stringResource(
R.string.message_preview, R.string.message_preview,
dialog.lastMessage.sender, dialog.lastMessage!!.sender,
dialog.lastMessage.text dialog.lastMessage!!.text
), ),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@ -65,7 +67,7 @@ fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit, modifier: Modifier = Mo
Column { Column {
if (dialog.lastMessage != null) { if (dialog.lastMessage != null) {
Row { Row {
when (dialog.lastMessage.ack) { when (dialog.lastMessage!!.ack) {
MessageAck.RECEIVED -> {} MessageAck.RECEIVED -> {}
MessageAck.SENT -> Icon( MessageAck.SENT -> Icon(
painterResource(R.drawable.sent), painterResource(R.drawable.sent),
@ -78,7 +80,7 @@ fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit, modifier: Modifier = Mo
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
dialog.lastMessage.date.format( LocalDateTime.ofInstant(dialog.lastMessage!!.date, ZoneOffset.UTC).format(
DateTimeFormatter.ofLocalizedTime( DateTimeFormatter.ofLocalizedTime(
FormatStyle.SHORT FormatStyle.SHORT
) )

View File

@ -18,8 +18,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import dev.lonami.talaria.R import dev.lonami.talaria.R
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.ui.theme.TalariaTheme import dev.lonami.talaria.ui.theme.TalariaTheme
import uniffi.talaria.requestLoginCode
import uniffi.talaria.signIn
enum class LoginStage { enum class LoginStage {
ASK_PHONE, ASK_PHONE,
@ -105,7 +106,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
var phone by remember { mutableStateOf("") } var phone by remember { mutableStateOf("") }
var otp by remember { mutableStateOf("") } var otp by remember { mutableStateOf("") }
var tokenPtr by remember { mutableStateOf(0L) } var tokenPtr by remember { mutableStateOf(0UL) }
Column( Column(
modifier = modifier modifier = modifier
@ -127,7 +128,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
phone, phone,
onPhoneChanged = { phone = it }, onPhoneChanged = { phone = it },
onSendCode = { onSendCode = {
tokenPtr = Native.requestLoginCode(phone) tokenPtr = requestLoginCode(phone)
stage = LoginStage.ASK_CODE stage = LoginStage.ASK_CODE
} }
) )
@ -135,7 +136,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
otp, otp,
onOtpChanged = { otp = it }, onOtpChanged = { otp = it },
onConfirmOtp = { onConfirmOtp = {
Native.signIn(tokenPtr, otp) signIn(tokenPtr, otp)
onConfirmOtp() onConfirmOtp()
} }
) )

View File

@ -1,7 +1,6 @@
package dev.lonami.talaria.ui.state package dev.lonami.talaria.ui.state
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.data.MessageRepository import dev.lonami.talaria.data.MessageRepository
import dev.lonami.talaria.models.Message import dev.lonami.talaria.models.Message
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -18,7 +17,7 @@ class ChatViewModel : ViewModel() {
} }
fun sendMessage(dialog: String, message: String) { fun sendMessage(dialog: String, message: String) {
Native.sendMessage(dialog, message) sendMessage(dialog, message)
_uiState.update { state -> _uiState.update { state ->
state.messages.add(Message("You", message)) state.messages.add(Message("You", message))
state state

View File

@ -1,5 +1,5 @@
package dev.lonami.talaria.ui.state package dev.lonami.talaria.ui.state
import dev.lonami.talaria.models.Dialog import uniffi.talaria.Dialog
data class DialogUiState(val dialogs: List<Dialog> = listOf()) data class DialogUiState(val dialogs: List<Dialog> = listOf())

View File

@ -4,8 +4,10 @@ buildscript {
} }
}// Top-level build file where you can add configuration options common to all sub-projects/modules. }// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '7.3.0' apply false // uniffi-recommended Gradle script fails with Android plugin 7.3.1.
id 'com.android.library' version '7.3.0' apply false // See https://github.com/mozilla/uniffi-rs/issues/1386 for details.
id 'com.android.application' version '7.2.2' apply false
id 'com.android.library' version '7.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3" id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3"
} }

View File

@ -4,11 +4,12 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[lib] [lib]
name = "talaria" name = "uniffi_talaria"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
jni = { version = "0.10.2", default-features = false } jni = { version = "0.10.2", default-features = false }
uniffi = "0.21.0"
# v0.4 of grammers-* is currently unreleased; clone the project and use path dependencies # v0.4 of grammers-* is currently unreleased; clone the project and use path dependencies
grammers-client = { version = "0.4.1" } grammers-client = { version = "0.4.1" }
grammers-tl-types = { version = "0.4.0" } grammers-tl-types = { version = "0.4.0" }
@ -19,5 +20,8 @@ android_logger = "0.11.1"
once_cell = "1.15.0" once_cell = "1.15.0"
sqlite = "0.27.0" sqlite = "0.27.0"
[build-dependencies]
uniffi_build = "0.21.0"
[profile.release] [profile.release]
lto = true lto = true

3
native/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
uniffi_build::generate_scaffolding("src/talaria.udl").unwrap();
}

View File

@ -3,26 +3,24 @@
mod db; mod db;
use grammers_client::types::{Dialog, LoginToken}; use grammers_client::types::LoginToken;
use grammers_client::{Client, Config, InitParams}; use grammers_client::{Client, Config, InitParams};
use grammers_session::{PackedChat, Session, UpdateState}; use grammers_session::{PackedChat, Session, UpdateState};
use grammers_tl_types as tl; use grammers_tl_types as tl;
use jni::objects::{JObject, JString};
use jni::sys::{jboolean, jint, jlong, jstring};
use jni::JNIEnv;
use log; use log;
use log::{error, info, Level}; use log::{error, info, Level};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::{CStr, CString}; use std::fmt;
use std::future::Future; use std::future::Future;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::Mutex; use std::sync::Mutex;
use std::time::SystemTime;
use tokio::runtime; use tokio::runtime;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; include!(concat!(env!("OUT_DIR"), "/talaria.uniffi.rs"));
const LOG_MIN_LEVEL: Level = Level::Trace; const LOG_MIN_LEVEL: Level = Level::Trace;
const LOG_TAG: &str = ".native.talari"; const LOG_TAG: &str = ".native.talari";
@ -47,6 +45,50 @@ static RUNTIME: OnceCell<Runtime> = OnceCell::new();
static CLIENT: OnceCell<Client> = OnceCell::new(); static CLIENT: OnceCell<Client> = OnceCell::new();
static DATABASE: Mutex<Option<sqlite::Connection>> = Mutex::new(None); static DATABASE: Mutex<Option<sqlite::Connection>> = Mutex::new(None);
#[derive(Debug, Clone, Copy)]
pub enum NativeError {
Initialization,
Database,
Network,
}
impl fmt::Display for NativeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Initialization => write!(f, "a resource could not be initialized correctly"),
Self::Database => write!(f, "a query to the local database failed"),
Self::Network => write!(f, "a network request could not complete successfully"),
}
}
}
impl std::error::Error for NativeError {}
#[derive(Debug, Clone, Copy)]
pub enum MessageAck {
Received,
Seen,
Sent,
}
#[derive(Debug, Clone)]
pub struct MessagePreview {
sender: String,
text: String,
date: SystemTime,
ack: MessageAck,
}
#[derive(Debug, Clone)]
pub struct Dialog {
id: String,
title: String,
last_message: Option<MessagePreview>,
pinned: bool,
}
type Result<T> = std::result::Result<T, NativeError>;
fn block_on<F: Future>(future: F) -> F::Output { fn block_on<F: Future>(future: F) -> F::Output {
if RUNTIME.get().is_none() { if RUNTIME.get().is_none() {
RUNTIME RUNTIME
@ -62,7 +104,22 @@ fn block_on<F: Future>(future: F) -> F::Output {
RUNTIME.get().unwrap().block_on(future) RUNTIME.get().unwrap().block_on(future)
} }
async fn init_client() -> Result<()> { pub fn init_database(path: String) -> Result<()> {
let mut guard = DATABASE.lock().unwrap();
if guard.is_some() {
info!("Database is already initialized");
}
match db::init_connection(&path) {
Ok(conn) => {
*guard = Some(conn);
Ok(())
}
Err(_) => Err(NativeError::Database),
}
}
pub fn init_client() -> Result<()> {
android_logger::init_once( android_logger::init_once(
android_logger::Config::default() android_logger::Config::default()
.with_min_level(LOG_MIN_LEVEL) .with_min_level(LOG_MIN_LEVEL)
@ -78,7 +135,8 @@ async fn init_client() -> Result<()> {
let conn = match guard.as_ref() { let conn = match guard.as_ref() {
Some(c) => c, Some(c) => c,
None => { None => {
return Err("Database was not initialized".into()); error!("Database was not initialized");
return Err(NativeError::Initialization);
} }
}; };
@ -86,7 +144,7 @@ async fn init_client() -> Result<()> {
let session = Session::new(); let session = Session::new();
let sessions = db::get_sessions(conn)?; let sessions = db::get_sessions(conn).map_err(|_| NativeError::Database)?;
if let Some(s) = sessions.get(0) { if let Some(s) = sessions.get(0) {
match (s.user_id, s.dc_id, s.bot) { match (s.user_id, s.dc_id, s.bot) {
(Some(id), Some(dc), Some(bot)) => session.set_user(id, dc, bot), (Some(id), Some(dc), Some(bot)) => session.set_user(id, dc, bot),
@ -112,7 +170,7 @@ async fn init_client() -> Result<()> {
} }
} }
let client = Client::connect(Config { let client = block_on(Client::connect(Config {
session, session,
api_id: API_ID, api_id: API_ID,
api_hash: API_HASH.to_string(), api_hash: API_HASH.to_string(),
@ -124,42 +182,46 @@ async fn init_client() -> Result<()> {
}, },
..Default::default() ..Default::default()
}, },
}) }))
.await?; .map_err(|_| NativeError::Network)?;
info!("Connected!"); info!("Connected!");
CLIENT CLIENT
.set(client) .set(client)
.map_err(|_| "Client was already initialized")?; .map_err(|_| NativeError::Initialization)?;
Ok(()) Ok(())
} }
async fn need_login() -> Result<bool> { pub fn need_login() -> Result<bool> {
let client = CLIENT.get().ok_or("Client not initialized")?; let client = CLIENT.get().ok_or(NativeError::Initialization)?;
Ok(client.is_authorized().await?) block_on(client.is_authorized()).map_err(|_| NativeError::Network)
} }
async fn request_login_code(phone: &str) -> Result<LoginToken> { pub fn request_login_code(phone: String) -> Result<u64> {
let client = CLIENT.get().ok_or("Client not initialized")?; let client = CLIENT.get().ok_or(NativeError::Initialization)?;
let token = client.request_login_code(&phone, API_ID, API_HASH).await?; block_on(client.request_login_code(&phone, API_ID, API_HASH))
Ok(token) .map(|token| Box::into_raw(Box::new(token)) as u64)
.map_err(|_| NativeError::Network)
} }
async fn sign_in(token: LoginToken, code: &str) -> Result<()> { pub fn sign_in(token_ptr: u64, code: String) -> Result<()> {
let client = CLIENT.get().ok_or("Client not initialized")?; let token = unsafe { *Box::from_raw(token_ptr as *mut LoginToken) };
client.sign_in(&token, &code).await?; let client = CLIENT.get().ok_or(NativeError::Initialization)?;
block_on(client.sign_in(&token, &code)).map_err(|_| NativeError::Network)?;
let guard = DATABASE.lock().unwrap(); let guard = DATABASE.lock().unwrap();
let conn = match guard.as_ref() { let conn = match guard.as_ref() {
Some(c) => c, Some(c) => c,
None => { None => {
return Err("Database was not initialized".into()); error!("Database was not initialized");
return Err(NativeError::Initialization);
} }
}; };
let mut session = db::create_session(conn)?; let mut session = db::create_session(conn).map_err(|_| NativeError::Database)?;
let s = client.session(); let s = client.session();
if let Some(user) = s.get_user() { if let Some(user) = s.get_user() {
session.user_id = Some(user.id); session.user_id = Some(user.id);
@ -189,280 +251,63 @@ async fn sign_in(token: LoginToken, code: &str) -> Result<()> {
} }
} }
db::update_session(conn, &session)?; db::update_session(conn, &session).map_err(|_| NativeError::Database)?;
Ok(()) Ok(())
} }
async fn get_dialogs() -> Result<Vec<Dialog>> { pub fn get_dialogs() -> Result<Vec<Dialog>> {
let client = CLIENT.get().ok_or("Client not initialized")?; let client = CLIENT.get().ok_or(NativeError::Initialization)?;
let mut result = Vec::new(); block_on(async {
let mut dialogs = client.iter_dialogs(); let mut result = Vec::new();
while let Some(dialog) = dialogs.next().await? { let mut dialogs = client.iter_dialogs();
result.push(dialog); while let Some(dialog) = dialogs.next().await.map_err(|_| NativeError::Network)? {
} result.push(dialog);
Ok(result)
}
async fn send_message(chat: PackedChat, text: &str) -> Result<()> {
let client = CLIENT.get().ok_or("Client not initialized")?;
client.send_message(chat, text).await?;
Ok(())
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_initDatabase(
env: JNIEnv,
_: JObject,
path: JString,
) {
let mut guard = DATABASE.lock().unwrap();
if guard.is_some() {
info!("Database is already initialized");
}
let path = CString::from(CStr::from_ptr(env.get_string(path).unwrap().as_ptr()));
match db::init_connection(path.to_str().unwrap()) {
Ok(conn) => *guard = Some(conn),
Err(e) => error!("Failed to initialize database: {}", e),
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_initClient(_: JNIEnv, _: JObject) {
match block_on(init_client()) {
Ok(_) => info!("Client initialized"),
Err(e) => error!("Failed to initialize client: {}", e),
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_needLogin(
_: JNIEnv,
_: JObject,
) -> jboolean {
match block_on(need_login()) {
Ok(login) => login.into(),
Err(e) => {
error!("Failed to check if user is authorized: {}", e);
false.into()
} }
} Ok(result)
} })
.map(|dialogs| {
#[no_mangle] dialogs
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_requestLoginCode( .into_iter()
env: JNIEnv, .map(|d| Dialog {
_: JObject, id: d.chat().pack().to_hex(),
phone: JString, title: d.chat().name().to_string(),
) -> jlong { last_message: d.last_message.map(|m| MessagePreview {
let phone = CString::from(CStr::from_ptr(env.get_string(phone).unwrap().as_ptr())); sender: if let Some(sender) = m.sender() {
sender.name().to_string()
match block_on(request_login_code(phone.to_str().unwrap())) {
Ok(token) => Box::into_raw(Box::new(token)) as jlong,
Err(e) => {
error!("Failed to request login code: {}", e);
0 as jlong
}
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_signIn(
env: JNIEnv,
_: JObject,
token_ptr: jlong,
code: JString,
) {
let token = *Box::from_raw(token_ptr as *mut LoginToken);
let code = CString::from(CStr::from_ptr(env.get_string(code).unwrap().as_ptr()));
match block_on(sign_in(token, code.to_str().unwrap())) {
Ok(_) => info!("Sign in success"),
Err(e) => error!("Failed to sign in: {}", e),
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_getDialogs(
_: JNIEnv,
_: JObject,
) -> jlong {
match block_on(get_dialogs()) {
Ok(dialogs) => Box::into_raw(Box::new(dialogs)) as jlong,
Err(e) => {
error!("Failed to get dialogs: {}", e);
0 as jlong
}
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogCount(
_: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
) -> jint {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
dialogs.len() as jint
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogPacked(
env: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jstring {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let packed = dialogs[index as usize].chat().pack().to_hex();
let output = env.new_string(packed).unwrap();
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogTitle(
env: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jstring {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let title = dialogs[index as usize].chat().name();
let output = env.new_string(title).unwrap();
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogSender(
env: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jstring {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let sender = if let Some(msg) = dialogs[index as usize].last_message.as_ref() {
if let Some(sender) = msg.sender() {
sender.name().to_string()
} else {
"unknown".to_string()
}
} else {
String::new()
};
let output = env.new_string(sender).unwrap();
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogText(
env: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jstring {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let text = if let Some(msg) = dialogs[index as usize].last_message.as_ref() {
msg.text()
} else {
""
};
let output = env.new_string(text).unwrap();
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogTime(
env: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jstring {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let time = if let Some(msg) = dialogs[index as usize].last_message.as_ref() {
msg.date().to_rfc3339().to_string()
} else {
String::new()
};
let output = env.new_string(time).unwrap();
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogAck(
_: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jint {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let dialog = &dialogs[index as usize];
let ack = if let Some(msg) = dialog.last_message.as_ref() {
if msg.outgoing() {
match &dialog.dialog {
tl::enums::Dialog::Dialog(d) => {
if msg.id() <= d.read_inbox_max_id {
2
} else { } else {
1 "unknown".to_string()
} },
} text: m.text().to_string(),
tl::enums::Dialog::Folder(_) => 0, date: m.date().into(),
} ack: if m.outgoing() {
} else { match &d.dialog {
0 tl::enums::Dialog::Dialog(d) => {
} if m.id() <= d.read_inbox_max_id {
} else { MessageAck::Seen
0 } else {
}; MessageAck::Sent
ack }
}
tl::enums::Dialog::Folder(_) => MessageAck::Received,
}
} else {
MessageAck::Received
},
}),
pinned: match d.dialog {
tl::enums::Dialog::Dialog(d) => d.pinned,
tl::enums::Dialog::Folder(f) => f.pinned,
},
})
.collect()
})
} }
#[no_mangle] pub fn send_message(packed: String, text: String) -> Result<()> {
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogPin( let chat = PackedChat::from_hex(&packed).unwrap();
_: JNIEnv, let client = CLIENT.get().ok_or(NativeError::Initialization)?;
_: JObject, block_on(client.send_message(chat, text)).map_err(|_| NativeError::Network)?;
dialogs_ptr: jlong, Ok(())
index: jint,
) -> jint {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let pinned = match &dialogs[index as usize].dialog {
tl::enums::Dialog::Dialog(d) => d.pinned,
tl::enums::Dialog::Folder(f) => f.pinned,
};
pinned as jint
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_freeDialogs(
_: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
) {
let _ = Box::from_raw(dialogs_ptr as *mut Vec<Dialog>);
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_sendMessage(
env: JNIEnv,
_: JObject,
packed: JString,
text: JString,
) {
let packed = CString::from(CStr::from_ptr(env.get_string(packed).unwrap().as_ptr()));
let text = CString::from(CStr::from_ptr(env.get_string(text).unwrap().as_ptr()));
let packed = PackedChat::from_hex(packed.to_str().unwrap()).unwrap();
match block_on(send_message(packed, text.to_str().unwrap())) {
Ok(_) => info!("Message sent"),
Err(e) => error!("Failed to send message: {}", e),
}
} }

43
native/src/talaria.udl Normal file
View File

@ -0,0 +1,43 @@
[Error]
enum NativeError {
"Initialization",
"Database",
"Network",
};
enum MessageAck {
"Received",
"Seen",
"Sent",
};
dictionary MessagePreview {
string sender;
string text;
timestamp date;
MessageAck ack;
};
dictionary Dialog {
string id;
string title;
MessagePreview? last_message;
boolean pinned;
};
namespace talaria {
[Throws=NativeError]
void init_database(string path);
[Throws=NativeError]
void init_client();
[Throws=NativeError]
boolean need_login();
[Throws=NativeError]
u64 request_login_code(string phone);
[Throws=NativeError]
void sign_in(u64 tokenPtr, string code);
[Throws=NativeError]
sequence<Dialog> get_dialogs();
[Throws=NativeError]
void send_message(string packed, string text);
};