diff --git a/src/observability/openapi_modules.rs b/src/observability/openapi_modules.rs index ce678e1..317bd61 100644 --- a/src/observability/openapi_modules.rs +++ b/src/observability/openapi_modules.rs @@ -4,7 +4,7 @@ use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( paths( - // Anime module handlers + // ── Anime module ────────────────────────────────────────── crate::presentation::handler::anime::anime_index, crate::presentation::handler::anime::genres, crate::presentation::handler::anime::detail_slug, @@ -16,7 +16,7 @@ use utoipa::OpenApi; crate::presentation::handler::anime::search_slug_page, crate::presentation::handler::anime::genre_slug_index, crate::presentation::handler::anime::genre_slug_page, - // Anime2 module handlers + // ── Anime2 module ───────────────────────────────────────── crate::presentation::handler::anime2::index, crate::presentation::handler::anime2::genre_list, crate::presentation::handler::anime2::filter, @@ -28,7 +28,7 @@ use utoipa::OpenApi; crate::presentation::handler::anime2::latest_slug, crate::presentation::handler::anime2::ongoing_anime_slug, crate::presentation::handler::anime2::complete_anime_slug, - // Komik module handlers + // ── Komik module ────────────────────────────────────────── crate::presentation::handler::komik::genre_list, crate::presentation::handler::komik::chapter_slug, crate::presentation::handler::komik::detail_slug, @@ -40,19 +40,95 @@ use utoipa::OpenApi; crate::presentation::handler::komik::popular_slug, crate::presentation::handler::komik::search_slug, crate::presentation::handler::komik::search_slug_page, - // Proxy module handlers + // ── Download module ──────────────────────────────────────── + crate::presentation::handler::downloader::download, + crate::presentation::handler::downloader::detect_platform_handler, + crate::presentation::handler::downloader::download_instagram, + crate::presentation::handler::downloader::download_facebook, + crate::presentation::handler::downloader::download_tiktok, + crate::presentation::handler::downloader::download_youtube, + crate::presentation::handler::downloader::download_youtube_mp3, + crate::presentation::handler::downloader::download_spotify, + crate::presentation::handler::downloader::download_twitter, + crate::presentation::handler::downloader::download_pinterest, + crate::presentation::handler::downloader::download_mega, + crate::presentation::handler::downloader::download_terabox, + crate::presentation::handler::downloader::download_gdrive, + crate::presentation::handler::downloader::download_mediafire, + crate::presentation::handler::downloader::download_pixeldrain, + crate::presentation::handler::downloader::download_threads, + crate::presentation::handler::downloader::download_doodstream, + crate::presentation::handler::downloader::download_krakenfiles, + crate::presentation::handler::downloader::download_danbooru, + crate::presentation::handler::downloader::download_soundcloud, + crate::presentation::handler::downloader::download_dailymotion, + crate::presentation::handler::downloader::download_reddit, + crate::presentation::handler::downloader::download_streamable, + crate::presentation::handler::downloader::download_videy, + crate::presentation::handler::downloader::download_bilibili, + // ── Misc module ──────────────────────────────────────────── + crate::presentation::handler::misc::currency_converter_handler, + crate::presentation::handler::misc::harga_emas_handler, + crate::presentation::handler::misc::kurs_bca_handler, + crate::presentation::handler::misc::server_info_handler, + // ── 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, + // ── Weebs module ─────────────────────────────────────────── + crate::presentation::handler::weebs::anime_info_handler, + crate::presentation::handler::weebs::manga_info_handler, + crate::presentation::handler::weebs::whatanime_handler, + crate::presentation::handler::weebs::sfw_waifu_handler, + crate::presentation::handler::weebs::nsfw_waifu_handler, + // ── Search module ────────────────────────────────────────── + crate::presentation::handler::search::bmkg_handler, + crate::presentation::handler::search::jadwal_sholat_handler, + crate::presentation::handler::search::weather_handler, + crate::presentation::handler::search::google_handler, + crate::presentation::handler::search::yt_handler, + // ── Tool module ──────────────────────────────────────────── + crate::presentation::handler::tools::whois_handler, + crate::presentation::handler::tools::ip_location_handler, + crate::presentation::handler::tools::tinyurl_handler, + crate::presentation::handler::tools::check_hosting_handler, + crate::presentation::handler::tools::hargapangan_handler, + crate::presentation::handler::tools::cek_resi_handler, + // ── Image module ─────────────────────────────────────────── + crate::presentation::handler::image::brat_handler, + crate::presentation::handler::image::brat_animated_handler, + // ── Uploader module ──────────────────────────────────────── + crate::presentation::handler::uploader::ryzencdn_handler, + crate::presentation::handler::uploader::serve_file_handler, + // ── AI module ────────────────────────────────────────────── + crate::presentation::handler::ai::chat_handler, + crate::presentation::handler::ai::gemini_handler, + crate::presentation::handler::ai::image_handler, ), components( schemas( - // Application response wrapper crate::presentation::dto::common::ApiResponse, + crate::presentation::dto::downloader::DownloadResponse, ) ), tags( (name = "anime", description = "Anime endpoints"), (name = "anime2", description = "Anime2 endpoints"), (name = "komik", description = "Komik endpoints"), - (name = "proxy", description = "Proxy 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 = "weebs", description = "Weeb endpoints (anime info, manga, anime search, waifu)"), + (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)"), + (name = "uploader", description = "File upload and serve"), + (name = "ai", description = "AI endpoints (chat, gemini, image generation)"), ) )] pub struct ModuleApiDoc; diff --git a/src/presentation/handler/ai.rs b/src/presentation/handler/ai.rs index 791980e..71c0b7c 100644 --- a/src/presentation/handler/ai.rs +++ b/src/presentation/handler/ai.rs @@ -23,6 +23,18 @@ pub struct AiParams { } /// GET /ai/chat?text=...&prompt=...&model=... +#[utoipa::path( + get, + path = "/ai/chat", + tag = "ai", + operation_id = "ai_chat_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn chat_handler(Query(params): Query) -> Result, AppError> { let text = params .text @@ -32,6 +44,18 @@ pub async fn chat_handler(Query(params): Query) -> Result, } /// GET /ai/gemini?text=...&prompt=...&image_url=... +#[utoipa::path( + get, + path = "/ai/gemini", + tag = "ai", + operation_id = "ai_gemini_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn gemini_handler(Query(params): Query) -> Result, AppError> { let text = params .text @@ -42,6 +66,18 @@ pub async fn gemini_handler(Query(params): Query) -> Result) -> Result, AppError> { let prompt = params .prompt @@ -59,9 +95,9 @@ pub async fn image_handler(Query(params): Query) -> Result, @@ -25,11 +23,24 @@ pub struct DownloadParams { pub quality: Option, } -// ============================================================================ +// ======================================================================== // Handlers -// ============================================================================ +// ======================================================================== -/// All-in-one downloader with auto-detection. +/// All-in-one auto-detect downloader. +/// `/download?url=...` +#[utoipa::path( + get, + path = "/download", + tag = "download", + operation_id = "dl_download", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download( Query(params): Query, ) -> Result, AppError> { @@ -37,10 +48,23 @@ pub async fn download( Ok(Json(DownloadResponse::ok(result))) } -/// Platform detection endpoint. +/// Detect platform from URL. +/// `/detect-platform?url=...` +#[utoipa::path( + get, + path = "/detect-platform", + tag = "download", + operation_id = "dl_detect_platform_handler", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn detect_platform_handler( Query(params): Query, -) -> Result, AppError> { +) -> Result, AppError> { let platform = use_cases::detect_platform(¶ms.url); Ok(Json(serde_json::json!({ "platform": platform, @@ -48,8 +72,20 @@ pub async fn detect_platform_handler( }))) } -/// Generate handler for each downloader platform. -/// Handler for _instagram. +/// Download Instagram media. +/// `/download/instagram?url=...` +#[utoipa::path( + get, + path = "/download/instagram", + tag = "download", + operation_id = "dl_download_instagram", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_instagram( Query(params): Query, ) -> Result, AppError> { @@ -57,7 +93,20 @@ pub async fn download_instagram( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _facebook. +/// Download Facebook video. +/// `/download/facebook?url=...` +#[utoipa::path( + get, + path = "/download/facebook", + tag = "download", + operation_id = "dl_download_facebook", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_facebook( Query(params): Query, ) -> Result, AppError> { @@ -65,7 +114,20 @@ pub async fn download_facebook( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _tiktok. +/// Download TikTok video. +/// `/download/tiktok?url=...` +#[utoipa::path( + get, + path = "/download/tiktok", + tag = "download", + operation_id = "dl_download_tiktok", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_tiktok( Query(params): Query, ) -> Result, AppError> { @@ -73,7 +135,20 @@ pub async fn download_tiktok( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _youtube. +/// Download YouTube video. +/// `/download/youtube?url=...` +#[utoipa::path( + get, + path = "/download/youtube", + tag = "download", + operation_id = "dl_download_youtube", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_youtube( Query(params): Query, ) -> Result, AppError> { @@ -83,7 +158,20 @@ pub async fn download_youtube( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _youtube_mp3. +/// Convert YouTube to MP3. +/// `/download/youtube/mp3?url=...` +#[utoipa::path( + get, + path = "/download/youtube/mp3", + tag = "download", + operation_id = "dl_download_youtube_mp3", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_youtube_mp3( Query(params): Query, ) -> Result, AppError> { @@ -91,7 +179,20 @@ pub async fn download_youtube_mp3( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _spotify. +/// Download Spotify track. +/// `/download/spotify?url=...` +#[utoipa::path( + get, + path = "/download/spotify", + tag = "download", + operation_id = "dl_download_spotify", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_spotify( Query(params): Query, ) -> Result, AppError> { @@ -99,7 +200,20 @@ pub async fn download_spotify( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _twitter. +/// Download Twitter/X media. +/// `/download/twitter?url=...` +#[utoipa::path( + get, + path = "/download/twitter", + tag = "download", + operation_id = "dl_download_twitter", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_twitter( Query(params): Query, ) -> Result, AppError> { @@ -107,7 +221,20 @@ pub async fn download_twitter( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _pinterest. +/// Download Pinterest pin. +/// `/download/pinterest?url=...` +#[utoipa::path( + get, + path = "/download/pinterest", + tag = "download", + operation_id = "dl_download_pinterest", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_pinterest( Query(params): Query, ) -> Result, AppError> { @@ -115,7 +242,20 @@ pub async fn download_pinterest( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _mega. +/// Download from Mega. +/// `/download/mega?url=...` +#[utoipa::path( + get, + path = "/download/mega", + tag = "download", + operation_id = "dl_download_mega", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_mega( Query(params): Query, ) -> Result, AppError> { @@ -123,7 +263,20 @@ pub async fn download_mega( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _terabox. +/// Download from Terabox. +/// `/download/terabox?url=...` +#[utoipa::path( + get, + path = "/download/terabox", + tag = "download", + operation_id = "dl_download_terabox", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_terabox( Query(params): Query, ) -> Result, AppError> { @@ -131,7 +284,20 @@ pub async fn download_terabox( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _gdrive. +/// Download from Google Drive. +/// `/download/gdrive?url=...` +#[utoipa::path( + get, + path = "/download/gdrive", + tag = "download", + operation_id = "dl_download_gdrive", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_gdrive( Query(params): Query, ) -> Result, AppError> { @@ -139,7 +305,20 @@ pub async fn download_gdrive( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _mediafire. +/// Download from MediaFire. +/// `/download/mediafire?url=...` +#[utoipa::path( + get, + path = "/download/mediafire", + tag = "download", + operation_id = "dl_download_mediafire", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_mediafire( Query(params): Query, ) -> Result, AppError> { @@ -147,7 +326,20 @@ pub async fn download_mediafire( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _pixeldrain. +/// Download from Pixeldrain. +/// `/download/pixeldrain?url=...` +#[utoipa::path( + get, + path = "/download/pixeldrain", + tag = "download", + operation_id = "dl_download_pixeldrain", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_pixeldrain( Query(params): Query, ) -> Result, AppError> { @@ -155,7 +347,20 @@ pub async fn download_pixeldrain( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _threads. +/// Download Threads media. +/// `/download/threads?url=...` +#[utoipa::path( + get, + path = "/download/threads", + tag = "download", + operation_id = "dl_download_threads", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_threads( Query(params): Query, ) -> Result, AppError> { @@ -163,7 +368,20 @@ pub async fn download_threads( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _doodstream. +/// Download from DoodStream. +/// `/download/doodstream?url=...` +#[utoipa::path( + get, + path = "/download/doodstream", + tag = "download", + operation_id = "dl_download_doodstream", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_doodstream( Query(params): Query, ) -> Result, AppError> { @@ -171,7 +389,20 @@ pub async fn download_doodstream( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _krakenfiles. +/// Download from KrakenFiles. +/// `/download/krakenfiles?url=...` +#[utoipa::path( + get, + path = "/download/krakenfiles", + tag = "download", + operation_id = "dl_download_krakenfiles", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_krakenfiles( Query(params): Query, ) -> Result, AppError> { @@ -179,7 +410,20 @@ pub async fn download_krakenfiles( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _danbooru. +/// Download from Danbooru. +/// `/download/danbooru?url=...` +#[utoipa::path( + get, + path = "/download/danbooru", + tag = "download", + operation_id = "dl_download_danbooru", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_danbooru( Query(params): Query, ) -> Result, AppError> { @@ -187,7 +431,20 @@ pub async fn download_danbooru( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _soundcloud. +/// Download SoundCloud track. +/// `/download/soundcloud?url=...` +#[utoipa::path( + get, + path = "/download/soundcloud", + tag = "download", + operation_id = "dl_download_soundcloud", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_soundcloud( Query(params): Query, ) -> Result, AppError> { @@ -195,7 +452,20 @@ pub async fn download_soundcloud( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _dailymotion. +/// Download Dailymotion video. +/// `/download/dailymotion?url=...` +#[utoipa::path( + get, + path = "/download/dailymotion", + tag = "download", + operation_id = "dl_download_dailymotion", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_dailymotion( Query(params): Query, ) -> Result, AppError> { @@ -203,7 +473,20 @@ pub async fn download_dailymotion( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _reddit. +/// Download Reddit media. +/// `/download/reddit?url=...` +#[utoipa::path( + get, + path = "/download/reddit", + tag = "download", + operation_id = "dl_download_reddit", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_reddit( Query(params): Query, ) -> Result, AppError> { @@ -211,7 +494,20 @@ pub async fn download_reddit( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _streamable. +/// Download Streamable video. +/// `/download/streamable?url=...` +#[utoipa::path( + get, + path = "/download/streamable", + tag = "download", + operation_id = "dl_download_streamable", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_streamable( Query(params): Query, ) -> Result, AppError> { @@ -219,7 +515,20 @@ pub async fn download_streamable( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _videy. +/// Download Videy video. +/// `/download/videy?url=...` +#[utoipa::path( + get, + path = "/download/videy", + tag = "download", + operation_id = "dl_download_videy", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_videy( Query(params): Query, ) -> Result, AppError> { @@ -227,7 +536,20 @@ pub async fn download_videy( Ok(Json(DownloadResponse::ok(result))) } -/// Handler for _bilibili. +/// Download Bilibili video. +/// `/download/bilibili?url=...` +#[utoipa::path( + get, + path = "/download/bilibili", + tag = "download", + operation_id = "dl_download_bilibili", + params(DownloadParams), + responses( + (status = 200, description = "Download result", body = DownloadResponse), + (status = 400, description = "Bad URL or platform"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn download_bilibili( Query(params): Query, ) -> Result, AppError> { diff --git a/src/presentation/handler/image.rs b/src/presentation/handler/image.rs index b447468..258a24f 100644 --- a/src/presentation/handler/image.rs +++ b/src/presentation/handler/image.rs @@ -7,35 +7,62 @@ use axum::extract::Query; use axum::http::{header, StatusCode}; use axum::response::Response; use serde::Deserialize; +use utoipa::IntoParams; use crate::application::image as use_cases; use crate::presentation::error::AppError; -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams)] pub struct BratParams { pub text: Option, } +/// GET /image/brat?text=Hello +#[utoipa::path( + get, + path = "/image/brat", + tag = "image", + operation_id = "image_brat", + params(BratParams), + responses( + (status = 200, description = "Static brat image (PNG)", body = Vec), + (status = 400, description = "Missing text"), + (status = 502, description = "Upstream error"), + ) +)] 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() + Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "image/png") .body(Body::from(bytes)) - .expect("valid response")) + .map_err(|e| AppError::Internal(format!("build response: {e}"))) } +/// GET /image/brat/animated?text=Hello +#[utoipa::path( + get, + path = "/image/brat/animated", + tag = "image", + operation_id = "image_brat_animated", + params(BratParams), + responses( + (status = 200, description = "Animated brat image (GIF)", body = Vec), + (status = 400, description = "Missing text"), + (status = 502, description = "Upstream error"), + ) +)] 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() + Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "image/gif") .body(Body::from(bytes)) - .expect("valid response")) + .map_err(|e| AppError::Internal(format!("build response: {e}"))) } diff --git a/src/presentation/handler/misc.rs b/src/presentation/handler/misc.rs index d2479da..c4d12e2 100644 --- a/src/presentation/handler/misc.rs +++ b/src/presentation/handler/misc.rs @@ -7,12 +7,13 @@ use axum::http::StatusCode; use axum::Json; use serde::Deserialize; use serde_json::Value; +use utoipa::IntoParams; use crate::application::misc as use_cases; use crate::presentation::error::AppError; /// Query params for currency converter. -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams)] pub struct CurrencyParams { pub amount: Option, pub from: Option, @@ -20,6 +21,17 @@ pub struct CurrencyParams { } /// GET /misc/currency-converter?amount=100&from=USD&to=IDR +#[utoipa::path( + get, + path = "/misc/currency-converter", + tag = "misc", + operation_id = "currency_converter", + params(CurrencyParams), + responses( + (status = 200, description = "Currency conversion result", body = Value), + (status = 502, description = "Upstream error"), + ) +)] pub async fn currency_converter_handler( Query(params): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -32,18 +44,48 @@ pub async fn currency_converter_handler( } /// GET /misc/harga-emas +#[utoipa::path( + get, + path = "/misc/harga-emas", + tag = "misc", + operation_id = "harga_emas", + responses( + (status = 200, description = "Gold prices (Antam)", body = Value), + (status = 502, description = "Upstream error"), + ) +)] pub async fn harga_emas_handler() -> Result<(StatusCode, Json), AppError> { let result = use_cases::harga_emas().await?; Ok((StatusCode::OK, Json(result))) } /// GET /misc/kurs-bca +#[utoipa::path( + get, + path = "/misc/kurs-bca", + tag = "misc", + operation_id = "kurs_bca", + responses( + (status = 200, description = "BCA exchange rates", body = Value), + (status = 502, description = "Upstream error"), + ) +)] pub async fn kurs_bca_handler() -> Result<(StatusCode, Json), AppError> { let result = use_cases::kurs_bca().await?; Ok((StatusCode::OK, Json(result))) } /// GET /misc/server-info +#[utoipa::path( + get, + path = "/misc/server-info", + tag = "misc", + operation_id = "server_info", + responses( + (status = 200, description = "Server info", body = Value), + (status = 502, description = "Upstream error"), + ) +)] pub async fn server_info_handler() -> Result<(StatusCode, Json), AppError> { let result = use_cases::server_info().await?; Ok((StatusCode::OK, Json(result))) diff --git a/src/presentation/handler/search.rs b/src/presentation/handler/search.rs index 61ff7b3..80b8b70 100644 --- a/src/presentation/handler/search.rs +++ b/src/presentation/handler/search.rs @@ -19,17 +19,41 @@ pub struct SearchParams { pub city: Option, } -fn require<'a>(v: Option<&'a String>, name: &str) -> Result { +fn require(v: Option<&String>, name: &str) -> Result { v.cloned() .ok_or_else(|| AppError::BadRequest(format!("Missing '{}' parameter", name))) } /// GET /search/bmkg +#[utoipa::path( + get, + path = "/search/bmkg", + tag = "search", + operation_id = "search_bmkg_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn bmkg_handler() -> Result<(StatusCode, Json), AppError> { Ok((StatusCode::OK, Json(use_cases::bmkg().await?))) } /// GET /search/jadwal-sholat?kota=jakarta +#[utoipa::path( + get, + path = "/search/jadwal-sholat", + tag = "search", + operation_id = "search_jadwal_sholat_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn jadwal_sholat_handler( Query(p): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -38,6 +62,18 @@ pub async fn jadwal_sholat_handler( } /// GET /search/weather?city=jakarta +#[utoipa::path( + get, + path = "/search/weather", + tag = "search", + operation_id = "search_weather_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn weather_handler( Query(p): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -46,6 +82,18 @@ pub async fn weather_handler( } /// GET /search/google?query=rust +#[utoipa::path( + get, + path = "/search/google", + tag = "search", + operation_id = "search_google_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn google_handler( Query(p): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -54,6 +102,18 @@ pub async fn google_handler( } /// GET /search/yt?query=rickroll +#[utoipa::path( + get, + path = "/search/yt", + tag = "search", + operation_id = "search_yt_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn yt_handler( Query(p): Query, ) -> Result<(StatusCode, Json), AppError> { diff --git a/src/presentation/handler/stalk.rs b/src/presentation/handler/stalk.rs index dc09c4d..2790cb9 100644 --- a/src/presentation/handler/stalk.rs +++ b/src/presentation/handler/stalk.rs @@ -7,12 +7,13 @@ use axum::http::StatusCode; use axum::Json; use serde::Deserialize; use serde_json::Value; +use utoipa::IntoParams; use crate::application::stalk as use_cases; use crate::presentation::error::AppError; /// Query params for stalk endpoints. -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams)] pub struct StalkParams { pub username: Option, pub user_id: Option, @@ -22,6 +23,18 @@ pub struct StalkParams { } /// GET /stalk/github?username=asepharyana +#[utoipa::path( + get, + path = "/stalk/github", + tag = "stalk", + operation_id = "stalk_github", + params(StalkParams), + responses( + (status = 200, description = "GitHub user profile", body = Value), + (status = 400, description = "Missing username"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn github_handler( Query(params): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -33,6 +46,18 @@ pub async fn github_handler( } /// GET /stalk/youtube?username= +#[utoipa::path( + get, + path = "/stalk/youtube", + tag = "stalk", + operation_id = "stalk_youtube", + params(StalkParams), + responses( + (status = 200, description = "YouTube channel profile", body = Value), + (status = 400, description = "Missing username"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn youtube_handler( Query(params): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -44,6 +69,18 @@ pub async fn youtube_handler( } /// 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> { @@ -60,6 +97,18 @@ pub async fn mobile_legends_handler( } /// 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> { @@ -72,6 +121,18 @@ pub async fn free_fire_handler( } /// GET /stalk/genshin-impact?user_id=... (alias: userid) +#[utoipa::path( + get, + path = "/stalk/genshin-impact", + tag = "stalk", + operation_id = "stalk_genshin", + params(StalkParams), + responses( + (status = 200, description = "Genshin Impact profile", body = Value), + (status = 400, description = "Missing user_id"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn genshin_handler( Query(params): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -84,6 +145,18 @@ pub async fn genshin_handler( } /// GET /stalk/twitter?username=... +#[utoipa::path( + get, + path = "/stalk/twitter", + tag = "stalk", + operation_id = "stalk_twitter", + params(StalkParams), + responses( + (status = 200, description = "Twitter/X user profile", body = Value), + (status = 400, description = "Missing username"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn twitter_handler( Query(params): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -95,6 +168,18 @@ pub async fn twitter_handler( } /// 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> { @@ -106,6 +191,18 @@ pub async fn tiktok_handler( } /// 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> { diff --git a/src/presentation/handler/tools.rs b/src/presentation/handler/tools.rs index 7139674..5b4a3d9 100644 --- a/src/presentation/handler/tools.rs +++ b/src/presentation/handler/tools.rs @@ -1,35 +1,49 @@ -//! Axum handlers for tools. +//! Axum handlers for tool endpoints. use axum::extract::Query; use axum::http::StatusCode; use axum::Json; use serde::Deserialize; use serde_json::Value; +use utoipa::IntoParams; use crate::application::tools as use_cases; use crate::presentation::error::AppError; -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams)] pub struct DomainParams { pub domain: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams)] pub struct IpParams { pub ip: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams)] pub struct UrlParams { pub url: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams)] pub struct ResiParams { pub resi: Option, pub ekspedisi: Option, } +/// GET /tool/whois?domain=example.com +#[utoipa::path( + get, + path = "/tool/whois", + tag = "tool", + operation_id = "tool_whois", + params(DomainParams), + responses( + (status = 200, description = "WHOIS info", body = Value), + (status = 400, description = "Missing domain"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn whois_handler( Query(params): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -39,6 +53,19 @@ pub async fn whois_handler( Ok((StatusCode::OK, Json(use_cases::whois(&domain).await?))) } +/// GET /tool/ip-location?ip=1.1.1.1 +#[utoipa::path( + get, + path = "/tool/ip-location", + tag = "tool", + operation_id = "tool_ip_location", + params(IpParams), + responses( + (status = 200, description = "IP geolocation", body = Value), + (status = 400, description = "Missing IP"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn ip_location_handler( Query(params): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -48,6 +75,19 @@ pub async fn ip_location_handler( Ok((StatusCode::OK, Json(use_cases::ip_location(&ip).await?))) } +/// GET /tool/tinyurl?url=https://example.com +#[utoipa::path( + get, + path = "/tool/tinyurl", + tag = "tool", + operation_id = "tool_tinyurl", + params(UrlParams), + responses( + (status = 200, description = "Shortened URL", body = Value), + (status = 400, description = "Missing URL"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn tinyurl_handler( Query(params): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -57,6 +97,19 @@ pub async fn tinyurl_handler( Ok((StatusCode::OK, Json(use_cases::tinyurl(&url).await?))) } +/// GET /tool/check-hosting?domain=example.com +#[utoipa::path( + get, + path = "/tool/check-hosting", + tag = "tool", + operation_id = "tool_check_hosting", + params(DomainParams), + responses( + (status = 200, description = "Hosting info", body = Value), + (status = 400, description = "Missing domain"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn check_hosting_handler( Query(params): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -69,10 +122,34 @@ pub async fn check_hosting_handler( )) } +/// GET /tool/hargapangan +#[utoipa::path( + get, + path = "/tool/hargapangan", + tag = "tool", + operation_id = "tool_hargapangan", + responses( + (status = 200, description = "Food prices", body = Value), + (status = 502, description = "Upstream error"), + ) +)] pub async fn hargapangan_handler() -> Result<(StatusCode, Json), AppError> { Ok((StatusCode::OK, Json(use_cases::hargapangan().await?))) } +/// GET /tool/cek-resi?resi=...&ekspedisi=... +#[utoipa::path( + get, + path = "/tool/cek-resi", + tag = "tool", + operation_id = "tool_cek_resi", + params(ResiParams), + responses( + (status = 200, description = "Shipment tracking", body = Value), + (status = 400, description = "Missing resi"), + (status = 502, description = "Upstream error"), + ) +)] pub async fn cek_resi_handler( Query(params): Query, ) -> Result<(StatusCode, Json), AppError> { diff --git a/src/presentation/handler/uploader.rs b/src/presentation/handler/uploader.rs index e14622e..2f4eee6 100644 --- a/src/presentation/handler/uploader.rs +++ b/src/presentation/handler/uploader.rs @@ -14,6 +14,18 @@ use crate::application::uploader as use_cases; use crate::presentation::error::AppError; /// POST /uploader/ryzencdn — accept a multipart file upload. +#[utoipa::path( + post, + path = "/uploader/ryzencdn", + tag = "uploader", + operation_id = "uploader_ryzencdn_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn ryzencdn_handler(mut multipart: Multipart) -> Result, AppError> { // Read the first file field. let mut file_name = String::from(""); @@ -47,12 +59,24 @@ pub async fn ryzencdn_handler(mut multipart: Multipart) -> Result, A } /// GET /uploader/file/:name — serve an uploaded file. +#[utoipa::path( + get, + path = "/uploader/file/:name", + tag = "uploader", + operation_id = "uploader_serve_file_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn serve_file_handler(Path(name): Path) -> Result, AppError> { let (bytes, mime) = use_cases::serve(&name)?; - Ok(Response::builder() + Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, mime) .header(header::CACHE_CONTROL, "public, max-age=86400") .body(Body::from(bytes)) - .unwrap()) + .map_err(|e| AppError::Internal(format!("build response: {e}"))) } diff --git a/src/presentation/handler/weebs.rs b/src/presentation/handler/weebs.rs index bb06d9d..7f1bd58 100644 --- a/src/presentation/handler/weebs.rs +++ b/src/presentation/handler/weebs.rs @@ -19,12 +19,24 @@ pub struct WeebsParams { pub tag: Option, } -fn require<'a>(v: Option<&'a String>, name: &str) -> Result { +fn require(v: Option<&String>, name: &str) -> Result { v.cloned() .ok_or_else(|| AppError::BadRequest(format!("Missing '{}' parameter", name))) } /// GET /weebs/anime-info?query=naruto +#[utoipa::path( + get, + path = "/weebs/anime-info", + tag = "weebs", + operation_id = "weebs_anime_info_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn anime_info_handler( Query(p): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -33,6 +45,18 @@ pub async fn anime_info_handler( } /// GET /weebs/manga-info?query=one%20piece +#[utoipa::path( + get, + path = "/weebs/manga-info", + tag = "weebs", + operation_id = "weebs_manga_info_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn manga_info_handler( Query(p): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -41,6 +65,18 @@ pub async fn manga_info_handler( } /// GET /weebs/whatanime?url= +#[utoipa::path( + get, + path = "/weebs/whatanime", + tag = "weebs", + operation_id = "weebs_whatanime_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn whatanime_handler( Query(p): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -49,6 +85,18 @@ pub async fn whatanime_handler( } /// GET /weebs/sfw-waifu?tag=waifu +#[utoipa::path( + get, + path = "/weebs/sfw-waifu", + tag = "weebs", + operation_id = "weebs_sfw_waifu_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn sfw_waifu_handler( Query(p): Query, ) -> Result<(StatusCode, Json), AppError> { @@ -57,6 +105,18 @@ pub async fn sfw_waifu_handler( } /// GET /weebs/nsfw-waifu?tag=waifu +#[utoipa::path( + get, + path = "/weebs/nsfw-waifu", + tag = "weebs", + operation_id = "weebs_nsfw_waifu_handler", + responses( + (status = 200, description = "OK", body = serde_json::Value), + (status = 400, description = "Bad Request"), + (status = 502, description = "Upstream error"), + ) +)] + pub async fn nsfw_waifu_handler( Query(p): Query, ) -> Result<(StatusCode, Json), AppError> {