From 476f80cb5ef98bf500369eb93e452d4f646b8ab3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 12 Oct 2022 21:26:51 +0200 Subject: [PATCH] Integrate with Telegram's API --- app/src/main/AndroidManifest.xml | 4 +- .../java/dev/lonami/talaria/MainActivity.kt | 3 + .../java/dev/lonami/talaria/TalariaApp.kt | 10 +- .../dev/lonami/talaria/bindings/Native.kt | 18 ++ .../dev/lonami/talaria/data/DialogSource.kt | 29 ++- .../java/dev/lonami/talaria/ui/ChatScreen.kt | 6 +- .../dev/lonami/talaria/ui/ChatViewModel.kt | 4 +- .../dev/lonami/talaria/ui/DialogScreen.kt | 9 +- .../java/dev/lonami/talaria/ui/LoginScreen.kt | 14 +- native/Cargo.toml | 8 + native/src/lib.rs | 236 +++++++++++++++++- 11 files changed, 307 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/dev/lonami/talaria/bindings/Native.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5d80ed5..97dd757 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + - \ No newline at end of file + diff --git a/app/src/main/java/dev/lonami/talaria/MainActivity.kt b/app/src/main/java/dev/lonami/talaria/MainActivity.kt index 0bea9d9..53f8b58 100644 --- a/app/src/main/java/dev/lonami/talaria/MainActivity.kt +++ b/app/src/main/java/dev/lonami/talaria/MainActivity.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.ui.Modifier +import dev.lonami.talaria.bindings.Native import dev.lonami.talaria.ui.theme.TalariaTheme class MainActivity : ComponentActivity() { @@ -23,5 +24,7 @@ class MainActivity : ComponentActivity() { } } } + + Native.initClient() } } diff --git a/app/src/main/java/dev/lonami/talaria/TalariaApp.kt b/app/src/main/java/dev/lonami/talaria/TalariaApp.kt index 262a63d..4952b7f 100644 --- a/app/src/main/java/dev/lonami/talaria/TalariaApp.kt +++ b/app/src/main/java/dev/lonami/talaria/TalariaApp.kt @@ -5,8 +5,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.navigation.compose.NavHost @@ -47,6 +46,8 @@ fun TalariaApp() { val currentScreen = TalariaScreen.valueOf(backStackEntry?.destination?.route ?: TalariaScreen.Login.name) + var selectedDialog by remember { mutableStateOf("") } + Scaffold( topBar = { TalariaAppBar( @@ -58,16 +59,17 @@ fun TalariaApp() { ) { innerPadding -> NavHost( navController = navController, - startDestination = TalariaScreen.Dialog.name, + startDestination = TalariaScreen.Login.name, Modifier.padding(innerPadding) ) { composable(route = TalariaScreen.Dialog.name) { DialogScreen(onDialogSelected = { + selectedDialog = it navController.navigate(TalariaScreen.Chat.name) }) } composable(route = TalariaScreen.Chat.name) { - ChatScreen() + ChatScreen(selectedDialog) } composable(route = TalariaScreen.Login.name) { LoginScreen(onConfirmOtp = { diff --git a/app/src/main/java/dev/lonami/talaria/bindings/Native.kt b/app/src/main/java/dev/lonami/talaria/bindings/Native.kt new file mode 100644 index 0000000..607fdaf --- /dev/null +++ b/app/src/main/java/dev/lonami/talaria/bindings/Native.kt @@ -0,0 +1,18 @@ +package dev.lonami.talaria.bindings + +object Native { + init { + System.loadLibrary("talaria") + } + + 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 freeDialogs(dialogsPtr: Long) + external fun sendMessage(packed: String, text: String) +} diff --git a/app/src/main/java/dev/lonami/talaria/data/DialogSource.kt b/app/src/main/java/dev/lonami/talaria/data/DialogSource.kt index ed513c2..beb5e5f 100644 --- a/app/src/main/java/dev/lonami/talaria/data/DialogSource.kt +++ b/app/src/main/java/dev/lonami/talaria/data/DialogSource.kt @@ -1,20 +1,25 @@ package dev.lonami.talaria.data +import dev.lonami.talaria.bindings.Native import dev.lonami.talaria.model.Dialog object DialogSource { fun loadDialogs(): List { - return listOf( - Dialog("Saved Messages", "Secret launch-code: banana", pinned = true), - Dialog("First Sample Dialog", "Photo", pinned = false), - Dialog("Second Sample Dialog", "Video", pinned = false), - Dialog("Third Sample Dialog", "Audio", pinned = false), - Dialog("Fourth Sample Dialog", "Sticker (just kidding who uses that)", pinned = false), - Dialog("Fifth Sample Dialog", "Photo", pinned = false), - Dialog("Sixth Sample Dialog", "Video", pinned = false), - Dialog("Seventh Sample Dialog", "Audio", pinned = false), - Dialog("Eighth Sample Dialog", "Sticker (just kidding who uses that)", pinned = false), - Dialog("Ninth Sample Dialog", "Hello, scroll!", pinned = false), - ) + val dialogs = mutableListOf() + + val dialogPtr = Native.getDialogs() + val dialogCount = Native.dialogCount(dialogPtr) + for (i in 0 until dialogCount) { + dialogs.add( + Dialog( + Native.dialogTitle(dialogPtr, i), + Native.dialogPacked(dialogPtr, i), + false + ) + ) + } + Native.freeDialogs(dialogPtr) + + return dialogs } } diff --git a/app/src/main/java/dev/lonami/talaria/ui/ChatScreen.kt b/app/src/main/java/dev/lonami/talaria/ui/ChatScreen.kt index ea213d2..7fbbe26 100644 --- a/app/src/main/java/dev/lonami/talaria/ui/ChatScreen.kt +++ b/app/src/main/java/dev/lonami/talaria/ui/ChatScreen.kt @@ -77,7 +77,7 @@ fun MessageInputField( } @Composable -fun ChatScreen(chatViewModel: ChatViewModel = viewModel()) { +fun ChatScreen(selectedDialog: String, chatViewModel: ChatViewModel = viewModel()) { val chatUiState by chatViewModel.uiState.collectAsState() var messageText by remember { mutableStateOf("") } val messageListState = rememberLazyListState() @@ -92,7 +92,7 @@ fun ChatScreen(chatViewModel: ChatViewModel = viewModel()) { MessageInputField(messageText, onMessageChanged = { messageText = it }, onSendMessage = { - chatViewModel.sendMessage(messageText) + chatViewModel.sendMessage(selectedDialog, messageText) messageText = "" coroutineScope.launch { messageListState.animateScrollToItem(chatUiState.messages.size - 1) @@ -105,6 +105,6 @@ fun ChatScreen(chatViewModel: ChatViewModel = viewModel()) { @Composable fun ChatPreview() { TalariaTheme { - ChatScreen() + ChatScreen("") } } diff --git a/app/src/main/java/dev/lonami/talaria/ui/ChatViewModel.kt b/app/src/main/java/dev/lonami/talaria/ui/ChatViewModel.kt index d8057fd..d06ce4f 100644 --- a/app/src/main/java/dev/lonami/talaria/ui/ChatViewModel.kt +++ b/app/src/main/java/dev/lonami/talaria/ui/ChatViewModel.kt @@ -1,6 +1,7 @@ package dev.lonami.talaria.ui import androidx.lifecycle.ViewModel +import dev.lonami.talaria.bindings.Native import dev.lonami.talaria.data.MessageSource import dev.lonami.talaria.model.Message import kotlinx.coroutines.flow.MutableStateFlow @@ -16,7 +17,8 @@ class ChatViewModel : ViewModel() { _uiState.value = ChatUiState(MessageSource.loadMessages().toMutableList()) } - fun sendMessage(message: String) { + fun sendMessage(dialog: String, message: String) { + Native.sendMessage(dialog, message) _uiState.update { state -> state.messages.add(Message("You", message)) state diff --git a/app/src/main/java/dev/lonami/talaria/ui/DialogScreen.kt b/app/src/main/java/dev/lonami/talaria/ui/DialogScreen.kt index 7b56ffc..116b672 100644 --- a/app/src/main/java/dev/lonami/talaria/ui/DialogScreen.kt +++ b/app/src/main/java/dev/lonami/talaria/ui/DialogScreen.kt @@ -47,18 +47,21 @@ fun DialogCard(dialog: Dialog, onDialogSelected: () -> Unit) { } @Composable -fun DialogList(dialogs: List, onDialogSelected: (Int) -> Unit) { +fun DialogList(dialogs: List, onDialogSelected: (String) -> Unit) { LazyColumn { items(dialogs.size) { DialogCard(dialogs[it], onDialogSelected = { - onDialogSelected(it) + onDialogSelected(dialogs[it].lastMessage) }) } } } @Composable -fun DialogScreen(onDialogSelected: (Int) -> Unit, dialogViewModel: DialogViewModel = viewModel()) { +fun DialogScreen( + onDialogSelected: (String) -> Unit, + dialogViewModel: DialogViewModel = viewModel() +) { val dialogUiState by dialogViewModel.uiState.collectAsState() DialogList(dialogUiState.dialogs, onDialogSelected = onDialogSelected) } diff --git a/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt b/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt index 9161f7d..26a1497 100644 --- a/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt +++ b/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.lonami.talaria.R +import dev.lonami.talaria.bindings.Native import dev.lonami.talaria.ui.theme.TalariaTheme enum class LoginStage { @@ -90,6 +91,8 @@ fun LoginScreen(onConfirmOtp: () -> Unit) { var phone by remember { mutableStateOf("") } var otp by remember { mutableStateOf("") } + var tokenPtr by remember { mutableStateOf(0L) } + Column( modifier = Modifier .fillMaxSize() @@ -109,11 +112,18 @@ fun LoginScreen(onConfirmOtp: () -> Unit) { LoginStage.ASK_PHONE -> PhoneInput( phone, onPhoneChanged = { phone = it }, - onSendCode = { stage = LoginStage.ASK_CODE }) + onSendCode = { + tokenPtr = Native.requestLoginCode(phone) + stage = LoginStage.ASK_CODE + } + ) LoginStage.ASK_CODE -> OtpInput( otp, onOtpChanged = { otp = it }, - onConfirmOtp = onConfirmOtp + onConfirmOtp = { + Native.signIn(tokenPtr, otp) + onConfirmOtp() + } ) } } diff --git a/native/Cargo.toml b/native/Cargo.toml index caad8ab..9741819 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -9,6 +9,14 @@ crate-type = ["cdylib"] [dependencies] jni = { version = "0.10.2", default-features = false } +# v0.4 of grammers-* is currently unreleased; clone the project and use path dependencies +grammers-client = { version = "0.4.0" } +grammers-tl-types = { version = "0.4.0" } +grammers-session = { version = "0.4.0" } +tokio = { version = "1.5.0", features = ["full"] } +log = "0.4.14" +android_logger = "0.11.1" +once_cell = "1.15.0" [profile.release] lto = true diff --git a/native/src/lib.rs b/native/src/lib.rs index f2636e7..249f743 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -2,19 +2,239 @@ #![allow(non_snake_case)] use std::ffi::{CStr, CString}; +use std::future::Future; +use grammers_client::{Client, Config}; +use grammers_client::types::{Dialog, LoginToken}; +use grammers_session::{PackedChat, Session}; use jni::JNIEnv; use jni::objects::{JObject, JString}; -use jni::sys::jstring; +use jni::sys::{jboolean, jint, jlong, jstring}; +use log; +use log::{error, info, Level}; +use once_cell::sync::OnceCell; +use tokio::runtime; +use tokio::runtime::Runtime; -#[no_mangle] -pub unsafe extern fn Java_dev_lonami_talaria_MainActivity_hello(env: JNIEnv, _: JObject, j_recipient: JString) -> jstring { - let recipient = CString::from( - CStr::from_ptr( - env.get_string(j_recipient).unwrap().as_ptr() - ) +const LOG_MIN_LEVEL: Level = Level::Trace; +const LOG_TAG: &str = ".native.talari"; +const API_ID: i32 = 0; +const API_HASH: &str = ""; + +type Result = std::result::Result>; + +static RUNTIME: OnceCell = OnceCell::new(); +static CLIENT: OnceCell = OnceCell::new(); + +fn block_on(future: F) -> F::Output { + if RUNTIME.get().is_none() { + RUNTIME + .set( + runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(), + ) + .ok(); + } + + RUNTIME.get().unwrap().block_on(future) +} + +async fn init_client() -> Result<()> { + android_logger::init_once( + android_logger::Config::default() + .with_min_level(LOG_MIN_LEVEL) + .with_tag(LOG_TAG), ); - let output = env.new_string("Hello ".to_owned() + recipient.to_str().unwrap()).unwrap(); + if CLIENT.get().is_some() { + info!("Client is already initialized"); + return Ok(()); + } + + info!("Connecting to Telegram..."); + + let client = Client::connect(Config { + session: Session::new(), + api_id: API_ID, + api_hash: API_HASH.to_string(), + params: Default::default(), + }) + .await?; + + info!("Connected!"); + + CLIENT + .set(client) + .map_err(|_| "Client was already initialized")?; + + Ok(()) +} + +async fn need_login() -> Result { + let client = CLIENT.get().ok_or("Client not initialized")?; + Ok(client.is_authorized().await?) +} + +async fn request_login_code(phone: &str) -> Result { + let client = CLIENT.get().ok_or("Client not initialized")?; + let token = client.request_login_code(&phone, API_ID, API_HASH).await?; + Ok(token) +} + +async fn sign_in(token: LoginToken, code: &str) -> Result<()> { + let client = CLIENT.get().ok_or("Client not initialized")?; + client.sign_in(&token, &code).await?; + Ok(()) +} + +async fn get_dialogs() -> Result> { + let client = CLIENT.get().ok_or("Client not initialized")?; + + let mut result = Vec::new(); + let mut dialogs = client.iter_dialogs(); + while let Some(dialog) = dialogs.next().await? { + 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_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() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_requestLoginCode( + env: JNIEnv, + _: JObject, + phone: JString, +) -> jlong { + let phone = CString::from(CStr::from_ptr(env.get_string(phone).unwrap().as_ptr())); + + 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 = Box::leak(Box::from_raw(dialogs_ptr as *mut Vec)); + 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 = Box::leak(Box::from_raw(dialogs_ptr as *mut Vec)); + + 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 = Box::leak(Box::from_raw(dialogs_ptr as *mut Vec)); + + 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_freeDialogs( + _: JNIEnv, + _: JObject, + dialogs_ptr: jlong, +) { + let _ = Box::from_raw(dialogs_ptr as *mut Vec); +} + +#[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), + } +}