mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-03-28 19:23:15 +00:00
feat: random stuff
This commit is contained in:
parent
2af1bf18a6
commit
cd34fb983d
7 changed files with 36 additions and 65 deletions
1
API.md
1
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!",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||
include: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -44,18 +45,16 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||
|
||||
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<Metadata> {
|
|||
},
|
||||
],
|
||||
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}`,
|
||||
|
|
|
|||
|
|
@ -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<Font[]> => {
|
|||
);
|
||||
};
|
||||
|
||||
export async function generateMetadataImage(mii: Mii, author: string): Promise<Buffer> {
|
||||
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<B
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
tw="absolute inset-0"
|
||||
style={{
|
||||
position: "absolute",
|
||||
backgroundImage: "linear-gradient(to right, #fffbeb00 70%, #fffbeb);",
|
||||
}}
|
||||
></div>
|
||||
<div tw="absolute inset-0" style={{ position: "absolute", backgroundImage: "linear-gradient(to right, #fffbeb00 70%, #fffbeb);" }}></div>
|
||||
</div>
|
||||
|
||||
{/* Author */}
|
||||
|
|
@ -242,11 +218,16 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise<B
|
|||
const buffer = await sharp(Buffer.from(svg)).png().toBuffer();
|
||||
|
||||
// Store the file
|
||||
try {
|
||||
// I tried using .webp here but the quality looked awful
|
||||
// but it actually might be well-liked due to the hatred of .webp
|
||||
const fileLocation = path.join(miiUploadsDirectory, "metadata.png");
|
||||
await fs.writeFile(fileLocation, buffer);
|
||||
} catch (error) {
|
||||
console.error("Error storing 'metadata' image type", error);
|
||||
return { error: `Failed to store metadata image for ${mii.id}`, status: 500 };
|
||||
}
|
||||
|
||||
return buffer;
|
||||
return { buffer };
|
||||
}
|
||||
//#endregion
|
||||
|
|
|
|||
|
|
@ -35,10 +35,7 @@ export const tagsSchema = z
|
|||
.min(1, { error: "There must be at least 1 tag" })
|
||||
.max(8, { error: "There cannot be more than 8 tags" });
|
||||
|
||||
export const idSchema = z.coerce
|
||||
.number({ error: "ID must be a number" })
|
||||
.int({ error: "ID must be an integer" })
|
||||
.positive({ error: "ID must be valid" });
|
||||
export const idSchema = z.coerce.number({ error: "ID must be a number" }).int({ error: "ID must be an integer" }).positive({ error: "ID must be valid" });
|
||||
|
||||
export const searchSchema = z.object({
|
||||
q: querySchema.optional(),
|
||||
|
|
@ -53,7 +50,7 @@ export const searchSchema = z.object({
|
|||
.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(),
|
||||
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(),
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue