gpipipi 0.1.2

a rust crate for the google play api
Documentation
use crate::{
    DeviceProperties,
    api::{form::Form, login::AuthToken},
    consts,
    error::{GenericRequestError, PropsError},
    proto,
};
use http::{HeaderMap, HeaderName, HeaderValue};
use prost::Message;
use reqwest::Response;
use std::{borrow::Cow, collections::HashMap, str::FromStr, sync::LazyLock};
use url::Url;

static HTTP_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(reqwest::Client::new);

#[derive(Default)]
pub struct Requester<'a> {
    props: Option<&'a DeviceProperties>,
    aas_token: Option<&'a str>,
    email: Option<&'a str>,

    auth_token: Option<&'a AuthToken>,
    checkin_token: Option<&'a str>,
    config_token: Option<&'a str>,
    dfe_cookie: Option<&'a str>,
    gsf: Option<i64>,

    headers: HashMap<&'a str, Cow<'a, str>>,
    params: &'a [(&'a str, &'a str)],
    user_headers: &'a [(&'a str, &'a str)],
}

impl<'a> Requester<'a> {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub const fn props(mut self, props: &'a DeviceProperties) -> Self {
        self.props = Some(props);
        self
    }

    #[must_use]
    pub const fn auth_token(mut self, token: &'a AuthToken) -> Self {
        self.auth_token = Some(token);
        self
    }

    #[must_use]
    pub const fn email(mut self, email: &'a str) -> Self {
        self.email = Some(email);
        self
    }

    #[must_use]
    pub const fn aas_token(mut self, token: &'a str) -> Self {
        self.aas_token = Some(token);
        self
    }

    #[must_use]
    pub const fn checkin_token(mut self, token: &'a str) -> Self {
        self.checkin_token = Some(token);
        self
    }

    #[must_use]
    pub const fn config_token(mut self, token: &'a str) -> Self {
        self.config_token = Some(token);
        self
    }

    #[must_use]
    pub const fn dfe_cookie(mut self, cookie: &'a str) -> Self {
        self.dfe_cookie = Some(cookie);
        self
    }

    #[must_use]
    pub const fn gsf(mut self, id: i64) -> Self {
        self.gsf = Some(id);
        self
    }

    #[must_use]
    pub const fn headers(mut self, headers: &'a [(&'a str, &'a str)]) -> Self {
        self.user_headers = headers;
        self
    }

    #[must_use]
    pub const fn params(mut self, params: &'a [(&'a str, &'a str)]) -> Self {
        self.params = params;
        self
    }

    /// # Errors
    /// ughh too many errors this is not outside of the lib anyways
    pub fn google_auth_headers(mut self) -> Result<Self, PropsError> {
        let props = self.props.ok_or(PropsError::NoProps())?;

        let build = props
            .android_checkin
            .build
            .as_ref()
            .ok_or(PropsError::NoBuild())?;

        let build_device = build.device.as_ref().ok_or(PropsError::NoBuildDevice())?;

        let build_id = props
            .extra_info
            .get("Build.ID")
            .ok_or(PropsError::NoBuildId())?;

        self.headers
            .insert("app", Cow::from(consts::ANDROID_VENDING));

        self.headers.insert(
            "User-Agent",
            Cow::from(format!("GoogleAuth/1.4 ({build_device} {build_id})")),
        );

        if let Some(id) = self.gsf {
            self.headers.insert("device", Cow::from(format!("{id:x}")));
        }

        Ok(self)
    }

    /// # Errors
    /// ughh too many errors this is not outside of the lib anyways
    pub async fn default_headers(mut self) -> Result<Self, PropsError> {
        if let Some(auth_token) = &self.auth_token {
            let maybe_existing = auth_token.get().map(|a| format!("Bearer {a}"));

            let maybe_new = if maybe_existing.is_none()
                && let Some(props) = self.props
                && let Some(aas) = self.aas_token
                && let Some(email) = self.email
                && let Some(gsf) = self.gsf
            {
                let token = super::login::fetch_auth(props, aas, email, gsf)
                    .await
                    .map_err(|a| PropsError::UpdateAuth(Box::new(a)))?;

                auth_token.update(&token);
                auth_token.get().map(|a| format!("Bearer {a}"))
            } else {
                None
            };

            if let Some(value) = maybe_existing.or(maybe_new) {
                self.headers.insert("Authorization", Cow::from(value));
            }
        }

        let (user_agent, sim_operator) = self.get_useragent()?;
        let mut new = HashMap::from([
            ("user-agent", Cow::from(user_agent)),
            (
                "accept-language",
                Cow::from(consts::LOCALE.replace('_', "-")),
            ),
            ("X-DFE-Encoded-Targets", Cow::from(consts::DFE_TARGETS)),
            ("X-DFE-Request-Params", Cow::from("timeoutMs=4000")),
            ("X-DFE-Phenotype", Cow::from(consts::DFE_PHENOTYPE)),
            ("X-Limit-Ad-Tracking-Enabled", Cow::from("false")),
            ("X-DFE-UserLanguages", Cow::from(consts::LOCALE)),
            ("X-DFE-Client-Id", Cow::from(consts::CLIENT_ID)),
            ("X-DFE-MCCMCN", Cow::from(sim_operator)),
            ("X-DFE-Content-Filters", Cow::from("")),
            ("X-DFE-Network-Type", Cow::from("4")),
            ("X-Ad-Id", Cow::from("")),
        ]);

        if let Some(id) = self.gsf {
            self.headers
                .insert("X-DFE-Device-Id", Cow::from(format!("{id:x}")));
        }

        if let Some(token) = self.checkin_token {
            new.insert("X-DFE-Device-Checkin-Consistency-Token", Cow::from(token));
        }

        if let Some(cookie) = self.dfe_cookie {
            new.insert("X-DFE-Cookie", Cow::from(cookie));
        }

        if let Some(token) = self.config_token {
            new.insert("X-DFE-Device-Config-Token", Cow::from(token));
        }

        self.headers.extend(new);
        Ok(self)
    }

    async fn inner_request(
        &self,
        mut url: Url,
        body: Option<Vec<u8>>,
    ) -> Result<Response, GenericRequestError> {
        if !self.params.is_empty() {
            let mut queries = url.query_pairs_mut();
            for (key, val) in self.params {
                queries.append_pair(key.as_ref(), val.as_ref());
            }
        }

        let mut req_headers = HeaderMap::new();

        for (name, value) in &self.headers {
            req_headers.insert(HeaderName::from_str(name)?, HeaderValue::from_str(value)?);
        }

        for (name, value) in self.user_headers {
            req_headers.insert(HeaderName::from_str(name)?, HeaderValue::from_str(value)?);
        }

        let response = if let Some(body) = body {
            HTTP_CLIENT.post(url).body(body).headers(req_headers)
        } else {
            HTTP_CLIENT.get(url).headers(req_headers)
        }
        .send()
        .await?;

        Ok(response)
    }

    /// # Errors
    /// ughh too many errors this is not outside of the lib anyways
    pub async fn fdfe_request(
        &self,
        endpoint: &str,
        body: Option<Vec<u8>>,
    ) -> Result<Box<proto::ResponseWrapper>, GenericRequestError> {
        let url = Url::parse(&format!("{}/fdfe/{}", consts::BASE_URL, endpoint))?;
        let response = self.inner_request(url, body).await?;
        let bytes = response.bytes().await?;

        Ok(Box::new(proto::ResponseWrapper::decode(bytes)?))
    }

    /// # Errors
    /// ughh too many errors this is not outside of the lib anyways
    pub async fn generic_request(
        &self,
        endpoint: &str,
        body: Option<Vec<u8>>,
    ) -> Result<Response, GenericRequestError> {
        let url = Url::from_str(&format!("{}/{}", consts::BASE_URL, endpoint))?;
        self.inner_request(url, body).await
    }

    /// # Errors
    /// ughh too many errors this is not outside of the lib anyways
    pub async fn form_request(
        &self,
        endpoint: &str,
        form: Form<'a>,
    ) -> Result<Form<'a>, GenericRequestError> {
        let url = Url::from_str(&format!("{}/{}", consts::BASE_URL, endpoint))?;

        let body = form.format().into_bytes();
        let response = self.inner_request(url, Some(body)).await?;

        let text = response.text().await?;
        let form = Form::parse(&text);
        Ok(form)
    }

    fn get_useragent(&self) -> Result<(String, String), PropsError> {
        let props = self.props.ok_or(PropsError::NoProps())?;

        let build = props
            .android_checkin
            .build
            .as_ref()
            .ok_or(PropsError::NoBuild())?;

        let version_string = props
            .extra_info
            .get("Vending.versionString")
            .ok_or(PropsError::NoVersionString())?;

        let vending_version = props
            .extra_info
            .get("Vending.version")
            .ok_or(PropsError::NoVersion())?;

        let release_version = props
            .extra_info
            .get("Build.VERSION.RELEASE")
            .ok_or(PropsError::NoVersionRelease())?;

        let build_id = props
            .extra_info
            .get("Build.ID")
            .ok_or(PropsError::NoBuildId())?;

        let sim_operator = props
            .extra_info
            .get("SimOperator")
            .ok_or(PropsError::NoSimOperator())?;

        let sdk_version = build
            .sdk_version
            .ok_or(PropsError::NoBuildSDK())?
            .to_string();

        let build_model = build.model.as_ref().ok_or(PropsError::NoBuildModel())?;
        let build_device = build.device.as_ref().ok_or(PropsError::NoBuildDevice())?;

        let build_product = build.product.as_ref().ok_or(PropsError::NoBuildProduct())?;

        let build_build_product = build
            .build_product
            .as_ref()
            .ok_or(PropsError::NoBuildProduct())?;

        let abis = props.device_config.native_platform.join(";");

        Ok((
            format!(
                "{}/{} (api={},versionCode={},sdk={},device={},hardware={},product={},platformVersionRelease={},model={},buildId={},isWideScreen={},supportedAbis={})",
                consts::FINSKY_AGENT,
                version_string,
                consts::API,
                vending_version,
                sdk_version,
                build_device,
                build_product,
                build_build_product,
                release_version,
                build_model,
                build_id,
                consts::IS_WIDE_SCREEN,
                abis
            ),
            sim_operator.clone(),
        ))
    }
}