forked from MightyPork/elefren-fork
convert all to sync, remove now broken doc comments, some renames
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ use std::borrow::Cow;
|
||||
/// Raw data about mastodon app. Save `Data` using `serde` to prevent needing
|
||||
/// to authenticate on every run.
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Data {
|
||||
pub struct ClientData {
|
||||
/// Base url of instance eg. `https://mastodon.social`.
|
||||
pub base: Cow<'static, str>,
|
||||
/// The client's id given by the instance.
|
||||
|
||||
@@ -2,27 +2,6 @@ use crate::page::Page;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Abstracts away the `next_page` logic into a single stream of items
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # use elefren::prelude::*;
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// # let data = Data {
|
||||
/// # base: "".into(),
|
||||
/// # client_id: "".into(),
|
||||
/// # client_secret: "".into(),
|
||||
/// # redirect: "".into(),
|
||||
/// # token: "".into(),
|
||||
/// # };
|
||||
/// let client = Mastodon::from(data);
|
||||
/// let statuses = client.statuses("user-id", None)?;
|
||||
/// for status in statuses.items_iter() {
|
||||
/// // do something with `status`
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ItemsIter<'a, T: Clone + for<'de> Deserialize<'de>> {
|
||||
page: Page<'a, T>,
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ pub struct Empty {}
|
||||
/// structs by adding a glob import to the top of mastodon heavy
|
||||
/// modules:
|
||||
pub mod prelude {
|
||||
pub use super::{
|
||||
pub use crate::entities::{
|
||||
account::{Account, Source},
|
||||
attachment::{Attachment, MediaType},
|
||||
card::Card,
|
||||
|
||||
+34
-196
@@ -1,12 +1,8 @@
|
||||
use serde::Deserialize;
|
||||
use std::{error, fmt, io::Error as IoError};
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
use ::toml::de::Error as TomlDeError;
|
||||
#[cfg(feature = "toml")]
|
||||
use ::toml::ser::Error as TomlSerError;
|
||||
#[cfg(feature = "env")]
|
||||
use envy::Error as EnvyError;
|
||||
use hyper_old_types::Error as HeaderParseError;
|
||||
use reqwest::{header::ToStrError as HeaderStrError, Error as HttpError, StatusCode};
|
||||
use serde_json::Error as SerdeError;
|
||||
@@ -19,91 +15,54 @@ use url::ParseError as UrlError;
|
||||
pub type Result<T> = ::std::result::Result<T, Error>;
|
||||
|
||||
/// enum of possible errors encountered using the mastodon API.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Error from the Mastodon API. This typically means something went
|
||||
/// wrong with your authentication or data.
|
||||
Api(ApiError),
|
||||
#[error(transparent)]
|
||||
Api(#[from] ApiError),
|
||||
/// Error deserialising to json. Typically represents a breaking change in
|
||||
/// the Mastodon API
|
||||
Serde(SerdeError),
|
||||
/// Error serializing to url-encoded string
|
||||
UrlEncoded(UrlEncodedError),
|
||||
/// Error encountered in the HTTP backend while requesting a route.
|
||||
Http(HttpError),
|
||||
/// Wrapper around the `std::io::Error` struct.
|
||||
Io(IoError),
|
||||
/// Wrapper around the `url::ParseError` struct.
|
||||
Url(UrlError),
|
||||
/// Missing Client Id.
|
||||
#[error(transparent)]
|
||||
Serde(#[from] SerdeError),
|
||||
#[error(transparent)]
|
||||
UrlEncoded(#[from] UrlEncodedError),
|
||||
#[error(transparent)]
|
||||
Http(#[from] HttpError),
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error(transparent)]
|
||||
Url(#[from] UrlError),
|
||||
#[error("Missing Client Id.")]
|
||||
ClientIdRequired,
|
||||
/// Missing Client Secret.
|
||||
#[error("Missing Client Secret.")]
|
||||
ClientSecretRequired,
|
||||
/// Missing Access Token.
|
||||
#[error("Missing Access Token.")]
|
||||
AccessTokenRequired,
|
||||
/// Generic client error.
|
||||
#[error("Generic client error, code {0}")]
|
||||
Client(StatusCode),
|
||||
/// Generic server error.
|
||||
#[error("Generic server error, code {0}")]
|
||||
Server(StatusCode),
|
||||
/// MastodonBuilder & AppBuilder error
|
||||
#[error("Missing field {0}")]
|
||||
MissingField(&'static str),
|
||||
#[cfg(feature = "toml")]
|
||||
/// Error serializing to toml
|
||||
TomlSer(TomlSerError),
|
||||
#[cfg(feature = "toml")]
|
||||
/// Error deserializing from toml
|
||||
TomlDe(TomlDeError),
|
||||
/// Error converting an http header to a string
|
||||
HeaderStrError(HeaderStrError),
|
||||
/// Error parsing the http Link header
|
||||
HeaderParseError(HeaderParseError),
|
||||
// #[cfg(feature = "env")]
|
||||
// /// Error deserializing from the environment
|
||||
// Envy(EnvyError),
|
||||
/// Error serializing to a query string
|
||||
SerdeQs(SerdeQsError),
|
||||
/// WebSocket error
|
||||
// WebSocket(WebSocketError),
|
||||
// /// Other errors
|
||||
#[error(transparent)]
|
||||
TomlSer(#[from] TomlSerError),
|
||||
#[error(transparent)]
|
||||
TomlDe(#[from] TomlDeError),
|
||||
#[error(transparent)]
|
||||
HeaderStrError(#[from] HeaderStrError),
|
||||
#[error(transparent)]
|
||||
HeaderParseError(#[from] HeaderParseError),
|
||||
#[error(transparent)]
|
||||
SerdeQs(#[from] SerdeQsError),
|
||||
#[error(transparent)]
|
||||
WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
|
||||
#[error("Error in streaming API data format")]
|
||||
StreamingFormat,
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
Some(match *self {
|
||||
Error::Api(ref e) => e,
|
||||
Error::Serde(ref e) => e,
|
||||
Error::UrlEncoded(ref e) => e,
|
||||
Error::Http(ref e) => e,
|
||||
Error::Io(ref e) => e,
|
||||
Error::Url(ref e) => e,
|
||||
#[cfg(feature = "toml")]
|
||||
Error::TomlSer(ref e) => e,
|
||||
#[cfg(feature = "toml")]
|
||||
Error::TomlDe(ref e) => e,
|
||||
Error::HeaderStrError(ref e) => e,
|
||||
Error::HeaderParseError(ref e) => e,
|
||||
#[cfg(feature = "env")]
|
||||
Error::Envy(ref e) => e,
|
||||
Error::SerdeQs(ref e) => e,
|
||||
// Error::WebSocket(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::Other(..) => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned from the Mastodon API.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ApiError {
|
||||
@@ -120,124 +79,3 @@ impl fmt::Display for ApiError {
|
||||
}
|
||||
|
||||
impl error::Error for ApiError {}
|
||||
|
||||
macro_rules! from {
|
||||
($($(#[$met:meta])* $typ:ident, $variant:ident,)*) => {
|
||||
$(
|
||||
$(#[$met])*
|
||||
impl From<$typ> for Error {
|
||||
fn from(from: $typ) -> Self {
|
||||
use crate::Error::*;
|
||||
$variant(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,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
/// Used to easily create errors from strings
|
||||
macro_rules! format_err {
|
||||
( $( $arg:tt )* ) => {
|
||||
{
|
||||
use elefren::Error;
|
||||
Error::Other(format!($($arg)*))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use reqwest;
|
||||
use serde_json;
|
||||
use serde_urlencoded;
|
||||
use std::io;
|
||||
|
||||
macro_rules! assert_is {
|
||||
($err:ident, $variant:pat) => {
|
||||
assert!(match $err {
|
||||
$variant => true,
|
||||
_ => false,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_http_error() {
|
||||
let err: HttpError = reqwest::blocking::get("not an actual URL").unwrap_err();
|
||||
let err: Error = Error::from(err);
|
||||
assert_is!(err, Error::Http(..));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_io_error() {
|
||||
let err: IoError = io::Error::new(io::ErrorKind::Other, "other error");
|
||||
let err: Error = Error::from(err);
|
||||
assert_is!(err, Error::Io(..));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_serde_error() {
|
||||
let err: SerdeError = serde_json::from_str::<()>("not valid json").unwrap_err();
|
||||
let err: Error = Error::from(err);
|
||||
assert_is!(err, Error::Serde(..));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_url_encoded_error() {
|
||||
let err: UrlEncodedError = serde_urlencoded::ser::Error::Custom("error".into());
|
||||
let err: Error = Error::from(err);
|
||||
assert_is!(err, Error::UrlEncoded(..));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_url_error() {
|
||||
let err: UrlError = UrlError::EmptyHost;
|
||||
let err: Error = Error::from(err);
|
||||
assert_is!(err, Error::Url(..));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_api_error() {
|
||||
let err: ApiError = ApiError {
|
||||
error: None,
|
||||
error_description: None,
|
||||
};
|
||||
let err: Error = Error::from(err);
|
||||
assert_is!(err, Error::Api(..));
|
||||
}
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
#[test]
|
||||
fn from_toml_ser_error() {
|
||||
let err: TomlSerError = TomlSerError::DateInvalid;
|
||||
let err: Error = Error::from(err);
|
||||
assert_is!(err, Error::TomlSer(..));
|
||||
}
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
#[test]
|
||||
fn from_toml_de_error() {
|
||||
let err: TomlDeError = ::toml::from_str::<()>("not valid toml").unwrap_err();
|
||||
let err: Error = Error::from(err);
|
||||
assert_is!(err, Error::TomlDe(..));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
use std::io::{self, BufRead, Write};
|
||||
|
||||
use crate::{errors::Result, registration::Registered, Mastodon};
|
||||
use crate::{errors::Result, registration::Registered, Client};
|
||||
|
||||
/// Finishes the authentication process for the given `Registered` object,
|
||||
/// using the command-line
|
||||
pub async fn authenticate(registration: Registered) -> Result<Mastodon> {
|
||||
pub async fn authenticate(registration: Registered) -> Result<Client> {
|
||||
let url = registration.authorize_url()?;
|
||||
|
||||
let stdout = io::stdout();
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
use crate::{data::Data, Result};
|
||||
|
||||
/// Attempts to deserialize a Data struct from the environment
|
||||
pub fn from_env() -> Result<Data> {
|
||||
Ok(envy::from_env()?)
|
||||
}
|
||||
|
||||
/// Attempts to deserialize a Data struct from the environment. All keys are
|
||||
/// prefixed with the given prefix
|
||||
pub fn from_env_prefixed(prefix: &str) -> Result<Data> {
|
||||
Ok(envy::prefixed(prefix).from_env()?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{
|
||||
env,
|
||||
ops::FnOnce,
|
||||
panic::{catch_unwind, UnwindSafe},
|
||||
};
|
||||
|
||||
fn withenv<F: FnOnce() -> R + UnwindSafe, R>(prefix: Option<&'static str>, test: F) -> R {
|
||||
env::set_var(makekey(prefix, "BASE"), "https://example.com");
|
||||
env::set_var(makekey(prefix, "CLIENT_ID"), "adbc01234");
|
||||
env::set_var(makekey(prefix, "CLIENT_SECRET"), "0987dcba");
|
||||
env::set_var(makekey(prefix, "REDIRECT"), "urn:ietf:wg:oauth:2.0:oob");
|
||||
env::set_var(makekey(prefix, "TOKEN"), "fedc5678");
|
||||
|
||||
let result = catch_unwind(test);
|
||||
|
||||
env::remove_var(makekey(prefix, "BASE"));
|
||||
env::remove_var(makekey(prefix, "CLIENT_ID"));
|
||||
env::remove_var(makekey(prefix, "CLIENT_SECRET"));
|
||||
env::remove_var(makekey(prefix, "REDIRECT"));
|
||||
env::remove_var(makekey(prefix, "TOKEN"));
|
||||
|
||||
fn makekey(prefix: Option<&'static str>, key: &str) -> String {
|
||||
if let Some(prefix) = prefix {
|
||||
format!("{}{}", prefix, key)
|
||||
} else {
|
||||
key.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
result.expect("failed")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_env_no_prefix() {
|
||||
let desered = withenv(None, || from_env()).expect("Couldn't deser");
|
||||
assert_eq!(
|
||||
desered,
|
||||
Data {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
redirect: "urn:ietf:wg:oauth:2.0:oob".into(),
|
||||
token: "fedc5678".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_env_prefixed() {
|
||||
let desered = withenv(Some("APP_"), || from_env_prefixed("APP_")).expect("Couldn't deser");
|
||||
assert_eq!(
|
||||
desered,
|
||||
Data {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
redirect: "urn:ietf:wg:oauth:2.0:oob".into(),
|
||||
token: "fedc5678".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+19
-19
@@ -4,46 +4,46 @@ use std::{
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::{data::Data, Result};
|
||||
use crate::{data::ClientData, Result};
|
||||
|
||||
/// Attempts to deserialize a Data struct from a string
|
||||
pub fn from_str(s: &str) -> Result<Data> {
|
||||
pub fn from_str(s: &str) -> Result<ClientData> {
|
||||
Ok(serde_json::from_str(s)?)
|
||||
}
|
||||
|
||||
/// Attempts to deserialize a Data struct from a slice of bytes
|
||||
pub fn from_slice(s: &[u8]) -> Result<Data> {
|
||||
pub fn from_slice(s: &[u8]) -> Result<ClientData> {
|
||||
Ok(serde_json::from_slice(s)?)
|
||||
}
|
||||
|
||||
/// Attempts to deserialize a Data struct from something that implements
|
||||
/// the std::io::Read trait
|
||||
pub fn from_reader<R: Read>(mut r: R) -> Result<Data> {
|
||||
pub fn from_reader<R: Read>(mut r: R) -> Result<ClientData> {
|
||||
let mut buffer = Vec::new();
|
||||
r.read_to_end(&mut buffer)?;
|
||||
from_slice(&buffer)
|
||||
}
|
||||
|
||||
/// Attempts to deserialize a Data struct from a file
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Data> {
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<ClientData> {
|
||||
let path = path.as_ref();
|
||||
let file = File::open(path)?;
|
||||
Ok(from_reader(file)?)
|
||||
}
|
||||
|
||||
/// Attempts to serialize a Data struct to a String
|
||||
pub fn to_string(data: &Data) -> Result<String> {
|
||||
pub fn to_string(data: &ClientData) -> Result<String> {
|
||||
Ok(serde_json::to_string_pretty(data)?)
|
||||
}
|
||||
|
||||
/// Attempts to serialize a Data struct to a Vec of bytes
|
||||
pub fn to_vec(data: &Data) -> Result<Vec<u8>> {
|
||||
pub fn to_vec(data: &ClientData) -> Result<Vec<u8>> {
|
||||
Ok(serde_json::to_vec(data)?)
|
||||
}
|
||||
|
||||
/// Attempts to serialize a Data struct to something that implements the
|
||||
/// std::io::Write trait
|
||||
pub fn to_writer<W: Write>(data: &Data, writer: W) -> Result<()> {
|
||||
pub fn to_writer<W: Write>(data: &ClientData, writer: W) -> Result<()> {
|
||||
let mut buf_writer = BufWriter::new(writer);
|
||||
let vec = to_vec(data)?;
|
||||
buf_writer.write_all(&vec)?;
|
||||
@@ -55,7 +55,7 @@ pub fn to_writer<W: Write>(data: &Data, writer: W) -> Result<()> {
|
||||
/// When opening the file, this will set the `.write(true)` and
|
||||
/// `.truncate(true)` options, use the next method for more
|
||||
/// fine-grained control
|
||||
pub fn to_file<P: AsRef<Path>>(data: &Data, path: P) -> Result<()> {
|
||||
pub fn to_file<P: AsRef<Path>>(data: &ClientData, path: P) -> Result<()> {
|
||||
let mut options = OpenOptions::new();
|
||||
options.create(true).write(true).truncate(true);
|
||||
to_file_with_options(data, path, options)?;
|
||||
@@ -63,7 +63,7 @@ pub fn to_file<P: AsRef<Path>>(data: &Data, path: P) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Attempts to serialize a Data struct to a file
|
||||
pub fn to_file_with_options<P: AsRef<Path>>(data: &Data, path: P, options: OpenOptions) -> Result<()> {
|
||||
pub fn to_file_with_options<P: AsRef<Path>>(data: &ClientData, path: P, options: OpenOptions) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
let file = options.open(path)?;
|
||||
to_writer(data, file)?;
|
||||
@@ -93,7 +93,7 @@ mod tests {
|
||||
let desered = from_str(DOC).expect("Couldn't deserialize Data");
|
||||
assert_eq!(
|
||||
desered,
|
||||
Data {
|
||||
ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -108,7 +108,7 @@ mod tests {
|
||||
let desered = from_slice(&doc).expect("Couldn't deserialize Data");
|
||||
assert_eq!(
|
||||
desered,
|
||||
Data {
|
||||
ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -124,7 +124,7 @@ mod tests {
|
||||
let desered = from_reader(doc).expect("Couldn't deserialize Data");
|
||||
assert_eq!(
|
||||
desered,
|
||||
Data {
|
||||
ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -140,7 +140,7 @@ mod tests {
|
||||
let desered = from_file(datafile.path()).expect("Couldn't deserialize Data");
|
||||
assert_eq!(
|
||||
desered,
|
||||
Data {
|
||||
ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -151,7 +151,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -164,7 +164,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_to_vec() {
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -177,7 +177,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_to_writer() {
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -192,7 +192,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_to_file() {
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -207,7 +207,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_to_file_with_options() {
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
|
||||
+23
-14
@@ -1,4 +1,3 @@
|
||||
#[cfg(feature = "toml")]
|
||||
/// Helpers for serializing to/deserializing from toml
|
||||
///
|
||||
/// In order to use this module, set the "toml" feature in your Cargo.toml:
|
||||
@@ -10,7 +9,6 @@
|
||||
/// ```
|
||||
pub mod toml;
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
/// Helpers for serializing to/deserializing from json
|
||||
///
|
||||
/// In order to use this module, set the "json" feature in your Cargo.toml:
|
||||
@@ -22,17 +20,28 @@ pub mod toml;
|
||||
/// ```
|
||||
pub mod json;
|
||||
|
||||
#[cfg(feature = "env")]
|
||||
/// Helpers for deserializing a `Data` struct from the environment
|
||||
///
|
||||
/// In order to use this module, set the "env" feature in your Cargo.toml:
|
||||
///
|
||||
/// ```toml,ignore
|
||||
/// [dependencies.elefren]
|
||||
/// version = "0.22"
|
||||
/// features = ["env"]
|
||||
/// ```
|
||||
pub mod env;
|
||||
|
||||
/// Helpers for working with the command line
|
||||
pub mod cli;
|
||||
|
||||
|
||||
/// # Low-level API for extending
|
||||
// Convert the HTTP response body from JSON. Pass up deserialization errors
|
||||
// transparently.
|
||||
pub async fn deserialise_response<T: for<'de> serde::Deserialize<'de>>(response: reqwest::Response) -> crate::Result<T> {
|
||||
let bytes = response.bytes().await?;
|
||||
match serde_json::from_slice(&bytes) {
|
||||
Ok(t) => {
|
||||
debug!("{}", String::from_utf8_lossy(&bytes));
|
||||
Ok(t)
|
||||
}
|
||||
// If deserializing into the desired type fails try again to
|
||||
// see if this is an error response.
|
||||
Err(e) => {
|
||||
error!("{}", String::from_utf8_lossy(&bytes));
|
||||
if let Ok(error) = serde_json::from_slice(&bytes) {
|
||||
return Err(crate::Error::Api(error));
|
||||
}
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-25
@@ -1,49 +1,47 @@
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{BufWriter, Read, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::{data::Data, Result};
|
||||
use crate::data::ClientData;
|
||||
use crate::Result;
|
||||
use std::io::{Read, Write, BufWriter};
|
||||
use std::path::Path;
|
||||
use std::fs::{File, OpenOptions};
|
||||
|
||||
/// Attempts to deserialize a Data struct from a string
|
||||
pub fn from_str(s: &str) -> Result<Data> {
|
||||
pub fn from_str(s: &str) -> Result<ClientData> {
|
||||
Ok(toml::from_str(s)?)
|
||||
}
|
||||
|
||||
/// Attempts to deserialize a Data struct from a slice of bytes
|
||||
pub fn from_slice(s: &[u8]) -> Result<Data> {
|
||||
pub fn from_slice(s: &[u8]) -> Result<ClientData> {
|
||||
Ok(toml::from_slice(s)?)
|
||||
}
|
||||
|
||||
/// Attempts to deserialize a Data struct from something that implements
|
||||
/// the std::io::Read trait
|
||||
pub fn from_reader<R: Read>(mut r: R) -> Result<Data> {
|
||||
pub fn from_reader<R: Read>(mut r: R) -> Result<ClientData> {
|
||||
let mut buffer = Vec::new();
|
||||
r.read_to_end(&mut buffer)?;
|
||||
from_slice(&buffer)
|
||||
}
|
||||
|
||||
/// Attempts to deserialize a Data struct from a file
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Data> {
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<ClientData> {
|
||||
let path = path.as_ref();
|
||||
let file = File::open(path)?;
|
||||
Ok(from_reader(file)?)
|
||||
}
|
||||
|
||||
/// Attempts to serialize a Data struct to a String
|
||||
pub fn to_string(data: &Data) -> Result<String> {
|
||||
pub fn to_string(data: &ClientData) -> Result<String> {
|
||||
Ok(toml::to_string_pretty(data)?)
|
||||
}
|
||||
|
||||
/// Attempts to serialize a Data struct to a Vec of bytes
|
||||
pub fn to_vec(data: &Data) -> Result<Vec<u8>> {
|
||||
pub fn to_vec(data: &ClientData) -> Result<Vec<u8>> {
|
||||
Ok(toml::to_vec(data)?)
|
||||
}
|
||||
|
||||
/// Attempts to serialize a Data struct to something that implements the
|
||||
/// std::io::Write trait
|
||||
pub fn to_writer<W: Write>(data: &Data, writer: W) -> Result<()> {
|
||||
pub fn to_writer<W: Write>(data: &ClientData, writer: W) -> Result<()> {
|
||||
let mut buf_writer = BufWriter::new(writer);
|
||||
let vec = to_vec(data)?;
|
||||
buf_writer.write_all(&vec)?;
|
||||
@@ -55,7 +53,7 @@ pub fn to_writer<W: Write>(data: &Data, writer: W) -> Result<()> {
|
||||
/// When opening the file, this will set the `.write(true)` and
|
||||
/// `.truncate(true)` options, use the next method for more
|
||||
/// fine-grained control
|
||||
pub fn to_file<P: AsRef<Path>>(data: &Data, path: P) -> Result<()> {
|
||||
pub fn to_file<P: AsRef<Path>>(data: &ClientData, path: P) -> Result<()> {
|
||||
let mut options = OpenOptions::new();
|
||||
options.create(true).write(true).truncate(true);
|
||||
to_file_with_options(data, path, options)?;
|
||||
@@ -63,7 +61,7 @@ pub fn to_file<P: AsRef<Path>>(data: &Data, path: P) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Attempts to serialize a Data struct to a file
|
||||
pub fn to_file_with_options<P: AsRef<Path>>(data: &Data, path: P, options: OpenOptions) -> Result<()> {
|
||||
pub fn to_file_with_options<P: AsRef<Path>>(data: &ClientData, path: P, options: OpenOptions) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
let file = options.open(path)?;
|
||||
to_writer(data, file)?;
|
||||
@@ -91,7 +89,7 @@ mod tests {
|
||||
let desered = from_str(DOC).expect("Couldn't deserialize Data");
|
||||
assert_eq!(
|
||||
desered,
|
||||
Data {
|
||||
ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -106,7 +104,7 @@ mod tests {
|
||||
let desered = from_slice(&doc).expect("Couldn't deserialize Data");
|
||||
assert_eq!(
|
||||
desered,
|
||||
Data {
|
||||
ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -122,7 +120,7 @@ mod tests {
|
||||
let desered = from_reader(doc).expect("Couldn't deserialize Data");
|
||||
assert_eq!(
|
||||
desered,
|
||||
Data {
|
||||
ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -138,7 +136,7 @@ mod tests {
|
||||
let desered = from_file(datafile.path()).expect("Couldn't deserialize Data");
|
||||
assert_eq!(
|
||||
desered,
|
||||
Data {
|
||||
ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -149,7 +147,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -162,7 +160,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_to_vec() {
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -175,7 +173,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_to_writer() {
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -190,7 +188,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_to_file() {
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
@@ -205,7 +203,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_to_file_with_options() {
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: "https://example.com".into(),
|
||||
client_id: "adbc01234".into(),
|
||||
client_secret: "0987dcba".into(),
|
||||
|
||||
+100
-498
@@ -2,97 +2,43 @@
|
||||
//!
|
||||
//! Most of the api is documented on [Mastodon's website](https://docs.joinmastodon.org/client/intro/)
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # extern crate elefren;
|
||||
//! # fn main() {
|
||||
//! # run().unwrap();
|
||||
//! # }
|
||||
//! # fn run() -> elefren::Result<()> {
|
||||
//! use elefren::{helpers::cli, prelude::*};
|
||||
//!
|
||||
//! let registration = Registration::new("https://mastodon.social")
|
||||
//! .client_name("elefren_test")
|
||||
//! .build()?;
|
||||
//! let mastodon = cli::authenticate(registration)?;
|
||||
//!
|
||||
//! println!(
|
||||
//! "{:?}",
|
||||
//! mastodon
|
||||
//! .get_home_timeline()?
|
||||
//! .items_iter()
|
||||
//! .take(100)
|
||||
//! .collect::<Vec<_>>()
|
||||
//! );
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Elefren also supports Mastodon's Streaming API:
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # extern crate elefren;
|
||||
//! # use elefren::prelude::*;
|
||||
//! # use std::error::Error;
|
||||
//! use elefren::entities::event::Event;
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! # let data = Data {
|
||||
//! # base: "".into(),
|
||||
//! # client_id: "".into(),
|
||||
//! # client_secret: "".into(),
|
||||
//! # redirect: "".into(),
|
||||
//! # token: "".into(),
|
||||
//! # };
|
||||
//! let client = Mastodon::from(data);
|
||||
//! for event in client.streaming_user()? {
|
||||
//! match event {
|
||||
//! Event::Update(ref status) => { /* .. */ }
|
||||
//! Event::Notification(ref notification) => { /* .. */ }
|
||||
//! Event::Delete(ref id) => { /* .. */ }
|
||||
//! Event::FiltersChanged => { /* .. */ }
|
||||
//! }
|
||||
//! }
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
// #![deny(
|
||||
// missing_docs,
|
||||
// warnings,
|
||||
// missing_debug_implementations,
|
||||
// missing_copy_implementations,
|
||||
// trivial_casts,
|
||||
// trivial_numeric_casts,
|
||||
// unsafe_code,
|
||||
// unstable_features,
|
||||
// unused_import_braces,
|
||||
// unused_qualifications
|
||||
// )]
|
||||
// #![cfg_attr(feature = "nightly", allow(broken_intra_doc_links))]
|
||||
// #![allow(broken_intra_doc_links)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate thiserror;
|
||||
|
||||
use std::{borrow::Cow, ops};
|
||||
use std::borrow::Cow;
|
||||
use std::ops;
|
||||
|
||||
pub use isolang::Language;
|
||||
use reqwest::{Client, multipart, RequestBuilder, Response, Body};
|
||||
// use tap_reader::Tap;
|
||||
// use tungstenite::stream::MaybeTlsStream;
|
||||
use reqwest::multipart;
|
||||
|
||||
use entities::prelude::*;
|
||||
pub use errors::{Error, Result};
|
||||
use helpers::deserialise_response;
|
||||
use requests::{
|
||||
AddFilterRequest,
|
||||
AddPushRequest,
|
||||
StatusesRequest,
|
||||
UpdateCredsRequest,
|
||||
UpdatePushRequest,
|
||||
};
|
||||
use streaming::EventReader;
|
||||
pub use streaming::StreamKind;
|
||||
|
||||
use crate::{entities::prelude::*, page::Page};
|
||||
pub use crate::{
|
||||
data::Data,
|
||||
errors::{ApiError, Error, Result},
|
||||
// mastodon_client::{MastodonClient, MastodonUnauthenticated},
|
||||
data::ClientData,
|
||||
media_builder::MediaBuilder,
|
||||
page::Page,
|
||||
registration::Registration,
|
||||
requests::{AddFilterRequest, AddPushRequest, StatusesRequest, UpdateCredsRequest, UpdatePushRequest},
|
||||
status_builder::{NewStatus, StatusBuilder},
|
||||
scopes::Scopes,
|
||||
status_builder::NewStatus,
|
||||
status_builder::StatusBuilder,
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
/// Registering your App
|
||||
pub mod apps;
|
||||
/// Contains the struct that holds the client auth data
|
||||
@@ -115,51 +61,59 @@ pub mod requests;
|
||||
pub mod scopes;
|
||||
/// Constructing a status
|
||||
pub mod status_builder;
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
/// Automatically import the things you need
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
Data, Mastodon, NewStatus, Registration, scopes::Scopes, StatusBuilder, StatusesRequest,
|
||||
};
|
||||
}
|
||||
|
||||
// type Stream = EventReader<WebSocket>;
|
||||
/// Client that doesn't need auth
|
||||
pub mod unauth;
|
||||
/// Streaming API
|
||||
pub mod streaming;
|
||||
|
||||
/// Your mastodon application client, handles all requests to and from Mastodon.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Mastodon {
|
||||
client: Client,
|
||||
pub struct Client {
|
||||
http_client: reqwest::Client,
|
||||
/// Raw data about your mastodon instance.
|
||||
pub data: Data,
|
||||
pub data: ClientData,
|
||||
}
|
||||
|
||||
impl From<Data> for Mastodon {
|
||||
impl From<ClientData> for Client {
|
||||
/// Creates a mastodon instance from the data struct.
|
||||
fn from(data: Data) -> Mastodon {
|
||||
let mut builder = MastodonBuilder::new();
|
||||
fn from(data: ClientData) -> Client {
|
||||
let mut builder = ClientBuilder::new();
|
||||
builder.data(data);
|
||||
builder.build().expect("We know `data` is present, so this should be fine")
|
||||
}
|
||||
}
|
||||
|
||||
// type MastodonStream = EventReader<WebSocket>;
|
||||
impl Client {
|
||||
methods![get, put, post, delete,];
|
||||
|
||||
impl Mastodon {
|
||||
methods![get, post, delete,];
|
||||
|
||||
fn route(&self, url: &str) -> String {
|
||||
pub fn route(&self, url: &str) -> String {
|
||||
format!("{}{}", self.base, url)
|
||||
}
|
||||
|
||||
pub(crate) async fn send(&self, req: RequestBuilder) -> Result<Response> {
|
||||
/// # Low-level API for extending
|
||||
/// Send a request and get response
|
||||
pub async fn send(&self, req: reqwest::RequestBuilder) -> Result<reqwest::Response> {
|
||||
let request = req.bearer_auth(&self.token).build()?;
|
||||
Ok(self.client.execute(request).await?)
|
||||
Ok(self.http_client.execute(request).await?)
|
||||
}
|
||||
|
||||
/// Open streaming API of the given kind
|
||||
pub async fn open_streaming_api<'k>(&self, kind: StreamKind<'k>) -> Result<EventReader> {
|
||||
let mut url: url::Url = self.route(&format!("/api/v1/streaming/{}", kind.get_url_fragment())).parse()?;
|
||||
|
||||
{
|
||||
let mut qpm = url.query_pairs_mut();
|
||||
qpm.append_pair("access_token", &self.token);
|
||||
//qpm.append_pair("stream", "user");
|
||||
|
||||
for (k, v) in kind.get_query_params() {
|
||||
qpm.append_pair(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
streaming::do_open_streaming(url.as_str()).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Mastodon {
|
||||
route_v1_paged!((get) favourites: "favourites" => Status);
|
||||
route_v1_paged!((get) blocks: "blocks" => Account);
|
||||
route_v1_paged!((get) domain_blocks: "domain_blocks" => String);
|
||||
@@ -222,7 +176,7 @@ impl Mastodon {
|
||||
/// POST /api/v1/filters
|
||||
pub async 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)).await?;
|
||||
let response = self.send(self.http_client.post(&url).json(&request)).await?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
@@ -238,7 +192,7 @@ impl Mastodon {
|
||||
/// 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.send(self.client.put(&url).json(&request)).await?;
|
||||
let response = self.send(self.http_client.put(&url).json(&request)).await?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
@@ -255,7 +209,7 @@ impl Mastodon {
|
||||
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.send(self.client.patch(&url).json(&changes)).await?;
|
||||
let response = self.send(self.http_client.patch(&url).json(&changes)).await?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
@@ -270,7 +224,7 @@ impl Mastodon {
|
||||
|
||||
/// Post a new status to the account.
|
||||
pub async fn new_status(&self, status: NewStatus) -> Result<Status> {
|
||||
let response = self.send(self.client.post(&self.route("/api/v1/statuses")).json(&status)).await?;
|
||||
let response = self.send(self.http_client.post(&self.route("/api/v1/statuses")).json(&status)).await?;
|
||||
|
||||
deserialise_response(response).await
|
||||
}
|
||||
@@ -285,7 +239,7 @@ impl Mastodon {
|
||||
self.route(&format!("{}{}", base, hashtag))
|
||||
};
|
||||
|
||||
Page::new(self, self.send(self.client.get(&url)).await?).await
|
||||
Page::new(self, self.send(self.http_client.get(&url)).await?).await
|
||||
}
|
||||
|
||||
/// Get statuses of a single account by id. Optionally only with pictures
|
||||
@@ -300,7 +254,7 @@ impl Mastodon {
|
||||
url = format!("{}{}", url, request.to_querystring()?);
|
||||
}
|
||||
|
||||
let response = self.send(self.client.get(&url)).await?;
|
||||
let response = self.send(self.http_client.get(&url)).await?;
|
||||
|
||||
Page::new(self, response).await
|
||||
}
|
||||
@@ -322,7 +276,7 @@ impl Mastodon {
|
||||
url.pop();
|
||||
}
|
||||
|
||||
let response = self.send(self.client.get(&url)).await?;
|
||||
let response = self.send(self.http_client.get(&url)).await?;
|
||||
|
||||
Page::new(self, response).await
|
||||
}
|
||||
@@ -330,7 +284,7 @@ impl Mastodon {
|
||||
/// Add a push notifications subscription
|
||||
pub async 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)).await?;
|
||||
let response = self.send(self.http_client.post(&self.route("/api/v1/push/subscription")).json(&request)).await?;
|
||||
|
||||
deserialise_response(response).await
|
||||
}
|
||||
@@ -339,7 +293,7 @@ impl Mastodon {
|
||||
/// access token
|
||||
pub async 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)).await?;
|
||||
let response = self.send(self.http_client.put(&self.route("/api/v1/push/subscription")).json(&request)).await?;
|
||||
|
||||
deserialise_response(response).await
|
||||
}
|
||||
@@ -356,200 +310,55 @@ impl Mastodon {
|
||||
self.following(&me.id).await
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
/// returns events that are relevant to the authorized user, i.e. home
|
||||
/// timeline & notifications
|
||||
pub fn streaming_user(&self) -> Result<MastodonStream> {
|
||||
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)))
|
||||
pub async fn streaming_user(&self) -> Result<EventReader> {
|
||||
self.open_streaming_api(StreamKind::User).await
|
||||
}
|
||||
|
||||
/// returns all public statuses
|
||||
pub fn streaming_public(&self) -> Result<MastodonStream> {
|
||||
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::blocking::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)))
|
||||
pub async fn streaming_public(&self) -> Result<EventReader> {
|
||||
self.open_streaming_api(StreamKind::Public).await
|
||||
}
|
||||
|
||||
/// Returns all local statuses
|
||||
pub fn streaming_local(&self) -> Result<MastodonStream> {
|
||||
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::blocking::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)))
|
||||
pub async fn streaming_local(&self) -> Result<EventReader> {
|
||||
self.open_streaming_api(StreamKind::PublicLocal).await
|
||||
}
|
||||
|
||||
/// Returns all public statuses for a particular hashtag
|
||||
pub fn streaming_public_hashtag(&self, hashtag: &str) -> Result<MastodonStream> {
|
||||
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::blocking::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)))
|
||||
pub async fn streaming_public_hashtag(&self, hashtag: &str) -> Result<EventReader> {
|
||||
self.open_streaming_api(StreamKind::Hashtag(hashtag)).await
|
||||
}
|
||||
|
||||
/// Returns all local statuses for a particular hashtag
|
||||
pub fn streaming_local_hashtag(&self, hashtag: &str) -> Result<MastodonStream> {
|
||||
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::blocking::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)))
|
||||
pub async fn streaming_local_hashtag(&self, hashtag: &str) -> Result<EventReader> {
|
||||
self.open_streaming_api(StreamKind::HashtagLocal(hashtag)).await
|
||||
}
|
||||
|
||||
/// Returns statuses for a list
|
||||
pub fn streaming_list(&self, list_id: &str) -> Result<MastodonStream> {
|
||||
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::blocking::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)))
|
||||
pub async fn streaming_list(&self, list_id: &str) -> Result<EventReader> {
|
||||
self.open_streaming_api(StreamKind::List(list_id)).await
|
||||
}
|
||||
|
||||
/// Returns all direct messages
|
||||
pub fn streaming_direct(&self) -> Result<MastodonStream> {
|
||||
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::blocking::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)))
|
||||
pub async fn streaming_direct(&self) -> Result<EventReader> {
|
||||
self.open_streaming_api(StreamKind::Direct).await
|
||||
}
|
||||
*/
|
||||
|
||||
/// Upload some media to the server for possible attaching to a new status
|
||||
///
|
||||
/// Upon successful upload of a media attachment, the server will assign it an id. To actually
|
||||
/// use the attachment in a new status, you can use the `media_ids` field of
|
||||
/// [`StatusBuilder`]
|
||||
///
|
||||
/// There are two ways of providing the data to be attached: by reading a file, or by using a
|
||||
/// reader.
|
||||
///
|
||||
/// ## Files
|
||||
/// If the `MediaBuilder` was supplied with a file path, the filename and mimetype will be
|
||||
/// automatically populated from that file; their values set in the `MediaBuilder` will be
|
||||
/// ignored. For example:
|
||||
///
|
||||
/// ```no_run
|
||||
/// let client = Mastodon::from(data);
|
||||
/// let builder = crate::MediaBuilder::from_file("/tmp/my_image.png".into());
|
||||
///
|
||||
/// let attachment = client.media(builder);
|
||||
/// ```
|
||||
///
|
||||
/// ## Readers
|
||||
/// The `MediaBuilder` can also be supplied with a reader. This is useful for uploading data
|
||||
/// already in memory, for example from a `Vec<u8>` containing some image data. For example:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::io::Cursor;
|
||||
/// let client = Mastodon::from(data);
|
||||
///
|
||||
/// let mut image_data: Vec<u8> = Vec::new();
|
||||
/// populate_image_data(&mut image_data);
|
||||
///
|
||||
/// let builder = crate::MediaBuilder::from_reader(Cursor::new(image_data));
|
||||
/// let attachment = client.media(builder);
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ## Errors
|
||||
/// This function may return an `Error::Http` before sending anything over the network if the
|
||||
/// `MediaBuilder` was supplied with a reader and a `mimetype` string which cannot be pasrsed.
|
||||
pub async fn media(&self, media: MediaBuilder) -> Result<Attachment> {
|
||||
use media_builder::MediaBuilderData;
|
||||
|
||||
let mut form = multipart::Form::new();
|
||||
form = match media.data {
|
||||
MediaBuilderData::Reader(reader) => {
|
||||
let mut part = multipart::Part::stream(Body::wrap_stream(tokio_util::io::ReaderStream::new(reader)));
|
||||
let mut part = multipart::Part::stream(reqwest::Body::wrap_stream(tokio_util::io::ReaderStream::new(reader)));
|
||||
if let Some(filename) = media.filename {
|
||||
part = part.file_name(filename);
|
||||
}
|
||||
@@ -562,10 +371,9 @@ impl Mastodon {
|
||||
}
|
||||
MediaBuilderData::File(file) => {
|
||||
let f = tokio::fs::OpenOptions::new().read(true).open(file).await?;
|
||||
let part = multipart::Part::stream(Body::wrap_stream(tokio_util::io::ReaderStream::new(f)));
|
||||
let part = multipart::Part::stream(reqwest::Body::wrap_stream(tokio_util::io::ReaderStream::new(f)));
|
||||
form.part("file", part)
|
||||
// form.file("file", &file)?
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(description) = media.description {
|
||||
@@ -576,152 +384,47 @@ impl Mastodon {
|
||||
form = form.text("focus", format!("{},{}", x, y));
|
||||
}
|
||||
|
||||
let response = self.send(self.client.post(&self.route("/api/v1/media")).multipart(form)).await?;
|
||||
let response = self.send(self.http_client.post(&self.route("/api/v1/media")).multipart(form)).await?;
|
||||
|
||||
deserialise_response(response).await
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
#[derive(Debug)]
|
||||
/// WebSocket newtype so that EventStream can be implemented without coherency
|
||||
/// issues
|
||||
pub struct WebSocket(tungstenite::protocol::WebSocket<MaybeTlsStream<TcpStream>>);
|
||||
|
||||
/// 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() {
|
||||
debug!("WS rx: {}", line);
|
||||
let line = line.trim().to_string();
|
||||
if line.starts_with(':') || line.is_empty() {
|
||||
debug!("discard as comment");
|
||||
continue;
|
||||
}
|
||||
lines.push(line);
|
||||
if let Ok(event) = self.make_event(&lines) {
|
||||
debug!("Parsed event");
|
||||
lines.clear();
|
||||
return Some(event);
|
||||
} else {
|
||||
debug!("Failed to parse");
|
||||
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:")) {
|
||||
debug!("plaintext formatted 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 {
|
||||
debug!("JSON formatted event");
|
||||
use serde::Deserialize;
|
||||
#[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 ops::Deref for Mastodon {
|
||||
type Target = Data;
|
||||
impl ops::Deref for Client {
|
||||
type Target = ClientData;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
struct MastodonBuilder {
|
||||
client: Option<Client>,
|
||||
data: Option<Data>,
|
||||
struct ClientBuilder {
|
||||
http_client: Option<reqwest::Client>,
|
||||
data: Option<ClientData>,
|
||||
}
|
||||
|
||||
impl MastodonBuilder {
|
||||
impl ClientBuilder {
|
||||
pub fn new() -> Self {
|
||||
MastodonBuilder {
|
||||
client: None,
|
||||
ClientBuilder {
|
||||
http_client: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client(&mut self, client: Client) -> &mut Self {
|
||||
self.client = Some(client);
|
||||
pub fn http_client(&mut self, client: reqwest::Client) -> &mut Self {
|
||||
self.http_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn data(&mut self, data: Data) -> &mut Self {
|
||||
pub fn data(&mut self, data: ClientData) -> &mut Self {
|
||||
self.data = Some(data);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<Mastodon> {
|
||||
pub fn build(self) -> Result<Client> {
|
||||
Ok(if let Some(data) = self.data {
|
||||
Mastodon {
|
||||
client: self.client.unwrap_or_else(Client::new),
|
||||
Client {
|
||||
http_client: self.http_client.unwrap_or_else(reqwest::Client::new),
|
||||
data,
|
||||
}
|
||||
} else {
|
||||
@@ -730,104 +433,3 @@ impl MastodonBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Client that can make unauthenticated calls to a mastodon instance
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MastodonUnauth {
|
||||
client: Client,
|
||||
base: url::Url,
|
||||
}
|
||||
|
||||
impl MastodonUnauth {
|
||||
/// Create a new unauthenticated client
|
||||
pub fn new(base: &str) -> Result<MastodonUnauth> {
|
||||
let base = if base.starts_with("https://") {
|
||||
base.to_string()
|
||||
} else {
|
||||
format!("https://{}", base)
|
||||
};
|
||||
Ok(MastodonUnauth {
|
||||
client: Client::new(),
|
||||
base: url::Url::parse(&base)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MastodonUnauth {
|
||||
fn route(&self, url: &str) -> Result<url::Url> {
|
||||
Ok(self.base.join(url)?)
|
||||
}
|
||||
|
||||
async fn send(&self, req: RequestBuilder) -> Result<Response> {
|
||||
let req = req.build()?;
|
||||
Ok(self.client.execute(req).await?)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
/// Get a stream of the public timeline
|
||||
pub fn streaming_public(&self) -> Result<EventReader<WebSocket>> {
|
||||
let mut url: url::Url = self.route("/api/v1/streaming/public/local")?;
|
||||
url.query_pairs_mut().append_pair("stream", "public");
|
||||
let mut url: url::Url = reqwest::blocking::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)))
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/// 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)?;
|
||||
let response = self.send(self.client.get(route)).await?;
|
||||
deserialise_response(response).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")?;
|
||||
let response = self.send(self.client.get(route)).await?;
|
||||
deserialise_response(response).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")?;
|
||||
let response = self.send(self.client.get(route)).await?;
|
||||
deserialise_response(response).await
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the HTTP response body from JSON. Pass up deserialization errors
|
||||
// transparently.
|
||||
async fn deserialise_response<T: for<'de> serde::Deserialize<'de>>(response: Response) -> Result<T> {
|
||||
let bytes = response.bytes().await?;
|
||||
match serde_json::from_slice(&bytes) {
|
||||
Ok(t) => {
|
||||
debug!("{}", String::from_utf8_lossy(&bytes));
|
||||
Ok(t)
|
||||
}
|
||||
// If deserializing into the desired type fails try again to
|
||||
// see if this is an error response.
|
||||
Err(e) => {
|
||||
error!("{}", String::from_utf8_lossy(&bytes));
|
||||
if let Ok(error) = serde_json::from_slice(&bytes) {
|
||||
return Err(Error::Api(error));
|
||||
}
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,11 +1,11 @@
|
||||
macro_rules! methods {
|
||||
($($method:ident,)+) => {
|
||||
$(
|
||||
async fn $method<T: for<'de> serde::Deserialize<'de>>(&self, url: String)
|
||||
pub async fn $method<T: for<'de> serde::Deserialize<'de>>(&self, url: String)
|
||||
-> Result<T>
|
||||
{
|
||||
let response = self.send(
|
||||
self.client.$method(&url)
|
||||
self.http_client.$method(&url)
|
||||
).await?;
|
||||
|
||||
deserialise_response(response).await
|
||||
@@ -25,7 +25,7 @@ macro_rules! route_v1_paged {
|
||||
pub async fn $name<'a>(&'a self) -> Result<Page<'a, $ret>> {
|
||||
let url = self.route(concat!("/api/v1/", $url));
|
||||
let response = self.send(
|
||||
self.client.$method(&url)
|
||||
self.http_client.$method(&url)
|
||||
).await?;
|
||||
|
||||
Page::new(self, response).await
|
||||
@@ -68,7 +68,7 @@ macro_rules! route_v1_paged {
|
||||
let url = format!(concat!("/api/v1/", $url, "?{}"), &qs);
|
||||
|
||||
let response = self.send(
|
||||
self.client.get(&url)
|
||||
self.http_client.get(&url)
|
||||
).await?;
|
||||
|
||||
Page::new(self, response).await
|
||||
@@ -168,7 +168,7 @@ macro_rules! route_v1 {
|
||||
});
|
||||
|
||||
let response = self.send(
|
||||
self.client.$method(&self.route(concat!("/api/v1/", $url)))
|
||||
self.http_client.$method(&self.route(concat!("/api/v1/", $url)))
|
||||
.json(&form_data)
|
||||
).await?;
|
||||
|
||||
@@ -224,7 +224,7 @@ macro_rules! route_v1_paged_id {
|
||||
pub async fn $name<'a>(&'a self, id: &str) -> Result<Page<'a, $ret>> {
|
||||
let url = self.route(&format!(concat!("/api/v1/", $url), id));
|
||||
let response = self.send(
|
||||
self.client.$method(&url)
|
||||
self.http_client.$method(&url)
|
||||
).await?;
|
||||
|
||||
Page::new(self, response).await
|
||||
|
||||
@@ -101,42 +101,3 @@ impl MediaBuilder {
|
||||
self.focus = Some((f1, f2));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn test_from_reader() {
|
||||
let source = vec![0u8, 1, 2];
|
||||
let builder = MediaBuilder::from_reader(Cursor::new(source.clone()));
|
||||
|
||||
assert_eq!(builder.filename, None);
|
||||
assert_eq!(builder.mimetype, None);
|
||||
assert_eq!(builder.description, None);
|
||||
assert_eq!(builder.focus, None);
|
||||
|
||||
if let MediaBuilderData::Reader(r) = builder.data {
|
||||
assert_eq!(r.bytes().map(|b| b.unwrap()).collect::<Vec<u8>>(), source);
|
||||
} else {
|
||||
panic!("Unable to destructure MediaBuilder.data into a reader");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_file() {
|
||||
let builder = MediaBuilder::from_file("/fake/file/path.png");
|
||||
|
||||
assert_eq!(builder.filename, Some("path.png".to_string()));
|
||||
assert_eq!(builder.mimetype, Some("image/png".to_string()));
|
||||
assert_eq!(builder.description, None);
|
||||
assert_eq!(builder.focus, None);
|
||||
|
||||
if let MediaBuilderData::File(f) = builder.data {
|
||||
assert_eq!(f.display().to_string().as_str(), "/fake/file/path.png");
|
||||
} else {
|
||||
panic!("Unable to destructure MediaBuilder.data into a filepath");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-94
@@ -1,4 +1,4 @@
|
||||
use super::{deserialise_response, Mastodon, Result};
|
||||
use super::{deserialise_response, Client, Result};
|
||||
use crate::entities::itemsiter::ItemsIter;
|
||||
use hyper_old_types::header::{parsing, Link, RelationType};
|
||||
use reqwest::{Response, header::LINK};
|
||||
@@ -17,8 +17,8 @@ macro_rules! pages {
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let response = self.mastodon.send(
|
||||
self.mastodon.client.get(url)
|
||||
let response = self.api_client.send(
|
||||
self.api_client.http_client.get(url)
|
||||
).await?;
|
||||
|
||||
let (prev, next) = get_links(&response)?;
|
||||
@@ -33,40 +33,9 @@ macro_rules! pages {
|
||||
|
||||
/// Owned version of the `Page` struct in this module. Allows this to be more
|
||||
/// easily stored for later use
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # use elefren::Mastodon;
|
||||
/// # use elefren::page::OwnedPage;
|
||||
/// # use elefren::entities::status::Status;
|
||||
/// # use std::cell::RefCell;
|
||||
/// # use elefren::prelude::*;
|
||||
/// # fn main() -> Result<(), elefren::Error> {
|
||||
/// # let data = Data {
|
||||
/// # base: "".into(),
|
||||
/// # client_id: "".into(),
|
||||
/// # client_secret: "".into(),
|
||||
/// # redirect: "".into(),
|
||||
/// # token: "".into(),
|
||||
/// # };
|
||||
/// struct HomeTimeline {
|
||||
/// client: Mastodon,
|
||||
/// page: RefCell<Option<OwnedPage<Status>>>,
|
||||
/// }
|
||||
/// let client = Mastodon::from(data);
|
||||
/// let home = client.get_home_timeline()?.into_owned();
|
||||
/// let tl = HomeTimeline {
|
||||
/// client,
|
||||
/// page: RefCell::new(Some(home)),
|
||||
/// };
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OwnedPage<T: for<'de> Deserialize<'de>> {
|
||||
mastodon: Mastodon,
|
||||
api_client: Client,
|
||||
next: Option<Url>,
|
||||
prev: Option<Url>,
|
||||
/// Initial set of items
|
||||
@@ -83,7 +52,7 @@ impl<T: for<'de> Deserialize<'de>> OwnedPage<T> {
|
||||
impl<'a, T: for<'de> Deserialize<'de>> From<Page<'a, T>> for OwnedPage<T> {
|
||||
fn from(page: Page<'a, T>) -> OwnedPage<T> {
|
||||
OwnedPage {
|
||||
mastodon: page.mastodon.clone(),
|
||||
api_client: page.api_client.clone(),
|
||||
next: page.next,
|
||||
prev: page.prev,
|
||||
initial_items: page.initial_items,
|
||||
@@ -94,7 +63,7 @@ impl<'a, T: for<'de> Deserialize<'de>> From<Page<'a, T>> for OwnedPage<T> {
|
||||
/// Represents a single page of API results
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Page<'a, T: for<'de> Deserialize<'de>> {
|
||||
mastodon: &'a Mastodon,
|
||||
api_client: &'a Client,
|
||||
next: Option<Url>,
|
||||
prev: Option<Url>,
|
||||
/// Initial set of items
|
||||
@@ -107,13 +76,13 @@ impl<'a, T: for<'de> Deserialize<'de>> Page<'a, T> {
|
||||
prev: prev_page
|
||||
}
|
||||
|
||||
pub(crate) async fn new<'m>(mastodon: &'m Mastodon, response: Response) -> Result<Page<'m, T>> {
|
||||
pub(crate) async fn new<'m>(api_client: &'m Client, response: Response) -> Result<Page<'m, T>> {
|
||||
let (prev, next) = get_links(&response)?;
|
||||
Ok(Page {
|
||||
initial_items: deserialise_response(response).await?,
|
||||
next,
|
||||
prev,
|
||||
mastodon,
|
||||
api_client: api_client,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -121,37 +90,6 @@ impl<'a, T: for<'de> Deserialize<'de>> Page<'a, T> {
|
||||
impl<'a, T: Clone + for<'de> Deserialize<'de>> Page<'a, T> {
|
||||
/// Returns an owned version of this struct that doesn't borrow the client
|
||||
/// that created it
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # use elefren::Mastodon;
|
||||
/// # use elefren::page::OwnedPage;
|
||||
/// # use elefren::entities::status::Status;
|
||||
/// # use std::cell::RefCell;
|
||||
/// # use elefren::prelude::*;
|
||||
/// # fn main() -> Result<(), elefren::Error> {
|
||||
/// # let data = Data {
|
||||
/// # base: "".into(),
|
||||
/// # client_id: "".into(),
|
||||
/// # client_secret: "".into(),
|
||||
/// # redirect: "".into(),
|
||||
/// # token: "".into(),
|
||||
/// # };
|
||||
/// struct HomeTimeline {
|
||||
/// client: Mastodon,
|
||||
/// page: RefCell<Option<OwnedPage<Status>>>,
|
||||
/// }
|
||||
/// let client = Mastodon::from(data);
|
||||
/// let home = client.get_home_timeline()?.into_owned();
|
||||
/// let tl = HomeTimeline {
|
||||
/// client,
|
||||
/// page: RefCell::new(Some(home)),
|
||||
/// };
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn into_owned(self) -> OwnedPage<T> {
|
||||
OwnedPage::from(self)
|
||||
}
|
||||
@@ -165,30 +103,6 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>> Page<'a, T> {
|
||||
/// when necessary to get
|
||||
/// more of them, until
|
||||
/// there are no more items.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # use std::error::Error;
|
||||
/// use elefren::prelude::*;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// # let data = Data {
|
||||
/// # base: "".into(),
|
||||
/// # client_id: "".into(),
|
||||
/// # client_secret: "".into(),
|
||||
/// # redirect: "".into(),
|
||||
/// # token: "".into(),
|
||||
/// # };
|
||||
/// let mastodon = Mastodon::from(data);
|
||||
/// let req = StatusesRequest::new();
|
||||
/// let resp = mastodon.statuses("some-id", req)?;
|
||||
/// for status in resp.items_iter() {
|
||||
/// // do something with status
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn items_iter(self) -> ItemsIter<'a, T>
|
||||
where
|
||||
T: 'a,
|
||||
|
||||
+21
-129
@@ -1,14 +1,11 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use reqwest::{Client, RequestBuilder, Response};
|
||||
use serde::Deserialize;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::{
|
||||
apps::{App, AppBuilder},
|
||||
scopes::Scopes,
|
||||
Data, Error, Mastodon, MastodonBuilder, Result,
|
||||
};
|
||||
use crate::apps::{AppBuilder, App};
|
||||
use crate::scopes::Scopes;
|
||||
use crate::{Result, Error, Client, ClientBuilder};
|
||||
use crate::data::ClientData;
|
||||
|
||||
const DEFAULT_REDIRECT_URI: &str = "urn:ietf:wg:oauth:2.0:oob";
|
||||
|
||||
@@ -17,7 +14,7 @@ const DEFAULT_REDIRECT_URI: &str = "urn:ietf:wg:oauth:2.0:oob";
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Registration<'a> {
|
||||
base: String,
|
||||
client: Client,
|
||||
http_client: reqwest::Client,
|
||||
app_builder: AppBuilder<'a>,
|
||||
force_login: bool,
|
||||
}
|
||||
@@ -40,16 +37,11 @@ struct AccessToken {
|
||||
}
|
||||
|
||||
impl<'a> Registration<'a> {
|
||||
/// Construct a new registration process to the instance of the `base` url.
|
||||
/// ```
|
||||
/// use elefren::prelude::*;
|
||||
///
|
||||
/// let registration = Registration::new("https://mastodon.social");
|
||||
/// ```
|
||||
/// Construct a new registration process to the instance of the `base` url (e.g. "https://mastodon.social")
|
||||
pub fn new<I: Into<String>>(base: I) -> Self {
|
||||
Registration {
|
||||
base: base.into(),
|
||||
client: Client::new(),
|
||||
http_client: reqwest::Client::new(),
|
||||
app_builder: AppBuilder::new(),
|
||||
force_login: false,
|
||||
}
|
||||
@@ -93,32 +85,12 @@ impl<'a> Registration<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
async fn send(&self, req: RequestBuilder) -> Result<Response> {
|
||||
async fn send(&self, req: reqwest::RequestBuilder) -> Result<reqwest::Response> {
|
||||
let req = req.build()?;
|
||||
Ok(self.client.execute(req).await?)
|
||||
Ok(self.http_client.execute(req).await?)
|
||||
}
|
||||
|
||||
/// Register the given application
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # fn main () -> elefren::Result<()> {
|
||||
/// use elefren::{apps::App, prelude::*};
|
||||
///
|
||||
/// let mut app = App::builder();
|
||||
/// app.client_name("elefren_test");
|
||||
///
|
||||
/// let registration = Registration::new("https://mastodon.social").register(app)?;
|
||||
/// let url = registration.authorize_url()?;
|
||||
/// // 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)?;
|
||||
///
|
||||
/// println!("{:?}", mastodon.get_home_timeline()?.initial_items);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn register<I: TryInto<App>>(&mut self, app: I) -> Result<Registered>
|
||||
where
|
||||
Error: From<<I as TryInto<App>>::Error>,
|
||||
@@ -128,7 +100,7 @@ impl<'a> Registration<'a> {
|
||||
|
||||
Ok(Registered {
|
||||
base: self.base.clone(),
|
||||
client: self.client.clone(),
|
||||
http_client: self.http_client.clone(),
|
||||
client_id: oauth.client_id,
|
||||
client_secret: oauth.client_secret,
|
||||
redirect: oauth.redirect_uri,
|
||||
@@ -138,32 +110,13 @@ impl<'a> Registration<'a> {
|
||||
}
|
||||
|
||||
/// Register the application with the server from the `base` url.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # fn main () -> elefren::Result<()> {
|
||||
/// use elefren::prelude::*;
|
||||
///
|
||||
/// let registration = Registration::new("https://mastodon.social")
|
||||
/// .client_name("elefren_test")
|
||||
/// .build()?;
|
||||
/// let url = registration.authorize_url()?;
|
||||
/// // 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)?;
|
||||
///
|
||||
/// println!("{:?}", mastodon.get_home_timeline()?.initial_items);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn build(&mut self) -> Result<Registered> {
|
||||
let app: App = self.app_builder.clone().build()?;
|
||||
let oauth = self.send_app(&app).await?;
|
||||
|
||||
Ok(Registered {
|
||||
base: self.base.clone(),
|
||||
client: self.client.clone(),
|
||||
http_client: self.http_client.clone(),
|
||||
client_id: oauth.client_id,
|
||||
client_secret: oauth.client_secret,
|
||||
redirect: oauth.redirect_uri,
|
||||
@@ -174,7 +127,7 @@ impl<'a> Registration<'a> {
|
||||
|
||||
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)).await?
|
||||
Ok(self.send(self.http_client.post(url).json(&app)).await?
|
||||
.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -182,32 +135,6 @@ impl<'a> Registration<'a> {
|
||||
impl Registered {
|
||||
/// Skip having to retrieve the client id and secret from the server by
|
||||
/// creating a `Registered` struct directly
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # fn main() -> elefren::Result<()> {
|
||||
/// 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()?;
|
||||
/// // 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)?;
|
||||
///
|
||||
/// println!("{:?}", mastodon.get_home_timeline()?.initial_items);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from_parts(
|
||||
base: &str,
|
||||
client_id: &str,
|
||||
@@ -218,7 +145,7 @@ impl Registered {
|
||||
) -> Registered {
|
||||
Registered {
|
||||
base: base.to_string(),
|
||||
client: Client::new(),
|
||||
http_client: reqwest::Client::new(),
|
||||
client_id: client_id.to_string(),
|
||||
client_secret: client_secret.to_string(),
|
||||
redirect: redirect.to_string(),
|
||||
@@ -229,48 +156,13 @@ impl Registered {
|
||||
}
|
||||
|
||||
impl Registered {
|
||||
async fn send(&self, req: RequestBuilder) -> Result<Response> {
|
||||
async fn send(&self, req: reqwest::RequestBuilder) -> Result<reqwest::Response> {
|
||||
let req = req.build()?;
|
||||
Ok(self.client.execute(req).await?)
|
||||
Ok(self.http_client.execute(req).await?)
|
||||
}
|
||||
|
||||
/// Returns the parts of the `Registered` struct that can be used to
|
||||
/// recreate another `Registered` struct
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate elefren;
|
||||
/// use elefren::{prelude::*, registration::Registered};
|
||||
/// # fn main() -> Result<(), Box<std::error::Error>> {
|
||||
///
|
||||
/// let origbase = "https://example.social";
|
||||
/// let origclient_id = "some-client_id";
|
||||
/// let origclient_secret = "some-client-secret";
|
||||
/// let origredirect = "https://example.social/redirect";
|
||||
/// let origscopes = Scopes::all();
|
||||
/// let origforce_login = false;
|
||||
///
|
||||
/// let registered = Registered::from_parts(
|
||||
/// origbase,
|
||||
/// origclient_id,
|
||||
/// origclient_secret,
|
||||
/// origredirect,
|
||||
/// origscopes.clone(),
|
||||
/// origforce_login,
|
||||
/// );
|
||||
///
|
||||
/// let (base, client_id, client_secret, redirect, scopes, force_login) = registered.into_parts();
|
||||
///
|
||||
/// assert_eq!(origbase, &base);
|
||||
/// assert_eq!(origclient_id, &client_id);
|
||||
/// assert_eq!(origclient_secret, &client_secret);
|
||||
/// assert_eq!(origredirect, &redirect);
|
||||
/// assert_eq!(origscopes, scopes);
|
||||
/// assert_eq!(origforce_login, force_login);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn into_parts(self) -> (String, String, String, String, Scopes, bool) {
|
||||
(
|
||||
self.base,
|
||||
@@ -299,16 +191,16 @@ impl Registered {
|
||||
|
||||
/// Create an access token from the client id, client secret, and code
|
||||
/// provided by the authorisation url.
|
||||
pub async fn complete(&self, code: &str) -> Result<Mastodon> {
|
||||
pub async fn complete(&self, code: &str) -> Result<Client> {
|
||||
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)).await?.json().await?;
|
||||
let token: AccessToken = self.send(self.http_client.post(&url)).await?.json().await?;
|
||||
|
||||
let data = Data {
|
||||
let data = ClientData {
|
||||
base: self.base.clone().into(),
|
||||
client_id: self.client_id.clone().into(),
|
||||
client_secret: self.client_secret.clone().into(),
|
||||
@@ -316,8 +208,8 @@ impl Registered {
|
||||
token: token.access_token.into(),
|
||||
};
|
||||
|
||||
let mut builder = MastodonBuilder::new();
|
||||
builder.client(self.client.clone()).data(data);
|
||||
let mut builder = ClientBuilder::new();
|
||||
builder.http_client(self.http_client.clone()).data(data);
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
@@ -327,7 +219,7 @@ impl Registered {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Registered {
|
||||
base: String,
|
||||
client: Client,
|
||||
http_client: reqwest::Client,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
redirect: String,
|
||||
|
||||
@@ -45,9 +45,9 @@ impl Keys {
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # use elefren::{MastodonClient, Mastodon, Data};
|
||||
/// # use elefren::{MastodonClient, Client, ClientData};
|
||||
/// # fn main() -> Result<(), elefren::Error> {
|
||||
/// # let data = Data {
|
||||
/// # let data = ClientData {
|
||||
/// # base: "".into(),
|
||||
/// # client_id: "".into(),
|
||||
/// # client_secret: "".into(),
|
||||
@@ -56,7 +56,7 @@ impl Keys {
|
||||
/// # };
|
||||
/// use elefren::requests::{AddPushRequest, Keys};
|
||||
///
|
||||
/// let client = Mastodon::from(data);
|
||||
/// let client = Client::from(data);
|
||||
///
|
||||
/// let keys = Keys::new("stahesuahoei293ise===", "tasecoa,nmeozka==");
|
||||
/// let mut request = AddPushRequest::new("http://example.com/push/endpoint", &keys);
|
||||
@@ -214,9 +214,9 @@ impl AddPushRequest {
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # use elefren::{MastodonClient, Mastodon, Data};
|
||||
/// # use elefren::{Client, ClientData};
|
||||
/// # fn main() -> Result<(), elefren::Error> {
|
||||
/// # let data = Data {
|
||||
/// # let data = ClientData {
|
||||
/// # base: "".into(),
|
||||
/// # client_id: "".into(),
|
||||
/// # client_secret: "".into(),
|
||||
@@ -225,7 +225,7 @@ impl AddPushRequest {
|
||||
/// # };
|
||||
/// use elefren::requests::UpdatePushRequest;
|
||||
///
|
||||
/// let client = Mastodon::from(data);
|
||||
/// let client = Client::from(data);
|
||||
///
|
||||
/// let mut request = UpdatePushRequest::new("foobar");
|
||||
/// request.follow(true).reblog(true);
|
||||
|
||||
@@ -15,9 +15,9 @@ use crate::{
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # use elefren::Data;
|
||||
/// # use elefren::ClientData;
|
||||
/// # fn main() -> Result<(), elefren::Error> {
|
||||
/// # let data = Data {
|
||||
/// # let data = ClientData {
|
||||
/// # base: "".into(),
|
||||
/// # client_id: "".into(),
|
||||
/// # client_secret: "".into(),
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
use crate::{Error, Result};
|
||||
use tokio_tungstenite::{WebSocketStream, MaybeTlsStream};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_stream::Stream;
|
||||
use crate::entities::event::Event;
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use crate::entities::notification::Notification;
|
||||
use crate::entities::status::Status;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum StreamKind<'a> {
|
||||
User,
|
||||
Public,
|
||||
PublicLocal,
|
||||
Direct,
|
||||
Hashtag(&'a str),
|
||||
HashtagLocal(&'a str),
|
||||
List(&'a str),
|
||||
}
|
||||
|
||||
impl<'a> StreamKind<'a> {
|
||||
pub(crate) fn get_url_fragment(&self) -> &'static str {
|
||||
match self {
|
||||
StreamKind::User => "user",
|
||||
StreamKind::Public => "public",
|
||||
StreamKind::PublicLocal => "public/local",
|
||||
StreamKind::Direct => "direct",
|
||||
StreamKind::Hashtag(_) => "hashtag",
|
||||
StreamKind::HashtagLocal(_) => "hashtag/local",
|
||||
StreamKind::List(_) => "list",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_query_params(&self) -> Vec<(&str, &str)> {
|
||||
match self {
|
||||
StreamKind::User => vec![],
|
||||
StreamKind::Public => vec![],
|
||||
StreamKind::PublicLocal => vec![],
|
||||
StreamKind::Direct => vec![],
|
||||
StreamKind::Hashtag(tag)
|
||||
| StreamKind::HashtagLocal(tag) => vec![("tag", tag)],
|
||||
StreamKind::List(list) => vec![("tag", list)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn do_open_streaming(url : &str) -> Result<EventReader> {
|
||||
let mut url: url::Url = reqwest::get(url).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()))?;
|
||||
|
||||
let (client, _response) = tokio_tungstenite::connect_async(url.as_str()).await?;
|
||||
|
||||
Ok(EventReader::new(client))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Iterator that produces events from a mastodon streaming API event stream
|
||||
pub struct EventReader {
|
||||
stream: WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
lines: Vec<String>,
|
||||
}
|
||||
|
||||
impl EventReader {
|
||||
fn new(stream: WebSocketStream<MaybeTlsStream<TcpStream>>) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
lines: vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for EventReader {
|
||||
type Item = Event;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match Pin::new(&mut self.stream).poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(Message::Text(line)))) => {
|
||||
debug!("WS rx: {}", line);
|
||||
let line = line.trim().to_string();
|
||||
if line.starts_with(':') || line.is_empty() {
|
||||
debug!("discard as comment");
|
||||
return Poll::Pending;
|
||||
}
|
||||
self.lines.push(line);
|
||||
if let Ok(event) = self.make_event(&self.lines) {
|
||||
debug!("Parsed event");
|
||||
self.lines.clear();
|
||||
return Poll::Ready(Some(event));
|
||||
} else {
|
||||
debug!("Failed to parse");
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
Poll::Ready(Some(Ok(other))) => {
|
||||
warn!("Unexpected msg: {:?}", other);
|
||||
Poll::Pending
|
||||
}
|
||||
Poll::Ready(Some(Err(error))) => {
|
||||
error!("Websocket error: {:?}", error);
|
||||
// Close
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
// Stream is closed
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Poll::Pending => {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventReader {
|
||||
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:")) {
|
||||
debug!("plaintext formatted 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 {
|
||||
debug!("JSON formatted event");
|
||||
use serde::Deserialize;
|
||||
#[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::StreamingFormat)?;
|
||||
let notification = serde_json::from_str::<Notification>(&data)?;
|
||||
Event::Notification(notification)
|
||||
}
|
||||
"update" => {
|
||||
let data = data.ok_or_else(|| Error::StreamingFormat)?;
|
||||
let status = serde_json::from_str::<Status>(&data)?;
|
||||
Event::Update(status)
|
||||
}
|
||||
"delete" => {
|
||||
let data = data.ok_or_else(|| Error::StreamingFormat)?;
|
||||
Event::Delete(data)
|
||||
}
|
||||
"filters_changed" => Event::FiltersChanged,
|
||||
_ => return Err(Error::Other(format!("Unknown event `{}`", event))),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use crate::{Result, streaming};
|
||||
use crate::entities::card::Card;
|
||||
use crate::entities::context::Context;
|
||||
use crate::entities::status::Status;
|
||||
use crate::helpers::deserialise_response;
|
||||
use crate::streaming::EventReader;
|
||||
|
||||
/// Client that can make unauthenticated calls to a mastodon instance
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Client {
|
||||
client: reqwest::Client,
|
||||
base: url::Url,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new unauthenticated client
|
||||
pub fn new(base: &str) -> Result<Client> {
|
||||
let base = if base.starts_with("https://") {
|
||||
base.to_string()
|
||||
} else {
|
||||
format!("https://{}", base)
|
||||
};
|
||||
Ok(Client {
|
||||
client: reqwest::Client::new(),
|
||||
base: url::Url::parse(&base)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// # Low-level API for extending
|
||||
/// Create a route with the given path fragment
|
||||
pub fn route(&self, url: &str) -> Result<url::Url> {
|
||||
Ok(self.base.join(url)?)
|
||||
}
|
||||
|
||||
/// # Low-level API for extending
|
||||
/// Send a request
|
||||
pub async fn send(&self, req: reqwest::RequestBuilder) -> Result<reqwest::Response> {
|
||||
let req = req.build()?;
|
||||
Ok(self.client.execute(req).await?)
|
||||
}
|
||||
|
||||
// TODO verify if this really works without auth
|
||||
|
||||
/// Get a stream of the public timeline
|
||||
pub async fn streaming_public(&self) -> Result<EventReader> {
|
||||
let url: url::Url = self.route("/api/v1/streaming/public")?;
|
||||
streaming::do_open_streaming(url.as_str()).await
|
||||
}
|
||||
|
||||
/// Get a stream of the local timeline
|
||||
pub async fn streaming_local(&self) -> Result<EventReader> {
|
||||
let url: url::Url = self.route("/api/v1/streaming/public/local")?;
|
||||
streaming::do_open_streaming(url.as_str()).await
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
let response = self.send(self.client.get(route)).await?;
|
||||
deserialise_response(response).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")?;
|
||||
let response = self.send(self.client.get(route)).await?;
|
||||
deserialise_response(response).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")?;
|
||||
let response = self.send(self.client.get(route)).await?;
|
||||
deserialise_response(response).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user