rss-mux/src/main.rs

180 lines
5.3 KiB
Rust

#![feature(proc_macro_hygiene, decl_macro, try_trait)]
#[macro_use] extern crate rocket;
extern crate config;
extern crate reqwest;
extern crate rss;
extern crate roxmltree;
extern crate chrono;
use chrono::prelude::*;
use rss::{Channel, Item, ChannelBuilder};
use rocket::State;
struct Settings {
pinboard_api_token: String
}
#[derive(Debug)]
struct Bookmark {
href: String,
time: DateTime<Utc>,
description: String,
extended: String,
tag: String,
}
#[derive(Debug)]
enum TransformError {
Xml(roxmltree::Error),
Rss(rss::Error),
Http(reqwest::Error),
Str(String)
}
impl std::convert::From<roxmltree::Error> for TransformError {
fn from(e: roxmltree::Error) -> TransformError {
TransformError::Xml(e)
}
}
impl std::convert::From<reqwest::Error> for TransformError {
fn from(e: reqwest::Error) -> TransformError {
TransformError::Http(e)
}
}
impl std::convert::From<String> for TransformError {
fn from(e: String) -> TransformError {
TransformError::Str(e)
}
}
impl std::convert::From<&Bookmark> for Item {
fn from(b: &Bookmark) -> Item {
let mut item = Item::default();
item.set_title(b.description.clone());
item.set_link(b.href.clone());
item.set_description(format!("{}\n{}", b.extended.clone(), b.tag.clone()));
item.set_pub_date(b.time.to_rfc2822());
item
}
}
fn fetch_pinboard_bookmarks_rss(settings: &Settings) -> Result<String, TransformError> {
let mut response = reqwest::get(&format!("https://api.pinboard.in/v1/posts/all?auth_token={}", settings.pinboard_api_token))?;
let bookmarks = parse_bookmarks(&response.text()?)?;
let channel = transform_bookmarks_to_rss(&bookmarks)?;
Ok(channel.to_string())
}
fn fetch_pinboard_bookmarks_list(settings: &Settings) -> Result<String, TransformError> {
let mut response = reqwest::get(&format!("https://api.pinboard.in/v1/posts/all?auth_token={}", settings.pinboard_api_token))?;
let bookmarks = parse_bookmarks(&response.text()?)?;
Ok(bookmarks.iter()
.map(|b| format!("{:?}", b))
.collect::<Vec<String>>().join("<br/>\n"))
}
fn parse_bookmarks(bookmarks_xml: &String) -> Result<Vec<Bookmark>, TransformError> {
let doc = roxmltree::Document::parse(bookmarks_xml)?;
let mut users = doc.root().descendants();
// only care about one user (for now? forever?)
let posts = match users.next() {
None => return Err(TransformError::Str("missing user node".to_string())),
Some(p) => p
};
Ok(posts
.descendants()
.filter_map(|p| read_bookmark_node(p))
.filter_map(|b| filter_noisy_bookmarks(b))
.collect())
}
fn transform_bookmarks_to_rss(bookmarks: &Vec<Bookmark>) -> Result<rss::Channel, TransformError> {
let items: Vec<Item> = bookmarks.iter().map(|b| Item::from(b)).collect();
let mut channel = ChannelBuilder::default()
.title("Viv's Pinboard")
.description("Bookmarks from Viv's Pinboard.")
.build()?;
channel.set_items(items);
Ok(channel)
}
fn read_bookmark_node(node: roxmltree::Node) -> Option<Bookmark>{
Some(Bookmark {
href: String::from(node.attribute("href")?),
time: match node.attribute("time")?.parse::<DateTime<Utc>>(){
Ok(t) => t,
Err(_) => return None
},
description: String::from(node.attribute("description")?),
extended: String::from(node.attribute("extended")?),
tag: String::from(node.attribute("tag")?)
})
}
fn filter_noisy_bookmarks(bookmark: Bookmark) -> Option<Bookmark>{
// Filter out bookmarks caused simply by retweeting or faving.
if bookmark.description.starts_with("(429)") && bookmark.href.contains("twitter.com"){
return None;
}
if bookmark.description.eq("Twitter") && bookmark.href.contains("twitter.com"){
return None;
}
Some(bookmark)
}
#[get("/")]
fn index() -> &'static str {
"Hello world!"
}
#[get("/pinboard_items_list")]
fn pinboard_items_list(settings: State<Settings>) -> String {
match fetch_pinboard_bookmarks_list(&settings) {
Ok(rss) => rss,
Err(e) => match e {
TransformError::Xml(e) => format!("Error parsing XML: {}", e),
TransformError::Rss(e) => format!("Error creating RSS: {}", e),
TransformError::Http(e) => format!("Error during HTTP request: {}", e),
TransformError::Str(e) => format!("Error: {}", e)
}
}
}
#[get("/pinboard/rss.xml")]
fn pinboard_items_rss(settings: State<Settings>) -> String {
match fetch_pinboard_bookmarks_rss(&settings) {
Ok(rss) => rss,
Err(e) => match e {
TransformError::Xml(e) => format!("Error parsing XML: {}", e),
TransformError::Rss(e) => format!("Error creating RSS: {}", e),
TransformError::Http(e) => format!("Error during HTTP request: {}", e),
TransformError::Str(e) => format!("Error: {}", e)
}
}
}
fn main() {
let mut settings_store = config::Config::default();
settings_store
.merge(config::File::with_name("Settings")).unwrap();
let settings = Settings {
pinboard_api_token: settings_store.get_str("pinboard_api_token").unwrap()
};
rocket::ignite()
.mount("/", routes![index, pinboard_items_rss, pinboard_items_list])
.manage(settings)
.launch();
}