chiark / gitweb /
Untested code to fetch feeds of items.
authorSimon Tatham <anakin@pobox.com>
Fri, 29 Dec 2023 10:54:00 +0000 (10:54 +0000)
committerSimon Tatham <anakin@pobox.com>
Fri, 29 Dec 2023 10:54:00 +0000 (10:54 +0000)
src/client.rs

index e567edc1c79651f965e43b4fc3b723196e3dd1d2..c5c9e62c61b0b83491353edb380821d37c7b2704 100644 (file)
@@ -1,14 +1,35 @@
 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,
 }
 
@@ -29,6 +50,66 @@ impl From<reqwest::Error> for ClientError {
     }
 }
 
+// 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 {
@@ -36,6 +117,7 @@ impl Client {
             client: reqwest::blocking::Client::new(),
             accounts: HashMap::new(),
             statuses: HashMap::new(),
+            feeds: HashMap::new(),
             permit_write: false,
         })
     }
@@ -44,22 +126,23 @@ impl Client {
         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)))
     }
 
@@ -77,8 +160,8 @@ impl Client {
             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),
@@ -89,7 +172,7 @@ impl Client {
                 url.clone(), format!("request returned wrong account id {}",
                                      &ac.id)));
         }
-        self.accounts.insert(id.to_string(), ac.clone());
+        self.cache_account(&ac);
         Ok(ac)
     }
 
@@ -104,8 +187,8 @@ impl Client {
             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),
@@ -116,9 +199,94 @@ impl Client {
                 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")
+    }
+}