patreon 0.2.1

Patreon client
Documentation
//! User API client.
//!
//! Used to access user-related data after a user completes the OAuth flow.
//! These APIs require a user `access_token`.
//!
//! ## Typical use cases
//! - Fetch the user's Patreon identity
//! - Check whether a user is a patron of a specific creator
//! - List memberships for the authorized user
//!
//! ## Available API
//! - `/api/oauth2/v2/identity` - Fetch the currently authorized user

use crate::models::*;
use crate::{Error, Result, API_BASE_URL};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};

/// Patreon user API client.
///
/// Accesses endpoints available to an OAuth-authorized user, including identity and memberships.
///
/// # Example
///
/// ```rust,ignore
/// use patreon::PatreonUserClient;
///
/// let client = PatreonUserClient::new("user_access_token");
///
/// // Fetch basic identity info
/// let identity = client.identity().await?;
///
/// // Fetch identity with memberships
/// let identity_with_memberships = client
///     .identity_with_memberships()
///     .await?;
/// ```
#[derive(Debug, Clone)]
pub struct PatreonUserClient {
    access_token: String,
    http_client: reqwest::Client,
    base_url: String,
}

impl PatreonUserClient {
    /// Creates a new user client.
    ///
    /// # Parameters
    /// - `access_token`: the user's OAuth access token
    pub fn new(access_token: impl Into<String>) -> Self {
        Self {
            access_token: access_token.into(),
            http_client: reqwest::Client::new(),
            base_url: API_BASE_URL.to_string(),
        }
    }

    /// Uses a custom `reqwest::Client`.
    pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
        self.http_client = client;
        self
    }

    /// Uses a custom base URL (useful for tests).
    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
        self.base_url = base_url.into();
        self
    }

    /// Builds authorization headers.
    fn auth_headers(&self) -> HeaderMap {
        let mut headers = HeaderMap::new();
        headers.insert(
            AUTHORIZATION,
            HeaderValue::from_str(&format!("Bearer {}", self.access_token))
                .expect("Invalid token"),
        );
        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
        headers
    }

    /// Sends a GET request.
    async fn get<T: serde::de::DeserializeOwned>(&self, endpoint: &str) -> Result<T> {
        let url = format!("{}{}", self.base_url, endpoint);
        let response = self
            .http_client
            .get(&url)
            .headers(self.auth_headers())
            .send()
            .await?;

        if response.status().is_success() {
            Ok(response.json().await?)
        } else {
            let status = response.status().as_u16();
            let text = response.text().await.unwrap_or_default();
            Err(Error::Api {
                status,
                message: text,
            })
        }
    }

    // ==================== Identity API ====================

    /// Fetches the current authorized user's identity.
    ///
    /// Returns basic user data (no included related resources).
    ///
    /// # Required scopes
    /// - `identity`
    pub async fn identity(&self) -> Result<SingleResponse<UserResource>> {
        self.get("/identity").await
    }

    /// Fetches user identity with custom fields.
    ///
    /// # Parameters
    /// - `fields`: list of user fields to request
    ///
    /// # Required scopes
    /// - `identity`
    /// - `identity[email]` (if requesting the `email` field)
    pub async fn identity_with_fields(
        &self,
        fields: &[&str],
    ) -> Result<SingleResponse<UserResource>> {
        let fields_param = fields.join(",");
        self.get(&format!("/identity?fields[user]={}", fields_param))
            .await
    }

    /// Fetches user identity including memberships.
    ///
    /// Returns the user identity and all memberships where the user is a patron.
    ///
    /// # Required scopes
    /// - `identity`
    /// - `identity.memberships`
    pub async fn identity_with_memberships(&self) -> Result<SingleResponse<UserResource>> {
        self.get("/identity?include=memberships&fields[user]=email,first_name,full_name,image_url,last_name,vanity,url&fields[member]=campaign_lifetime_support_cents,currently_entitled_amount_cents,email,full_name,is_follower,last_charge_date,last_charge_status,lifetime_support_cents,next_charge_date,note,patron_status,pledge_relationship_start").await
    }

    /// Fetches user identity including memberships and the related campaigns.
    ///
    /// Returns the user identity, memberships, and the referenced campaign resources.
    ///
    /// # Required scopes
    /// - `identity`
    /// - `identity.memberships`
    pub async fn identity_with_memberships_and_campaign(
        &self,
    ) -> Result<SingleResponse<UserResource>> {
        self.get("/identity?include=memberships,memberships.campaign&fields[user]=email,first_name,full_name,image_url,last_name,vanity,url&fields[member]=currently_entitled_amount_cents,lifetime_support_cents,patron_status&fields[campaign]=creation_name,image_url,url,vanity").await
    }

    /// Fetches a full identity response.
    ///
    /// Includes many available fields and related resources.
    ///
    /// # Required scopes
    /// - `identity`
    /// - `identity[email]`
    /// - `identity.memberships`
    pub async fn identity_full(&self) -> Result<SingleResponse<UserResource>> {
        self.get("/identity?include=memberships,memberships.campaign,memberships.currently_entitled_tiers&fields[user]=about,created,email,first_name,full_name,hide_pledges,image_url,is_creator,is_email_verified,last_name,like_count,social_connections,thumb_url,url,vanity&fields[member]=campaign_lifetime_support_cents,currently_entitled_amount_cents,email,full_name,is_follower,last_charge_date,last_charge_status,lifetime_support_cents,next_charge_date,note,patron_status,pledge_relationship_start,will_pay_amount_cents&fields[campaign]=creation_name,image_url,url,vanity&fields[tier]=amount_cents,description,title,url").await
    }
}

/// Field names for `identity_with_fields`.
pub mod identity_fields {
    /// User bio.
    pub const ABOUT: &str = "about";
    /// Account creation time.
    pub const CREATED: &str = "created";
    /// Email.
    pub const EMAIL: &str = "email";
    /// First name.
    pub const FIRST_NAME: &str = "first_name";
    /// Full name.
    pub const FULL_NAME: &str = "full_name";
    /// Whether pledges are hidden.
    pub const HIDE_PLEDGES: &str = "hide_pledges";
    /// Avatar image URL.
    pub const IMAGE_URL: &str = "image_url";
    /// Whether the user is a creator.
    pub const IS_CREATOR: &str = "is_creator";
    /// Whether the email is verified.
    pub const IS_EMAIL_VERIFIED: &str = "is_email_verified";
    /// Last name.
    pub const LAST_NAME: &str = "last_name";
    /// Like count.
    pub const LIKE_COUNT: &str = "like_count";
    /// Social connections.
    pub const SOCIAL_CONNECTIONS: &str = "social_connections";
    /// Thumbnail URL.
    pub const THUMB_URL: &str = "thumb_url";
    /// Patreon profile URL.
    pub const URL: &str = "url";
    /// Vanity username.
    pub const VANITY: &str = "vanity";
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new_client() {
        let client = PatreonUserClient::new("test_token");
        assert_eq!(client.access_token, "test_token");
    }

    #[test]
    fn test_auth_headers() {
        let client = PatreonUserClient::new("test_token");
        let headers = client.auth_headers();
        assert!(headers.get(AUTHORIZATION).is_some());
        assert_eq!(
            headers.get(AUTHORIZATION).unwrap().to_str().unwrap(),
            "Bearer test_token"
        );
    }
}