452 lines
13 KiB
Rust
452 lines
13 KiB
Rust
#![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<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 {}
|
|
|
|
struct LoginProcedure {
|
|
token: Mutex<Option<LoginToken>>,
|
|
}
|
|
|
|
#[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<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Message {
|
|
id: i32,
|
|
sender: String,
|
|
text: String,
|
|
date: SystemTime,
|
|
edit_date: Option<SystemTime>,
|
|
formatting: Vec<TextFormat>,
|
|
}
|
|
|
|
#[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
|
|
.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<bool> {
|
|
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<Vec<Dialog>> {
|
|
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<Vec<Message>> {
|
|
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<Message> {
|
|
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)
|
|
}
|