feat: custom mii images and refactor submit route

This commit is contained in:
trafficlunar 2025-04-07 20:20:23 +01:00
parent 45fb0c07a7
commit 1e0132990a
8 changed files with 226 additions and 103 deletions

68
src/lib/images.ts Normal file
View file

@ -0,0 +1,68 @@
// import * as tf from "@tensorflow/tfjs-node";
// import * as nsfwjs from "nsfwjs";
import sharp from "sharp";
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!;
// }
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.type.startsWith("image/")) return { valid: false, error: "Invalid file type. Only images are allowed" };
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` };
try {
const buffer = Buffer.from(await file.arrayBuffer());
const metadata = await sharp(buffer).metadata();
// Check image dimensions
if (
!metadata.width ||
!metadata.height ||
metadata.width < MIN_IMAGE_DIMENSIONS ||
metadata.width > MAX_IMAGE_DIMENSIONS ||
metadata.height < MIN_IMAGE_DIMENSIONS ||
metadata.height > MAX_IMAGE_DIMENSIONS
) {
return { valid: false, error: "Image dimensions are invalid. Width and height must be between 128px and 1024px" };
}
// 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();
// 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" };
// }
// }
} catch (error) {
console.error("Error validating image:", error);
return { valid: false, error: "Failed to process image file.", status: 500 };
}
return { valid: true };
}

46
src/lib/qr-codes.ts Normal file
View file

@ -0,0 +1,46 @@
import { AES_CCM } from "@trafficlunar/asmcrypto.js";
import { MII_DECRYPTION_KEY } from "./constants";
import Mii from "./mii.js/mii";
import TomodachiLifeMii from "./tomodachi-life-mii";
export function convertQrCode(bytes: Uint8Array): { mii: Mii; tomodachiLifeMii: TomodachiLifeMii } {
// Decrypt the Mii part of the QR code
// (Credits to kazuki-4ys)
const nonce = bytes.subarray(0, 8);
const content = bytes.subarray(8, 0x70);
const nonceWithZeros = new Uint8Array(12);
nonceWithZeros.set(nonce, 0);
let decrypted: Uint8Array<ArrayBufferLike> = new Uint8Array();
try {
decrypted = AES_CCM.decrypt(content, MII_DECRYPTION_KEY, nonceWithZeros, undefined, 16);
} catch (error) {
throw new Error("Failed to decrypt QR code. It may be invalid or corrupted");
}
const result = new Uint8Array(96);
result.set(decrypted.subarray(0, 12), 0);
result.set(nonce, 12);
result.set(decrypted.subarray(12), 20);
// Check if QR code is valid (after decryption)
if (result.length !== 0x60 || (result[0x16] !== 0 && result[0x17] !== 0)) throw new Error("QR code is not a valid Mii QR code");
// Convert to Mii classes
try {
const buffer = Buffer.from(result);
const mii = new Mii(buffer);
const tomodachiLifeMii = TomodachiLifeMii.fromBytes(bytes);
if (tomodachiLifeMii.hairDyeEnabled) {
mii.hairColor = tomodachiLifeMii.studioHairColor;
mii.eyebrowColor = tomodachiLifeMii.studioHairColor;
mii.facialHairColor = tomodachiLifeMii.studioHairColor;
}
return { mii, tomodachiLifeMii };
} catch (error) {
throw new Error("Mii data is not valid");
}
}