diff --git a/src/lib/images.ts b/src/lib/images.ts index fdacfca..93a9a0a 100644 --- a/src/lib/images.ts +++ b/src/lib/images.ts @@ -1,40 +1,29 @@ -// import * as tf from "@tensorflow/tfjs-node"; -// import * as nsfwjs from "nsfwjs"; import sharp from "sharp"; import { fileTypeFromBuffer } from "file-type"; const MIN_IMAGE_DIMENSIONS = 128; const MAX_IMAGE_DIMENSIONS = 1024; const MAX_IMAGE_SIZE = 1024 * 1024; // 1 MB - -// const THRESHOLD = 0.5; - -// tf.enableProdMode(); - -// Load NSFW.JS model -// let _model: nsfwjs.NSFWJS | undefined = undefined; - -// async function loadModel() { -// if (!_model) { -// const model = await nsfwjs.load("MobileNetV2Mid"); -// _model = model; -// } -// return _model!; -// } +const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"]; export async function validateImage(file: File): Promise<{ valid: boolean; error?: string; status?: number }> { if (!file || file.size == 0) return { valid: false, error: "Empty image file" }; - if (file.size > MAX_IMAGE_SIZE) - return { valid: false, error: `One or more of your images are too large. Maximum size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB` }; + if (file.size > MAX_IMAGE_SIZE) return { valid: false, error: `Image too large. Maximum size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB` }; try { const buffer = Buffer.from(await file.arrayBuffer()); // Check mime type const fileType = await fileTypeFromBuffer(buffer); - if (!fileType || !fileType.mime.startsWith("image/")) return { valid: false, error: "Invalid image file type. Only actual images are allowed" }; + if (!fileType || !ALLOWED_MIME_TYPES.includes(fileType.mime)) + return { valid: false, error: "Invalid image file type. Only .jpeg, .png, .gif, and .webp are allowed" }; - const metadata = await sharp(buffer).metadata(); + let metadata: sharp.Metadata; + try { + metadata = await sharp(buffer).metadata(); + } catch { + return { valid: false, error: "Invalid or corrupted image file" }; + } // Check image dimensions if ( @@ -49,25 +38,31 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error } // Check for inappropriate content - // const image = tf.node.decodeImage(buffer, 3) as tf.Tensor3D; - // const model = await loadModel(); - // const predictions = await model.classify(image); - // image.dispose(); + // https://github.com/trafficlunar/api-moderation + try { + const blob = new Blob([buffer]); + const formData = new FormData(); + formData.append("image", blob); - // for (const pred of predictions) { - // if ( - // (pred.className === "Porn" && pred.probability > THRESHOLD) || - // (pred.className === "Hentai" && pred.probability > THRESHOLD) || - // (pred.className === "Sexy" && pred.probability > THRESHOLD) - // ) { - // // reject image - // return { valid: false, error: "Image contains inappropriate content" }; - // } - // } + const moderationResponse = await fetch("https://api.trafficlunar.net/moderate/image", { method: "POST", body: formData }); + const result = await moderationResponse.json(); + + if (!moderationResponse.ok) { + console.error("Moderation API error:", result); + return { valid: false, error: "Content moderation check failed", status: 500 }; + } + + if (result.error) { + return { valid: false, error: result.error }; + } + } catch (moderationError) { + console.error("Error fetching moderation API:", moderationError); + return { valid: false, error: "Moderation API is down", status: 503 }; + } + + return { valid: true }; } catch (error) { console.error("Error validating image:", error); return { valid: false, error: "Failed to process image file.", status: 500 }; } - - return { valid: true }; }