#![deny(warnings)]
#[macro_use]
extern crate log;
#[cfg(feature = "with-serde")]
use serde::{Deserialize, Serialize};
use chrono::DateTime;
use futures::future;
use hyper::{
rt::{Future, Stream},
Client, Request,
};
#[derive(Debug)]
pub enum Error {
Hyper(hyper::Error),
MissingToken,
MissingUrl(String),
ServerError(String),
}
pub type Result<T> = ::std::result::Result<T, Error>;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
pub struct Archived {
pub target_url: String,
pub archived_url: String,
pub time_stamp: Option<DateTime<chrono::Utc>>,
pub submit_token: String,
}
pub struct ArchiveClient {
client: Client<hyper::client::HttpConnector, hyper::Body>,
user_agent: String,
}
impl ArchiveClient {
pub fn new<T: Into<String>>(user_agent: T) -> Self {
ArchiveClient {
client: Client::new(),
user_agent: user_agent.into(),
}
}
pub fn capture_all<T: Into<String> + 'static + Send>(
self,
links: Vec<T>,
) -> impl Future<Item = (Self, Vec<Result<Archived>>), Error = Error> + Send {
self.get_unique_token().and_then(move |token| {
let mut futures = Vec::with_capacity(links.len());
for url in links {
futures.push(self.capture_with_token(url.into(), token.clone()).then(Ok));
}
future::join_all(futures).and_then(|archives| Ok((self, archives)))
})
}
pub fn capture<T: Into<String> + 'static + Send>(
self,
url: T,
) -> impl Future<Item = (Self, Result<Archived>), Error = Error> + Send {
self.get_unique_token().then(move |token| {
let capture: Box<dyn Future<Item = (Self, Result<Archived>), Error = Error> + Send> =
if let Ok(token) = token {
Box::new(
self.capture_with_token(url.into(), token)
.then(move |archive| Ok((self, archive))),
)
} else {
error!("Failed to capture token while archiving {}", url.into());
Box::new(future::ok((self, Err(Error::MissingToken))))
};
capture
})
}
pub fn capture_with_token<T: Into<String> + Send + 'static>(
&self,
url: T,
submit_token: T,
) -> impl Future<Item = Archived, Error = Error> + Send {
use chrono::TimeZone;
let target_url = url.into();
let submit_token = submit_token.into();
let body: String = url::form_urlencoded::Serializer::new(String::new())
.append_pair("url", &target_url)
.append_pair("anyway", "1")
.append_pair("submitid", &submit_token)
.finish();
let req = Request::post("http://archive.is/submit/")
.header("User-Agent", self.user_agent.as_str())
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body.into())
.unwrap();
self.client
.request(req)
.map_err(Error::Hyper)
.and_then(move |resp| {
let archived_url = resp.headers().get("Refresh").and_then(|x| {
x.to_str()
.ok()
.and_then(|x| x.split('=').nth(1).map(str::to_owned))
});
let archive: Box<dyn Future<Item = Archived, Error = Error> + Send> =
if let Some(archived_url) = archived_url {
let time_stamp = resp.headers().get("Date").and_then(|x| {
x.to_str().ok().and_then(|x| {
chrono::Utc.datetime_from_str(x, "%a, %e %b %Y %T GMT").ok()
})
});
let archived = Archived {
target_url,
archived_url,
time_stamp,
submit_token,
};
debug!(
"Archived target url {} at {}",
archived.target_url, archived.archived_url
);
Box::new(future::ok(archived))
} else {
let err_resp_handling = resp
.into_body()
.concat2()
.map_err(Error::Hyper)
.and_then(move |ch| {
if let Ok(html) = ::std::str::from_utf8(&ch) {
if html.starts_with("<h1>Server Error</h1>") {
error!("Server Error while archiving {}", target_url);
return Err(Error::ServerError(target_url));
}
let archived_url = html
.splitn(2, "<meta property=\"og:url\"")
.nth(1)
.and_then(|x| {
x.splitn(2, "content=\"").nth(1).and_then(|id| {
id.splitn(2, '\"').next().map(str::to_owned)
})
});
if let Some(archived_url) = archived_url {
let archived = Archived {
target_url,
archived_url,
time_stamp: None,
submit_token,
};
debug!(
"Archived target url {} at {}",
archived.target_url, archived.archived_url
);
return Ok(archived);
}
}
error!("Failed to archive {}", target_url);
Err(Error::MissingUrl(target_url))
});
Box::new(err_resp_handling)
};
archive
})
}
pub fn get_unique_token(&self) -> impl Future<Item = String, Error = Error> + Send {
let req = Request::get("http://archive.is/")
.header("User-Agent", self.user_agent.as_str())
.body(hyper::Body::empty())
.unwrap();
self.client
.request(req)
.map_err(Error::Hyper)
.and_then(|res| {
res.into_body()
.concat2()
.map_err(Error::Hyper)
.and_then(|ch| {
::std::str::from_utf8(&ch)
.map_err(|_| Error::MissingToken)
.and_then(|html| {
html.rsplitn(2, "name=\"submitid")
.next()
.and_then(|x| {
x.splitn(2, "value=\"").nth(1).and_then(|token| {
token.splitn(2, '\"').next().map(str::to_string)
})
})
.ok_or(Error::MissingToken)
})
})
})
}
}
impl Default for ArchiveClient {
fn default() -> Self {
ArchiveClient::new( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36")
}
}
#[cfg(test)]
mod tests {
#[test]
fn extract_unique_token() {
let html = r###"type="hidden" name="submitid" value="1yPA39C6QcM84Dzspl+7s28rrAFOnliPMCiJtoP+OlTKmd5kJd21G4ucgTkx0mnZ"/>"###;
let split = html
.rsplitn(2, "name=\"submitid")
.filter_map(|x| {
x.splitn(2, "value=\"")
.skip(1)
.filter_map(|token| token.splitn(2, '\"').next())
.next()
})
.next();
assert_eq!(
Some("1yPA39C6QcM84Dzspl+7s28rrAFOnliPMCiJtoP+OlTKmd5kJd21G4ucgTkx0mnZ"),
split
);
}
}