mail4agent_messenger_shell/cli/
element_open.rs1use super::session_arg::{resolve, take_flag};
19
20pub fn run(args: Vec<String>) {
21 match go(args) {
22 Ok(()) => println!("element opened (login token not shown)"),
23 Err(e) => {
24 eprintln!("element-open: {e}");
25 std::process::exit(if e.starts_with("usage") { 64 } else { 1 });
26 }
27 }
28}
29
30const USAGE: &str = "usage: m4a-agent element-open (--as <nick>|--session <id>) --element <url> (env: M4A_STORE_ROOT, M4A_PRODUCT_URL, M4A_TIER, M4A_BROWSER)";
31
32pub(crate) fn page(element: &str, token: &str) -> String {
34 let enc: String = token.bytes().map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") }).collect();
35 format!("{}/?loginToken={enc}", element.trim_end_matches('/'))
36}
37
38fn go(mut args: Vec<String>) -> Result<(), String> {
39 let element = take_flag(&mut args, "--element")?.or_else(|| std::env::var("M4A_ELEMENT_URL").ok().filter(|v| !v.is_empty())).ok_or(USAGE)?;
40 let target = resolve(&mut args)?;
41 if !args.is_empty() || !(element.starts_with("https://") || element.starts_with("http://")) {
42 return Err(USAGE.into());
43 }
44 let url = std::env::var("M4A_PRODUCT_URL").or_else(|_| std::env::var(crate::HOMESERVER_URL_ENV)).ok().filter(|v| !v.is_empty()).ok_or("M4A_PRODUCT_URL is not set")?;
45 let token = login_token(&target.store_root, &target.session_id, &url)?;
46 let opener = std::env::var("M4A_BROWSER").ok().filter(|v| !v.is_empty()).unwrap_or_else(|| if cfg!(target_os = "macos") { "open".into() } else { "xdg-open".into() });
47 let st = std::process::Command::new(&opener).arg(page(&element, &token)).stdin(std::process::Stdio::null()).stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).status().map_err(|_| format!("the browser opener {opener:?} could not be started (set M4A_BROWSER)"))?;
48 if st.success() {
49 Ok(())
50 } else {
51 Err("the browser opener failed".into())
52 }
53}
54
55fn login_token(store_root: &std::path::Path, session_id: &str, url: &str) -> Result<String, String> {
56 use m4a_agent::backend::Backend;
57 let fail = |e: m4a_agent::AgentError| e.to_string();
58 let ids = m4a_agent::IdentityStore::new(crate::store_key::vault(store_root).map_err(|e| e.to_string())?);
59 let backend = m4a_agent::backend::server::ServerBackend::new(url).map_err(fail)?;
60 let id = ids.resolve(session_id, m4a_agent::BackendKind::Server, backend.server_ref()).map_err(fail)?;
61 if !id.enrolled {
62 return Err("this identity is not enrolled on that server yet".into());
63 }
64 let wire = m4a_agent::backend::wire::HttpWire::new(url).map_err(fail)?;
65 m4a_agent::keyauth::login_token(&wire, &ids, &id).map_err(fail)
66}
67
68#[cfg(test)]
69mod tests {
70 use super::page;
71
72 #[test]
73 fn the_page_carries_the_token_in_the_query_encoded() {
74 assert_eq!(page("https://chat.example/", "abc-DEF_1.2~x"), "https://chat.example/?loginToken=abc-DEF_1.2~x");
75 assert_eq!(page("http://localhost:8080", "a b/c"), "http://localhost:8080/?loginToken=a%20b%2Fc");
76 }
77}