#![cfg(target_os = "android")] #![allow(non_snake_case)] mod db; use grammers_client::types::{LoginToken, Message as OgMessage}; 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 {} struct LoginProcedure { token: Mutex>, } #[derive(Debug, Clone, Copy)] pub enum MessageAck { Received, Seen, Sent, } #[derive(Debug, Clone, Copy)] pub enum Formatting { Unknown, Mention, HashTag, BotCommand, Url, Email, Bold, Italic, Code, Pre, TextUrl, MentionName, Phone, CashTag, Underline, Strike, Blockquote, BankCard, Spoiler, CustomEmoji, } #[derive(Debug, Clone)] pub struct TextFormat { format: Formatting, offset: i32, length: i32, extra: Option, } #[derive(Debug, Clone)] pub struct Message { id: i32, sender: String, text: String, date: SystemTime, edit_date: Option, formatting: Vec, } #[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, 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) } 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<()> { 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 = block_on(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() }, })) .map_err(|_| NativeError::Network)?; info!("Connected!"); CLIENT .set(client) .map_err(|_| NativeError::Initialization)?; Ok(()) } pub fn need_login() -> Result { let client = CLIENT.get().ok_or(NativeError::Initialization)?; block_on(client.is_authorized()).map_err(|_| NativeError::Network) } impl LoginProcedure { fn new() -> Self { Self { token: Mutex::new(None), } } fn request_login_code(&self, phone: String) -> Result<()> { let client = CLIENT.get().ok_or(NativeError::Initialization)?; let token = block_on(client.request_login_code(&phone, API_ID, API_HASH)) .map_err(|_| NativeError::Network)?; *self.token.lock().unwrap() = Some(token); Ok(()) } fn sign_in(&self, code: String) -> Result<()> { let token = self .token .lock() .unwrap() .take() .ok_or(NativeError::Initialization)?; let client = CLIENT.get().ok_or(NativeError::Initialization)?; block_on(client.sign_in(&token, &code)).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(()) } } pub fn get_dialogs() -> Result> { let client = CLIENT.get().ok_or(NativeError::Initialization)?; 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)? { 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() { 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() }) } fn adapt_message(m: OgMessage) -> Message { Message { id: m.id(), sender: if let Some(sender) = m.sender() { sender.name().to_string() } else { "unknown".to_string() }, text: m.text().to_string(), date: m.date().into(), edit_date: m.edit_date().map(|d| d.into()), formatting: if let Some(entities) = m.fmt_entities() { use tl::enums::MessageEntity as ME; macro_rules! tf { ($formatting:ident($entity:ident)) => { tf!($formatting($entity).extra(None)) }; ($formatting:ident($entity:ident).extra($extra:expr)) => { TextFormat { format: Formatting::$formatting, offset: $entity.offset, length: $entity.length, extra: $extra, } }; } entities .into_iter() .map(|e| match e { ME::Unknown(e) => tf!(Unknown(e)), ME::Mention(e) => tf!(Mention(e)), ME::Hashtag(e) => tf!(HashTag(e)), ME::BotCommand(e) => tf!(BotCommand(e)), ME::Url(e) => tf!(Url(e)), ME::Email(e) => tf!(Email(e)), ME::Bold(e) => tf!(Bold(e)), ME::Italic(e) => tf!(Italic(e)), ME::Code(e) => tf!(Code(e)), ME::Pre(e) => tf!(Pre(e).extra(Some(e.language.to_string()))), ME::TextUrl(e) => tf!(TextUrl(e).extra(Some(e.url.to_string()))), ME::MentionName(e) => tf!(MentionName(e).extra(Some(e.user_id.to_string()))), ME::InputMessageEntityMentionName(e) => tf!(Unknown(e)), ME::Phone(e) => tf!(Phone(e)), ME::Cashtag(e) => tf!(CashTag(e)), ME::Underline(e) => tf!(Underline(e)), ME::Strike(e) => tf!(Strike(e)), ME::Blockquote(e) => tf!(Blockquote(e)), ME::BankCard(e) => tf!(BankCard(e)), ME::Spoiler(e) => tf!(Spoiler(e)), ME::CustomEmoji(e) => { tf!(CustomEmoji(e).extra(Some(e.document_id.to_string()))) } }) .collect() } else { Vec::new() }, } } pub fn get_messages(packed: String) -> Result> { let chat = PackedChat::from_hex(&packed).unwrap(); let client = CLIENT.get().ok_or(NativeError::Initialization)?; block_on(async { let mut result = Vec::new(); let mut messages = client.iter_messages(chat); while let Some(message) = messages.next().await.map_err(|_| NativeError::Network)? { result.push(message); } Ok(result) }) .map(|messages| messages.into_iter().map(adapt_message).collect()) } 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(adapt_message) .map_err(|_| NativeError::Network) }