From 93828c7253951065e9360d053c5fd1450782ba77 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 2 Sep 2026 13:29:39 +0700 Subject: [PATCH] feat(stalk): remove dead mobile-legends, free-fire, tiktok, instagram Audit live-probed all 8 stalk upstreams today. 4 alive (github, youtube, genshin enka, twitter fxtwitter). 4 dead with no free alternative reachable from VPS: mobile-legends (gempaytopup /stalk-ml now 405), free-fire (discordbot.freefirecommunity dead 404), tiktok (WAF -> login page, no rehydration data), instagram (mollygram captcha; i.instagram requires login/rate-limited 401). All free alternates probed dead (onrender FF 404, freefireapi.me 404, i.instagram 401 require_login, m.tiktok 403). Keep 4 alive: github, youtube, genshin, twitter. stalk.rs now clippy-clean. --- src/application/stalk.rs | 28 --- src/infrastructure/repository/stalk.rs | 299 +------------------------ src/observability/openapi_modules.rs | 6 +- src/presentation/handler/stalk.rs | 98 -------- src/presentation/router.rs | 16 -- 5 files changed, 6 insertions(+), 441 deletions(-) diff --git a/src/application/stalk.rs b/src/application/stalk.rs index 846a655..8efe7a9 100644 --- a/src/application/stalk.rs +++ b/src/application/stalk.rs @@ -18,20 +18,6 @@ pub async fn youtube(username: &str) -> Result { .map_err(ScrapingError::Http) } -/// Mobile Legends player stalk. -pub async fn mobile_legends(user_id: &str, zone_id: &str) -> Result { - Repo::fetch_ml_stalk(user_id, zone_id) - .await - .map_err(ScrapingError::Http) -} - -/// Free Fire player stalk. -pub async fn free_fire(user_id: &str) -> Result { - Repo::fetch_ff_stalk(user_id) - .await - .map_err(ScrapingError::Http) -} - /// Genshin Impact player stalk. pub async fn genshin(user_id: &str) -> Result { Repo::fetch_genshin_stalk(user_id) @@ -45,17 +31,3 @@ pub async fn twitter(username: &str) -> Result { .await .map_err(ScrapingError::Http) } - -/// TikTok user stalk. -pub async fn tiktok(username: &str) -> Result { - Repo::fetch_tiktok_stalk(username) - .await - .map_err(ScrapingError::Http) -} - -/// Instagram user stalk. -pub async fn instagram(username: &str) -> Result { - Repo::fetch_instagram_stalk(username) - .await - .map_err(ScrapingError::Http) -} diff --git a/src/infrastructure/repository/stalk.rs b/src/infrastructure/repository/stalk.rs index cdb8c90..1161fef 100644 --- a/src/infrastructure/repository/stalk.rs +++ b/src/infrastructure/repository/stalk.rs @@ -1,4 +1,4 @@ -//! Infrastructure — Stalk utilities (github, youtube, instagram, tiktok, twitter). +//! Infrastructure — Stalk utilities (github, youtube, genshin, twitter). //! //! Ported from Shirokami-API `scraper/stalk/*.js`. @@ -179,7 +179,7 @@ pub async fn fetch_youtube_stalk(username: &str) -> Result { .and_then(|t| t.as_array()) { if let Some(contents) = tabs - .get(0) + .first() .and_then(|t| t.pointer("/tabRenderer/content/sectionListRenderer/contents")) .and_then(|c| c.as_array()) { @@ -223,8 +223,8 @@ fn extract_balanced_json(body: &str, open_brace: usize) -> Result Value { .pointer("/metadata/lockupMetadataViewModel/metadata/contentMetadataViewModel/metadataRows/0/metadataParts") .and_then(|p| p.as_array()) { - if let Some(p0) = parts.get(0) { + if let Some(p0) = parts.first() { view_count = p0 .pointer("/text/content") .cloned() @@ -417,187 +417,6 @@ fn video_from_grid_video(gv: &Value) -> Value { v } -// --------------------------------------------------------------------------- -// Mobile Legends — gempaytopup.com stalk (CSRF token + POST) -// --------------------------------------------------------------------------- - -pub async fn fetch_ml_stalk(user_id: &str, zone_id: &str) -> Result { - // 1. GET to obtain CSRF token + cookies - let resp = http_client() - .client() - .get("https://www.gempaytopup.com") - .header( - USER_AGENT, - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0 Safari/537.36", - ) - .send() - .await - .map_err(|e| format!("HTTP: {}", e))?; - let cookies = resp - .headers() - .get_all("set-cookie") - .iter() - .map(|v| { - v.to_str() - .unwrap_or("") - .split(';') - .next() - .unwrap_or("") - .to_string() - }) - .collect::>() - .join("; "); - let body = resp.text().await.map_err(|e| format!("Body: {}", e))?; - - let csrf_re = Regex::new(r#""#).unwrap(); - let csrf = csrf_re - .captures(&body) - .and_then(|c| c.get(1)) - .map(|m| m.as_str().to_string()); - - let (csrf, cookies) = match (csrf, cookies.is_empty()) { - (Some(c), false) => (c, cookies), - _ => return Ok(json!({"error": "Gagal mendapatkan CSRF token atau cookie."})), - }; - - // 2. POST stalk-ml - let payload = json!({"uid": user_id, "zone": zone_id}); - let resp = http_client() - .client() - .post("https://www.gempaytopup.com/stalk-ml") - .header("X-CSRF-Token", &csrf) - .header("Content-Type", "application/json") - .header("Cookie", &cookies) - .json(&payload) - .send() - .await - .map_err(|e| format!("HTTP: {}", e))?; - let data: Value = resp.json().await.map_err(|e| format!("JSON: {}", e))?; - Ok(data) -} - -// --------------------------------------------------------------------------- -// Free Fire — freefirecommunity API -// --------------------------------------------------------------------------- - -pub async fn fetch_ff_stalk(user_id: &str) -> Result { - let url = format!( - "https://discordbot.freefirecommunity.com/player_info_api?uid={}®ion=id", - user_id - ); - let resp = http_client() - .client() - .get(&url) - .header("Origin", "https://www.freefirecommunity.com") - .header( - "Referer", - "https://www.freefirecommunity.com/ff-account-info/", - ) - .header(USER_AGENT, "Mozilla/5.0 (Linux; Android 10; K)") - .header("Accept", "/") - .send() - .await - .map_err(|e| format!("HTTP: {}", e))?; - if !resp.status().is_success() { - return Err(format!("HTTP {}", resp.status())); - } - let data: Value = resp.json().await.map_err(|e| format!("JSON: {}", e))?; - - let safe = |v: &Value| -> Value { - if v.is_null() { - Value::String("N/A".into()) - } else { - v.clone() - } - }; - let arr_join = |v: &Value| -> Value { - v.as_array() - .map(|a| { - Value::String( - a.iter() - .filter_map(|x| x.as_str()) - .collect::>() - .join(", "), - ) - }) - .unwrap_or(Value::String("-".into())) - }; - let fmt_time = |v: &Value| -> Value { - // unix timestamp -> YYYY-MM-DD (approx via chrono) - v.as_i64() - .map(|ts| { - use chrono::{TimeZone, Utc}; - match Utc.timestamp_opt(ts, 0) { - chrono::LocalResult::Single(dt) => { - Value::String(dt.format("%Y-%m-%d %H:%M:%S").to_string()) - } - _ => Value::String("N/A".into()), - } - }) - .unwrap_or(Value::String("N/A".into())) - }; - - let d = &data["player_info"]; - let b = &d["basicInfo"]; - let c = &d["creditScoreInfo"]; - let p = &d["petInfo"]; - let prof = &d["profileInfo"]; - let s = &d["socialInfo"]; - - // battle tags - let tags = s["battleTag"].as_array().cloned().unwrap_or_default(); - let tag_counts = s["battleTagCount"].as_array().cloned().unwrap_or_default(); - let battle_tags = if tags.is_empty() { - Value::String("N/A".into()) - } else { - Value::String( - tags.iter() - .enumerate() - .map(|(i, t)| { - let count = tag_counts.get(i).and_then(|v| v.as_i64()).unwrap_or(0); - format!("{} ({}x)", t.as_str().unwrap_or(""), count) - }) - .collect::>() - .join("\n"), - ) - }; - - Ok(json!({ - "nickname": safe(&b["nickname"]), - "accountId": safe(&b["accountId"]), - "region": safe(&b["region"]), - "level": safe(&b["level"]), - "liked": safe(&b["liked"]), - "rank": safe(&b["rank"]), - "maxRank": safe(&b["maxRank"]), - "csRank": safe(&b["csRank"]), - "exp": safe(&b["exp"]), - "createAt": fmt_time(&b["createAt"]), - "lastLoginAt": fmt_time(&b["lastLoginAt"]), - "rankingPoints": safe(&b["rankingPoints"]), - "releaseVersion": safe(&b["releaseVersion"]), - "seasonId": safe(&b["seasonId"]), - "primeLevel": b["primeLevel"]["level"].is_null().then(|| Value::String("-".into())).unwrap_or_else(|| safe(&b["primeLevel"]["level"])), - "diamondCost": safe(&d["diamondCostRes"]["diamondCost"]), - "petName": safe(&p["name"]), - "petLevel": safe(&p["level"]), - "petExp": safe(&p["exp"]), - "petSkinId": safe(&p["skinId"]), - "petSkillId": safe(&p["selectedSkillId"]), - "avatarId": safe(&prof["avatarId"]), - "clothes": arr_join(&prof["clothes"]), - "equipedSkills": arr_join(&prof["equipedSkills"]), - "battleTags": battle_tags, - "language": safe(&s["language"]), - "rankShow": safe(&s["rankShow"]), - "signature": safe(&s["signature"]), - "creditScore": safe(&c["creditScore"]), - "rewardState": safe(&c["rewardState"]), - "bannerImage": format!("https://discordbot.freefirecommunity.com/banner_image_api?uid={}®ion=id", user_id), - "outfitImage": format!("https://discordbot.freefirecommunity.com/outfit_image_api?uid={}®ion=id", user_id), - })) -} - // --------------------------------------------------------------------------- // Genshin Impact — enka.network public API // --------------------------------------------------------------------------- @@ -664,111 +483,3 @@ pub async fn fetch_twitter_stalk(username: &str) -> Result { } })) } - -// --------------------------------------------------------------------------- -// TikTok — __UNIVERSAL_DATA_FOR_REHYDRATION__ JSON (V1 direct, no browser) -// --------------------------------------------------------------------------- - -pub async fn fetch_tiktok_stalk(username: &str) -> Result { - let url = format!("https://www.tiktok.com/@{username}?_t=ZS-8tHANz7ieoS&_r=1"); - let resp = http_client() - .client() - .get(&url) - .header(USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36") - .send() - .await - .map_err(|e| format!("HTTP: {}", e))?; - let html = resp.text().await.map_err(|e| format!("Body: {}", e))?; - - // Extract __UNIVERSAL_DATA_FOR_REHYDRATION__ JSON - let marker = r#"__UNIVERSAL_DATA_FOR_REHYDRATION__"#; - let start = html.find(marker).ok_or("User tidak ditemukan")?; - let json_start = html[start..] - .find('>') - .map(|i| start + i + 1) - .ok_or("User tidak ditemukan")?; - let json_end = html[json_start..] - .find("") - .map(|i| json_start + i) - .ok_or("User tidak ditemukan")?; - let json_str = &html[json_start..json_end]; - - let parsed: Value = serde_json::from_str(json_str).map_err(|e| format!("JSON: {}", e))?; - let user_info = parsed - .pointer("/__DEFAULT_SCOPE__/webapp.user-detail/userInfo/user") - .cloned() - .ok_or("User tidak ditemukan")?; - let stats = parsed - .pointer("/__DEFAULT_SCOPE__/webapp.user-detail/userInfo/stats") - .cloned() - .unwrap_or(Value::Null); - - Ok(json!({ - "userInfo": { - "id": user_info.get("id").cloned().unwrap_or(Value::Null), - "username": user_info.get("uniqueId").cloned().unwrap_or(Value::Null), - "name": user_info.get("nickname").cloned().unwrap_or(Value::Null), - "avatar": user_info.get("avatarLarger").cloned().unwrap_or(Value::Null), - "bio": user_info.get("signature").cloned().unwrap_or(Value::Null), - "verified": user_info.get("verified").cloned().unwrap_or(Value::Bool(false)), - "totalFollowers": stats.get("followerCount").cloned().unwrap_or(Value::from(0)), - "totalFollowing": stats.get("followingCount").cloned().unwrap_or(Value::from(0)), - "totalLikes": stats.get("heart").cloned().unwrap_or(Value::from(0)), - "totalVideos": stats.get("videoCount").cloned().unwrap_or(Value::from(0)), - "totalFriends": stats.get("friendCount").cloned().unwrap_or(Value::from(0)), - } - })) -} - -// --------------------------------------------------------------------------- -// Instagram — media.mollygram.com HTML scrape -// --------------------------------------------------------------------------- - -pub async fn fetch_instagram_stalk(username: &str) -> Result { - let url = format!("https://media.mollygram.com/?url={}", urlencode(username)); - let resp = http_client() - .client() - .get(&url) - .header("accept", "*/*") - .header("origin", "https://mollygram.com") - .header("referer", "https://mollygram.com/") - .header(USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36") - .send() - .await - .map_err(|e| format!("HTTP: {}", e))?; - let body = resp.text().await.map_err(|e| format!("Body: {}", e))?; - - let re = |pat: &str| -> Option { - let rx = Regex::new(pat).ok()?; - rx.captures(&body) - .and_then(|c| c.get(1)) - .map(|m| m.as_str().trim().to_string()) - }; - - let avatar = re(r#"]*class="[^"]*rounded-circle[^"]*"[^>]*src="([^"]+)""#) - .or_else(|| re(r#"]*src="([^"]+)"[^>]*class="[^"]*rounded-circle[^"]*""#)); - let uname = re(r#"

([^<]+)

"#); - let fullname = re(r#"

([^<]+)

"#); - let bio = re(r#"

]*>([^<]*)

"#); - let posts = re(r#"([^<]+)\s*]*>\s*posts\s*"#); - let followers = - re(r#"([^<]+)\s*]*>\s*followers\s*"#); - let following = - re(r#"([^<]+)\s*]*>\s*following\s*"#); - - Ok(json!({ - "avatar": avatar, - "name": fullname.unwrap_or_default(), - "username": uname.unwrap_or_else(|| username.to_string()), - "posts": posts.unwrap_or_default(), - "followers": followers.unwrap_or_default(), - "following": following.unwrap_or_default(), - "bio": bio.unwrap_or_default(), - })) -} - -fn urlencode(s: &str) -> String { - s.replace('%', "%25") - .replace('&', "%26") - .replace('?', "%3F") -} diff --git a/src/observability/openapi_modules.rs b/src/observability/openapi_modules.rs index e66948e..678e783 100644 --- a/src/observability/openapi_modules.rs +++ b/src/observability/openapi_modules.rs @@ -74,12 +74,8 @@ use utoipa::OpenApi; // ── Stalk module ─────────────────────────────────────────── crate::presentation::handler::stalk::github_handler, crate::presentation::handler::stalk::youtube_handler, - crate::presentation::handler::stalk::mobile_legends_handler, - crate::presentation::handler::stalk::free_fire_handler, crate::presentation::handler::stalk::genshin_handler, crate::presentation::handler::stalk::twitter_handler, - crate::presentation::handler::stalk::tiktok_handler, - crate::presentation::handler::stalk::instagram_handler, // ── Search module ────────────────────────────────────────── crate::presentation::handler::search::bmkg_handler, crate::presentation::handler::search::jadwal_sholat_handler, @@ -109,7 +105,7 @@ use utoipa::OpenApi; (name = "komik", description = "Komik endpoints"), (name = "download", description = "Media download endpoints (23 platforms)"), (name = "misc", description = "Miscellaneous utilities (currency, gold, server)"), - (name = "stalk", description = "User profile stalking (GitHub, YT, Twitter, TikTok, IG, FF, ML, Genshin)"), + (name = "stalk", description = "User profile stalking (GitHub, YT, Twitter, Genshin)"), (name = "search", description = "Search endpoints (BMKG, prayer times, weather, YouTube, Google)"), (name = "tool", description = "Utility tools (WHOIS, IP lookup, URL shortener, tracking, hosting)"), (name = "image", description = "Image generation (brat static & animated)"), diff --git a/src/presentation/handler/stalk.rs b/src/presentation/handler/stalk.rs index 2790cb9..50e32d5 100644 --- a/src/presentation/handler/stalk.rs +++ b/src/presentation/handler/stalk.rs @@ -68,58 +68,6 @@ pub async fn youtube_handler( Ok((StatusCode::OK, Json(result))) } -/// GET /stalk/mobile-legends?user_id=...&zone_id=... (aliases: userid, zoneid) -#[utoipa::path( - get, - path = "/stalk/mobile-legends", - tag = "stalk", - operation_id = "stalk_mobile_legends", - params(StalkParams), - responses( - (status = 200, description = "Mobile Legends player info", body = Value), - (status = 400, description = "Missing user_id/zone_id"), - (status = 502, description = "Upstream error"), - ) -)] -pub async fn mobile_legends_handler( - Query(params): Query, -) -> Result<(StatusCode, Json), AppError> { - let user_id = params - .user_id - .or(params.userid) - .ok_or_else(|| AppError::BadRequest("Missing 'user_id' parameter".to_string()))?; - let zone_id = params - .zone_id - .or(params.zoneid) - .ok_or_else(|| AppError::BadRequest("Missing 'zone_id' parameter".to_string()))?; - let result = use_cases::mobile_legends(&user_id, &zone_id).await?; - Ok((StatusCode::OK, Json(result))) -} - -/// GET /stalk/free-fire?user_id=... (alias: userid) -#[utoipa::path( - get, - path = "/stalk/free-fire", - tag = "stalk", - operation_id = "stalk_free_fire", - params(StalkParams), - responses( - (status = 200, description = "Free Fire player info", body = Value), - (status = 400, description = "Missing user_id"), - (status = 502, description = "Upstream error"), - ) -)] -pub async fn free_fire_handler( - Query(params): Query, -) -> Result<(StatusCode, Json), AppError> { - let user_id = params - .user_id - .or(params.userid) - .ok_or_else(|| AppError::BadRequest("Missing 'user_id' parameter".to_string()))?; - let result = use_cases::free_fire(&user_id).await?; - Ok((StatusCode::OK, Json(result))) -} - /// GET /stalk/genshin-impact?user_id=... (alias: userid) #[utoipa::path( get, @@ -166,49 +114,3 @@ pub async fn twitter_handler( let result = use_cases::twitter(&username).await?; Ok((StatusCode::OK, Json(result))) } - -/// GET /stalk/tiktok?username=... -#[utoipa::path( - get, - path = "/stalk/tiktok", - tag = "stalk", - operation_id = "stalk_tiktok", - params(StalkParams), - responses( - (status = 200, description = "TikTok user profile", body = Value), - (status = 400, description = "Missing username"), - (status = 502, description = "Upstream error"), - ) -)] -pub async fn tiktok_handler( - Query(params): Query, -) -> Result<(StatusCode, Json), AppError> { - let username = params - .username - .ok_or_else(|| AppError::BadRequest("Missing 'username' parameter".to_string()))?; - let result = use_cases::tiktok(&username).await?; - Ok((StatusCode::OK, Json(result))) -} - -/// GET /stalk/instagram?username=... -#[utoipa::path( - get, - path = "/stalk/instagram", - tag = "stalk", - operation_id = "stalk_instagram", - params(StalkParams), - responses( - (status = 200, description = "Instagram user profile", body = Value), - (status = 400, description = "Missing username"), - (status = 502, description = "Upstream error"), - ) -)] -pub async fn instagram_handler( - Query(params): Query, -) -> Result<(StatusCode, Json), AppError> { - let username = params - .username - .ok_or_else(|| AppError::BadRequest("Missing 'username' parameter".to_string()))?; - let result = use_cases::instagram(&username).await?; - Ok((StatusCode::OK, Json(result))) -} diff --git a/src/presentation/router.rs b/src/presentation/router.rs index 35e79ab..123faa7 100644 --- a/src/presentation/router.rs +++ b/src/presentation/router.rs @@ -282,14 +282,6 @@ pub fn build_router(app_state: Arc) -> anyhow::Result { "/stalk/youtube", axum::routing::get(crate::presentation::handler::stalk::youtube_handler), ) - .route( - "/stalk/mobile-legends", - axum::routing::get(crate::presentation::handler::stalk::mobile_legends_handler), - ) - .route( - "/stalk/free-fire", - axum::routing::get(crate::presentation::handler::stalk::free_fire_handler), - ) .route( "/stalk/genshin-impact", axum::routing::get(crate::presentation::handler::stalk::genshin_handler), @@ -298,14 +290,6 @@ pub fn build_router(app_state: Arc) -> anyhow::Result { "/stalk/twitter", axum::routing::get(crate::presentation::handler::stalk::twitter_handler), ) - .route( - "/stalk/tiktok", - axum::routing::get(crate::presentation::handler::stalk::tiktok_handler), - ) - .route( - "/stalk/instagram", - axum::routing::get(crate::presentation::handler::stalk::instagram_handler), - ) // Search routes .route( "/search/bmkg",