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">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">

View File

@ -57,7 +57,6 @@ dependencies {
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation 'androidx.compose.material:material:1.2.1'
implementation "androidx.navigation:navigation-compose:2.5.2"
implementation "net.java.dev.jna:jna:5.12.0@aar"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
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
cargo {
module = "../native"
libname = "uniffi_talaria"
libname = "talaria"
targets = ["arm64"]
profile = 'release'
}
@ -79,14 +78,3 @@ tasks.whenTaskAdded { task ->
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.Surface
import androidx.compose.ui.Modifier
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.ui.theme.TalariaTheme
import uniffi.talaria.initClient
import uniffi.talaria.initDatabase
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -25,7 +24,7 @@ class MainActivity : ComponentActivity() {
}
}
initDatabase(getDatabasePath("talaria.db").path)
initClient()
Native.initDatabase(getDatabasePath("talaria.db").path)
Native.initClient()
}
}

View File

@ -18,11 +18,11 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.ui.screens.ChatScreen
import dev.lonami.talaria.ui.screens.DialogScreen
import dev.lonami.talaria.ui.screens.LoginScreen
import kotlinx.coroutines.launch
import uniffi.talaria.needLogin
enum class TalariaScreen(@StringRes val title: Int) {
Login(title = R.string.app_name),
@ -148,7 +148,7 @@ fun TalariaApp(modifier: Modifier = Modifier) {
val currentScreen =
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("") }
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
import uniffi.talaria.Dialog
import uniffi.talaria.MessageAck
import uniffi.talaria.MessagePreview
import uniffi.talaria.getDialogs
import java.time.Instant
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.models.Dialog
import dev.lonami.talaria.models.MessageAck
import dev.lonami.talaria.models.MessagePreview
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
interface DialogRepository {
fun loadDialogs(): List<Dialog>
@ -12,7 +13,39 @@ interface DialogRepository {
class NativeDialogRepository : DialogRepository {
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 {
"Sample Message $i"
},
date = Instant.now(),
date = LocalDateTime.now(),
ack = when (i % 3) {
0 -> MessageAck.RECEIVED
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 dev.lonami.talaria.R
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.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.FormatStyle
@ -56,8 +54,8 @@ fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit, modifier: Modifier = Mo
Text(
stringResource(
R.string.message_preview,
dialog.lastMessage!!.sender,
dialog.lastMessage!!.text
dialog.lastMessage.sender,
dialog.lastMessage.text
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -67,7 +65,7 @@ fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit, modifier: Modifier = Mo
Column {
if (dialog.lastMessage != null) {
Row {
when (dialog.lastMessage!!.ack) {
when (dialog.lastMessage.ack) {
MessageAck.RECEIVED -> {}
MessageAck.SENT -> Icon(
painterResource(R.drawable.sent),
@ -80,7 +78,7 @@ fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit, modifier: Modifier = Mo
}
Spacer(Modifier.width(8.dp))
Text(
LocalDateTime.ofInstant(dialog.lastMessage!!.date, ZoneOffset.UTC).format(
dialog.lastMessage.date.format(
DateTimeFormatter.ofLocalizedTime(
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.sp
import dev.lonami.talaria.R
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.ui.theme.TalariaTheme
import uniffi.talaria.requestLoginCode
import uniffi.talaria.signIn
enum class LoginStage {
ASK_PHONE,
@ -106,7 +105,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
var phone by remember { mutableStateOf("") }
var otp by remember { mutableStateOf("") }
var tokenPtr by remember { mutableStateOf(0UL) }
var tokenPtr by remember { mutableStateOf(0L) }
Column(
modifier = modifier
@ -128,7 +127,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
phone,
onPhoneChanged = { phone = it },
onSendCode = {
tokenPtr = requestLoginCode(phone)
tokenPtr = Native.requestLoginCode(phone)
stage = LoginStage.ASK_CODE
}
)
@ -136,7 +135,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
otp,
onOtpChanged = { otp = it },
onConfirmOtp = {
signIn(tokenPtr, otp)
Native.signIn(tokenPtr, otp)
onConfirmOtp()
}
)

View File

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

View File

@ -1,5 +1,5 @@
package dev.lonami.talaria.ui.state
import uniffi.talaria.Dialog
import dev.lonami.talaria.models.Dialog
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.
plugins {
// uniffi-recommended Gradle script fails with Android plugin 7.3.1.
// 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 'com.android.application' version '7.3.0' apply false
id 'com.android.library' version '7.3.0' 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"
}

View File

@ -4,12 +4,11 @@ version = "0.1.0"
edition = "2021"
[lib]
name = "uniffi_talaria"
name = "talaria"
crate-type = ["cdylib"]
[dependencies]
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
grammers-client = { version = "0.4.1" }
grammers-tl-types = { version = "0.4.0" }
@ -20,8 +19,5 @@ android_logger = "0.11.1"
once_cell = "1.15.0"
sqlite = "0.27.0"
[build-dependencies]
uniffi_build = "0.21.0"
[profile.release]
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;
use grammers_client::types::LoginToken;
use grammers_client::types::{Dialog, LoginToken};
use grammers_client::{Client, Config, InitParams};
use grammers_session::{PackedChat, Session, UpdateState};
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::{error, info, Level};
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::fmt;
use std::ffi::{CStr, CString};
use std::future::Future;
use std::net::SocketAddr;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::Mutex;
use std::time::SystemTime;
use tokio::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_TAG: &str = ".native.talari";
@ -45,50 +47,6 @@ static RUNTIME: OnceCell<Runtime> = OnceCell::new();
static CLIENT: OnceCell<Client> = OnceCell::new();
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 {
if RUNTIME.get().is_none() {
RUNTIME
@ -104,22 +62,7 @@ fn block_on<F: Future>(future: F) -> F::Output {
RUNTIME.get().unwrap().block_on(future)
}
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<()> {
async fn init_client() -> Result<()> {
android_logger::init_once(
android_logger::Config::default()
.with_min_level(LOG_MIN_LEVEL)
@ -135,8 +78,7 @@ pub fn init_client() -> Result<()> {
let conn = match guard.as_ref() {
Some(c) => c,
None => {
error!("Database was not initialized");
return Err(NativeError::Initialization);
return Err("Database was not initialized".into());
}
};
@ -144,7 +86,7 @@ pub fn init_client() -> Result<()> {
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) {
match (s.user_id, s.dc_id, s.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,
api_id: API_ID,
api_hash: API_HASH.to_string(),
@ -182,46 +124,42 @@ pub fn init_client() -> Result<()> {
},
..Default::default()
},
}))
.map_err(|_| NativeError::Network)?;
})
.await?;
info!("Connected!");
CLIENT
.set(client)
.map_err(|_| NativeError::Initialization)?;
.map_err(|_| "Client was already initialized")?;
Ok(())
}
pub fn need_login() -> Result<bool> {
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
block_on(client.is_authorized()).map_err(|_| NativeError::Network)
async fn need_login() -> Result<bool> {
let client = CLIENT.get().ok_or("Client not initialized")?;
Ok(client.is_authorized().await?)
}
pub fn request_login_code(phone: String) -> Result<u64> {
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
block_on(client.request_login_code(&phone, API_ID, API_HASH))
.map(|token| Box::into_raw(Box::new(token)) as u64)
.map_err(|_| NativeError::Network)
async fn request_login_code(phone: &str) -> Result<LoginToken> {
let client = CLIENT.get().ok_or("Client not initialized")?;
let token = client.request_login_code(&phone, API_ID, API_HASH).await?;
Ok(token)
}
pub fn sign_in(token_ptr: u64, code: String) -> Result<()> {
let token = unsafe { *Box::from_raw(token_ptr as *mut LoginToken) };
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
block_on(client.sign_in(&token, &code)).map_err(|_| NativeError::Network)?;
async fn sign_in(token: LoginToken, code: &str) -> Result<()> {
let client = CLIENT.get().ok_or("Client not initialized")?;
client.sign_in(&token, &code).await?;
let guard = DATABASE.lock().unwrap();
let conn = match guard.as_ref() {
Some(c) => c,
None => {
error!("Database was not initialized");
return Err(NativeError::Initialization);
return Err("Database was not initialized".into());
}
};
let mut session = db::create_session(conn).map_err(|_| NativeError::Database)?;
let mut session = db::create_session(conn)?;
let s = client.session();
if let Some(user) = s.get_user() {
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(())
}
pub fn get_dialogs() -> Result<Vec<Dialog>> {
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
async fn get_dialogs() -> Result<Vec<Dialog>> {
let client = CLIENT.get().ok_or("Client not initialized")?;
block_on(async {
let mut result = Vec::new();
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);
}
Ok(result)
})
.map(|dialogs| {
dialogs
.into_iter()
.map(|d| Dialog {
id: d.chat().pack().to_hex(),
title: d.chat().name().to_string(),
last_message: d.last_message.map(|m| MessagePreview {
sender: if let Some(sender) = m.sender() {
}
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()
}
}
}
#[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()
} else {
"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 {
MessageAck::Received
},
}),
pinned: match d.dialog {
tl::enums::Dialog::Dialog(d) => d.pinned,
tl::enums::Dialog::Folder(f) => f.pinned,
},
})
.collect()
})
String::new()
};
let output = env.new_string(sender).unwrap();
output.into_inner()
}
pub fn send_message(packed: String, text: String) -> Result<()> {
let chat = PackedChat::from_hex(&packed).unwrap();
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
block_on(client.send_message(chat, text)).map_err(|_| NativeError::Network)?;
Ok(())
#[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 {
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);
};