use reqwest_middleware::{
reqwest::{
header::{ACCEPT_LANGUAGE, USER_AGENT},
ClientBuilder, Response, StatusCode,
},
ClientWithMiddleware,
};
use serde::Deserialize;
use std::error::Error;
use std::{collections::HashMap, sync::Arc};
use crate::{
tokens::{Session, SessionManager},
PixivAppError,
};
pub struct PixivAppClient {
pub(crate) session: Arc<SessionManager>,
pub(crate) http_client: ClientWithMiddleware,
pub(crate) host: String,
pub(crate) platform: params::Platform,
}
pub mod params {
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter, Result};
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
pub enum Platform {
IOS,
Android,
}
impl Display for Platform {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(
f,
"{}",
match self {
Self::IOS => "ios",
Self::Android => "android",
}
)
}
}
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
pub enum Visibility {
Public,
Private,
}
impl Display for Visibility {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(
f,
"{}",
match self {
Self::Public => "public",
Self::Private => "private",
}
)
}
}
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
pub enum SortMode {
DateAscending,
DateDescending,
PopularDescending,
}
impl Display for SortMode {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(
f,
"{}",
match self {
Self::DateAscending => "date_asc",
Self::DateDescending => "date_desc",
Self::PopularDescending => "popular_desc",
}
)
}
}
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
pub enum SearchMode {
PartialMatchForTags,
ExactMatchForTags,
TitleAndCaption,
}
impl Display for SearchMode {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(
f,
"{}",
match self {
Self::PartialMatchForTags => "partial_match_for_tags",
Self::ExactMatchForTags => "exact_match_for_tags",
Self::TitleAndCaption => "title_and_caption",
}
)
}
}
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
pub enum RankingMode {
Day,
Week,
Month,
}
impl Display for RankingMode {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(
f,
"{}",
match self {
Self::Day => String::from("day"),
Self::Week => String::from("week"),
Self::Month => String::from("month"),
}
)
}
}
}
impl PixivAppClient {
pub fn new(token: String) -> Self {
Self::_new(Arc::new(SessionManager::new(token)))
}
pub fn restore(session: Session) -> Self {
Self::_new(Arc::new(SessionManager::restore(session)))
}
fn _new(sessionman: Arc<SessionManager>) -> Self {
Self {
session: sessionman.clone(),
http_client: reqwest_middleware::ClientBuilder::new(
ClientBuilder::new()
.default_headers(
(&HashMap::from([
("app-os".to_string(), "ios".to_string()),
("app-os-version".to_string(), "12.2".to_string()),
("app-version".to_string(), "7.6.2".to_string()),
(
USER_AGENT.to_string(),
"PixivIOSApp/7.6.2 (iOS 12.2; iPhone9,1)".to_string(),
),
]))
.try_into()
.unwrap(),
)
.build()
.unwrap(),
)
.with_arc(sessionman.clone())
.build(),
host: String::from("https://app-api.pixiv.net"),
platform: params::Platform::IOS,
}
}
pub async fn session(&self) -> Session {
self.session.session.read().await.clone()
}
pub(crate) async fn process_request<T: for<'de> Deserialize<'de>>(
&self,
reqb: reqwest_middleware::RequestBuilder,
) -> Result<T, Box<dyn Error + Send + Sync>> {
Ok(reqb
.header(ACCEPT_LANGUAGE, "en-US")
.send()
.await
.map_or_else(
|e| Err::<Response, Box<dyn Error + Send + Sync>>(Box::new(e)),
|r| match r.status() {
x if StatusCode::is_success(&x) || StatusCode::is_redirection(&x) => Ok(r),
x if StatusCode::is_client_error(&x) => Err(Box::new(match x {
StatusCode::BAD_REQUEST => PixivAppError::RequestFailed,
StatusCode::UNAUTHORIZED => PixivAppError::MissingLogin,
StatusCode::NOT_FOUND => PixivAppError::TargetNotFound,
StatusCode::TOO_MANY_REQUESTS => PixivAppError::RateLimitReached,
s => PixivAppError::UnhandledStatus(s),
})),
x => Err(Box::new(PixivAppError::UnhandledStatus(x))),
},
)?
.text()
.await
.map(|r| async move {
if cfg!(test) {
eprintln!("{}", r);
}
let d = &mut serde_json::Deserializer::from_str(&r);
let r = serde_path_to_error::deserialize(d);
r
})
.map_err(Box::new)?
.await?)
}
}
#[cfg(test)]
pub mod tests {
use once_cell::sync::Lazy;
use super::*;
use tokio::test;
pub static USER: &str = "Aio";
pub static USER_ID: u32 = 25308802;
pub static ILLUST_ID: u32 = 132610892;
pub static _STUB: Lazy<Arc<()>> = Lazy::new(|| Arc::new(env_logger::init()));
pub fn get_client() -> PixivAppClient {
let _ = _STUB.clone();
let client = PixivAppClient::new(env!("PIXIV_REFRESH_TOKEN").to_string());
client
}
#[test]
async fn test_session_restore() {
let client = get_client();
assert!(client.illust_details(ILLUST_ID).await.is_ok());
let client2 = PixivAppClient::restore(client.session().await);
assert!(client2.illust_details(ILLUST_ID).await.is_ok());
}
}