180 lines
5.3 KiB
Rust
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();
|
|
} |