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}")))
}