From cd34fb983d642d48ab5e3fafe9ffea80262f4e62 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Fri, 2 Jan 2026 16:36:59 +0000 Subject: [PATCH] feat: random stuff --- API.md | 1 + src/app/api/submit/route.ts | 4 --- src/app/mii/[id]/data/route.ts | 3 +- src/app/mii/[id]/image/route.ts | 10 +++---- src/app/mii/[id]/page.tsx | 15 +++++----- src/lib/images.tsx | 53 +++++++++++---------------------- src/lib/schemas.ts | 15 +++------- 7 files changed, 36 insertions(+), 65 deletions(-) diff --git a/API.md b/API.md index 87edda1..eb18ec5 100644 --- a/API.md +++ b/API.md @@ -106,6 +106,7 @@ https://tomodachishare.com/mii/1/data { "id": 1, "name": "Frieren", + "platform": "THREE_DS", "imageCount": 3, "tags": ["anime", "frieren"], "description": "Frieren from 'Frieren: Beyond Journey's End'\r\nThe first Mii on the site!", diff --git a/src/app/api/submit/route.ts b/src/app/api/submit/route.ts index 2701e10..561c762 100644 --- a/src/app/api/submit/route.ts +++ b/src/app/api/submit/route.ts @@ -120,12 +120,8 @@ export async function POST(request: NextRequest) { } } - console.log(data.miiPortraitImage); - // Check Mii portrait image as well (Switch) if (data.platform === "SWITCH") { - if (data.miiPortraitImage.length === 0) return rateLimit.sendResponse({ error: "No mii portrait found!" }, 400); - const imageValidation = await validateImage(data.miiPortraitImage); if (!imageValidation.valid) return rateLimit.sendResponse({ error: imageValidation.error }, imageValidation.status ?? 400); } diff --git a/src/app/mii/[id]/data/route.ts b/src/app/mii/[id]/data/route.ts index 6c20aa4..4f4134d 100644 --- a/src/app/mii/[id]/data/route.ts +++ b/src/app/mii/[id]/data/route.ts @@ -5,7 +5,7 @@ import { RateLimit } from "@/lib/rate-limit"; import { prisma } from "@/lib/prisma"; export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const rateLimit = new RateLimit(request, 200, "/mii/data"); + const rateLimit = new RateLimit(request, 3, "/mii/data"); const check = await rateLimit.handle(); if (check) return check; @@ -24,6 +24,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ likedBy: true, }, }, + platform: true, imageCount: true, tags: true, description: true, diff --git a/src/app/mii/[id]/image/route.ts b/src/app/mii/[id]/image/route.ts index 89c19a9..50e23df 100644 --- a/src/app/mii/[id]/image/route.ts +++ b/src/app/mii/[id]/image/route.ts @@ -74,13 +74,13 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ if (imageType === "metadata" && mii) { // Metadata images were added after 1274 Miis were submitted, so we generate it on-the-fly console.log(`Metadata image not found for mii ID ${miiId}, generating metadata image...`); + const { buffer: metadataBuffer, error, status } = await generateMetadataImage(mii, mii.user.name!); - try { - buffer = await generateMetadataImage(mii, mii.user.name!); - } catch (error) { - console.error(error); - return rateLimit.sendResponse({ error: `Failed to generate 'metadata' type image for mii ${miiId}` }, 500); + if (error) { + return rateLimit.sendResponse({ error }, status); } + + buffer = metadataBuffer; } else { return rateLimit.sendResponse({ error: "Image not found" }, 404); } diff --git a/src/app/mii/[id]/page.tsx b/src/app/mii/[id]/page.tsx index fa46e67..4acf6ea 100644 --- a/src/app/mii/[id]/page.tsx +++ b/src/app/mii/[id]/page.tsx @@ -30,6 +30,7 @@ export async function generateMetadata({ params }: Props): Promise { include: { user: { select: { + name: true, username: true, }, }, @@ -44,18 +45,16 @@ export async function generateMetadata({ params }: Props): Promise { const metadataImageUrl = `/mii/${mii.id}/image?type=metadata`; - const username = `@${mii.user.username}`; - return { metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL!), title: `${mii.name} - TomodachiShare`, - description: `Check out '${mii.name}', a Tomodachi Life Mii created by ${username} on TomodachiShare with ${mii._count.likedBy} likes.`, + description: `Check out '${mii.name}', a Tomodachi Life Mii created by ${mii.user.name} on TomodachiShare with ${mii._count.likedBy} likes.`, keywords: ["mii", "tomodachi life", "nintendo", "tomodachishare", "tomodachi-share", "mii creator", "mii collection", ...mii.tags], - creator: username, + creator: mii.user.username, openGraph: { type: "article", title: `${mii.name} - TomodachiShare`, - description: `Check out '${mii.name}', a Tomodachi Life Mii created by ${username} on TomodachiShare with ${mii._count.likedBy} likes.`, + description: `Check out '${mii.name}', a Tomodachi Life Mii created by ${mii.user.name} on TomodachiShare with ${mii._count.likedBy} likes.`, images: [ { url: metadataImageUrl, @@ -63,19 +62,19 @@ export async function generateMetadata({ params }: Props): Promise { }, ], publishedTime: mii.createdAt.toISOString(), - authors: username, + authors: mii.user.username, }, twitter: { card: "summary_large_image", title: `${mii.name} - TomodachiShare`, - description: `Check out '${mii.name}', a Tomodachi Life Mii created by ${username} on TomodachiShare with ${mii._count.likedBy} likes.`, + description: `Check out '${mii.name}', a Tomodachi Life Mii created by ${mii.user.name} on TomodachiShare with ${mii._count.likedBy} likes.`, images: [ { url: metadataImageUrl, alt: `${mii.name}, ${mii.tags.join(", ")} ${mii.gender} Mii character`, }, ], - creator: username, + creator: mii.user.username!, }, alternates: { canonical: `/mii/${mii.id}`, diff --git a/src/lib/images.tsx b/src/lib/images.tsx index b7f856d..7e6f149 100644 --- a/src/lib/images.tsx +++ b/src/lib/images.tsx @@ -22,11 +22,7 @@ const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp" //#region Image validation 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: `Image 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()); @@ -34,10 +30,7 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error // Check mime type const fileType = await fileTypeFromBuffer(buffer); if (!fileType || !ALLOWED_MIME_TYPES.includes(fileType.mime)) - return { - valid: false, - error: "Invalid image file type. Only .jpeg, .png, .gif, and .webp are allowed", - }; + return { valid: false, error: "Invalid image file type. Only .jpeg, .png, .gif, and .webp are allowed" }; let metadata: sharp.Metadata; try { @@ -55,10 +48,7 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error metadata.height < MIN_IMAGE_DIMENSIONS[1] || metadata.height > MAX_IMAGE_DIMENSIONS[1] ) { - return { - valid: false, - error: "Image dimensions are invalid. Resolution must be between 128x128 and 1920x1080", - }; + return { valid: false, error: "Image dimensions are invalid. Resolution must be between 128x128 and 1920x1080" }; } // Check for inappropriate content @@ -72,11 +62,7 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error if (!moderationResponse.ok) { console.error("Moderation API error"); - return { - valid: false, - error: "Content moderation check failed", - status: 500, - }; + return { valid: false, error: "Content moderation check failed", status: 500 }; } const result = await moderationResponse.json(); @@ -91,11 +77,7 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error return { valid: true }; } catch (error) { console.error("Error validating image:", error); - return { - valid: false, - error: "Failed to process image file.", - status: 500, - }; + return { valid: false, error: "Failed to process image file.", status: 500 }; } } //#endregion @@ -139,7 +121,7 @@ const loadFonts = async (): Promise => { ); }; -export async function generateMetadataImage(mii: Mii, author: string): Promise { +export async function generateMetadataImage(mii: Mii, author: string): Promise<{ buffer?: Buffer; error?: string; status?: number }> { const miiUploadsDirectory = path.join(uploadsDirectory, mii.id.toString()); // Load assets concurrently @@ -203,13 +185,7 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise -
+
{/* Author */} @@ -242,11 +218,16 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise tag.trim()) .filter((tag) => tag.length > 0) ), - platform: z.enum(MiiPlatform, { error: "Platform must be either 'THREE_DS', or 'SWITCH'" }).optional(), -gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', or 'FEMALE'" }).optional(), + platform: z.enum(MiiPlatform, { error: "Platform must be either 'THREE_DS', or 'SWITCH'" }).optional(), + gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', or 'FEMALE'" }).optional(), // todo: incorporate tagsSchema // Pages limit: z.coerce @@ -62,11 +59,7 @@ gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', or 'FEMALE'" } .min(1, { error: "Limit must be at least 1" }) .max(100, { error: "Limit cannot be more than 100" }) .optional(), - page: z.coerce - .number({ error: "Page must be a number" }) - .int({ error: "Page must be an integer" }) - .min(1, { error: "Page must be at least 1" }) - .optional(), + page: z.coerce.number({ error: "Page must be a number" }).int({ error: "Page must be an integer" }).min(1, { error: "Page must be at least 1" }).optional(), // Random sort seed: z.coerce.number({ error: "Seed must be a number" }).int({ error: "Seed must be an integer" }).optional(), });