#![cfg(target_os = "android")] #![allow(non_snake_case)] mod db; use grammers_client::types::{Dialog as OgDialog, 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::fmt; 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")); 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); #[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, lastMessage: Option, pinned: bool, } type Result = std::result::Result; 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 => { error!("Database was not initialized"); return Err(NativeError::Initialization); } }; info!("Connecting to Telegram..."); let session = Session::new(); let sessions = db::get_sessions(conn).map_err(|_| NativeError::Database)?; 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 .map_err(|_| NativeError::Network)?; info!("Connected!"); CLIENT .set(client) .map_err(|_| NativeError::Initialization)?; Ok(()) } async fn need_login() -> Result { let client = CLIENT.get().ok_or(NativeError::Initialization)?; client .is_authorized() .await .map_err(|_| NativeError::Network) } async fn request_login_code(phone: &str) -> Result { let client = CLIENT.get().ok_or(NativeError::Initialization)?; client .request_login_code(&phone, API_ID, API_HASH) .await .map_err(|_| NativeError::Network) } async fn sign_in(token: LoginToken, code: &str) -> Result<()> { let client = CLIENT.get().ok_or(NativeError::Initialization)?; client .sign_in(&token, &code) .await .map_err(|_| NativeError::Network)?; let guard = DATABASE.lock().unwrap(); let conn = match guard.as_ref() { Some(c) => c, None => { error!("Database was not initialized"); return Err(NativeError::Initialization); } }; let mut session = db::create_session(conn).map_err(|_| NativeError::Database)?; 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).map_err(|_| NativeError::Database)?; Ok(()) } async fn get_dialogs() -> Result> { let client = CLIENT.get().ok_or(NativeError::Initialization)?; let mut result = Vec::new(); let mut dialogs = client.iter_dialogs(); 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(NativeError::Initialization)?; client .send_message(chat, text) .await .map_err(|_| NativeError::Network)?; Ok(()) } pub fn initDatabase(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(e) => Err(NativeError::Database), } } pub fn initClient() -> Result<()> { block_on(init_client()) } pub fn needLogin() -> Result { block_on(need_login()) } pub fn requestLoginCode(phone: String) -> Result { block_on(request_login_code(&phone)).map(|token| Box::into_raw(Box::new(token)) as u64) } pub fn signIn(token_ptr: u64, code: String) -> Result<()> { let token = unsafe { *Box::from_raw(token_ptr as *mut LoginToken) }; block_on(sign_in(token, &code)) } pub fn getDialogs() -> Result> { block_on(get_dialogs()).map(|dialogs| { dialogs .into_iter() .map(|d| Dialog { id: d.chat().pack().to_hex(), title: d.chat().name().to_string(), lastMessage: d.last_message.map(|m| MessagePreview { sender: if let Some(sender) = m.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() }) } pub fn sendMessage(packed: String, text: String) -> Result<()> { let packed = PackedChat::from_hex(&packed).unwrap(); block_on(send_message(packed, &text)) }