feat(tools): add whois(RDAP), iplocation, tinyurl, check-hosting, cek-resi, hargapangan

Ported from Shirokami-API scraper/tool/*.js:
- /tool/whois - RDAP lookup (whois.com HTML .df-raw is dead/JS-rendered since 2026; RDAP returns structured register/expiry/registrar data)
- /tool/iplocation - ipapi.co JSON
- /tool/tinyurl - tinyurl.com API
- /tool/check-hosting - hosting-checker.net API
- /tool/cek-resi - cekresi.com AES-128-CBC encrypted form POST
- /tool/hargapangan - Bapanas government API (upstream unreachable from VPS)

Fixes: scraper crate has no :has-text() pseudo-class -> replaced history-table scan with 2-cell-row detection; Html held across .await makes future non-Send -> scoped doc parsing
This commit is contained in:
asepharyana
2026-09-01 20:12:52 +07:00
parent 7ffc1bdd81
commit 278d4fe32a
7 changed files with 586 additions and 0 deletions
+1
View File
@@ -7,4 +7,5 @@ pub mod misc;
pub mod proxy;
pub mod search;
pub mod stalk;
pub mod tools;
pub mod weebs;
+85
View File
@@ -0,0 +1,85 @@
//! Axum handlers for tools.
use axum::extract::Query;
use axum::http::StatusCode;
use axum::Json;
use serde::Deserialize;
use serde_json::Value;
use crate::application::tools as use_cases;
use crate::presentation::error::AppError;
#[derive(Deserialize)]
pub struct DomainParams {
pub domain: Option<String>,
}
#[derive(Deserialize)]
pub struct IpParams {
pub ip: Option<String>,
}
#[derive(Deserialize)]
pub struct UrlParams {
pub url: Option<String>,
}
#[derive(Deserialize)]
pub struct ResiParams {
pub resi: Option<String>,
pub ekspedisi: Option<String>,
}
pub async fn whois_handler(
Query(params): Query<DomainParams>,
) -> Result<(StatusCode, Json<Value>), AppError> {
let domain = params
.domain
.ok_or_else(|| AppError::BadRequest("domain parameter is required".into()))?;
Ok((StatusCode::OK, Json(use_cases::whois(&domain).await?)))
}
pub async fn ip_location_handler(
Query(params): Query<IpParams>,
) -> Result<(StatusCode, Json<Value>), AppError> {
let ip = params
.ip
.ok_or_else(|| AppError::BadRequest("ip parameter is required".into()))?;
Ok((StatusCode::OK, Json(use_cases::ip_location(&ip).await?)))
}
pub async fn tinyurl_handler(
Query(params): Query<UrlParams>,
) -> Result<(StatusCode, Json<Value>), AppError> {
let url = params
.url
.ok_or_else(|| AppError::BadRequest("url parameter is required".into()))?;
Ok((StatusCode::OK, Json(use_cases::tinyurl(&url).await?)))
}
pub async fn check_hosting_handler(
Query(params): Query<DomainParams>,
) -> Result<(StatusCode, Json<Value>), AppError> {
let domain = params
.domain
.ok_or_else(|| AppError::BadRequest("domain parameter is required".into()))?;
Ok((
StatusCode::OK,
Json(use_cases::check_hosting(&domain).await?),
))
}
pub async fn hargapangan_handler() -> Result<(StatusCode, Json<Value>), AppError> {
Ok((StatusCode::OK, Json(use_cases::hargapangan().await?)))
}
pub async fn cek_resi_handler(
Query(params): Query<ResiParams>,
) -> Result<(StatusCode, Json<Value>), AppError> {
let ResiParams { resi, ekspedisi } = params;
let resi = resi.ok_or_else(|| AppError::BadRequest("resi parameter is required".into()))?;
Ok((
StatusCode::OK,
Json(use_cases::cek_resi(resi, ekspedisi).await?),
))
}
+25
View File
@@ -324,6 +324,31 @@ pub fn build_router(app_state: Arc<AppState>) -> anyhow::Result<Router> {
"/search/yt",
axum::routing::get(crate::presentation::handler::search::yt_handler),
)
// Tool routes
.route(
"/tool/whois",
axum::routing::get(crate::presentation::handler::tools::whois_handler),
)
.route(
"/tool/iplocation",
axum::routing::get(crate::presentation::handler::tools::ip_location_handler),
)
.route(
"/tool/tinyurl",
axum::routing::get(crate::presentation::handler::tools::tinyurl_handler),
)
.route(
"/tool/check-hosting",
axum::routing::get(crate::presentation::handler::tools::check_hosting_handler),
)
.route(
"/tool/hargapangan",
axum::routing::get(crate::presentation::handler::tools::hargapangan_handler),
)
.route(
"/tool/cek-resi",
axum::routing::get(crate::presentation::handler::tools::cek_resi_handler),
)
// Health
.route(
"/health",