use serde_json::Value;
use tail_fin_common::BrowserSession;
use tail_fin_common::TailFinError;
pub async fn resolve_query_id(
session: &BrowserSession,
endpoint: &str,
fallback: &str,
) -> Result<String, TailFinError> {
let js = format!(
r#"
(async () => {{
const operationName = "{endpoint}";
// Strategy 1: Community-maintained queryId mapping (twitter-openapi)
try {{
const ghResp = await fetch(
'https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json'
);
if (ghResp.ok) {{
const data = await ghResp.json();
const entry = data?.[operationName];
if (entry && entry.queryId) return entry.queryId;
}}
}} catch {{}}
// Strategy 2: Scan loaded JS bundles via script tags
try {{
const scripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src);
for (const scriptUrl of scripts) {{
try {{
const text = await (await fetch(scriptUrl)).text();
const re = new RegExp(
'queryId:"([A-Za-z0-9_-]+)"[^{{}}]{{0,200}}operationName:"' + operationName + '"'
);
const match = text.match(re);
if (match) return match[1];
}} catch {{}}
}}
}} catch {{}}
return null;
}})()
"#
);
let result = session.eval(&js).await?;
if let Some(id) = result.as_str() {
if !id.is_empty() {
return Ok(id.to_string());
}
}
Ok(fallback.to_string())
}
pub fn build_graphql_url(
query_id: &str,
endpoint: &str,
variables: &Value,
features: &Value,
) -> String {
let vars_str = serde_json::to_string(variables).unwrap_or_default();
let feats_str = serde_json::to_string(features).unwrap_or_default();
let vars_encoded = urlencoding::encode(&vars_str);
let feats_encoded = urlencoding::encode(&feats_str);
format!(
"/i/api/graphql/{}/{}?variables={}&features={}",
query_id, endpoint, vars_encoded, feats_encoded
)
}
pub fn default_features() -> Value {
serde_json::json!({
"rweb_tipjar_consumption_enabled": true,
"responsive_web_graphql_exclude_directive_enabled": true,
"verified_phone_label_enabled": false,
"creator_subscriptions_tweet_preview_api_enabled": true,
"responsive_web_graphql_timeline_navigation_enabled": true,
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
"communities_web_enable_tweet_community_results_fetch": true,
"c9s_tweet_anatomy_moderator_badge_enabled": true,
"articles_preview_enabled": true,
"responsive_web_edit_tweet_api_enabled": true,
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
"view_counts_everywhere_api_enabled": true,
"longform_notetweets_consumption_enabled": true,
"responsive_web_twitter_article_tweet_consumption_enabled": true,
"tweet_awards_web_tipping_enabled": false,
"creator_subscriptions_quote_tweet_preview_enabled": false,
"freedom_of_speech_not_reach_fetch_enabled": true,
"standardized_nudges_misinfo": true,
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
"rweb_video_timestamps_enabled": true,
"longform_notetweets_rich_text_read_enabled": true,
"longform_notetweets_inline_media_enabled": true,
"responsive_web_enhance_cards_enabled": false
})
}