use std::path::{Path,PathBuf}; use std::str::FromStr; use std::time::Duration; use std::time::Instant; use ansi_to_html; use chrono; use rss; use crate::conf::Conf; pub struct Job { pub url: String, pub selector: String, pub every: Duration, pub last_run: Option, pub output_file: Option, pub channel: Option, pub source_file: Option, } impl Job { fn default(conf: &Conf) -> Job { return Job { url: "".to_string(), selector: "".to_string(), every: conf.check_interval, last_run: None, output_file: None, channel: None, source_file: None, }; } fn set_default_output_file(&mut self, conf: &Conf) { let mut output_file = conf.output_dir.clone(); let mut file_name = self.url.clone().replace("/", "-"); file_name.push_str(".rss"); output_file = output_file.join(Path::new(&file_name)); self.output_file = Some(output_file); } pub fn new(url: &str, selector: &str, conf: &Conf) -> Job { let mut job = Job::default(conf); job.url = url.to_string(); job.set_default_output_file(conf); job.selector = selector.to_string(); match std::fs::File::open(job.output_file.as_ref().unwrap()) { Err(why) => { println!("Failed to open '{}': {}", job.output_file.as_ref().unwrap().display(), why); println!("Creating empty RSS channel for job '{}'", job.url); job.channel = Some( rss::ChannelBuilder::default() .title(url) .link(url) .description("haunting") .build() ); job.channel.as_mut().unwrap().set_generator("Haunter".to_string()); }, Ok(file) => { job.channel = Some( rss::Channel::read_from(std::io::BufReader::new(file)).unwrap() ); }, }; return job; } pub fn from_file<'a>(path: &'a Path, conf: &'a Conf) -> Result { let mut job = Job::default(conf); let items = match crate::conf::read_conf_file(path) { Err(_) => return Err("Failed to read from configuration file"), Ok(items) => items, }; for item in items.iter() { let key = item.0.as_str(); match key { "url" => { job.url = item.1.clone(); }, "selector" => { job.selector = item.1.clone(); }, "every" => { let converted_value = match item.1.parse::() { Err(why) => { println!("Failed to convert '{}' to u64: {}", item.1, why); return Err("Failed to parse value of 'every'"); }, Ok(v) => v, }; job.every = Duration::new(converted_value, 0); }, "output_file" => { job.output_file = Some( conf.output_dir.join(PathBuf::from_str(item.1.as_str()).unwrap()) ); } _ => { println!("Unknown key '{}' in job file '{}'", key, path.display()); return Err("Unknown key"); } } } if job.output_file.is_none() { job.set_default_output_file(conf); } job.source_file = Some(PathBuf::from(path)); return Ok(job); } pub fn update(&mut self, value: &str, diff: &str) { if self.channel.is_none() { println!("Skipping update of channel: no channel set"); return; } let channel = self.channel.as_mut().unwrap(); let update_time = chrono::Utc::now(); let item = rss::ItemBuilder::default() .title(format!("Update to '{}'", self.url)) .link(self.url.clone()) .pub_date(update_time.to_rfc2822()) .content(format!(r#" New content at {}:
{}


Diff:
{}
"#, update_time.format("%d/%m/%Y %H:%M"), ansi_to_html::convert_escaped(value).unwrap().as_str(), ansi_to_html::convert_escaped(diff).unwrap().as_str() ) ) .build(); channel.items.push(item); if self.output_file.is_some() { match std::fs::File::create(self.output_file.as_ref().unwrap()) { Err(why) => { println!("Failed to open '{}' for writing: {}", self.output_file.as_ref().unwrap().display(), why); }, Ok(file) => { channel.write_to( std::io::BufWriter::new(file) ).expect("Failed to write updated channel"); }, }; } } } #[cfg(test)] mod tests { use super::*; use std::io::Write; use tempfile::NamedTempFile; #[test] fn create() { let conf = Conf::get_default_conf(); let job = Job::new(&"my/url", &"myselector", &conf); assert_eq!(job.url, "my/url"); assert_eq!(job.output_file.unwrap().to_str().unwrap(), "results.d/my-url.rss"); assert_eq!(job.selector, "myselector"); } #[test] fn create_from_file() { let conf = Conf::get_default_conf(); let mut tf = NamedTempFile::new().unwrap(); let job_conf = r#" url = http://example.com/test output_file = example_output.atom every=7200 selector = section.listing:nth-child(2) > ul:nth-child(1) > li:nth-child(3) > header:nth-child(3) > h2:nth-child(1) > a:nth-child(1) "#; tf.write_all(job_conf.as_bytes()).expect("Failed to write configuration to file"); let job = Job::from_file(tf.path(), &conf).expect("Failed to read configuration file"); assert_eq!(job.url, "http://example.com/test"); assert_eq!(job.output_file.unwrap().to_str().unwrap(), "results.d/example_output.atom"); assert_eq!(job.every.as_secs(), 7200); assert_eq!(job.selector, "section.listing:nth-child(2) > ul:nth-child(1) > li:nth-child(3) > header:nth-child(3) > h2:nth-child(1) > a:nth-child(1)"); } #[test] fn create_from_file_default_output_file() { let conf = Conf::get_default_conf(); let mut tf = NamedTempFile::new().unwrap(); let job_conf = r#" url = http://example.com/test every=7200 selector = section.listing:nth-child(2) > ul:nth-child(1) > li:nth-child(3) > header:nth-child(3) > h2:nth-child(1) > a:nth-child(1) "#; tf.write_all(job_conf.as_bytes()).expect("Failed to write configuration to file"); let job = Job::from_file(tf.path(), &conf).expect("Failed to read configuration file"); assert_eq!(job.url, "http://example.com/test"); assert_eq!(job.output_file.unwrap().to_str().unwrap(), "results.d/http:--example.com-test.rss"); assert_eq!(job.every.as_secs(), 7200); assert_eq!(job.selector, "section.listing:nth-child(2) > ul:nth-child(1) > li:nth-child(3) > header:nth-child(3) > h2:nth-child(1) > a:nth-child(1)"); assert_eq!(job.source_file.unwrap().to_str(), tf.path().to_str()); } }