use reqwest::header::HeaderValue;
use rustybook_http::client::Request as HttpRequest;
use serde_json::Value;
use super::State;
use super::http::HttpRequestMeta;
use crate::error::MessengerError;
use crate::http::graphql::strip_json_guard;
impl State {
pub async fn fetch_sequence_id(&self) -> Result<u64, MessengerError> {
let queries = serde_json::json!({
"q0": {
"doc_id": "1349387578499440",
"query_params": {
"limit": 1,
"tags": ["INBOX"],
"before": serde_json::Value::Null,
"includeDeliveryReceipts": false,
"includeSeqID": true
}
}
})
.to_string();
let mut form_fields = vec![
("method", "GET".to_string()),
("response_format", "json".to_string()),
(
"batch_name",
"MessengerGraphQLThreadlistFetcher".to_string(),
),
("queries", queries),
("__user", self.user_id.clone()),
("__a", "1".to_string()),
("__req", "1".to_string()),
];
if let Some(revision) = self.client_revision {
form_fields.push(("__rev", revision.to_string()));
}
if let Some(fb_dtsg) = &self.fb_dtsg {
form_fields.push(("fb_dtsg", fb_dtsg.clone()));
}
if let Some(jazoest) = &self.jazoest {
form_fields.push(("jazoest", jazoest.clone()));
}
if let Some(lsd) = &self.lsd {
form_fields.push(("lsd", lsd.clone()));
}
let body = encode_form_fields(&form_fields);
let mut headers = self.base_headers()?;
headers.insert(reqwest::header::ACCEPT, HeaderValue::from_static("*/*"));
headers.insert(
reqwest::header::HeaderName::from_static("origin"),
HeaderValue::from_static("https://www.facebook.com"),
);
headers.insert(
reqwest::header::REFERER,
HeaderValue::from_static("https://www.facebook.com/"),
);
headers.insert(
reqwest::header::CONTENT_TYPE,
HeaderValue::from_static("application/x-www-form-urlencoded"),
);
let mut request = HttpRequest::post("https://www.facebook.com/api/graphqlbatch/")
.headers(headers)
.body(body.clone());
if let Some(lsd) = &self.lsd {
let lsd_header = HeaderValue::from_str(lsd)
.map_err(|error| MessengerError::State(format!("invalid lsd header: {error}")))?;
request = request.header(
reqwest::header::HeaderName::from_static("x-fb-lsd"),
lsd_header,
);
}
let text = self
.send(
request,
HttpRequestMeta {
label: "graphql_batch_sequence".to_string(),
method: "POST".to_string(),
url: "https://www.facebook.com/api/graphqlbatch/".to_string(),
},
)
.await?;
if text.trim().is_empty() {
return Err(MessengerError::State(
"empty graphql batch response".to_string(),
));
}
extract_sequence_id_from_graphql_response(&text)
}
}
pub(super) fn encode_form_fields(fields: &[(&str, String)]) -> String {
let mut output = String::new();
for (index, (key, value)) in fields.iter().enumerate() {
if index > 0 {
output.push('&');
}
output.push_str(key);
output.push('=');
output.push_str(&url_encode_component(value));
}
output
}
pub(super) fn extract_sequence_id_from_graphql_response(text: &str) -> Result<u64, MessengerError> {
let parsed = strip_json_guard(text).trim();
if parsed.is_empty() {
return Err(MessengerError::State(
"empty graphql batch response".to_string(),
));
}
if let Ok(value) = serde_json::from_str::<Value>(parsed)
&& let Some(sequence_id) = find_sync_sequence_id(&value)
{
return Ok(sequence_id);
}
for line in parsed.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let Ok(value) = serde_json::from_str::<Value>(line) else {
continue;
};
if let Some(sequence_id) = find_sync_sequence_id(&value) {
return Ok(sequence_id);
}
}
Err(MessengerError::State(
"missing sync_sequence_id from graphql response".to_string(),
))
}
fn url_encode_component(value: &str) -> String {
let mut output = String::with_capacity(value.len());
for byte in value.bytes() {
let is_unreserved =
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~');
if is_unreserved {
output.push(byte as char);
} else {
output.push('%');
output.push_str(&format!("{byte:02X}"));
}
}
output
}
fn find_sync_sequence_id(value: &Value) -> Option<u64> {
match value {
Value::Object(map) => {
if let Some(sequence) = map.get("sync_sequence_id")
&& let Some(id) = value_to_u64(sequence)
{
return Some(id);
}
for nested in map.values() {
if let Some(id) = find_sync_sequence_id(nested) {
return Some(id);
}
}
None
}
Value::Array(items) => {
for item in items {
if let Some(id) = find_sync_sequence_id(item) {
return Some(id);
}
}
None
}
_ => None,
}
}
fn value_to_u64(value: &Value) -> Option<u64> {
match value {
Value::Number(number) => number.as_u64(),
Value::String(text) => text.parse::<u64>().ok(),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{
encode_form_fields,
extract_sequence_id_from_graphql_response,
};
#[test]
fn extracts_sequence_id_from_array_response() {
let text = r#"for (;;);[{"o0":{"data":{"viewer":{"message_threads":{"sync_sequence_id":"12345"}}}}}]"#;
let sequence_id = extract_sequence_id_from_graphql_response(text)
.unwrap_or_else(|error| panic!("failed to extract sequence id: {error}"));
assert_eq!(sequence_id, 12345);
}
#[test]
fn extracts_sequence_id_from_json_lines_response() {
let text = r#"for (;;);
{"o0":{"data":{"viewer":{"message_threads":{"sync_sequence_id":"777"}}}}}
{"q0":{"response":{"ok":true}}}
"#;
let sequence_id = extract_sequence_id_from_graphql_response(text)
.unwrap_or_else(|error| panic!("failed to extract sequence id: {error}"));
assert_eq!(sequence_id, 777);
}
#[test]
fn encodes_form_fields() {
let fields = vec![("a", "value".to_string()), ("b", "a+b&c=d".to_string())];
let encoded = encode_form_fields(&fields);
assert_eq!(encoded, "a=value&b=a%2Bb%26c%3Dd");
}
}