From 25c9bb079cdfd7278dd7e320d3f445085e05e10b Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Wed, 23 Apr 2025 22:04:05 +0100 Subject: [PATCH] feat: profanity censoring and filtering --- package.json | 1 + pnpm-lock.yaml | 9 +++++++++ src/app/api/auth/display-name/route.ts | 4 ++++ src/app/api/auth/username/route.ts | 7 ++++++- src/app/api/mii/[id]/edit/route.ts | 6 ++++-- src/app/api/submit/route.ts | 7 ++++++- src/app/terms-of-service/page.tsx | 3 ++- src/lib/qr-codes.ts | 7 +++++++ src/lib/schemas.ts | 2 ++ 9 files changed, 41 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 150f42a..d3742dc 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "postinstall": "prisma generate" }, "dependencies": { + "@2toad/profanity": "^3.1.1", "@auth/prisma-adapter": "2.7.2", "@bprogress/next": "^3.2.11", "@hello-pangea/dnd": "^18.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 727a73a..093f2b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@2toad/profanity': + specifier: ^3.1.1 + version: 3.1.1 '@auth/prisma-adapter': specifier: 2.7.2 version: 2.7.2(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3)) @@ -108,6 +111,10 @@ importers: packages: + '@2toad/profanity@3.1.1': + resolution: {integrity: sha512-07ny4pCSa4gDrcJ4vZ/WWmiM90+8kv/clXfnDvThf9IJq0GldpjRVdzHCfMwGDs2Y/8eClmTGzKb5tEfUWy/uA==} + engines: {node: '>=12'} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -2301,6 +2308,8 @@ packages: snapshots: + '@2toad/profanity@3.1.1': {} + '@alloc/quick-lru@5.2.0': {} '@auth/core@0.37.2': diff --git a/src/app/api/auth/display-name/route.ts b/src/app/api/auth/display-name/route.ts index f65a23e..3528aa2 100644 --- a/src/app/api/auth/display-name/route.ts +++ b/src/app/api/auth/display-name/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { profanity } from "@2toad/profanity"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; @@ -14,6 +15,9 @@ export async function PATCH(request: NextRequest) { const validation = displayNameSchema.safeParse(displayName); if (!validation.success) return NextResponse.json({ error: validation.error.errors[0].message }, { status: 400 }); + // Check for inappropriate words + if (profanity.exists(displayName)) return NextResponse.json({ error: "Display name contains inappropriate words" }, { status: 400 }); + try { await prisma.user.update({ where: { email: session.user?.email ?? undefined }, diff --git a/src/app/api/auth/username/route.ts b/src/app/api/auth/username/route.ts index afccb68..c784e8e 100644 --- a/src/app/api/auth/username/route.ts +++ b/src/app/api/auth/username/route.ts @@ -1,9 +1,11 @@ import { NextRequest, NextResponse } from "next/server"; +import dayjs from "dayjs"; +import { profanity } from "@2toad/profanity"; + import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { usernameSchema } from "@/lib/schemas"; -import dayjs from "dayjs"; export async function PATCH(request: NextRequest) { const session = await auth(); @@ -24,6 +26,9 @@ export async function PATCH(request: NextRequest) { const validation = usernameSchema.safeParse(username); if (!validation.success) return NextResponse.json({ error: validation.error.errors[0].message }, { status: 400 }); + // Check for inappropriate words + if (profanity.exists(username)) return NextResponse.json({ error: "Username contains inappropriate words" }, { status: 400 }); + const existingUser = await prisma.user.findUnique({ where: { username } }); if (existingUser) return NextResponse.json({ error: "Username is already taken" }, { status: 400 }); diff --git a/src/app/api/mii/[id]/edit/route.ts b/src/app/api/mii/[id]/edit/route.ts index 9720525..d7a975e 100644 --- a/src/app/api/mii/[id]/edit/route.ts +++ b/src/app/api/mii/[id]/edit/route.ts @@ -6,6 +6,8 @@ import fs from "fs/promises"; import path from "path"; import sharp from "sharp"; +import { profanity } from "@2toad/profanity"; + import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { idSchema, nameSchema, tagsSchema } from "@/lib/schemas"; @@ -81,8 +83,8 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id // Edit Mii in database const updateData: Partial = {}; - if (name !== undefined) updateData.name = name; - if (tags !== undefined) updateData.tags = tags; + if (name !== undefined) updateData.name = profanity.censor(name); // Censor potential inappropriate words + if (tags !== undefined) updateData.tags = tags.map((t) => profanity.censor(t)); // Same here if (images.length > 0) updateData.imageCount = images.length; if (Object.keys(updateData).length == 0) return NextResponse.json({ error: "Nothing was changed" }, { status: 400 }); diff --git a/src/app/api/submit/route.ts b/src/app/api/submit/route.ts index 31e257f..86e65a9 100644 --- a/src/app/api/submit/route.ts +++ b/src/app/api/submit/route.ts @@ -6,6 +6,7 @@ import path from "path"; import sharp from "sharp"; import qrcode from "qrcode-generator"; +import { profanity } from "@2toad/profanity"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; @@ -54,7 +55,11 @@ export async function POST(request: Request) { }); if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 }); - const { name, tags, qrBytesRaw, image1, image2, image3 } = parsed.data; + const { name: uncensoredName, tags: uncensoredTags, qrBytesRaw, image1, image2, image3 } = parsed.data; + + // Censor potential inappropriate words + const name = profanity.censor(uncensoredName); + const tags = uncensoredTags.map((t) => profanity.censor(t)); // Validate image files const images: File[] = []; diff --git a/src/app/terms-of-service/page.tsx b/src/app/terms-of-service/page.tsx index fa6dd4c..f21dc43 100644 --- a/src/app/terms-of-service/page.tsx +++ b/src/app/terms-of-service/page.tsx @@ -3,7 +3,7 @@ export default function PrivacyPage() {

Terms of Service

- Effective Date: April 06, 2025 + Effective Date: April 23, 2025


@@ -34,6 +34,7 @@ export default function PrivacyPage() {
  • No impersonation of others.
  • No malware, malicious links, or phishing content.
  • No harassment, hate speech, threats, or bullying towards others.
  • +
  • Avoid using inappropriate language. Profanity may be automatically censored.
  • No use of automated scripts, bots, or scrapers to access or interact with the site.
  • diff --git a/src/lib/qr-codes.ts b/src/lib/qr-codes.ts index cc376e7..18cb0a3 100644 --- a/src/lib/qr-codes.ts +++ b/src/lib/qr-codes.ts @@ -1,4 +1,6 @@ +import { profanity } from "@2toad/profanity"; 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"; @@ -39,6 +41,11 @@ export function convertQrCode(bytes: Uint8Array): { mii: Mii; tomodachiLifeMii: mii.facialHairColor = tomodachiLifeMii.studioHairColor; } + // Censor potential inappropriate words + tomodachiLifeMii.firstName = profanity.censor(tomodachiLifeMii.firstName); + tomodachiLifeMii.lastName = profanity.censor(tomodachiLifeMii.lastName); + tomodachiLifeMii.islandName = profanity.censor(tomodachiLifeMii.islandName); + return { mii, tomodachiLifeMii }; } catch (error) { console.error(error); diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 710843c..b27d887 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +// profanity censoring bypasses the regex in some of these but I think it's funny + export const querySchema = z .string() .trim()