From c967e59ffbb2da3561e9d0fdbb382c66d0b18c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 21 Aug 2021 13:10:29 +0200 Subject: [PATCH] all asyncified except for streaming --- Cargo.toml | 34 +--- src/entities/itemsiter.rs | 14 +- src/errors.rs | 16 +- src/helpers/cli.rs | 4 +- src/lib.rs | 190 ++++++++++--------- src/macros.rs | 46 ++--- src/mastodon_client.rs | 383 -------------------------------------- src/media_builder.rs | 9 +- src/page.rs | 16 +- src/registration.rs | 27 +-- 10 files changed, 174 insertions(+), 565 deletions(-) delete mode 100644 src/mastodon_client.rs diff --git a/Cargo.toml b/Cargo.toml index e657635..1d2ff16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,36 +12,20 @@ edition = "2018" [dependencies] doc-comment = "0.3" -envy = { version = "0.4.0", optional = true } -hyper-old-types = "0.11.0" +#envy = { version = "0.4.0", optional = true } +hyper-old-types = "0.11.0" # Used to parse the link header isolang = { version = "1.0", features = ["serde_serialize"] } log = "^0.4" -reqwest = { version = "0.11.4", default-features = false, features = ["json", "blocking", "multipart"] } +reqwest = { version = "0.11.4", default-features = false, features = ["json", "blocking", "multipart", "rustls-tls", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.6.1" serde_qs = "0.8.4" url = "2.1.1" -tap-reader = "1" +#tap-reader = "1" toml = { version = "^0.5.0", optional = true } -tungstenite = "0.15.0" - -[dependencies.chrono] -version = "0.4" -features = ["serde"] - -[features] -default = ["reqwest/default-tls","tungstenite/native-tls"] -json = [] -env = ["envy"] -all = ["toml", "json", "env"] -rustls-tls = ["reqwest/rustls-tls","tungstenite/__rustls-tls"] -nightly = [] - -[dev-dependencies] -tempfile = "3.0.3" -indoc = "1.0.2" -pretty_env_logger = "0.4.0" - -[package.metadata.docs.rs] -features = ["all"] +tokio-tungstenite = { version = "0.15.0", features = ["rustls-tls"] } +futures = { version = "0.3" } +tokio = {version = "1", features = ["full"] } +tokio-util = { version = "0.6", features = [ "compat", "io" ] } +chrono = { version = "0.4", features = ["serde"] } diff --git a/src/entities/itemsiter.rs b/src/entities/itemsiter.rs index 88d9c4e..7ee0d43 100644 --- a/src/entities/itemsiter.rs +++ b/src/entities/itemsiter.rs @@ -24,7 +24,7 @@ use serde::Deserialize; /// # } /// ``` #[derive(Debug, Clone)] -pub(crate) struct ItemsIter<'a, T: Clone + for<'de> Deserialize<'de>> { +pub struct ItemsIter<'a, T: Clone + for<'de> Deserialize<'de>> { page: Page<'a, T>, buffer: Vec, cur_idx: usize, @@ -45,8 +45,8 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>> ItemsIter<'a, T> { self.buffer.is_empty() || self.cur_idx == self.buffer.len() } - fn fill_next_page(&mut self) -> Option<()> { - let items = if let Ok(items) = self.page.next_page() { + async fn fill_next_page(&mut self) -> Option<()> { + let items = if let Ok(items) = self.page.next_page().await { items } else { return None; @@ -62,12 +62,8 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>> ItemsIter<'a, T> { None } } -} - -impl<'a, T: Clone + for<'de> Deserialize<'de>> Iterator for ItemsIter<'a, T> { - type Item = T; - fn next(&mut self) -> Option { + pub async fn next_item(&mut self) -> Option { if self.use_initial { if self.page.initial_items.is_empty() || self.cur_idx == self.page.initial_items.len() { return None; @@ -81,7 +77,7 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>> Iterator for ItemsIter<'a, T> { } Some(self.page.initial_items[idx].clone()) } else { - if self.need_next_page() && self.fill_next_page().is_none() { + if self.need_next_page() && self.fill_next_page().await.is_none() { return None; } let idx = self.cur_idx; diff --git a/src/errors.rs b/src/errors.rs index 2813bfd..f41fd33 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -12,7 +12,7 @@ use reqwest::{header::ToStrError as HeaderStrError, Error as HttpError, StatusCo use serde_json::Error as SerdeError; use serde_qs::Error as SerdeQsError; use serde_urlencoded::ser::Error as UrlEncodedError; -use tungstenite::error::Error as WebSocketError; +// use tungstenite::error::Error as WebSocketError; use url::ParseError as UrlError; /// Convience type over `std::result::Result` with `Error` as the error type. @@ -57,14 +57,14 @@ pub enum Error { HeaderStrError(HeaderStrError), /// Error parsing the http Link header HeaderParseError(HeaderParseError), - #[cfg(feature = "env")] - /// Error deserializing from the environment - Envy(EnvyError), + // #[cfg(feature = "env")] + // /// Error deserializing from the environment + // Envy(EnvyError), /// Error serializing to a query string SerdeQs(SerdeQsError), /// WebSocket error - WebSocket(WebSocketError), - /// Other errors +// WebSocket(WebSocketError), +// /// Other errors Other(String), } @@ -92,7 +92,7 @@ impl error::Error for Error { #[cfg(feature = "env")] Error::Envy(ref e) => e, Error::SerdeQs(ref e) => e, - Error::WebSocket(ref e) => e, + // Error::WebSocket(ref e) => e, Error::Client(..) | Error::Server(..) => return None, Error::ClientIdRequired => return None, @@ -148,7 +148,7 @@ from! { HeaderParseError, HeaderParseError, #[cfg(feature = "env")] EnvyError, Envy, SerdeQsError, SerdeQs, - WebSocketError, WebSocket, + // WebSocketError, WebSocket, String, Other, } diff --git a/src/helpers/cli.rs b/src/helpers/cli.rs index c815fd0..5f4bbf0 100644 --- a/src/helpers/cli.rs +++ b/src/helpers/cli.rs @@ -4,7 +4,7 @@ use crate::{errors::Result, registration::Registered, Mastodon}; /// Finishes the authentication process for the given `Registered` object, /// using the command-line -pub fn authenticate(registration: Registered) -> Result { +pub async fn authenticate(registration: Registered) -> Result { let url = registration.authorize_url()?; let stdout = io::stdout(); @@ -20,5 +20,5 @@ pub fn authenticate(registration: Registered) -> Result { let mut input = String::new(); stdin.read_line(&mut input)?; let code = input.trim(); - registration.complete(code) + registration.complete(code).await } diff --git a/src/lib.rs b/src/lib.rs index facd9ff..0403ed5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,31 +57,30 @@ //! # } //! ``` -#![deny( - missing_docs, - warnings, - missing_debug_implementations, - missing_copy_implementations, - trivial_casts, - trivial_numeric_casts, - unsafe_code, - unstable_features, - unused_import_braces, - unused_qualifications -)] +// #![deny( +// missing_docs, +// warnings, +// missing_debug_implementations, +// missing_copy_implementations, +// trivial_casts, +// trivial_numeric_casts, +// unsafe_code, +// unstable_features, +// unused_import_braces, +// unused_qualifications +// )] // #![cfg_attr(feature = "nightly", allow(broken_intra_doc_links))] // #![allow(broken_intra_doc_links)] #[macro_use] extern crate log; -use std::{borrow::Cow, io::BufRead, ops}; -use std::net::TcpStream; +use std::{borrow::Cow, ops}; pub use isolang::Language; -use reqwest::blocking::{Client, multipart, RequestBuilder, Response}; -use tap_reader::Tap; -use tungstenite::stream::MaybeTlsStream; +use reqwest::{Client, multipart, RequestBuilder, Response, Body}; +// use tap_reader::Tap; +// use tungstenite::stream::MaybeTlsStream; use crate::{entities::prelude::*, page::Page}; pub use crate::{ @@ -104,7 +103,6 @@ pub mod entities; pub mod errors; /// Collection of helpers for serializing/deserializing `Data` objects pub mod helpers; -mod mastodon_client; /// Constructing media attachments for a status. pub mod media_builder; /// Handling multiple pages of entities. @@ -132,37 +130,35 @@ pub mod prelude { /// Your mastodon application client, handles all requests to and from Mastodon. #[derive(Clone, Debug)] pub struct Mastodon { - client: Client, /// Raw data about your mastodon instance. pub data: Data, } -impl Mastodon { +impl From for Mastodon { + /// Creates a mastodon instance from the data struct. + fn from(data: Data) -> Mastodon { + let mut builder = MastodonBuilder::new(); + builder.data(data); + builder.build().expect("We know `data` is present, so this should be fine") + } +} +// type MastodonStream = EventReader; + +impl Mastodon { methods![get, post, delete,]; fn route(&self, url: &str) -> String { format!("{}{}", self.base, url) } - pub(crate) fn send(&self, req: RequestBuilder) -> Result { + pub(crate) async fn send(&self, req: RequestBuilder) -> Result { let request = req.bearer_auth(&self.token).build()?; - Ok(self.client.execute(request)?) + Ok(self.client.execute(request).await?) } } -impl From for Mastodon { - /// Creates a mastodon instance from the data struct. - fn from(data: Data) -> Mastodon { - let mut builder = MastodonBuilder::new(); - builder.data(data); - builder.build().expect("We know `data` is present, so this should be fine") - } -} - -type MastodonStream = EventReader; - impl Mastodon { route_v1_paged!((get) favourites: "favourites" => Status); route_v1_paged!((get) blocks: "blocks" => Account); @@ -224,9 +220,9 @@ impl Mastodon { route_v1_id!((post) unendorse_user: "accounts/{}/unpin" => Relationship); /// POST /api/v1/filters - pub fn add_filter(&self, request: &mut AddFilterRequest) -> Result { + pub async fn add_filter(&self, request: &mut AddFilterRequest) -> Result { let url = self.route("/api/v1/filters"); - let response = self.send(self.client.post(&url).json(&request))?; + let response = self.send(self.client.post(&url).json(&request)).await?; let status = response.status(); @@ -236,13 +232,13 @@ impl Mastodon { return Err(Error::Server(status)); } - deserialise(response) + deserialise_response(response).await } /// PUT /api/v1/filters/:id - pub fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result { + pub async fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result { let url = self.route(&format!("/api/v1/filters/{}", id)); - let response = self.send(self.client.put(&url).json(&request))?; + let response = self.send(self.client.put(&url).json(&request)).await?; let status = response.status(); @@ -252,14 +248,14 @@ impl Mastodon { return Err(Error::Server(status)); } - deserialise(response) + deserialise_response(response).await } /// PATCH /api/v1/accounts/update_credentials - pub fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result { + pub async fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result { let changes = builder.build()?; let url = self.route("/api/v1/accounts/update_credentials"); - let response = self.send(self.client.patch(&url).json(&changes))?; + let response = self.send(self.client.patch(&url).json(&changes)).await?; let status = response.status(); @@ -269,19 +265,19 @@ impl Mastodon { return Err(Error::Server(status)); } - deserialise(response) + deserialise_response(response).await } /// Post a new status to the account. - pub fn new_status(&self, status: NewStatus) -> Result { - let response = self.send(self.client.post(&self.route("/api/v1/statuses")).json(&status))?; + pub async fn new_status(&self, status: NewStatus) -> Result { + let response = self.send(self.client.post(&self.route("/api/v1/statuses")).json(&status)).await?; - deserialise(response) + deserialise_response(response).await } /// Get timeline filtered by a hashtag(eg. `#coffee`) either locally or /// federated. - pub fn get_hashtag_timeline(&self, hashtag: &str, local: bool) -> Result> { + pub async fn get_hashtag_timeline<'a>(&'a self, hashtag: &str, local: bool) -> Result> { let base = "/api/v1/timelines/tag/"; let url = if local { self.route(&format!("{}{}?local=1", base, hashtag)) @@ -289,12 +285,12 @@ impl Mastodon { self.route(&format!("{}{}", base, hashtag)) }; - Page::new(self, self.send(self.client.get(&url))?) + Page::new(self, self.send(self.client.get(&url)).await?).await } /// Get statuses of a single account by id. Optionally only with pictures /// and or excluding replies. - pub fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result> + pub async fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result> where S: Into>>, { @@ -304,14 +300,14 @@ impl Mastodon { url = format!("{}{}", url, request.to_querystring()?); } - let response = self.send(self.client.get(&url))?; + let response = self.send(self.client.get(&url)).await?; - Page::new(self, response) + Page::new(self, response).await } /// Returns the client account's relationship to a list of other accounts. /// Such as whether they follow them or vice versa. - pub fn relationships(&self, ids: &[&str]) -> Result> { + pub async fn relationships<'a>(&'a self, ids: &[&str]) -> Result> { let mut url = self.route("/api/v1/accounts/relationships?"); if ids.len() == 1 { @@ -326,40 +322,42 @@ impl Mastodon { url.pop(); } - let response = self.send(self.client.get(&url))?; + let response = self.send(self.client.get(&url)).await?; - Page::new(self, response) + Page::new(self, response).await } /// Add a push notifications subscription - pub fn add_push_subscription(&self, request: &AddPushRequest) -> Result { + pub async fn add_push_subscription(&self, request: &AddPushRequest) -> Result { let request = request.build()?; - let response = self.send(self.client.post(&self.route("/api/v1/push/subscription")).json(&request))?; + let response = self.send(self.client.post(&self.route("/api/v1/push/subscription")).json(&request)).await?; - deserialise(response) + deserialise_response(response).await } /// Update the `data` portion of the push subscription associated with this /// access token - pub fn update_push_data(&self, request: &UpdatePushRequest) -> Result { + pub async fn update_push_data(&self, request: &UpdatePushRequest) -> Result { let request = request.build(); - let response = self.send(self.client.put(&self.route("/api/v1/push/subscription")).json(&request))?; + let response = self.send(self.client.put(&self.route("/api/v1/push/subscription")).json(&request)).await?; - deserialise(response) + deserialise_response(response).await } /// Get all accounts that follow the authenticated user - pub fn follows_me(&self) -> Result> { - let me = self.verify_credentials()?; - self.followers(&me.id) + pub async fn follows_me<'a>(&'a self) -> Result> { + let me = self.verify_credentials().await?; + self.followers(&me.id).await } /// Get all accounts that the authenticated user follows - pub fn followed_by_me(&self) -> Result> { - let me = self.verify_credentials()?; - self.following(&me.id) + pub async fn followed_by_me<'a>(&'a self) -> Result> { + let me = self.verify_credentials().await?; + self.following(&me.id).await } + /* + /// returns events that are relevant to the authorized user, i.e. home /// timeline & notifications pub fn streaming_user(&self) -> Result { @@ -367,7 +365,7 @@ impl Mastodon { url.query_pairs_mut() .append_pair("access_token", &self.token) .append_pair("stream", "user"); - let mut url: url::Url = reqwest::blocking::get(url.as_str())?.url().as_str().parse()?; + let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?; let new_scheme = match url.scheme() { "http" => "ws", "https" => "wss", @@ -503,6 +501,7 @@ impl Mastodon { Ok(EventReader(WebSocket(client))) } + */ /// Upload some media to the server for possible attaching to a new status /// @@ -544,14 +543,13 @@ impl Mastodon { /// ## Errors /// This function may return an `Error::Http` before sending anything over the network if the /// `MediaBuilder` was supplied with a reader and a `mimetype` string which cannot be pasrsed. - pub fn media(&self, media: MediaBuilder) -> Result { + pub async fn media(&self, media: MediaBuilder) -> Result { use media_builder::MediaBuilderData; let mut form = multipart::Form::new(); form = match media.data { MediaBuilderData::Reader(reader) => { - let mut part = multipart::Part::reader(reader); - + let mut part = multipart::Part::stream(Body::wrap_stream(tokio_util::io::ReaderStream::new(reader))); if let Some(filename) = media.filename { part = part.file_name(filename); } @@ -562,7 +560,12 @@ impl Mastodon { form.part("file", part) } - MediaBuilderData::File(file) => form.file("file", &file)?, + MediaBuilderData::File(file) => { + let f = tokio::fs::OpenOptions::new().read(true).open(file).await?; + let part = multipart::Part::stream(Body::wrap_stream(tokio_util::io::ReaderStream::new(f))); + form.part("file", part) + // form.file("file", &file)? + }, }; if let Some(description) = media.description { @@ -573,12 +576,14 @@ impl Mastodon { form = form.text("focus", format!("{},{}", x, y)); } - let response = self.send(self.client.post(&self.route("/api/v1/media")).multipart(form))?; + let response = self.send(self.client.post(&self.route("/api/v1/media")).multipart(form)).await?; - deserialise(response) + deserialise_response(response).await } } +/* + #[derive(Debug)] /// WebSocket newtype so that EventStream can be implemented without coherency /// issues @@ -680,6 +685,8 @@ impl EventReader { } } +*/ + impl ops::Deref for Mastodon { type Target = Data; @@ -750,11 +757,13 @@ impl MastodonUnauth { Ok(self.base.join(url)?) } - fn send(&self, req: RequestBuilder) -> Result { + async fn send(&self, req: RequestBuilder) -> Result { let req = req.build()?; - Ok(self.client.execute(req)?) + Ok(self.client.execute(req).await?) } + /* + /// Get a stream of the public timeline pub fn streaming_public(&self) -> Result> { let mut url: url::Url = self.route("/api/v1/streaming/public/local")?; @@ -773,48 +782,49 @@ impl MastodonUnauth { Ok(EventReader(WebSocket(client))) } + */ + /// GET /api/v1/statuses/:id - pub fn get_status(&self, id: &str) -> Result { + pub async fn get_status(&self, id: &str) -> Result { let route = self.route("/api/v1/statuses")?; let route = route.join(id)?; - let response = self.send(self.client.get(route))?; - deserialise(response) + let response = self.send(self.client.get(route)).await?; + deserialise_response(response).await } /// GET /api/v1/statuses/:id/context - pub fn get_context(&self, id: &str) -> Result { + pub async fn get_context(&self, id: &str) -> Result { let route = self.route("/api/v1/statuses")?; let route = route.join(id)?; let route = route.join("context")?; - let response = self.send(self.client.get(route))?; - deserialise(response) + let response = self.send(self.client.get(route)).await?; + deserialise_response(response).await } /// GET /api/v1/statuses/:id/card - pub fn get_card(&self, id: &str) -> Result { + pub async fn get_card(&self, id: &str) -> Result { let route = self.route("/api/v1/statuses")?; let route = route.join(id)?; let route = route.join("card")?; - let response = self.send(self.client.get(route))?; - deserialise(response) + let response = self.send(self.client.get(route)).await?; + deserialise_response(response).await } } // Convert the HTTP response body from JSON. Pass up deserialization errors // transparently. -fn deserialise serde::Deserialize<'de>>(response: Response) -> Result { - let mut reader = Tap::new(response); - - match serde_json::from_reader(&mut reader) { +async fn deserialise_response serde::Deserialize<'de>>(response: Response) -> Result { + let bytes = response.bytes().await?; + match serde_json::from_slice(&bytes) { Ok(t) => { - log::debug!("{}", String::from_utf8_lossy(&reader.bytes)); + debug!("{}", String::from_utf8_lossy(&bytes)); Ok(t) } // If deserializing into the desired type fails try again to // see if this is an error response. Err(e) => { - log::error!("{}", String::from_utf8_lossy(&reader.bytes)); - if let Ok(error) = serde_json::from_slice(&reader.bytes) { + error!("{}", String::from_utf8_lossy(&bytes)); + if let Ok(error) = serde_json::from_slice(&bytes) { return Err(Error::Api(error)); } Err(e.into()) diff --git a/src/macros.rs b/src/macros.rs index a002bba..2ac94ec 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,14 +1,14 @@ macro_rules! methods { ($($method:ident,)+) => { $( - fn $method serde::Deserialize<'de>>(&self, url: String) + async fn $method serde::Deserialize<'de>>(&self, url: String) -> Result { let response = self.send( self.client.$method(&url) - )?; + ).await?; - deserialise(response) + deserialise_response(response).await } )+ }; @@ -22,13 +22,13 @@ macro_rules! route_v1_paged { "# Errors\n", "If `access_token` is not set.", ), - pub fn $name(&self) -> Result> { + pub async fn $name<'a>(&'a self) -> Result> { let url = self.route(concat!("/api/v1/", $url)); let response = self.send( self.client.$method(&url) - )?; + ).await?; - Page::new(self, response) + Page::new(self, response).await } } }; @@ -40,7 +40,7 @@ macro_rules! route_v1_paged { "# Errors\n", "If `access_token` is not set.", ), - pub fn $name<'a>(&self, $($param: $typ,)*) -> Result> { + pub async fn $name<'a, 'b>(&'b self, $($param: $typ,)*) -> Result> { use serde_urlencoded; use serde::Serialize; @@ -69,9 +69,9 @@ macro_rules! route_v1_paged { let response = self.send( self.client.get(&url) - )?; + ).await?; - Page::new(self, response) + Page::new(self, response).await } } }; @@ -85,7 +85,7 @@ macro_rules! route_v2 { "# Errors\n", "If `access_token` is not set." ), - pub fn $name<'a>(&self, $($param: $typ,)*) -> Result<$ret> { + pub async fn $name<'a>(&self, $($param: $typ,)*) -> Result<$ret> { use serde_urlencoded; use serde::Serialize; @@ -109,7 +109,7 @@ macro_rules! route_v2 { let url = format!(concat!("/api/v2/", $url, "?{}"), &qs); - self.get(self.route(&url)) + self.get(self.route(&url)).await } } }; @@ -123,7 +123,7 @@ macro_rules! route_v1 { "# Errors\n", "If `access_token` is not set." ), - pub fn $name<'a>(&self, $($param: $typ,)*) -> Result<$ret> { + pub async fn $name<'a>(&self, $($param: $typ,)*) -> Result<$ret> { use serde_urlencoded; use serde::Serialize; @@ -147,7 +147,7 @@ macro_rules! route_v1 { let url = format!(concat!("/api/v1/", $url, "?{}"), &qs); - self.get(self.route(&url)) + self.get(self.route(&url)).await } } }; @@ -159,7 +159,7 @@ macro_rules! route_v1 { "# Errors\n", "If `access_token` is not set." ), - pub fn $name(&self, $($param: $typ,)*) -> Result<$ret> { + pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> { let form_data = serde_json::json!({ $( @@ -170,7 +170,7 @@ macro_rules! route_v1 { let response = self.send( self.client.$method(&self.route(concat!("/api/v1/", $url))) .json(&form_data) - )?; + ).await?; let status = response.status().clone(); @@ -180,7 +180,7 @@ macro_rules! route_v1 { return Err(Error::Server(status)); } - deserialise(response) + deserialise_response(response).await } } }; @@ -192,8 +192,8 @@ macro_rules! route_v1 { "# Errors\n", "If `access_token` is not set." ), - pub fn $name(&self) -> Result<$ret> { - self.$method(self.route(concat!("/api/v1/", $url))) + pub async fn $name(&self) -> Result<$ret> { + self.$method(self.route(concat!("/api/v1/", $url))).await } } }; @@ -207,8 +207,8 @@ macro_rules! route_v1_id { "# Errors\n", "If `access_token` is not set." ), - pub fn $name(&self, id: &str) -> Result<$ret> { - self.$method(self.route(&format!(concat!("/api/v1/", $url), id))) + pub async fn $name(&self, id: &str) -> Result<$ret> { + self.$method(self.route(&format!(concat!("/api/v1/", $url), id))).await } } } @@ -221,13 +221,13 @@ macro_rules! route_v1_paged_id { "# Errors\n", "If `access_token` is not set." ), - pub fn $name(&self, id: &str) -> Result> { + pub async fn $name<'a>(&'a self, id: &str) -> Result> { let url = self.route(&format!(concat!("/api/v1/", $url), id)); let response = self.send( self.client.$method(&url) - )?; + ).await?; - Page::new(self, response) + Page::new(self, response).await } } }; diff --git a/src/mastodon_client.rs b/src/mastodon_client.rs deleted file mode 100644 index ba0521c..0000000 --- a/src/mastodon_client.rs +++ /dev/null @@ -1,383 +0,0 @@ -use std::borrow::Cow; - -use crate::{ - entities::prelude::*, - errors::Result, - media_builder::MediaBuilder, - page::Page, - requests::{AddFilterRequest, AddPushRequest, StatusesRequest, UpdateCredsRequest, UpdatePushRequest}, - status_builder::NewStatus, -}; - -/// Represents the set of methods that a Mastodon Client can do, so that -/// implementations might be swapped out for testing -#[allow(unused)] -pub trait MastodonClient { - /// Type that wraps streaming API streams - type Stream: Iterator; - - /// GET /api/v1/favourites - fn favourites(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/blocks - fn blocks(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/domain_blocks - fn domain_blocks(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/follow_requests - fn follow_requests(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/timelines/home - fn get_home_timeline(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/custom_emojis - fn get_emojis(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/mutes - fn mutes(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/notifications - fn notifications(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/reports - fn reports(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/:id/followers - fn followers(&self, id: &str) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/:id/following - fn following(&self, id: &str) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/statuses/:id/reblogged_by - fn reblogged_by(&self, id: &str) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/statuses/:id/favourited_by - fn favourited_by(&self, id: &str) -> Result> { - unimplemented!("This method was not implemented"); - } - /// DELETE /api/v1/domain_blocks - fn unblock_domain(&self, domain: String) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/instance - fn instance(&self) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/verify_credentials - fn verify_credentials(&self) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/reports - fn report(&self, account_id: &str, status_ids: Vec<&str>, comment: String) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/domain_blocks - fn block_domain(&self, domain: String) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/accounts/follow_requests/authorize - fn authorize_follow_request(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/accounts/follow_requests/reject - fn reject_follow_request(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/search - fn search(&self, q: &'_ str, resolve: bool) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v2/search - fn search_v2(&self, q: &'_ str, resolve: bool) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/follows - fn follows(&self, uri: Cow<'static, str>) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/media - fn media(&self, media_builder: MediaBuilder) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/notifications/clear - fn clear_notifications(&self) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/notifications/dismiss - fn dismiss_notification(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/:id - fn get_account(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/accounts/:id/follow - fn follow(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/accounts/:id/unfollow - fn unfollow(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/:id/block - fn block(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/:id/unblock - fn unblock(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/:id/mute - fn mute(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/:id/unmute - fn unmute(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/notifications/:id - fn get_notification(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/statuses/:id - fn get_status(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/statuses/:id/context - fn get_context(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/statuses/:id/card - fn get_card(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/statuses/:id/reblog - fn reblog(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/statuses/:id/unreblog - fn unreblog(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/statuses/:id/favourite - fn favourite(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/statuses/:id/unfavourite - fn unfavourite(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// DELETE /api/v1/statuses/:id - fn delete_status(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// PATCH /api/v1/accounts/update_credentials - fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/statuses - fn new_status(&self, status: NewStatus) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/timelines/public?local=true - fn get_local_timeline(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/timelines/public?local=false - fn get_federated_timeline(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/timelines/tag/:hashtag - fn get_hashtag_timeline(&self, hashtag: &str, local: bool) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/:id/statuses - fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result> - where - S: Into>>, - { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/relationships - fn relationships(&self, ids: &[&str]) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/accounts/search?q=:query&limit=:limit&following=:following - fn search_accounts(&self, query: &str, limit: Option, following: bool) -> Result> { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/push/subscription - fn add_push_subscription(&self, request: &AddPushRequest) -> Result { - unimplemented!("This method was not implemented"); - } - /// PUT /api/v1/push/subscription - fn update_push_data(&self, request: &UpdatePushRequest) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/push/subscription - fn get_push_subscription(&self) -> Result { - unimplemented!("This method was not implemented"); - } - /// DELETE /api/v1/push/subscription - fn delete_push_subscription(&self) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/filters - fn get_filters(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/filters - fn add_filter(&self, request: &mut AddFilterRequest) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/filters/:id - fn get_filter(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// PUT /api/v1/filters/:id - fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result { - unimplemented!("This method was not implemented"); - } - /// DELETE /api/v1/filters/:id - fn delete_filter(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/suggestions - fn get_follow_suggestions(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// DELETE /api/v1/suggestions/:account_id - fn delete_from_suggestions(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/endorsements - fn get_endorsements(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/accounts/:id/pin - fn endorse_user(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// POST /api/v1/accounts/:id/unpin - fn unendorse_user(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// Shortcut for: `let me = client.verify_credentials(); client.followers()` - /// - /// ```no_run - /// # extern crate elefren; - /// # use std::error::Error; - /// # use elefren::prelude::*; - /// # fn main() -> Result<(), Box> { - /// # let data = Data { - /// # base: "".into(), - /// # client_id: "".into(), - /// # client_secret: "".into(), - /// # redirect: "".into(), - /// # token: "".into(), - /// # }; - /// # let client = Mastodon::from(data); - /// let follows_me = client.follows_me()?; - /// # Ok(()) - /// # } - fn follows_me(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - /// Shortcut for - /// `let me = client.verify_credentials(); client.following(&me.id)` - /// - /// ```no_run - /// # extern crate elefren; - /// # use std::error::Error; - /// # use elefren::prelude::*; - /// # fn main() -> Result<(), Box> { - /// # let data = Data { - /// # base: "".into(), - /// # client_id: "".into(), - /// # client_secret: "".into(), - /// # redirect: "".into(), - /// # token: "".into(), - /// # }; - /// # let client = Mastodon::from(data); - /// let follows_me = client.followed_by_me()?; - /// # Ok(()) - /// # } - fn followed_by_me(&self) -> Result> { - unimplemented!("This method was not implemented"); - } - - /// Returns events that are relevant to the authorized user, i.e. home - /// timeline and notifications - fn streaming_user(&self) -> Result { - unimplemented!("This method was not implemented"); - } - - /// Returns all public statuses - fn streaming_public(&self) -> Result { - unimplemented!("This method was not implemented"); - } - - /// Returns all local statuses - fn streaming_local(&self) -> Result { - unimplemented!("This method was not implemented"); - } - - /// Returns all public statuses for a particular hashtag - fn streaming_public_hashtag(&self, hashtag: &str) -> Result { - unimplemented!("This method was not implemented"); - } - - /// Returns all local statuses for a particular hashtag - fn streaming_local_hashtag(&self, hashtag: &str) -> Result { - unimplemented!("This method was not implemented"); - } - - /// Returns statuses for a list - fn streaming_list(&self, list_id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - - /// Returns all direct messages - fn streaming_direct(&self) -> Result { - unimplemented!("This method was not implemented"); - } -} - -/// Trait that represents clients that can make unauthenticated calls to a -/// mastodon instance -#[allow(unused)] -pub trait MastodonUnauthenticated { - /// GET /api/v1/statuses/:id - fn get_status(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/statuses/:id/context - fn get_context(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/statuses/:id/card - fn get_card(&self, id: &str) -> Result { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/statuses/:id/reblogged_by - fn reblogged_by(&self, id: &str) -> Result> { - unimplemented!("This method was not implemented"); - } - /// GET /api/v1/statuses/:id/favourited_by - fn favourited_by(&self, id: &str) -> Result> { - unimplemented!("This method was not implemented"); - } -} diff --git a/src/media_builder.rs b/src/media_builder.rs index dec70b1..4f10836 100644 --- a/src/media_builder.rs +++ b/src/media_builder.rs @@ -1,6 +1,7 @@ use std::fmt; -use std::io::Read; use std::path::{Path, PathBuf}; +use tokio::io::AsyncRead; +use std::pin::Pin; #[derive(Debug)] /// A builder pattern struct for preparing a single attachment for upload. @@ -28,7 +29,7 @@ pub struct MediaBuilder { /// Enum representing possible sources of attachments to upload pub enum MediaBuilderData { /// An arbitrary reader. It is useful for reading from media already in memory. - Reader(Box), + Reader(Pin>), /// Variant represening a file path of the file to attach. File(PathBuf), @@ -45,9 +46,9 @@ impl fmt::Debug for MediaBuilderData { impl MediaBuilder { /// Create a new MediaBuilder from a reader `data` - pub fn from_reader(data: R) -> MediaBuilder { + pub fn from_reader(data: R) -> MediaBuilder { MediaBuilder { - data: MediaBuilderData::Reader(Box::from(data)), + data: MediaBuilderData::Reader(Box::pin(data)), filename: None, mimetype: None, description: None, diff --git a/src/page.rs b/src/page.rs index ea90fcf..a2a120b 100644 --- a/src/page.rs +++ b/src/page.rs @@ -1,7 +1,7 @@ -use super::{deserialise, Mastodon, Result}; +use super::{deserialise_response, Mastodon, Result}; use crate::entities::itemsiter::ItemsIter; use hyper_old_types::header::{parsing, Link, RelationType}; -use reqwest::{blocking::Response, header::LINK}; +use reqwest::{Response, header::LINK}; use serde::Deserialize; use url::Url; @@ -11,7 +11,7 @@ macro_rules! pages { $( doc_comment::doc_comment!(concat!( "Method to retrieve the ", stringify!($direction), " page of results"), - pub fn $fun(&mut self) -> Result>> { + pub async fn $fun(&mut self) -> Result>> { let url = match self.$direction.take() { Some(s) => s, None => return Ok(None), @@ -19,13 +19,13 @@ macro_rules! pages { let response = self.mastodon.send( self.mastodon.client.get(url) - )?; + ).await?; let (prev, next) = get_links(&response)?; self.next = next; self.prev = prev; - deserialise(response) + deserialise_response(response).await }); )* } @@ -107,10 +107,10 @@ impl<'a, T: for<'de> Deserialize<'de>> Page<'a, T> { prev: prev_page } - pub(crate) fn new(mastodon: &'a Mastodon, response: Response) -> Result { + pub(crate) async fn new<'m>(mastodon: &'m Mastodon, response: Response) -> Result> { let (prev, next) = get_links(&response)?; Ok(Page { - initial_items: deserialise(response)?, + initial_items: deserialise_response(response).await?, next, prev, mastodon, @@ -189,7 +189,7 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>> Page<'a, T> { /// # Ok(()) /// # } /// ``` - pub fn items_iter(self) -> impl Iterator + 'a + pub fn items_iter(self) -> ItemsIter<'a, T> where T: 'a, { diff --git a/src/registration.rs b/src/registration.rs index fb9b512..ff2cdf4 100644 --- a/src/registration.rs +++ b/src/registration.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use reqwest::blocking::{Client, RequestBuilder, Response}; +use reqwest::{Client, RequestBuilder, Response}; use serde::Deserialize; use std::convert::TryInto; @@ -93,9 +93,9 @@ impl<'a> Registration<'a> { self } - fn send(&self, req: RequestBuilder) -> Result { + async fn send(&self, req: RequestBuilder) -> Result { let req = req.build()?; - Ok(self.client.execute(req)?) + Ok(self.client.execute(req).await?) } /// Register the given application @@ -119,12 +119,12 @@ impl<'a> Registration<'a> { /// # Ok(()) /// # } /// ``` - pub fn register>(&mut self, app: I) -> Result + pub async fn register>(&mut self, app: I) -> Result where Error: From<>::Error>, { let app = app.try_into()?; - let oauth = self.send_app(&app)?; + let oauth = self.send_app(&app).await?; Ok(Registered { base: self.base.clone(), @@ -157,9 +157,9 @@ impl<'a> Registration<'a> { /// # Ok(()) /// # } /// ``` - pub fn build(&mut self) -> Result { + pub async fn build(&mut self) -> Result { let app: App = self.app_builder.clone().build()?; - let oauth = self.send_app(&app)?; + let oauth = self.send_app(&app).await?; Ok(Registered { base: self.base.clone(), @@ -172,9 +172,10 @@ impl<'a> Registration<'a> { }) } - fn send_app(&self, app: &App) -> Result { + async fn send_app(&self, app: &App) -> Result { let url = format!("{}/api/v1/apps", self.base); - Ok(self.send(self.client.post(&url).json(&app))?.json()?) + Ok(self.send(self.client.post(&url).json(&app)).await? + .json().await?) } } @@ -228,9 +229,9 @@ impl Registered { } impl Registered { - fn send(&self, req: RequestBuilder) -> Result { + async fn send(&self, req: RequestBuilder) -> Result { let req = req.build()?; - Ok(self.client.execute(req)?) + Ok(self.client.execute(req).await?) } /// Returns the parts of the `Registered` struct that can be used to @@ -298,14 +299,14 @@ impl Registered { /// Create an access token from the client id, client secret, and code /// provided by the authorisation url. - pub fn complete(&self, code: &str) -> Result { + pub async fn complete(&self, code: &str) -> Result { let url = format!( "{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&\ redirect_uri={}", self.base, self.client_id, self.client_secret, code, self.redirect ); - let token: AccessToken = self.send(self.client.post(&url))?.json()?; + let token: AccessToken = self.send(self.client.post(&url)).await?.json().await?; let data = Data { base: self.base.clone().into(),