use serde::{Deserialize, Serialize};
use std::{convert::TryInto, io};
use twitch_oauth2::TwitchToken;
pub mod channels;
pub mod clips;
pub mod moderation;
pub mod streams;
pub mod subscriptions;
pub mod users;
pub(crate) mod ser;
pub use ser::Error;
#[doc(no_inline)]
pub use twitch_oauth2::Scope;
#[derive(Clone)]
pub struct HelixClient {
client: reqwest::Client,
}
#[derive(PartialEq, Deserialize, Debug)]
struct InnerResponse<D> {
data: Vec<D>,
#[serde(default)]
pagination: Pagination,
}
#[derive(Deserialize, Clone, Debug)]
struct HelixRequestError {
error: String,
status: u16,
message: String,
}
impl HelixClient {
pub fn with_client(client: reqwest::Client) -> HelixClient { HelixClient { client } }
pub fn new() -> HelixClient {
let client = reqwest::Client::new();
HelixClient::with_client(client)
}
pub fn clone_client(&self) -> reqwest::Client { self.client.clone() }
pub async fn req_get<R, D, T>(
&self,
request: R,
token: &T,
) -> Result<Response<R, D>, RequestError>
where
R: Request<Response = D> + Request + RequestGet,
D: serde::de::DeserializeOwned,
T: TwitchToken + ?Sized,
{
let url = url::Url::parse(&format!(
"{}{}?{}",
crate::TWITCH_HELIX_URL,
<R as Request>::PATH,
request.query()?
))?;
let req = self
.client
.get(url.clone())
.header("Client-ID", token.client_id().as_str())
.bearer_auth(token.token().secret())
.send()
.await?;
let text = req.text().await?;
if let Ok(HelixRequestError {
error,
status,
message,
}) = serde_json::from_str::<HelixRequestError>(&text)
{
return Err(RequestError::HelixRequestGetError {
error,
status: status
.try_into()
.unwrap_or_else(|_| reqwest::StatusCode::BAD_REQUEST),
message,
url,
});
}
let response: InnerResponse<D> = serde_json::from_str(&text)?;
Ok(Response {
data: response.data,
pagination: response.pagination,
request,
})
}
pub async fn req_post<R, B, D, T>(
&self,
request: R,
body: B,
token: &T,
) -> Result<Response<R, D>, RequestError>
where
R: Request<Response = D> + Request + RequestPost<Body = B>,
B: serde::Serialize,
D: serde::de::DeserializeOwned,
T: TwitchToken + ?Sized,
{
let url = url::Url::parse(&format!(
"{}{}?{}",
crate::TWITCH_HELIX_URL,
<R as Request>::PATH,
request.query()?
))?;
let body = request.body(&body)?;
let req = self
.client
.post(url.clone())
.header("Client-ID", token.client_id().as_str())
.header("Content-Type", "application/json")
.bearer_auth(token.token().secret())
.body(body.clone())
.send()
.await?;
let text = req.text().await?;
if let Ok(HelixRequestError {
error,
status,
message,
}) = serde_json::from_str::<HelixRequestError>(&text)
{
return Err(RequestError::HelixRequestPutError {
error,
status: status
.try_into()
.unwrap_or_else(|_| reqwest::StatusCode::BAD_REQUEST),
message,
url,
body,
});
}
let response: InnerResponse<D> = serde_json::from_str(&text)?;
Ok(Response {
data: response.data,
pagination: response.pagination,
request,
})
}
pub async fn req_patch<R, B, D, T>(
&self,
request: R,
body: B,
token: &T,
) -> Result<D, RequestError>
where
R: Request<Response = D> + Request + RequestPatch<Body = B>,
B: serde::Serialize,
D: std::convert::TryFrom<http::StatusCode, Error = std::borrow::Cow<'static, str>>,
T: TwitchToken + ?Sized,
{
let url = url::Url::parse(&format!(
"{}{}?{}",
crate::TWITCH_HELIX_URL,
<R as Request>::PATH,
request.query()?
))?;
let body = request.body(&body)?;
let req = self
.client
.patch(url.clone())
.header("Client-ID", token.client_id().as_str())
.header("Content-Type", "application/json")
.bearer_auth(token.token().secret())
.body(body.clone())
.send()
.await?;
match req.status().try_into() {
Ok(result) => Ok(result),
Err(err) => Err(RequestError::HelixRequestPatchError {
status: req.status(),
message: err.to_string(),
url,
body,
}),
}
}
}
impl Default for HelixClient {
fn default() -> Self { HelixClient::new() }
}
#[async_trait::async_trait]
pub trait Request: serde::Serialize {
const PATH: &'static str;
const SCOPE: &'static [twitch_oauth2::Scope];
const OPT_SCOPE: &'static [twitch_oauth2::Scope] = &[];
type Response;
fn query(&self) -> Result<String, ser::Error> { ser::to_string(&self) }
}
pub trait RequestPost: Request {
type Body: serde::Serialize;
fn body(&self, body: &Self::Body) -> Result<String, serde_json::Error> {
serde_json::to_string(body)
}
}
pub trait RequestPatch: Request {
type Body: serde::Serialize;
fn body(&self, body: &Self::Body) -> Result<String, serde_json::Error> {
serde_json::to_string(body)
}
}
pub trait RequestGet: Request {}
#[derive(PartialEq, Debug)]
pub struct Response<R, D>
where R: Request<Response = D> {
pub data: Vec<D>,
pub pagination: Pagination,
pub request: R,
}
impl<R, D> Response<R, D>
where
R: Request<Response = D> + Clone + Paginated + RequestGet,
D: serde::de::DeserializeOwned,
{
pub async fn get_next(
self,
client: &HelixClient,
token: &impl TwitchToken,
) -> Result<Option<Response<R, D>>, RequestError>
{
let mut req = self.request.clone();
if let Some(ref cursor) = self.pagination.cursor {
req.set_pagination(cursor.clone());
client.req_get(req, token).await.map(Some)
} else {
Ok(None)
}
}
}
pub trait Paginated: Request {
fn set_pagination(&mut self, cursor: Cursor);
}
#[derive(PartialEq, Deserialize, Serialize, Debug, Clone, Default)]
pub struct Pagination {
#[serde(default)]
cursor: Option<Cursor>,
}
pub type Cursor = String;
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum RequestError {
UrlParseError(#[from] url::ParseError),
IOError(#[from] io::Error),
DeserializeError(#[from] serde_json::Error),
QuerySerializeError(#[from] ser::Error),
RequestError(#[from] reqwest::Error),
NoPage,
PatchParseError(std::borrow::Cow<'static, str>),
Custom(std::borrow::Cow<'static, str>),
HelixRequestGetError {
error: String,
status: http::StatusCode,
message: String,
url: url::Url,
},
HelixRequestPutError {
error: String,
status: http::StatusCode,
message: String,
url: url::Url,
body: String,
},
HelixRequestPatchError {
status: http::StatusCode,
message: String,
url: url::Url,
body: String,
},
}
pub fn repeat_query(name: &str, items: &[String]) -> String {
let mut s = String::new();
for (idx, item) in items.iter().enumerate() {
s.push_str(&format!("{}={}", name, item));
if idx + 1 != items.len() {
s.push_str("&")
}
}
s
}