use reqwest::Url;
-use std::collections::HashMap;
+use std::collections::{HashMap, VecDeque};
use super::auth::{AuthConfig,AuthError};
use super::types::*;
+#[derive(Hash, PartialEq, Eq, Clone, Copy)]
+pub enum Boosts { Show, Hide }
+
+#[derive(Hash, PartialEq, Eq, Clone, Copy)]
+pub enum Replies { Show, Hide }
+
+#[derive(Hash, PartialEq, Eq, Clone)]
+pub enum FeedId {
+ Home,
+ Local,
+ Public,
+ Hashtag(String),
+ User(String, Boosts, Replies),
+}
+
+pub struct Feed {
+ ids: VecDeque<String>, // ids, whether of statuses, accounts or what
+ origin: isize,
+}
+
pub struct Client {
auth: AuthConfig,
client: reqwest::blocking::Client,
accounts: HashMap<String, Account>,
statuses: HashMap<String, Status>,
+ feeds: HashMap<FeedId, Feed>,
permit_write: bool,
}
}
}
+// Our own struct to collect the pieces of an HTTP request before we
+// pass it on to reqwests. Allows incremental adding of request parameters.
+struct Req {
+ method: reqwest::Method,
+ url_suffix: String,
+ parameters: Vec<(String, String)>,
+}
+
+impl Req {
+ fn get(url_suffix: &str) -> Self {
+ Req {
+ method: reqwest::Method::GET,
+ url_suffix: url_suffix.to_owned(),
+ parameters: Vec::new(),
+ }
+ }
+
+ fn post(url_suffix: &str) -> Self {
+ Req {
+ method: reqwest::Method::POST,
+ url_suffix: url_suffix.to_owned(),
+ parameters: Vec::new(),
+ }
+ }
+
+ fn param<T>(mut self, key: &str, value: T) -> Self
+ where T: ReqParam
+ {
+ self.parameters.push((key.to_owned(), value.param_value()));
+ self
+ }
+}
+
+trait ReqParam {
+ fn param_value(self) -> String;
+}
+
+impl ReqParam for &str {
+ fn param_value(self) -> String { self.to_owned() }
+}
+impl ReqParam for String {
+ fn param_value(self) -> String { self }
+}
+impl ReqParam for &String {
+ fn param_value(self) -> String { self.clone() }
+}
+impl ReqParam for bool {
+ fn param_value(self) -> String {
+ match self {
+ false => "false",
+ true => "true",
+ }.to_owned()
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum FeedExtend {
+ Initial, Past, Future
+}
+
impl Client {
pub fn new() -> Result<Self, AuthError> {
Ok(Client {
client: reqwest::blocking::Client::new(),
accounts: HashMap::new(),
statuses: HashMap::new(),
+ feeds: HashMap::new(),
permit_write: false,
})
}
self.permit_write = permit;
}
- fn api_request(&self, method: reqwest::Method, url_suffix: &str) ->
+ fn api_request(&self, req: Req) ->
Result<(String, reqwest::blocking::RequestBuilder), ClientError>
{
- if method != reqwest::Method::GET && !self.permit_write {
+ if req.method != reqwest::Method::GET && !self.permit_write {
return Err(ClientError::InternalError(
"Non-GET request attempted in readonly mode".to_string()));
}
- let urlstr = self.auth.instance_url.clone() + "/api/v1/" + url_suffix;
- let url = match Url::parse(&urlstr) {
+ let urlstr = self.auth.instance_url.clone() + "/api/v1/" +
+ &req.url_suffix;
+ let url = match Url::parse_with_params(&urlstr, req.parameters.iter()) {
Ok(url) => Ok(url),
Err(e) => Err(ClientError::UrlParseError(
urlstr.clone(), e.to_string())),
}?;
- Ok((urlstr, self.client.request(method, url)
+ Ok((urlstr, self.client.request(req.method, url)
.bearer_auth(&self.auth.user_token)))
}
return Ok(st.clone());
}
- let (url, req) = self.api_request(reqwest::Method::GET,
- &("accounts/".to_owned() + id))?;
+ let (url, req) = self.api_request(Req::get(
+ &("accounts/".to_owned() + id)))?;
let body = req.send()?.text()?;
let ac: Account = match serde_json::from_str(&body) {
Ok(ac) => Ok(ac),
url.clone(), format!("request returned wrong account id {}",
&ac.id)));
}
- self.accounts.insert(id.to_string(), ac.clone());
+ self.cache_account(&ac);
Ok(ac)
}
return Ok(st);
}
- let (url, req) = self.api_request(reqwest::Method::GET,
- &("statuses/".to_owned() + id))?;
+ let (url, req) = self.api_request(Req::get(
+ &("statuses/".to_owned() + id)))?;
let body = req.send()?.text()?;
let st: Status = match serde_json::from_str(&body) {
Ok(st) => Ok(st),
url.clone(), format!("request returned wrong status id {}",
&st.id)));
}
- self.accounts.insert(id.to_string(), st.account.clone());
- self.statuses.insert(id.to_string(), st.clone());
+ self.cache_status(&st);
Ok(st)
}
-}
+ pub fn fetch_feed(&mut self, id: FeedId, ext: FeedExtend) ->
+ Result<(), ClientError>
+ {
+ if ext == FeedExtend::Initial {
+ if self.feeds.contains_key(&id) {
+ // No need to fetch the initial contents - we already have some
+ return Ok(());
+ }
+ } else {
+ assert!(self.feeds.contains_key(&id),
+ "Shouldn't be extending a feed we've never fetched")
+ }
+
+ let req = match id {
+ FeedId::Home => Req::get("timelines/home"),
+ FeedId::Local => {
+ Req::get("timelines/public").param("local", true)
+ },
+ FeedId::Public => {
+ Req::get("timelines/public").param("local", false)
+ },
+ FeedId::Hashtag(ref tag) => {
+ Req::get(&format!("timelines/tag/{}", &tag))
+ },
+ FeedId::User(ref id, boosts, replies) => {
+ Req::get(&format!("accounts/{}/statuses", id))
+ .param("exclude_reblogs", boosts == Boosts::Hide)
+ .param("exclude_replies", replies == Replies::Hide)
+ },
+ };
+
+ let req = match ext {
+ FeedExtend::Initial => req,
+ FeedExtend::Past => if let Some(ref feed) = self.feeds.get(&id) {
+ match feed.ids.front() {
+ None => req,
+ Some(id) => req.param("max_id", id),
+ }
+ } else { req },
+ FeedExtend::Future => if let Some(ref feed) = self.feeds.get(&id) {
+ match feed.ids.back() {
+ None => req,
+ Some(id) => req.param("min_id", id),
+ }
+ } else { req },
+ };
+
+ let (url, req) = self.api_request(req)?;
+ let body = req.send()?.text()?;
+ let sts: Vec<Status> = match serde_json::from_str(&body) {
+ Ok(sts) => Ok(sts),
+ Err(e) => Err(ClientError::UrlError(url.clone(), e.to_string())),
+ }?;
+ for st in &sts {
+ self.cache_status(st);
+ }
+ let ids = sts.iter().rev().map(|st| st.id.clone()).collect();
+ match ext {
+ FeedExtend::Initial => {
+ self.feeds.insert(id, Feed {
+ ids: ids,
+ origin: 0,
+ });
+ },
+ FeedExtend::Future => {
+ let feed = self.feeds.get_mut(&id).unwrap();
+ for id in ids.iter() {
+ feed.ids.push_back(id.to_string());
+ }
+ },
+ FeedExtend::Past => {
+ let feed = self.feeds.get_mut(&id).unwrap();
+ for id in ids.iter().rev() {
+ feed.ids.push_front(id.to_string());
+ feed.origin += 1;
+ }
+ },
+ }
+
+ Ok(())
+ }
+
+ pub fn borrow_feed(&self, id: FeedId) -> &Feed {
+ self.feeds.get(&id).expect(
+ "should only ever borrow feeds that have been fetched")
+ }
+}