mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 13:17:45 +00:00
fix: can't edit custom images properly
also cleanup
This commit is contained in:
parent
a5080f1b2e
commit
913f0ef65a
5 changed files with 35 additions and 53 deletions
|
|
@ -80,6 +80,8 @@ For Discord, create an application in the developer portal, go to 'OAuth2', copy
|
||||||
|
|
||||||
For GitHub, navigate to your profile settings, then 'Developer Settings', and create a new application. Set the homepage URL to `http://localhost:3000` and copy the Client ID and generate a new client secret. Finally, add in a callback URL with the value `http://localhost:3000/api/auth/callback/github`.
|
For GitHub, navigate to your profile settings, then 'Developer Settings', and create a new application. Set the homepage URL to `http://localhost:3000` and copy the Client ID and generate a new client secret. Finally, add in a callback URL with the value `http://localhost:3000/api/auth/callback/github`.
|
||||||
|
|
||||||
|
Google is annoying so I'm not explaining it.
|
||||||
|
|
||||||
After configuring the environment variables, you can run a development server.
|
After configuring the environment variables, you can run a development server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -110,26 +110,28 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
parsed.data;
|
parsed.data;
|
||||||
|
|
||||||
// Validate image files
|
// Validate image files
|
||||||
let wasImagesModerated = false;
|
const customImages: File[] = [];
|
||||||
const images: File[] = [];
|
|
||||||
|
|
||||||
for (const img of [image1, image2, image3]) {
|
for (const img of [image1, image2, image3]) {
|
||||||
if (!img) continue;
|
if (!img) continue;
|
||||||
|
|
||||||
const validation = await validateImage(img);
|
const validation = await validateImage(img);
|
||||||
if (!validation.valid) wasImagesModerated = true;
|
if (validation.valid) {
|
||||||
images.push(img);
|
customImages.push(img);
|
||||||
|
} else {
|
||||||
|
return rateLimit.sendResponse({ error: `Failed to verify custom image: ${validation.error}` }, validation.status ?? 400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Mii portrait & features image (Switch)
|
// Check Mii portrait & features image (Switch)
|
||||||
if (mii.platform === "SWITCH") {
|
if (mii.platform === "SWITCH") {
|
||||||
if (miiPortraitImage) {
|
if (miiPortraitImage) {
|
||||||
const validation = await validateImage(miiPortraitImage);
|
const validation = await validateImage(miiPortraitImage);
|
||||||
if (!validation.valid) wasImagesModerated = true;
|
if (!validation.valid) return rateLimit.sendResponse({ error: `Failed to verify portrait: ${validation.error}` }, validation.status ?? 400);
|
||||||
}
|
}
|
||||||
if (miiFeaturesImage) {
|
if (miiFeaturesImage) {
|
||||||
const validation = await validateImage(miiFeaturesImage);
|
const validation = await validateImage(miiFeaturesImage);
|
||||||
if (!validation.valid) wasImagesModerated = true;
|
if (!validation.valid) return rateLimit.sendResponse({ error: `Failed to verify features: ${validation.error}` }, validation.status ?? 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,10 +149,10 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
if (makeup !== undefined) updateData.makeup = makeup;
|
if (makeup !== undefined) updateData.makeup = makeup;
|
||||||
if (youtubeId !== undefined) updateData.youtubeId = youtubeId;
|
if (youtubeId !== undefined) updateData.youtubeId = youtubeId;
|
||||||
if (instructions !== undefined) updateData.instructions = instructions;
|
if (instructions !== undefined) updateData.instructions = instructions;
|
||||||
if (images.length > 0) updateData.imageCount = images.length;
|
if (customImages.length > 0) updateData.imageCount = customImages.length;
|
||||||
|
|
||||||
const imagesChanged = images.length > 0 || miiPortraitImage || miiFeaturesImage;
|
const imagesChanged = customImages.length > 0 || miiPortraitImage || miiFeaturesImage;
|
||||||
if ((settings.queueEnabled && imagesChanged) || wasImagesModerated) updateData.in_queue = true;
|
if (settings.queueEnabled && imagesChanged) updateData.in_queue = true;
|
||||||
|
|
||||||
if (Object.keys(updateData).length === 0) return rateLimit.sendResponse({ error: "Nothing was changed" }, 400);
|
if (Object.keys(updateData).length === 0) return rateLimit.sendResponse({ error: "Nothing was changed" }, 400);
|
||||||
const updatedMii = await prisma.mii.update({
|
const updatedMii = await prisma.mii.update({
|
||||||
|
|
@ -172,7 +174,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
await fs.mkdir(miiUploadsDirectory, { recursive: true });
|
await fs.mkdir(miiUploadsDirectory, { recursive: true });
|
||||||
|
|
||||||
// Only touch files if new images were uploaded
|
// Only touch files if new images were uploaded
|
||||||
if (images.length > 0) {
|
if (customImages.length > 0) {
|
||||||
// Delete all custom images
|
// Delete all custom images
|
||||||
const files = await fs.readdir(miiUploadsDirectory);
|
const files = await fs.readdir(miiUploadsDirectory);
|
||||||
await Promise.all(files.filter((file) => file.startsWith("image")).map((file) => fs.unlink(path.join(miiUploadsDirectory, file))));
|
await Promise.all(files.filter((file) => file.startsWith("image")).map((file) => fs.unlink(path.join(miiUploadsDirectory, file))));
|
||||||
|
|
@ -180,7 +182,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
// Compress and upload new images
|
// Compress and upload new images
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
images.map(async (image, index) => {
|
customImages.map(async (image, index) => {
|
||||||
const buffer = Buffer.from(await image.arrayBuffer());
|
const buffer = Buffer.from(await image.arrayBuffer());
|
||||||
const pngBuffer = await sharp(buffer).resize({ height: 800, fit: "inside", withoutEnlargement: true }).png({ quality: 85 }).toBuffer();
|
const pngBuffer = await sharp(buffer).resize({ height: 800, fit: "inside", withoutEnlargement: true }).png({ quality: 85 }).toBuffer();
|
||||||
const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`);
|
const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`);
|
||||||
|
|
@ -198,17 +200,6 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
// Only save portrait & features for Switch Miis when they are provided
|
// Only save portrait & features for Switch Miis when they are provided
|
||||||
if (mii.platform === "SWITCH" && (miiPortraitImage || miiFeaturesImage)) {
|
if (mii.platform === "SWITCH" && (miiPortraitImage || miiFeaturesImage)) {
|
||||||
try {
|
try {
|
||||||
// Delete existing portrait/features if they're being replaced
|
|
||||||
await Promise.all(
|
|
||||||
["mii.png", "features.png"]
|
|
||||||
.filter((file) => {
|
|
||||||
if (file === "mii.png") return miiPortraitImage;
|
|
||||||
if (file === "features.png") return miiFeaturesImage;
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.map((file) => fs.unlink(path.join(miiUploadsDirectory, file))),
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[
|
[
|
||||||
miiPortraitImage &&
|
miiPortraitImage &&
|
||||||
|
|
|
||||||
|
|
@ -63,13 +63,13 @@ const submitSchema = z
|
||||||
(data) => {
|
(data) => {
|
||||||
// If platform is Switch, gender, miiPortraitImage, and miiFeaturesImage must be present
|
// If platform is Switch, gender, miiPortraitImage, and miiFeaturesImage must be present
|
||||||
if (data.platform === "SWITCH") {
|
if (data.platform === "SWITCH") {
|
||||||
return data.gender !== undefined && data.miiPortraitImage !== undefined;
|
return data.gender !== undefined && data.miiPortraitImage !== undefined && data.miiFeaturesImage !== undefined;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Gender, Mii portrait & features image, and instructions are required for Switch platform",
|
message: "Gender, Mii portrait & features image are required for Switch platform",
|
||||||
path: ["gender", "miiPortraitImage", "miiFeaturesImage", "instructions"],
|
path: ["gender", "miiPortraitImage", "miiFeaturesImage"],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -161,23 +161,27 @@ export async function POST(request: NextRequest) {
|
||||||
const description = uncensoredDescription && profanity.censor(uncensoredDescription);
|
const description = uncensoredDescription && profanity.censor(uncensoredDescription);
|
||||||
|
|
||||||
// Validate image files
|
// Validate image files
|
||||||
let wasImagesModerated = false;
|
|
||||||
const customImages: File[] = [];
|
const customImages: File[] = [];
|
||||||
|
|
||||||
for (const img of [image1, image2, image3]) {
|
for (const img of [image1, image2, image3]) {
|
||||||
if (!img) continue;
|
if (!img) continue;
|
||||||
|
|
||||||
const validation = await validateImage(img);
|
const validation = await validateImage(img);
|
||||||
if (!validation.valid) wasImagesModerated = true;
|
if (validation.valid) {
|
||||||
customImages.push(img);
|
customImages.push(img);
|
||||||
|
} else {
|
||||||
|
return rateLimit.sendResponse({ error: `Failed to verify custom image: ${validation.error}` }, validation.status ?? 400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Mii portrait & features image (Switch)
|
// Check Mii portrait & features image (Switch)
|
||||||
if (platform === "SWITCH") {
|
if (platform === "SWITCH") {
|
||||||
const portraitValidation = await validateImage(miiPortraitImage);
|
const portraitValidation = await validateImage(miiPortraitImage);
|
||||||
const featuresValidation = await validateImage(miiFeaturesImage);
|
const featuresValidation = await validateImage(miiFeaturesImage);
|
||||||
if (!portraitValidation.valid) wasImagesModerated = true;
|
if (!portraitValidation.valid)
|
||||||
if (!featuresValidation.valid) wasImagesModerated = true;
|
return rateLimit.sendResponse({ error: `Failed to verify portrait: ${portraitValidation.error}` }, portraitValidation.status ?? 400);
|
||||||
|
if (!featuresValidation.valid)
|
||||||
|
return rateLimit.sendResponse({ error: `Failed to verify features: ${featuresValidation.error}` }, featuresValidation.status ?? 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const qrBytes = new Uint8Array(qrBytesRaw ?? []);
|
const qrBytes = new Uint8Array(qrBytesRaw ?? []);
|
||||||
|
|
@ -202,7 +206,7 @@ export async function POST(request: NextRequest) {
|
||||||
tags,
|
tags,
|
||||||
description,
|
description,
|
||||||
gender: gender ?? "MALE",
|
gender: gender ?? "MALE",
|
||||||
in_queue: wasImagesModerated || settings.queueEnabled,
|
in_queue: settings.queueEnabled,
|
||||||
|
|
||||||
// Automatically detect certain information if on 3DS
|
// Automatically detect certain information if on 3DS
|
||||||
...(platform === "THREE_DS"
|
...(platform === "THREE_DS"
|
||||||
|
|
@ -340,5 +344,5 @@ export async function POST(request: NextRequest) {
|
||||||
return rateLimit.sendResponse({ error: "Failed to store user images" }, 500);
|
return rateLimit.sendResponse({ error: "Failed to store user images" }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rateLimit.sendResponse({ success: true, id: miiRecord.id, inQueue: wasImagesModerated });
|
return rateLimit.sendResponse({ success: true, id: miiRecord.id });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,11 @@ export default function EditForm({ mii, likes }: Props) {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const [files, setFiles] = useState<FileWithPath[]>([]);
|
const [files, setFiles] = useState<FileWithPath[]>([]);
|
||||||
|
|
||||||
|
const handleFilesChange: React.Dispatch<React.SetStateAction<FileWithPath[]>> = (updater) => {
|
||||||
|
hasCustomImagesChanged.current = true;
|
||||||
|
setFiles(updater);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
(acceptedFiles: FileWithPath[]) => {
|
(acceptedFiles: FileWithPath[]) => {
|
||||||
if (files.length >= 3) return;
|
if (files.length >= 3) return;
|
||||||
|
|
@ -439,7 +444,7 @@ export default function EditForm({ mii, likes }: Props) {
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ImageList files={files} setFiles={setFiles} />
|
<ImageList files={files} setFiles={handleFilesChange} />
|
||||||
|
|
||||||
<hr className="border-zinc-300 my-2" />
|
<hr className="border-zinc-300 my-2" />
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
|
|
|
||||||
|
|
@ -52,26 +52,6 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error
|
||||||
return { valid: false, error: "Image dimensions are invalid. Resolution must be between 128x128 and 8000x8000" };
|
return { valid: false, error: "Image dimensions are invalid. Resolution must be between 128x128 and 8000x8000" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for inappropriate content
|
|
||||||
// https://github.com/trafficlunar/api-moderation
|
|
||||||
try {
|
|
||||||
const blob = new Blob([buffer]);
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("image", blob);
|
|
||||||
|
|
||||||
const headers = new Headers();
|
|
||||||
headers.append("token", process.env.TOKEN ?? "");
|
|
||||||
const moderationResponse = await fetch("https://api.trafficlunar.net/moderate/image", { method: "POST", body: formData, headers });
|
|
||||||
const result = await moderationResponse.json();
|
|
||||||
if (result.error) {
|
|
||||||
return { valid: false, error: result.error };
|
|
||||||
}
|
|
||||||
} catch (moderationError) {
|
|
||||||
console.error("Error fetching moderation API:", moderationError);
|
|
||||||
Sentry.captureException(moderationError, { extra: { stage: "moderation-api-fetch" } });
|
|
||||||
return { valid: false, error: "Moderation API is down", status: 503 };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error validating image:", error);
|
console.error("Error validating image:", error);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue