Compare commits

..

No commits in common. "912949a07943a1d43bf942e11ba64cf3dd256c1f" and "b5dddcef0203f185e3d95998a5dc3f594de9d7fc" have entirely different histories.

18 changed files with 404 additions and 235 deletions

View File

@ -1,7 +1,6 @@
<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,7 +57,6 @@ 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'
@ -69,7 +68,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 = "uniffi_talaria" libname = "talaria"
targets = ["arm64"] targets = ["arm64"]
profile = 'release' profile = 'release'
} }
@ -79,14 +78,3 @@ 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,9 +7,8 @@ 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?) {
@ -25,7 +24,7 @@ class MainActivity : ComponentActivity() {
} }
} }
initDatabase(getDatabasePath("talaria.db").path) Native.initDatabase(getDatabasePath("talaria.db").path)
initClient() Native.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(!needLogin()) } val loggedIn by remember { mutableStateOf(!Native.needLogin()) }
var selectedDialog by remember { mutableStateOf("") } var selectedDialog by remember { mutableStateOf("") }
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)

View File

@ -0,0 +1,24 @@
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,10 +1,11 @@
package dev.lonami.talaria.data package dev.lonami.talaria.data
import uniffi.talaria.Dialog import dev.lonami.talaria.bindings.Native
import uniffi.talaria.MessageAck import dev.lonami.talaria.models.Dialog
import uniffi.talaria.MessagePreview import dev.lonami.talaria.models.MessageAck
import uniffi.talaria.getDialogs import dev.lonami.talaria.models.MessagePreview
import java.time.Instant import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
interface DialogRepository { interface DialogRepository {
fun loadDialogs(): List<Dialog> fun loadDialogs(): List<Dialog>
@ -12,7 +13,39 @@ interface DialogRepository {
class NativeDialogRepository : DialogRepository { class NativeDialogRepository : DialogRepository {
override fun loadDialogs(): List<Dialog> { override fun loadDialogs(): List<Dialog> {
return getDialogs() val dialogs = mutableListOf<Dialog>()
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
} }
} }
@ -38,7 +71,7 @@ class MockDialogRepository : DialogRepository {
} else { } else {
"Sample Message $i" "Sample Message $i"
}, },
date = Instant.now(), date = LocalDateTime.now(),
ack = when (i % 3) { ack = when (i % 3) {
0 -> MessageAck.RECEIVED 0 -> MessageAck.RECEIVED
1 -> MessageAck.SENT 1 -> MessageAck.SENT

View File

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

View File

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

View File

@ -0,0 +1,10 @@
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,12 +23,10 @@ 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
@ -56,8 +54,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,
@ -67,7 +65,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),
@ -80,7 +78,7 @@ fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit, modifier: Modifier = Mo
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
LocalDateTime.ofInstant(dialog.lastMessage!!.date, ZoneOffset.UTC).format( dialog.lastMessage.date.format(
DateTimeFormatter.ofLocalizedTime( DateTimeFormatter.ofLocalizedTime(
FormatStyle.SHORT FormatStyle.SHORT
) )

View File

@ -18,9 +18,8 @@ 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,
@ -106,7 +105,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(0UL) } var tokenPtr by remember { mutableStateOf(0L) }
Column( Column(
modifier = modifier modifier = modifier
@ -128,7 +127,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
phone, phone,
onPhoneChanged = { phone = it }, onPhoneChanged = { phone = it },
onSendCode = { onSendCode = {
tokenPtr = requestLoginCode(phone) tokenPtr = Native.requestLoginCode(phone)
stage = LoginStage.ASK_CODE stage = LoginStage.ASK_CODE
} }
) )
@ -136,7 +135,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
otp, otp,
onOtpChanged = { otp = it }, onOtpChanged = { otp = it },
onConfirmOtp = { onConfirmOtp = {
signIn(tokenPtr, otp) Native.signIn(tokenPtr, otp)
onConfirmOtp() onConfirmOtp()
} }
) )

View File

@ -1,6 +1,7 @@
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
@ -17,7 +18,7 @@ class ChatViewModel : ViewModel() {
} }
fun sendMessage(dialog: String, message: String) { fun sendMessage(dialog: String, message: String) {
sendMessage(dialog, message) Native.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 uniffi.talaria.Dialog import dev.lonami.talaria.models.Dialog
data class DialogUiState(val dialogs: List<Dialog> = listOf()) data class DialogUiState(val dialogs: List<Dialog> = listOf())

View File

@ -4,10 +4,8 @@ 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 {
// uniffi-recommended Gradle script fails with Android plugin 7.3.1. id 'com.android.application' version '7.3.0' apply false
// See https://github.com/mozilla/uniffi-rs/issues/1386 for details. id 'com.android.library' version '7.3.0' apply false
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,12 +4,11 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[lib] [lib]
name = "uniffi_talaria" name = "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" }
@ -20,8 +19,5 @@ 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

View File

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

View File

@ -3,24 +3,26 @@
mod db; mod db;
use grammers_client::types::LoginToken; use grammers_client::types::{Dialog, 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::fmt; use std::ffi::{CStr, CString};
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;
include!(concat!(env!("OUT_DIR"), "/talaria.uniffi.rs")); type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
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";
@ -45,50 +47,6 @@ 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
@ -104,22 +62,7 @@ fn block_on<F: Future>(future: F) -> F::Output {
RUNTIME.get().unwrap().block_on(future) RUNTIME.get().unwrap().block_on(future)
} }
pub fn init_database(path: String) -> Result<()> { async fn init_client() -> 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)
@ -135,8 +78,7 @@ pub fn init_client() -> Result<()> {
let conn = match guard.as_ref() { let conn = match guard.as_ref() {
Some(c) => c, Some(c) => c,
None => { None => {
error!("Database was not initialized"); return Err("Database was not initialized".into());
return Err(NativeError::Initialization);
} }
}; };
@ -144,7 +86,7 @@ pub fn init_client() -> Result<()> {
let session = Session::new(); let session = Session::new();
let sessions = db::get_sessions(conn).map_err(|_| NativeError::Database)?; let sessions = db::get_sessions(conn)?;
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),
@ -170,7 +112,7 @@ pub fn init_client() -> Result<()> {
} }
} }
let client = block_on(Client::connect(Config { let client = 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(),
@ -182,46 +124,42 @@ pub fn init_client() -> Result<()> {
}, },
..Default::default() ..Default::default()
}, },
})) })
.map_err(|_| NativeError::Network)?; .await?;
info!("Connected!"); info!("Connected!");
CLIENT CLIENT
.set(client) .set(client)
.map_err(|_| NativeError::Initialization)?; .map_err(|_| "Client was already initialized")?;
Ok(()) Ok(())
} }
pub fn need_login() -> Result<bool> { async fn need_login() -> Result<bool> {
let client = CLIENT.get().ok_or(NativeError::Initialization)?; let client = CLIENT.get().ok_or("Client not initialized")?;
block_on(client.is_authorized()).map_err(|_| NativeError::Network) Ok(client.is_authorized().await?)
} }
pub fn request_login_code(phone: String) -> Result<u64> { async fn request_login_code(phone: &str) -> Result<LoginToken> {
let client = CLIENT.get().ok_or(NativeError::Initialization)?; let client = CLIENT.get().ok_or("Client not initialized")?;
block_on(client.request_login_code(&phone, API_ID, API_HASH)) let token = client.request_login_code(&phone, API_ID, API_HASH).await?;
.map(|token| Box::into_raw(Box::new(token)) as u64) Ok(token)
.map_err(|_| NativeError::Network)
} }
pub fn sign_in(token_ptr: u64, code: String) -> Result<()> { async fn sign_in(token: LoginToken, code: &str) -> Result<()> {
let token = unsafe { *Box::from_raw(token_ptr as *mut LoginToken) }; let client = CLIENT.get().ok_or("Client not initialized")?;
let client = CLIENT.get().ok_or(NativeError::Initialization)?; client.sign_in(&token, &code).await?;
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 => {
error!("Database was not initialized"); return Err("Database was not initialized".into());
return Err(NativeError::Initialization);
} }
}; };
let mut session = db::create_session(conn).map_err(|_| NativeError::Database)?; let mut session = db::create_session(conn)?;
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);
@ -251,63 +189,280 @@ pub fn sign_in(token_ptr: u64, code: String) -> Result<()> {
} }
} }
db::update_session(conn, &session).map_err(|_| NativeError::Database)?; db::update_session(conn, &session)?;
Ok(()) Ok(())
} }
pub fn get_dialogs() -> Result<Vec<Dialog>> { async fn get_dialogs() -> Result<Vec<Dialog>> {
let client = CLIENT.get().ok_or(NativeError::Initialization)?; let client = CLIENT.get().ok_or("Client not initialized")?;
block_on(async {
let mut result = Vec::new(); let mut result = Vec::new();
let mut dialogs = client.iter_dialogs(); let mut dialogs = client.iter_dialogs();
while let Some(dialog) = dialogs.next().await.map_err(|_| NativeError::Network)? { while let Some(dialog) = dialogs.next().await? {
result.push(dialog); result.push(dialog);
} }
Ok(result) Ok(result)
}) }
.map(|dialogs| {
dialogs async fn send_message(chat: PackedChat, text: &str) -> Result<()> {
.into_iter() let client = CLIENT.get().ok_or("Client not initialized")?;
.map(|d| Dialog { client.send_message(chat, text).await?;
id: d.chat().pack().to_hex(), Ok(())
title: d.chat().name().to_string(), }
last_message: d.last_message.map(|m| MessagePreview {
sender: if let Some(sender) = m.sender() { #[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()
}
}
}
#[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 = &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() sender.name().to_string()
} else { } else {
"unknown".to_string() "unknown".to_string()
},
text: m.text().to_string(),
date: m.date().into(),
ack: if m.outgoing() {
match &d.dialog {
tl::enums::Dialog::Dialog(d) => {
if m.id() <= d.read_inbox_max_id {
MessageAck::Seen
} else {
MessageAck::Sent
}
}
tl::enums::Dialog::Folder(_) => MessageAck::Received,
} }
} else { } else {
MessageAck::Received String::new()
}, };
}), let output = env.new_string(sender).unwrap();
pinned: match d.dialog { output.into_inner()
tl::enums::Dialog::Dialog(d) => d.pinned,
tl::enums::Dialog::Folder(f) => f.pinned,
},
})
.collect()
})
} }
pub fn send_message(packed: String, text: String) -> Result<()> { #[no_mangle]
let chat = PackedChat::from_hex(&packed).unwrap(); pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogText(
let client = CLIENT.get().ok_or(NativeError::Initialization)?; env: JNIEnv,
block_on(client.send_message(chat, text)).map_err(|_| NativeError::Network)?; _: JObject,
Ok(()) 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 {
1
}
}
tl::enums::Dialog::Folder(_) => 0,
}
} else {
0
}
} else {
0
};
ack
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogPin(
_: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
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),
}
} }

View File

@ -1,43 +0,0 @@
[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);
};