all asyncified except for streaming

master
Ondřej Hruška 3 years ago
parent b6b7372ebb
commit c967e59ffb
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 34
      Cargo.toml
  2. 14
      src/entities/itemsiter.rs
  3. 16
      src/errors.rs
  4. 4
      src/helpers/cli.rs
  5. 190
      src/lib.rs
  6. 46
      src/macros.rs
  7. 383
      src/mastodon_client.rs
  8. 9
      src/media_builder.rs
  9. 16
      src/page.rs
  10. 27
      src/registration.rs

@ -12,36 +12,20 @@ edition = "2018"
[dependencies] [dependencies]
doc-comment = "0.3" doc-comment = "0.3"
envy = { version = "0.4.0", optional = true } #envy = { version = "0.4.0", optional = true }
hyper-old-types = "0.11.0" hyper-old-types = "0.11.0" # Used to parse the link header
isolang = { version = "1.0", features = ["serde_serialize"] } isolang = { version = "1.0", features = ["serde_serialize"] }
log = "^0.4" 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 = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
serde_urlencoded = "0.6.1" serde_urlencoded = "0.6.1"
serde_qs = "0.8.4" serde_qs = "0.8.4"
url = "2.1.1" url = "2.1.1"
tap-reader = "1" #tap-reader = "1"
toml = { version = "^0.5.0", optional = true } toml = { version = "^0.5.0", optional = true }
tungstenite = "0.15.0" tokio-tungstenite = { version = "0.15.0", features = ["rustls-tls"] }
futures = { version = "0.3" }
[dependencies.chrono] tokio = {version = "1", features = ["full"] }
version = "0.4" tokio-util = { version = "0.6", features = [ "compat", "io" ] }
features = ["serde"] 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"]

@ -24,7 +24,7 @@ use serde::Deserialize;
/// # } /// # }
/// ``` /// ```
#[derive(Debug, Clone)] #[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>, page: Page<'a, T>,
buffer: Vec<T>, buffer: Vec<T>,
cur_idx: usize, 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() self.buffer.is_empty() || self.cur_idx == self.buffer.len()
} }
fn fill_next_page(&mut self) -> Option<()> { async fn fill_next_page(&mut self) -> Option<()> {
let items = if let Ok(items) = self.page.next_page() { let items = if let Ok(items) = self.page.next_page().await {
items items
} else { } else {
return None; return None;
@ -62,12 +62,8 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>> ItemsIter<'a, T> {
None None
} }
} }
}
impl<'a, T: Clone + for<'de> Deserialize<'de>> Iterator for ItemsIter<'a, T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> { pub async fn next_item(&mut self) -> Option<T> {
if self.use_initial { if self.use_initial {
if self.page.initial_items.is_empty() || self.cur_idx == self.page.initial_items.len() { if self.page.initial_items.is_empty() || self.cur_idx == self.page.initial_items.len() {
return None; 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()) Some(self.page.initial_items[idx].clone())
} else { } 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; return None;
} }
let idx = self.cur_idx; let idx = self.cur_idx;

@ -12,7 +12,7 @@ use reqwest::{header::ToStrError as HeaderStrError, Error as HttpError, StatusCo
use serde_json::Error as SerdeError; use serde_json::Error as SerdeError;
use serde_qs::Error as SerdeQsError; use serde_qs::Error as SerdeQsError;
use serde_urlencoded::ser::Error as UrlEncodedError; use serde_urlencoded::ser::Error as UrlEncodedError;
use tungstenite::error::Error as WebSocketError; // use tungstenite::error::Error as WebSocketError;
use url::ParseError as UrlError; use url::ParseError as UrlError;
/// Convience type over `std::result::Result` with `Error` as the error type. /// Convience type over `std::result::Result` with `Error` as the error type.
@ -57,14 +57,14 @@ pub enum Error {
HeaderStrError(HeaderStrError), HeaderStrError(HeaderStrError),
/// Error parsing the http Link header /// Error parsing the http Link header
HeaderParseError(HeaderParseError), HeaderParseError(HeaderParseError),
#[cfg(feature = "env")] // #[cfg(feature = "env")]
/// Error deserializing from the environment // /// Error deserializing from the environment
Envy(EnvyError), // Envy(EnvyError),
/// Error serializing to a query string /// Error serializing to a query string
SerdeQs(SerdeQsError), SerdeQs(SerdeQsError),
/// WebSocket error /// WebSocket error
WebSocket(WebSocketError), // WebSocket(WebSocketError),
/// Other errors // /// Other errors
Other(String), Other(String),
} }
@ -92,7 +92,7 @@ impl error::Error for Error {
#[cfg(feature = "env")] #[cfg(feature = "env")]
Error::Envy(ref e) => e, Error::Envy(ref e) => e,
Error::SerdeQs(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::Client(..) | Error::Server(..) => return None,
Error::ClientIdRequired => return None, Error::ClientIdRequired => return None,
@ -148,7 +148,7 @@ from! {
HeaderParseError, HeaderParseError, HeaderParseError, HeaderParseError,
#[cfg(feature = "env")] EnvyError, Envy, #[cfg(feature = "env")] EnvyError, Envy,
SerdeQsError, SerdeQs, SerdeQsError, SerdeQs,
WebSocketError, WebSocket, // WebSocketError, WebSocket,
String, Other, String, Other,
} }

@ -4,7 +4,7 @@ use crate::{errors::Result, registration::Registered, Mastodon};
/// Finishes the authentication process for the given `Registered` object, /// Finishes the authentication process for the given `Registered` object,
/// using the command-line /// using the command-line
pub fn authenticate(registration: Registered) -> Result<Mastodon> { pub async fn authenticate(registration: Registered) -> Result<Mastodon> {
let url = registration.authorize_url()?; let url = registration.authorize_url()?;
let stdout = io::stdout(); let stdout = io::stdout();
@ -20,5 +20,5 @@ pub fn authenticate(registration: Registered) -> Result<Mastodon> {
let mut input = String::new(); let mut input = String::new();
stdin.read_line(&mut input)?; stdin.read_line(&mut input)?;
let code = input.trim(); let code = input.trim();
registration.complete(code) registration.complete(code).await
} }

@ -57,31 +57,30 @@
//! # } //! # }
//! ``` //! ```
#![deny( // #![deny(
missing_docs, // missing_docs,
warnings, // warnings,
missing_debug_implementations, // missing_debug_implementations,
missing_copy_implementations, // missing_copy_implementations,
trivial_casts, // trivial_casts,
trivial_numeric_casts, // trivial_numeric_casts,
unsafe_code, // unsafe_code,
unstable_features, // unstable_features,
unused_import_braces, // unused_import_braces,
unused_qualifications // unused_qualifications
)] // )]
// #![cfg_attr(feature = "nightly", allow(broken_intra_doc_links))] // #![cfg_attr(feature = "nightly", allow(broken_intra_doc_links))]
// #![allow(broken_intra_doc_links)] // #![allow(broken_intra_doc_links)]
#[macro_use] #[macro_use]
extern crate log; extern crate log;
use std::{borrow::Cow, io::BufRead, ops}; use std::{borrow::Cow, ops};
use std::net::TcpStream;
pub use isolang::Language; pub use isolang::Language;
use reqwest::blocking::{Client, multipart, RequestBuilder, Response}; use reqwest::{Client, multipart, RequestBuilder, Response, Body};
use tap_reader::Tap; // use tap_reader::Tap;
use tungstenite::stream::MaybeTlsStream; // use tungstenite::stream::MaybeTlsStream;
use crate::{entities::prelude::*, page::Page}; use crate::{entities::prelude::*, page::Page};
pub use crate::{ pub use crate::{
@ -104,7 +103,6 @@ pub mod entities;
pub mod errors; pub mod errors;
/// Collection of helpers for serializing/deserializing `Data` objects /// Collection of helpers for serializing/deserializing `Data` objects
pub mod helpers; pub mod helpers;
mod mastodon_client;
/// Constructing media attachments for a status. /// Constructing media attachments for a status.
pub mod media_builder; pub mod media_builder;
/// Handling multiple pages of entities. /// Handling multiple pages of entities.
@ -132,37 +130,35 @@ pub mod prelude {
/// Your mastodon application client, handles all requests to and from Mastodon. /// Your mastodon application client, handles all requests to and from Mastodon.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Mastodon { pub struct Mastodon {
client: Client, client: Client,
/// Raw data about your mastodon instance. /// Raw data about your mastodon instance.
pub data: Data, pub data: Data,
} }
impl Mastodon { impl From<Data> 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<WebSocket>;
impl Mastodon {
methods![get, post, delete,]; methods![get, post, delete,];
fn route(&self, url: &str) -> String { fn route(&self, url: &str) -> String {
format!("{}{}", self.base, url) format!("{}{}", self.base, url)
} }
pub(crate) fn send(&self, req: RequestBuilder) -> Result<Response> { pub(crate) async fn send(&self, req: RequestBuilder) -> Result<Response> {
let request = req.bearer_auth(&self.token).build()?; let request = req.bearer_auth(&self.token).build()?;
Ok(self.client.execute(request)?) Ok(self.client.execute(request).await?)
} }
} }
impl From<Data> 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<WebSocket>;
impl Mastodon { impl Mastodon {
route_v1_paged!((get) favourites: "favourites" => Status); route_v1_paged!((get) favourites: "favourites" => Status);
route_v1_paged!((get) blocks: "blocks" => Account); route_v1_paged!((get) blocks: "blocks" => Account);
@ -224,9 +220,9 @@ impl Mastodon {
route_v1_id!((post) unendorse_user: "accounts/{}/unpin" => Relationship); route_v1_id!((post) unendorse_user: "accounts/{}/unpin" => Relationship);
/// POST /api/v1/filters /// POST /api/v1/filters
pub fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> { pub async fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> {
let url = self.route("/api/v1/filters"); 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(); let status = response.status();
@ -236,13 +232,13 @@ impl Mastodon {
return Err(Error::Server(status)); return Err(Error::Server(status));
} }
deserialise(response) deserialise_response(response).await
} }
/// PUT /api/v1/filters/:id /// PUT /api/v1/filters/:id
pub fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result<Filter> { pub async fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result<Filter> {
let url = self.route(&format!("/api/v1/filters/{}", id)); 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(); let status = response.status();
@ -252,14 +248,14 @@ impl Mastodon {
return Err(Error::Server(status)); return Err(Error::Server(status));
} }
deserialise(response) deserialise_response(response).await
} }
/// PATCH /api/v1/accounts/update_credentials /// PATCH /api/v1/accounts/update_credentials
pub fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result<Account> { pub async fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result<Account> {
let changes = builder.build()?; let changes = builder.build()?;
let url = self.route("/api/v1/accounts/update_credentials"); 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(); let status = response.status();
@ -269,19 +265,19 @@ impl Mastodon {
return Err(Error::Server(status)); return Err(Error::Server(status));
} }
deserialise(response) deserialise_response(response).await
} }
/// Post a new status to the account. /// Post a new status to the account.
pub fn new_status(&self, status: NewStatus) -> Result<Status> { pub async fn new_status(&self, status: NewStatus) -> Result<Status> {
let response = self.send(self.client.post(&self.route("/api/v1/statuses")).json(&status))?; 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 /// Get timeline filtered by a hashtag(eg. `#coffee`) either locally or
/// federated. /// federated.
pub fn get_hashtag_timeline(&self, hashtag: &str, local: bool) -> Result<Page<Status>> { pub async fn get_hashtag_timeline<'a>(&'a self, hashtag: &str, local: bool) -> Result<Page<'a, Status>> {
let base = "/api/v1/timelines/tag/"; let base = "/api/v1/timelines/tag/";
let url = if local { let url = if local {
self.route(&format!("{}{}?local=1", base, hashtag)) self.route(&format!("{}{}?local=1", base, hashtag))
@ -289,12 +285,12 @@ impl Mastodon {
self.route(&format!("{}{}", base, hashtag)) 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 /// Get statuses of a single account by id. Optionally only with pictures
/// and or excluding replies. /// and or excluding replies.
pub fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result<Page<Status>> pub async fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result<Page<'b, Status>>
where where
S: Into<Option<StatusesRequest<'a>>>, S: Into<Option<StatusesRequest<'a>>>,
{ {
@ -304,14 +300,14 @@ impl Mastodon {
url = format!("{}{}", url, request.to_querystring()?); 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. /// Returns the client account's relationship to a list of other accounts.
/// Such as whether they follow them or vice versa. /// Such as whether they follow them or vice versa.
pub fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship>> { pub async fn relationships<'a>(&'a self, ids: &[&str]) -> Result<Page<'a, Relationship>> {
let mut url = self.route("/api/v1/accounts/relationships?"); let mut url = self.route("/api/v1/accounts/relationships?");
if ids.len() == 1 { if ids.len() == 1 {
@ -326,40 +322,42 @@ impl Mastodon {
url.pop(); 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 /// Add a push notifications subscription
pub fn add_push_subscription(&self, request: &AddPushRequest) -> Result<Subscription> { pub async fn add_push_subscription(&self, request: &AddPushRequest) -> Result<Subscription> {
let request = request.build()?; 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 /// Update the `data` portion of the push subscription associated with this
/// access token /// access token
pub fn update_push_data(&self, request: &UpdatePushRequest) -> Result<Subscription> { pub async fn update_push_data(&self, request: &UpdatePushRequest) -> Result<Subscription> {
let request = request.build(); 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 /// Get all accounts that follow the authenticated user
pub fn follows_me(&self) -> Result<Page<Account>> { pub async fn follows_me<'a>(&'a self) -> Result<Page<'a, Account>> {
let me = self.verify_credentials()?; let me = self.verify_credentials().await?;
self.followers(&me.id) self.followers(&me.id).await
} }
/// Get all accounts that the authenticated user follows /// Get all accounts that the authenticated user follows
pub fn followed_by_me(&self) -> Result<Page<Account>> { pub async fn followed_by_me<'a>(&'a self) -> Result<Page<'a, Account>> {
let me = self.verify_credentials()?; let me = self.verify_credentials().await?;
self.following(&me.id) self.following(&me.id).await
} }
/*
/// returns events that are relevant to the authorized user, i.e. home /// returns events that are relevant to the authorized user, i.e. home
/// timeline & notifications /// timeline & notifications
pub fn streaming_user(&self) -> Result<MastodonStream> { pub fn streaming_user(&self) -> Result<MastodonStream> {
@ -367,7 +365,7 @@ impl Mastodon {
url.query_pairs_mut() url.query_pairs_mut()
.append_pair("access_token", &self.token) .append_pair("access_token", &self.token)
.append_pair("stream", "user"); .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() { let new_scheme = match url.scheme() {
"http" => "ws", "http" => "ws",
"https" => "wss", "https" => "wss",
@ -503,6 +501,7 @@ impl Mastodon {
Ok(EventReader(WebSocket(client))) Ok(EventReader(WebSocket(client)))
} }
*/
/// Upload some media to the server for possible attaching to a new status /// Upload some media to the server for possible attaching to a new status
/// ///
@ -544,14 +543,13 @@ impl Mastodon {
/// ## Errors /// ## Errors
/// This function may return an `Error::Http` before sending anything over the network if the /// 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. /// `MediaBuilder` was supplied with a reader and a `mimetype` string which cannot be pasrsed.
pub fn media(&self, media: MediaBuilder) -> Result<Attachment> { pub async fn media(&self, media: MediaBuilder) -> Result<Attachment> {
use media_builder::MediaBuilderData; use media_builder::MediaBuilderData;
let mut form = multipart::Form::new(); let mut form = multipart::Form::new();
form = match media.data { form = match media.data {
MediaBuilderData::Reader(reader) => { 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 { if let Some(filename) = media.filename {
part = part.file_name(filename); part = part.file_name(filename);
} }
@ -562,7 +560,12 @@ impl Mastodon {
form.part("file", part) 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 { if let Some(description) = media.description {
@ -573,12 +576,14 @@ impl Mastodon {
form = form.text("focus", format!("{},{}", x, y)); 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)] #[derive(Debug)]
/// WebSocket newtype so that EventStream can be implemented without coherency /// WebSocket newtype so that EventStream can be implemented without coherency
/// issues /// issues
@ -680,6 +685,8 @@ impl<R: EventStream> EventReader<R> {
} }
} }
*/
impl ops::Deref for Mastodon { impl ops::Deref for Mastodon {
type Target = Data; type Target = Data;
@ -750,11 +757,13 @@ impl MastodonUnauth {
Ok(self.base.join(url)?) Ok(self.base.join(url)?)
} }
fn send(&self, req: RequestBuilder) -> Result<Response> { async fn send(&self, req: RequestBuilder) -> Result<Response> {
let req = req.build()?; let req = req.build()?;
Ok(self.client.execute(req)?) Ok(self.client.execute(req).await?)
} }
/*
/// Get a stream of the public timeline /// Get a stream of the public timeline
pub fn streaming_public(&self) -> Result<EventReader<WebSocket>> { pub fn streaming_public(&self) -> Result<EventReader<WebSocket>> {
let mut url: url::Url = self.route("/api/v1/streaming/public/local")?; let mut url: url::Url = self.route("/api/v1/streaming/public/local")?;
@ -773,48 +782,49 @@ impl MastodonUnauth {
Ok(EventReader(WebSocket(client))) Ok(EventReader(WebSocket(client)))
} }
*/
/// GET /api/v1/statuses/:id /// GET /api/v1/statuses/:id
pub fn get_status(&self, id: &str) -> Result<Status> { pub async fn get_status(&self, id: &str) -> Result<Status> {
let route = self.route("/api/v1/statuses")?; let route = self.route("/api/v1/statuses")?;
let route = route.join(id)?; let route = route.join(id)?;
let response = self.send(self.client.get(route))?; let response = self.send(self.client.get(route)).await?;
deserialise(response) deserialise_response(response).await
} }
/// GET /api/v1/statuses/:id/context /// GET /api/v1/statuses/:id/context
pub fn get_context(&self, id: &str) -> Result<Context> { pub async fn get_context(&self, id: &str) -> Result<Context> {
let route = self.route("/api/v1/statuses")?; let route = self.route("/api/v1/statuses")?;
let route = route.join(id)?; let route = route.join(id)?;
let route = route.join("context")?; let route = route.join("context")?;
let response = self.send(self.client.get(route))?; let response = self.send(self.client.get(route)).await?;
deserialise(response) deserialise_response(response).await
} }
/// GET /api/v1/statuses/:id/card /// GET /api/v1/statuses/:id/card
pub fn get_card(&self, id: &str) -> Result<Card> { pub async fn get_card(&self, id: &str) -> Result<Card> {
let route = self.route("/api/v1/statuses")?; let route = self.route("/api/v1/statuses")?;
let route = route.join(id)?; let route = route.join(id)?;
let route = route.join("card")?; let route = route.join("card")?;
let response = self.send(self.client.get(route))?; let response = self.send(self.client.get(route)).await?;
deserialise(response) deserialise_response(response).await
} }
} }
// Convert the HTTP response body from JSON. Pass up deserialization errors // Convert the HTTP response body from JSON. Pass up deserialization errors
// transparently. // transparently.
fn deserialise<T: for<'de> serde::Deserialize<'de>>(response: Response) -> Result<T> { async fn deserialise_response<T: for<'de> serde::Deserialize<'de>>(response: Response) -> Result<T> {
let mut reader = Tap::new(response); let bytes = response.bytes().await?;
match serde_json::from_slice(&bytes) {
match serde_json::from_reader(&mut reader) {
Ok(t) => { Ok(t) => {
log::debug!("{}", String::from_utf8_lossy(&reader.bytes)); debug!("{}", String::from_utf8_lossy(&bytes));
Ok(t) Ok(t)
} }
// If deserializing into the desired type fails try again to // If deserializing into the desired type fails try again to
// see if this is an error response. // see if this is an error response.
Err(e) => { Err(e) => {
log::error!("{}", String::from_utf8_lossy(&reader.bytes)); error!("{}", String::from_utf8_lossy(&bytes));
if let Ok(error) = serde_json::from_slice(&reader.bytes) { if let Ok(error) = serde_json::from_slice(&bytes) {
return Err(Error::Api(error)); return Err(Error::Api(error));
} }
Err(e.into()) Err(e.into())

@ -1,14 +1,14 @@
macro_rules! methods { macro_rules! methods {
($($method:ident,)+) => { ($($method:ident,)+) => {
$( $(
fn $method<T: for<'de> serde::Deserialize<'de>>(&self, url: String) async fn $method<T: for<'de> serde::Deserialize<'de>>(&self, url: String)
-> Result<T> -> Result<T>
{ {
let response = self.send( let response = self.send(
self.client.$method(&url) self.client.$method(&url)
)?; ).await?;
deserialise(response) deserialise_response(response).await
} }
)+ )+
}; };
@ -22,13 +22,13 @@ macro_rules! route_v1_paged {
"# Errors\n", "# Errors\n",
"If `access_token` is not set.", "If `access_token` is not set.",
), ),
pub fn $name(&self) -> Result<Page<$ret>> { pub async fn $name<'a>(&'a self) -> Result<Page<'a, $ret>> {
let url = self.route(concat!("/api/v1/", $url)); let url = self.route(concat!("/api/v1/", $url));
let response = self.send( let response = self.send(
self.client.$method(&url) 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", "# Errors\n",
"If `access_token` is not set.", "If `access_token` is not set.",
), ),
pub fn $name<'a>(&self, $($param: $typ,)*) -> Result<Page<$ret>> { pub async fn $name<'a, 'b>(&'b self, $($param: $typ,)*) -> Result<Page<'b, $ret>> {
use serde_urlencoded; use serde_urlencoded;
use serde::Serialize; use serde::Serialize;
@ -69,9 +69,9 @@ macro_rules! route_v1_paged {
let response = self.send( let response = self.send(
self.client.get(&url) self.client.get(&url)
)?; ).await?;
Page::new(self, response) Page::new(self, response).await
} }
} }
}; };
@ -85,7 +85,7 @@ macro_rules! route_v2 {
"# Errors\n", "# Errors\n",
"If `access_token` is not set." "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_urlencoded;
use serde::Serialize; use serde::Serialize;
@ -109,7 +109,7 @@ macro_rules! route_v2 {
let url = format!(concat!("/api/v2/", $url, "?{}"), &qs); 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", "# Errors\n",
"If `access_token` is not set." "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_urlencoded;
use serde::Serialize; use serde::Serialize;
@ -147,7 +147,7 @@ macro_rules! route_v1 {
let url = format!(concat!("/api/v1/", $url, "?{}"), &qs); 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", "# Errors\n",
"If `access_token` is not set." "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!({ let form_data = serde_json::json!({
$( $(
@ -170,7 +170,7 @@ macro_rules! route_v1 {
let response = self.send( let response = self.send(
self.client.$method(&self.route(concat!("/api/v1/", $url))) self.client.$method(&self.route(concat!("/api/v1/", $url)))
.json(&form_data) .json(&form_data)
)?; ).await?;
let status = response.status().clone(); let status = response.status().clone();
@ -180,7 +180,7 @@ macro_rules! route_v1 {
return Err(Error::Server(status)); return Err(Error::Server(status));
} }
deserialise(response) deserialise_response(response).await
} }
} }
}; };
@ -192,8 +192,8 @@ macro_rules! route_v1 {
"# Errors\n", "# Errors\n",
"If `access_token` is not set." "If `access_token` is not set."
), ),
pub fn $name(&self) -> Result<$ret> { pub async fn $name(&self) -> Result<$ret> {
self.$method(self.route(concat!("/api/v1/", $url))) self.$method(self.route(concat!("/api/v1/", $url))).await
} }
} }
}; };
@ -207,8 +207,8 @@ macro_rules! route_v1_id {
"# Errors\n", "# Errors\n",
"If `access_token` is not set." "If `access_token` is not set."
), ),
pub fn $name(&self, id: &str) -> Result<$ret> { pub async fn $name(&self, id: &str) -> Result<$ret> {
self.$method(self.route(&format!(concat!("/api/v1/", $url), id))) self.$method(self.route(&format!(concat!("/api/v1/", $url), id))).await
} }
} }
} }
@ -221,13 +221,13 @@ macro_rules! route_v1_paged_id {
"# Errors\n", "# Errors\n",
"If `access_token` is not set." "If `access_token` is not set."
), ),
pub fn $name(&self, id: &str) -> Result<Page<$ret>> { pub async fn $name<'a>(&'a self, id: &str) -> Result<Page<'a, $ret>> {
let url = self.route(&format!(concat!("/api/v1/", $url), id)); let url = self.route(&format!(concat!("/api/v1/", $url), id));
let response = self.send( let response = self.send(
self.client.$method(&url) self.client.$method(&url)
)?; ).await?;
Page::new(self, response) Page::new(self, response).await
} }
} }
}; };

@ -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<Item = Event>;
/// GET /api/v1/favourites
fn favourites(&self) -> Result<Page<Status>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/blocks
fn blocks(&self) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/domain_blocks
fn domain_blocks(&self) -> Result<Page<String>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/follow_requests
fn follow_requests(&self) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/timelines/home
fn get_home_timeline(&self) -> Result<Page<Status>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/custom_emojis
fn get_emojis(&self) -> Result<Page<Emoji>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/mutes
fn mutes(&self) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/notifications
fn notifications(&self) -> Result<Page<Notification>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/reports
fn reports(&self) -> Result<Page<Report>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/accounts/:id/followers
fn followers(&self, id: &str) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/accounts/:id/following
fn following(&self, id: &str) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/statuses/:id/reblogged_by
fn reblogged_by(&self, id: &str) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/statuses/:id/favourited_by
fn favourited_by(&self, id: &str) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
/// DELETE /api/v1/domain_blocks
fn unblock_domain(&self, domain: String) -> Result<Empty> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/instance
fn instance(&self) -> Result<Instance> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/accounts/verify_credentials
fn verify_credentials(&self) -> Result<Account> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/reports
fn report(&self, account_id: &str, status_ids: Vec<&str>, comment: String) -> Result<Report> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/domain_blocks
fn block_domain(&self, domain: String) -> Result<Empty> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/accounts/follow_requests/authorize
fn authorize_follow_request(&self, id: &str) -> Result<Empty> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/accounts/follow_requests/reject
fn reject_follow_request(&self, id: &str) -> Result<Empty> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/search
fn search(&self, q: &'_ str, resolve: bool) -> Result<SearchResult> {
unimplemented!("This method was not implemented");
}
/// GET /api/v2/search
fn search_v2(&self, q: &'_ str, resolve: bool) -> Result<SearchResultV2> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/follows
fn follows(&self, uri: Cow<'static, str>) -> Result<Account> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/media
fn media(&self, media_builder: MediaBuilder) -> Result<Attachment> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/notifications/clear
fn clear_notifications(&self) -> Result<Empty> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/notifications/dismiss
fn dismiss_notification(&self, id: &str) -> Result<Empty> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/accounts/:id
fn get_account(&self, id: &str) -> Result<Account> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/accounts/:id/follow
fn follow(&self, id: &str) -> Result<Relationship> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/accounts/:id/unfollow
fn unfollow(&self, id: &str) -> Result<Relationship> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/accounts/:id/block
fn block(&self, id: &str) -> Result<Relationship> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/accounts/:id/unblock
fn unblock(&self, id: &str) -> Result<Relationship> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/accounts/:id/mute
fn mute(&self, id: &str) -> Result<Relationship> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/accounts/:id/unmute
fn unmute(&self, id: &str) -> Result<Relationship> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/notifications/:id
fn get_notification(&self, id: &str) -> Result<Notification> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/statuses/:id
fn get_status(&self, id: &str) -> Result<Status> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/statuses/:id/context
fn get_context(&self, id: &str) -> Result<Context> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/statuses/:id/card
fn get_card(&self, id: &str) -> Result<Card> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/statuses/:id/reblog
fn reblog(&self, id: &str) -> Result<Status> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/statuses/:id/unreblog
fn unreblog(&self, id: &str) -> Result<Status> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/statuses/:id/favourite
fn favourite(&self, id: &str) -> Result<Status> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/statuses/:id/unfavourite
fn unfavourite(&self, id: &str) -> Result<Status> {
unimplemented!("This method was not implemented");
}
/// DELETE /api/v1/statuses/:id
fn delete_status(&self, id: &str) -> Result<Empty> {
unimplemented!("This method was not implemented");
}
/// PATCH /api/v1/accounts/update_credentials
fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result<Account> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/statuses
fn new_status(&self, status: NewStatus) -> Result<Status> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/timelines/public?local=true
fn get_local_timeline(&self) -> Result<Page<Status>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/timelines/public?local=false
fn get_federated_timeline(&self) -> Result<Page<Status>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/timelines/tag/:hashtag
fn get_hashtag_timeline(&self, hashtag: &str, local: bool) -> Result<Page<Status>> {
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<Page<Status>>
where
S: Into<Option<StatusesRequest<'a>>>,
{
unimplemented!("This method was not implemented");
}
/// GET /api/v1/accounts/relationships
fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship>> {
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<u64>, following: bool) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/push/subscription
fn add_push_subscription(&self, request: &AddPushRequest) -> Result<Subscription> {
unimplemented!("This method was not implemented");
}
/// PUT /api/v1/push/subscription
fn update_push_data(&self, request: &UpdatePushRequest) -> Result<Subscription> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/push/subscription
fn get_push_subscription(&self) -> Result<Subscription> {
unimplemented!("This method was not implemented");
}
/// DELETE /api/v1/push/subscription
fn delete_push_subscription(&self) -> Result<Empty> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/filters
fn get_filters(&self) -> Result<Vec<Filter>> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/filters
fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/filters/:id
fn get_filter(&self, id: &str) -> Result<Filter> {
unimplemented!("This method was not implemented");
}
/// PUT /api/v1/filters/:id
fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result<Filter> {
unimplemented!("This method was not implemented");
}
/// DELETE /api/v1/filters/:id
fn delete_filter(&self, id: &str) -> Result<Empty> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/suggestions
fn get_follow_suggestions(&self) -> Result<Vec<Account>> {
unimplemented!("This method was not implemented");
}
/// DELETE /api/v1/suggestions/:account_id
fn delete_from_suggestions(&self, id: &str) -> Result<Empty> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/endorsements
fn get_endorsements(&self) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/accounts/:id/pin
fn endorse_user(&self, id: &str) -> Result<Relationship> {
unimplemented!("This method was not implemented");
}
/// POST /api/v1/accounts/:id/unpin
fn unendorse_user(&self, id: &str) -> Result<Relationship> {
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<dyn Error>> {
/// # 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<Page<Account>> {
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<dyn Error>> {
/// # 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<Page<Account>> {
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<Self::Stream> {
unimplemented!("This method was not implemented");
}
/// Returns all public statuses
fn streaming_public(&self) -> Result<Self::Stream> {
unimplemented!("This method was not implemented");
}
/// Returns all local statuses
fn streaming_local(&self) -> Result<Self::Stream> {
unimplemented!("This method was not implemented");
}
/// Returns all public statuses for a particular hashtag
fn streaming_public_hashtag(&self, hashtag: &str) -> Result<Self::Stream> {
unimplemented!("This method was not implemented");
}
/// Returns all local statuses for a particular hashtag
fn streaming_local_hashtag(&self, hashtag: &str) -> Result<Self::Stream> {
unimplemented!("This method was not implemented");
}
/// Returns statuses for a list
fn streaming_list(&self, list_id: &str) -> Result<Self::Stream> {
unimplemented!("This method was not implemented");
}
/// Returns all direct messages
fn streaming_direct(&self) -> Result<Self::Stream> {
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<Status> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/statuses/:id/context
fn get_context(&self, id: &str) -> Result<Context> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/statuses/:id/card
fn get_card(&self, id: &str) -> Result<Card> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/statuses/:id/reblogged_by
fn reblogged_by(&self, id: &str) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
/// GET /api/v1/statuses/:id/favourited_by
fn favourited_by(&self, id: &str) -> Result<Page<Account>> {
unimplemented!("This method was not implemented");
}
}

@ -1,6 +1,7 @@
use std::fmt; use std::fmt;
use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tokio::io::AsyncRead;
use std::pin::Pin;
#[derive(Debug)] #[derive(Debug)]
/// A builder pattern struct for preparing a single attachment for upload. /// 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 /// Enum representing possible sources of attachments to upload
pub enum MediaBuilderData { pub enum MediaBuilderData {
/// An arbitrary reader. It is useful for reading from media already in memory. /// An arbitrary reader. It is useful for reading from media already in memory.
Reader(Box<dyn Read + Send>), Reader(Pin<Box<dyn AsyncRead + Send + Sync>>),
/// Variant represening a file path of the file to attach. /// Variant represening a file path of the file to attach.
File(PathBuf), File(PathBuf),
@ -45,9 +46,9 @@ impl fmt::Debug for MediaBuilderData {
impl MediaBuilder { impl MediaBuilder {
/// Create a new MediaBuilder from a reader `data` /// Create a new MediaBuilder from a reader `data`
pub fn from_reader<R: Read + Send + 'static>(data: R) -> MediaBuilder { pub fn from_reader<R: AsyncRead + Send + Sync + 'static>(data: R) -> MediaBuilder {
MediaBuilder { MediaBuilder {
data: MediaBuilderData::Reader(Box::from(data)), data: MediaBuilderData::Reader(Box::pin(data)),
filename: None, filename: None,
mimetype: None, mimetype: None,
description: None, description: None,

@ -1,7 +1,7 @@
use super::{deserialise, Mastodon, Result}; use super::{deserialise_response, Mastodon, Result};
use crate::entities::itemsiter::ItemsIter; use crate::entities::itemsiter::ItemsIter;
use hyper_old_types::header::{parsing, Link, RelationType}; use hyper_old_types::header::{parsing, Link, RelationType};
use reqwest::{blocking::Response, header::LINK}; use reqwest::{Response, header::LINK};
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
@ -11,7 +11,7 @@ macro_rules! pages {
$( $(
doc_comment::doc_comment!(concat!( doc_comment::doc_comment!(concat!(
"Method to retrieve the ", stringify!($direction), " page of results"), "Method to retrieve the ", stringify!($direction), " page of results"),
pub fn $fun(&mut self) -> Result<Option<Vec<T>>> { pub async fn $fun(&mut self) -> Result<Option<Vec<T>>> {
let url = match self.$direction.take() { let url = match self.$direction.take() {
Some(s) => s, Some(s) => s,
None => return Ok(None), None => return Ok(None),
@ -19,13 +19,13 @@ macro_rules! pages {
let response = self.mastodon.send( let response = self.mastodon.send(
self.mastodon.client.get(url) self.mastodon.client.get(url)
)?; ).await?;
let (prev, next) = get_links(&response)?; let (prev, next) = get_links(&response)?;
self.next = next; self.next = next;
self.prev = prev; 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 prev: prev_page
} }
pub(crate) fn new(mastodon: &'a Mastodon, response: Response) -> Result<Self> { pub(crate) async fn new<'m>(mastodon: &'m Mastodon, response: Response) -> Result<Page<'m, T>> {
let (prev, next) = get_links(&response)?; let (prev, next) = get_links(&response)?;
Ok(Page { Ok(Page {
initial_items: deserialise(response)?, initial_items: deserialise_response(response).await?,
next, next,
prev, prev,
mastodon, mastodon,
@ -189,7 +189,7 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>> Page<'a, T> {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn items_iter(self) -> impl Iterator<Item = T> + 'a pub fn items_iter(self) -> ItemsIter<'a, T>
where where
T: 'a, T: 'a,
{ {

@ -1,6 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use reqwest::blocking::{Client, RequestBuilder, Response}; use reqwest::{Client, RequestBuilder, Response};
use serde::Deserialize; use serde::Deserialize;
use std::convert::TryInto; use std::convert::TryInto;
@ -93,9 +93,9 @@ impl<'a> Registration<'a> {
self self
} }
fn send(&self, req: RequestBuilder) -> Result<Response> { async fn send(&self, req: RequestBuilder) -> Result<Response> {
let req = req.build()?; let req = req.build()?;
Ok(self.client.execute(req)?) Ok(self.client.execute(req).await?)
} }
/// Register the given application /// Register the given application
@ -119,12 +119,12 @@ impl<'a> Registration<'a> {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn register<I: TryInto<App>>(&mut self, app: I) -> Result<Registered> pub async fn register<I: TryInto<App>>(&mut self, app: I) -> Result<Registered>
where where
Error: From<<I as TryInto<App>>::Error>, Error: From<<I as TryInto<App>>::Error>,
{ {
let app = app.try_into()?; let app = app.try_into()?;
let oauth = self.send_app(&app)?; let oauth = self.send_app(&app).await?;
Ok(Registered { Ok(Registered {
base: self.base.clone(), base: self.base.clone(),
@ -157,9 +157,9 @@ impl<'a> Registration<'a> {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn build(&mut self) -> Result<Registered> { pub async fn build(&mut self) -> Result<Registered> {
let app: App = self.app_builder.clone().build()?; let app: App = self.app_builder.clone().build()?;
let oauth = self.send_app(&app)?; let oauth = self.send_app(&app).await?;
Ok(Registered { Ok(Registered {
base: self.base.clone(), base: self.base.clone(),
@ -172,9 +172,10 @@ impl<'a> Registration<'a> {
}) })
} }
fn send_app(&self, app: &App) -> Result<OAuth> { async fn send_app(&self, app: &App) -> Result<OAuth> {
let url = format!("{}/api/v1/apps", self.base); 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 { impl Registered {
fn send(&self, req: RequestBuilder) -> Result<Response> { async fn send(&self, req: RequestBuilder) -> Result<Response> {
let req = req.build()?; 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 /// 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 /// Create an access token from the client id, client secret, and code
/// provided by the authorisation url. /// provided by the authorisation url.
pub fn complete(&self, code: &str) -> Result<Mastodon> { pub async fn complete(&self, code: &str) -> Result<Mastodon> {
let url = format!( let url = format!(
"{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&\ "{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&\
redirect_uri={}", redirect_uri={}",
self.base, self.client_id, self.client_secret, code, self.redirect 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 { let data = Data {
base: self.base.clone().into(), base: self.base.clone().into(),

Loading…
Cancel
Save