mod structs; use std::env; use std::io::{stdin, BufRead}; use std::process::{exit, Command}; use std::os::unix::process::CommandExt; use reqwest::Client; use quick_xml::Reader; use quick_xml::events::Event; extern crate tokio; fn main() { let view_image_command = ["xdg-open", "{}"]; let nitter_instance = "https://nitter.nixnet.services"; let twitter_accounts = [ structs::TwitterAccount { display_name: "calli", twitter_username: "moricalliope", schedule_keyword: "schedule" }, structs::TwitterAccount { display_name: "kiara", twitter_username: "takanashikiara", schedule_keyword: "schedule" }, ]; let mut args = env::args(); args.next(); let mut preselect = String::new(); for arg in args { preselect.push_str(&format!("{} ", arg)); } let mut preselect = preselect.trim().to_lowercase(); if preselect.is_empty() { let mut index = 0; for account in &twitter_accounts { eprintln!("{}. {}", index, &account.display_name); index += 1; } eprint!("> "); // https://www.programming-idioms.org/idiom/120/read-integer-from-stdin/1906/rust preselect = stdin() .lock() .lines() .next() .expect("stdin is not avaliable") .expect("can't read from stdin") .trim() .to_lowercase(); } let mut account_index = 0; match preselect.parse::() { Ok(index) => { if twitter_accounts.len() > index { account_index = index; } else { eprintln!("Invalid index"); exit(1); } }, Err(_) => { let mut exhausted = true; for account in &twitter_accounts { if &account.display_name.to_lowercase() == &preselect { exhausted = false; break; } account_index += 1; } if exhausted { eprintln!("Cannot find account"); exit(1); } } }; let account = &twitter_accounts[account_index]; let text = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap() .block_on(get_feed_text(nitter_instance, &account)); let mut is_inside_item = false; let mut is_inside_title = false; let mut is_inside_description = false; let mut title: Option = None; let mut url: Option = None; let mut reader = Reader::from_str(&text); let mut buf = Vec::new(); loop { match reader.read_event(&mut buf) { Ok(Event::Start(ref e)) => { match e.name() { b"item" => { is_inside_item = true; title = None; url = None; }, b"title" => is_inside_title = true, b"description" => is_inside_description = true, _ => () }; }, Ok(Event::Text(e)) => { if is_inside_item && is_inside_title { title = Some(e.unescape_and_decode(&reader).unwrap()); } }, Ok(Event::CData(e)) => { if is_inside_item && is_inside_description { let description_text = e.unescape_and_decode(&reader).unwrap(); let mut description_reader = Reader::from_str(&description_text); let mut description_buf = Vec::new(); loop { match description_reader.read_event(&mut description_buf) { Ok(Event::Empty(ref e)) => { if e.name() == b"img" { url = Some(e.attributes() .find(|attribute| attribute.as_ref().unwrap().key == b"src") .unwrap() .unwrap() .unescape_and_decode_value(&description_reader) .unwrap()); } }, Err(err) => panic!("Error at position {}: {:?}", description_reader.buffer_position(), err), Ok(Event::Eof) => break, _ => () } } } }, Ok(Event::End(ref e)) => { match e.name() { b"item" => { is_inside_item = false; if title.is_some() && url.is_some() { break; } }, b"description" => { is_inside_description = false; if title.is_some() && url.is_some() { break; } }, b"title" => is_inside_title = false, _ => () }; }, Err(err) => panic!("Error at position {}: {:?}", reader.buffer_position(), err), Ok(Event::Eof) => { if title.is_none() || url.is_none() { eprintln!("Cannot find tweet"); exit(1); } break; }, _ => () }; buf.clear(); } drop(buf); println!("{}", title.unwrap()); let mut args = view_image_command.to_vec().split_off(1); let url = url.unwrap(); for i in 0..args.len() { if args[i] == "{}" { args[i] = url.as_str(); } } Command::new(&view_image_command[0]) .args(args) .exec(); } async fn get_feed_text(nitter_instance: &str, account: &structs::TwitterAccount) -> String { let mut url = nitter_instance.to_string(); url.push_str("/search/rss"); match Client::new().get(&url) .query(&[ ("f", "tweets"), ("f-images", "on"), ("q", &format!("{} from:{}", &account.schedule_keyword, &account.twitter_username)) ]) .send() .await { Ok(resp) => { match resp.text().await { Ok(text) => text, Err(err) => { eprintln!("Failed to get text due to {}", err); exit(1); } } }, Err(err) => { eprintln!("Failed to connect to Nitter due to {}", err); exit(1); } } }