libpixiv 0.2.1

pixiv.net API client library
Documentation
use crate::models::{SessionResponse, SessionResponseWrapper};
use chrono::{DateTime, Duration, Local};
use http::Extensions;
use reqwest_middleware::{
    reqwest::{
        header::{AUTHORIZATION, CONTENT_TYPE, USER_AGENT},
        Request, Response,
    },
    Middleware, Next, Result,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;

/// Session data required to start and maintain an OAuth2 session for pixiv
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
    access_token: Option<String>,
    refresh_token: String,
    expiry: Option<DateTime<Local>>,
}

impl Session {
    // Returns whether the session is (still) valid, using a tolerance of `expiry_tolerance` secs
    pub fn is_valid(&self, expiry_tolerance: i64) -> bool {
        self.access_token.is_some()
            || self
                .expiry
                .is_some_and(|d| d.timestamp() - Local::now().timestamp() > expiry_tolerance)
    }
}

/// Session manager containing a session and implementing logic for token auto refreshing
pub struct SessionManager {
    pub(crate) session: Arc<RwLock<Session>>,
}

impl SessionManager {
    /// Create SessionManager using refresh token
    pub fn new(token: String) -> Self {
        Self {
            session: Arc::new(RwLock::new(Session {
                access_token: None,
                refresh_token: token,
                expiry: None,
            })),
        }
    }

    /// Restore session
    pub fn restore(session: Session) -> Self {
        Self {
            session: Arc::new(RwLock::new(session)),
        }
    }

    /// Refresh the credentials of the API client.
    ///
    /// <div class="warning">This function panics if the authorization fails</div>
    pub async fn refresh_token(&self, session: &mut Session) -> Result<()> {
        let time = Local::now().format("%y-%m-%dT%H:%m:%s+00:00").to_string();
        let req = reqwest_middleware::reqwest::Client::new()
            .post("https://oauth.secure.pixiv.net/auth/token")
            .body(
                [
                    ("grant_type", "refresh_token"),
                    ("refresh_token", &session.refresh_token),
                    ("client_id", "MOBrBDS8blbauoSck0ZfDbtuzpyT"),
                    ("client_secret", "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj"),
                    ("get_secure_url", "1"),
                ]
                .iter()
                .map(|p| format!("{}={}", p.0, p.1))
                .collect::<Vec<_>>()
                .join("&"),
            )
            .headers(
                (&HashMap::from([
                    (
                        CONTENT_TYPE.to_string(),
                        "application/x-www-form-urlencoded".to_string(),
                    ),
                    (
                        USER_AGENT.to_string(),
                        "PixivIOSApp/7.6.2 (iOS 12.2; iPhone9,1)".to_string(),
                    ),
                    (
                        "x-client-hash".to_string(),
                        format!(
                            "{:02x}",
                            md5::compute(
                                format!(
                            "{}{}\n",
                            time,
                            "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c"
                        )
                                .as_str()
                            )
                        ),
                    ),
                    ("x-client-time".to_string(), time),
                ]))
                    .try_into()
                    .unwrap(),
            )
            .send()
            .await?
            .json::<SessionResponseWrapper>()
            .await?;
        *session = Session::from(req.response);
        Ok(())
    }
}

impl From<SessionResponse> for Session {
    fn from(value: SessionResponse) -> Self {
        Self {
            access_token: Some(value.access_token).clone(),
            refresh_token: value.refresh_token.clone(),
            expiry: Some(Local::now() + Duration::seconds(value.expires_in)),
        }
    }
}

#[async_trait::async_trait]
impl Middleware for SessionManager {
    async fn handle(
        &self,
        mut req: Request,
        extensions: &mut Extensions,
        next: Next<'_>,
    ) -> Result<Response> {
        let valid = {
            let session = self.session.read().await;
            session.is_valid(30)
        };
        if !valid {
            let mut session_rw = self.session.write().await;
            if !session_rw.is_valid(30) {
                self.refresh_token(&mut session_rw).await?;
            }
        }
        if let Some(access_token) = &self.session.read().await.access_token {
            req.headers_mut().insert(
                AUTHORIZATION,
                format!("Bearer {}", access_token).try_into().unwrap(),
            );
        }
        next.run(req, extensions).await
    }
}