From 5470d74921512ed1878b4372389e5b0f5d08e8a5 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 2 Sep 2026 13:22:00 +0700 Subject: [PATCH] feat: remove ai, uploader, and weebs modules Remove all AI chat/gemini/image endpoints, ryzencdn uploader/file-serve endpoints, and weebs anime-info/manga-info/whatanime/waifu endpoints. 14 files changed, 1032 lines deleted. cargo check clean, all tests pass. --- src/application/ai.rs | 36 --- src/application/mod.rs | 3 - src/application/uploader.rs | 13 -- src/application/weebs.rs | 35 --- src/infrastructure/repository/ai.rs | 272 ---------------------- src/infrastructure/repository/mod.rs | 3 - src/infrastructure/repository/uploader.rs | 102 -------- src/infrastructure/repository/weebs.rs | 196 ---------------- src/observability/openapi_modules.rs | 16 -- src/presentation/handler/ai.rs | 103 -------- src/presentation/handler/mod.rs | 3 - src/presentation/handler/uploader.rs | 82 ------- src/presentation/handler/weebs.rs | 125 ---------- src/presentation/router.rs | 43 ---- 14 files changed, 1032 deletions(-) delete mode 100644 src/application/ai.rs delete mode 100644 src/application/uploader.rs delete mode 100644 src/application/weebs.rs delete mode 100644 src/infrastructure/repository/ai.rs delete mode 100644 src/infrastructure/repository/uploader.rs delete mode 100644 src/infrastructure/repository/weebs.rs delete mode 100644 src/presentation/handler/ai.rs delete mode 100644 src/presentation/handler/uploader.rs delete mode 100644 src/presentation/handler/weebs.rs diff --git a/src/application/ai.rs b/src/application/ai.rs deleted file mode 100644 index 103d020..0000000 --- a/src/application/ai.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Application use-cases for AI endpoints (Pollinations). - -use crate::domain::error::ScrapingError; -use crate::infrastructure::repository::ai as Repo; -use serde_json::Value; - -pub async fn chat( - text: String, - prompt: String, - model: Option, -) -> Result { - Repo::chat(text, prompt, model) - .await - .map_err(ScrapingError::Http) -} - -pub async fn gemini( - text: String, - prompt: String, - image_url: Option, -) -> Result { - Repo::gemini(text, prompt, image_url) - .await - .map_err(ScrapingError::Http) -} - -pub async fn image( - prompt: String, - model: String, - width: u32, - height: u32, -) -> Result, ScrapingError> { - Repo::image(prompt, model, width, height) - .await - .map_err(ScrapingError::Http) -} diff --git a/src/application/mod.rs b/src/application/mod.rs index 48f4771..c3f4853 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -1,4 +1,3 @@ -pub mod ai; pub mod anime; pub mod anime2; pub mod downloader; @@ -9,5 +8,3 @@ pub mod proxy; pub mod search; pub mod stalk; pub mod tools; -pub mod uploader; -pub mod weebs; diff --git a/src/application/uploader.rs b/src/application/uploader.rs deleted file mode 100644 index ecce741..0000000 --- a/src/application/uploader.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Application use-cases for the uploader. - -use crate::domain::error::ScrapingError; -use crate::infrastructure::repository::uploader as Repo; -use serde_json::Value; - -pub fn upload(buffer: &[u8], file_name: &str) -> Result { - Repo::save_file(buffer, file_name).map_err(ScrapingError::Http) -} - -pub fn serve(file_name: &str) -> Result<(Vec, String), ScrapingError> { - Repo::read_file(file_name).map_err(ScrapingError::Http) -} diff --git a/src/application/weebs.rs b/src/application/weebs.rs deleted file mode 100644 index 345d0f4..0000000 --- a/src/application/weebs.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Application use-cases for weebs (anime/manga info, waifu, whatanime). - -use crate::domain::error::ScrapingError; -use crate::infrastructure::repository::weebs as Repo; -use serde_json::Value; - -pub async fn anime_info(query: &str) -> Result { - Repo::fetch_anime_info(query) - .await - .map_err(ScrapingError::Http) -} - -pub async fn manga_info(query: &str) -> Result { - Repo::fetch_manga_info(query) - .await - .map_err(ScrapingError::Http) -} - -pub async fn whatanime(url: &str) -> Result { - Repo::fetch_whatanime(url) - .await - .map_err(ScrapingError::Http) -} - -pub async fn sfw_waifu(tag: &str) -> Result { - Repo::fetch_sfw_waifu(tag) - .await - .map_err(ScrapingError::Http) -} - -pub async fn nsfw_waifu(tag: &str) -> Result { - Repo::fetch_nsfw_waifu(tag) - .await - .map_err(ScrapingError::Http) -} diff --git a/src/infrastructure/repository/ai.rs b/src/infrastructure/repository/ai.rs deleted file mode 100644 index 690d292..0000000 --- a/src/infrastructure/repository/ai.rs +++ /dev/null @@ -1,272 +0,0 @@ -//! Infrastructure — AI endpoints via Pollinations.ai (free, no API key). -//! -//! Ported from Shirokami-API `scraper/ai/pollinationsai-{text,image,gemini}.js`. -//! All three use the public Pollinations API: -//! - text: POST https://text.pollinations.ai/openai (OpenAI-compatible) -//! - gemini: POST https://text.pollinations.ai/openai with model=gemini -//! - image: GET https://image.pollinations.ai/prompt/{prompt} -//! Retry logic follows the source: try Bearer token (hardcoded in source), -//! fall back to Referer when 401/403 (no key needed). - -use crate::infrastructure::utils::http_client::http_client; -use serde_json::{json, Value}; - -const TEXT_URL: &str = "https://text.pollinations.ai/openai"; -const IMAGE_URL: &str = "https://image.pollinations.ai/prompt"; -// The source ships a hardcoded token; on 401/403 it falls back to Referer-only. -// We skip the token and use Referer directly (anonymous tier) — same result, -// no secret embedded in the binary. -const REFERER: &str = "https://api.ryzumi.vip"; - -fn ua() -> &'static str { - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" -} - -fn build_messages(system: &str, text: &str, image_data_url: Option) -> Vec { - let mut messages = Vec::new(); - if !system.is_empty() { - messages.push(json!({"role": "system", "content": system})); - } - if let Some(img) = image_data_url { - let mut content = Vec::new(); - if !text.is_empty() { - content.push(json!({"type": "text", "text": text})); - } - content.push(json!({"type": "image_url", "image_url": {"url": img}})); - messages.push(json!({"role": "user", "content": content})); - } else { - messages.push(json!({"role": "user", "content": text})); - } - messages -} - -/// Chat via Pollinations (OpenAI-compatible). -/// Default model "openai" (the source's "gpt-5-nano" no longer exists on -/// Pollinations and 404s; fallback to "openai" on any error, incl. 404). -pub async fn chat(text: String, prompt: String, model: Option) -> Result { - let model = model.unwrap_or_else(|| "openai".to_string()); - let system = if prompt.trim().is_empty() { - String::new() - } else { - format!("{}\nConversation history:", prompt) - }; - let messages = build_messages(&system, &text, None); - - let body = json!({ - "model": model, - "messages": messages, - "referrer": REFERER, - }); - - let resp = http_client() - .client() - .post(TEXT_URL) - .header("Content-Type", "application/json") - .header("Referer", REFERER) - .header("User-Agent", ua()) - .json(&body) - .send() - .await - .map_err(|e| format!("HTTP: {e}"))?; - - if !resp.status().is_success() { - // Match source: on failure retry with model "openai" - let status = resp.status(); - if status == reqwest::StatusCode::UNAUTHORIZED - || status == reqwest::StatusCode::FORBIDDEN - || status == reqwest::StatusCode::NOT_FOUND - || status == reqwest::StatusCode::PAYMENT_REQUIRED - { - let body2 = json!({ - "model": "openai", - "messages": messages, - "referrer": REFERER, - }); - let resp2 = http_client() - .client() - .post(TEXT_URL) - .header("Content-Type", "application/json") - .header("Referer", REFERER) - .header("User-Agent", ua()) - .json(&body2) - .send() - .await - .map_err(|e| format!("HTTP retry: {e}"))?; - return parse_text_response(resp2).await; - } - return Err(format!("API error {status}")); - } - - parse_text_response(resp).await -} - -/// Gemini chat via Pollinations (model "gemini"). -pub async fn gemini( - text: String, - prompt: String, - image_url: Option, -) -> Result { - let system = if prompt.trim().is_empty() { - String::new() - } else { - format!("{}\nConversation history:", prompt) - }; - - // If an image URL is given, fetch it and convert to a data URL (source behavior). - let image_data_url = match &image_url { - Some(url) if !url.trim().is_empty() => Some(fetch_data_url(url).await?), - _ => None, - }; - - let messages = build_messages(&system, &text, image_data_url); - - let body = json!({ - "model": "gemini", - "messages": messages, - "referrer": REFERER, - }); - - let resp = http_client() - .client() - .post(TEXT_URL) - .header("Content-Type", "application/json") - .header("Referer", REFERER) - .header("User-Agent", ua()) - .json(&body) - .send() - .await - .map_err(|e| format!("HTTP: {e}"))?; - - if !resp.status().is_success() { - let status = resp.status(); - if status == reqwest::StatusCode::UNAUTHORIZED - || status == reqwest::StatusCode::FORBIDDEN - || status == reqwest::StatusCode::NOT_FOUND - || status == reqwest::StatusCode::PAYMENT_REQUIRED - { - let body2 = json!({ - "model": "openai", - "messages": messages, - "referrer": REFERER, - }); - let resp2 = http_client() - .client() - .post(TEXT_URL) - .header("Content-Type", "application/json") - .header("Referer", REFERER) - .header("User-Agent", ua()) - .json(&body2) - .send() - .await - .map_err(|e| format!("HTTP retry: {e}"))?; - return parse_text_response(resp2).await; - } - return Err(format!("API error {status}")); - } - - parse_text_response(resp).await -} - -/// Image generation via Pollinations. Returns raw image bytes. -pub async fn image( - prompt: String, - model: String, - width: u32, - height: u32, -) -> Result, String> { - let model = if model.is_empty() { - "flux".to_string() - } else { - model - }; - let valid = ["flux", "kontext", "turbo"]; - if !valid.contains(&model.as_str()) { - return Err(format!( - "Invalid model. Available models: {}", - valid.join(", ") - )); - } - - let url = format!( - "{}/{}?model={}&width={}&height={}&nologo=true&private=false&enhance=false&safe=false&referrer={}", - IMAGE_URL, - urlencode(&prompt), - model, - width, - height, - urlencode(REFERER) - ); - - let resp = http_client() - .client() - .get(&url) - .header("Referer", REFERER) - .header("User-Agent", ua()) - .send() - .await - .map_err(|e| format!("HTTP: {e}"))?; - - if !resp.status().is_success() { - return Err(format!("Image API error {}", resp.status())); - } - - let bytes = resp.bytes().await.map_err(|e| format!("Body: {e}"))?; - Ok(bytes.to_vec()) -} - -async fn parse_text_response(resp: reqwest::Response) -> Result { - let data: Value = resp.json().await.map_err(|e| format!("JSON: {e}"))?; - let content = data - .pointer("/choices/0/message/content") - .or_else(|| data.get("message")) - .cloned() - .unwrap_or_else(|| Value::String("No response from assistant.".into())); - Ok(json!({"success": true, "result": content})) -} - -async fn fetch_data_url(url: &str) -> Result { - if url.starts_with("data:") { - return Ok(url.to_string()); - } - let resp = http_client() - .client() - .get(url) - .header("User-Agent", ua()) - .send() - .await - .map_err(|e| format!("Fetch image failed: {e}"))?; - if !resp.status().is_success() { - return Err(format!("Fetch image failed: {}", resp.status())); - } - let ct = resp - .headers() - .get(reqwest::header::CONTENT_TYPE) - .and_then(|v| v.to_str().ok()) - .unwrap_or("image/jpeg") - .to_string(); - let ext = ct - .split('/') - .nth(1) - .map(|s| s.split('+').next().unwrap_or("jpeg").to_string()) - .unwrap_or_else(|| "jpeg".to_string()); - let bytes = resp.bytes().await.map_err(|e| format!("Body: {e}"))?; - use base64::Engine; - Ok(format!( - "data:image/{};base64,{}", - ext, - base64::engine::general_purpose::STANDARD.encode(bytes) - )) -} - -fn urlencode(s: &str) -> String { - let mut out = String::new(); - for b in s.bytes() { - match b { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { - out.push(b as char) - } - _ => out.push_str(&format!("%{:02X}", b)), - } - } - out -} diff --git a/src/infrastructure/repository/mod.rs b/src/infrastructure/repository/mod.rs index 676799e..71c4442 100644 --- a/src/infrastructure/repository/mod.rs +++ b/src/infrastructure/repository/mod.rs @@ -1,4 +1,3 @@ -pub mod ai; pub mod alqanime; pub mod downloader; pub mod image; @@ -10,8 +9,6 @@ pub mod proxy; pub mod search; pub mod stalk; pub mod tools; -pub mod uploader; -pub mod weebs; pub use alqanime::AlqanimeRepository; pub use downloader::DownloaderRepository; diff --git a/src/infrastructure/repository/uploader.rs b/src/infrastructure/repository/uploader.rs deleted file mode 100644 index fd82b97..0000000 --- a/src/infrastructure/repository/uploader.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Infrastructure — Uploader utilities. -//! -//! The Shirokami-API source uploads to Ryzumi S3 (s3.ryzumi.vip) which is -//! DNS-dead from this VPS. This implementation stores uploaded files to a -//! local directory and serves them back, keeping the source's response shape: -//! `{ success, url, fileName, size }`. - -use std::path::{Path, PathBuf}; - -use fastrand; -use serde_json::{json, Value}; - -/// Directory where uploaded files are stored. -const UPLOAD_DIR: &str = "/var/lib/scraper/uploads"; - -/// Base URL prefix used in the returned `url` field. -fn public_base() -> String { - "/uploader/file".to_string() -} - -/// Save an uploaded buffer to disk with a random filename + detected extension. -pub fn save_file(buffer: &[u8], file_name: &str) -> Result { - if buffer.is_empty() { - return Err("Invalid content: buffer is required".into()); - } - - let dir = PathBuf::from(UPLOAD_DIR); - std::fs::create_dir_all(&dir).map_err(|e| format!("mkdir: {e}"))?; - - // Detect extension: prefer the provided filename, else infer from magic. - let extension = if file_name.contains('.') { - file_name.split('.').last().unwrap_or("").to_lowercase() - } else { - infer_extension(buffer).to_string() - }; - - if extension.is_empty() { - return Err("Unable to determine file extension".into()); - } - - let random = hex::encode(&fastrand::u64(..).to_le_bytes()); - let key = format!("{random}.{extension}"); - let path = dir.join(&key); - - std::fs::write(&path, buffer).map_err(|e| format!("write: {e}"))?; - - let url = format!("{}/{}", public_base(), key); - Ok(json!({ - "success": true, - "url": url, - "fileName": key, - "size": buffer.len(), - })) -} - -/// Serve the file bytes for a given filename. -pub fn read_file(file_name: &str) -> Result<(Vec, String), String> { - // Prevent path traversal. - let safe = Path::new(file_name) - .file_name() - .and_then(|s| s.to_str()) - .ok_or("invalid filename")?; - let path = PathBuf::from(UPLOAD_DIR).join(safe); - let bytes = std::fs::read(&path).map_err(|e| format!("read: {e}"))?; - let mime = infer_mime(safe).to_string(); - Ok((bytes, mime)) -} - -fn infer_extension(buf: &[u8]) -> &'static str { - if buf.starts_with(b"\x89PNG\r\n\x1a\n") { - "png" - } else if buf.starts_with(b"\xff\xd8\xff") { - "jpg" - } else if buf.starts_with(b"GIF87a") || buf.starts_with(b"GIF89a") { - "gif" - } else if buf.starts_with(b"RIFF") && &buf[8..12] == b"WEBP" { - "webp" - } else if buf.starts_with(b"%PDF") { - "pdf" - } else if buf.starts_with(b"PK\x03\x04") { - "zip" - } else { - "bin" - } -} - -fn infer_mime(name: &str) -> &'static str { - let ext = name.rsplit('.').next().unwrap_or(""); - match ext { - "png" => "image/png", - "jpg" | "jpeg" => "image/jpeg", - "gif" => "image/gif", - "webp" => "image/webp", - "pdf" => "application/pdf", - "zip" => "application/zip", - "txt" => "text/plain", - "mp4" => "video/mp4", - "webm" => "video/webm", - "mp3" => "audio/mpeg", - _ => "application/octet-stream", - } -} diff --git a/src/infrastructure/repository/weebs.rs b/src/infrastructure/repository/weebs.rs deleted file mode 100644 index 92be188..0000000 --- a/src/infrastructure/repository/weebs.rs +++ /dev/null @@ -1,196 +0,0 @@ -//! Infrastructure — Weebs endpoints (anime/manga info, waifu pics, whatanime). -//! -//! Ported from Shirokami-API `scraper/weebs/*.js`. These are public JSON -//! APIs that we proxy/forward (jikan.moe, waifu.pics, trace.moe). - -use crate::infrastructure::utils::http_client::http_client; -use reqwest::header::USER_AGENT; -use serde_json::{json, Value}; - -/// Anime info from jikan.moe, ported from anime-search's `getAnimeInfo`. -pub async fn fetch_anime_info(query: &str) -> Result { - let url = format!("https://api.jikan.moe/v4/anime?q={}", urlencode(query)); - let data = get_json(&url).await?; - - let arr = data - .get("data") - .and_then(|d| d.as_array()) - .cloned() - .unwrap_or_default(); - if arr.is_empty() { - return Ok(json!({ "error": "Anime tidak ditemukan" })); - } - let anime = &arr[0]; - - let judul = anime - .get("titles") - .and_then(|t| t.as_array()) - .and_then(|t| t.first()) - .and_then(|t| t.get("title")) - .and_then(|t| t.as_str()) - .unwrap_or("Title not available"); - let genrenya = join_names(anime.get("genres")); - - Ok(json!({ - "title": judul, - "url": anime.get("url").and_then(|v| v.as_str()).unwrap_or(""), - "type": anime.get("type").and_then(|v| v.as_str()).unwrap_or(""), - "score": anime.get("score").cloned().unwrap_or(Value::Null), - "members": anime.get("members").cloned().unwrap_or(Value::Null), - "status": anime.get("status").and_then(|v| v.as_str()).unwrap_or(""), - "synopsis": anime.get("synopsis").and_then(|v| v.as_str()).unwrap_or(""), - "favorites": anime.get("favorites").cloned().unwrap_or(Value::Null), - "images": anime.get("images").cloned().unwrap_or(Value::Null), - "genres": genrenya, - })) -} - -/// Manga info from jikan.moe, ported from anime-search's `getMangaInfo`. -pub async fn fetch_manga_info(query: &str) -> Result { - let url = format!("https://api.jikan.moe/v4/manga?q={}", urlencode(query)); - let data = get_json(&url).await?; - - let arr = data - .get("data") - .and_then(|d| d.as_array()) - .cloned() - .unwrap_or_default(); - if arr.is_empty() { - return Ok(json!({ "error": "Manga tidak ditemukan" })); - } - let manga = &arr[0]; - - let judul = manga - .get("titles") - .and_then(|t| t.as_array()) - .map(|tt| { - tt.iter() - .filter_map(|j| { - let title = j.get("title").and_then(|v| v.as_str()).unwrap_or(""); - let ty = j.get("type").and_then(|v| v.as_str()).unwrap_or(""); - if title.is_empty() { - None - } else { - Some(format!("{} [{}]", title, ty)) - } - }) - .collect::>() - .join(", ") - }) - .unwrap_or_default(); - let genrenya = join_names(manga.get("genres")); - - Ok(json!({ - "title": judul, - "chapters": manga.get("chapters").cloned().unwrap_or(Value::Null), - "type": manga.get("type").and_then(|v| v.as_str()).unwrap_or(""), - "status": manga.get("status").and_then(|v| v.as_str()).unwrap_or(""), - "genre": genrenya, - "volumes": manga.get("volumes").cloned().unwrap_or(Value::Null), - "favorites": manga.get("favorites").cloned().unwrap_or(Value::Null), - "score": manga.get("score").cloned().unwrap_or(Value::Null), - "scored": manga.get("scored").cloned().unwrap_or(Value::Null), - "scored_by": manga.get("scored_by").cloned().unwrap_or(Value::Null), - "rank": manga.get("rank").cloned().unwrap_or(Value::Null), - "popularity": manga.get("popularity").cloned().unwrap_or(Value::Null), - "members": manga.get("members").cloned().unwrap_or(Value::Null), - "url": manga.get("url").and_then(|v| v.as_str()).unwrap_or(""), - "background": manga.get("background").and_then(|v| v.as_str()).unwrap_or(""), - "synopsis": manga.get("synopsis").and_then(|v| v.as_str()).unwrap_or(""), - })) -} - -/// What anime is in this screenshot? trace.moe reverse-image search. -pub async fn fetch_whatanime(url: &str) -> Result { - let api = format!( - "https://api.trace.moe/search?cutBorders&url={}", - urlencode(url) - ); - let data = get_json(&api).await?; - - let result = data - .get("result") - .and_then(|r| r.as_array()) - .and_then(|r| r.first()) - .cloned(); - let Some(r) = result else { - return Ok(json!({ "error": "Anime tidak ditemukan" })); - }; - - let similarity = r - .get("similarity") - .and_then(|s| s.as_f64()) - .map(|s| (s * 100.0).round() as i64) - .unwrap_or(0); - - Ok(json!({ - "judul": r.get("filename").and_then(|v| v.as_str()).unwrap_or(""), - "episode": r.get("episode").and_then(|v| v.as_str()).unwrap_or(""), - "similarity": similarity, - "videoURL": r.get("video").and_then(|v| v.as_str()).unwrap_or(""), - "videoIMG": r.get("image").and_then(|v| v.as_str()).unwrap_or(""), - })) -} - -/// SFW waifu image from waifu.pics. -pub async fn fetch_sfw_waifu(tag: &str) -> Result { - let url = format!("https://api.waifu.pics/sfw/{}", urlencode(tag)); - get_json(&url).await -} - -/// NSFW waifu image from waifu.pics. -pub async fn fetch_nsfw_waifu(tag: &str) -> Result { - let url = format!("https://api.waifu.pics/nsfw/{}", urlencode(tag)); - get_json(&url).await -} - -// ============================================================================ -// Helpers -// ============================================================================ - -async fn get_json(url: &str) -> Result { - 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/120.0 Safari/537.36") - .send() - .await - .map_err(|e| format!("HTTP error: {}", e))?; - if !resp.status().is_success() { - return Err(format!("HTTP {} from upstream", resp.status())); - } - resp.json::() - .await - .map_err(|e| format!("JSON parse: {}", e)) -} - -/// Join an array of { name: "X" } objects into a comma-separated string. -fn join_names(v: Option<&Value>) -> String { - v.and_then(|g| g.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|g| { - g.get("name") - .and_then(|n| n.as_str()) - .map(|s| s.to_string()) - }) - .collect::>() - .join(", ") - }) - .unwrap_or_default() -} - -/// Minimal URL-encode (space -> %20, keep alphanumerics and safe chars). -fn urlencode(s: &str) -> String { - let mut out = String::new(); - for b in s.bytes() { - match b { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { - out.push(b as char) - } - b' ' => out.push_str("%20"), - _ => out.push_str(&format!("%{:02X}", b)), - } - } - out -} diff --git a/src/observability/openapi_modules.rs b/src/observability/openapi_modules.rs index 317bd61..e66948e 100644 --- a/src/observability/openapi_modules.rs +++ b/src/observability/openapi_modules.rs @@ -80,12 +80,6 @@ use utoipa::OpenApi; 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, @@ -102,13 +96,6 @@ use utoipa::OpenApi; // ── 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( @@ -123,12 +110,9 @@ use utoipa::OpenApi; (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 deleted file mode 100644 index 71c0b7c..0000000 --- a/src/presentation/handler/ai.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Axum handlers for AI endpoints (Pollinations). - -use axum::body::Body; -use axum::extract::Query; -use axum::http::{header, StatusCode}; -use axum::response::Response; -use axum::Json; -use serde::Deserialize; -use serde_json::Value; - -use crate::application::ai as use_cases; -use crate::presentation::error::AppError; - -#[derive(Deserialize)] -pub struct AiParams { - pub text: Option, - pub prompt: Option, - pub model: Option, - pub image_url: Option, - pub url: Option, - pub width: Option, - pub height: Option, -} - -/// 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 - .ok_or_else(|| AppError::BadRequest("Missing 'text' parameter".to_string()))?; - let result = use_cases::chat(text, params.prompt.unwrap_or_default(), params.model).await?; - Ok(Json(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 - .ok_or_else(|| AppError::BadRequest("Missing 'text' parameter".to_string()))?; - let result = - use_cases::gemini(text, params.prompt.unwrap_or_default(), params.image_url).await?; - Ok(Json(result)) -} - -/// GET /ai/image?prompt=...&model=...&width=...&height=... — returns image bytes. -#[utoipa::path( - get, - path = "/ai/image", - tag = "ai", - operation_id = "ai_image_handler", - responses( - (status = 200, description = "OK", body = serde_json::Value), - (status = 400, description = "Bad Request"), - (status = 502, description = "Upstream error"), - ) -)] - -pub async fn image_handler(Query(params): Query) -> Result, AppError> { - let prompt = params - .prompt - .ok_or_else(|| AppError::BadRequest("Missing 'prompt' parameter".to_string()))?; - let model = params.model.unwrap_or_else(|| "flux".to_string()); - let width = params.width.unwrap_or(1024); - let height = params.height.unwrap_or(1024); - - let bytes = use_cases::image(prompt, model, width, height).await?; - // Pollinations flux returns JPEG by default. - let mime = if bytes.starts_with(b"\xff\xd8\xff") { - "image/jpeg" - } else if bytes.starts_with(b"\x89PNG\r\n\x1a\n") { - "image/png" - } else { - "application/octet-stream" - }; - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, mime) - .body(Body::from(bytes)) - .map_err(|e| AppError::Internal(format!("build response: {e}"))) -} diff --git a/src/presentation/handler/mod.rs b/src/presentation/handler/mod.rs index a6f3856..e65db17 100644 --- a/src/presentation/handler/mod.rs +++ b/src/presentation/handler/mod.rs @@ -1,4 +1,3 @@ -pub mod ai; pub mod anime; pub mod anime2; pub mod downloader; @@ -10,5 +9,3 @@ pub mod proxy; pub mod search; pub mod stalk; pub mod tools; -pub mod uploader; -pub mod weebs; diff --git a/src/presentation/handler/uploader.rs b/src/presentation/handler/uploader.rs deleted file mode 100644 index 2f4eee6..0000000 --- a/src/presentation/handler/uploader.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Axum handlers for the uploader. -//! -//! POST /uploader/ryzencdn — multipart file upload, saved to local disk. -//! GET /uploader/file/:name — serve an uploaded file. - -use axum::body::Body; -use axum::extract::{Multipart, Path}; -use axum::http::{header, StatusCode}; -use axum::response::Response; -use axum::Json; -use serde_json::Value; - -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(""); - let mut data: Vec = Vec::new(); - - while let Some(field) = multipart - .next_field() - .await - .map_err(|e| AppError::BadRequest(format!("multipart: {e}")))? - { - let name = field.name().unwrap_or("file").to_string(); - if name == "file" { - file_name = field.file_name().unwrap_or("").to_string(); - let content = field - .bytes() - .await - .map_err(|e| AppError::BadRequest(format!("bytes: {e}")))?; - data = content.to_vec(); - break; - } - } - - if data.is_empty() { - return Err(AppError::BadRequest( - "No file field named 'file' in multipart body".into(), - )); - } - - let result = use_cases::upload(&data, &file_name)?; - Ok(Json(result)) -} - -/// 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)?; - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, mime) - .header(header::CACHE_CONTROL, "public, max-age=86400") - .body(Body::from(bytes)) - .map_err(|e| AppError::Internal(format!("build response: {e}"))) -} diff --git a/src/presentation/handler/weebs.rs b/src/presentation/handler/weebs.rs deleted file mode 100644 index 7f1bd58..0000000 --- a/src/presentation/handler/weebs.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Axum handlers for weebs endpoints. -//! -//! Ported from Shirokami-API `scraper/weebs/*.js`. - -use axum::extract::Query; -use axum::http::StatusCode; -use axum::Json; -use serde::Deserialize; -use serde_json::Value; - -use crate::application::weebs as use_cases; -use crate::presentation::error::AppError; - -#[derive(Deserialize)] -pub struct WeebsParams { - pub query: Option, - pub q: Option, - pub url: Option, - pub tag: Option, -} - -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> { - let query = require(p.query.as_ref().or(p.q.as_ref()), "query")?; - Ok((StatusCode::OK, Json(use_cases::anime_info(&query).await?))) -} - -/// 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> { - let query = require(p.query.as_ref().or(p.q.as_ref()), "query")?; - Ok((StatusCode::OK, Json(use_cases::manga_info(&query).await?))) -} - -/// 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> { - let url = require(p.url.as_ref(), "url")?; - Ok((StatusCode::OK, Json(use_cases::whatanime(&url).await?))) -} - -/// 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> { - let tag = require(p.tag.as_ref(), "tag")?; - Ok((StatusCode::OK, Json(use_cases::sfw_waifu(&tag).await?))) -} - -/// 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> { - let tag = require(p.tag.as_ref(), "tag")?; - Ok((StatusCode::OK, Json(use_cases::nsfw_waifu(&tag).await?))) -} diff --git a/src/presentation/router.rs b/src/presentation/router.rs index cdc8bc5..35e79ab 100644 --- a/src/presentation/router.rs +++ b/src/presentation/router.rs @@ -306,27 +306,6 @@ pub fn build_router(app_state: Arc) -> anyhow::Result { "/stalk/instagram", axum::routing::get(crate::presentation::handler::stalk::instagram_handler), ) - // Weebs routes - .route( - "/weebs/anime-info", - axum::routing::get(crate::presentation::handler::weebs::anime_info_handler), - ) - .route( - "/weebs/manga-info", - axum::routing::get(crate::presentation::handler::weebs::manga_info_handler), - ) - .route( - "/weebs/whatanime", - axum::routing::get(crate::presentation::handler::weebs::whatanime_handler), - ) - .route( - "/weebs/sfw-waifu", - axum::routing::get(crate::presentation::handler::weebs::sfw_waifu_handler), - ) - .route( - "/weebs/nsfw-waifu", - axum::routing::get(crate::presentation::handler::weebs::nsfw_waifu_handler), - ) // Search routes .route( "/search/bmkg", @@ -382,28 +361,6 @@ pub fn build_router(app_state: Arc) -> anyhow::Result { "/image/brat/animated", axum::routing::get(crate::presentation::handler::image::brat_animated_handler), ) - // Uploader routes - .route( - "/uploader/ryzencdn", - axum::routing::post(crate::presentation::handler::uploader::ryzencdn_handler), - ) - .route( - "/uploader/file/{name}", - axum::routing::get(crate::presentation::handler::uploader::serve_file_handler), - ) - // AI routes - .route( - "/ai/chat", - axum::routing::get(crate::presentation::handler::ai::chat_handler), - ) - .route( - "/ai/gemini", - axum::routing::get(crate::presentation::handler::ai::gemini_handler), - ) - .route( - "/ai/image", - axum::routing::get(crate::presentation::handler::ai::image_handler), - ) // Health .route( "/health",