use ansi_term::Style;
use chrono::offset::Utc;
use chrono::DateTime;
use feed_rs::parser;
use itertools::Itertools;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::fs;
use std::fs::File;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::path::Path;
#[derive(Debug)]
pub struct ProcessedFeed {
pub title: String,
pub items: Vec<String>,
}
impl std::fmt::Display for ProcessedFeed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"📖 {}\n\t{}",
Style::new().bold().paint(&self.title),
format!("{}", self.items.iter().format("\n\t"))
)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Feed {
pub uri: String,
pub last_accessed: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigObj {
pub feeds: Vec<Feed>,
}
pub fn find_config() -> std::path::PathBuf {
let homedir: std::path::PathBuf = dirs::home_dir().expect("no home dir");
Path::new(&homedir).join(".hemrc")
}
fn config_to_rust() -> ConfigObj {
let config = fs::read_to_string(&find_config()).expect("reading config failed");
serde_json::from_str(&config).unwrap()
}
pub fn rust_to_config(content: &[u8]) {
let mut file = match File::create(find_config()) {
Err(why) => panic!("config file access failed: {}", why),
Ok(file) => file,
};
file.write_all(content)
.expect("Writing to .hemrc failed :(");
}
pub fn add_feed(feed: &str) {
let mut my_feeds: ConfigObj = config_to_rust();
my_feeds.feeds.push(Feed {
uri: feed.to_owned(),
last_accessed: Utc::now().to_rfc3339().to_owned(),
});
rust_to_config(serde_json::to_string(&my_feeds).unwrap().as_bytes());
}
pub fn list_feeds() {
let my_feeds: ConfigObj = config_to_rust();
for f in my_feeds.feeds {
println!("{}", f.uri);
}
}
pub async fn top<'a>(num: usize) -> Result<Vec<ProcessedFeed>, Box<dyn std::error::Error>> {
let config_path = find_config();
let client = Client::new();
let mut processed: Vec<ProcessedFeed> = Vec::new();
let config = match fs::read_to_string(&config_path) {
Ok(config) => config,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
eprintln!("Didn't find a .hemrc, creating it now...");
let mut configfile = fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&config_path)?;
configfile.write_all(r#"{"feeds": []}"#.as_bytes())?;
let mut bufreader = BufReader::new(configfile);
let mut contents = String::new();
bufreader.read_to_string(&mut contents)?;
contents
} else {
return Err(Box::from("Catastrophe!"));
}
}
};
let config_obj: ConfigObj = match serde_json::from_str(&config) {
Ok(config_obj) => config_obj,
Err(_) => {
return Err(Box::from(
"Your feeds list is empty! use `hem add` to add a feed.",
))
}
};
for i in 0..config_obj.feeds.len() {
let resp = client
.get(&config_obj.feeds[i].uri)
.send()
.await?
.text()
.await?;
let feed = parser::parse(resp.as_bytes()).unwrap();
let procfeed = {
let title = feed.title.unwrap();
let title_owned = title.content.to_owned();
let entries = feed.entries.iter().enumerate();
let mut processed_items = Vec::<String>::new();
for (j, e) in entries {
if j < num {
let e_title = e.title.as_ref().unwrap();
processed_items.push(format!(
"{} \n\t {}\n",
Style::new().italic().paint(e_title.content.clone()),
e.id
));
}
}
ProcessedFeed {
title: title_owned,
items: processed_items,
}
};
processed.push(procfeed);
}
rust_to_config(serde_json::to_string(&config_obj).unwrap().as_bytes());
Ok(processed)
}
pub async fn hem<'a>() -> Result<Vec<ProcessedFeed>, Box<dyn std::error::Error>> {
let config_path = find_config();
let mut processed: Vec<ProcessedFeed> = Vec::new();
let client = Client::new();
let config = match fs::read_to_string(&config_path) {
Ok(config) => config,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
eprintln!("Didn't find a .hemrc, creating it now...");
let mut configfile = fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&config_path)?;
configfile.write_all(r#"{"feeds": []}"#.as_bytes())?;
let mut bufreader = BufReader::new(configfile);
let mut contents = String::new();
bufreader.read_to_string(&mut contents)?;
contents
} else {
return Err(Box::from("Catastrophe!"));
}
}
};
let mut config_obj: ConfigObj = match serde_json::from_str(&config) {
Ok(config_obj) => config_obj,
Err(_) => {
return Err(Box::from(
"Your feeds list is empty! use `hem add` to add a feed.",
))
}
};
for i in 0..config_obj.feeds.len() {
let resp = client
.get(&config_obj.feeds[i].uri)
.send()
.await?
.text()
.await?;
let feed = parser::parse(resp.as_bytes()).unwrap();
let last_accessed = DateTime::from(
DateTime::parse_from_rfc3339(&config_obj.feeds[i].last_accessed).unwrap(),
);
let procfeed = {
let title = feed.title.unwrap();
let title_owned = title.content.to_owned();
let entries = feed.entries.iter().enumerate();
let mut processed_items = Vec::<String>::new();
let mut entry_date;
for (j, e) in entries {
if e.updated.is_none() {
entry_date = e.published.unwrap();
} else {
entry_date = e.updated.unwrap();
}
let entry_duration = last_accessed - entry_date; if j < 5 && entry_duration.num_seconds() < 0 {
let e_title = e.title.as_ref().unwrap();
processed_items.push(format!("{} \n\t {}\n", e_title.content.clone(), e.id));
}
}
if processed_items.len() == 0 {
processed_items.push(format!("Nothing new here..."));
}
ProcessedFeed {
title: title_owned,
items: processed_items,
}
};
processed.push(procfeed);
config_obj.feeds[i].last_accessed = Utc::now().to_rfc3339().to_owned();
}
rust_to_config(serde_json::to_string(&config_obj).unwrap().as_bytes());
Ok(processed)
}