pub mod api; mod listen; mod web; use std::fmt::Write as _; use std::io::{Error, ErrorKind, Read, Write as StdWrite}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; pub use api::*; use chat::Chat; use listen::listen; use locales::t; use modals::Modals; use pddb::Pddb; use ticktimer_server::Ticktimer; use tls::xtls::TlsConnector; use trng::*; use ureq::Agent; use url::Url; use crate::web::get_username; /// PDDB Dict for mtxchat keys const MTXCHAT_STATE: &str = "mtxchat.state"; const MTXCHAT_DIALOGUE: &str = "mtxchat.dialogue"; const FILTER_KEY: &str = "_filter"; const PASSWORD_KEY: &str = "password"; const ROOM_ID_KEY: &str = "_room_id"; const ROOM_NAME_KEY: &str = "room_name"; const ROOM_DOMAIN_KEY: &str = "room_domain"; const SINCE_KEY: &str = "_since"; const TOKEN_KEY: &str = "_token"; const USER_ID_KEY: &str = "_user_id"; const USER_NAME_KEY: &str = "user_name"; const USER_DOMAIN_KEY: &str = "user_domain"; const DOMAIN_MATRIX: &str = "matrix.org"; const MTX_LONG_TIMEOUT_MS: i32 = 60000; // ms const WIFI_TIMEOUT_MS: u32 = 30_000; // ms const SEND_RETRIES: usize = 3; pub const CLOCK_NOT_SET_ID: usize = 1; pub const PDDB_NOT_MOUNTED_ID: usize = 2; pub const WIFI_NOT_CONNECTED_ID: usize = 3; pub const MTXCLI_INITIALIZED_ID: usize = 4; pub const WIFI_CONNECTED_ID: usize = 5; pub const SET_USER_ID: usize = 6; pub const SET_PASSWORD_ID: usize = 7; pub const LOGGED_IN_ID: usize = 8; pub const LOGIN_FAILED_ID: usize = 9; pub const SET_ROOM_ID: usize = 10; pub const ROOMID_FAILED_ID: usize = 11; pub const FILTER_FAILED_ID: usize = 12; pub const SET_SERVER_ID: usize = 13; pub const LOGGING_IN_ID: usize = 14; pub const LOGGED_OUT_ID: usize = 15; pub const NOT_CONNECTED_ID: usize = 16; pub const FAILED_TO_SEND_ID: usize = 17; pub const PLEASE_LOGIN_ID: usize = 18; #[cfg(not(target_os = "xous"))] pub const HOSTED_MODE: bool = true; #[cfg(target_os = "xous")] pub const HOSTED_MODE: bool = false; //#[derive(Debug)] pub struct MtxChat<'a> { chat: &'a Chat, trng: Trng, pddb: Pddb, netmgr: net::NetManager, user_id: Option, user_name: Option, user_domain: Option, agent: Agent, token: Option, logged_in: bool, room_id: Option, room_name: Option, room_domain: Option, filter: Option, since: Option, listening: bool, modals: Modals, new_username: bool, new_room: bool, tt: Ticktimer, } impl<'a> MtxChat<'a> { pub fn new(chat: &Chat) -> MtxChat { let xns = xous_names::XousNames::new().unwrap(); let modals = Modals::new(&xns).expect("can't connect to Modals server"); let trng = Trng::new(&xns).unwrap(); let pddb = pddb::Pddb::new(); pddb.try_mount(); let status = t!("mtxchat.status.default", locales::LANG).to_owned(); chat.set_status_text(&status); MtxChat { chat, trng, pddb, netmgr: net::NetManager::new(), user_id: None, user_name: None, user_domain: Some(DOMAIN_MATRIX.to_string()), agent: ureq::builder().tls_connector(Arc::new(TlsConnector {})).build(), token: None, logged_in: false, room_id: None, room_name: None, room_domain: None, filter: None, since: None, listening: false, modals, new_username: false, new_room: false, tt: ticktimer_server::Ticktimer::new().unwrap(), } } pub fn set(&mut self, key: &str, value: &str) -> Result<(), Error> { if key.starts_with("__") { Err(Error::new(ErrorKind::PermissionDenied, "may not set a variable beginning with __ ")) } else { log::info!("set '{}' = '{}'", key, value); // delete key first to ensure data in a prior longer key is gone self.pddb.delete_key(MTXCHAT_STATE, key, None).ok(); match self.pddb.get(MTXCHAT_STATE, key, None, true, true, None, None::) { Ok(mut pddb_key) => match pddb_key.write(&value.as_bytes()) { Ok(len) => { self.pddb.sync().ok(); log::trace!("Wrote {} bytes to {}:{}", len, MTXCHAT_STATE, key); } Err(e) => { log::warn!("Error writing {}:{} {:?}", MTXCHAT_STATE, key, e); } }, Err(e) => log::warn!("failed to set pddb {}:{} {:?}", MTXCHAT_STATE, key, e), }; match key { // update cached values FILTER_KEY => self.filter = Some(value.to_string()), PASSWORD_KEY => (), ROOM_ID_KEY => self.room_id = Some(value.to_string()), ROOM_NAME_KEY => self.room_name = Some(value.to_string()), ROOM_DOMAIN_KEY => self.room_domain = Some(value.to_string()), SINCE_KEY => self.since = Some(value.to_string()), TOKEN_KEY => self.token = Some(value.to_string()), USER_NAME_KEY => self.user_name = Some(value.to_string()), USER_DOMAIN_KEY => self.user_domain = Some(value.to_string()), USER_ID_KEY => self.user_id = Some(value.to_string()), _ => {} } Ok(()) } } // will log on error (vs. panic) pub fn set_debug(&mut self, key: &str, value: &str) -> bool { match self.set(key, value) { Ok(()) => true, Err(e) => { log::info!("error setting key {}: {:?}", key, e); false } } } pub fn unset(&mut self, key: &str) -> Result<(), Error> { if key.starts_with("__") { Err(Error::new(ErrorKind::PermissionDenied, "may not unset a variable beginning with __ ")) } else { log::info!("unset '{}'", key); match self.pddb.delete_key(MTXCHAT_STATE, key, None) { Ok(_) => log::info!("pddb key deleted: {key}"), Err(e) => match e.kind() { ErrorKind::NotFound => (), // ignore, nothing to do _ => log::warn!("failed to delete pddb key: {key}: {:?}", e), }, } match key { // update cached values FILTER_KEY => self.filter = None, PASSWORD_KEY => (), ROOM_ID_KEY => self.room_id = None, ROOM_DOMAIN_KEY => self.room_domain = None, SINCE_KEY => self.since = None, USER_DOMAIN_KEY => self.user_domain = None, USER_ID_KEY => self.user_id = None, USER_NAME_KEY => self.user_name = None, _ => {} } Ok(()) } } // will log on error (vs. panic) pub fn unset_debug(&mut self, key: &str) -> bool { match self.unset(key) { Ok(()) => true, Err(e) => { log::info!("error unsetting key {}: {:?}", key, e); false } } } pub fn get(&self, key: &str) -> Result, Error> { let value = match self.pddb.get(MTXCHAT_STATE, key, None, true, false, None, None::) { Ok(mut pddb_key) => { let mut buffer = [0; 256]; match pddb_key.read(&mut buffer) { Ok(len) => match String::from_utf8(buffer[..len].to_vec()) { Ok(s) => Some(s), Err(e) => { log::warn!("failed to String: {:?}", e); None } }, Err(e) => { log::warn!("failed pddb_key read: {:?}", e); None } } } Err(_) => None, }; log::info!("get '{}' = '{:?}'", key, value); Ok(value) } pub fn connect(&mut self) -> bool { log::info!("Attempting connect to Matrix server"); if self.wifi() { if self.login() { if let Some(_room_id) = self.get_room_id() { self.dialogue_set(self.room_alias().as_deref()); self.listen(); if self.new_room { self.new_room = false; self.chat.set_status_text(t!("mtxchat.busy.new_listen", locales::LANG)); self.chat.set_busy_state(true); } return true; } else { self.modals .show_notification(t!("mtxchat.roomid.failed", locales::LANG), None) .expect("notification failed"); } } else { self.modals .show_notification(t!("mtxchat.login.failed", locales::LANG), None) .expect("notification failed"); } if self.new_username { self.new_username = false; self.help(); } } else { self.modals .show_notification(t!("mtxchat.wifi.warning", locales::LANG), None) .expect("notification failed"); } self.dialogue_set(None); false } pub fn login(&mut self) -> bool { self.chat.set_status_text(t!("mtxchat.busy.login", locales::LANG)); self.chat.set_busy_state(true); self.token = self.get(TOKEN_KEY).unwrap_or(None); self.logged_in = false; let mut url = Url::parse("https://matrix.org").unwrap(); if let Ok(Some(host)) = self.get(USER_DOMAIN_KEY) { url.set_host(Some(&host)).expect("failed to set host"); } if let Some(token) = &self.token { if let Some(user_id) = web::whoami(&mut url, &token, &mut self.agent) { let i = match user_id.find('@') { Some(index) => index + 1, None => 0, }; let j = match user_id.find(':') { Some(index) => index, None => user_id.len(), }; self.set(USER_ID_KEY, &user_id).expect("failed to save user id"); self.set(USER_NAME_KEY, &user_id[i..j]).expect("failed to save user name"); self.set(USER_DOMAIN_KEY, &user_id[j + 1..]).expect("failed to save user domain"); self.logged_in = true; } } if !self.logged_in { if web::get_login_type(&mut url, &mut self.agent) { self.login_modal(); let log_entry = match (&self.user_id, self.get(PASSWORD_KEY).unwrap_or(None)) { (Some(user_id), Some(password)) => { if let Some(new_token) = web::authenticate_user(&mut url, &user_id, &password, &mut self.agent) { self.set_debug(TOKEN_KEY, &new_token); self.logged_in = true; "authenticated user" } else { "Error: cannnot login with password" } } (None, _) => "missing user id", (_, None) => "missing password", }; log::info!("{log_entry}"); } else { log::warn!("failed to web::get_login_type()"); } } if self.logged_in { log::info!("logged_in"); } else { log::info!("login failed"); // unset credentials to facilitate re-attempt self.unset(TOKEN_KEY).expect("failed to unset token"); self.unset(USER_DOMAIN_KEY).expect("failed to unset user domain"); } self.chat.set_busy_state(false); self.logged_in } pub fn login_modal(&mut self) { const HIDE: &str = "*****"; let mut old_username = String::new(); let mut builder = self.modals.alert_builder(t!("mtxchat.login.title", locales::LANG)); let builder = match self.get(USER_NAME_KEY) { // TODO add TextValidationFn Ok(Some(user)) => { old_username = user.clone(); builder.field_placeholder_persist(Some(user), None) } _ => builder.field(Some(t!("mtxchat.user_name", locales::LANG).to_string()), None), }; let builder = match self.get(USER_DOMAIN_KEY) { // TODO add TextValidationFn Ok(Some(server)) => builder.field_placeholder_persist(Some(server), None), _ => builder.field(Some(t!("mtxchat.domain", locales::LANG).to_string()), None), }; let builder = match self.get(PASSWORD_KEY) { Ok(Some(_pwd)) => builder.field_placeholder_persist(Some(HIDE.to_string()), None), _ => builder.field(Some(t!("mtxchat.password", locales::LANG).to_string()), None), }; if let Ok(payloads) = builder.build() { self.unset_debug(TOKEN_KEY); self.set(USER_NAME_KEY, payloads.content()[0].content.as_str()).expect("failed to save username"); self.new_username = payloads.content()[0].content.as_str().ne(&old_username); self.set(USER_DOMAIN_KEY, payloads.content()[1].content.as_str()).expect("failed to save server"); if payloads.content()[2].content.as_str().ne(HIDE) { self.set(PASSWORD_KEY, payloads.content()[2].content.as_str()) .expect("failed to save password"); } if let Some(user_name) = &self.user_name { if let Some(user_domain) = &self.user_domain { let mut user_id = String::new(); write!(user_id, "@{}:{}", user_name, user_domain).expect("failed to write user_id"); self.set(USER_ID_KEY, &user_id).expect("failed to save user"); } } } log::info!( "# user = {:?} user_name = {:?} server = {:?}", self.user_id, self.user_name, self.user_domain, ); } pub fn logout(&mut self) { self.unset_debug(TOKEN_KEY); // TODO logout with server } pub fn room_alias(&self) -> Option { let log_entry = match (&self.room_name, &self.room_domain) { (Some(room_name), Some(room_domain)) => { let mut room_alias = String::new(); write!(room_alias, "#{}:{}", &room_name, &room_domain).expect("failed to write room"); return Some(room_alias); } (None, _) => "No room name set", (_, None) => "No room domain set", }; log::warn!("{log_entry}"); None } pub fn get_room_id(&mut self) -> Option { self.room_modal(); let log_entry = match (self.logged_in, &self.token, &self.user_domain, &self.room_alias()) { (true, Some(token), Some(user_domain), Some(room_alias)) => { let mut url = Url::parse("https://matrix.org").unwrap(); url.set_host(Some(user_domain)).expect("failed to set host"); self.chat.set_status_text(t!("mtxchat.busy.room_id", locales::LANG)); self.chat.set_busy_state(true); if let Some(room_id) = web::get_room_id(&mut url, &room_alias, &token, &mut self.agent) { self.set_debug(ROOM_ID_KEY, &room_id); self.chat.set_busy_state(false); return Some(room_id); } else { self.chat.set_busy_state(false); "failed to get room_id" } } (false, _, _, _) => "Not logged in", (_, None, _, _) => "No token set", (_, _, None, _) => "No user domain set", (_, _, _, None) => "No room alias set", }; log::warn!("{log_entry}"); None } pub fn redraw(&self) { self.chat.redraw(); } pub fn room_modal(&mut self) { let mut old_room = String::new(); let mut builder = self.modals.alert_builder(t!("mtxchat.room.title", locales::LANG)); let builder = match self.get(ROOM_NAME_KEY) { // TODO add TextValidationFn Ok(Some(room)) => { old_room = room.clone(); builder.field_placeholder_persist(Some(room), None) } _ => builder.field(Some(t!("mtxchat.room.name", locales::LANG).to_string()), None), }; let builder = match self.get(ROOM_DOMAIN_KEY) { // TODO add TextValidationFn Ok(Some(server)) => builder.field_placeholder_persist(Some(server), None), _ => builder.field(Some(t!("mtxchat.domain", locales::LANG).to_string()), None), }; if let Ok(payloads) = builder.build() { self.unset_debug(ROOM_ID_KEY); self.unset_debug(SINCE_KEY); self.unset_debug(FILTER_KEY); self.set(ROOM_NAME_KEY, payloads.content()[0].content.as_str()).expect("failed to save server"); self.new_room = payloads.content()[0].content.as_str().ne(&old_room); self.set(ROOM_DOMAIN_KEY, payloads.content()[1].content.as_str()).expect("failed to save server"); } log::info!("# ROOM_NAME_KEY set '{}' => clearing ROOM_ID_KEY, SINCE_KEY, FILTER_KEY", ROOM_NAME_KEY); } pub fn dialogue_set(&self, room_alias: Option<&str>) { self.chat.dialogue_set(MTXCHAT_DIALOGUE, room_alias).expect("failed to set dialogue"); } pub fn help(&self) { self.chat.help(); } pub fn get_filter(&mut self) -> bool { let log_entry = match ( &self.filter, &self.logged_in, &self.token, &self.user_id, &self.user_domain, &self.room_id, ) { (Some(_filter), _, _, _, _, _) => "filter already set", (_, true, Some(token), Some(user_id), Some(user_domain), Some(room_id)) => { let mut url = Url::parse("https://matrix.org").unwrap(); url.set_host(Some(user_domain)).expect("failed to set host"); log::info!("get_filter {} : {} : {} : {}", &user_id, &url.as_str(), &room_id, &token); if let Some(new_filter) = web::get_filter(&user_id, &mut url, &room_id, &token, &mut self.agent) { if self.set_debug(FILTER_KEY, &new_filter) { "set filter" } else { "failed to set" } } else { "failed to get filter" } } (_, false, _, _, _, _) => "Not logged in", (_, _, None, _, _, _) => "No token set", (_, _, _, None, _, _) => "No user id set", (_, _, _, _, None, _) => "No user domain set", (_, _, _, _, _, None) => "No room id set", }; log::warn!("{log_entry}"); self.filter.is_some() } pub fn listen(&mut self) { self.get_filter(); let log_entry = match ( self.listening, self.logged_in, &self.token, &self.room_id, &self.filter, &self.room_alias(), ) { (false, true, Some(token), Some(room_id), Some(filter), Some(room_alias)) => { self.listening = true; std::thread::spawn({ let mut url = Url::parse("https://matrix.org").unwrap(); if let Ok(host) = self.get(ROOM_DOMAIN_KEY) { url.set_host(host.as_deref()).expect("failed to set host"); } let token = token.clone(); let room_id = room_id.clone(); let since = self.since.clone(); let filter = filter.clone(); let room_alias = room_alias.clone(); let chat_cid = self.chat.cid().clone(); move || { listen(&mut url, &token, &room_id, since.as_deref(), &filter, &room_alias, chat_cid); } }); "Started listening" } (true, _, _, _, _, _) => "Already listening", (_, false, _, _, _, _) => "Not logged in", (_, _, None, _, _, _) => "No token set", (_, _, _, None, _, _) => "No room id set", (_, _, _, _, None, _) => "No filter set", (_, _, _, _, _, None) => "No room alias set", }; log::info!("{log_entry}"); } pub fn listen_over(&mut self, since: &str) { self.listening = false; log::info!("Stopped listening"); if since.len() > 0 { self.set_debug(SINCE_KEY, since); // don't re-start listening if there was an error if self.logged_in && self.wifi() { self.listen(); } } } pub fn gen_txn_id(&mut self) -> String { let mut txn_id = self.trng.get_u32().expect("unable to generate random u32"); log::info!("trng.get_u32() = {}", txn_id); txn_id += SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").subsec_nanos(); txn_id.to_string() } pub fn post(&mut self, text: &str) { let txn_id = self.gen_txn_id(); let log_entry = match (self.logged_in, &self.token, &self.user_domain, &self.room_id) { (true, Some(token), Some(user_domain), Some(room_id)) => { self.chat.set_status_text(t!("mtxchat.busy.sending", locales::LANG)); self.chat.set_busy_state(true); log::info!("txn_id = {}", txn_id); let mut url = Url::parse("https://matrix.org").unwrap(); url.set_host(Some(user_domain)).expect("failed to set host"); let mut success = false; for _ in 0..SEND_RETRIES { if web::send_message(&mut url, &room_id, &text, &txn_id, token, &mut self.agent) { success = true; break; } } let r = if success { "SENT" } else { "FAILED TO SEND" }; self.chat.set_busy_state(false); r } (false, _, _, _) => "Not logged in", (_, None, _, _) => "No token set", (_, _, None, _) => "No user domain set", (_, _, _, None) => "No room id set", }; log::info!("{log_entry}"); } // returns true is wifi is connected // // If wifi is not connected then a modal offers to "Connect to wifi?" // and tries for 10 seconds before representing. // pub fn wifi(&self) -> bool { if HOSTED_MODE { return true; } if let Some(conf) = self.netmgr.get_ipv4_config() { if conf.dhcp == com_rs::DhcpState::Bound { return true; } } while self.wifi_try_modal() { self.netmgr.connection_manager_wifi_on_and_run().unwrap(); self.chat.set_status_text(t!("mtxchat.busy.connecting", locales::LANG)); self.chat.set_busy_state(true); let start = self.tt.elapsed_ms(); while self.tt.elapsed_ms() - start < WIFI_TIMEOUT_MS as u64 { if let Some(conf) = self.netmgr.get_ipv4_config() { if conf.dhcp == com_rs::DhcpState::Bound { self.chat.set_busy_state(false); return true; } } self.tt.sleep_ms(1000).unwrap(); } self.modals.show_notification(t!("mtxchat.wifi.warning", locales::LANG), None).unwrap(); } self.chat.set_busy_state(false); self.chat.set_status_text(t!("mtxchat.wifi.warning_status", locales::LANG)); false } // returns true if "Connect to WiFi?" yes option is chosen // fn wifi_try_modal(&self) -> bool { self.modals.add_list_item("yes").expect("failed radio yes"); self.modals.add_list_item("no").expect("failed radio no"); self.modals.get_radiobutton("Connect to WiFi?").expect("failed radiobutton modal"); match self.modals.get_radio_index() { Ok(button) => button == 0, _ => false, } } } pub(crate) fn heap_usage() -> usize { match xous::rsyscall(xous::SysCall::IncreaseHeap(0, xous::MemoryFlags::R)) .expect("couldn't get heap size") { xous::Result::MemoryRange(m) => { let usage = m.len(); usage } _ => { log::info!("Couldn't measure heap usage"); 0 } } }