Update client to work asynchronously
- Use reqwest's async client - Convert items_iter() to a futures::Stream - make Mastodon client an Arc smart pointer, removing the need for OwnedPage. - remove MastodonClient and HttpSender traits; these can be re-added once async trait fns are stabilized - make EventStream a futures::Stream
This commit is contained in:
parent
f054c7d805
commit
e69d92f71e
85
Cargo.toml
85
Cargo.toml
|
@ -1,97 +1,70 @@
|
||||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
|
||||||
#
|
|
||||||
# When uploading crates to the registry Cargo will automatically
|
|
||||||
# "normalize" Cargo.toml files for maximal compatibility
|
|
||||||
# with all versions of Cargo and also rewrite `path` dependencies
|
|
||||||
# to registry (e.g., crates.io) dependencies
|
|
||||||
#
|
|
||||||
# If you believe there's an error in this file please file an
|
|
||||||
# issue against the rust-lang/cargo repository. If you're
|
|
||||||
# editing this file be aware that the upstream Cargo.toml
|
|
||||||
# will likely look very different (and much more reasonable)
|
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "elefren"
|
name = "elefren"
|
||||||
version = "0.23.0"
|
version = "0.23.0"
|
||||||
authors = ["Aaron Power <theaaronepower@gmail.com>", "Paul Woolcock <paul@woolcock.us>"]
|
authors = ["Aaron Power <theaaronepower@gmail.com>", "Paul Woolcock <paul@woolcock.us>", "D. Scott Boggs <scott@tams.tech>"]
|
||||||
description = "A wrapper around the Mastodon API."
|
description = "A wrapper around the Mastodon API."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["api", "web", "social", "mastodon", "wrapper"]
|
keywords = ["api", "web", "social", "mastodon", "wrapper"]
|
||||||
categories = ["web-programming", "web-programming::http-client", "api-bindings"]
|
categories = ["web-programming", "web-programming::http-client", "api-bindings"]
|
||||||
license = "MIT/Apache-2.0"
|
license = "MIT/Apache-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
# TODO setup repo
|
repository = "https://github.com/dscottboggs/elefren.git"
|
||||||
# repository = "https://github.com/pwoolcoc/elefren.git"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = ["all"]
|
features = ["all"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
futures = "0.3.25"
|
||||||
|
doc-comment = "0.3"
|
||||||
|
log = "0.4"
|
||||||
|
serde_json = "1"
|
||||||
|
serde_qs = "0.4.5"
|
||||||
|
serde_urlencoded = "0.6.1"
|
||||||
|
tap-reader = "1"
|
||||||
|
tungstenite = "0.18"
|
||||||
|
url = "1"
|
||||||
|
# Provides parsing for the link header in get_links() in page.rs
|
||||||
|
hyper-old-types = "0.11.0"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
version = "0.4"
|
version = "0.4"
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|
||||||
[dependencies.doc-comment]
|
|
||||||
version = "0.3"
|
|
||||||
|
|
||||||
[dependencies.envy]
|
[dependencies.envy]
|
||||||
version = "0.4"
|
version = "0.4"
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
# Provides parsing for the link header in get_links() in page.rs
|
|
||||||
[dependencies.hyper-old-types]
|
|
||||||
version = "0.11.0"
|
|
||||||
|
|
||||||
[dependencies.isolang]
|
[dependencies.isolang]
|
||||||
version = "2.2"
|
version = "2.2"
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|
||||||
[dependencies.log]
|
|
||||||
version = "0.4"
|
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.9"
|
version = "0.11"
|
||||||
default-features = false
|
features = ["multipart", "json"]
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
features = ["derive"]
|
features = ["derive"]
|
||||||
|
|
||||||
[dependencies.serde_json]
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[dependencies.serde_qs]
|
|
||||||
version = "0.4.5"
|
|
||||||
|
|
||||||
[dependencies.serde_urlencoded]
|
|
||||||
version = "0.6.1"
|
|
||||||
|
|
||||||
[dependencies.tap-reader]
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[dependencies.toml]
|
[dependencies.toml]
|
||||||
version = "0.5"
|
version = "0.5"
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.tungstenite]
|
[dev-dependencies]
|
||||||
version = "0.10.1"
|
tokio-test = "0.4.2"
|
||||||
|
futures-util = "0.3.25"
|
||||||
[dependencies.url]
|
indoc = "1.0"
|
||||||
version = "1"
|
pretty_env_logger = "0.3.0"
|
||||||
|
skeptic = "0.13"
|
||||||
[dev-dependencies.indoc]
|
tempfile = "3"
|
||||||
version = "1.0"
|
|
||||||
|
|
||||||
[dev-dependencies.pretty_env_logger]
|
|
||||||
version = "0.3.0"
|
|
||||||
|
|
||||||
[dev-dependencies.skeptic]
|
|
||||||
version = "0.13"
|
|
||||||
|
|
||||||
[dev-dependencies.tempfile]
|
|
||||||
version = "3"
|
|
||||||
|
|
||||||
[build-dependencies.skeptic]
|
[build-dependencies.skeptic]
|
||||||
version = "0.13"
|
version = "0.13"
|
||||||
|
|
||||||
|
[dev-dependencies.tokio]
|
||||||
|
version = "1.22.0"
|
||||||
|
features = ["rt-multi-thread", "macros"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
all = ["toml", "json", "env"]
|
all = ["toml", "json", "env"]
|
||||||
default = ["reqwest/default-tls"]
|
default = ["reqwest/default-tls"]
|
||||||
|
|
|
@ -5,7 +5,7 @@ extern crate pretty_env_logger;
|
||||||
extern crate elefren;
|
extern crate elefren;
|
||||||
mod register;
|
mod register;
|
||||||
|
|
||||||
use register::MastodonClient;
|
use register::Mastodon;
|
||||||
use std::error;
|
use std::error;
|
||||||
|
|
||||||
#[cfg(feature = "toml")]
|
#[cfg(feature = "toml")]
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
extern crate pretty_env_logger;
|
extern crate pretty_env_logger;
|
||||||
mod register;
|
mod register;
|
||||||
|
|
||||||
use register::MastodonClient;
|
use register::Mastodon;
|
||||||
use std::error;
|
use std::error;
|
||||||
|
|
||||||
#[cfg(feature = "toml")]
|
#[cfg(feature = "toml")]
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
extern crate pretty_env_logger;
|
extern crate pretty_env_logger;
|
||||||
mod register;
|
mod register;
|
||||||
|
|
||||||
use register::MastodonClient;
|
use register::Mastodon;
|
||||||
use std::error;
|
use std::error;
|
||||||
|
|
||||||
#[cfg(feature = "toml")]
|
#[cfg(feature = "toml")]
|
||||||
|
|
|
@ -5,7 +5,7 @@ extern crate pretty_env_logger;
|
||||||
extern crate elefren;
|
extern crate elefren;
|
||||||
mod register;
|
mod register;
|
||||||
|
|
||||||
use register::MastodonClient;
|
use register::Mastodon;
|
||||||
use std::error;
|
use std::error;
|
||||||
|
|
||||||
#[cfg(feature = "toml")]
|
#[cfg(feature = "toml")]
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
extern crate pretty_env_logger;
|
extern crate pretty_env_logger;
|
||||||
mod register;
|
mod register;
|
||||||
|
|
||||||
use register::MastodonClient;
|
use register::Mastodon;
|
||||||
use std::error;
|
use std::error;
|
||||||
|
|
||||||
#[cfg(feature = "toml")]
|
#[cfg(feature = "toml")]
|
||||||
|
|
|
@ -5,7 +5,7 @@ extern crate pretty_env_logger;
|
||||||
extern crate elefren;
|
extern crate elefren;
|
||||||
mod register;
|
mod register;
|
||||||
|
|
||||||
use register::MastodonClient;
|
use register::Mastodon;
|
||||||
use std::error;
|
use std::error;
|
||||||
|
|
||||||
#[cfg(feature = "toml")]
|
#[cfg(feature = "toml")]
|
||||||
|
|
|
@ -1,27 +1,36 @@
|
||||||
use crate::{http_send::HttpSend, page::Page};
|
use futures::{stream::unfold, Stream};
|
||||||
|
|
||||||
|
use crate::page::Page;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
/// Abstracts away the `next_page` logic into a single stream of items
|
/// Abstracts away the `next_page` logic into a single stream of items
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run,async
|
||||||
/// use elefren::prelude::*;
|
/// use elefren::prelude::*;
|
||||||
|
/// use futures::stream::StreamExt;
|
||||||
|
/// use futures_util::pin_mut;
|
||||||
|
///
|
||||||
|
/// tokio_test::block_on(async {
|
||||||
/// let data = Data::default();
|
/// let data = Data::default();
|
||||||
/// let client = Mastodon::from(data);
|
/// let client = Mastodon::from(data);
|
||||||
/// let statuses = client.statuses("user-id", None).unwrap();
|
/// let statuses = client.statuses("user-id", None).await.unwrap().items_iter();
|
||||||
/// for status in statuses.items_iter() {
|
/// statuses.for_each(|status| async move {
|
||||||
/// // do something with `status`
|
/// // Do something with the status
|
||||||
/// }
|
/// }).await;
|
||||||
|
/// })
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// See documentation for `futures::Stream::StreamExt` for available methods.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ItemsIter<'a, T: Clone + for<'de> Deserialize<'de>, H: 'a + HttpSend> {
|
pub(crate) struct ItemsIter<T: Clone + for<'de> Deserialize<'de>> {
|
||||||
page: Page<'a, T, H>,
|
page: Page<T>,
|
||||||
buffer: Vec<T>,
|
buffer: Vec<T>,
|
||||||
cur_idx: usize,
|
cur_idx: usize,
|
||||||
use_initial: bool,
|
use_initial: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> ItemsIter<'a, T, H> {
|
impl<'a, T: Clone + for<'de> Deserialize<'de>> ItemsIter<T> {
|
||||||
pub(crate) fn new(page: Page<'a, T, H>) -> ItemsIter<'a, T, H> {
|
pub(crate) fn new(page: Page<T>) -> ItemsIter<T> {
|
||||||
ItemsIter {
|
ItemsIter {
|
||||||
page,
|
page,
|
||||||
buffer: vec![],
|
buffer: vec![],
|
||||||
|
@ -34,8 +43,8 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> ItemsIter<'a, T, H>
|
||||||
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;
|
||||||
|
@ -51,33 +60,39 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> ItemsIter<'a, T, H>
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> Iterator for ItemsIter<'a, T, H> {
|
pub(crate) fn stream(self) -> impl Stream<Item = T> {
|
||||||
type Item = T;
|
unfold(self, |mut this| async move {
|
||||||
|
if this.use_initial {
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
if this.page.initial_items.is_empty()
|
||||||
if self.use_initial {
|
|| this.cur_idx == this.page.initial_items.len()
|
||||||
if self.page.initial_items.is_empty() || self.cur_idx == self.page.initial_items.len() {
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let idx = self.cur_idx;
|
let idx = this.cur_idx;
|
||||||
if self.cur_idx == self.page.initial_items.len() - 1 {
|
if this.cur_idx == this.page.initial_items.len() - 1 {
|
||||||
self.cur_idx = 0;
|
this.cur_idx = 0;
|
||||||
self.use_initial = false;
|
this.use_initial = false;
|
||||||
} else {
|
} else {
|
||||||
self.cur_idx += 1;
|
this.cur_idx += 1;
|
||||||
}
|
}
|
||||||
Some(self.page.initial_items[idx].clone())
|
let item = this.page.initial_items[idx].clone();
|
||||||
|
// let item = Box::pin(item);
|
||||||
|
// pin_mut!(item);
|
||||||
|
Some((item, this))
|
||||||
} else {
|
} else {
|
||||||
if self.need_next_page() {
|
if this.need_next_page() {
|
||||||
if self.fill_next_page().is_none() {
|
if this.fill_next_page().await.is_none() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let idx = self.cur_idx;
|
let idx = this.cur_idx;
|
||||||
self.cur_idx += 1;
|
this.cur_idx += 1;
|
||||||
Some(self.buffer[idx].clone())
|
let item = this.buffer[idx].clone();
|
||||||
}
|
// let item = Box::pin(item);
|
||||||
|
// pin_mut!(item);
|
||||||
|
Some((item, this))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{error, fmt, io::Error as IoError};
|
use std::{error, fmt, io::Error as IoError, num::TryFromIntError};
|
||||||
|
|
||||||
#[cfg(feature = "env")]
|
#[cfg(feature = "env")]
|
||||||
use envy::Error as EnvyError;
|
use envy::Error as EnvyError;
|
||||||
|
@ -12,7 +12,7 @@ use serde_urlencoded::ser::Error as UrlEncodedError;
|
||||||
use tomlcrate::de::Error as TomlDeError;
|
use tomlcrate::de::Error as TomlDeError;
|
||||||
#[cfg(feature = "toml")]
|
#[cfg(feature = "toml")]
|
||||||
use tomlcrate::ser::Error as TomlSerError;
|
use tomlcrate::ser::Error as TomlSerError;
|
||||||
use tungstenite::error::Error as WebSocketError;
|
use tungstenite::{error::Error as WebSocketError, Message as WebSocketMessage};
|
||||||
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.
|
||||||
|
@ -64,6 +64,14 @@ pub enum Error {
|
||||||
SerdeQs(SerdeQsError),
|
SerdeQs(SerdeQsError),
|
||||||
/// WebSocket error
|
/// WebSocket error
|
||||||
WebSocket(WebSocketError),
|
WebSocket(WebSocketError),
|
||||||
|
/// An integer conversion was attempted, but the value didn't fit into the
|
||||||
|
/// target type.
|
||||||
|
///
|
||||||
|
/// At the time of writing, this can only be triggered when a file is
|
||||||
|
/// larger than the system's usize allows.
|
||||||
|
IntConversion(TryFromIntError),
|
||||||
|
/// A stream message was received that wasn't recognized
|
||||||
|
UnrecognizedStreamMessage(WebSocketMessage),
|
||||||
/// Other errors
|
/// Other errors
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
@ -93,12 +101,13 @@ impl error::Error for Error {
|
||||||
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::IntConversion(ref e) => e,
|
||||||
Error::Client(..) | Error::Server(..) => return None,
|
Error::Client(..) | Error::Server(..) => return None,
|
||||||
Error::ClientIdRequired => return None,
|
Error::ClientIdRequired => return None,
|
||||||
Error::ClientSecretRequired => return None,
|
Error::ClientSecretRequired => return None,
|
||||||
Error::AccessTokenRequired => return None,
|
Error::AccessTokenRequired => return None,
|
||||||
Error::MissingField(_) => return None,
|
Error::MissingField(_) => return None,
|
||||||
|
Error::UnrecognizedStreamMessage(_) => return None,
|
||||||
Error::Other(..) => return None,
|
Error::Other(..) => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -122,7 +131,7 @@ impl fmt::Display for ApiError {
|
||||||
impl error::Error for ApiError {}
|
impl error::Error for ApiError {}
|
||||||
|
|
||||||
macro_rules! from {
|
macro_rules! from {
|
||||||
($($(#[$met:meta])* $typ:ident, $variant:ident,)*) => {
|
($($(#[$met:meta])* $typ:ident => $variant:ident,)*) => {
|
||||||
$(
|
$(
|
||||||
$(#[$met])*
|
$(#[$met])*
|
||||||
impl From<$typ> for Error {
|
impl From<$typ> for Error {
|
||||||
|
@ -136,20 +145,22 @@ macro_rules! from {
|
||||||
}
|
}
|
||||||
|
|
||||||
from! {
|
from! {
|
||||||
HttpError, Http,
|
HttpError => Http,
|
||||||
IoError, Io,
|
IoError => Io,
|
||||||
SerdeError, Serde,
|
SerdeError => Serde,
|
||||||
UrlEncodedError, UrlEncoded,
|
UrlEncodedError => UrlEncoded,
|
||||||
UrlError, Url,
|
UrlError => Url,
|
||||||
ApiError, Api,
|
ApiError => Api,
|
||||||
#[cfg(feature = "toml")] TomlSerError, TomlSer,
|
#[cfg(feature = "toml")] TomlSerError => TomlSer,
|
||||||
#[cfg(feature = "toml")] TomlDeError, TomlDe,
|
#[cfg(feature = "toml")] TomlDeError => TomlDe,
|
||||||
HeaderStrError, HeaderStrError,
|
HeaderStrError => HeaderStrError,
|
||||||
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,
|
||||||
|
TryFromIntError => IntConversion,
|
||||||
|
WebSocketMessage => UnrecognizedStreamMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
@ -157,8 +168,7 @@ from! {
|
||||||
macro_rules! format_err {
|
macro_rules! format_err {
|
||||||
( $( $arg:tt )* ) => {
|
( $( $arg:tt )* ) => {
|
||||||
{
|
{
|
||||||
use elefren::Error;
|
crate::Error::Other(format!($($arg)*))
|
||||||
Error::Other(format!($($arg)*))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,9 +187,9 @@ mod tests {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn from_http_error() {
|
async fn from_http_error() {
|
||||||
let err: HttpError = reqwest::get("not an actual URL").unwrap_err();
|
let err: HttpError = reqwest::get("not an actual URL").await.unwrap_err();
|
||||||
let err: Error = Error::from(err);
|
let err: Error = Error::from(err);
|
||||||
assert_is!(err, Error::Http(..));
|
assert_is!(err, Error::Http(..));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
use crate::{
|
||||||
|
entities::{event::Event, prelude::Notification, status::Status},
|
||||||
|
errors::Result,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
use futures::{stream::try_unfold, TryStream};
|
||||||
|
use log::debug;
|
||||||
|
use tungstenite::Message;
|
||||||
|
|
||||||
|
/// Returns a stream of events at the given url location.
|
||||||
|
pub fn event_stream(
|
||||||
|
location: impl AsRef<str>,
|
||||||
|
) -> Result<impl TryStream<Ok = Event, Error = Error, Item = Result<Event>>> {
|
||||||
|
let (client, response) = tungstenite::connect(location.as_ref())?;
|
||||||
|
let status = response.status();
|
||||||
|
if !status.is_success() {
|
||||||
|
return Err(Error::Api(crate::ApiError {
|
||||||
|
error: status.canonical_reason().map(String::from),
|
||||||
|
error_description: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Ok(try_unfold(client, |mut client| async move {
|
||||||
|
let mut lines = vec![];
|
||||||
|
loop {
|
||||||
|
match client.read_message() {
|
||||||
|
Ok(Message::Text(message)) => {
|
||||||
|
let line = message.trim().to_string();
|
||||||
|
if line.starts_with(":") || line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lines.push(line);
|
||||||
|
if let Ok(event) = make_event(&lines) {
|
||||||
|
lines.clear();
|
||||||
|
return Ok(Some((event, client)));
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(Message::Ping(data)) => {
|
||||||
|
debug!("received ping, ponging (metadata: {data:?})");
|
||||||
|
client.write_message(Message::Pong(data))?;
|
||||||
|
},
|
||||||
|
Ok(message) => return Err(message.into()),
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_event(lines: &[String]) -> Result<Event> {
|
||||||
|
let event;
|
||||||
|
let data;
|
||||||
|
if let Some(event_line) = lines.iter().find(|line| line.starts_with("event:")) {
|
||||||
|
event = event_line[6..].trim().to_string();
|
||||||
|
data = lines
|
||||||
|
.iter()
|
||||||
|
.find(|line| line.starts_with("data:"))
|
||||||
|
.map(|x| x[5..].trim().to_string());
|
||||||
|
} else {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Message {
|
||||||
|
pub event: String,
|
||||||
|
pub payload: Option<String>,
|
||||||
|
}
|
||||||
|
let message = serde_json::from_str::<Message>(&lines[0])?;
|
||||||
|
event = message.event;
|
||||||
|
data = message.payload;
|
||||||
|
}
|
||||||
|
let event: &str = &event;
|
||||||
|
Ok(match event {
|
||||||
|
"notification" => {
|
||||||
|
let data = data
|
||||||
|
.ok_or_else(|| Error::Other("Missing `data` line for notification".to_string()))?;
|
||||||
|
let notification = serde_json::from_str::<Notification>(&data)?;
|
||||||
|
Event::Notification(notification)
|
||||||
|
},
|
||||||
|
"update" => {
|
||||||
|
let data =
|
||||||
|
data.ok_or_else(|| Error::Other("Missing `data` line for update".to_string()))?;
|
||||||
|
let status = serde_json::from_str::<Status>(&data)?;
|
||||||
|
Event::Update(status)
|
||||||
|
},
|
||||||
|
"delete" => {
|
||||||
|
let data =
|
||||||
|
data.ok_or_else(|| Error::Other("Missing `data` line for delete".to_string()))?;
|
||||||
|
Event::Delete(data)
|
||||||
|
},
|
||||||
|
"filters_changed" => Event::FiltersChanged,
|
||||||
|
_ => return Err(Error::Other(format!("Unknown event `{}`", event))),
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
use std::io::{self, BufRead, Write};
|
use std::io::{self, BufRead, Write};
|
||||||
|
|
||||||
use crate::{errors::Result, http_send::HttpSend, registration::Registered, Mastodon};
|
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<H: HttpSend>(registration: Registered<H>) -> Result<Mastodon<H>> {
|
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<H: HttpSend>(registration: Registered<H>) -> 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();
|
||||||
Ok(registration.complete(code)?)
|
registration.complete(code).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
use crate::Result;
|
|
||||||
use reqwest::{Client, Request, RequestBuilder, Response};
|
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
/// Abstracts away the process of turning an HTTP request into an HTTP response
|
|
||||||
pub trait HttpSend: Clone + Debug {
|
|
||||||
/// Converts an HTTP request into an HTTP response
|
|
||||||
fn execute(&self, client: &Client, request: Request) -> Result<Response>;
|
|
||||||
|
|
||||||
/// Convenience method so that .build() doesn't have to be called at every
|
|
||||||
/// call site
|
|
||||||
fn send(&self, client: &Client, builder: RequestBuilder) -> Result<Response> {
|
|
||||||
let request = builder.build()?;
|
|
||||||
self.execute(client, request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct HttpSender;
|
|
||||||
|
|
||||||
impl HttpSend for HttpSender {
|
|
||||||
fn execute(&self, client: &Client, request: Request) -> Result<Response> {
|
|
||||||
Ok(client.execute(request)?)
|
|
||||||
}
|
|
||||||
}
|
|
704
src/lib.rs
704
src/lib.rs
|
@ -1,43 +1,55 @@
|
||||||
//! // Elefren: API Wrapper around the Mastodon API.
|
//! # Elefren: API Wrapper around the Mastodon API.
|
||||||
//!
|
//!
|
||||||
//! Most of the api is documented on [Mastodon's website](https://docs.joinmastodon.org/client/intro/)
|
//! Most of the api is documented on [Mastodon's website](https://docs.joinmastodon.org/client/intro/)
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use elefren::{helpers::cli, prelude::*};
|
//! use elefren::{helpers::cli, prelude::*};
|
||||||
|
//! use futures_util::StreamExt;
|
||||||
//!
|
//!
|
||||||
|
//! tokio_test::block_on(async {
|
||||||
//! let registration = Registration::new("https://botsin.space")
|
//! let registration = Registration::new("https://botsin.space")
|
||||||
//! .client_name("elefren_test")
|
//! .client_name("elefren_test")
|
||||||
//! .build()
|
//! .build()
|
||||||
|
//! .await
|
||||||
//! .unwrap();
|
//! .unwrap();
|
||||||
//! let mastodon = cli::authenticate(registration).unwrap();
|
//! let mastodon = cli::authenticate(registration).await.unwrap();
|
||||||
//!
|
//!
|
||||||
//! println!(
|
//! println!(
|
||||||
//! "{:?}",
|
//! "{:?}",
|
||||||
//! mastodon
|
//! mastodon
|
||||||
//! .get_home_timeline()
|
//! .get_home_timeline()
|
||||||
|
//! .await
|
||||||
//! .unwrap()
|
//! .unwrap()
|
||||||
//! .items_iter()
|
//! .items_iter()
|
||||||
//! .take(100)
|
//! .take(100)
|
||||||
//! .collect::<Vec<_>>()
|
//! .collect::<Vec<_>>()
|
||||||
|
//! .await
|
||||||
//! );
|
//! );
|
||||||
|
//! });
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Elefren also supports Mastodon's Streaming API:
|
//! Elefren also supports Mastodon's Streaming API:
|
||||||
//!
|
//!
|
||||||
//! // Example
|
//! ## Example
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use elefren::{prelude::*, entities::event::Event};
|
//! use elefren::{prelude::*, entities::event::Event};
|
||||||
|
//! use futures_util::TryStreamExt;
|
||||||
|
//!
|
||||||
//! let data = Data::default();
|
//! let data = Data::default();
|
||||||
//! let client = Mastodon::from(data);
|
//! let client = Mastodon::from(data);
|
||||||
//! for event in client.streaming_user().unwrap() {
|
//! tokio_test::block_on(async {
|
||||||
|
//! let stream = client.streaming_user().await.unwrap();
|
||||||
|
//! stream.try_for_each(|event| async move {
|
||||||
//! match event {
|
//! match event {
|
||||||
//! Event::Update(ref status) => { /* .. */ },
|
//! Event::Update(ref status) => { /* .. */ },
|
||||||
//! Event::Notification(ref notification) => { /* .. */ },
|
//! Event::Notification(ref notification) => { /* .. */ },
|
||||||
//! Event::Delete(ref id) => { /* .. */ },
|
//! Event::Delete(ref id) => { /* .. */ },
|
||||||
//! Event::FiltersChanged => { /* .. */ },
|
//! Event::FiltersChanged => { /* .. */ },
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! Ok(())
|
||||||
|
//! }).await.unwrap();
|
||||||
|
//! });
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
#![deny(
|
#![deny(
|
||||||
|
@ -53,7 +65,7 @@
|
||||||
unused_qualifications
|
unused_qualifications
|
||||||
)]
|
)]
|
||||||
|
|
||||||
#[macro_use]
|
// #[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate doc_comment;
|
extern crate doc_comment;
|
||||||
|
@ -84,20 +96,13 @@ extern crate tempfile;
|
||||||
#[cfg_attr(all(test, any(feature = "toml", feature = "json")), macro_use)]
|
#[cfg_attr(all(test, any(feature = "toml", feature = "json")), macro_use)]
|
||||||
extern crate indoc;
|
extern crate indoc;
|
||||||
|
|
||||||
use std::{borrow::Cow, io::BufRead, ops};
|
|
||||||
|
|
||||||
use reqwest::{Client, RequestBuilder, Response};
|
|
||||||
use tap_reader::Tap;
|
|
||||||
use tungstenite::client::AutoStream;
|
|
||||||
|
|
||||||
use entities::prelude::*;
|
|
||||||
use http_send::{HttpSend, HttpSender};
|
|
||||||
use page::Page;
|
use page::Page;
|
||||||
|
|
||||||
pub use data::Data;
|
pub use data::Data;
|
||||||
pub use errors::{ApiError, Error, Result};
|
pub use errors::{ApiError, Error, Result};
|
||||||
pub use isolang::Language;
|
pub use isolang::Language;
|
||||||
pub use mastodon_client::{MastodonClient, MastodonUnauthenticated};
|
pub use mastodon::{Mastodon, MastodonUnauthenticated};
|
||||||
|
// pub use mastodon_client::{MastodonClient, MastodonUnauthenticated};
|
||||||
pub use registration::Registration;
|
pub use registration::Registration;
|
||||||
pub use requests::{
|
pub use requests::{
|
||||||
AddFilterRequest,
|
AddFilterRequest,
|
||||||
|
@ -116,11 +121,10 @@ pub mod data;
|
||||||
pub mod entities;
|
pub mod entities;
|
||||||
/// Errors
|
/// Errors
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
/// Event stream generators
|
||||||
|
pub mod event_stream;
|
||||||
/// Collection of helpers for serializing/deserializing `Data` objects
|
/// Collection of helpers for serializing/deserializing `Data` objects
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
/// Contains trait for converting `reqwest::Request`s to `reqwest::Response`s
|
|
||||||
pub mod http_send;
|
|
||||||
mod mastodon_client;
|
|
||||||
/// Handling multiple pages of entities.
|
/// Handling multiple pages of entities.
|
||||||
pub mod page;
|
pub mod page;
|
||||||
/// Registering your app.
|
/// Registering your app.
|
||||||
|
@ -139,670 +143,12 @@ pub mod prelude {
|
||||||
scopes::Scopes,
|
scopes::Scopes,
|
||||||
Data,
|
Data,
|
||||||
Mastodon,
|
Mastodon,
|
||||||
MastodonClient,
|
// MastodonClient,
|
||||||
NewStatus,
|
NewStatus,
|
||||||
Registration,
|
Registration,
|
||||||
StatusBuilder,
|
StatusBuilder,
|
||||||
StatusesRequest,
|
StatusesRequest,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/// The mastodon client
|
||||||
/// Your mastodon application client, handles all requests to and from Mastodon.
|
pub mod mastodon;
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Mastodon<H: HttpSend = HttpSender> {
|
|
||||||
client: Client,
|
|
||||||
http_sender: H,
|
|
||||||
/// Raw data about your mastodon instance.
|
|
||||||
pub data: Data,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<H: HttpSend> Mastodon<H> {
|
|
||||||
methods![get, post, delete,];
|
|
||||||
|
|
||||||
fn route(&self, url: &str) -> String {
|
|
||||||
format!("{}{}", self.base, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn send(&self, req: RequestBuilder) -> Result<Response> {
|
|
||||||
Ok(self
|
|
||||||
.http_sender
|
|
||||||
.send(&self.client, req.bearer_auth(&self.token))?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Data> for Mastodon<HttpSender> {
|
|
||||||
/// Creates a mastodon instance from the data struct.
|
|
||||||
fn from(data: Data) -> Mastodon<HttpSender> {
|
|
||||||
let mut builder = MastodonBuilder::new(HttpSender);
|
|
||||||
builder.data(data);
|
|
||||||
builder
|
|
||||||
.build()
|
|
||||||
.expect("We know `data` is present, so this should be fine")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<H: HttpSend> MastodonClient<H> for Mastodon<H> {
|
|
||||||
type Stream = EventReader<WebSocket>;
|
|
||||||
|
|
||||||
paged_routes! {
|
|
||||||
(get) favourites: "favourites" => Status,
|
|
||||||
(get) blocks: "blocks" => Account,
|
|
||||||
(get) domain_blocks: "domain_blocks" => String,
|
|
||||||
(get) follow_requests: "follow_requests" => Account,
|
|
||||||
(get) get_home_timeline: "timelines/home" => Status,
|
|
||||||
(get) get_emojis: "custom_emojis" => Emoji,
|
|
||||||
(get) mutes: "mutes" => Account,
|
|
||||||
(get) notifications: "notifications" => Notification,
|
|
||||||
(get) reports: "reports" => Report,
|
|
||||||
(get (q: &'a str, #[serde(skip_serializing_if = "Option::is_none")] limit: Option<u64>, following: bool,)) search_accounts: "accounts/search" => Account,
|
|
||||||
(get) get_endorsements: "endorsements" => Account,
|
|
||||||
}
|
|
||||||
|
|
||||||
paged_routes_with_id! {
|
|
||||||
(get) followers: "accounts/{}/followers" => Account,
|
|
||||||
(get) following: "accounts/{}/following" => Account,
|
|
||||||
(get) reblogged_by: "statuses/{}/reblogged_by" => Account,
|
|
||||||
(get) favourited_by: "statuses/{}/favourited_by" => Account,
|
|
||||||
}
|
|
||||||
|
|
||||||
route! {
|
|
||||||
(delete (domain: String,)) unblock_domain: "domain_blocks" => Empty,
|
|
||||||
(get) instance: "instance" => Instance,
|
|
||||||
(get) verify_credentials: "accounts/verify_credentials" => Account,
|
|
||||||
(post (account_id: &str, status_ids: Vec<&str>, comment: String,)) report: "reports" => Report,
|
|
||||||
(post (domain: String,)) block_domain: "domain_blocks" => Empty,
|
|
||||||
(post (id: &str,)) authorize_follow_request: "accounts/follow_requests/authorize" => Empty,
|
|
||||||
(post (id: &str,)) reject_follow_request: "accounts/follow_requests/reject" => Empty,
|
|
||||||
(get (q: &'a str, resolve: bool,)) search: "search" => SearchResult,
|
|
||||||
(get (local: bool,)) get_public_timeline: "timelines/public" => Vec<Status>,
|
|
||||||
(post (uri: Cow<'static, str>,)) follows: "follows" => Account,
|
|
||||||
(post multipart (file: Cow<'static, str>,)) media: "media" => Attachment,
|
|
||||||
(post) clear_notifications: "notifications/clear" => Empty,
|
|
||||||
(post (id: &str,)) dismiss_notification: "notifications/dismiss" => Empty,
|
|
||||||
(get) get_push_subscription: "push/subscription" => Subscription,
|
|
||||||
(delete) delete_push_subscription: "push/subscription" => Empty,
|
|
||||||
(get) get_filters: "filters" => Vec<Filter>,
|
|
||||||
(get) get_follow_suggestions: "suggestions" => Vec<Account>,
|
|
||||||
}
|
|
||||||
|
|
||||||
route_v2! {
|
|
||||||
(get (q: &'a str, resolve: bool,)) search_v2: "search" => SearchResultV2,
|
|
||||||
}
|
|
||||||
|
|
||||||
route_id! {
|
|
||||||
(get) get_account: "accounts/{}" => Account,
|
|
||||||
(post) follow: "accounts/{}/follow" => Relationship,
|
|
||||||
(post) unfollow: "accounts/{}/unfollow" => Relationship,
|
|
||||||
(post) block: "accounts/{}/block" => Relationship,
|
|
||||||
(post) unblock: "accounts/{}/unblock" => Relationship,
|
|
||||||
(get) mute: "accounts/{}/mute" => Relationship,
|
|
||||||
(get) unmute: "accounts/{}/unmute" => Relationship,
|
|
||||||
(get) get_notification: "notifications/{}" => Notification,
|
|
||||||
(get) get_status: "statuses/{}" => Status,
|
|
||||||
(get) get_context: "statuses/{}/context" => Context,
|
|
||||||
(get) get_card: "statuses/{}/card" => Card,
|
|
||||||
(post) reblog: "statuses/{}/reblog" => Status,
|
|
||||||
(post) unreblog: "statuses/{}/unreblog" => Status,
|
|
||||||
(post) favourite: "statuses/{}/favourite" => Status,
|
|
||||||
(post) unfavourite: "statuses/{}/unfavourite" => Status,
|
|
||||||
(delete) delete_status: "statuses/{}" => Empty,
|
|
||||||
(get) get_filter: "filters/{}" => Filter,
|
|
||||||
(delete) delete_filter: "filters/{}" => Empty,
|
|
||||||
(delete) delete_from_suggestions: "suggestions/{}" => Empty,
|
|
||||||
(post) endorse_user: "accounts/{}/pin" => Relationship,
|
|
||||||
(post) unendorse_user: "accounts/{}/unpin" => Relationship,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> {
|
|
||||||
let url = self.route("/api/v1/filters");
|
|
||||||
let response = self.send(self.client.post(&url).json(&request))?;
|
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
|
|
||||||
if status.is_client_error() {
|
|
||||||
return Err(Error::Client(status.clone()));
|
|
||||||
} else if status.is_server_error() {
|
|
||||||
return Err(Error::Server(status.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialise(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PUT /api/v1/filters/:id
|
|
||||||
fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result<Filter> {
|
|
||||||
let url = self.route(&format!("/api/v1/filters/{}", id));
|
|
||||||
let response = self.send(self.client.put(&url).json(&request))?;
|
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
|
|
||||||
if status.is_client_error() {
|
|
||||||
return Err(Error::Client(status.clone()));
|
|
||||||
} else if status.is_server_error() {
|
|
||||||
return Err(Error::Server(status.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialise(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result<Account> {
|
|
||||||
let changes = builder.build()?;
|
|
||||||
let url = self.route("/api/v1/accounts/update_credentials");
|
|
||||||
let response = self.send(self.client.patch(&url).json(&changes))?;
|
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
|
|
||||||
if status.is_client_error() {
|
|
||||||
return Err(Error::Client(status.clone()));
|
|
||||||
} else if status.is_server_error() {
|
|
||||||
return Err(Error::Server(status.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialise(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Post a new status to the account.
|
|
||||||
fn new_status(&self, status: NewStatus) -> Result<Status> {
|
|
||||||
let response = self.send(
|
|
||||||
self.client
|
|
||||||
.post(&self.route("/api/v1/statuses"))
|
|
||||||
.json(&status),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
deserialise(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get timeline filtered by a hashtag(eg. `#coffee`) either locally or
|
|
||||||
/// federated.
|
|
||||||
fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> {
|
|
||||||
let base = "/api/v1/timelines/tag/";
|
|
||||||
let url = if local {
|
|
||||||
self.route(&format!("{}{}?local=1", base, hashtag))
|
|
||||||
} else {
|
|
||||||
self.route(&format!("{}{}", base, hashtag))
|
|
||||||
};
|
|
||||||
|
|
||||||
self.get(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get statuses of a single account by id. Optionally only with pictures
|
|
||||||
/// and or excluding replies.
|
|
||||||
///
|
|
||||||
/// // Example
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use elefren::prelude::*;
|
|
||||||
/// let data = Data::default();
|
|
||||||
/// let client = Mastodon::from(data);
|
|
||||||
/// let statuses = client.statuses("user-id", None).unwrap();
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use elefren::prelude::*;
|
|
||||||
/// let data = Data::default();
|
|
||||||
/// let client = Mastodon::from(data);
|
|
||||||
/// let mut request = StatusesRequest::new();
|
|
||||||
/// request.only_media();
|
|
||||||
/// let statuses = client.statuses("user-id", request).unwrap();
|
|
||||||
/// ```
|
|
||||||
fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result<Page<Status, H>>
|
|
||||||
where
|
|
||||||
S: Into<Option<StatusesRequest<'a>>>,
|
|
||||||
{
|
|
||||||
let mut url = format!("{}/api/v1/accounts/{}/statuses", self.base, id);
|
|
||||||
|
|
||||||
if let Some(request) = request.into() {
|
|
||||||
url = format!("{}{}", url, request.to_querystring()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = self.send(self.client.get(&url))?;
|
|
||||||
|
|
||||||
Page::new(self, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the client account's relationship to a list of other accounts.
|
|
||||||
/// Such as whether they follow them or vice versa.
|
|
||||||
fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship, H>> {
|
|
||||||
let mut url = self.route("/api/v1/accounts/relationships?");
|
|
||||||
|
|
||||||
if ids.len() == 1 {
|
|
||||||
url += "id=";
|
|
||||||
url += &ids[0];
|
|
||||||
} else {
|
|
||||||
for id in ids {
|
|
||||||
url += "id[]=";
|
|
||||||
url += &id;
|
|
||||||
url += "&";
|
|
||||||
}
|
|
||||||
url.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = self.send(self.client.get(&url))?;
|
|
||||||
|
|
||||||
Page::new(self, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a push notifications subscription
|
|
||||||
fn add_push_subscription(&self, request: &AddPushRequest) -> Result<Subscription> {
|
|
||||||
let request = request.build()?;
|
|
||||||
let response = self.send(
|
|
||||||
self.client
|
|
||||||
.post(&self.route("/api/v1/push/subscription"))
|
|
||||||
.json(&request),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
deserialise(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the `data` portion of the push subscription associated with this
|
|
||||||
/// access token
|
|
||||||
fn update_push_data(&self, request: &UpdatePushRequest) -> Result<Subscription> {
|
|
||||||
let request = request.build();
|
|
||||||
let response = self.send(
|
|
||||||
self.client
|
|
||||||
.put(&self.route("/api/v1/push/subscription"))
|
|
||||||
.json(&request),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
deserialise(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all accounts that follow the authenticated user
|
|
||||||
fn follows_me(&self) -> Result<Page<Account, H>> {
|
|
||||||
let me = self.verify_credentials()?;
|
|
||||||
Ok(self.followers(&me.id)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all accounts that the authenticated user follows
|
|
||||||
fn followed_by_me(&self) -> Result<Page<Account, H>> {
|
|
||||||
let me = self.verify_credentials()?;
|
|
||||||
Ok(self.following(&me.id)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns events that are relevant to the authorized user, i.e. home
|
|
||||||
/// timeline & notifications
|
|
||||||
///
|
|
||||||
/// // Example
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use elefren::prelude::*;
|
|
||||||
/// use elefren::entities::event::Event;
|
|
||||||
/// let data = Data::default();
|
|
||||||
/// let client = Mastodon::from(data);
|
|
||||||
/// for event in client.streaming_user().unwrap() {
|
|
||||||
/// match event {
|
|
||||||
/// Event::Update(ref status) => { /* .. */ },
|
|
||||||
/// Event::Notification(ref notification) => { /* .. */ },
|
|
||||||
/// Event::Delete(ref id) => { /* .. */ },
|
|
||||||
/// Event::FiltersChanged => { /* .. */ },
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
fn streaming_user(&self) -> Result<Self::Stream> {
|
|
||||||
let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("access_token", &self.token)
|
|
||||||
.append_pair("stream", "user");
|
|
||||||
let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
|
|
||||||
let new_scheme = match url.scheme() {
|
|
||||||
"http" => "ws",
|
|
||||||
"https" => "wss",
|
|
||||||
x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
|
|
||||||
};
|
|
||||||
url.set_scheme(new_scheme)
|
|
||||||
.map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
|
|
||||||
|
|
||||||
let client = tungstenite::connect(url.as_str())?.0;
|
|
||||||
|
|
||||||
Ok(EventReader(WebSocket(client)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns all public statuses
|
|
||||||
fn streaming_public(&self) -> Result<Self::Stream> {
|
|
||||||
let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("access_token", &self.token)
|
|
||||||
.append_pair("stream", "public");
|
|
||||||
let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
|
|
||||||
let new_scheme = match url.scheme() {
|
|
||||||
"http" => "ws",
|
|
||||||
"https" => "wss",
|
|
||||||
x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
|
|
||||||
};
|
|
||||||
url.set_scheme(new_scheme)
|
|
||||||
.map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
|
|
||||||
|
|
||||||
let client = tungstenite::connect(url.as_str())?.0;
|
|
||||||
|
|
||||||
Ok(EventReader(WebSocket(client)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all local statuses
|
|
||||||
fn streaming_local(&self) -> Result<Self::Stream> {
|
|
||||||
let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("access_token", &self.token)
|
|
||||||
.append_pair("stream", "public:local");
|
|
||||||
let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
|
|
||||||
let new_scheme = match url.scheme() {
|
|
||||||
"http" => "ws",
|
|
||||||
"https" => "wss",
|
|
||||||
x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
|
|
||||||
};
|
|
||||||
url.set_scheme(new_scheme)
|
|
||||||
.map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
|
|
||||||
|
|
||||||
let client = tungstenite::connect(url.as_str())?.0;
|
|
||||||
|
|
||||||
Ok(EventReader(WebSocket(client)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all public statuses for a particular hashtag
|
|
||||||
fn streaming_public_hashtag(&self, hashtag: &str) -> Result<Self::Stream> {
|
|
||||||
let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("access_token", &self.token)
|
|
||||||
.append_pair("stream", "hashtag")
|
|
||||||
.append_pair("tag", hashtag);
|
|
||||||
let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
|
|
||||||
let new_scheme = match url.scheme() {
|
|
||||||
"http" => "ws",
|
|
||||||
"https" => "wss",
|
|
||||||
x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
|
|
||||||
};
|
|
||||||
url.set_scheme(new_scheme)
|
|
||||||
.map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
|
|
||||||
|
|
||||||
let client = tungstenite::connect(url.as_str())?.0;
|
|
||||||
|
|
||||||
Ok(EventReader(WebSocket(client)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all local statuses for a particular hashtag
|
|
||||||
fn streaming_local_hashtag(&self, hashtag: &str) -> Result<Self::Stream> {
|
|
||||||
let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("access_token", &self.token)
|
|
||||||
.append_pair("stream", "hashtag:local")
|
|
||||||
.append_pair("tag", hashtag);
|
|
||||||
let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
|
|
||||||
let new_scheme = match url.scheme() {
|
|
||||||
"http" => "ws",
|
|
||||||
"https" => "wss",
|
|
||||||
x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
|
|
||||||
};
|
|
||||||
url.set_scheme(new_scheme)
|
|
||||||
.map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
|
|
||||||
|
|
||||||
let client = tungstenite::connect(url.as_str())?.0;
|
|
||||||
|
|
||||||
Ok(EventReader(WebSocket(client)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns statuses for a list
|
|
||||||
fn streaming_list(&self, list_id: &str) -> Result<Self::Stream> {
|
|
||||||
let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("access_token", &self.token)
|
|
||||||
.append_pair("stream", "list")
|
|
||||||
.append_pair("list", list_id);
|
|
||||||
let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
|
|
||||||
let new_scheme = match url.scheme() {
|
|
||||||
"http" => "ws",
|
|
||||||
"https" => "wss",
|
|
||||||
x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
|
|
||||||
};
|
|
||||||
url.set_scheme(new_scheme)
|
|
||||||
.map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
|
|
||||||
|
|
||||||
let client = tungstenite::connect(url.as_str())?.0;
|
|
||||||
|
|
||||||
Ok(EventReader(WebSocket(client)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all direct messages
|
|
||||||
fn streaming_direct(&self) -> Result<Self::Stream> {
|
|
||||||
let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("access_token", &self.token)
|
|
||||||
.append_pair("stream", "direct");
|
|
||||||
let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
|
|
||||||
let new_scheme = match url.scheme() {
|
|
||||||
"http" => "ws",
|
|
||||||
"https" => "wss",
|
|
||||||
x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
|
|
||||||
};
|
|
||||||
url.set_scheme(new_scheme)
|
|
||||||
.map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
|
|
||||||
|
|
||||||
let client = tungstenite::connect(url.as_str())?.0;
|
|
||||||
|
|
||||||
Ok(EventReader(WebSocket(client)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
/// WebSocket newtype so that EventStream can be implemented without coherency
|
|
||||||
/// issues
|
|
||||||
pub struct WebSocket(tungstenite::protocol::WebSocket<AutoStream>);
|
|
||||||
|
|
||||||
/// A type that streaming events can be read from
|
|
||||||
pub trait EventStream {
|
|
||||||
/// Read a message from this stream
|
|
||||||
fn read_message(&mut self) -> Result<String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: BufRead> EventStream for R {
|
|
||||||
fn read_message(&mut self) -> Result<String> {
|
|
||||||
let mut buf = String::new();
|
|
||||||
self.read_line(&mut buf)?;
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventStream for WebSocket {
|
|
||||||
fn read_message(&mut self) -> Result<String> {
|
|
||||||
Ok(self.0.read_message()?.into_text()?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
/// Iterator that produces events from a mastodon streaming API event stream
|
|
||||||
pub struct EventReader<R: EventStream>(R);
|
|
||||||
impl<R: EventStream> Iterator for EventReader<R> {
|
|
||||||
type Item = Event;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
loop {
|
|
||||||
if let Ok(line) = self.0.read_message() {
|
|
||||||
let line = line.trim().to_string();
|
|
||||||
if line.starts_with(":") || line.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
lines.push(line);
|
|
||||||
if let Ok(event) = self.make_event(&lines) {
|
|
||||||
lines.clear();
|
|
||||||
return Some(event);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: EventStream> EventReader<R> {
|
|
||||||
fn make_event(&self, lines: &[String]) -> Result<Event> {
|
|
||||||
let event;
|
|
||||||
let data;
|
|
||||||
if let Some(event_line) = lines.iter().find(|line| line.starts_with("event:")) {
|
|
||||||
event = event_line[6..].trim().to_string();
|
|
||||||
data = lines
|
|
||||||
.iter()
|
|
||||||
.find(|line| line.starts_with("data:"))
|
|
||||||
.map(|x| x[5..].trim().to_string());
|
|
||||||
} else {
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct Message {
|
|
||||||
pub event: String,
|
|
||||||
pub payload: Option<String>,
|
|
||||||
}
|
|
||||||
let message = serde_json::from_str::<Message>(&lines[0])?;
|
|
||||||
event = message.event;
|
|
||||||
data = message.payload;
|
|
||||||
}
|
|
||||||
let event: &str = &event;
|
|
||||||
Ok(match event {
|
|
||||||
"notification" => {
|
|
||||||
let data = data.ok_or_else(|| {
|
|
||||||
Error::Other("Missing `data` line for notification".to_string())
|
|
||||||
})?;
|
|
||||||
let notification = serde_json::from_str::<Notification>(&data)?;
|
|
||||||
Event::Notification(notification)
|
|
||||||
},
|
|
||||||
"update" => {
|
|
||||||
let data =
|
|
||||||
data.ok_or_else(|| Error::Other("Missing `data` line for update".to_string()))?;
|
|
||||||
let status = serde_json::from_str::<Status>(&data)?;
|
|
||||||
Event::Update(status)
|
|
||||||
},
|
|
||||||
"delete" => {
|
|
||||||
let data =
|
|
||||||
data.ok_or_else(|| Error::Other("Missing `data` line for delete".to_string()))?;
|
|
||||||
Event::Delete(data)
|
|
||||||
},
|
|
||||||
"filters_changed" => Event::FiltersChanged,
|
|
||||||
_ => return Err(Error::Other(format!("Unknown event `{}`", event))),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<H: HttpSend> ops::Deref for Mastodon<H> {
|
|
||||||
type Target = Data;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MastodonBuilder<H: HttpSend> {
|
|
||||||
client: Option<Client>,
|
|
||||||
http_sender: H,
|
|
||||||
data: Option<Data>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<H: HttpSend> MastodonBuilder<H> {
|
|
||||||
pub fn new(sender: H) -> Self {
|
|
||||||
MastodonBuilder {
|
|
||||||
http_sender: sender,
|
|
||||||
client: None,
|
|
||||||
data: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn client(&mut self, client: Client) -> &mut Self {
|
|
||||||
self.client = Some(client);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn data(&mut self, data: Data) -> &mut Self {
|
|
||||||
self.data = Some(data);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(self) -> Result<Mastodon<H>> {
|
|
||||||
Ok(if let Some(data) = self.data {
|
|
||||||
Mastodon {
|
|
||||||
client: self.client.unwrap_or_else(|| Client::new()),
|
|
||||||
http_sender: self.http_sender,
|
|
||||||
data,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(Error::MissingField("missing field 'data'"));
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Client that can make unauthenticated calls to a mastodon instance
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct MastodonUnauth<H: HttpSend = HttpSender> {
|
|
||||||
client: Client,
|
|
||||||
http_sender: H,
|
|
||||||
base: url::Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MastodonUnauth<HttpSender> {
|
|
||||||
/// Create a new unauthenticated client
|
|
||||||
pub fn new(base: &str) -> Result<MastodonUnauth<HttpSender>> {
|
|
||||||
let base = if base.starts_with("https://") {
|
|
||||||
base.to_string()
|
|
||||||
} else {
|
|
||||||
format!("https://{}", base)
|
|
||||||
};
|
|
||||||
Ok(MastodonUnauth {
|
|
||||||
client: Client::new(),
|
|
||||||
http_sender: HttpSender,
|
|
||||||
base: url::Url::parse(&base)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<H: HttpSend> MastodonUnauth<H> {
|
|
||||||
fn route(&self, url: &str) -> Result<url::Url> {
|
|
||||||
Ok(self.base.join(url)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(&self, req: RequestBuilder) -> Result<Response> {
|
|
||||||
Ok(self.http_sender.send(&self.client, req)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<H: HttpSend> MastodonUnauthenticated<H> for MastodonUnauth<H> {
|
|
||||||
/// GET /api/v1/statuses/:id
|
|
||||||
fn get_status(&self, id: &str) -> Result<Status> {
|
|
||||||
let route = self.route("/api/v1/statuses")?;
|
|
||||||
let route = route.join(id)?;
|
|
||||||
let response = self.send(self.client.get(route))?;
|
|
||||||
deserialise(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GET /api/v1/statuses/:id/context
|
|
||||||
fn get_context(&self, id: &str) -> Result<Context> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GET /api/v1/statuses/:id/card
|
|
||||||
fn get_card(&self, id: &str) -> Result<Card> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the HTTP response body from JSON. Pass up deserialization errors
|
|
||||||
// transparently.
|
|
||||||
fn deserialise<T: for<'de> serde::Deserialize<'de>>(response: Response) -> Result<T> {
|
|
||||||
let mut reader = Tap::new(response);
|
|
||||||
|
|
||||||
match serde_json::from_reader(&mut reader) {
|
|
||||||
Ok(t) => {
|
|
||||||
debug!("{}", String::from_utf8_lossy(&reader.bytes));
|
|
||||||
Ok(t)
|
|
||||||
},
|
|
||||||
// If deserializing into the desired type fails try again to
|
|
||||||
// see if this is an error response.
|
|
||||||
Err(e) => {
|
|
||||||
error!("{}", String::from_utf8_lossy(&reader.bytes));
|
|
||||||
if let Ok(error) = serde_json::from_slice(&reader.bytes) {
|
|
||||||
return Err(Error::Api(error));
|
|
||||||
}
|
|
||||||
Err(e.into())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
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: impl AsRef<str>) -> Result<T>
|
||||||
-> Result<T>
|
|
||||||
{
|
{
|
||||||
let response = self.send(
|
let url = url.as_ref();
|
||||||
self.client.$method(&url)
|
Ok(
|
||||||
)?;
|
self.client
|
||||||
|
.$method(url)
|
||||||
deserialise(response)
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json()
|
||||||
|
.await?
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)+
|
)+
|
||||||
};
|
};
|
||||||
|
@ -30,13 +34,11 @@ macro_rules! paged_routes {
|
||||||
"client.", stringify!($name), "();\n",
|
"client.", stringify!($name), "();\n",
|
||||||
"```"
|
"```"
|
||||||
),
|
),
|
||||||
fn $name(&self) -> Result<Page<$ret, H>> {
|
pub async fn $name(&self) -> Result<Page<$ret>> {
|
||||||
let url = self.route(concat!("/api/v1/", $url));
|
let url = self.route(concat!("/api/v1/", $url));
|
||||||
let response = self.send(
|
let response = self.client.$method(&url).send().await?;
|
||||||
self.client.$method(&url)
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Page::new(self, response)
|
Page::new(self.clone(), response).await
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -51,7 +53,7 @@ macro_rules! paged_routes {
|
||||||
$url,
|
$url,
|
||||||
"`\n# Errors\nIf `access_token` is not set."
|
"`\n# Errors\nIf `access_token` is not set."
|
||||||
),
|
),
|
||||||
fn $name<'a>(&self, $($param: $typ,)*) -> Result<Page<$ret, H>> {
|
pub async fn $name<'a>(&self, $($param: $typ,)*) -> Result<Page<$ret>> {
|
||||||
use serde_urlencoded;
|
use serde_urlencoded;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -77,11 +79,9 @@ macro_rules! paged_routes {
|
||||||
|
|
||||||
let url = format!(concat!("/api/v1/", $url, "?{}"), &qs);
|
let url = format!(concat!("/api/v1/", $url, "?{}"), &qs);
|
||||||
|
|
||||||
let response = self.send(
|
let response = self.client.get(&url).send().await?;
|
||||||
self.client.get(&url)
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Page::new(self, response)
|
Page::new(self.clone(), response).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ macro_rules! route_v2 {
|
||||||
$url,
|
$url,
|
||||||
"`\n# Errors\nIf `access_token` is not set."
|
"`\n# Errors\nIf `access_token` is not set."
|
||||||
),
|
),
|
||||||
fn $name<'a>(&self, $($param: $typ,)*) -> Result<$ret> {
|
pub async fn $name<'a>(&self, $($param: $typ,)*) -> Result<$ret> {
|
||||||
use serde_urlencoded;
|
use serde_urlencoded;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -122,7 +122,7 @@ macro_rules! route_v2 {
|
||||||
|
|
||||||
let url = format!(concat!("/api/v2/", $url, "?{}"), &qs);
|
let url = format!(concat!("/api/v2/", $url, "?{}"), &qs);
|
||||||
|
|
||||||
Ok(self.get(self.route(&url))?)
|
self.get(self.route(&url)).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,19 +140,29 @@ macro_rules! route {
|
||||||
"Equivalent to `post /api/v1/",
|
"Equivalent to `post /api/v1/",
|
||||||
$url,
|
$url,
|
||||||
"`\n# Errors\nIf `access_token` is not set."),
|
"`\n# Errors\nIf `access_token` is not set."),
|
||||||
fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
|
pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
|
||||||
use reqwest::multipart::Form;
|
use reqwest::multipart::{Form, Part};
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
let form_data = Form::new()
|
let form_data = Form::new()
|
||||||
$(
|
$(
|
||||||
.file(stringify!($param), $param.as_ref())?
|
.part(stringify!($param), {
|
||||||
|
let mut file = std::fs::File::open($param.as_ref())?;
|
||||||
|
let mut data = if let Ok(metadata) = file.metadata() {
|
||||||
|
Vec::with_capacity(metadata.len().try_into()?)
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
file.read_to_end(&mut data)?;
|
||||||
|
Part::bytes(data)
|
||||||
|
})
|
||||||
)*;
|
)*;
|
||||||
|
|
||||||
let response = self.send(
|
let response = self.client
|
||||||
self.client
|
|
||||||
.post(&self.route(concat!("/api/v1/", $url)))
|
.post(&self.route(concat!("/api/v1/", $url)))
|
||||||
.multipart(form_data)
|
.multipart(form_data)
|
||||||
)?;
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
let status = response.status().clone();
|
let status = response.status().clone();
|
||||||
|
|
||||||
|
@ -162,7 +172,7 @@ macro_rules! route {
|
||||||
return Err(Error::Server(status));
|
return Err(Error::Server(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
deserialise(response)
|
Ok(response.json().await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,7 +186,7 @@ macro_rules! route {
|
||||||
$url,
|
$url,
|
||||||
"`\n# Errors\nIf `access_token` is not set."
|
"`\n# Errors\nIf `access_token` is not set."
|
||||||
),
|
),
|
||||||
fn $name<'a>(&self, $($param: $typ,)*) -> Result<$ret> {
|
pub async fn $name<'a>(&self, $($param: $typ,)*) -> Result<$ret> {
|
||||||
use serde_urlencoded;
|
use serde_urlencoded;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -199,7 +209,7 @@ macro_rules! route {
|
||||||
|
|
||||||
let url = format!(concat!("/api/v1/", $url, "?{}"), &qs);
|
let url = format!(concat!("/api/v1/", $url, "?{}"), &qs);
|
||||||
|
|
||||||
Ok(self.get(self.route(&url))?)
|
self.get(self.route(&url)).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +223,7 @@ macro_rules! route {
|
||||||
$url,
|
$url,
|
||||||
"`\n# Errors\nIf `access_token` is not set.",
|
"`\n# Errors\nIf `access_token` is not set.",
|
||||||
),
|
),
|
||||||
fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
|
pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
|
||||||
|
|
||||||
let form_data = json!({
|
let form_data = json!({
|
||||||
$(
|
$(
|
||||||
|
@ -221,10 +231,11 @@ macro_rules! route {
|
||||||
)*
|
)*
|
||||||
});
|
});
|
||||||
|
|
||||||
let response = self.send(
|
let response = self.client
|
||||||
self.client.$method(&self.route(concat!("/api/v1/", $url)))
|
.$method(&self.route(concat!("/api/v1/", $url)))
|
||||||
.json(&form_data)
|
.json(&form_data)
|
||||||
)?;
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
let status = response.status().clone();
|
let status = response.status().clone();
|
||||||
|
|
||||||
|
@ -234,7 +245,7 @@ macro_rules! route {
|
||||||
return Err(Error::Server(status));
|
return Err(Error::Server(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
deserialise(response)
|
Ok(response.json().await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,8 +266,8 @@ macro_rules! route {
|
||||||
"client.", stringify!($name), "();\n",
|
"client.", stringify!($name), "();\n",
|
||||||
"```"
|
"```"
|
||||||
),
|
),
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,8 +296,8 @@ macro_rules! route_id {
|
||||||
"# }\n",
|
"# }\n",
|
||||||
"```"
|
"```"
|
||||||
),
|
),
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
@ -309,13 +320,11 @@ macro_rules! paged_routes_with_id {
|
||||||
"client.", stringify!($name), "(\"some-id\");\n",
|
"client.", stringify!($name), "(\"some-id\");\n",
|
||||||
"```"
|
"```"
|
||||||
),
|
),
|
||||||
fn $name(&self, id: &str) -> Result<Page<$ret, H>> {
|
pub async fn $name(&self, id: &str) -> Result<Page<$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.client.$method(&url).send().await?;
|
||||||
self.client.$method(&url)
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Page::new(self, response)
|
Page::new(self.clone(), response).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,410 @@
|
||||||
|
use std::{borrow::Cow, ops::Deref, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
entities::{
|
||||||
|
account::Account,
|
||||||
|
prelude::*,
|
||||||
|
report::Report,
|
||||||
|
status::{Emoji, Status},
|
||||||
|
Empty,
|
||||||
|
},
|
||||||
|
errors::{Error, Result},
|
||||||
|
event_stream::event_stream,
|
||||||
|
AddFilterRequest,
|
||||||
|
AddPushRequest,
|
||||||
|
Data,
|
||||||
|
NewStatus,
|
||||||
|
Page,
|
||||||
|
StatusesRequest,
|
||||||
|
UpdateCredsRequest,
|
||||||
|
UpdatePushRequest,
|
||||||
|
};
|
||||||
|
use futures::TryStream;
|
||||||
|
use reqwest::Client;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// The Mastodon client is a smart pointer to this struct
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MastodonClient {
|
||||||
|
pub(crate) client: Client,
|
||||||
|
/// Raw data about your mastodon instance.
|
||||||
|
pub data: Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Your mastodon application client, handles all requests to and from Mastodon.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Mastodon(Arc<MastodonClient>);
|
||||||
|
|
||||||
|
/// A client for making unauthenticated requests to the public API.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MastodonUnauthenticated {
|
||||||
|
client: Client,
|
||||||
|
/// Which Mastodon instance to contact
|
||||||
|
pub base: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Data> for Mastodon {
|
||||||
|
/// Creates a mastodon instance from the data struct.
|
||||||
|
fn from(data: Data) -> Mastodon {
|
||||||
|
MastodonClient {
|
||||||
|
client: Client::new(),
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Mastodon {
|
||||||
|
methods![get, post, delete,];
|
||||||
|
|
||||||
|
paged_routes! {
|
||||||
|
(get) favourites: "favourites" => Status,
|
||||||
|
(get) blocks: "blocks" => Account,
|
||||||
|
(get) domain_blocks: "domain_blocks" => String,
|
||||||
|
(get) follow_requests: "follow_requests" => Account,
|
||||||
|
(get) get_home_timeline: "timelines/home" => Status,
|
||||||
|
(get) get_emojis: "custom_emojis" => Emoji,
|
||||||
|
(get) mutes: "mutes" => Account,
|
||||||
|
(get) notifications: "notifications" => Notification,
|
||||||
|
(get) reports: "reports" => Report,
|
||||||
|
(get (q: &'a str, #[serde(skip_serializing_if = "Option::is_none")] limit: Option<u64>, following: bool,)) search_accounts: "accounts/search" => Account,
|
||||||
|
(get) get_endorsements: "endorsements" => Account,
|
||||||
|
}
|
||||||
|
|
||||||
|
paged_routes_with_id! {
|
||||||
|
(get) followers: "accounts/{}/followers" => Account,
|
||||||
|
(get) following: "accounts/{}/following" => Account,
|
||||||
|
(get) reblogged_by: "statuses/{}/reblogged_by" => Account,
|
||||||
|
(get) favourited_by: "statuses/{}/favourited_by" => Account,
|
||||||
|
}
|
||||||
|
|
||||||
|
route! {
|
||||||
|
(delete (domain: String,)) unblock_domain: "domain_blocks" => Empty,
|
||||||
|
(get) instance: "instance" => Instance,
|
||||||
|
(get) verify_credentials: "accounts/verify_credentials" => Account,
|
||||||
|
(post (account_id: &str, status_ids: Vec<&str>, comment: String,)) report: "reports" => Report,
|
||||||
|
(post (domain: String,)) block_domain: "domain_blocks" => Empty,
|
||||||
|
(post (id: &str,)) authorize_follow_request: "accounts/follow_requests/authorize" => Empty,
|
||||||
|
(post (id: &str,)) reject_follow_request: "accounts/follow_requests/reject" => Empty,
|
||||||
|
(get (q: &'a str, resolve: bool,)) search: "search" => SearchResult,
|
||||||
|
(get (local: bool,)) get_public_timeline: "timelines/public" => Vec<Status>,
|
||||||
|
(post (uri: Cow<'static, str>,)) follows: "follows" => Account,
|
||||||
|
(post multipart (file: Cow<'static, str>,)) media: "media" => Attachment,
|
||||||
|
(post) clear_notifications: "notifications/clear" => Empty,
|
||||||
|
(post (id: &str,)) dismiss_notification: "notifications/dismiss" => Empty,
|
||||||
|
(get) get_push_subscription: "push/subscription" => Subscription,
|
||||||
|
(delete) delete_push_subscription: "push/subscription" => Empty,
|
||||||
|
(get) get_filters: "filters" => Vec<Filter>,
|
||||||
|
(get) get_follow_suggestions: "suggestions" => Vec<Account>,
|
||||||
|
}
|
||||||
|
|
||||||
|
route_v2! {
|
||||||
|
(get (q: &'a str, resolve: bool,)) search_v2: "search" => SearchResultV2,
|
||||||
|
}
|
||||||
|
|
||||||
|
route_id! {
|
||||||
|
(get) get_account: "accounts/{}" => Account,
|
||||||
|
(post) follow: "accounts/{}/follow" => Relationship,
|
||||||
|
(post) unfollow: "accounts/{}/unfollow" => Relationship,
|
||||||
|
(post) block: "accounts/{}/block" => Relationship,
|
||||||
|
(post) unblock: "accounts/{}/unblock" => Relationship,
|
||||||
|
(get) mute: "accounts/{}/mute" => Relationship,
|
||||||
|
(get) unmute: "accounts/{}/unmute" => Relationship,
|
||||||
|
(get) get_notification: "notifications/{}" => Notification,
|
||||||
|
(get) get_status: "statuses/{}" => Status,
|
||||||
|
(get) get_context: "statuses/{}/context" => Context,
|
||||||
|
(get) get_card: "statuses/{}/card" => Card,
|
||||||
|
(post) reblog: "statuses/{}/reblog" => Status,
|
||||||
|
(post) unreblog: "statuses/{}/unreblog" => Status,
|
||||||
|
(post) favourite: "statuses/{}/favourite" => Status,
|
||||||
|
(post) unfavourite: "statuses/{}/unfavourite" => Status,
|
||||||
|
(delete) delete_status: "statuses/{}" => Empty,
|
||||||
|
(get) get_filter: "filters/{}" => Filter,
|
||||||
|
(delete) delete_filter: "filters/{}" => Empty,
|
||||||
|
(delete) delete_from_suggestions: "suggestions/{}" => Empty,
|
||||||
|
(post) endorse_user: "accounts/{}/pin" => Relationship,
|
||||||
|
(post) unendorse_user: "accounts/{}/unpin" => Relationship,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Mastodon Client
|
||||||
|
pub fn new(client: Client, data: Data) -> Self {
|
||||||
|
Mastodon(Arc::new(MastodonClient {
|
||||||
|
client,
|
||||||
|
data,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn route(&self, url: &str) -> String {
|
||||||
|
format!("{}{}", self.data.base, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /api/v1/filters
|
||||||
|
pub async fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> {
|
||||||
|
Ok(self
|
||||||
|
.client
|
||||||
|
.post(self.route("/api/v1/filters"))
|
||||||
|
.json(&request)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PUT /api/v1/filters/:id
|
||||||
|
pub async fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result<Filter> {
|
||||||
|
let url = self.route(&format!("/api/v1/filters/{}", id));
|
||||||
|
let response = self.client.put(&url).json(&request).send().await?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
|
||||||
|
if status.is_client_error() {
|
||||||
|
return Err(Error::Client(status.clone()));
|
||||||
|
} else if status.is_server_error() {
|
||||||
|
return Err(Error::Server(status.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response.json().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the user credentials
|
||||||
|
pub async fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result<Account> {
|
||||||
|
let changes = builder.build()?;
|
||||||
|
let url = self.route("/api/v1/accounts/update_credentials");
|
||||||
|
let response = self.client.patch(&url).json(&changes).send().await?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
|
||||||
|
if status.is_client_error() {
|
||||||
|
return Err(Error::Client(status.clone()));
|
||||||
|
} else if status.is_server_error() {
|
||||||
|
return Err(Error::Server(status.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response.json().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Post a new status to the account.
|
||||||
|
pub async fn new_status(&self, status: NewStatus) -> Result<Status> {
|
||||||
|
Ok(self
|
||||||
|
.client
|
||||||
|
.post(&self.route("/api/v1/statuses"))
|
||||||
|
.json(&status)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get timeline filtered by a hashtag(eg. `#coffee`) either locally or
|
||||||
|
/// federated.
|
||||||
|
pub async fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> {
|
||||||
|
let base = "/api/v1/timelines/tag/";
|
||||||
|
let url = if local {
|
||||||
|
self.route(&format!("{}{}?local=1", base, hashtag))
|
||||||
|
} else {
|
||||||
|
self.route(&format!("{}{}", base, hashtag))
|
||||||
|
};
|
||||||
|
|
||||||
|
self.get(url).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get statuses of a single account by id. Optionally only with pictures
|
||||||
|
/// and or excluding replies.
|
||||||
|
///
|
||||||
|
/// // Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use elefren::prelude::*;
|
||||||
|
/// tokio_test::block_on(async {
|
||||||
|
/// let data = Data::default();
|
||||||
|
/// let client = Mastodon::from(data);
|
||||||
|
/// let statuses = client.statuses("user-id", None).await.unwrap();
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use elefren::prelude::*;
|
||||||
|
/// tokio_test::block_on(async {
|
||||||
|
/// let data = Data::default();
|
||||||
|
/// let client = Mastodon::from(data);
|
||||||
|
/// let mut request = StatusesRequest::new();
|
||||||
|
/// request.only_media();
|
||||||
|
/// let statuses = client.statuses("user-id", request).await.unwrap();
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub async fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result<Page<Status>>
|
||||||
|
where
|
||||||
|
S: Into<Option<StatusesRequest<'a>>>,
|
||||||
|
{
|
||||||
|
let mut url = format!("{}/api/v1/accounts/{}/statuses", self.data.base, id);
|
||||||
|
|
||||||
|
if let Some(request) = request.into() {
|
||||||
|
url = format!("{}{}", url, request.to_querystring()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = self.client.get(&url).send().await?;
|
||||||
|
|
||||||
|
Page::new(self.clone(), response).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the client account's relationship to a list of other accounts.
|
||||||
|
/// Such as whether they follow them or vice versa.
|
||||||
|
pub async fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship>> {
|
||||||
|
let mut url = self.route("/api/v1/accounts/relationships?");
|
||||||
|
|
||||||
|
if ids.len() == 1 {
|
||||||
|
url += "id=";
|
||||||
|
url += &ids[0];
|
||||||
|
} else {
|
||||||
|
for id in ids {
|
||||||
|
url += "id[]=";
|
||||||
|
url += &id;
|
||||||
|
url += "&";
|
||||||
|
}
|
||||||
|
url.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = self.client.get(&url).send().await?;
|
||||||
|
|
||||||
|
Page::new(self.clone(), response).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a push notifications subscription
|
||||||
|
pub async fn add_push_subscription(&self, request: &AddPushRequest) -> Result<Subscription> {
|
||||||
|
let request = request.build()?;
|
||||||
|
Ok(self
|
||||||
|
.client
|
||||||
|
.post(&self.route("/api/v1/push/subscription"))
|
||||||
|
.json(&request)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the `data` portion of the push subscription associated with this
|
||||||
|
/// access token
|
||||||
|
pub async fn update_push_data(&self, request: &UpdatePushRequest) -> Result<Subscription> {
|
||||||
|
let request = request.build();
|
||||||
|
Ok(self
|
||||||
|
.client
|
||||||
|
.put(&self.route("/api/v1/push/subscription"))
|
||||||
|
.json(&request)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all accounts that follow the authenticated user
|
||||||
|
pub async fn follows_me(&self) -> Result<Page<Account>> {
|
||||||
|
let me = self.verify_credentials().await?;
|
||||||
|
self.followers(&me.id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all accounts that the authenticated user follows
|
||||||
|
pub async fn followed_by_me(&self) -> Result<Page<Account>> {
|
||||||
|
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
|
||||||
|
///
|
||||||
|
/// // Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use elefren::prelude::*;
|
||||||
|
/// use elefren::entities::event::Event;
|
||||||
|
/// use futures_util::{pin_mut, StreamExt, TryStreamExt};
|
||||||
|
///
|
||||||
|
/// tokio_test::block_on(async {
|
||||||
|
/// let data = Data::default();
|
||||||
|
/// let client = Mastodon::from(data);
|
||||||
|
/// let stream = client.streaming_user().await.unwrap();
|
||||||
|
/// stream.try_for_each(|event| async move {
|
||||||
|
/// match event {
|
||||||
|
/// Event::Update(ref status) => { /* .. */ },
|
||||||
|
/// Event::Notification(ref notification) => { /* .. */ },
|
||||||
|
/// Event::Delete(ref id) => { /* .. */ },
|
||||||
|
/// Event::FiltersChanged => { /* .. */ },
|
||||||
|
/// }
|
||||||
|
/// Ok(())
|
||||||
|
/// }).await.unwrap();
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub async fn streaming_user(&self) -> Result<impl TryStream<Ok = Event, Error = Error>> {
|
||||||
|
let mut url: Url = self.route("/api/v1/streaming").parse()?;
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("access_token", &self.data.token)
|
||||||
|
.append_pair("stream", "user");
|
||||||
|
let mut url: Url = reqwest::get(url.as_str()).await?.url().as_str().parse()?;
|
||||||
|
let new_scheme = match url.scheme() {
|
||||||
|
"http" => "ws",
|
||||||
|
"https" => "wss",
|
||||||
|
x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
|
||||||
|
};
|
||||||
|
url.set_scheme(new_scheme)
|
||||||
|
.map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
|
||||||
|
|
||||||
|
event_stream(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MastodonUnauthenticated {
|
||||||
|
methods![get,];
|
||||||
|
|
||||||
|
/// Create a new client for unauthenticated requests to a given Mastodon
|
||||||
|
/// instance.
|
||||||
|
pub fn new(base: impl AsRef<str>) -> Result<MastodonUnauthenticated> {
|
||||||
|
let base = base.as_ref();
|
||||||
|
let base = if base.starts_with("https://") {
|
||||||
|
base.to_string()
|
||||||
|
} else {
|
||||||
|
format!("https://{}", base.trim_start_matches("http://"))
|
||||||
|
};
|
||||||
|
Ok(MastodonUnauthenticated {
|
||||||
|
client: Client::new(),
|
||||||
|
base: Url::parse(&base)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn route(&self, url: &str) -> Result<Url> {
|
||||||
|
Ok(self.base.join(url)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/v1/statuses/:id
|
||||||
|
pub async fn get_status(&self, id: &str) -> Result<Status> {
|
||||||
|
let route = self.route("/api/v1/statuses")?;
|
||||||
|
let route = route.join(id)?;
|
||||||
|
self.get(route.as_str()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/v1/statuses/:id/context
|
||||||
|
pub async fn get_context(&self, id: &str) -> Result<Context> {
|
||||||
|
let route = self.route("/api/v1/statuses")?;
|
||||||
|
let route = route.join(id)?;
|
||||||
|
let route = route.join("context")?;
|
||||||
|
self.get(route.as_str()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/v1/statuses/:id/card
|
||||||
|
pub async fn get_card(&self, id: &str) -> Result<Card> {
|
||||||
|
let route = self.route("/api/v1/statuses")?;
|
||||||
|
let route = route.join(id)?;
|
||||||
|
let route = route.join("card")?;
|
||||||
|
self.get(route.as_str()).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Deref for Mastodon {
|
||||||
|
type Target = Arc<MastodonClient>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MastodonClient> for Mastodon {
|
||||||
|
fn from(value: MastodonClient) -> Self {
|
||||||
|
Mastodon(Arc::new(value))
|
||||||
|
}
|
||||||
|
}
|
124
src/page.rs
124
src/page.rs
|
@ -1,11 +1,10 @@
|
||||||
use super::{deserialise, Mastodon, Result};
|
use super::{Mastodon, Result};
|
||||||
use crate::entities::itemsiter::ItemsIter;
|
use crate::{entities::itemsiter::ItemsIter, format_err};
|
||||||
|
use futures::Stream;
|
||||||
use hyper_old_types::header::{parsing, Link, RelationType};
|
use hyper_old_types::header::{parsing, Link, RelationType};
|
||||||
use reqwest::{header::LINK, Response};
|
use reqwest::{header::LINK, Response, Url};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use url::Url;
|
// use url::Url;
|
||||||
|
|
||||||
use crate::http_send::HttpSend;
|
|
||||||
|
|
||||||
macro_rules! pages {
|
macro_rules! pages {
|
||||||
($($direction:ident: $fun:ident),*) => {
|
($($direction:ident: $fun:ident),*) => {
|
||||||
|
@ -13,21 +12,19 @@ macro_rules! pages {
|
||||||
$(
|
$(
|
||||||
doc_comment!(concat!(
|
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),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = self.mastodon.send(
|
let response = self.mastodon.client.get(url).send().await?;
|
||||||
self.mastodon.client.get(url)
|
|
||||||
)?;
|
|
||||||
|
|
||||||
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)
|
Ok(Some(response.json().await?))
|
||||||
});
|
});
|
||||||
)*
|
)*
|
||||||
}
|
}
|
||||||
|
@ -41,71 +38,47 @@ macro_rules! pages {
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use elefren::{
|
/// use elefren::{
|
||||||
/// prelude::*,
|
/// prelude::*,
|
||||||
/// page::OwnedPage,
|
/// page::Page,
|
||||||
/// http_send::HttpSender,
|
|
||||||
/// entities::status::Status
|
/// entities::status::Status
|
||||||
/// };
|
/// };
|
||||||
/// use std::cell::RefCell;
|
/// use std::cell::RefCell;
|
||||||
///
|
///
|
||||||
|
/// tokio_test::block_on(async {
|
||||||
/// let data = Data::default();
|
/// let data = Data::default();
|
||||||
/// struct HomeTimeline {
|
/// struct HomeTimeline {
|
||||||
/// client: Mastodon,
|
/// client: Mastodon,
|
||||||
/// page: RefCell<Option<OwnedPage<Status, HttpSender>>>,
|
/// page: RefCell<Option<Page<Status>>>,
|
||||||
/// }
|
/// }
|
||||||
/// let client = Mastodon::from(data);
|
/// let client = Mastodon::from(data);
|
||||||
/// let home = client.get_home_timeline().unwrap().to_owned();
|
/// let home = client.get_home_timeline().await.unwrap();
|
||||||
/// let tl = HomeTimeline {
|
/// let tl = HomeTimeline {
|
||||||
/// client,
|
/// client,
|
||||||
/// page: RefCell::new(Some(home)),
|
/// page: RefCell::new(Some(home)),
|
||||||
/// };
|
/// };
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct OwnedPage<T: for<'de> Deserialize<'de>, H: HttpSend> {
|
|
||||||
mastodon: Mastodon<H>,
|
|
||||||
next: Option<Url>,
|
|
||||||
prev: Option<Url>,
|
|
||||||
/// Initial set of items
|
|
||||||
pub initial_items: Vec<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: for<'de> Deserialize<'de>, H: HttpSend> OwnedPage<T, H> {
|
|
||||||
pages! {
|
|
||||||
next: next_page,
|
|
||||||
prev: prev_page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T: for<'de> Deserialize<'de>, H: HttpSend> From<Page<'a, T, H>> for OwnedPage<T, H> {
|
|
||||||
fn from(page: Page<'a, T, H>) -> OwnedPage<T, H> {
|
|
||||||
OwnedPage {
|
|
||||||
mastodon: page.mastodon.clone(),
|
|
||||||
next: page.next,
|
|
||||||
prev: page.prev,
|
|
||||||
initial_items: page.initial_items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a single page of API results
|
/// Represents a single page of API results
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Page<'a, T: for<'de> Deserialize<'de>, H: 'a + HttpSend> {
|
pub struct Page<T: for<'de> Deserialize<'de>> {
|
||||||
mastodon: &'a Mastodon<H>,
|
mastodon: Mastodon,
|
||||||
next: Option<Url>,
|
next: Option<Url>,
|
||||||
prev: Option<Url>,
|
prev: Option<Url>,
|
||||||
/// Initial set of items
|
/// Initial set of items
|
||||||
pub initial_items: Vec<T>,
|
pub initial_items: Vec<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: for<'de> Deserialize<'de>, H: HttpSend> Page<'a, T, H> {
|
impl<'a, T: for<'de> Deserialize<'de>> Page<T> {
|
||||||
pages! {
|
pages! {
|
||||||
next: next_page,
|
next: next_page,
|
||||||
prev: prev_page
|
prev: prev_page
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(mastodon: &'a Mastodon<H>, response: Response) -> Result<Self> {
|
/// Create a new Page.
|
||||||
|
pub(crate) async fn new(mastodon: Mastodon, response: Response) -> Result<Self> {
|
||||||
let (prev, next) = get_links(&response)?;
|
let (prev, next) = get_links(&response)?;
|
||||||
Ok(Page {
|
Ok(Page {
|
||||||
initial_items: deserialise(response)?,
|
initial_items: response.json().await?,
|
||||||
next,
|
next,
|
||||||
prev,
|
prev,
|
||||||
mastodon,
|
mastodon,
|
||||||
|
@ -113,31 +86,7 @@ impl<'a, T: for<'de> Deserialize<'de>, H: HttpSend> Page<'a, T, H> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> Page<'a, T, H> {
|
impl<T: Clone + for<'de> Deserialize<'de>> Page<T> {
|
||||||
/// Returns an owned version of this struct that doesn't borrow the client
|
|
||||||
/// that created it
|
|
||||||
///
|
|
||||||
/// // Example
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use elefren::{Mastodon, page::OwnedPage, http_send::HttpSender, entities::status::Status, prelude::*};
|
|
||||||
/// use std::cell::RefCell;
|
|
||||||
/// let data = Data::default();
|
|
||||||
/// struct HomeTimeline {
|
|
||||||
/// client: Mastodon,
|
|
||||||
/// page: RefCell<Option<OwnedPage<Status, HttpSender>>>,
|
|
||||||
/// }
|
|
||||||
/// let client = Mastodon::from(data);
|
|
||||||
/// let home = client.get_home_timeline().unwrap().to_owned();
|
|
||||||
/// let tl = HomeTimeline {
|
|
||||||
/// client,
|
|
||||||
/// page: RefCell::new(Some(home)),
|
|
||||||
/// };
|
|
||||||
/// ```
|
|
||||||
pub fn to_owned(self) -> OwnedPage<T, H> {
|
|
||||||
OwnedPage::from(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an iterator that provides a stream of `T`s
|
/// Returns an iterator that provides a stream of `T`s
|
||||||
///
|
///
|
||||||
/// This abstracts away the process of iterating over each item in a page,
|
/// This abstracts away the process of iterating over each item in a page,
|
||||||
|
@ -152,19 +101,21 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> Page<'a, T, H> {
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use elefren::prelude::*;
|
/// use elefren::prelude::*;
|
||||||
|
/// use futures_util::StreamExt;
|
||||||
|
///
|
||||||
/// let data = Data::default();
|
/// let data = Data::default();
|
||||||
/// let mastodon = Mastodon::from(data);
|
/// let mastodon = Mastodon::from(data);
|
||||||
/// let req = StatusesRequest::new();
|
/// let req = StatusesRequest::new();
|
||||||
/// let resp = mastodon.statuses("some-id", req).unwrap();
|
///
|
||||||
/// for status in resp.items_iter() {
|
/// tokio_test::block_on(async {
|
||||||
|
/// let resp = mastodon.statuses("some-id", req).await.unwrap();
|
||||||
|
/// resp.items_iter().for_each(|status| async move {
|
||||||
/// // do something with status
|
/// // do something with status
|
||||||
/// }
|
/// }).await;
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn items_iter(self) -> impl Iterator<Item = T> + 'a
|
pub fn items_iter(self) -> impl Stream<Item = T> {
|
||||||
where
|
ItemsIter::new(self).stream()
|
||||||
T: 'a,
|
|
||||||
{
|
|
||||||
ItemsIter::new(self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,11 +130,22 @@ fn get_links(response: &Response) -> Result<(Option<Url>, Option<Url>)> {
|
||||||
for value in link_header.values() {
|
for value in link_header.values() {
|
||||||
if let Some(relations) = value.rel() {
|
if let Some(relations) = value.rel() {
|
||||||
if relations.contains(&RelationType::Next) {
|
if relations.contains(&RelationType::Next) {
|
||||||
next = Some(Url::parse(value.link())?);
|
// next = Some(Url::parse(value.link())?);
|
||||||
|
next = if let Ok(url) = Url::parse(value.link()) {
|
||||||
|
Some(url)
|
||||||
|
} else {
|
||||||
|
// HACK: url::ParseError::into isn't working for some reason.
|
||||||
|
return Err(format_err!("error parsing url {:?}", value.link()));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if relations.contains(&RelationType::Prev) {
|
if relations.contains(&RelationType::Prev) {
|
||||||
prev = Some(Url::parse(value.link())?);
|
prev = if let Ok(url) = Url::parse(value.link()) {
|
||||||
|
Some(url)
|
||||||
|
} else {
|
||||||
|
// HACK: url::ParseError::into isn't working for some reason.
|
||||||
|
return Err(format_err!("error parsing url {:?}", value.link()));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use reqwest::{Client, RequestBuilder, Response};
|
use reqwest::Client;
|
||||||
use url::percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
|
use url::percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
apps::{App, AppBuilder},
|
apps::{App, AppBuilder},
|
||||||
http_send::{HttpSend, HttpSender},
|
|
||||||
scopes::Scopes,
|
scopes::Scopes,
|
||||||
Data,
|
Data,
|
||||||
Error,
|
Error,
|
||||||
Mastodon,
|
Mastodon,
|
||||||
MastodonBuilder,
|
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,12 +17,11 @@ const DEFAULT_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob";
|
||||||
/// Handles registering your mastodon app to your instance. It is recommended
|
/// Handles registering your mastodon app to your instance. It is recommended
|
||||||
/// you cache your data struct to avoid registering on every run.
|
/// you cache your data struct to avoid registering on every run.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Registration<'a, H: HttpSend = HttpSender> {
|
pub struct Registration<'a> {
|
||||||
base: String,
|
base: String,
|
||||||
client: Client,
|
client: Client,
|
||||||
app_builder: AppBuilder<'a>,
|
app_builder: AppBuilder<'a>,
|
||||||
force_login: bool,
|
force_login: bool,
|
||||||
http_sender: H,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -44,7 +41,7 @@ struct AccessToken {
|
||||||
access_token: String,
|
access_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Registration<'a, HttpSender> {
|
impl<'a> Registration<'a> {
|
||||||
/// Construct a new registration process to the instance of the `base` url.
|
/// Construct a new registration process to the instance of the `base` url.
|
||||||
/// ```
|
/// ```
|
||||||
/// use elefren::prelude::*;
|
/// use elefren::prelude::*;
|
||||||
|
@ -57,20 +54,18 @@ impl<'a> Registration<'a, HttpSender> {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
app_builder: AppBuilder::new(),
|
app_builder: AppBuilder::new(),
|
||||||
force_login: false,
|
force_login: false,
|
||||||
http_sender: HttpSender,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, H: HttpSend> Registration<'a, H> {
|
impl<'a> Registration<'a> {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn with_sender<I: Into<String>>(base: I, http_sender: H) -> Self {
|
pub(crate) fn with_sender<I: Into<String>>(base: I) -> Self {
|
||||||
Registration {
|
Registration {
|
||||||
base: base.into(),
|
base: base.into(),
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
app_builder: AppBuilder::new(),
|
app_builder: AppBuilder::new(),
|
||||||
force_login: false,
|
force_login: false,
|
||||||
http_sender,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,33 +105,34 @@ impl<'a, H: HttpSend> Registration<'a, H> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send(&self, req: RequestBuilder) -> Result<Response> {
|
|
||||||
Ok(self.http_sender.send(&self.client, req)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register the given application
|
/// Register the given application
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use elefren::{apps::App, prelude::*};
|
/// use elefren::{apps::App, prelude::*};
|
||||||
///
|
///
|
||||||
|
/// tokio_test::block_on(async {
|
||||||
/// let mut app = App::builder();
|
/// let mut app = App::builder();
|
||||||
/// app.client_name("elefren_test");
|
/// app.client_name("elefren_test");
|
||||||
///
|
///
|
||||||
/// let registration = Registration::new("https://botsin.space").register(app).unwrap();
|
/// let registration = Registration::new("https://botsin.space")
|
||||||
|
/// .register(app)
|
||||||
|
/// .await
|
||||||
|
/// .unwrap();
|
||||||
/// let url = registration.authorize_url().unwrap();
|
/// let url = registration.authorize_url().unwrap();
|
||||||
/// // Here you now need to open the url in the browser
|
/// // Here you now need to open the url in the browser
|
||||||
/// // And handle a the redirect url coming back with the code.
|
/// // And handle a the redirect url coming back with the code.
|
||||||
/// let code = String::from("RETURNED_FROM_BROWSER");
|
/// let code = String::from("RETURNED_FROM_BROWSER");
|
||||||
/// let mastodon = registration.complete(&code).unwrap();
|
/// let mastodon = registration.complete(&code).await.unwrap();
|
||||||
///
|
///
|
||||||
/// println!("{:?}", mastodon.get_home_timeline().unwrap().initial_items);
|
/// println!("{:?}", mastodon.get_home_timeline().await.unwrap().initial_items);
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn register<I: TryInto<App>>(&mut self, app: I) -> Result<Registered<H>>
|
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(),
|
||||||
|
@ -146,7 +142,6 @@ impl<'a, H: HttpSend> Registration<'a, H> {
|
||||||
redirect: oauth.redirect_uri,
|
redirect: oauth.redirect_uri,
|
||||||
scopes: app.scopes().clone(),
|
scopes: app.scopes().clone(),
|
||||||
force_login: self.force_login,
|
force_login: self.force_login,
|
||||||
http_sender: self.http_sender.clone(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,20 +150,24 @@ impl<'a, H: HttpSend> Registration<'a, H> {
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use elefren::prelude::*;
|
/// use elefren::prelude::*;
|
||||||
///
|
///
|
||||||
|
/// tokio_test::block_on(async {
|
||||||
/// let registration = Registration::new("https://botsin.space")
|
/// let registration = Registration::new("https://botsin.space")
|
||||||
/// .client_name("elefren_test")
|
/// .client_name("elefren_test")
|
||||||
/// .build().unwrap();
|
/// .build()
|
||||||
|
/// .await
|
||||||
|
/// .unwrap();
|
||||||
/// let url = registration.authorize_url().unwrap();
|
/// let url = registration.authorize_url().unwrap();
|
||||||
/// // Here you now need to open the url in the browser
|
/// // Here you now need to open the url in the browser
|
||||||
/// // And handle a the redirect url coming back with the code.
|
/// // And handle a the redirect url coming back with the code.
|
||||||
/// let code = String::from("RETURNED_FROM_BROWSER");
|
/// let code = String::from("RETURNED_FROM_BROWSER");
|
||||||
/// let mastodon = registration.complete(&code).unwrap();
|
/// let mastodon = registration.complete(&code).await.unwrap();
|
||||||
///
|
///
|
||||||
/// println!("{:?}", mastodon.get_home_timeline().unwrap().initial_items);
|
/// println!("{:?}", mastodon.get_home_timeline().await.unwrap().initial_items);
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn build(&mut self) -> Result<Registered<H>> {
|
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(),
|
||||||
|
@ -178,17 +177,17 @@ impl<'a, H: HttpSend> Registration<'a, H> {
|
||||||
redirect: oauth.redirect_uri,
|
redirect: oauth.redirect_uri,
|
||||||
scopes: app.scopes().clone(),
|
scopes: app.scopes().clone(),
|
||||||
force_login: self.force_login,
|
force_login: self.force_login,
|
||||||
http_sender: self.http_sender.clone(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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()?)
|
let response = self.client.post(&url).json(&app).send().await?;
|
||||||
|
Ok(response.json().await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Registered<HttpSender> {
|
impl Registered {
|
||||||
/// Skip having to retrieve the client id and secret from the server by
|
/// Skip having to retrieve the client id and secret from the server by
|
||||||
/// creating a `Registered` struct directly
|
/// creating a `Registered` struct directly
|
||||||
///
|
///
|
||||||
|
@ -197,6 +196,7 @@ impl Registered<HttpSender> {
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use elefren::{prelude::*, registration::Registered};
|
/// use elefren::{prelude::*, registration::Registered};
|
||||||
///
|
///
|
||||||
|
/// tokio_test::block_on(async {
|
||||||
/// let registration = Registered::from_parts(
|
/// let registration = Registered::from_parts(
|
||||||
/// "https://example.com",
|
/// "https://example.com",
|
||||||
/// "the-client-id",
|
/// "the-client-id",
|
||||||
|
@ -209,9 +209,10 @@ impl Registered<HttpSender> {
|
||||||
/// // Here you now need to open the url in the browser
|
/// // Here you now need to open the url in the browser
|
||||||
/// // And handle a the redirect url coming back with the code.
|
/// // And handle a the redirect url coming back with the code.
|
||||||
/// let code = String::from("RETURNED_FROM_BROWSER");
|
/// let code = String::from("RETURNED_FROM_BROWSER");
|
||||||
/// let mastodon = registration.complete(&code).unwrap();
|
/// let mastodon = registration.complete(&code).await.unwrap();
|
||||||
///
|
///
|
||||||
/// println!("{:?}", mastodon.get_home_timeline().unwrap().initial_items);
|
/// println!("{:?}", mastodon.get_home_timeline().await.unwrap().initial_items);
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn from_parts(
|
pub fn from_parts(
|
||||||
base: &str,
|
base: &str,
|
||||||
|
@ -220,7 +221,7 @@ impl Registered<HttpSender> {
|
||||||
redirect: &str,
|
redirect: &str,
|
||||||
scopes: Scopes,
|
scopes: Scopes,
|
||||||
force_login: bool,
|
force_login: bool,
|
||||||
) -> Registered<HttpSender> {
|
) -> Registered {
|
||||||
Registered {
|
Registered {
|
||||||
base: base.to_string(),
|
base: base.to_string(),
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
|
@ -229,16 +230,11 @@ impl Registered<HttpSender> {
|
||||||
redirect: redirect.to_string(),
|
redirect: redirect.to_string(),
|
||||||
scopes,
|
scopes,
|
||||||
force_login,
|
force_login,
|
||||||
http_sender: HttpSender,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<H: HttpSend> Registered<H> {
|
impl Registered {
|
||||||
fn send(&self, req: RequestBuilder) -> Result<Response> {
|
|
||||||
Ok(self.http_sender.send(&self.client, req)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the parts of the `Registered` struct that can be used to
|
/// Returns the parts of the `Registered` struct that can be used to
|
||||||
/// recreate another `Registered` struct
|
/// recreate another `Registered` struct
|
||||||
///
|
///
|
||||||
|
@ -304,35 +300,45 @@ impl<H: HttpSend> Registered<H> {
|
||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct authentication data once token is known
|
||||||
|
fn registered(&self, token: String) -> Data {
|
||||||
|
Data {
|
||||||
|
base: self.base.clone().into(),
|
||||||
|
client_id: self.client_id.clone().into(),
|
||||||
|
client_secret: self.client_secret.clone().into(),
|
||||||
|
redirect: self.redirect.clone().into(),
|
||||||
|
token: token.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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 authorization url.
|
/// provided by the authorization url.
|
||||||
pub fn complete(&self, code: &str) -> Result<Mastodon<H>> {
|
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
|
||||||
|
.client
|
||||||
|
.post(&url)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
let data = Data {
|
let data = self.registered(token.access_token);
|
||||||
base: self.base.clone().into(),
|
|
||||||
client_id: self.client_id.clone().into(),
|
|
||||||
client_secret: self.client_secret.clone().into(),
|
|
||||||
redirect: self.redirect.clone().into(),
|
|
||||||
token: token.access_token.into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut builder = MastodonBuilder::new(self.http_sender.clone());
|
Ok(Mastodon::new(self.client.clone(), data))
|
||||||
builder.client(self.client.clone()).data(data);
|
|
||||||
Ok(builder.build()?)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the state of the auth flow when the app has been registered but
|
/// Represents the state of the auth flow when the app has been registered but
|
||||||
/// the user is not authenticated
|
/// the user is not authenticated
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Registered<H: HttpSend> {
|
pub struct Registered {
|
||||||
base: String,
|
base: String,
|
||||||
client: Client,
|
client: Client,
|
||||||
client_id: String,
|
client_id: String,
|
||||||
|
@ -340,7 +346,6 @@ pub struct Registered<H: HttpSend> {
|
||||||
redirect: String,
|
redirect: String,
|
||||||
scopes: Scopes,
|
scopes: Scopes,
|
||||||
force_login: bool,
|
force_login: bool,
|
||||||
http_sender: H,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -352,15 +357,13 @@ mod tests {
|
||||||
let r = Registration::new("https://example.com");
|
let r = Registration::new("https://example.com");
|
||||||
assert_eq!(r.base, "https://example.com".to_string());
|
assert_eq!(r.base, "https://example.com".to_string());
|
||||||
assert_eq!(r.app_builder, AppBuilder::new());
|
assert_eq!(r.app_builder, AppBuilder::new());
|
||||||
assert_eq!(r.http_sender, HttpSender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_registration_with_sender() {
|
fn test_registration_with_sender() {
|
||||||
let r = Registration::with_sender("https://example.com", HttpSender);
|
let r = Registration::with_sender("https://example.com");
|
||||||
assert_eq!(r.base, "https://example.com".to_string());
|
assert_eq!(r.base, "https://example.com".to_string());
|
||||||
assert_eq!(r.app_builder, AppBuilder::new());
|
assert_eq!(r.app_builder, AppBuilder::new());
|
||||||
assert_eq!(r.http_sender, HttpSender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -41,9 +41,10 @@ impl Keys {
|
||||||
/// // Example
|
/// // Example
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use elefren::{MastodonClient, Mastodon, Data};
|
/// use elefren::{Mastodon, Data};
|
||||||
/// use elefren::requests::{AddPushRequest, Keys};
|
/// use elefren::requests::{AddPushRequest, Keys};
|
||||||
///
|
///
|
||||||
|
/// tokio_test::block_on(async {
|
||||||
/// let data = Data::default();
|
/// let data = Data::default();
|
||||||
/// let client = Mastodon::from(data);
|
/// let client = Mastodon::from(data);
|
||||||
///
|
///
|
||||||
|
@ -51,7 +52,8 @@ impl Keys {
|
||||||
/// let mut request = AddPushRequest::new("http://example.com/push/endpoint", &keys);
|
/// let mut request = AddPushRequest::new("http://example.com/push/endpoint", &keys);
|
||||||
/// request.follow().reblog();
|
/// request.follow().reblog();
|
||||||
///
|
///
|
||||||
/// client.add_push_subscription(&request).unwrap();
|
/// client.add_push_subscription(&request).await.unwrap();
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Default, Clone, PartialEq)]
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
pub struct AddPushRequest {
|
pub struct AddPushRequest {
|
||||||
|
@ -200,16 +202,18 @@ impl AddPushRequest {
|
||||||
/// // Example
|
/// // Example
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use elefren::{MastodonClient, Mastodon, Data, requests::UpdatePushRequest};
|
/// use elefren::{Mastodon, Data, requests::UpdatePushRequest};
|
||||||
/// let data = Data::default();
|
|
||||||
///
|
///
|
||||||
|
/// let data = Data::default();
|
||||||
/// let client = Mastodon::from(data);
|
/// let client = Mastodon::from(data);
|
||||||
///
|
///
|
||||||
/// let mut request = UpdatePushRequest::new("foobar");
|
/// let mut request = UpdatePushRequest::new("foobar");
|
||||||
/// request.follow(true)
|
/// request.follow(true)
|
||||||
/// .reblog(true);
|
/// .reblog(true);
|
||||||
///
|
///
|
||||||
/// client.update_push_data(&request).unwrap();
|
/// tokio_test::block_on(async {
|
||||||
|
/// client.update_push_data(&request).await.unwrap();
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
|
||||||
pub struct UpdatePushRequest {
|
pub struct UpdatePushRequest {
|
||||||
|
|
|
@ -22,7 +22,9 @@ use crate::{
|
||||||
///
|
///
|
||||||
/// builder.privacy(Visibility::Unlisted);
|
/// builder.privacy(Visibility::Unlisted);
|
||||||
///
|
///
|
||||||
/// let result = client.update_credentials(&mut builder).unwrap();
|
/// tokio_test::block_on(async {
|
||||||
|
/// let result = client.update_credentials(&mut builder).await.unwrap();
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Default, Clone, PartialEq)]
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
pub struct UpdateCredsRequest {
|
pub struct UpdateCredsRequest {
|
||||||
|
|
|
@ -42,7 +42,10 @@ impl StatusBuilder {
|
||||||
/// .visibility(Visibility::Public)
|
/// .visibility(Visibility::Public)
|
||||||
/// .build()
|
/// .build()
|
||||||
/// .unwrap();
|
/// .unwrap();
|
||||||
/// client.new_status(status).unwrap();
|
///
|
||||||
|
/// tokio_test::block_on(async {
|
||||||
|
/// client.new_status(status).await.unwrap();
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new() -> StatusBuilder {
|
pub fn new() -> StatusBuilder {
|
||||||
StatusBuilder::default()
|
StatusBuilder::default()
|
||||||
|
|
Loading…
Reference in New Issue