gpipipi 0.2.1

a rust crate for the google play api
Documentation
use crate::{
    DeviceProperties,
    api::{
        endpoint::Requester,
        login::{AuthToken, checkin, fetch_auth, toc, upload_device},
    },
    client::{Client, ClientRequester, async_doc},
    error::{LoginError, PropsError},
};

/// a logged in google play client that owns an aas token
///
/// aas tokens don't seem to expire, so this client renews its own auth token whenever the current
/// one runs out, which makes it the one to use for anything long running
///
/// to create one, see [`LongLivedClient::login`], the requests themselves live on [`Client`]
pub struct LongLivedClient {
    aas_token: String,
    email: String,

    props: DeviceProperties,
    auth_token: AuthToken,

    checkin_token: String,
    config_token: String,
    dfe_cookie: String,
    gsf: i64,
}

impl LongLivedClient {
    /// creates a new client, logging in using the email and AAS token, acting as the given
    /// device props, **AND accepting Terms of Service if required!!**
    ///
    /// if you have an oauth token, you first need to convert it to aas using [`OAuthRequest`]
    ///
    /// # Examples
    #[doc = async_doc!(r#"let mut props_map = gpipipi::PropsMap::parse_aurora_config(PROPS)?;
let props = props_map.remove("arm").ok_or("No 'arm' device props!")?;
let client = gpipipi::LongLivedClient::login(email, aas_token, props).await?;"#)]
    ///
    /// # Errors
    /// - `Checkin` - failed to checkin into google
    /// - `Upload` - failed to upload device props to google
    /// - `Auth` - failed to fetch the auth token from google
    /// - `Toc` - failed to fetch the terms of service / accept them
    /// - `NoDeviceToken` - google returned no `device_checkin_consistency_token`
    /// - `NoAndroidId` - google returned no `android_id`
    /// - `NoConfigToken` - google returned no `upload_device_config_token`
    pub async fn login<A: Into<String>, B: Into<String>>(
        email: A,
        aas_token: B,
        props: DeviceProperties,
    ) -> Result<Self, LoginError> {
        // internal state is all handled here so there's no case where clients can exist
        // with incomplete data

        let email = email.into();
        let aas_token = aas_token.into();

        let checkin_response = checkin(&props).await?;
        let gsf = checkin_response
            .android_id
            .ok_or(LoginError::NoAndroidId())?
            .cast_signed();

        let checkin_token = checkin_response
            .device_checkin_consistency_token
            .ok_or(LoginError::NoDeviceToken())?;

        let upload_response = upload_device(&props, gsf, &checkin_token).await?;
        let config_token = upload_response
            .upload_device_config_token
            .ok_or(LoginError::NoConfigToken())?;

        let auth_token = fetch_auth(&props, &aas_token, &email, gsf).await?;
        let dfe_cookie = toc(&auth_token, gsf, &checkin_token, &props).await?;

        Ok(Self {
            aas_token,
            email,
            props,
            auth_token,
            checkin_token,
            config_token,
            dfe_cookie,
            gsf,
        })
    }
}

impl ClientRequester for LongLivedClient {
    async fn requester(&self) -> Result<Requester<'_>, PropsError> {
        Requester::new()
            .checkin_token(&self.checkin_token)
            .config_token(&self.config_token)
            .auth_token(&self.auth_token)
            .dfe_cookie(&self.dfe_cookie)
            .aas_token(&self.aas_token)
            .email(&self.email)
            .props(&self.props)
            .gsf(self.gsf)
            .default_headers()
            .await
    }
}

impl Client for LongLivedClient {}