diff --git a/src/application/image.rs b/src/application/image.rs new file mode 100644 index 0000000..a475e87 --- /dev/null +++ b/src/application/image.rs @@ -0,0 +1,14 @@ +//! Application use-cases for image generation. + +use crate::domain::error::ScrapingError; +use crate::infrastructure::repository::image as Repo; + +pub async fn brat(text: &str) -> Result, ScrapingError> { + Repo::fetch_brat(text).await.map_err(ScrapingError::Http) +} + +pub async fn brat_animated(text: &str) -> Result, ScrapingError> { + Repo::fetch_brat_animated(text) + .await + .map_err(ScrapingError::Http) +} diff --git a/src/application/mod.rs b/src/application/mod.rs index a8a533d..eaea6d5 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -1,6 +1,7 @@ pub mod anime; pub mod anime2; pub mod downloader; +pub mod image; pub mod komik; pub mod misc; pub mod proxy; diff --git a/src/infrastructure/repository/image.rs b/src/infrastructure/repository/image.rs new file mode 100644 index 0000000..2fa27cd --- /dev/null +++ b/src/infrastructure/repository/image.rs @@ -0,0 +1,46 @@ +//! Infrastructure — Image generation endpoints (brat). +//! +//! Ported from Shirokami-API `scraper/image/brat.js`. +//! brat v2 uses the external brat.siputzx.my.id API (returns image bytes). + +use crate::infrastructure::utils::http_client::http_client; +use reqwest::header::USER_AGENT; + +const BRAT_UA: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0 Safari/537.36"; + +fn urlencode(s: &str) -> String { + s.replace('%', "%25") + .replace('&', "%26") + .replace('?', "%3F") +} + +/// Fetch a brat image (static) for the given text. +pub async fn fetch_brat(text: &str) -> Result, String> { + let url = format!("https://brat.siputzx.my.id/image?text={}", urlencode(text)); + fetch_bytes(&url).await +} + +/// Fetch an animated brat GIF for the given text. +pub async fn fetch_brat_animated(text: &str) -> Result, String> { + let url = format!("https://brat.siputzx.my.id/gif?text={}", urlencode(text)); + fetch_bytes(&url).await +} + +async fn fetch_bytes(url: &str) -> Result, String> { + let resp = http_client() + .client() + .get(url) + .header(USER_AGENT, BRAT_UA) + .header("X-Forwarded-For", "104.28.209.35") + .header("X-Real-IP", "104.28.209.35") + .send() + .await + .map_err(|e| format!("HTTP: {}", e))?; + if !resp.status().is_success() { + return Err(format!("HTTP {}", resp.status())); + } + resp.bytes() + .await + .map(|b| b.to_vec()) + .map_err(|e| format!("Body: {}", e)) +} diff --git a/src/infrastructure/repository/mod.rs b/src/infrastructure/repository/mod.rs index 8e46577..2cc010c 100644 --- a/src/infrastructure/repository/mod.rs +++ b/src/infrastructure/repository/mod.rs @@ -1,5 +1,6 @@ pub mod alqanime; pub mod downloader; +pub mod image; pub mod komik; pub mod misc; pub mod otakudesu; diff --git a/src/presentation/handler/image.rs b/src/presentation/handler/image.rs new file mode 100644 index 0000000..b447468 --- /dev/null +++ b/src/presentation/handler/image.rs @@ -0,0 +1,41 @@ +//! Axum handlers for image generation endpoints. +//! +//! Returns raw image bytes with the proper Content-Type. + +use axum::body::Body; +use axum::extract::Query; +use axum::http::{header, StatusCode}; +use axum::response::Response; +use serde::Deserialize; + +use crate::application::image as use_cases; +use crate::presentation::error::AppError; + +#[derive(Deserialize)] +pub struct BratParams { + pub text: Option, +} + +pub async fn brat_handler(Query(params): Query) -> Result { + let text = params + .text + .ok_or_else(|| AppError::BadRequest("text parameter is required".into()))?; + let bytes = use_cases::brat(&text).await?; + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "image/png") + .body(Body::from(bytes)) + .expect("valid response")) +} + +pub async fn brat_animated_handler(Query(params): Query) -> Result { + let text = params + .text + .ok_or_else(|| AppError::BadRequest("text parameter is required".into()))?; + let bytes = use_cases::brat_animated(&text).await?; + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "image/gif") + .body(Body::from(bytes)) + .expect("valid response")) +} diff --git a/src/presentation/handler/mod.rs b/src/presentation/handler/mod.rs index 9e7918b..7316e0d 100644 --- a/src/presentation/handler/mod.rs +++ b/src/presentation/handler/mod.rs @@ -2,6 +2,7 @@ pub mod anime; pub mod anime2; pub mod downloader; pub mod health; +pub mod image; pub mod komik; pub mod misc; pub mod proxy; diff --git a/src/presentation/router.rs b/src/presentation/router.rs index fc50be1..d1cc4d7 100644 --- a/src/presentation/router.rs +++ b/src/presentation/router.rs @@ -349,6 +349,15 @@ pub fn build_router(app_state: Arc) -> anyhow::Result { "/tool/cek-resi", axum::routing::get(crate::presentation::handler::tools::cek_resi_handler), ) + // Image routes + .route( + "/image/brat", + axum::routing::get(crate::presentation::handler::image::brat_handler), + ) + .route( + "/image/brat/animated", + axum::routing::get(crate::presentation::handler::image::brat_animated_handler), + ) // Health .route( "/health",