#![cfg(target_os = "android")] #![allow(non_snake_case)] mod db; 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 log; use log::{error, info, Level}; use once_cell::sync::OnceCell; use std::collections::HashMap; use std::future::Future; use std::net::SocketAddr; use std::net::{Ipv4Addr, Ipv6Addr}; use std::sync::Mutex; use tokio::runtime; use tokio::runtime::Runtime; include!(concat!(env!("OUT_DIR"), "/talaria.uniffi.rs")); type Result = std::result::Result>; const LOG_MIN_LEVEL: Level = Level::Trace; const LOG_TAG: &str = ".native.talari"; const API_ID: i32 = { let mut index = 0; let mut value = 0; let api_id = env!("TALARIA_API_ID"); let bytes = api_id.as_bytes(); while index < bytes.len() { match bytes[index] { b @ b'0'..=b'9' => value = value * 10 + (b - b'0') as i32, _ => panic!("non-digit character found in API ID"), } index += 1 } value }; const API_HASH: &str = env!("TALARIA_API_HASH"); const SERVER_ADDR: &str = env!("TALARIA_SERVER_ADDR"); static RUNTIME: OnceCell = OnceCell::new(); static CLIENT: OnceCell = OnceCell::new(); static DATABASE: Mutex> = Mutex::new(None); 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), ); if CLIENT.get().is_some() { info!("Client is already initialized"); return Ok(()); } let guard = DATABASE.lock().unwrap(); let conn = match guard.as_ref() { Some(c) => c, None => { return Err("Database was not initialized".into()); } }; info!("Connecting to Telegram..."); let session = Session::new(); 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), _ => {} } match (s.pts, s.qts, s.seq, s.date) { (Some(pts), Some(qts), Some(seq), Some(date)) => session.set_state(UpdateState { pts, qts, seq, date, channels: HashMap::new(), }), _ => {} } match (s.dc_id, s.dc_addr.as_ref(), s.dc_port, s.dc_auth) { (Some(id), Some(addr), Some(port), Some(auth)) => { session.insert_dc(id, SocketAddr::new(addr.parse().unwrap(), port), auth) } _ => {} } } let client = Client::connect(Config { session, api_id: API_ID, api_hash: API_HASH.to_string(), params: InitParams { server_addr: if SERVER_ADDR.is_empty() { None } else { Some(SERVER_ADDR.parse().unwrap()) }, ..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?; let guard = DATABASE.lock().unwrap(); let conn = match guard.as_ref() { Some(c) => c, None => { return Err("Database was not initialized".into()); } }; let mut session = db::create_session(conn)?; let s = client.session(); if let Some(user) = s.get_user() { session.user_id = Some(user.id); session.dc_id = Some(user.dc); session.bot = Some(user.bot); } if let Some(state) = s.get_state() { session.pts = Some(state.pts); session.qts = Some(state.qts); session.seq = Some(state.seq); session.date = Some(state.date); } if let Some(dc_id) = session.dc_id { for dc in s.get_dcs() { if dc.id == dc_id { if let Some(ipv4) = dc.ipv4 { session.dc_addr = Some(Ipv4Addr::from(ipv4.to_le_bytes()).to_string()) } else if let Some(ipv6) = dc.ipv6 { session.dc_addr = Some(Ipv6Addr::from(ipv6).to_string()) } session.dc_port = Some(dc.port as u16); session.dc_auth = dc.auth.map(|b| b.try_into().unwrap()); break; } } } db::update_session(conn, &session)?; 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(()) } pub fn initDatabase(path: String) { 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), Err(e) => error!("Failed to initialize database: {}", e), } } pub fn initClient() { match block_on(init_client()) { Ok(_) => info!("Client initialized"), Err(e) => error!("Failed to initialize client: {}", e), } } pub fn needLogin() -> bool { match block_on(need_login()) { Ok(login) => login.into(), Err(e) => { error!("Failed to check if user is authorized: {}", e); false.into() } } } pub fn requestLoginCode(phone: String) -> u64 { match block_on(request_login_code(&phone)) { Ok(token) => Box::into_raw(Box::new(token)) as u64, Err(e) => { error!("Failed to request login code: {}", e); 0 as u64 } } } pub fn signIn(token_ptr: u64, code: String) { let token = unsafe { *Box::from_raw(token_ptr as *mut LoginToken) }; match block_on(sign_in(token, &code)) { Ok(_) => info!("Sign in success"), Err(e) => error!("Failed to sign in: {}", e), } } pub fn getDialogs() -> u64 { match block_on(get_dialogs()) { Ok(dialogs) => Box::into_raw(Box::new(dialogs)) as u64, Err(e) => { error!("Failed to get dialogs: {}", e); 0 as u64 } } } pub fn dialogCount(dialogsPtr: u64) -> u32 { let dialogs = unsafe { &mut *(dialogsPtr as *mut Vec) }; dialogs.len() as u32 } pub fn dialogPacked(dialogsPtr: u64, index: u32) -> String { let dialogs = unsafe { &mut *(dialogsPtr as *mut Vec) }; dialogs[index as usize].chat().pack().to_hex() } pub fn dialogTitle(dialogsPtr: u64, index: u32) -> String { let dialogs = unsafe { &mut *(dialogsPtr as *mut Vec) }; dialogs[index as usize].chat().name().to_string() } pub fn dialogSender(dialogsPtr: u64, index: u32) -> String { let dialogs = unsafe { &mut *(dialogsPtr as *mut Vec) }; 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() } } pub fn dialogText(dialogsPtr: u64, index: u32) -> String { let dialogs = unsafe { &mut *(dialogsPtr as *mut Vec) }; if let Some(msg) = dialogs[index as usize].last_message.as_ref() { msg.text().to_string() } else { "".to_string() } } pub fn dialogTime(dialogsPtr: u64, index: u32) -> String { let dialogs = unsafe { &mut *(dialogsPtr as *mut Vec) }; if let Some(msg) = dialogs[index as usize].last_message.as_ref() { msg.date().to_rfc3339().to_string() } else { String::new() } } pub fn dialogAck(dialogsPtr: u64, index: u32) -> u32 { let dialogs = unsafe { &mut *(dialogsPtr as *mut Vec) }; let dialog = &dialogs[index as usize]; 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 } } pub fn dialogPin(dialogsPtr: u64, index: u32) -> u32 { let dialogs = unsafe { &mut *(dialogsPtr as *mut Vec) }; let pinned = match &dialogs[index as usize].dialog { tl::enums::Dialog::Dialog(d) => d.pinned, tl::enums::Dialog::Folder(f) => f.pinned, }; pinned as u32 } pub fn freeDialogs(dialogsPtr: u64) { let _ = unsafe { Box::from_raw(dialogsPtr as *mut Vec) }; } pub fn sendMessage(packed: String, text: String) { let packed = PackedChat::from_hex(&packed).unwrap(); match block_on(send_message(packed, &text)) { Ok(_) => info!("Message sent"), Err(e) => error!("Failed to send message: {}", e), } }