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]
|
||||
name = "elefren"
|
||||
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."
|
||||
readme = "README.md"
|
||||
keywords = ["api", "web", "social", "mastodon", "wrapper"]
|
||||
categories = ["web-programming", "web-programming::http-client", "api-bindings"]
|
||||
license = "MIT/Apache-2.0"
|
||||
edition = "2021"
|
||||
# TODO setup repo
|
||||
# repository = "https://github.com/pwoolcoc/elefren.git"
|
||||
repository = "https://github.com/dscottboggs/elefren.git"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
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]
|
||||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.doc-comment]
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.envy]
|
||||
version = "0.4"
|
||||
optional = true
|
||||
|
||||
# Provides parsing for the link header in get_links() in page.rs
|
||||
[dependencies.hyper-old-types]
|
||||
version = "0.11.0"
|
||||
|
||||
[dependencies.isolang]
|
||||
version = "2.2"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.9"
|
||||
default-features = false
|
||||
version = "0.11"
|
||||
features = ["multipart", "json"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
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]
|
||||
version = "0.5"
|
||||
optional = true
|
||||
|
||||
[dependencies.tungstenite]
|
||||
version = "0.10.1"
|
||||
|
||||
[dependencies.url]
|
||||
version = "1"
|
||||
|
||||
[dev-dependencies.indoc]
|
||||
version = "1.0"
|
||||
|
||||
[dev-dependencies.pretty_env_logger]
|
||||
version = "0.3.0"
|
||||
|
||||
[dev-dependencies.skeptic]
|
||||
version = "0.13"
|
||||
|
||||
[dev-dependencies.tempfile]
|
||||
version = "3"
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.2"
|
||||
futures-util = "0.3.25"
|
||||
indoc = "1.0"
|
||||
pretty_env_logger = "0.3.0"
|
||||
skeptic = "0.13"
|
||||
tempfile = "3"
|
||||
|
||||
[build-dependencies.skeptic]
|
||||
version = "0.13"
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
version = "1.22.0"
|
||||
features = ["rt-multi-thread", "macros"]
|
||||
|
||||
[features]
|
||||
all = ["toml", "json", "env"]
|
||||
default = ["reqwest/default-tls"]
|
||||
|
|
|
@ -5,7 +5,7 @@ extern crate pretty_env_logger;
|
|||
extern crate elefren;
|
||||
mod register;
|
||||
|
||||
use register::MastodonClient;
|
||||
use register::Mastodon;
|
||||
use std::error;
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
extern crate pretty_env_logger;
|
||||
mod register;
|
||||
|
||||
use register::MastodonClient;
|
||||
use register::Mastodon;
|
||||
use std::error;
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
extern crate pretty_env_logger;
|
||||
mod register;
|
||||
|
||||
use register::MastodonClient;
|
||||
use register::Mastodon;
|
||||
use std::error;
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
|
|
|
@ -5,7 +5,7 @@ extern crate pretty_env_logger;
|
|||
extern crate elefren;
|
||||
mod register;
|
||||
|
||||
use register::MastodonClient;
|
||||
use register::Mastodon;
|
||||
use std::error;
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
extern crate pretty_env_logger;
|
||||
mod register;
|
||||
|
||||
use register::MastodonClient;
|
||||
use register::Mastodon;
|
||||
use std::error;
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
|
|
|
@ -5,7 +5,7 @@ extern crate pretty_env_logger;
|
|||
extern crate elefren;
|
||||
mod register;
|
||||
|
||||
use register::MastodonClient;
|
||||
use register::Mastodon;
|
||||
use std::error;
|
||||
|
||||
#[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;
|
||||
|
||||
/// Abstracts away the `next_page` logic into a single stream of items
|
||||
///
|
||||
/// ```no_run
|
||||
/// ```no_run,async
|
||||
/// use elefren::prelude::*;
|
||||
/// let data = Data::default();
|
||||
/// let client = Mastodon::from(data);
|
||||
/// let statuses = client.statuses("user-id", None).unwrap();
|
||||
/// for status in statuses.items_iter() {
|
||||
/// // do something with `status`
|
||||
/// }
|
||||
/// use futures::stream::StreamExt;
|
||||
/// use futures_util::pin_mut;
|
||||
///
|
||||
/// tokio_test::block_on(async {
|
||||
/// let data = Data::default();
|
||||
/// let client = Mastodon::from(data);
|
||||
/// let statuses = client.statuses("user-id", None).await.unwrap().items_iter();
|
||||
/// statuses.for_each(|status| async move {
|
||||
/// // Do something with the status
|
||||
/// }).await;
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// See documentation for `futures::Stream::StreamExt` for available methods.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ItemsIter<'a, T: Clone + for<'de> Deserialize<'de>, H: 'a + HttpSend> {
|
||||
page: Page<'a, T, H>,
|
||||
pub(crate) struct ItemsIter<T: Clone + for<'de> Deserialize<'de>> {
|
||||
page: Page<T>,
|
||||
buffer: Vec<T>,
|
||||
cur_idx: usize,
|
||||
use_initial: bool,
|
||||
}
|
||||
|
||||
impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> ItemsIter<'a, T, H> {
|
||||
pub(crate) fn new(page: Page<'a, T, H>) -> ItemsIter<'a, T, H> {
|
||||
impl<'a, T: Clone + for<'de> Deserialize<'de>> ItemsIter<T> {
|
||||
pub(crate) fn new(page: Page<T>) -> ItemsIter<T> {
|
||||
ItemsIter {
|
||||
page,
|
||||
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()
|
||||
}
|
||||
|
||||
fn fill_next_page(&mut self) -> Option<()> {
|
||||
let items = if let Ok(items) = self.page.next_page() {
|
||||
async fn fill_next_page(&mut self) -> Option<()> {
|
||||
let items = if let Ok(items) = self.page.next_page().await {
|
||||
items
|
||||
} else {
|
||||
return None;
|
||||
|
@ -51,33 +60,39 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> ItemsIter<'a, T, H>
|
|||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> Iterator for ItemsIter<'a, T, H> {
|
||||
type Item = T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.use_initial {
|
||||
if self.page.initial_items.is_empty() || self.cur_idx == self.page.initial_items.len() {
|
||||
return None;
|
||||
}
|
||||
let idx = self.cur_idx;
|
||||
if self.cur_idx == self.page.initial_items.len() - 1 {
|
||||
self.cur_idx = 0;
|
||||
self.use_initial = false;
|
||||
} else {
|
||||
self.cur_idx += 1;
|
||||
}
|
||||
Some(self.page.initial_items[idx].clone())
|
||||
} else {
|
||||
if self.need_next_page() {
|
||||
if self.fill_next_page().is_none() {
|
||||
pub(crate) fn stream(self) -> impl Stream<Item = T> {
|
||||
unfold(self, |mut this| async move {
|
||||
if this.use_initial {
|
||||
if this.page.initial_items.is_empty()
|
||||
|| this.cur_idx == this.page.initial_items.len()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let idx = this.cur_idx;
|
||||
if this.cur_idx == this.page.initial_items.len() - 1 {
|
||||
this.cur_idx = 0;
|
||||
this.use_initial = false;
|
||||
} else {
|
||||
this.cur_idx += 1;
|
||||
}
|
||||
let item = this.page.initial_items[idx].clone();
|
||||
// let item = Box::pin(item);
|
||||
// pin_mut!(item);
|
||||
Some((item, this))
|
||||
} else {
|
||||
if this.need_next_page() {
|
||||
if this.fill_next_page().await.is_none() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let idx = this.cur_idx;
|
||||
this.cur_idx += 1;
|
||||
let item = this.buffer[idx].clone();
|
||||
// let item = Box::pin(item);
|
||||
// pin_mut!(item);
|
||||
Some((item, this))
|
||||
}
|
||||
let idx = self.cur_idx;
|
||||
self.cur_idx += 1;
|
||||
Some(self.buffer[idx].clone())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
use envy::Error as EnvyError;
|
||||
|
@ -12,7 +12,7 @@ use serde_urlencoded::ser::Error as UrlEncodedError;
|
|||
use tomlcrate::de::Error as TomlDeError;
|
||||
#[cfg(feature = "toml")]
|
||||
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;
|
||||
|
||||
/// Convience type over `std::result::Result` with `Error` as the error type.
|
||||
|
@ -64,6 +64,14 @@ pub enum Error {
|
|||
SerdeQs(SerdeQsError),
|
||||
/// WebSocket error
|
||||
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(String),
|
||||
}
|
||||
|
@ -93,12 +101,13 @@ impl error::Error for Error {
|
|||
Error::Envy(ref e) => e,
|
||||
Error::SerdeQs(ref e) => e,
|
||||
Error::WebSocket(ref e) => e,
|
||||
|
||||
Error::IntConversion(ref e) => e,
|
||||
Error::Client(..) | Error::Server(..) => return None,
|
||||
Error::ClientIdRequired => return None,
|
||||
Error::ClientSecretRequired => return None,
|
||||
Error::AccessTokenRequired => return None,
|
||||
Error::MissingField(_) => return None,
|
||||
Error::UnrecognizedStreamMessage(_) => return None,
|
||||
Error::Other(..) => return None,
|
||||
})
|
||||
}
|
||||
|
@ -122,7 +131,7 @@ impl fmt::Display for ApiError {
|
|||
impl error::Error for ApiError {}
|
||||
|
||||
macro_rules! from {
|
||||
($($(#[$met:meta])* $typ:ident, $variant:ident,)*) => {
|
||||
($($(#[$met:meta])* $typ:ident => $variant:ident,)*) => {
|
||||
$(
|
||||
$(#[$met])*
|
||||
impl From<$typ> for Error {
|
||||
|
@ -136,20 +145,22 @@ macro_rules! from {
|
|||
}
|
||||
|
||||
from! {
|
||||
HttpError, Http,
|
||||
IoError, Io,
|
||||
SerdeError, Serde,
|
||||
UrlEncodedError, UrlEncoded,
|
||||
UrlError, Url,
|
||||
ApiError, Api,
|
||||
#[cfg(feature = "toml")] TomlSerError, TomlSer,
|
||||
#[cfg(feature = "toml")] TomlDeError, TomlDe,
|
||||
HeaderStrError, HeaderStrError,
|
||||
HeaderParseError, HeaderParseError,
|
||||
#[cfg(feature = "env")] EnvyError, Envy,
|
||||
SerdeQsError, SerdeQs,
|
||||
WebSocketError, WebSocket,
|
||||
String, Other,
|
||||
HttpError => Http,
|
||||
IoError => Io,
|
||||
SerdeError => Serde,
|
||||
UrlEncodedError => UrlEncoded,
|
||||
UrlError => Url,
|
||||
ApiError => Api,
|
||||
#[cfg(feature = "toml")] TomlSerError => TomlSer,
|
||||
#[cfg(feature = "toml")] TomlDeError => TomlDe,
|
||||
HeaderStrError => HeaderStrError,
|
||||
HeaderParseError => HeaderParseError,
|
||||
#[cfg(feature = "env")] EnvyError => Envy,
|
||||
SerdeQsError => SerdeQs,
|
||||
WebSocketError => WebSocket,
|
||||
String => Other,
|
||||
TryFromIntError => IntConversion,
|
||||
WebSocketMessage => UnrecognizedStreamMessage,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
@ -157,8 +168,7 @@ from! {
|
|||
macro_rules! format_err {
|
||||
( $( $arg:tt )* ) => {
|
||||
{
|
||||
use elefren::Error;
|
||||
Error::Other(format!($($arg)*))
|
||||
crate::Error::Other(format!($($arg)*))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -177,9 +187,9 @@ mod tests {
|
|||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_http_error() {
|
||||
let err: HttpError = reqwest::get("not an actual URL").unwrap_err();
|
||||
#[tokio::test]
|
||||
async fn from_http_error() {
|
||||
let err: HttpError = reqwest::get("not an actual URL").await.unwrap_err();
|
||||
let err: Error = Error::from(err);
|
||||
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 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,
|
||||
/// 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 stdout = io::stdout();
|
||||
|
@ -20,5 +20,5 @@ pub fn authenticate<H: HttpSend>(registration: Registered<H>) -> Result<Mastodon
|
|||
let mut input = String::new();
|
||||
stdin.read_line(&mut input)?;
|
||||
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)?)
|
||||
}
|
||||
}
|
742
src/lib.rs
742
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/)
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use elefren::{helpers::cli, prelude::*};
|
||||
//! use futures_util::StreamExt;
|
||||
//!
|
||||
//! let registration = Registration::new("https://botsin.space")
|
||||
//! .client_name("elefren_test")
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
//! let mastodon = cli::authenticate(registration).unwrap();
|
||||
//! tokio_test::block_on(async {
|
||||
//! let registration = Registration::new("https://botsin.space")
|
||||
//! .client_name("elefren_test")
|
||||
//! .build()
|
||||
//! .await
|
||||
//! .unwrap();
|
||||
//! let mastodon = cli::authenticate(registration).await.unwrap();
|
||||
//!
|
||||
//! println!(
|
||||
//! "{:?}",
|
||||
//! mastodon
|
||||
//! .get_home_timeline()
|
||||
//! .unwrap()
|
||||
//! .items_iter()
|
||||
//! .take(100)
|
||||
//! .collect::<Vec<_>>()
|
||||
//! );
|
||||
//! println!(
|
||||
//! "{:?}",
|
||||
//! mastodon
|
||||
//! .get_home_timeline()
|
||||
//! .await
|
||||
//! .unwrap()
|
||||
//! .items_iter()
|
||||
//! .take(100)
|
||||
//! .collect::<Vec<_>>()
|
||||
//! .await
|
||||
//! );
|
||||
//! });
|
||||
//! ```
|
||||
//!
|
||||
//! Elefren also supports Mastodon's Streaming API:
|
||||
//!
|
||||
//! // Example
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use elefren::{prelude::*, entities::event::Event};
|
||||
//! use futures_util::TryStreamExt;
|
||||
//!
|
||||
//! 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 => { /* .. */ },
|
||||
//! }
|
||||
//! }
|
||||
//! tokio_test::block_on(async {
|
||||
//! 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();
|
||||
//! });
|
||||
//! ```
|
||||
|
||||
#![deny(
|
||||
|
@ -53,7 +65,7 @@
|
|||
unused_qualifications
|
||||
)]
|
||||
|
||||
#[macro_use]
|
||||
// #[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate doc_comment;
|
||||
|
@ -84,20 +96,13 @@ extern crate tempfile;
|
|||
#[cfg_attr(all(test, any(feature = "toml", feature = "json")), macro_use)]
|
||||
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;
|
||||
|
||||
pub use data::Data;
|
||||
pub use errors::{ApiError, Error, Result};
|
||||
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 requests::{
|
||||
AddFilterRequest,
|
||||
|
@ -116,11 +121,10 @@ pub mod data;
|
|||
pub mod entities;
|
||||
/// Errors
|
||||
pub mod errors;
|
||||
/// Event stream generators
|
||||
pub mod event_stream;
|
||||
/// Collection of helpers for serializing/deserializing `Data` objects
|
||||
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.
|
||||
pub mod page;
|
||||
/// Registering your app.
|
||||
|
@ -139,670 +143,12 @@ pub mod prelude {
|
|||
scopes::Scopes,
|
||||
Data,
|
||||
Mastodon,
|
||||
MastodonClient,
|
||||
// MastodonClient,
|
||||
NewStatus,
|
||||
Registration,
|
||||
StatusBuilder,
|
||||
StatusesRequest,
|
||||
};
|
||||
}
|
||||
|
||||
/// Your mastodon application client, handles all requests to and from 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())
|
||||
},
|
||||
}
|
||||
}
|
||||
/// The mastodon client
|
||||
pub mod mastodon;
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
macro_rules! methods {
|
||||
($($method:ident,)+) => {
|
||||
$(
|
||||
fn $method<T: for<'de> serde::Deserialize<'de>>(&self, url: String)
|
||||
-> Result<T>
|
||||
async fn $method<T: for<'de> serde::Deserialize<'de>>(&self, url: impl AsRef<str>) -> Result<T>
|
||||
{
|
||||
let response = self.send(
|
||||
self.client.$method(&url)
|
||||
)?;
|
||||
|
||||
deserialise(response)
|
||||
let url = url.as_ref();
|
||||
Ok(
|
||||
self.client
|
||||
.$method(url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?
|
||||
)
|
||||
}
|
||||
)+
|
||||
};
|
||||
|
@ -30,13 +34,11 @@ macro_rules! paged_routes {
|
|||
"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 response = self.send(
|
||||
self.client.$method(&url)
|
||||
)?;
|
||||
let response = self.client.$method(&url).send().await?;
|
||||
|
||||
Page::new(self, response)
|
||||
Page::new(self.clone(), response).await
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,7 +53,7 @@ macro_rules! paged_routes {
|
|||
$url,
|
||||
"`\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;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -77,11 +79,9 @@ macro_rules! paged_routes {
|
|||
|
||||
let url = format!(concat!("/api/v1/", $url, "?{}"), &qs);
|
||||
|
||||
let response = self.send(
|
||||
self.client.get(&url)
|
||||
)?;
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
Page::new(self, response)
|
||||
Page::new(self.clone(), response).await
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ macro_rules! route_v2 {
|
|||
$url,
|
||||
"`\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;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -122,7 +122,7 @@ macro_rules! route_v2 {
|
|||
|
||||
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/",
|
||||
$url,
|
||||
"`\n# Errors\nIf `access_token` is not set."),
|
||||
fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
|
||||
use reqwest::multipart::Form;
|
||||
pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use std::io::Read;
|
||||
|
||||
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(
|
||||
self.client
|
||||
.post(&self.route(concat!("/api/v1/", $url)))
|
||||
.multipart(form_data)
|
||||
)?;
|
||||
let response = self.client
|
||||
.post(&self.route(concat!("/api/v1/", $url)))
|
||||
.multipart(form_data)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = response.status().clone();
|
||||
|
||||
|
@ -162,7 +172,7 @@ macro_rules! route {
|
|||
return Err(Error::Server(status));
|
||||
}
|
||||
|
||||
deserialise(response)
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,7 +186,7 @@ macro_rules! route {
|
|||
$url,
|
||||
"`\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;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -199,7 +209,7 @@ macro_rules! route {
|
|||
|
||||
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,
|
||||
"`\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!({
|
||||
$(
|
||||
|
@ -221,10 +231,11 @@ macro_rules! route {
|
|||
)*
|
||||
});
|
||||
|
||||
let response = self.send(
|
||||
self.client.$method(&self.route(concat!("/api/v1/", $url)))
|
||||
.json(&form_data)
|
||||
)?;
|
||||
let response = self.client
|
||||
.$method(&self.route(concat!("/api/v1/", $url)))
|
||||
.json(&form_data)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = response.status().clone();
|
||||
|
||||
|
@ -234,7 +245,7 @@ macro_rules! route {
|
|||
return Err(Error::Server(status));
|
||||
}
|
||||
|
||||
deserialise(response)
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,8 +266,8 @@ macro_rules! route {
|
|||
"client.", stringify!($name), "();\n",
|
||||
"```"
|
||||
),
|
||||
fn $name(&self) -> Result<$ret> {
|
||||
self.$method(self.route(concat!("/api/v1/", $url)))
|
||||
pub async fn $name(&self) -> Result<$ret> {
|
||||
self.$method(self.route(concat!("/api/v1/", $url))).await
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -285,8 +296,8 @@ macro_rules! route_id {
|
|||
"# }\n",
|
||||
"```"
|
||||
),
|
||||
fn $name(&self, id: &str) -> Result<$ret> {
|
||||
self.$method(self.route(&format!(concat!("/api/v1/", $url), id)))
|
||||
pub async fn $name(&self, id: &str) -> Result<$ret> {
|
||||
self.$method(self.route(&format!(concat!("/api/v1/", $url), id))).await
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
@ -309,13 +320,11 @@ macro_rules! paged_routes_with_id {
|
|||
"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 response = self.send(
|
||||
self.client.$method(&url)
|
||||
)?;
|
||||
let response = self.client.$method(&url).send().await?;
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
144
src/page.rs
144
src/page.rs
|
@ -1,11 +1,10 @@
|
|||
use super::{deserialise, Mastodon, Result};
|
||||
use crate::entities::itemsiter::ItemsIter;
|
||||
use super::{Mastodon, Result};
|
||||
use crate::{entities::itemsiter::ItemsIter, format_err};
|
||||
use futures::Stream;
|
||||
use hyper_old_types::header::{parsing, Link, RelationType};
|
||||
use reqwest::{header::LINK, Response};
|
||||
use reqwest::{header::LINK, Response, Url};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::http_send::HttpSend;
|
||||
// use url::Url;
|
||||
|
||||
macro_rules! pages {
|
||||
($($direction:ident: $fun:ident),*) => {
|
||||
|
@ -13,21 +12,19 @@ macro_rules! pages {
|
|||
$(
|
||||
doc_comment!(concat!(
|
||||
"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() {
|
||||
Some(s) => s,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let response = self.mastodon.send(
|
||||
self.mastodon.client.get(url)
|
||||
)?;
|
||||
let response = self.mastodon.client.get(url).send().await?;
|
||||
|
||||
let (prev, next) = get_links(&response)?;
|
||||
self.next = next;
|
||||
self.prev = prev;
|
||||
|
||||
deserialise(response)
|
||||
Ok(Some(response.json().await?))
|
||||
});
|
||||
)*
|
||||
}
|
||||
|
@ -41,71 +38,47 @@ macro_rules! pages {
|
|||
/// ```no_run
|
||||
/// use elefren::{
|
||||
/// prelude::*,
|
||||
/// page::OwnedPage,
|
||||
/// http_send::HttpSender,
|
||||
/// page::Page,
|
||||
/// entities::status::Status
|
||||
/// };
|
||||
/// 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)),
|
||||
/// };
|
||||
/// tokio_test::block_on(async {
|
||||
/// let data = Data::default();
|
||||
/// struct HomeTimeline {
|
||||
/// client: Mastodon,
|
||||
/// page: RefCell<Option<Page<Status>>>,
|
||||
/// }
|
||||
/// let client = Mastodon::from(data);
|
||||
/// let home = client.get_home_timeline().await.unwrap();
|
||||
/// let tl = HomeTimeline {
|
||||
/// client,
|
||||
/// 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
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Page<'a, T: for<'de> Deserialize<'de>, H: 'a + HttpSend> {
|
||||
mastodon: &'a Mastodon<H>,
|
||||
pub struct Page<T: for<'de> Deserialize<'de>> {
|
||||
mastodon: Mastodon,
|
||||
next: Option<Url>,
|
||||
prev: Option<Url>,
|
||||
/// Initial set of items
|
||||
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! {
|
||||
next: next_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)?;
|
||||
Ok(Page {
|
||||
initial_items: deserialise(response)?,
|
||||
initial_items: response.json().await?,
|
||||
next,
|
||||
prev,
|
||||
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> {
|
||||
/// 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)
|
||||
}
|
||||
|
||||
impl<T: Clone + for<'de> Deserialize<'de>> Page<T> {
|
||||
/// Returns an iterator that provides a stream of `T`s
|
||||
///
|
||||
/// 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
|
||||
/// use elefren::prelude::*;
|
||||
/// use futures_util::StreamExt;
|
||||
///
|
||||
/// let data = Data::default();
|
||||
/// let mastodon = Mastodon::from(data);
|
||||
/// let req = StatusesRequest::new();
|
||||
/// let resp = mastodon.statuses("some-id", req).unwrap();
|
||||
/// for status in resp.items_iter() {
|
||||
/// // do something with status
|
||||
/// }
|
||||
///
|
||||
/// 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
|
||||
/// }).await;
|
||||
/// });
|
||||
/// ```
|
||||
pub fn items_iter(self) -> impl Iterator<Item = T> + 'a
|
||||
where
|
||||
T: 'a,
|
||||
{
|
||||
ItemsIter::new(self)
|
||||
pub fn items_iter(self) -> impl Stream<Item = T> {
|
||||
ItemsIter::new(self).stream()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,11 +130,22 @@ fn get_links(response: &Response) -> Result<(Option<Url>, Option<Url>)> {
|
|||
for value in link_header.values() {
|
||||
if let Some(relations) = value.rel() {
|
||||
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) {
|
||||
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 reqwest::{Client, RequestBuilder, Response};
|
||||
use reqwest::Client;
|
||||
use url::percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
|
||||
|
||||
use crate::{
|
||||
apps::{App, AppBuilder},
|
||||
http_send::{HttpSend, HttpSender},
|
||||
scopes::Scopes,
|
||||
Data,
|
||||
Error,
|
||||
Mastodon,
|
||||
MastodonBuilder,
|
||||
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
|
||||
/// you cache your data struct to avoid registering on every run.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Registration<'a, H: HttpSend = HttpSender> {
|
||||
pub struct Registration<'a> {
|
||||
base: String,
|
||||
client: Client,
|
||||
app_builder: AppBuilder<'a>,
|
||||
force_login: bool,
|
||||
http_sender: H,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -44,7 +41,7 @@ struct AccessToken {
|
|||
access_token: String,
|
||||
}
|
||||
|
||||
impl<'a> Registration<'a, HttpSender> {
|
||||
impl<'a> Registration<'a> {
|
||||
/// Construct a new registration process to the instance of the `base` url.
|
||||
/// ```
|
||||
/// use elefren::prelude::*;
|
||||
|
@ -57,20 +54,18 @@ impl<'a> Registration<'a, HttpSender> {
|
|||
client: Client::new(),
|
||||
app_builder: AppBuilder::new(),
|
||||
force_login: false,
|
||||
http_sender: HttpSender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, H: HttpSend> Registration<'a, H> {
|
||||
impl<'a> Registration<'a> {
|
||||
#[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 {
|
||||
base: base.into(),
|
||||
client: Client::new(),
|
||||
app_builder: AppBuilder::new(),
|
||||
force_login: false,
|
||||
http_sender,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,33 +105,34 @@ impl<'a, H: HttpSend> Registration<'a, H> {
|
|||
self
|
||||
}
|
||||
|
||||
fn send(&self, req: RequestBuilder) -> Result<Response> {
|
||||
Ok(self.http_sender.send(&self.client, req)?)
|
||||
}
|
||||
|
||||
/// Register the given application
|
||||
///
|
||||
/// ```no_run
|
||||
/// use elefren::{apps::App, prelude::*};
|
||||
///
|
||||
/// let mut app = App::builder();
|
||||
/// app.client_name("elefren_test");
|
||||
/// tokio_test::block_on(async {
|
||||
/// let mut app = App::builder();
|
||||
/// app.client_name("elefren_test");
|
||||
///
|
||||
/// let registration = Registration::new("https://botsin.space").register(app).unwrap();
|
||||
/// let url = registration.authorize_url().unwrap();
|
||||
/// // Here you now need to open the url in the browser
|
||||
/// // And handle a the redirect url coming back with the code.
|
||||
/// let code = String::from("RETURNED_FROM_BROWSER");
|
||||
/// let mastodon = registration.complete(&code).unwrap();
|
||||
/// let registration = Registration::new("https://botsin.space")
|
||||
/// .register(app)
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// let url = registration.authorize_url().unwrap();
|
||||
/// // Here you now need to open the url in the browser
|
||||
/// // And handle a the redirect url coming back with the code.
|
||||
/// let code = String::from("RETURNED_FROM_BROWSER");
|
||||
/// 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
|
||||
Error: From<<I as TryInto<App>>::Error>,
|
||||
{
|
||||
let app = app.try_into()?;
|
||||
let oauth = self.send_app(&app)?;
|
||||
let oauth = self.send_app(&app).await?;
|
||||
|
||||
Ok(Registered {
|
||||
base: self.base.clone(),
|
||||
|
@ -146,7 +142,6 @@ impl<'a, H: HttpSend> Registration<'a, H> {
|
|||
redirect: oauth.redirect_uri,
|
||||
scopes: app.scopes().clone(),
|
||||
force_login: self.force_login,
|
||||
http_sender: self.http_sender.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -155,20 +150,24 @@ impl<'a, H: HttpSend> Registration<'a, H> {
|
|||
/// ```no_run
|
||||
/// use elefren::prelude::*;
|
||||
///
|
||||
/// let registration = Registration::new("https://botsin.space")
|
||||
/// .client_name("elefren_test")
|
||||
/// .build().unwrap();
|
||||
/// let url = registration.authorize_url().unwrap();
|
||||
/// // Here you now need to open the url in the browser
|
||||
/// // And handle a the redirect url coming back with the code.
|
||||
/// let code = String::from("RETURNED_FROM_BROWSER");
|
||||
/// let mastodon = registration.complete(&code).unwrap();
|
||||
/// tokio_test::block_on(async {
|
||||
/// let registration = Registration::new("https://botsin.space")
|
||||
/// .client_name("elefren_test")
|
||||
/// .build()
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// let url = registration.authorize_url().unwrap();
|
||||
/// // Here you now need to open the url in the browser
|
||||
/// // And handle a the redirect url coming back with the code.
|
||||
/// let code = String::from("RETURNED_FROM_BROWSER");
|
||||
/// 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 oauth = self.send_app(&app)?;
|
||||
let oauth = self.send_app(&app).await?;
|
||||
|
||||
Ok(Registered {
|
||||
base: self.base.clone(),
|
||||
|
@ -178,17 +177,17 @@ impl<'a, H: HttpSend> Registration<'a, H> {
|
|||
redirect: oauth.redirect_uri,
|
||||
scopes: app.scopes().clone(),
|
||||
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);
|
||||
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
|
||||
/// creating a `Registered` struct directly
|
||||
///
|
||||
|
@ -197,21 +196,23 @@ impl Registered<HttpSender> {
|
|||
/// ```no_run
|
||||
/// use elefren::{prelude::*, registration::Registered};
|
||||
///
|
||||
/// let registration = Registered::from_parts(
|
||||
/// "https://example.com",
|
||||
/// "the-client-id",
|
||||
/// "the-client-secret",
|
||||
/// "https://example.com/redirect",
|
||||
/// Scopes::read_all(),
|
||||
/// false,
|
||||
/// );
|
||||
/// let url = registration.authorize_url().unwrap();
|
||||
/// // Here you now need to open the url in the browser
|
||||
/// // And handle a the redirect url coming back with the code.
|
||||
/// let code = String::from("RETURNED_FROM_BROWSER");
|
||||
/// let mastodon = registration.complete(&code).unwrap();
|
||||
/// tokio_test::block_on(async {
|
||||
/// let registration = Registered::from_parts(
|
||||
/// "https://example.com",
|
||||
/// "the-client-id",
|
||||
/// "the-client-secret",
|
||||
/// "https://example.com/redirect",
|
||||
/// Scopes::read_all(),
|
||||
/// false,
|
||||
/// );
|
||||
/// let url = registration.authorize_url().unwrap();
|
||||
/// // Here you now need to open the url in the browser
|
||||
/// // And handle a the redirect url coming back with the code.
|
||||
/// let code = String::from("RETURNED_FROM_BROWSER");
|
||||
/// 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(
|
||||
base: &str,
|
||||
|
@ -220,7 +221,7 @@ impl Registered<HttpSender> {
|
|||
redirect: &str,
|
||||
scopes: Scopes,
|
||||
force_login: bool,
|
||||
) -> Registered<HttpSender> {
|
||||
) -> Registered {
|
||||
Registered {
|
||||
base: base.to_string(),
|
||||
client: Client::new(),
|
||||
|
@ -229,16 +230,11 @@ impl Registered<HttpSender> {
|
|||
redirect: redirect.to_string(),
|
||||
scopes,
|
||||
force_login,
|
||||
http_sender: HttpSender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: HttpSend> Registered<H> {
|
||||
fn send(&self, req: RequestBuilder) -> Result<Response> {
|
||||
Ok(self.http_sender.send(&self.client, req)?)
|
||||
}
|
||||
|
||||
impl Registered {
|
||||
/// Returns the parts of the `Registered` struct that can be used to
|
||||
/// recreate another `Registered` struct
|
||||
///
|
||||
|
@ -304,35 +300,45 @@ impl<H: HttpSend> Registered<H> {
|
|||
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
|
||||
/// 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!(
|
||||
"{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&\
|
||||
redirect_uri={}",
|
||||
self.base, self.client_id, self.client_secret, code, self.redirect
|
||||
);
|
||||
|
||||
let token: AccessToken = self.send(self.client.post(&url))?.json()?;
|
||||
let token: AccessToken = self
|
||||
.client
|
||||
.post(&url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
let 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.access_token.into(),
|
||||
};
|
||||
let data = self.registered(token.access_token);
|
||||
|
||||
let mut builder = MastodonBuilder::new(self.http_sender.clone());
|
||||
builder.client(self.client.clone()).data(data);
|
||||
Ok(builder.build()?)
|
||||
Ok(Mastodon::new(self.client.clone(), data))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of the auth flow when the app has been registered but
|
||||
/// the user is not authenticated
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Registered<H: HttpSend> {
|
||||
pub struct Registered {
|
||||
base: String,
|
||||
client: Client,
|
||||
client_id: String,
|
||||
|
@ -340,7 +346,6 @@ pub struct Registered<H: HttpSend> {
|
|||
redirect: String,
|
||||
scopes: Scopes,
|
||||
force_login: bool,
|
||||
http_sender: H,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -352,15 +357,13 @@ mod tests {
|
|||
let r = Registration::new("https://example.com");
|
||||
assert_eq!(r.base, "https://example.com".to_string());
|
||||
assert_eq!(r.app_builder, AppBuilder::new());
|
||||
assert_eq!(r.http_sender, HttpSender);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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.app_builder, AppBuilder::new());
|
||||
assert_eq!(r.http_sender, HttpSender);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -41,17 +41,19 @@ impl Keys {
|
|||
/// // Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use elefren::{MastodonClient, Mastodon, Data};
|
||||
/// use elefren::{Mastodon, Data};
|
||||
/// use elefren::requests::{AddPushRequest, Keys};
|
||||
///
|
||||
/// let data = Data::default();
|
||||
/// let client = Mastodon::from(data);
|
||||
/// tokio_test::block_on(async {
|
||||
/// let data = Data::default();
|
||||
/// let client = Mastodon::from(data);
|
||||
///
|
||||
/// let keys = Keys::new("stahesuahoei293ise===", "tasecoa,nmeozka==");
|
||||
/// let mut request = AddPushRequest::new("http://example.com/push/endpoint", &keys);
|
||||
/// request.follow().reblog();
|
||||
/// let keys = Keys::new("stahesuahoei293ise===", "tasecoa,nmeozka==");
|
||||
/// let mut request = AddPushRequest::new("http://example.com/push/endpoint", &keys);
|
||||
/// request.follow().reblog();
|
||||
///
|
||||
/// client.add_push_subscription(&request).unwrap();
|
||||
/// client.add_push_subscription(&request).await.unwrap();
|
||||
/// });
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct AddPushRequest {
|
||||
|
@ -200,16 +202,18 @@ impl AddPushRequest {
|
|||
/// // Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use elefren::{MastodonClient, Mastodon, Data, requests::UpdatePushRequest};
|
||||
/// let data = Data::default();
|
||||
/// use elefren::{Mastodon, Data, requests::UpdatePushRequest};
|
||||
///
|
||||
/// let data = Data::default();
|
||||
/// let client = Mastodon::from(data);
|
||||
///
|
||||
/// let mut request = UpdatePushRequest::new("foobar");
|
||||
/// request.follow(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)]
|
||||
pub struct UpdatePushRequest {
|
||||
|
|
|
@ -22,7 +22,9 @@ use crate::{
|
|||
///
|
||||
/// 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)]
|
||||
pub struct UpdateCredsRequest {
|
||||
|
|
|
@ -42,7 +42,10 @@ impl StatusBuilder {
|
|||
/// .visibility(Visibility::Public)
|
||||
/// .build()
|
||||
/// .unwrap();
|
||||
/// client.new_status(status).unwrap();
|
||||
///
|
||||
/// tokio_test::block_on(async {
|
||||
/// client.new_status(status).await.unwrap();
|
||||
/// });
|
||||
/// ```
|
||||
pub fn new() -> StatusBuilder {
|
||||
StatusBuilder::default()
|
||||
|
|
Loading…
Reference in New Issue