rustybook-messenger 0.2.1

Messenger client for Rustybook
Documentation
use reqwest::StatusCode;
use reqwest::header::{
    COOKIE,
    HeaderMap,
    HeaderValue,
    USER_AGENT,
};
use rustybook_http::client::{
    Client as HttpClient,
    Request as HttpRequest,
};
use tracing::{
    debug,
    warn,
};

use super::State;
use crate::error::MessengerError;

const BOOTSTRAP_URL: &str = "https://www.facebook.com/";

impl State {
    pub fn base_headers(&self) -> Result<HeaderMap, MessengerError> {
        let mut headers = HeaderMap::new();
        let cookie = HeaderValue::from_str(&self.cookie_header)
            .map_err(|error| MessengerError::State(format!("invalid cookie header: {error}")))?;
        let user_agent = HeaderValue::from_str(&self.user_agent)
            .map_err(|error| MessengerError::State(format!("invalid user agent: {error}")))?;

        headers.insert(COOKIE, cookie);
        headers.insert(USER_AGENT, user_agent);
        headers.insert(
            reqwest::header::ACCEPT,
            HeaderValue::from_static(
                "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            ),
        );
        headers.insert(
            reqwest::header::ACCEPT_LANGUAGE,
            HeaderValue::from_static("en-US,en;q=0.9"),
        );
        headers.insert(
            reqwest::header::ACCEPT_ENCODING,
            HeaderValue::from_static("br, gzip, deflate"),
        );
        headers.insert(
            reqwest::header::HeaderName::from_static("sec-fetch-dest"),
            HeaderValue::from_static("document"),
        );
        headers.insert(
            reqwest::header::HeaderName::from_static("sec-fetch-mode"),
            HeaderValue::from_static("navigate"),
        );
        headers.insert(
            reqwest::header::HeaderName::from_static("sec-fetch-site"),
            HeaderValue::from_static("none"),
        );
        headers.insert(
            reqwest::header::HeaderName::from_static("sec-fetch-user"),
            HeaderValue::from_static("?1"),
        );
        headers.insert(
            reqwest::header::UPGRADE_INSECURE_REQUESTS,
            HeaderValue::from_static("1"),
        );

        Ok(headers)
    }

    pub(super) async fn fetch_bootstrap_html(&self) -> Result<String, MessengerError> {
        let request = HttpRequest::get(BOOTSTRAP_URL).headers(self.base_headers()?);
        let response = self
            .send(
                request,
                HttpRequestMeta {
                    label: "facebook_bootstrap".to_string(),
                    method: "GET".to_string(),
                    url: BOOTSTRAP_URL.to_string(),
                },
            )
            .await?;

        debug!(
            label = "facebook_bootstrap",
            method = "GET",
            url = BOOTSTRAP_URL,
            "bootstrap html fetched"
        );

        Ok(response)
    }

    pub(super) async fn send(
        &self,
        request: HttpRequest,
        context: HttpRequestMeta,
    ) -> Result<String, MessengerError> {
        let http = self
            .http
            .as_ref()
            .ok_or_else(|| MessengerError::State("missing http client".to_string()))?;
        let response = http
            .request(request)
            .await
            .map_err(|error| MessengerError::State(format!("http request failed: {error}")))?;
        let status = response.status;
        let final_url = response.url.to_string();
        let body = response.text;

        if status == StatusCode::OK {
            debug!(
                label = context.label,
                method = context.method,
                url = context.url,
                final_url,
                status = status.as_u16(),
                "http request completed"
            );
        } else {
            warn!(
                label = context.label,
                method = context.method,
                url = context.url,
                final_url,
                status = status.as_u16(),
                "http request returned non-200 status"
            );
        }

        if !status.is_success() {
            return Err(MessengerError::State(format!(
                "http request failed for {} {} with status {}",
                context.method,
                context.url,
                status.as_u16()
            )));
        }

        Ok(body)
    }
}

#[derive(Debug, Clone)]
pub(super) struct HttpRequestMeta {
    pub label: String,
    pub method: String,
    pub url: String,
}

pub(super) fn build_http_client(
    cookie_header: &str,
    user_agent: &str,
    proxy: Option<&str>,
) -> Result<HttpClient, MessengerError> {
    let mut builder = rustybook_http::ClientBuilder::new()
        .max_redirect(10)
        .cookie_header(cookie_header.to_string())
        .user_agent(user_agent.to_string());

    if let Some(proxy_url) = proxy {
        builder = builder.proxy(proxy_url.to_string());
    }

    builder
        .build()
        .map_err(|error| MessengerError::State(format!("failed to build http client: {error}")))
}