use super::{HttpSnafu, MalformedTokenSnafu, Result};
use chrono::{DateTime, Utc};
use reqwest::{IntoUrl, Url};
use serde::Deserialize;
use serde::{de, de::Visitor, Deserializer};
use snafu::ResultExt;
#[derive(Clone, Debug)]
pub struct Client {
base_url: Url,
client: reqwest::blocking::Client,
token: String,
}
#[derive(Deserialize, Debug)]
struct Bookmark {
pub href: String,
pub description: String,
pub extended: String,
#[serde(deserialize_with = "from_space_separated_string")]
pub tags: Vec<String>,
pub time: DateTime<Utc>,
pub shared: String,
pub toread: String,
pub meta: String,
pub hash: String,
}
fn from_space_separated_string<'de, D>(
deserializer: D,
) -> std::result::Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrVector;
impl<'de> Visitor<'de> for StringOrVector {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or space-separated string")
}
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut vec = Vec::new();
while let Some(s) = seq.next_element()? {
vec.push(s);
}
Ok(vec)
}
fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(s.split(' ')
.filter(|&tag| !tag.is_empty())
.map(|tag| tag.to_owned())
.collect())
}
}
deserializer.deserialize_any(StringOrVector)
}
impl Client {
pub fn new<U: IntoUrl>(base_url: U, token: &str) -> Result<Client> {
Ok(Client {
base_url: base_url.into_url().context(HttpSnafu {})?,
client: reqwest::blocking::Client::builder()
.user_agent("pingrep")
.build()
.context(HttpSnafu {})?,
token: token.to_string(),
})
}
pub fn user(&self) -> Result<String> {
match self.token.split(':').next() {
Some(user) => Ok(user.to_string()),
None => MalformedTokenSnafu {
reason: "Missing ':' in token",
}
.fail(),
}
}
pub fn last_update(&self) -> Result<DateTime<Utc>> {
#[derive(Deserialize)]
struct LastUpdateResponse {
#[serde(rename = "update_time")]
pub last_updated: DateTime<Utc>,
}
let url = self
.base_url
.join("posts/update")
.expect("bad url in last_update");
let params = [("auth_token", self.token.as_str()), ("format", "json")];
let response = self
.client
.get(url)
.query(¶ms)
.send()
.context(HttpSnafu {})?;
response.error_for_status_ref().context(HttpSnafu {})?;
Ok(response
.json::<LastUpdateResponse>()
.context(HttpSnafu {})?
.last_updated)
}
pub fn bookmarks(&self) -> Result<Vec<super::Bookmark>> {
let url = self
.base_url
.join("posts/all")
.expect("bad url in last_update");
let params = [("auth_token", self.token.as_str()), ("format", "json")];
let response = self
.client
.get(url)
.query(¶ms)
.send()
.context(HttpSnafu {})?;
response.error_for_status_ref().context(HttpSnafu {})?;
let bookmarks = response.json::<Vec<Bookmark>>().context(HttpSnafu {})?;
Ok(bookmarks
.into_iter()
.map(|b| super::Bookmark {
url: b.href,
description: b.extended,
title: b.description,
tags: b.tags,
time: b.time,
shared: b.shared == "yes",
toread: b.toread == "yes",
meta: b.meta,
hash: b.hash,
})
.collect())
}
}