mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-06-28 06:34:15 +00:00
feat: custom mii images and refactor submit route
This commit is contained in:
parent
45fb0c07a7
commit
1e0132990a
8 changed files with 226 additions and 103 deletions
68
src/lib/images.ts
Normal file
68
src/lib/images.ts
Normal 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
46
src/lib/qr-codes.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue