From 7d7e940ae850cc432f8bd72168b528d9c5425ccb Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Fri, 29 Dec 2023 10:54:00 +0000 Subject: [PATCH] Untested code to fetch feeds of items. --- src/client.rs | 196 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 182 insertions(+), 14 deletions(-) diff --git a/src/client.rs b/src/client.rs index e567edc..c5c9e62 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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, // ids, whether of statuses, accounts or what + origin: isize, +} + pub struct Client { auth: AuthConfig, client: reqwest::blocking::Client, accounts: HashMap, statuses: HashMap, + feeds: HashMap, permit_write: bool, } @@ -29,6 +50,66 @@ impl From 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(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 { 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 = 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") + } +} -- 2.30.2