convert all to sync, remove now broken doc comments, some renames

This commit is contained in:
2021-08-21 15:19:22 +02:00
parent c967e59ffb
commit 568b1ff07a
59 changed files with 504 additions and 4737 deletions
+1 -1
View File
@@ -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.
-21
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
-78
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
-39
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+6 -6
View File
@@ -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);
+2 -2
View File
@@ -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(),
+165
View File
@@ -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))),
})
}
}
+83
View File
@@ -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
}
}