commit
6fb63800d7
|
@ -62,6 +62,9 @@ optional = true
|
||||||
version = "1.22.0"
|
version = "1.22.0"
|
||||||
features = ["rt-multi-thread", "macros"]
|
features = ["rt-multi-thread", "macros"]
|
||||||
|
|
||||||
|
[dependencies.tokio-util]
|
||||||
|
version = "0.7.4"
|
||||||
|
features = ["io"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4.2"
|
tokio-test = "0.4.2"
|
||||||
|
|
48
README.md
48
README.md
|
@ -2,12 +2,11 @@
|
||||||
|
|
||||||
## A Wrapper for the Mastodon API.
|
## A Wrapper for the Mastodon API.
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/pwoolcoc/elefren.svg?branch=master)](https://travis-ci.org/pwoolcoc/elefren)
|
[![Build Status](https://github.com/dscottboggs/elefren/actions/workflows/rust.yml/badge.svg)](https://travis-ci.org/pwoolcoc/elefren)
|
||||||
[![Build Status](https://ci.appveyor.com/api/projects/status/qeigk3nmmps52wxv?svg=true)](https://ci.appveyor.com/project/pwoolcoc/elefren)
|
<!-- [![Coverage Status](https://coveralls.io/repos/github/pwoolcoc/elefren/badge.svg?branch=master&service=github)](https://coveralls.io/github/pwoolcoc/elefren?branch=master) -->
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/pwoolcoc/elefren/badge.svg?branch=master&service=github)](https://coveralls.io/github/pwoolcoc/elefren?branch=master)
|
<!-- [![crates.io](https://img.shields.io/crates/v/elefren.svg)](https://crates.io/crates/elefren) -->
|
||||||
[![crates.io](https://img.shields.io/crates/v/elefren.svg)](https://crates.io/crates/elefren)
|
<!-- [![Docs](https://docs.rs/elefren/badge.svg)](https://docs.rs/elefren) -->
|
||||||
[![Docs](https://docs.rs/elefren/badge.svg)](https://docs.rs/elefren)
|
<!-- [![MIT/APACHE-2.0](https://img.shields.io/crates/l/elefren.svg)](https://crates.io/crates/elefren) -->
|
||||||
[![MIT/APACHE-2.0](https://img.shields.io/crates/l/elefren.svg)](https://crates.io/crates/elefren)
|
|
||||||
|
|
||||||
[Documentation](https://docs.rs/elefren/)
|
[Documentation](https://docs.rs/elefren/)
|
||||||
|
|
||||||
|
@ -46,14 +45,15 @@ use elefren::prelude::*;
|
||||||
use elefren::helpers::toml; // requires `features = ["toml"]`
|
use elefren::helpers::toml; // requires `features = ["toml"]`
|
||||||
use elefren::helpers::cli;
|
use elefren::helpers::cli;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let mastodon = if let Ok(data) = toml::from_file("mastodon-data.toml") {
|
let mastodon = if let Ok(data) = toml::from_file("mastodon-data.toml") {
|
||||||
Mastodon::from(data)
|
Mastodon::from(data)
|
||||||
} else {
|
} else {
|
||||||
register()?
|
register()?
|
||||||
};
|
};
|
||||||
|
|
||||||
let you = mastodon.verify_credentials()?;
|
let you = mastodon.verify_credentials().await?;
|
||||||
|
|
||||||
println!("{:#?}", you);
|
println!("{:#?}", you);
|
||||||
|
|
||||||
|
@ -81,25 +81,21 @@ use elefren::entities::event::Event;
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<Error>> {
|
#[tokio::main]
|
||||||
let data = Data {
|
async fn main() -> Result<(), Box<Error>> {
|
||||||
base: "".into(),
|
let client = Mastodon::from(Data::default());
|
||||||
client_id: "".into(),
|
|
||||||
client_secret: "".into(),
|
|
||||||
redirect: "".into(),
|
|
||||||
token: "".into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = Mastodon::from(data);
|
client.stream_user()
|
||||||
|
.await?
|
||||||
for event in client.streaming_user()? {
|
.try_for_each(|event| {
|
||||||
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 => { /* .. */ },
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -12,7 +12,6 @@ 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, 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.
|
||||||
|
@ -62,16 +61,12 @@ pub enum Error {
|
||||||
Envy(EnvyError),
|
Envy(EnvyError),
|
||||||
/// Error serializing to a query string
|
/// Error serializing to a query string
|
||||||
SerdeQs(SerdeQsError),
|
SerdeQs(SerdeQsError),
|
||||||
/// WebSocket error
|
|
||||||
WebSocket(WebSocketError),
|
|
||||||
/// An integer conversion was attempted, but the value didn't fit into the
|
/// An integer conversion was attempted, but the value didn't fit into the
|
||||||
/// target type.
|
/// target type.
|
||||||
///
|
///
|
||||||
/// At the time of writing, this can only be triggered when a file is
|
/// At the time of writing, this can only be triggered when a file is
|
||||||
/// larger than the system's usize allows.
|
/// larger than the system's usize allows.
|
||||||
IntConversion(TryFromIntError),
|
IntConversion(TryFromIntError),
|
||||||
/// A stream message was received that wasn't recognized
|
|
||||||
UnrecognizedStreamMessage(WebSocketMessage),
|
|
||||||
/// Other errors
|
/// Other errors
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
@ -100,14 +95,12 @@ impl error::Error for Error {
|
||||||
#[cfg(feature = "env")]
|
#[cfg(feature = "env")]
|
||||||
Error::Envy(ref e) => e,
|
Error::Envy(ref e) => e,
|
||||||
Error::SerdeQs(ref e) => e,
|
Error::SerdeQs(ref e) => e,
|
||||||
Error::WebSocket(ref e) => e,
|
|
||||||
Error::IntConversion(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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -157,10 +150,8 @@ from! {
|
||||||
HeaderParseError => HeaderParseError,
|
HeaderParseError => HeaderParseError,
|
||||||
#[cfg(feature = "env")] EnvyError => Envy,
|
#[cfg(feature = "env")] EnvyError => Envy,
|
||||||
SerdeQsError => SerdeQs,
|
SerdeQsError => SerdeQs,
|
||||||
WebSocketError => WebSocket,
|
|
||||||
String => Other,
|
String => Other,
|
||||||
TryFromIntError => IntConversion,
|
TryFromIntError => IntConversion,
|
||||||
WebSocketMessage => UnrecognizedStreamMessage,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
|
|
@ -1,61 +1,49 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
entities::{event::Event, prelude::Notification, status::Status},
|
entities::{event::Event, prelude::Notification, status::Status},
|
||||||
errors::Result,
|
errors::Result,
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
use futures::{stream::try_unfold, TryStream};
|
use futures::{stream::try_unfold, TryStream, TryStreamExt};
|
||||||
use log::{as_debug, as_serde, debug, error, info, trace};
|
use log::{as_debug, as_serde, debug, error, info, trace};
|
||||||
use tungstenite::Message;
|
use reqwest::Response;
|
||||||
|
use tokio::io::AsyncBufReadExt;
|
||||||
|
use tokio_util::io::StreamReader;
|
||||||
|
|
||||||
/// Returns a stream of events at the given url location.
|
/// Return a stream of events from the given response by parsing Server-Sent
|
||||||
|
/// Events as they come in.
|
||||||
|
///
|
||||||
|
/// See <https://docs.joinmastodon.org/methods/streaming/> for more info
|
||||||
pub fn event_stream(
|
pub fn event_stream(
|
||||||
|
response: Response,
|
||||||
location: String,
|
location: String,
|
||||||
) -> Result<impl TryStream<Ok = Event, Error = Error, Item = Result<Event>>> {
|
) -> impl TryStream<Ok = Event, Error = Error> {
|
||||||
trace!(location = location; "connecting to websocket for events");
|
let stream = StreamReader::new(response.bytes_stream().map_err(|err| {
|
||||||
let (client, response) = tungstenite::connect(&location)?;
|
error!(err = as_debug!(err); "error reading stream");
|
||||||
let status = response.status();
|
io::Error::new(io::ErrorKind::BrokenPipe, format!("{err:?}"))
|
||||||
if !status.is_success() {
|
}));
|
||||||
error!(
|
let lines_iter = stream.lines();
|
||||||
status = as_debug!(status),
|
try_unfold((lines_iter, location), |mut this| async move {
|
||||||
body = response.body().as_ref().map(|it| String::from_utf8_lossy(it.as_slice())).unwrap_or("(empty body)".into()),
|
let (ref mut lines_iter, ref location) = this;
|
||||||
location = &location;
|
|
||||||
"error connecting to websocket"
|
|
||||||
);
|
|
||||||
return Err(Error::Api(crate::ApiError {
|
|
||||||
error: status.canonical_reason().map(String::from),
|
|
||||||
error_description: None,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
debug!(location = &location, status = as_debug!(status); "successfully connected to websocket");
|
|
||||||
Ok(try_unfold((client, location), |mut this| async move {
|
|
||||||
let (ref mut client, ref location) = this;
|
|
||||||
let mut lines = vec![];
|
let mut lines = vec![];
|
||||||
loop {
|
while let Some(line) = lines_iter.next_line().await? {
|
||||||
match client.read_message() {
|
debug!(message = line, location = &location; "received message");
|
||||||
Ok(Message::Text(line)) => {
|
let line = line.trim().to_string();
|
||||||
debug!(message = line, location = &location; "received websocket message");
|
if line.starts_with(":") || line.is_empty() {
|
||||||
let line = line.trim().to_string();
|
continue;
|
||||||
if line.starts_with(":") || line.is_empty() {
|
}
|
||||||
continue;
|
lines.push(line);
|
||||||
}
|
if let Ok(event) = make_event(&lines) {
|
||||||
lines.push(line);
|
info!(event = as_serde!(event), location = location; "received event");
|
||||||
if let Ok(event) = make_event(&lines) {
|
lines.clear();
|
||||||
info!(event = as_serde!(event), location = location; "received websocket event");
|
return Ok(Some((event, this)));
|
||||||
lines.clear();
|
} else {
|
||||||
return Ok(Some((event, this)));
|
continue;
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(Message::Ping(data)) => {
|
|
||||||
debug!(metadata = as_serde!(data); "received ping, ponging");
|
|
||||||
client.write_message(Message::Pong(data))?;
|
|
||||||
},
|
|
||||||
Ok(message) => return Err(message.into()),
|
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
Ok(None)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_event(lines: &[String]) -> Result<Event> {
|
fn make_event(lines: &[String]) -> Result<Event> {
|
||||||
|
@ -78,7 +66,7 @@ fn make_event(lines: &[String]) -> Result<Event> {
|
||||||
data = message.payload;
|
data = message.payload;
|
||||||
}
|
}
|
||||||
let event: &str = &event;
|
let event: &str = &event;
|
||||||
trace!(event = event, payload = data; "websocket message parsed");
|
trace!(event = event, payload = data; "SSE message parsed");
|
||||||
Ok(match event {
|
Ok(match event {
|
||||||
"notification" => {
|
"notification" => {
|
||||||
let data = data
|
let data = data
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
//! let data = Data::default();
|
//! let data = Data::default();
|
||||||
//! let client = Mastodon::from(data);
|
//! let client = Mastodon::from(data);
|
||||||
//! tokio_test::block_on(async {
|
//! tokio_test::block_on(async {
|
||||||
//! let stream = client.streaming_user().await.unwrap();
|
//! let stream = client.stream_user().await.unwrap();
|
||||||
//! stream.try_for_each(|event| async move {
|
//! stream.try_for_each(|event| async move {
|
||||||
//! match event {
|
//! match event {
|
||||||
//! Event::Update(ref status) => { /* .. */ },
|
//! Event::Update(ref status) => { /* .. */ },
|
||||||
|
|
|
@ -438,3 +438,91 @@ macro_rules! paged_routes_with_id {
|
||||||
|
|
||||||
() => {}
|
() => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! streaming {
|
||||||
|
($desc:tt $fn_name:ident@$stream:literal, $($rest:tt)*) => {
|
||||||
|
doc_comment! {
|
||||||
|
concat!(
|
||||||
|
$desc,
|
||||||
|
"\n\nExample:\n\n",
|
||||||
|
"
|
||||||
|
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.",
|
||||||
|
stringify!($fn_name),
|
||||||
|
"().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 $fn_name(&self) -> Result<impl TryStream<Ok=Event, Error=Error>> {
|
||||||
|
let url = self.route(&format!("/api/v1/streaming/{}", $stream));
|
||||||
|
let response = self.authenticated(self.client.get(&url)).send().await?;
|
||||||
|
debug!(
|
||||||
|
status = log_serde!(response Status), url = &url,
|
||||||
|
headers = log_serde!(response Headers);
|
||||||
|
"received API response"
|
||||||
|
);
|
||||||
|
Ok(event_stream(response.error_for_status()?, url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streaming! { $($rest)* }
|
||||||
|
};
|
||||||
|
($desc:tt $fn_name:ident($param:ident: $param_type:ty, like $param_doc_val:literal)@$stream:literal, $($rest:tt)*) => {
|
||||||
|
doc_comment! {
|
||||||
|
concat!(
|
||||||
|
$desc,
|
||||||
|
"\n\nExample:\n\n",
|
||||||
|
"
|
||||||
|
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.",
|
||||||
|
stringify!($fn_name),
|
||||||
|
"(",
|
||||||
|
$param_doc_val,
|
||||||
|
").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 $fn_name(&self, $param: $param_type) -> Result<impl TryStream<Ok=Event, Error=Error>> {
|
||||||
|
let mut url: Url = self.route(concat!("/api/v1/streaming/", stringify!($stream))).parse()?;
|
||||||
|
url.query_pairs_mut().append_pair(stringify!($param), $param.as_ref());
|
||||||
|
let url = url.to_string();
|
||||||
|
let response = self.authenticated(self.client.get(url.as_str())).send().await?;
|
||||||
|
debug!(
|
||||||
|
status = log_serde!(response Status), url = as_debug!(url),
|
||||||
|
headers = log_serde!(response Headers);
|
||||||
|
"received API response"
|
||||||
|
);
|
||||||
|
Ok(event_stream(response.error_for_status()?, url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streaming! { $($rest)* }
|
||||||
|
};
|
||||||
|
() => {}
|
||||||
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ use crate::{
|
||||||
UpdatePushRequest,
|
UpdatePushRequest,
|
||||||
};
|
};
|
||||||
use futures::TryStream;
|
use futures::TryStream;
|
||||||
use log::{as_debug, as_serde, debug, error, info, trace};
|
use log::{as_debug, as_serde, debug, error, trace};
|
||||||
use reqwest::{Client, RequestBuilder};
|
use reqwest::{Client, RequestBuilder};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -129,6 +129,33 @@ impl Mastodon {
|
||||||
(post) unendorse_user: "accounts/{}/unpin" => Relationship,
|
(post) unendorse_user: "accounts/{}/unpin" => Relationship,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
streaming! {
|
||||||
|
"returns events that are relevant to the authorized user, i.e. home timeline & notifications"
|
||||||
|
stream_user@"user",
|
||||||
|
"All public posts known to the server. Analogous to the federated timeline."
|
||||||
|
stream_public@"public",
|
||||||
|
"All public posts known to the server, filtered for media attachments. Analogous to the federated timeline with 'only media' enabled."
|
||||||
|
stream_public_media@"public:media",
|
||||||
|
"All public posts originating from this server."
|
||||||
|
stream_local@"public:local",
|
||||||
|
"All public posts originating from this server, filtered for media attachments. Analogous to the local timeline with 'only media' enabled."
|
||||||
|
stream_local_media@"public:local:media",
|
||||||
|
"All public posts originating from other servers."
|
||||||
|
stream_remote@"public:remote",
|
||||||
|
"All public posts originating from other servers, filtered for media attachments."
|
||||||
|
stream_remote_media@"public:remote:media",
|
||||||
|
"All public posts using a certain hashtag."
|
||||||
|
stream_hashtag(tag: impl AsRef<str>, like "#bots")@"hashtag",
|
||||||
|
"All public posts using a certain hashtag, originating from this server."
|
||||||
|
stream_local_hashtag(tag: impl AsRef<str>, like "#bots")@"hashtag:local",
|
||||||
|
"Notifications for the current user."
|
||||||
|
stream_notifications@"user:notification",
|
||||||
|
"Updates to a specific list."
|
||||||
|
stream_list(list: impl AsRef<str>, like "12345")@"list",
|
||||||
|
"Updates to direct conversations."
|
||||||
|
stream_direct@"direct",
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new Mastodon Client
|
/// Create a new Mastodon Client
|
||||||
pub fn new(client: Client, data: Data) -> Self {
|
pub fn new(client: Client, data: Data) -> Self {
|
||||||
Mastodon(Arc::new(MastodonClient {
|
Mastodon(Arc::new(MastodonClient {
|
||||||
|
@ -368,56 +395,6 @@ impl Mastodon {
|
||||||
self.following(&me.id).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 call_id = Uuid::new_v4();
|
|
||||||
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");
|
|
||||||
debug!(
|
|
||||||
url = url.as_str(), call_id = as_debug!(call_id);
|
|
||||||
"making user streaming API request"
|
|
||||||
);
|
|
||||||
let response = reqwest::get(url.as_str()).await?;
|
|
||||||
let mut url: Url = response.url().as_str().parse()?;
|
|
||||||
info!(
|
|
||||||
url = url.as_str(), call_id = as_debug!(call_id),
|
|
||||||
status = response.status().as_str();
|
|
||||||
"received url from streaming API request"
|
|
||||||
);
|
|
||||||
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()))?;
|
|
||||||
|
|
||||||
/// Set the bearer authentication token
|
/// Set the bearer authentication token
|
||||||
fn authenticated(&self, request: RequestBuilder) -> RequestBuilder {
|
fn authenticated(&self, request: RequestBuilder) -> RequestBuilder {
|
||||||
request.bearer_auth(&self.data.token)
|
request.bearer_auth(&self.data.token)
|
||||||
|
|
|
@ -1,370 +0,0 @@
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
entities::prelude::*,
|
|
||||||
errors::Result,
|
|
||||||
http_send::{HttpSend, HttpSender},
|
|
||||||
page::Page,
|
|
||||||
requests::{
|
|
||||||
AddFilterRequest,
|
|
||||||
AddPushRequest,
|
|
||||||
StatusesRequest,
|
|
||||||
UpdateCredsRequest,
|
|
||||||
UpdatePushRequest,
|
|
||||||
},
|
|
||||||
status_builder::NewStatus,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Represents the set of methods that a Mastodon Client can do, so that
|
|
||||||
/// implementations might be swapped out for testing
|
|
||||||
#[allow(unused)]
|
|
||||||
pub trait MastodonClient<H: HttpSend = HttpSender> {
|
|
||||||
/// Type that wraps streaming API streams
|
|
||||||
type Stream: Iterator<Item = Event>;
|
|
||||||
|
|
||||||
/// GET /api/v1/favourites
|
|
||||||
fn favourites(&self) -> Result<Page<Status, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/blocks
|
|
||||||
fn blocks(&self) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/domain_blocks
|
|
||||||
fn domain_blocks(&self) -> Result<Page<String, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/follow_requests
|
|
||||||
fn follow_requests(&self) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/timelines/home
|
|
||||||
fn get_home_timeline(&self) -> Result<Page<Status, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/custom_emojis
|
|
||||||
fn get_emojis(&self) -> Result<Page<Emoji, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/mutes
|
|
||||||
fn mutes(&self) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/notifications
|
|
||||||
fn notifications(&self) -> Result<Page<Notification, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/reports
|
|
||||||
fn reports(&self) -> Result<Page<Report, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/:id/followers
|
|
||||||
fn followers(&self, id: &str) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/:id/following
|
|
||||||
fn following(&self, id: &str) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/statuses/:id/reblogged_by
|
|
||||||
fn reblogged_by(&self, id: &str) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/statuses/:id/favourited_by
|
|
||||||
fn favourited_by(&self, id: &str) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// DELETE /api/v1/domain_blocks
|
|
||||||
fn unblock_domain(&self, domain: String) -> Result<Empty> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/instance
|
|
||||||
fn instance(&self) -> Result<Instance> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/verify_credentials
|
|
||||||
fn verify_credentials(&self) -> Result<Account> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/reports
|
|
||||||
fn report(&self, account_id: &str, status_ids: Vec<&str>, comment: String) -> Result<Report> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/domain_blocks
|
|
||||||
fn block_domain(&self, domain: String) -> Result<Empty> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/accounts/follow_requests/authorize
|
|
||||||
fn authorize_follow_request(&self, id: &str) -> Result<Empty> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/accounts/follow_requests/reject
|
|
||||||
fn reject_follow_request(&self, id: &str) -> Result<Empty> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/search
|
|
||||||
fn search<'a>(&self, q: &'a str, resolve: bool) -> Result<SearchResult> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v2/search
|
|
||||||
fn search_v2<'a>(&self, q: &'a str, resolve: bool) -> Result<SearchResultV2> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/follows
|
|
||||||
fn follows(&self, uri: Cow<'static, str>) -> Result<Account> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/media
|
|
||||||
fn media(&self, file: Cow<'static, str>) -> Result<Attachment> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/notifications/clear
|
|
||||||
fn clear_notifications(&self) -> Result<Empty> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/notifications/dismiss
|
|
||||||
fn dismiss_notification(&self, id: &str) -> Result<Empty> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/:id
|
|
||||||
fn get_account(&self, id: &str) -> Result<Account> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/accounts/:id/follow
|
|
||||||
fn follow(&self, id: &str) -> Result<Relationship> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/accounts/:id/unfollow
|
|
||||||
fn unfollow(&self, id: &str) -> Result<Relationship> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/:id/block
|
|
||||||
fn block(&self, id: &str) -> Result<Relationship> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/:id/unblock
|
|
||||||
fn unblock(&self, id: &str) -> Result<Relationship> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/:id/mute
|
|
||||||
fn mute(&self, id: &str) -> Result<Relationship> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/:id/unmute
|
|
||||||
fn unmute(&self, id: &str) -> Result<Relationship> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/notifications/:id
|
|
||||||
fn get_notification(&self, id: &str) -> Result<Notification> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/statuses/:id
|
|
||||||
fn get_status(&self, id: &str) -> Result<Status> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/statuses/:id/context
|
|
||||||
fn get_context(&self, id: &str) -> Result<Context> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/statuses/:id/card
|
|
||||||
fn get_card(&self, id: &str) -> Result<Card> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/statuses/:id/reblog
|
|
||||||
fn reblog(&self, id: &str) -> Result<Status> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/statuses/:id/unreblog
|
|
||||||
fn unreblog(&self, id: &str) -> Result<Status> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/statuses/:id/favourite
|
|
||||||
fn favourite(&self, id: &str) -> Result<Status> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/statuses/:id/unfavourite
|
|
||||||
fn unfavourite(&self, id: &str) -> Result<Status> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// DELETE /api/v1/statuses/:id
|
|
||||||
fn delete_status(&self, id: &str) -> Result<Empty> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// PATCH /api/v1/accounts/update_credentials
|
|
||||||
fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result<Account> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/statuses
|
|
||||||
fn new_status(&self, status: NewStatus) -> Result<Status> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/timelines/public
|
|
||||||
fn get_public_timeline(&self, local: bool) -> Result<Vec<Status>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/timelines/tag/:hashtag
|
|
||||||
fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/:id/statuses
|
|
||||||
fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result<Page<Status, H>>
|
|
||||||
where
|
|
||||||
S: Into<Option<StatusesRequest<'a>>>,
|
|
||||||
{
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/relationships
|
|
||||||
fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/accounts/search?q=:query&limit=:limit&following=:following
|
|
||||||
fn search_accounts(
|
|
||||||
&self,
|
|
||||||
query: &str,
|
|
||||||
limit: Option<u64>,
|
|
||||||
following: bool,
|
|
||||||
) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/push/subscription
|
|
||||||
fn add_push_subscription(&self, request: &AddPushRequest) -> Result<Subscription> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// PUT /api/v1/push/subscription
|
|
||||||
fn update_push_data(&self, request: &UpdatePushRequest) -> Result<Subscription> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/push/subscription
|
|
||||||
fn get_push_subscription(&self) -> Result<Subscription> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// DELETE /api/v1/push/subscription
|
|
||||||
fn delete_push_subscription(&self) -> Result<Empty> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/filters
|
|
||||||
fn get_filters(&self) -> Result<Vec<Filter>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/filters
|
|
||||||
fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/filters/:id
|
|
||||||
fn get_filter(&self, id: &str) -> Result<Filter> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// PUT /api/v1/filters/:id
|
|
||||||
fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result<Filter> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// DELETE /api/v1/filters/:id
|
|
||||||
fn delete_filter(&self, id: &str) -> Result<Empty> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/suggestions
|
|
||||||
fn get_follow_suggestions(&self) -> Result<Vec<Account>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// DELETE /api/v1/suggestions/:account_id
|
|
||||||
fn delete_from_suggestions(&self, id: &str) -> Result<Empty> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/endorsements
|
|
||||||
fn get_endorsements(&self) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/accounts/:id/pin
|
|
||||||
fn endorse_user(&self, id: &str) -> Result<Relationship> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// POST /api/v1/accounts/:id/unpin
|
|
||||||
fn unendorse_user(&self, id: &str) -> Result<Relationship> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// Shortcut for: `let me = client.verify_credentials(); client.followers()`
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use elefren::prelude::*;
|
|
||||||
/// let data = Data::default();
|
|
||||||
/// let client = Mastodon::from(data);
|
|
||||||
/// let follows_me = client.follows_me().unwrap();
|
|
||||||
/// ```
|
|
||||||
fn follows_me(&self) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// Shortcut for
|
|
||||||
/// `let me = client.verify_credentials(); client.following(&me.id)`
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use elefren::prelude::*;
|
|
||||||
/// let data = Data::default();
|
|
||||||
/// let client = Mastodon::from(data);
|
|
||||||
/// let follows_me = client.followed_by_me().unwrap();
|
|
||||||
/// ```
|
|
||||||
fn followed_by_me(&self) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns events that are relevant to the authorized user, i.e. home
|
|
||||||
/// timeline and notifications
|
|
||||||
fn streaming_user(&self) -> Result<Self::Stream> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all public statuses
|
|
||||||
fn streaming_public(&self) -> Result<Self::Stream> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all local statuses
|
|
||||||
fn streaming_local(&self) -> Result<Self::Stream> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all public statuses for a particular hashtag
|
|
||||||
fn streaming_public_hashtag(&self, hashtag: &str) -> Result<Self::Stream> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all local statuses for a particular hashtag
|
|
||||||
fn streaming_local_hashtag(&self, hashtag: &str) -> Result<Self::Stream> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns statuses for a list
|
|
||||||
fn streaming_list(&self, list_id: &str) -> Result<Self::Stream> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all direct messages
|
|
||||||
fn streaming_direct(&self) -> Result<Self::Stream> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait that represents clients that can make unauthenticated calls to a
|
|
||||||
/// mastodon instance
|
|
||||||
#[allow(unused)]
|
|
||||||
pub trait MastodonUnauthenticated<H: HttpSend> {
|
|
||||||
/// GET /api/v1/statuses/:id
|
|
||||||
fn get_status(&self, id: &str) -> Result<Status> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/statuses/:id/context
|
|
||||||
fn get_context(&self, id: &str) -> Result<Context> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/statuses/:id/card
|
|
||||||
fn get_card(&self, id: &str) -> Result<Card> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/statuses/:id/reblogged_by
|
|
||||||
fn reblogged_by(&self, id: &str) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
/// GET /api/v1/statuses/:id/favourited_by
|
|
||||||
fn favourited_by(&self, id: &str) -> Result<Page<Account, H>> {
|
|
||||||
unimplemented!("This method was not implemented");
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue